Oregami
Repositories/oxedyne/ore

oxedyne/ore/cli/tests/stat.rs

19.9 KiB, 1 run

created by r2848102244:747, 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//! What the working copy index must never do, which is lose an edit.
2//!
3//! The index lets a capture skip reading the working copy and skip rendering
4//! the history. Every other cache in this tool is checked after the fact -- the
5//! verdict cache by a digest of the bytes it skipped -- and this one is not.
6//! If it says unchanged and the file changed, the edit is not refused and not
7//! reported. It is gone.
8//!
9//! So these are loss tests before they are speed tests. Each one changes a file
10//! in a way an index could plausibly miss, runs a verb, and asserts the change
11//! reached the history.
12
13mod support;
14
15use support::{
16 ore,
17 write,
18 Scratch,
19};
20
21use oxedyne_fe2o3_core::prelude::*;
22
23use std::fs;
24use std::path::Path;
25use std::time::{
26 Duration,
27 SystemTime,
28};
29
30
31/// Sets a file's modification time to `secs` ago, so that a test can put an
32/// entry safely outside the window in which a time means nothing.
33///
34/// The change time cannot be set this way, by this or by `touch`, which is
35/// exactly why the index records it.
36fn backdate(path: &Path, secs: u64)
37 -> Outcome<()>
38{
39 let when = match SystemTime::now().checked_sub(Duration::from_secs(secs)) {
40 Some(t) => t,
41 None => return Err(err!("The clock is before the epoch."; Test)),
42 };
43 let file = res!(fs::OpenOptions::new().write(true).open(path));
44 res!(file.set_modified(when));
45 Ok(())
46}
47
48/// A repository holding one file, with an index that believes it.
49///
50/// The file is backdated because a file touched a moment ago is deliberately
51/// not believed, and a test that wants the fast path has to get past that
52/// rather than around it.
53fn settled(what: &str)
54 -> Outcome<(Scratch, std::path::PathBuf)>
55{
56 let scratch = res!(Scratch::new(what));
57 let root = res!(scratch.sub("work"));
58 res!(res!(ore(&root, &["init"])).good("init"));
59 res!(write(&root, "a.txt", b"one two three four five six seven\n"));
60 res!(res!(ore(&root, &["log"])).good("log"));
61 res!(backdate(&root.join("a.txt"), 60));
62 // This one appends nothing, so it is the one that leaves an index.
63 res!(res!(ore(&root, &["log"])).good("log"));
64 let index = root.join(".ore").join("stat");
65 if !index.is_file() {
66 return Err(err!("No index was written at {:?}.", index; Test, Missing));
67 }
68 Ok((scratch, root))
69}
70
71/// Does the report say the file was edited?
72fn said_edited(text: &str, path: &str) -> bool {
73 text.lines().any(|line| {
74 let t = line.trim_start();
75 t.starts_with("edited") && t.contains(path)
76 })
77}
78
79
80/// **The load-bearing test.** An edit that keeps the size and puts the time
81/// back is still captured.
82///
83/// This is the shape of every way an index gets it wrong: a restore, a
84/// checkout, a `touch -r`, a copy that preserves times, or an editor that
85/// rewrites a line with one of the same length. Size and modification time both
86/// agree with what was recorded, and the bytes do not. What catches it is the
87/// change time, which a write always moves and which nothing can set.
88///
89/// The fixture is proved able to distinguish before it is trusted: the two
90/// fields an index would naively compare are asserted to be **equal** to what
91/// was recorded, so an index checking only those would call this file clean.
92#[test]
93fn an_edit_that_keeps_the_size_and_the_time_is_still_captured() -> Outcome<()> {
94 let (_scratch, root) = res!(settled("racy_edit"));
95 let file = root.join("a.txt");
96 let before = res!(fs::metadata(&file));
97 let was_len = before.len();
98 let was_mtime = res!(before.modified());
99
100 // Same length, different bytes.
101 res!(fs::write(&file, b"ONE TWO THREE FOUR FIVE SIX SEVEN\n"));
102 let file_handle = res!(fs::OpenOptions::new().write(true).open(&file));
103 res!(file_handle.set_modified(was_mtime));
104 drop(file_handle);
105
106 let now = res!(fs::metadata(&file));
107 assert_eq!(now.len(), was_len, "the size is what it was");
108 assert_eq!(res!(now.modified()), was_mtime, "and so is the modification time");
109
110 let ran = res!(ore(&root, &["log"]));
111 let out = res!(ran.good("log"));
112 assert!(said_edited(out, "a.txt"),
113 "the edit must reach the history whatever the size and the time say: {}", out);
114
115 // And it is really there, not merely reported.
116 let again = res!(ore(&root, &["log"]));
117 let out = res!(again.good("log"));
118 assert!(!said_edited(out, "a.txt"), "and once recorded it is not recorded twice: {}", out);
119 let held = res!(ore(&root, &["who", "a.txt"]));
120 let seen = res!(held.good("who"));
121 assert!(seen.contains("ONE TWO THREE"), "the new bytes are what the history holds: {}", seen);
122 Ok(())
123}
124
125/// A capture that recorded something leaves no index behind.
126///
127/// It has moved the history and cannot say, without rendering again, that the
128/// working copy now matches it. The command after it pays one full capture.
129#[test]
130fn a_capture_that_recorded_something_leaves_no_index() -> Outcome<()> {
131 let (_scratch, root) = res!(settled("no_index_after_write"));
132 let index = root.join(".ore").join("stat");
133 assert!(index.is_file());
134
135 res!(write(&root, "b.txt", b"a new file\n"));
136 let ran = res!(ore(&root, &["log"]));
137 res!(ran.good("log"));
138 assert!(!index.is_file(), "a capture that appended left no index");
139
140 res!(res!(ore(&root, &["log"])).good("log"));
141 assert!(index.is_file(), "and the next command establishes one");
142 Ok(())
143}
144
145/// A file that arrived, and one that left, are both changes.
146#[test]
147fn a_path_that_arrived_or_left_is_not_clean() -> Outcome<()> {
148 let (_scratch, root) = res!(settled("paths"));
149 res!(write(&root, "c.txt", b"arrived\n"));
150 let ran = res!(ore(&root, &["log"]));
151 let out = res!(ran.good("log"));
152 assert!(out.contains("created") && out.contains("c.txt"),
153 "a path the index does not know is a change: {}", out);
154
155 res!(backdate(&root.join("c.txt"), 60));
156 res!(res!(ore(&root, &["log"])).good("log"));
157 res!(fs::remove_file(root.join("c.txt")));
158 let ran = res!(ore(&root, &["log"]));
159 let out = res!(ran.good("log"));
160 assert!(out.contains("deleted") && out.contains("c.txt"),
161 "and a path the index knows and the disk does not is a change: {}", out);
162 Ok(())
163}
164
165/// An index nothing can read costs an edit nothing.
166///
167/// Asserted as a loss test: the file is made nonsense **and** the working copy
168/// is changed, so anything that took the rubbish for "clean" would lose the
169/// edit. It passes for more than one reason -- a nonsense index is discarded,
170/// and one that survived as a fragment would disagree about the path count
171/// anyway -- and that is the point of it rather than a weakness. Which of the
172/// two is doing the work is settled in `stat.rs` by
173/// `a_line_this_build_cannot_read_discards_the_index`, because no fixture here
174/// can tell them apart.
175#[test]
176fn an_index_that_does_not_parse_reads_everything() -> Outcome<()> {
177 for rubbish in [
178 &b""[..],
179 &b"ORESTAT 1\n"[..],
180 &b"ORESTAT 2\ntaken 1\nat\n"[..],
181 &b"ORESTAT 1\ntaken not-a-number\n"[..],
182 &b"ORESTAT 1\ntaken 1\nat r1:1\n12 nonsense\n"[..],
183 &b"\x00\x01\x02 not text at all"[..],
184 ] {
185 let (_scratch, root) = res!(settled("rubbish"));
186 res!(fs::write(root.join(".ore").join("stat"), rubbish));
187 res!(fs::write(root.join("a.txt"), b"CHANGED, and the same length ......\n"));
188 let ran = res!(ore(&root, &["log"]));
189 let out = res!(ran.good("log"));
190 assert!(said_edited(out, "a.txt"),
191 "an index of {:?} must be read as none at all: {}", rubbish, out);
192 }
193 Ok(())
194}
195
196/// Deleting the index costs a capture and nothing else.
197#[test]
198fn the_index_is_derived_and_deleting_it_is_safe() -> Outcome<()> {
199 let (_scratch, root) = res!(settled("deletable"));
200 let index = root.join(".ore").join("stat");
201 let before = fmt!("{}", res!(res!(ore(&root, &["log"])).good("log")));
202 res!(fs::remove_file(&index));
203 let after = fmt!("{}", res!(res!(ore(&root, &["log"])).good("log")));
204 assert_eq!(before, after, "the same answer with it and without it");
205 assert!(index.is_file(), "and it comes back");
206 // It is beside the log, never in it.
207 let mut inside: Vec<String> = Vec::new();
208 for entry in res!(fs::read_dir(root.join(".ore").join("log"))) {
209 inside.push(res!(entry).file_name().to_string_lossy().into_owned());
210 }
211 assert!(inside.iter().all(|n| n.ends_with(".seg")),
212 "the log holds segments and nothing else: {:?}", inside);
213 Ok(())
214}
215
216/// The index is CONSULTED and not merely written, shown by what the tool does
217/// to its own bookkeeping.
218///
219/// The two paths are observationally identical by design -- a capture that finds
220/// nothing and a capture that looks at nothing both print the same line -- and
221/// every way of making one of them fail changes a file's metadata, which is the
222/// very thing the index reads. What does differ is that the fast path returns
223/// before writing anything, so the index file it believed is left exactly as it
224/// was, while a slow capture that finds nothing writes a fresh one.
225///
226/// The fixture is two-state: the same assertion is made where the fast path
227/// cannot be taken, and there the index is expected to move. A test that only
228/// asked "did it stay put" would pass against a tool that never wrote one.
229#[test]
230fn the_index_is_consulted_and_not_merely_written() -> Outcome<()> {
231 let (_scratch, root) = res!(settled("consulted"));
232 let index = root.join(".ore").join("stat");
233 let first = res!(res!(fs::metadata(&index)).modified());
234
235 res!(res!(ore(&root, &["log"])).good("log"));
236 let after = res!(res!(fs::metadata(&index)).modified());
237 assert_eq!(first, after,
238 "a capture that consulted the index wrote nothing back to it");
239
240 // The other state: with no index to consult, the same command writes one.
241 res!(fs::remove_file(&index));
242 res!(res!(ore(&root, &["log"])).good("log"));
243 let fresh = res!(res!(fs::metadata(&index)).modified());
244 assert!(fresh > first,
245 "and with none to consult it establishes one, so the assertion above is \
246 about the consulting and not about the writing");
247
248 // And the third: a change removes it, which the command after pays for.
249 res!(fs::write(root.join("a.txt"), b"different, and a different length too\n"));
250 res!(res!(ore(&root, &["log"])).good("log"));
251 assert!(!index.is_file(), "a capture that recorded something left none");
252 Ok(())
253}
254
255
256/// The credential guard still sees everything it saw before.
257///
258/// The fast path does not run it, and that is a claim about `refuse_credentials`
259/// rather than a shortcut: it inspects only files whose bytes differ from what
260/// the history says, and on the path where nothing differs there is nothing for
261/// it to catch. What matters is the case that does reach it -- a credential
262/// arriving -- and a path the index does not know is never clean, so it always
263/// does. Asserted here because that is lane C's guard and this lane moved the
264/// road it stands on.
265#[test]
266fn a_credential_is_still_refused_with_an_index_in_place() -> Outcome<()> {
267 let (_scratch, root) = res!(settled("guarded"));
268 // The index is believed for the file that is there.
269 res!(res!(ore(&root, &["log"])).good("log"));
270
271 // Spelled in two pieces and joined here, so that the scanners which read this
272 // very file -- the commit hook, and the guard under test -- find nothing in
273 // it. The idiom is lane C's, in `cli/tests/guard.rs`, and it is theirs to
274 // keep.
275 let planted = fmt!("let key = \"{}{}\";\n", "fw", "_7QvRt42nXyZaBcDeFgHiJkLmNoPq");
276 res!(write(&root, "conf.txt", planted.as_bytes()));
277 let ran = res!(ore(&root, &["log"]));
278 assert!(!ran.ok, "a credential arriving must refuse the command: {}{}", ran.out, ran.err);
279 assert!(!ran.out.contains("created"), "and record nothing: {}", ran.out);
280 assert!(ran.err.contains("a credential is in what this command was about to record"),
281 "and say so: {}", ran.err);
282
283 // And nothing was recorded, so the guard's own promise holds.
284 res!(fs::remove_file(root.join("conf.txt")));
285 let after = res!(ore(&root, &["log"]));
286 let out = res!(after.good("log"));
287 assert!(out.contains("captured nothing"),
288 "the refused command wrote nothing to take back: {}", out);
289 Ok(())
290}
291
292
293/// A mark and a note leave the working copy alone, driven through the binary.
294///
295/// This is the weaker half of the claim and it is worth saying which half: a
296/// mark never writes to the working copy, so a mark that had somehow changed
297/// the RENDER would still leave the disk agreeing with itself here. What this
298/// establishes is that neither verb touches a file on its way past, which is
299/// the other thing the index would be wrong about. The render is compared in
300/// `cli/src/stat.rs`, by
301/// `a_mark_and_a_note_change_nothing_the_render_produces`.
302///
303/// **The fixture is built to be able to disagree**, and the test asserts that
304/// before it asserts anything else: several files, two of them edited by
305/// separate operations so the runs are not one apiece, one executable, one
306/// holding bytes that are not text.
307#[test]
308fn a_mark_and_a_note_leave_every_file_exactly_where_it_was() -> Outcome<()> {
309 let scratch = res!(Scratch::new("renders_nothing"));
310 let root = res!(scratch.sub("work"));
311 res!(res!(ore(&root, &["init"])).good("init"));
312
313 // Content the render has to work at: several files, several operations each,
314 // a mode that is not the default, and bytes that are not text.
315 res!(write(&root, "one.txt", b"alpha\nbeta\ngamma\ndelta\n"));
316 res!(write(&root, "deep/two.txt", b"first\nsecond\nthird\n"));
317 res!(write(&root, "bin.dat", &[0u8, 1, 2, 0xff, 0xfe, b'\n', 0x80]));
318 res!(write(&root, "run.sh", b"#!/bin/sh\necho hello\n"));
319 res!(support::make_exec(&root.join("run.sh")));
320 res!(res!(ore(&root, &["log"])).good("log"));
321 // A second round of edits, so a file's bytes come from more than one
322 // operation and the render has runs to order rather than one run a file.
323 res!(write(&root, "one.txt", b"alpha\nBETA\ngamma\ndelta\nepsilon\n"));
324 res!(write(&root, "deep/two.txt", b"first\nsecond\nTHIRD\nfourth\n"));
325 res!(res!(ore(&root, &["log"])).good("log"));
326
327 let before = res!(support::tree_raw(&root));
328 let modes_before = res!(support::exec_bits_of(&root));
329 assert!(before.len() >= 4, "the fixture holds files: {}", before.len());
330
331 // FIRST, that this comparison can fail. An operation that does change a file
332 // must show up in it, or nothing below means anything.
333 res!(write(&root, "one.txt", b"alpha\nBETA\ngamma\ndelta\nepsilon\nzeta\n"));
334 res!(res!(ore(&root, &["log"])).good("log"));
335 let disturbed = res!(support::tree_raw(&root));
336 assert_ne!(before, disturbed, "the comparison notices a change, so it is worth making");
337 // Put it back, and take the reference from where the test really starts.
338 res!(write(&root, "one.txt", b"alpha\nBETA\ngamma\ndelta\nepsilon\n"));
339 res!(res!(ore(&root, &["log"])).good("log"));
340 let before = res!(support::tree_raw(&root));
341 let modes_before = { let _ = modes_before; res!(support::exec_bits_of(&root)) };
342
343 // A MARK.
344 res!(res!(ore(&root, &["mark", "a point somebody named"])).good("mark"));
345 let after = res!(support::tree_raw(&root));
346 assert_eq!(before, after, "a mark left every path and every byte where it was");
347 assert_eq!(modes_before, res!(support::exec_bits_of(&root)),
348 "and every mode");
349
350 // A NOTE, which names content and is the one of the two that does.
351 res!(res!(ore(&root, &["note", "one.txt:2", "this line was shouted"])).good("note"));
352 let after = res!(support::tree_raw(&root));
353 assert_eq!(before, after, "a note left every path and every byte where it was");
354 assert_eq!(modes_before, res!(support::exec_bits_of(&root)),
355 "and every mode");
356
357 // And the history really did grow, so the two above are not comparing a
358 // repository nothing happened to.
359 let out = fmt!("{}", res!(res!(ore(&root, &["log"])).good("log")));
360 assert!(out.contains("a point somebody named"), "the mark is in the history: {}", out);
361 Ok(())
362}
363
364/// A mark moves the frontier and the index moves with it, without a file being
365/// read.
366#[test]
367fn a_command_that_only_marked_keeps_its_index() -> Outcome<()> {
368 let (_scratch, root) = res!(settled("restamp"));
369 let index = root.join(".ore").join("stat");
370 let was = res!(fs::read_to_string(&index));
371
372 res!(res!(ore(&root, &["mark", "still here"])).good("mark"));
373 assert!(index.is_file(), "the mark kept the index");
374 let now = res!(fs::read_to_string(&index));
375 assert_ne!(was, now, "and moved its frontier on");
376
377 // Only the frontier moved. Every file fact is what it was, which is what
378 // keeps a verb that wrote to the working copy catchable.
379 let facts = |text: &str| -> Vec<String> {
380 text.lines().filter(|l| !l.starts_with("at ") && !l.starts_with("taken "))
381 .map(|l| fmt!("{}", l)).collect()
382 };
383 assert_eq!(facts(&was), facts(&now), "and nothing else about it");
384 let frontier = |text: &str| -> String {
385 text.lines().find(|l| l.starts_with("at ")).map(|l| fmt!("{}", l)).unwrap_or_default()
386 };
387 assert_ne!(frontier(&was), frontier(&now), "the frontier is the field that moved");
388 Ok(())
389}
390
391/// A verb that wrote to the working copy is still caught after a mark.
392///
393/// The re-stamp rewrites one field and leaves every file fact alone, so a file
394/// that moved between the index being taken and the next command is caught by
395/// its size, its times and its inode exactly as it was before. Asserted because
396/// the alternative reading of "keep the index" -- widening the frontier check --
397/// would lose this.
398#[test]
399fn a_file_that_moved_after_a_mark_is_still_captured() -> Outcome<()> {
400 let (_scratch, root) = res!(settled("restamp_safety"));
401 res!(res!(ore(&root, &["mark", "before the change"])).good("mark"));
402 assert!(root.join(".ore").join("stat").is_file(), "the index survived the mark");
403
404 res!(fs::write(root.join("a.txt"), b"one two three four five six SEVEN\n"));
405 let ran = res!(ore(&root, &["log"]));
406 let out = res!(ran.good("log"));
407 assert!(said_edited(out, "a.txt"),
408 "a file that moved after the mark is still read: {}", out);
409 Ok(())
410}
411
412/// A command that appended anything else takes the index away.
413#[test]
414fn a_command_that_appended_an_edit_leaves_no_index() -> Outcome<()> {
415 let (_scratch, root) = res!(settled("restamp_edit"));
416 res!(res!(ore(&root, &["mark", "a point"])).good("mark"));
417 assert!(root.join(".ore").join("stat").is_file());
418
419 // `ore note` appends a note, which leaves the files alone, so the index
420 // stays. `ore back` appends edits, and it does not.
421 res!(res!(ore(&root, &["note", "a.txt:1", "a remark"])).good("note"));
422 assert!(root.join(".ore").join("stat").is_file(),
423 "a note leaves every file where it was, so the index stands");
424
425 res!(fs::write(root.join("a.txt"), b"quite different, and longer than before\n"));
426 res!(res!(ore(&root, &["log"])).good("log"));
427 assert!(!root.join(".ore").join("stat").is_file(),
428 "an edit took it away");
429 Ok(())
430}
431
432
433/// `ore back` moves the working copy without appending, and the next command
434/// still captures every byte of it.
435///
436/// The interesting half of the re-stamp's safety. `back` materialises an earlier
437/// state and appends nothing, so the frontier does not move and the index is
438/// left standing -- with file facts describing files that have just been
439/// rewritten. Nothing about the frontier saves this; what saves it is that the
440/// facts are the ones that were measured, and every file `back` touched now
441/// disagrees with them.
442#[test]
443fn a_working_copy_moved_without_an_append_is_still_captured() -> Outcome<()> {
444 let (_scratch, root) = res!(settled("back_moves"));
445 res!(res!(ore(&root, &["mark", "the point to come back to"])).good("mark"));
446 res!(write(&root, "b.txt", b"a second file, so there is something to come back from\n"));
447 res!(res!(ore(&root, &["log"])).good("log"));
448 res!(backdate(&root.join("b.txt"), 60));
449 res!(backdate(&root.join("a.txt"), 60));
450 res!(res!(ore(&root, &["log"])).good("log"));
451 assert!(root.join(".ore").join("stat").is_file(), "a clean command established one");
452
453 let ran = res!(ore(&root, &["back", "the point to come back to"]));
454 let out = res!(ran.good("back"));
455 assert!(!out.contains("nothing moved"), "the fixture must really move something: {}", out);
456 assert!(!root.join("b.txt").is_file(), "and b.txt is gone from the working copy");
457
458 // Whatever became of the index, the next command must see what `back` did.
459 let ran = res!(ore(&root, &["log"]));
460 let out = res!(ran.good("log"));
461 assert!(out.contains("deleted") && out.contains("b.txt"),
462 "the file `back` removed is recorded as removed: {}", out);
463 let again = res!(ore(&root, &["log"]));
464 assert!(res!(again.good("log")).contains("captured nothing"),
465 "and once recorded, not again");
466 Ok(())
467}