Oregami
Repositories/oxedyne/ore

oxedyne/ore/cli/tests/support/mod.rs

21.6 KiB, 14 runs

created by r2848102244:39, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1#![allow(dead_code)]
2
3//! What the integration tests share: a scratch directory that cleans itself, a
4//! way of running the compiled binary, a way of running git under an
5//! environment that no developer's configuration can reach into, and the small
6//! amount of forgery the provenance tests need.
7
8use oxedyne_fe2o3_core::prelude::*;
9use oxedyne_fe2o3_hash::hash::HashScheme;
10use oxedyne_fe2o3_ore::envelope::Envelope;
11use oxedyne_fe2o3_ore::segment::{
12 self,
13 Entry,
14 Head,
15};
16
17use std::collections::BTreeMap;
18use std::env;
19use std::fs;
20use std::path::{
21 Path,
22 PathBuf,
23};
24use std::process::{
25 Command,
26 Output,
27};
28use std::time::{
29 SystemTime,
30 UNIX_EPOCH,
31};
32
33
34/// A fixed timestamp, so that a git repository built by a test is the same one
35/// every run.
36pub const WHEN: &str = "1700000000 +1000";
37
38/// The binary file the oracle test commits: NUL bytes, a bare line feed and a
39/// byte no text encoding leaves alone.
40pub const BINARY: &[u8] = b"\x00\x01\n\x02binary\x00\xff\n";
41
42
43/// A directory that removes itself however the test ends.
44pub struct Scratch {
45 /// Where it is.
46 pub path: PathBuf,
47}
48
49impl Scratch {
50 /// Makes a directory of its own for this test.
51 pub fn new(what: &str)
52 -> Outcome<Self>
53 {
54 let stamp = res!(SystemTime::now().duration_since(UNIX_EPOCH));
55 let path = env::temp_dir().join(fmt!(
56 "ore_cli_{}_{}_{}", what, std::process::id(), stamp.as_nanos(),
57 ));
58 res!(fs::create_dir_all(&path));
59 Ok(Self { path })
60 }
61
62 /// Returns a subdirectory, made if it is not there.
63 pub fn sub(&self, name: &str)
64 -> Outcome<PathBuf>
65 {
66 let path = self.path.join(name);
67 res!(fs::create_dir_all(&path));
68 Ok(path)
69 }
70}
71
72impl Drop for Scratch {
73 fn drop(&mut self) {
74 let _ = fs::remove_dir_all(&self.path);
75 }
76}
77
78
79/// What running the binary produced.
80pub struct Ran {
81 /// Whether it exited successfully.
82 pub ok: bool,
83 /// Its standard output.
84 pub out: String,
85 /// Its standard error.
86 pub err: String,
87}
88
89impl Ran {
90 /// Returns the standard output, failing if the command did not succeed.
91 pub fn good(&self, what: &str)
92 -> Outcome<&str>
93 {
94 if !self.ok {
95 return Err(err!(
96 "`ore {}` failed: {}{}", what, self.out, self.err;
97 Test, IO));
98 }
99 Ok(&self.out)
100 }
101}
102
103/// Runs the compiled binary in a directory.
104pub fn ore(dir: &Path, args: &[&str])
105 -> Outcome<Ran>
106{
107 let out: Output = match Command::new(env!("CARGO_BIN_EXE_ore"))
108 .current_dir(dir)
109 .args(args)
110 .output()
111 {
112 Ok(o) => o,
113 Err(e) => return Err(err!(e,
114 "The ore binary could not be run in {:?}.", dir;
115 Test, IO)),
116 };
117 Ok(Ran {
118 ok: out.status.success(),
119 out: fmt!("{}", String::from_utf8_lossy(&out.stdout)),
120 err: fmt!("{}", String::from_utf8_lossy(&out.stderr)),
121 })
122}
123
124/// Runs git in a directory, with the caller's configuration, hooks and signing
125/// keys all fenced off.
126pub fn git(dir: &Path, args: &[&str])
127 -> Outcome<Vec<u8>>
128{
129 git_as(dir, "Ada Lovelace", "ada@example.org", args)
130}
131
132/// Runs git under a named identity.
133pub fn git_as(dir: &Path, who: &str, email: &str, args: &[&str])
134 -> Outcome<Vec<u8>>
135{
136 git_when(dir, who, email, WHEN, args)
137}
138
139/// Runs git under a named identity at a named moment, spelled as an identity
140/// line spells one: seconds since the epoch, a space, and a zone offset.
141pub fn git_when(dir: &Path, who: &str, email: &str, when: &str, args: &[&str])
142 -> Outcome<Vec<u8>>
143{
144 let mut cmd = Command::new("git");
145 cmd.current_dir(dir)
146 .args(args)
147 .env("GIT_CONFIG_GLOBAL", "/dev/null")
148 .env("GIT_CONFIG_NOSYSTEM", "1")
149 .env("GIT_AUTHOR_NAME", who)
150 .env("GIT_AUTHOR_EMAIL", email)
151 .env("GIT_COMMITTER_NAME", who)
152 .env("GIT_COMMITTER_EMAIL", email)
153 .env("GIT_AUTHOR_DATE", when)
154 .env("GIT_COMMITTER_DATE", when);
155 let out = match cmd.output() {
156 Ok(o) => o,
157 Err(e) => return Err(err!(e,
158 "`git {}` could not be run in {:?}.", args.join(" "), dir;
159 Test, IO)),
160 };
161 if !out.status.success() {
162 return Err(err!(
163 "`git {}` failed in {:?}: {}", args.join(" "), dir,
164 String::from_utf8_lossy(&out.stderr);
165 Test, IO));
166 }
167 Ok(out.stdout)
168}
169
170/// Runs git where failing is one of the expected outcomes, as it is for a merge
171/// that conflicts.
172pub fn git_try(dir: &Path, args: &[&str])
173 -> Outcome<Vec<u8>>
174{
175 let mut cmd = Command::new("git");
176 cmd.current_dir(dir)
177 .args(args)
178 .env("GIT_CONFIG_GLOBAL", "/dev/null")
179 .env("GIT_CONFIG_NOSYSTEM", "1")
180 .env("GIT_AUTHOR_NAME", "Ada Lovelace")
181 .env("GIT_AUTHOR_EMAIL", "ada@example.org")
182 .env("GIT_COMMITTER_NAME", "Ada Lovelace")
183 .env("GIT_COMMITTER_EMAIL", "ada@example.org")
184 .env("GIT_AUTHOR_DATE", WHEN)
185 .env("GIT_COMMITTER_DATE", WHEN);
186 let out = match cmd.output() {
187 Ok(o) => o,
188 Err(e) => return Err(err!(e,
189 "`git {}` could not be run in {:?}.", args.join(" "), dir;
190 Test, IO)),
191 };
192 Ok(out.stdout)
193}
194
195/// Prepares a directory as a git repository that no hook and no global setting
196/// can interfere with.
197pub fn git_init(dir: &Path)
198 -> Outcome<()>
199{
200 res!(git(dir, &["-c", "init.defaultBranch=main", "init", "--quiet", "."]));
201 // A hooks path that does not exist keeps this machine's global pre-commit
202 // hook, and any other, out of the way.
203 res!(git(dir, &["config", "core.hooksPath", "nohooks"]));
204 res!(git(dir, &["config", "commit.gpgsign", "false"]));
205 res!(git(dir, &["config", "user.name", "Ada Lovelace"]));
206 res!(git(dir, &["config", "user.email", "ada@example.org"]));
207 Ok(())
208}
209
210/// Writes a file, making the directories above it.
211pub fn write(root: &Path, rel: &str, bytes: &[u8])
212 -> Outcome<()>
213{
214 let path = root.join(rel);
215 if let Some(parent) = path.parent() {
216 res!(fs::create_dir_all(parent));
217 }
218 res!(fs::write(&path, bytes));
219 Ok(())
220}
221
222/// Reads every regular file of a tree, skipping the directories the two version
223/// control systems keep their own business in.
224pub fn tree_of(root: &Path)
225 -> Outcome<BTreeMap<String, Vec<u8>>>
226{
227 let mut out = BTreeMap::new();
228 res!(collect(root, "", &mut out));
229 Ok(out)
230}
231
232/// Reads one directory into the map, recursing.
233fn collect(dir: &Path, prefix: &str, out: &mut BTreeMap<String, Vec<u8>>)
234 -> Outcome<()>
235{
236 for entry in res!(fs::read_dir(dir)) {
237 let entry = res!(entry);
238 let name = entry.file_name().to_string_lossy().into_owned();
239 if name == ".git" || name == ".ore" {
240 continue;
241 }
242 let rel = if prefix.is_empty() {
243 name.clone()
244 } else {
245 fmt!("{}/{}", prefix, name)
246 };
247 let kind = res!(entry.file_type());
248 if kind.is_dir() {
249 res!(collect(&entry.path(), &rel, out));
250 } else if kind.is_file() {
251 out.insert(rel, res!(fs::read(entry.path())));
252 }
253 }
254 Ok(())
255}
256
257/// Reads whether every regular file of a tree is executable, by path.
258///
259/// The owner's execute bit is what git records and what a checkout restores, so
260/// it is what two trees are compared on.
261pub fn exec_bits_of(root: &Path)
262 -> Outcome<BTreeMap<String, bool>>
263{
264 let mut out = BTreeMap::new();
265 for (path, _) in res!(tree_of(root)) {
266 let meta = res!(fs::metadata(root.join(&path)));
267 out.insert(path, is_exec(&meta));
268 }
269 Ok(out)
270}
271
272/// Reads whether every regular file of a tree is executable, keyed by the bytes
273/// of its path.
274///
275/// A path is not required to be UTF-8, and [`exec_bits_of`] keys by a lossy
276/// string, so it cannot find again a file whose name the lossiness changed. This
277/// one can.
278pub fn exec_bits_raw(root: &Path)
279 -> Outcome<BTreeMap<Vec<u8>, bool>>
280{
281 use std::os::unix::ffi::OsStrExt;
282 let mut out = BTreeMap::new();
283 for (path, _) in res!(tree_raw(root)) {
284 let mut at = root.to_path_buf();
285 for part in path.split(|b| *b == b'/') {
286 at.push(std::ffi::OsStr::from_bytes(part));
287 }
288 let meta = res!(fs::metadata(&at));
289 out.insert(path, is_exec(&meta));
290 }
291 Ok(out)
292}
293
294/// Returns how many operations `ore log` said the history holds.
295///
296/// Here rather than in one test file because more than one of them wants it, and
297/// because the tests that want it are the ones asking whether the log grew --
298/// which is a comparison between two readings and not a number to write down.
299/// Every command that appends anything now ends by marking the point it reached,
300/// so a total written into a test as a literal is a total that moves whenever the
301/// commands a test runs change.
302pub fn operations(text: &str)
303 -> Outcome<usize>
304{
305 for line in text.lines() {
306 if let Some(n) = line.split_whitespace().next() {
307 if line.contains("operation") && line.contains(" in ") {
308 if let Ok(n) = n.parse::<usize>() {
309 return Ok(n);
310 }
311 }
312 }
313 }
314 Err(err!("The log does not say how many operations there are: {}", text; Test, Missing))
315}
316
317/// Returns the number a phrase in the tool's output is followed by.
318///
319/// For the counts a test wants to compare with something rather than write down.
320/// A rehearsal saying how much would arrive is worth checking against what the
321/// sync afterwards actually brings; it is not worth pinning to a literal, since
322/// what the other end has to hand over now depends on how many commands were run
323/// there, each of which ends by marking the point it reached.
324pub fn counted(text: &str, after: &str)
325 -> Outcome<usize>
326{
327 let at = match text.find(after) {
328 Some(i) => i + after.len(),
329 None => return Err(err!(
330 "The output does not say {:?}: {}", after, text; Test, Missing)),
331 };
332 let digits: String = text[at..].chars().take_while(|c| c.is_ascii_digit()).collect();
333 match digits.parse::<usize>() {
334 Ok(n) => Ok(n),
335 Err(e) => Err(err!(e,
336 "What follows {:?} is not a number: {}", after, &text[at..]; Test, Mismatch)),
337 }
338}
339
340/// Returns how many marks `ore log` named, and how many of those this tool wrote
341/// itself.
342///
343/// The two are counted apart because a test that says "every commit of every
344/// branch is a mark" is making a claim about the marks a person or an import
345/// named, and the automatic ones are not those. See [`crate::support`] callers.
346pub fn marks(text: &str) -> (usize, usize) {
347 let mut named = 0usize;
348 let mut automatic = 0usize;
349 for line in text.lines() {
350 if !line.starts_with("mark ") {
351 continue;
352 }
353 match line.starts_with("mark \"@") {
354 true => automatic += 1,
355 false => named += 1,
356 }
357 }
358 (named, automatic)
359}
360
361/// Reports whether a file's owner may run it.
362pub fn is_exec(meta: &fs::Metadata) -> bool {
363 use std::os::unix::fs::PermissionsExt;
364 meta.permissions().mode() & 0o100 != 0
365}
366
367/// Makes a file executable, as `chmod +x` does.
368pub fn make_exec(path: &Path)
369 -> Outcome<()>
370{
371 use std::os::unix::fs::PermissionsExt;
372 let meta = res!(fs::metadata(path));
373 let mode = meta.permissions().mode();
374 res!(fs::set_permissions(path, fs::Permissions::from_mode(mode | 0o111)));
375 Ok(())
376}
377
378/// Reads every regular file of a tree, keyed by the bytes of its path.
379///
380/// A path is not required to be UTF-8, and two names that differ only outside it
381/// are two names; keying by a lossy string would quietly merge them.
382pub fn tree_raw(root: &Path)
383 -> Outcome<BTreeMap<Vec<u8>, Vec<u8>>>
384{
385 let mut out = BTreeMap::new();
386 res!(collect_raw(root, &[], &mut out));
387 Ok(out)
388}
389
390/// Reads one directory into the map, recursing, with names as bytes.
391fn collect_raw(dir: &Path, prefix: &[u8], out: &mut BTreeMap<Vec<u8>, Vec<u8>>)
392 -> Outcome<()>
393{
394 use std::os::unix::ffi::OsStrExt;
395 for entry in res!(fs::read_dir(dir)) {
396 let entry = res!(entry);
397 let name = entry.file_name();
398 let name = name.as_bytes();
399 if name == b".git" || name == b".ore" {
400 continue;
401 }
402 let mut rel = prefix.to_vec();
403 if !rel.is_empty() {
404 rel.push(b'/');
405 }
406 rel.extend_from_slice(name);
407 let kind = res!(entry.file_type());
408 if kind.is_dir() {
409 res!(collect_raw(&entry.path(), &rel, out));
410 } else if kind.is_file() {
411 out.insert(rel, res!(fs::read(entry.path())));
412 }
413 }
414 Ok(())
415}
416
417/// Writes a file whose path is bytes rather than a string, making the
418/// directories above it.
419pub fn write_raw(root: &Path, rel: &[u8], bytes: &[u8])
420 -> Outcome<()>
421{
422 use std::os::unix::ffi::OsStrExt;
423 let path = root.join(std::ffi::OsStr::from_bytes(rel));
424 if let Some(parent) = path.parent() {
425 res!(fs::create_dir_all(parent));
426 }
427 res!(fs::write(&path, bytes));
428 Ok(())
429}
430
431
432/// One run of a `who` listing: where it sits, what is known about who wrote it,
433/// and which operation did.
434pub struct Run {
435 /// First byte of the file the run covers.
436 pub from: usize,
437 /// The provenance mark, one character.
438 pub mark: char,
439 /// The operation that wrote it, as `who` printed it.
440 pub op: String,
441 /// The content, as `who` printed it.
442 pub text: String,
443}
444
445/// Reads the runs out of a `who` listing.
446pub fn runs_in(text: &str) -> Vec<Run> {
447 let mut out = Vec::new();
448 for line in text.lines() {
449 let trimmed = line.trim_start();
450 if !trimmed.starts_with(|c: char| c.is_ascii_digit()) {
451 continue;
452 }
453 let mut fields = trimmed.split_whitespace();
454 // A run line opens with a byte range. Other lines of the listing open
455 // with a number too -- "1 file left alone" is one -- so the range is what
456 // is looked for rather than the digit.
457 let span = match fields.next() {
458 Some(s) if s.contains("..") => s,
459 _ => continue,
460 };
461 let from = match span.split("..").next().and_then(|n| n.parse::<usize>().ok()) {
462 Some(n) => n,
463 None => continue,
464 };
465 let mark = match fields.next().and_then(|f| f.chars().next()) {
466 Some(c) => c,
467 None => continue,
468 };
469 let op = match fields.next() {
470 Some(s) => fmt!("{}", s),
471 None => continue,
472 };
473 out.push(Run { from, mark, op, text: fields.collect::<Vec<&str>>().join(" ") });
474 }
475 out
476}
477
478
479/// The salt the tool writes every segment digest under, which a test that
480/// rewrites a segment has to write it under too.
481pub const SALT: [u8; 8] = *b"oreseg01";
482
483/// The path of the first segment of a repository.
484pub fn segment_path(root: &Path) -> PathBuf {
485 root.join(".ore").join("log").join("000000.seg")
486}
487
488/// Returns the hasher the tool computes segment digests with.
489pub fn hasher() -> HashScheme {
490 HashScheme::new_sha256()
491}
492
493/// Reads a segment into its header and its entries.
494pub fn read_segment(path: &Path)
495 -> Outcome<(Head, Vec<Entry>)>
496{
497 let bytes = res!(fs::read(path));
498 let (head, entries) = res!(segment::decode(&bytes, hasher(), SALT));
499 Ok((head, entries))
500}
501
502/// Writes a segment back out, digests and all.
503pub fn write_segment(path: &Path, head: &Head, entries: &[Entry])
504 -> Outcome<()>
505{
506 let mut writer = segment::Writer::new(head, hasher(), SALT);
507 res!(writer.extend(entries));
508 res!(fs::write(path, writer.finish()));
509 Ok(())
510}
511
512/// Alters the operation a sealed entry carries and rewrites the segment as a
513/// well formed one, returning the operation that was altered.
514///
515/// This is the forgery the signature exists to catch, and it is deliberately
516/// the competent version of it: flipping a byte and leaving the digest alone
517/// would be caught by the digest, which is a check against damage rather than
518/// against a forger. Somebody who can rewrite a repository's files can rewrite
519/// the digests beside them, so the test does exactly that -- everything about
520/// the segment is consistent afterwards except the one thing the forger cannot
521/// produce, which is a signature over the bytes they substituted.
522///
523/// `which` picks the sealed entry to alter, counting from the last one
524/// backwards, so a test can name the most recent operation without knowing how
525/// many there are.
526pub fn forge_payload(path: &Path, from_end: usize)
527 -> Outcome<String>
528{
529 let (head, entries) = res!(read_segment(path));
530 let mut sealed: Vec<usize> = Vec::new();
531 for (i, entry) in entries.iter().enumerate() {
532 if let Entry::Sealed(_) = entry {
533 sealed.push(i);
534 }
535 }
536 if sealed.len() <= from_end {
537 return Err(err!(
538 "The segment {:?} holds {} sealed entries, and the test asked for the one \
539 {} from the end.", path, sealed.len(), from_end; Test, Range));
540 }
541 let at = sealed[sealed.len() - 1 - from_end];
542 let mut out = entries.clone();
543 let named = match &entries[at] {
544 Entry::Sealed(env) => {
545 let id = fmt!("{}", res!(env.peek_record()).id());
546 // The last byte of the payload belongs to the operation rather than to
547 // the frame around it, so what is altered is the edit itself.
548 let mut payload = env.payload().to_vec();
549 let last = payload.len() - 1;
550 payload[last] ^= 0x01;
551 out[at] = Entry::Sealed(Envelope::new(
552 payload,
553 env.signer().to_vec(),
554 env.signature().to_vec(),
555 ));
556 id
557 },
558 other => return Err(err!(
559 "The entry chosen for forgery is a {}.", other.name(); Test, Mismatch)),
560 };
561 res!(write_segment(path, &head, &out));
562 Ok(named)
563}
564
565/// Sets `require_signed` in a repository's configuration.
566///
567/// The tool has no verb for it, on purpose: ten verbs is the budget and a
568/// setting that is read once is a line of configuration rather than a command.
569pub fn set_require_signed(root: &Path, to: bool)
570 -> Outcome<()>
571{
572 let path = root.join(".ore").join("config");
573 let text = res!(fs::read_to_string(&path));
574 let was = fmt!("\"require_signed\": ({})", !to);
575 let now = fmt!("\"require_signed\": ({})", to);
576 if !text.contains(&was) {
577 return Err(err!(
578 "The configuration {:?} does not hold {:?}: {}", path, was, text;
579 Test, Missing));
580 }
581 res!(fs::write(&path, text.replace(&was, &now)));
582 Ok(())
583}
584
585/// Removes one public key from a repository's configuration, leaving the rest.
586///
587/// It stands for a history that arrived without its keys: the operations verify
588/// as signed and this repository cannot say by whom.
589pub fn forget_key(root: &Path, key: &str)
590 -> Outcome<()>
591{
592 let path = root.join(".ore").join("config");
593 let text = res!(fs::read_to_string(&path));
594 let mut out = String::new();
595 let mut held: Vec<String> = Vec::new();
596 let mut depth = 0usize;
597 for line in text.lines() {
598 let trimmed = line.trim();
599 if trimmed == "[" {
600 depth += 1;
601 }
602 if depth > 0 {
603 held.push(fmt!("{}", line));
604 if trimmed == "]," || trimmed == "]" {
605 depth -= 1;
606 if depth == 0 {
607 let block = held.join("\n");
608 if !block.contains(key) {
609 out.push_str(&block);
610 out.push('\n');
611 }
612 held.clear();
613 }
614 }
615 continue;
616 }
617 out.push_str(line);
618 out.push('\n');
619 }
620 if out.contains(key) {
621 return Err(err!(
622 "The key {:?} is still in {:?} after forgetting it: {}", key, path, out;
623 Test, Invalid));
624 }
625 res!(fs::write(&path, out));
626 Ok(())
627}
628
629/// Returns the public key `ore init` or `ore key` printed.
630pub fn key_in(text: &str)
631 -> Outcome<String>
632{
633 for line in text.lines() {
634 let trimmed = line.trim();
635 if let Some(rest) = trimmed.strip_prefix("signing key Ed25519 ") {
636 return Ok(fmt!("{}", rest.trim()));
637 }
638 if let Some(rest) = trimmed.strip_prefix("Ed25519 ") {
639 return Ok(fmt!("{}", rest.trim()));
640 }
641 }
642 Err(err!("No public key is named in: {}", text; Test, Missing))
643}
644
645
646/// Builds the git repository both oracles are judged against.
647///
648/// It holds everything a translation in either direction has to survive: an edit,
649/// a rename, a binary file, a script that is executable from its first commit,
650/// a bit given and then taken away, a branch, a merge whose two sides edited the
651/// same line and which a person resolved to a third thing, and a deletion.
652/// Returns how many commits it has.
653pub fn git_fixture(src: &Path)
654 -> Outcome<usize>
655{
656 res!(git_init(src));
657
658 res!(write(src, "f.txt", b"alpha\nbeta\ngamma\n"));
659 res!(write(src, "k.txt", b"keep\n"));
660 res!(write(src, "bin.dat", BINARY));
661 res!(write(src, "sub/deep.txt", b"deep\n"));
662 // A script that is executable from its first commit, and one that is not yet.
663 res!(write(src, "build.sh", b"#!/bin/sh\necho building\n"));
664 res!(make_exec(&src.join("build.sh")));
665 res!(write(src, "later.sh", b"#!/bin/sh\necho later\n"));
666 res!(git(src, &["add", "."]));
667 res!(git(src, &["commit", "--quiet", "--no-verify", "-m", "base"]));
668
669 // The trunk edits a line and renames a file.
670 res!(write(src, "f.txt", b"alpha\nBETA-main\ngamma\n"));
671 res!(git(src, &["mv", "k.txt", "kept.txt"]));
672 // A script gains the bit in a commit of its own, and an executable one is
673 // renamed and edited, which is what the mode has to survive.
674 res!(git(src, &["update-index", "--chmod=+x", "later.sh"]));
675 res!(fs::create_dir_all(src.join("tools")));
676 res!(git(src, &["mv", "build.sh", "tools/build.sh"]));
677 res!(write(src, "tools/build.sh", b"#!/bin/sh\nset -e\necho building\n"));
678 res!(git(src, &["add", "."]));
679 res!(git(src, &["commit", "--quiet", "--no-verify", "-m", "trunk edit and rename"]));
680
681 // A branch off the base edits the same line, and adds a file of its own.
682 res!(git(src, &["checkout", "--quiet", "-b", "side", "HEAD~1"]));
683 res!(write(src, "f.txt", b"alpha\nBETA-side\ngamma\n"));
684 res!(write(src, "s.txt", b"only on the side\n"));
685 res!(git(src, &["add", "."]));
686 res!(git(src, &["commit", "--quiet", "--no-verify", "-m", "side edit"]));
687
688 // The merge conflicts, and a person resolves it to a third thing entirely.
689 res!(git(src, &["checkout", "--quiet", "main"]));
690 res!(git_try(src, &["merge", "--no-ff", "--no-verify", "side", "-m", "merge side"]));
691 res!(write(src, "f.txt", b"alpha\nBETA-resolved\ngamma\n"));
692 res!(git(src, &["add", "-A"]));
693 res!(git(src, &["commit", "--quiet", "--no-verify", "-m", "merge side"]));
694
695 res!(git(src, &["rm", "-q", "sub/deep.txt"]));
696 // And a bit taken away again, so the import is not merely additive.
697 res!(git(src, &["update-index", "--chmod=-x", "later.sh"]));
698 res!(git(src, &["commit", "--quiet", "--no-verify", "-m", "drop the deep file"]));
699
700 let out = res!(git(src, &["rev-list", "--all", "--count"]));
701 let text = String::from_utf8_lossy(&out);
702 match text.trim().parse::<usize>() {
703 Ok(n) => Ok(n),
704 Err(e) => Err(err!(e, "git counted its commits as {:?}.", text.trim(); Test)),
705 }
706}
707
708/// Copies a directory tree, for a test that wants two of the same repository.
709pub fn copy_dir(from: &Path, to: &Path) -> Outcome<()> {
710 let out = match std::process::Command::new("cp").arg("-a")
711 .arg(fmt!("{}/.", from.display())).arg(to).output()
712 {
713 Ok(o) => o,
714 Err(e) => return Err(err!(e, "{:?} could not be copied.", from; Test, IO)),
715 };
716 if !out.status.success() {
717 return Err(err!("Copying {:?} failed: {}", from,
718 String::from_utf8_lossy(&out.stderr).trim(); Test, IO));
719 }
720 Ok(())
721}