Oregami
Repositories/oxedyne/ore

oxedyne/ore/cli/tests/oracle.rs

14.1 KiB, 24 runs

created by r2848102244:33, 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//! The test that matters: git is the oracle.
2//!
3//! Everything else here proves the tool self-consistent, which proves nothing
4//! about whether an imported repository is the repository that was imported.
5//! This builds a real git repository with the git binary -- several commits, a
6//! rename, a binary file, an executable script, a merge whose two sides edited
7//! the same line, and a deletion -- imports it, and then compares every file of
8//! the working copy, byte for byte and mode for mode, against what
9//! `git checkout` of the same repository produces.
10//!
11//! It is not ignored. Git is on this machine, and a test of the importer that
12//! does not run is a test of nothing.
13
14mod support;
15
16use support::{
17 exec_bits_of,
18 git,
19 git_fixture,
20 git_init,
21 make_exec,
22 ore,
23 tree_of,
24 tree_raw,
25 write,
26 write_raw,
27 Scratch,
28 BINARY,
29};
30
31use oxedyne_fe2o3_core::prelude::*;
32
33use std::fs;
34use std::os::unix::fs::PermissionsExt;
35
36
37/// An imported repository is the repository that was imported.
38#[test]
39fn import_matches_git_byte_for_byte() -> Outcome<()> {
40 let scratch = res!(Scratch::new("oracle"));
41 let src = res!(scratch.sub("src"));
42 let into = res!(scratch.sub("ore"));
43 let check = scratch.path.join("check");
44
45 let commits = res!(git_fixture(&src));
46
47 res!(res!(ore(&into, &["init"])).good("init"));
48 let out = res!(ore(&into, &["import", &fmt!("{}", src.display())]));
49 let text = fmt!("{}", res!(out.good("import")));
50 assert!(text.contains(&fmt!("imported {} commits", commits)),
51 "every commit is imported: {}", text);
52
53 // git checks itself out into a directory of its own, and that is the answer
54 // the import has to match.
55 res!(git(&scratch.path, &["clone", "--quiet",
56 &fmt!("{}", src.display()), &fmt!("{}", check.display())]));
57 res!(git(&check, &["checkout", "--quiet", "main"]));
58
59 let theirs = res!(tree_of(&check));
60 let ours = res!(tree_of(&into));
61 assert!(!theirs.is_empty(), "git checked something out");
62 let mine: Vec<&String> = ours.keys().collect();
63 let yours: Vec<&String> = theirs.keys().collect();
64 assert_eq!(mine, yours, "the same files, and no others");
65 for (path, want) in &theirs {
66 let got = match ours.get(path) {
67 Some(g) => g,
68 None => return Err(err!("The import has no {:?}.", path; Test, Missing)),
69 };
70 assert_eq!(got.len(), want.len(), "{:?} is the same length", path);
71 assert!(got == want, "{:?} is byte for byte what git holds", path);
72 }
73
74 // The same tree is not the same working copy if a script cannot be run, so
75 // the modes are compared beside the bytes and against the same oracle.
76 let their_modes = res!(exec_bits_of(&check));
77 let our_modes = res!(exec_bits_of(&into));
78 assert_eq!(our_modes, their_modes, "every file is executable exactly where \
79 git's own checkout says it is");
80 // And named outright, so the assertion above cannot pass by both sides being
81 // wrong in the same way.
82 assert_eq!(their_modes.get("tools/build.sh"), Some(&true),
83 "git checks the script out executable");
84 assert_eq!(our_modes.get("tools/build.sh"), Some(&true),
85 "and so does the import, through a rename and an edit");
86 assert_eq!(our_modes.get("later.sh"), Some(&false),
87 "a bit given and then taken away is taken away");
88 assert_eq!(our_modes.get("f.txt"), Some(&false),
89 "an ordinary file stays ordinary");
90
91 // One mark per commit, and nothing left to capture: the working copy is
92 // exactly what the operations say it is.
93 let out = res!(ore(&into, &["log"]));
94 let text = fmt!("{}", res!(out.good("log")));
95 let marks = text.lines().filter(|l| l.starts_with("mark ")).count();
96 assert_eq!(marks, commits, "one mark per commit: {}", text);
97 assert!(text.contains("captured nothing"),
98 "the import left the working copy where the history says: {}", text);
99 for name in ["base", "trunk edit and rename", "side edit", "merge side",
100 "drop the deep file"]
101 {
102 assert!(text.contains(&fmt!("mark {:?}", name)),
103 "the mark {:?} is named after the commit: {}", name, text);
104 }
105 Ok(())
106}
107
108/// The two sides of the merge were concurrent, and the tool says so.
109#[test]
110fn a_merged_conflict_is_flagged_and_attributed() -> Outcome<()> {
111 let scratch = res!(Scratch::new("oracle_flags"));
112 let src = res!(scratch.sub("src"));
113 let into = res!(scratch.sub("ore"));
114 res!(git_fixture(&src));
115 res!(res!(ore(&into, &["init"])).good("init"));
116 res!(res!(ore(&into, &["import", &fmt!("{}", src.display())])).good("import"));
117
118 let out = res!(ore(&into, &["flags"]));
119 let text = fmt!("{}", res!(out.good("flags")));
120 assert!(text.contains("overlap"),
121 "the two sides of the merge named the same bytes: {}", text);
122 assert!(text.contains("f.txt"), "and it says which file: {}", text);
123
124 // The resolution is in the file, whoever the bytes came from.
125 let out = res!(ore(&into, &["who", "f.txt"]));
126 let text = fmt!("{}", res!(out.good("who f.txt")));
127 assert!(text.contains("resolved"),
128 "the resolved bytes are shown against the operation that wrote them: {}", text);
129 Ok(())
130}
131
132/// An import will not be laid over a history that is already there.
133#[test]
134fn import_refuses_a_repository_that_holds_history() -> Outcome<()> {
135 let scratch = res!(Scratch::new("oracle_twice"));
136 let src = res!(scratch.sub("src"));
137 let into = res!(scratch.sub("ore"));
138 res!(git_fixture(&src));
139 res!(res!(ore(&into, &["init"])).good("init"));
140 res!(write(&into, "mine.txt", b"already here\n"));
141 res!(res!(ore(&into, &["mark", "mine"])).good("mark mine"));
142 let out = res!(ore(&into, &["import", &fmt!("{}", src.display())]));
143 assert!(!out.ok, "the import is refused");
144 assert!(out.err.contains("already holds"), "and says why: {}", out.err);
145 Ok(())
146}
147
148/// A path git permits and UTF-8 does not is imported like any other, and the
149/// working copy holds the same bytes git checks out.
150///
151/// Git stores a path as bytes and this vocabulary does too, so there is nothing
152/// in the middle to lose them. The stream quotes such a path with octal escapes;
153/// the parser unquotes it; nothing between there and the filesystem turns it into
154/// a string.
155#[test]
156fn a_path_that_is_not_utf8_imports() -> Outcome<()> {
157 let scratch = res!(Scratch::new("oracle_bytes"));
158 let src = res!(scratch.sub("src"));
159 let into = res!(scratch.sub("ore"));
160 let check = scratch.path.join("check");
161
162 // Latin-1 "café.txt", a lone 0xff, and a directory component that is not
163 // UTF-8 either: three paths no string type will hold.
164 let cafe: &[u8] = b"caf\xe9.txt";
165 let lone: &[u8] = b"\xff\xfe.bin";
166 let deep: &[u8] = b"d\x80ir/n\x81ame.txt";
167
168 res!(git_init(&src));
169 res!(write(&src, "plain.txt", b"ordinary\n"));
170 res!(write_raw(&src, cafe, b"latin one\n"));
171 res!(write_raw(&src, lone, BINARY));
172 res!(write_raw(&src, deep, b"deep and unreadable\n"));
173 res!(git(&src, &["add", "-A"]));
174 res!(git(&src, &["commit", "--quiet", "--no-verify", "-m", "paths that are bytes"]));
175
176 // And edited afterwards, so the importer splices against a path it can only
177 // have matched by bytes.
178 res!(write_raw(&src, cafe, b"latin one\nand two\n"));
179 res!(git(&src, &["add", "-A"]));
180 res!(git(&src, &["commit", "--quiet", "--no-verify", "-m", "edit the odd one"]));
181
182 res!(res!(ore(&into, &["init"])).good("init"));
183 let out = res!(ore(&into, &["import", &fmt!("{}", src.display())]));
184 res!(out.good("import"));
185
186 res!(git(&scratch.path, &["clone", "--quiet",
187 &fmt!("{}", src.display()), &fmt!("{}", check.display())]));
188 res!(git(&check, &["checkout", "--quiet", "main"]));
189
190 let theirs = res!(tree_raw(&check));
191 let ours = res!(tree_raw(&into));
192 assert!(theirs.contains_key(cafe), "git holds the latin-1 path");
193 let mine: Vec<&Vec<u8>> = ours.keys().collect();
194 let yours: Vec<&Vec<u8>> = theirs.keys().collect();
195 assert_eq!(mine, yours, "the same paths, byte for byte, and no others");
196 for (path, want) in &theirs {
197 let got = match ours.get(path) {
198 Some(g) => g,
199 None => return Err(err!(
200 "The import has no {:?}.", String::from_utf8_lossy(path); Test, Missing)),
201 };
202 assert!(got == want, "{:?} is byte for byte what git holds",
203 String::from_utf8_lossy(path));
204 }
205 assert_eq!(ours.get(cafe).map(|v| v.as_slice()), Some(&b"latin one\nand two\n"[..]),
206 "the edit landed in the file it named");
207
208 // And the working copy is what the history says it is, so nothing about
209 // those paths is captured a second time.
210 let out = res!(ore(&into, &["log"]));
211 let text = fmt!("{}", res!(out.good("log")));
212 assert!(text.contains("captured nothing"),
213 "a path that is not UTF-8 round trips through the working copy: {}", text);
214 Ok(())
215}
216
217/// A lightweight tag imports as a mark on the commit it named.
218///
219/// A mark is what says "this point in history, by this name", which is exactly
220/// what a lightweight tag is: a reference and nothing more. So no wire cost, no
221/// new operation, and `ore back <tag>` works the moment the import finishes.
222#[test]
223fn a_lightweight_tag_imports_as_a_mark() -> Outcome<()> {
224 let scratch = res!(Scratch::new("oracle_tag"));
225 let src = res!(scratch.sub("src"));
226 let into = res!(scratch.sub("ore"));
227
228 res!(git_init(&src));
229 res!(write(&src, "a.txt", b"first\n"));
230 res!(git(&src, &["add", "-A"]));
231 res!(git(&src, &["commit", "--quiet", "--no-verify", "-m", "first"]));
232 res!(git(&src, &["tag", "v1.0"]));
233 res!(write(&src, "a.txt", b"first\nsecond\n"));
234 res!(git(&src, &["add", "-A"]));
235 res!(git(&src, &["commit", "--quiet", "--no-verify", "-m", "second"]));
236 res!(git(&src, &["tag", "v1.1"]));
237
238 res!(res!(ore(&into, &["init"])).good("init"));
239 let out = res!(ore(&into, &["import", &fmt!("{}", src.display())]));
240 let text = fmt!("{}", res!(out.good("import")));
241 assert!(text.contains("2 lightweight tags recorded as marks"),
242 "both tags are recorded, and the import says so: {}", text);
243
244 let out = res!(ore(&into, &["log"]));
245 let text = fmt!("{}", res!(out.good("log")));
246 assert!(text.contains("mark \"v1.0\""), "the tag is a mark: {}", text);
247 assert!(text.contains("mark \"v1.1\""), "and so is the other: {}", text);
248
249 // And it names the point it tagged, which is what makes it worth having:
250 // v1.0 is the first commit's state, before the second line arrived.
251 res!(res!(ore(&into, &["back", "v1.0"])).good("back v1.0"));
252 let tree = res!(tree_of(&into));
253 assert_eq!(tree.get("a.txt").map(|v| v.as_slice()), Some(&b"first\n"[..]),
254 "the tag stands where git put it");
255 Ok(())
256}
257
258/// An annotated tag is refused, by name, rather than flattened into a mark that
259/// could not carry its message.
260#[test]
261fn an_annotated_tag_is_refused() -> Outcome<()> {
262 let scratch = res!(Scratch::new("oracle_annotated"));
263 let src = res!(scratch.sub("src"));
264 let into = res!(scratch.sub("ore"));
265
266 res!(git_init(&src));
267 res!(write(&src, "a.txt", b"content\n"));
268 res!(git(&src, &["add", "-A"]));
269 res!(git(&src, &["commit", "--quiet", "--no-verify", "-m", "only commit"]));
270 res!(git(&src, &["tag", "-a", "release-1", "-m", "the first release"]));
271
272 res!(res!(ore(&into, &["init"])).good("init"));
273 let out = res!(ore(&into, &["import", &fmt!("{}", src.display())]));
274 assert!(!out.ok, "the import is refused");
275 assert!(out.err.contains("release-1"), "and names the tag: {}", out.err);
276 assert!(out.err.contains("annotated"), "and says what it could not carry: {}",
277 out.err);
278 Ok(())
279}
280
281/// A submodule is refused, by path, rather than skipped in silence.
282///
283/// Silently leaving the entry out was the old behaviour, and it is the wrong
284/// kind of nothing: a repository with submodules would have imported with a
285/// hole in every tree from that commit on and no word said.
286#[test]
287fn a_submodule_is_refused() -> Outcome<()> {
288 let scratch = res!(Scratch::new("oracle_gitlink"));
289 let src = res!(scratch.sub("src"));
290 let into = res!(scratch.sub("ore"));
291
292 res!(git_init(&src));
293 res!(write(&src, "a.txt", b"content\n"));
294 res!(git(&src, &["add", "-A"]));
295 res!(git(&src, &["commit", "--quiet", "--no-verify", "-m", "base"]));
296 // A gitlink written straight into the index, which is what a submodule
297 // leaves in a tree once the dance around it is done.
298 res!(git(&src, &["update-index", "--add", "--cacheinfo",
299 "160000,0000000000000000000000000000000000000001,vendor"]));
300 res!(git(&src, &["commit", "--quiet", "--no-verify", "-m", "add a submodule"]));
301
302 res!(res!(ore(&into, &["init"])).good("init"));
303 let out = res!(ore(&into, &["import", &fmt!("{}", src.display())]));
304 assert!(!out.ok, "the import is refused");
305 assert!(out.err.contains("vendor"), "and names the path: {}", out.err);
306 assert!(out.err.contains("submodule"), "and says what it is: {}", out.err);
307 assert!(out.err.contains("160000"), "and the mode git recorded: {}", out.err);
308 Ok(())
309}
310
311/// Materialising a state puts a file's mode back, even where not one byte of it
312/// changed.
313///
314/// A script whose executable bit has been cleared is not the file the history
315/// says it is, however well its bytes match, so the bit is asserted on every
316/// path of the tree rather than only on the ones being rewritten.
317#[test]
318fn back_restores_a_mode_the_working_copy_lost() -> Outcome<()> {
319 let scratch = res!(Scratch::new("oracle_remode"));
320 let src = res!(scratch.sub("src"));
321 let into = res!(scratch.sub("ore"));
322
323 res!(git_init(&src));
324 res!(write(&src, "run.sh", b"#!/bin/sh\necho go\n"));
325 res!(make_exec(&src.join("run.sh")));
326 res!(write(&src, "plain.txt", b"ordinary\n"));
327 res!(git(&src, &["add", "-A"]));
328 res!(git(&src, &["commit", "--quiet", "--no-verify", "-m", "base"]));
329
330 res!(res!(ore(&into, &["init"])).good("init"));
331 res!(res!(ore(&into, &["import", &fmt!("{}", src.display())])).good("import"));
332 let script = into.join("run.sh");
333 assert!(res!(fs::metadata(&script)).permissions().mode() & 0o100 != 0,
334 "the import wrote it executable");
335
336 // Somebody clears the bit. Not a byte of the file changed, so there is
337 // nothing for a capture to see and nothing it should invent.
338 let mode = res!(fs::metadata(&script)).permissions().mode();
339 res!(fs::set_permissions(&script, fs::Permissions::from_mode(mode & !0o111)));
340 let out = res!(ore(&into, &["log"]));
341 let text = fmt!("{}", res!(out.good("log")));
342 assert!(text.contains("captured nothing"),
343 "a mode is not something the working copy scan records: {}", text);
344
345 res!(res!(ore(&into, &["back", "base"])).good("back base"));
346 assert!(res!(fs::metadata(&script)).permissions().mode() & 0o100 != 0,
347 "and materialising the state puts it back");
348 assert!(res!(fs::metadata(into.join("plain.txt"))).permissions().mode() & 0o111 == 0,
349 "while an ordinary file is left ordinary");
350 Ok(())
351}