Oregami
Repositories/oxedyne/ore

oxedyne/ore/cli/tests/revert.rs

23.4 KiB, 1 run

created by r2848102244:387, 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//! `ore revert`, driven as a person drives it.
2//!
3//! Three things are being insisted on here, and only the first is ordinary.
4//!
5//! The first is that the inverse is right: the bytes an insertion put in come
6//! out, the bytes a deletion took out come back, a move goes back where it was
7//! **as a move**, and the history grew while each of those happened rather than
8//! shrank.
9//!
10//! The second is attribution, which is the reason the format gained an operation
11//! for this. Restored text is a copy under a new identity, so the naive reading
12//! credits whoever ran the revert with somebody else's writing. The test for it
13//! uses three replicas and three keys, because with fewer the assertion passes
14//! whether or not the code does anything: one author cannot be credited to the
15//! wrong person.
16//!
17//! The third is the candour gap, and it is asserted **as a fact about the tool
18//! rather than as a wish**. `ore flags` says the same thing before a revert and
19//! after it, and that is checked here so that the day it stops being true, the
20//! sentence `ore revert` prints about it fails a test instead of quietly becoming
21//! a lie.
22
23mod support;
24
25use support::{
26 ore,
27 tree_of,
28 write,
29 Scratch,
30};
31
32use oxedyne_fe2o3_core::prelude::*;
33
34use std::path::Path;
35
36
37/// Returns the operation identifiers `ore who` names, one per run of the file.
38///
39/// The identity is the token beginning `r` and holding a colon, found rather
40/// than counted to, so that the provenance mark in front of it may be any of the
41/// three characters it can be.
42fn runs_of(text: &str) -> Vec<String> {
43 let mut out = Vec::new();
44 for line in text.lines() {
45 // A run line begins with a byte range; the continuation line beneath a
46 // restored run does not, and is not a run.
47 let first = match line.split_whitespace().next() {
48 Some(f) => f,
49 None => continue,
50 };
51 if !first.contains("..") || first.starts_with('r') {
52 continue;
53 }
54 if let Some(id) = line.split_whitespace().find(|w| w.starts_with('r') && w.contains(':')) {
55 out.push(fmt!("{}", id));
56 }
57 }
58 out
59}
60
61/// Returns every mark `ore log --auto` lists, as its identity, its name and the
62/// first parent it was written over.
63fn points(text: &str) -> Vec<(String, String, String)> {
64 let mut out: Vec<(String, String, String)> = Vec::new();
65 let mut lines = text.lines().peekable();
66 while let Some(line) = lines.next() {
67 if !line.starts_with("mark ") {
68 continue;
69 }
70 let id = match line.split_whitespace().find(|w| w.starts_with('r') && w.contains(':')) {
71 Some(w) => fmt!("{}", w),
72 None => continue,
73 };
74 let name = line.trim_start_matches("mark ").split('"').nth(1).unwrap_or("").to_string();
75 let parent = match lines.peek() {
76 Some(next) if next.contains("parents") => {
77 let at = next.find("parents").unwrap_or(0) + "parents".len();
78 fmt!("{}", next[at..].split(',').next().unwrap_or("").trim())
79 },
80 _ => fmt!(""),
81 };
82 out.push((id, name, parent));
83 }
84 out
85}
86
87/// Returns the last operation the command that wrote a named mark appended.
88///
89/// Every command that appends anything ends by marking the point it reached, so
90/// a mark's parent is the last thing that command wrote -- which is how a test
91/// names an operation that renders nowhere and so appears in no other output.
92/// Where that parent is itself a mark, the walk carries on through it: a command
93/// that appended nothing but its own automatic mark sits between the two.
94fn under_mark(text: &str, name: &str)
95 -> Outcome<String>
96{
97 let points = points(text);
98 let mut at = res!(points.iter().find(|(_, got, _)| got == name)
99 .map(|(_, _, parent)| parent.clone())
100 .ok_or_else(|| err!(
101 "There is no mark {:?} in the log: {}", name, text; Test, Missing)));
102 // Automatic marks sit between commands, and the caller wants the work.
103 while let Some((_, _, parent)) = points.iter().find(|(id, _, _)| *id == at) {
104 if parent.is_empty() {
105 break;
106 }
107 at = parent.clone();
108 }
109 if at.is_empty() {
110 return Err(err!(
111 "The mark {:?} was written over nothing: {}", name, text; Test, Missing));
112 }
113 Ok(at)
114}
115
116/// Returns the bytes of one file of the working copy, or nothing where the
117/// working copy does not hold it.
118fn bytes_of(root: &Path, path: &str)
119 -> Outcome<Option<Vec<u8>>>
120{
121 Ok(res!(tree_of(root)).get(path).cloned())
122}
123
124/// Returns what `ore flags` says, which is the surface this feature is invisible
125/// to.
126fn flags(root: &Path)
127 -> Outcome<String>
128{
129 let out = res!(ore(root, &["flags"]));
130 Ok(fmt!("{}", res!(out.good("flags"))))
131}
132
133
134/// Reverting an insertion takes exactly those bytes out, and the history grows
135/// while it happens.
136#[test]
137fn reverting_an_insertion_removes_what_it_inserted() -> Outcome<()> {
138 let scratch = res!(Scratch::new("revert_insert"));
139 let root = res!(scratch.sub("work"));
140 res!(res!(ore(&root, &["init"])).good("init"));
141 res!(write(&root, "a.txt", b"alpha\nbeta\n"));
142 res!(res!(ore(&root, &["mark", "one"])).good("mark one"));
143 res!(write(&root, "a.txt", b"alpha\nbeta\nadded later\n"));
144 res!(res!(ore(&root, &["mark", "two"])).good("mark two"));
145
146 // The added line is its own run, and its own operation, which is what makes
147 // this revertible at all: capture writes the difference and not the file.
148 let out = res!(ore(&root, &["who", "a.txt"]));
149 let runs = runs_of(res!(out.good("who")));
150 if runs.len() != 2 {
151 return Err(err!(
152 "The file should be two runs, one per edit, and `ore who` names {}: {}",
153 runs.len(), res!(out.good("who")); Test, Mismatch));
154 }
155 let added = runs[1].clone();
156
157 let before = res!(ore(&root, &["log", "--auto"]));
158 let held = fmt!("{}", res!(before.good("log"))).lines()
159 .filter(|l| l.contains(" operations in ")).count();
160 assert_eq!(held, 1, "the log says how many operations it holds, once");
161
162 let out = res!(ore(&root, &["revert", &added]));
163 let text = fmt!("{}", res!(out.good("revert")));
164 assert!(text.contains("this removes 12 bytes"),
165 "the revert says what it takes out: {}", text);
166
167 assert_eq!(res!(bytes_of(&root, "a.txt")), Some(b"alpha\nbeta\n".to_vec()),
168 "the inserted line is gone and nothing else is");
169
170 // And nothing was taken out of the history to do it. The reverted operation
171 // is still there, and so is everything else.
172 let out = res!(ore(&root, &["who", "a.txt"]));
173 let after = runs_of(res!(out.good("who")));
174 assert_eq!(after.len(), 1, "one run is left");
175 assert_ne!(after[0], added, "and it is not the one that was reverted");
176 Ok(())
177}
178
179/// Every operation with no inverse is refused, by name, with the reason.
180///
181/// The reasons matter as much as the refusals. Each of these looks at first like
182/// an omission -- surely a deletion can be undone, surely a note can be taken
183/// back -- and each is a consequence of what the history is, so the message says
184/// which consequence rather than "unsupported".
185#[test]
186fn an_operation_with_no_inverse_is_refused_and_says_why() -> Outcome<()> {
187 let scratch = res!(Scratch::new("revert_refuse"));
188 let root = res!(scratch.sub("work"));
189 res!(res!(ore(&root, &["init"])).good("init"));
190 res!(write(&root, "a.txt", b"alpha\nbeta\n"));
191 res!(write(&root, "b.txt", b"gone soon\n"));
192 res!(res!(ore(&root, &["mark", "one"])).good("mark one"));
193 res!(std::fs::remove_file(root.join("b.txt")));
194 res!(res!(ore(&root, &["mark", "deleted"])).good("mark deleted"));
195 res!(res!(ore(&root, &["note", "a.txt:1", "a remark"])).good("note"));
196 res!(res!(ore(&root, &["mark", "noted"])).good("mark noted"));
197
198 let out = res!(ore(&root, &["log", "--auto"]));
199 let log = fmt!("{}", res!(out.good("log")));
200 // A file's deletion, named through the mark the command that made it wrote.
201 let deletion = res!(under_mark(&log, "deleted"));
202 // The note, likewise.
203 let note = res!(under_mark(&log, "noted"));
204 // And a mark, which is named outright.
205 let mark = res!(log.lines()
206 .find(|l| l.starts_with("mark \"one\""))
207 .and_then(|l| l.split_whitespace().find(|w| w.starts_with('r') && w.contains(':')))
208 .map(|w| fmt!("{}", w))
209 .ok_or_else(|| err!("The log names no mark \"one\": {}", log; Test, Missing)));
210
211 for (what, id, says) in [
212 ("a file's deletion", &deletion, "no operation revives a file"),
213 ("a note", &note, "nothing un-says it"),
214 ("a mark", &mark, "the history only grows"),
215 ] {
216 let out = res!(ore(&root, &["revert", id]));
217 if out.ok {
218 return Err(err!(
219 "Reverting {} ({}) was allowed: {}", what, id, out.out; Test, Mismatch));
220 }
221 assert!(out.err.contains("Nothing undoes"),
222 "reverting {} says what happened: {}", what, out.err);
223 assert!(out.err.contains(says),
224 "reverting {} says why: {}", what, out.err);
225 }
226
227 // And a refusal writes nothing at all, which is what makes it a refusal.
228 let out = res!(ore(&root, &["log", "--auto"]));
229 assert_eq!(fmt!("{}", res!(out.good("log"))).contains("mark \"noted\""), true);
230 Ok(())
231}
232
233/// `ore who` names whoever wrote restored bytes first, and not whoever put them
234/// back.
235///
236/// Three replicas, because with fewer the assertion cannot fail: the whole risk
237/// is that a copy is credited to the wrong author, and one author is never the
238/// wrong one. The line is written by p, deleted by q, and the deletion is
239/// reverted by r, so the bytes in r's working copy are held by an operation of
240/// r's and were written by p.
241#[test]
242fn who_names_whoever_wrote_restored_bytes_first() -> Outcome<()> {
243 let scratch = res!(Scratch::new("revert_who"));
244 let p = res!(scratch.sub("p"));
245 let q = res!(scratch.sub("q"));
246 let r = res!(scratch.sub("r"));
247 res!(res!(ore(&p, &["init"])).good("init p"));
248 res!(res!(ore(&q, &["init"])).good("init q"));
249 res!(res!(ore(&r, &["init"])).good("init r"));
250
251 // p writes the line.
252 res!(write(&p, "notes.txt", b"p wrote this line\n"));
253 res!(res!(ore(&p, &["mark", "written"])).good("mark written"));
254 let out = res!(ore(&p, &["who", "notes.txt"]));
255 let wrote = res!(runs_of(res!(out.good("who"))).first().cloned()
256 .ok_or_else(|| err!("p's file has no runs."; Test, Missing)));
257
258 // q takes it out.
259 let path = fmt!("{}", p.display());
260 res!(res!(ore(&q, &["sync", &path])).good("q syncs p"));
261 res!(write(&q, "notes.txt", b""));
262 res!(res!(ore(&q, &["mark", "deleted"])).good("mark deleted"));
263 let out = res!(ore(&q, &["log", "--auto"]));
264 let deletion = res!(under_mark(&fmt!("{}", res!(out.good("log"))), "deleted"));
265
266 // r takes both and puts the line back.
267 let path = fmt!("{}", q.display());
268 res!(res!(ore(&r, &["sync", &path])).good("r syncs q"));
269 assert_eq!(res!(bytes_of(&r, "notes.txt")), Some(Vec::new()),
270 "r holds the state q left, which is the line gone");
271 res!(res!(ore(&r, &["revert", &deletion])).good("revert"));
272 assert_eq!(res!(bytes_of(&r, "notes.txt")), Some(b"p wrote this line\n".to_vec()),
273 "the bytes are back exactly");
274
275 let out = res!(ore(&r, &["who", "notes.txt"]));
276 let text = fmt!("{}", res!(out.good("who")));
277 let runs = runs_of(&text);
278 assert_eq!(runs.len(), 1, "one run holds the restored line: {}", text);
279
280 // The identity is r's, honestly: the bytes are a copy and nothing un-buries.
281 assert_ne!(runs[0], wrote,
282 "restored bytes take a new identity; if they kept the old one this test is \
283 measuring something else: {}", text);
284
285 // And the accounting names p all the same, which is the whole point of the
286 // record the revert wrote.
287 assert!(text.contains("first written by"),
288 "`ore who` says the bytes were written before they were restored: {}", text);
289 let credited = res!(text.lines()
290 .find(|l| l.contains("first written by"))
291 .ok_or_else(|| err!("no line credits the first author: {}", text; Test, Missing)));
292 assert!(credited.ends_with(&wrote),
293 "the author credited is {}, which wrote the bytes, and the line says {:?}",
294 wrote, credited);
295 assert!(credited.contains("restored by"),
296 "and it says the bytes were restored rather than written here: {:?}", credited);
297 Ok(())
298}
299
300/// No flag reports a revert, and this test is what stops that sentence going
301/// stale.
302///
303/// `ore revert` tells a person, in as many words, that reading `ore flags`
304/// afterwards is not a check on what the revert did. That claim is only worth
305/// printing while it is true, and a claim about what a tool *cannot* do is
306/// exactly the kind that rots unnoticed. So it is asserted: the flag surface says
307/// the same thing before and after, while the file itself has demonstrably
308/// changed.
309///
310/// It is not a claim that no flag can ever fire for anything a revert writes.
311/// Two of them can -- a cycle among a move's anchors, and a move into a file a
312/// revert retires -- and neither is about the work that was undone.
313#[test]
314fn the_flag_surface_says_nothing_about_a_revert() -> Outcome<()> {
315 let scratch = res!(Scratch::new("revert_flags"));
316 let root = res!(scratch.sub("work"));
317 res!(res!(ore(&root, &["init"])).good("init"));
318 res!(write(&root, "a.txt", b"alpha\nbeta\n"));
319 res!(res!(ore(&root, &["mark", "one"])).good("mark one"));
320 res!(write(&root, "a.txt", b"alpha\nbeta\ncontested\n"));
321 res!(res!(ore(&root, &["mark", "two"])).good("mark two"));
322 // A second edit, written inside nothing, so that the history has work after
323 // the operation being reverted.
324 res!(write(&root, "a.txt", b"alpha\nbeta\ncontested\nlater\n"));
325 res!(res!(ore(&root, &["mark", "three"])).good("mark three"));
326
327 let out = res!(ore(&root, &["who", "a.txt"]));
328 let runs = runs_of(res!(out.good("who")));
329 let contested = res!(runs.get(1).cloned()
330 .ok_or_else(|| err!("the file has fewer runs than the edits made it"; Test)));
331
332 let before = res!(flags(&root));
333 assert!(before.contains("clean"), "nothing is flagged to begin with: {}", before);
334
335 // The revert says so itself, before it writes.
336 let out = res!(ore(&root, &["revert", &contested]));
337 let said = fmt!("{}", res!(out.good("revert")));
338 assert!(said.contains("what nothing will tell you afterwards"),
339 "the revert carries the candour the flags cannot: {}", said);
340 assert!(said.contains("reading it afterwards is not a check"),
341 "and says outright what reading the flags would be worth: {}", said);
342
343 // The file changed. The flags did not.
344 assert_eq!(res!(bytes_of(&root, "a.txt")), Some(b"alpha\nbeta\nlater\n".to_vec()),
345 "the reverted line is gone and the one written after it is not");
346 let after = res!(flags(&root));
347 assert_eq!(
348 before.lines().filter(|l| !l.starts_with("captured")).collect::<Vec<_>>(),
349 after.lines().filter(|l| !l.starts_with("captured")).collect::<Vec<_>>(),
350 "the flag surface is unmoved by a revert, which is the gap this feature \
351 ships with and the reason the command says so itself",
352 );
353 Ok(())
354}
355
356/// A dry run gives the whole account and writes none of it.
357#[test]
358fn a_dry_run_says_everything_and_writes_nothing() -> Outcome<()> {
359 let scratch = res!(Scratch::new("revert_dry"));
360 let root = res!(scratch.sub("work"));
361 res!(res!(ore(&root, &["init"])).good("init"));
362 res!(write(&root, "a.txt", b"alpha\n"));
363 res!(res!(ore(&root, &["mark", "one"])).good("mark one"));
364 res!(write(&root, "a.txt", b"alpha\nbeta\n"));
365 res!(res!(ore(&root, &["mark", "two"])).good("mark two"));
366
367 let out = res!(ore(&root, &["who", "a.txt"]));
368 let added = res!(runs_of(res!(out.good("who"))).get(1).cloned()
369 .ok_or_else(|| err!("the second edit is not its own run"; Test)));
370
371 let out = res!(ore(&root, &["log", "--auto"]));
372 let was = fmt!("{}", res!(out.good("log")));
373 let frontier = res!(was.lines().find(|l| l.starts_with("frontier"))
374 .ok_or_else(|| err!("the log names no frontier"; Test, Missing)));
375
376 let out = res!(ore(&root, &["revert", &added, "--dry-run"]));
377 let said = fmt!("{}", res!(out.good("revert --dry-run")));
378 assert!(said.contains("nothing was written"), "it says so: {}", said);
379 assert!(said.contains("what goes with it"),
380 "and gives the whole account first: {}", said);
381
382 // The frontier has not moved, which is the strongest thing a test can say
383 // about a history: an operation count can match by coincidence.
384 let out = res!(ore(&root, &["log", "--auto"]));
385 let now = fmt!("{}", res!(out.good("log")));
386 assert!(now.contains(frontier),
387 "the history is where it was: {:?} is not in {}", frontier, now);
388 assert_eq!(res!(bytes_of(&root, "a.txt")), Some(b"alpha\nbeta\n".to_vec()),
389 "and the working copy is untouched");
390 Ok(())
391}
392
393/// A revert says what else in the history is built on the work it is undoing,
394/// before it writes.
395///
396/// The operation reverted here has another edit written *inside* the text it
397/// added. That edit keeps its bytes and loses everything around them, and nothing
398/// afterwards will say so, so it has to be said now.
399#[test]
400fn a_revert_names_the_work_built_on_what_it_undoes() -> Outcome<()> {
401 let scratch = res!(Scratch::new("revert_consequences"));
402 let root = res!(scratch.sub("work"));
403 res!(res!(ore(&root, &["init"])).good("init"));
404 res!(write(&root, "a.txt", b"top\nbottom\n"));
405 res!(res!(ore(&root, &["mark", "one"])).good("mark one"));
406 // A paragraph in the middle.
407 res!(write(&root, "a.txt", b"top\nPARAGRAPH\nbottom\n"));
408 res!(res!(ore(&root, &["mark", "two"])).good("mark two"));
409 let out = res!(ore(&root, &["who", "a.txt"]));
410 let para = res!(runs_of(res!(out.good("who"))).get(1).cloned()
411 .ok_or_else(|| err!("the paragraph is not its own run"; Test)));
412
413 // And somebody writes inside it, anchoring to bytes the paragraph owns.
414 res!(write(&root, "a.txt", b"top\nPARA-inserted-GRAPH\nbottom\n"));
415 res!(res!(ore(&root, &["mark", "three"])).good("mark three"));
416 // A note about the paragraph as well.
417 res!(res!(ore(&root, &["note", "a.txt:2", "worth keeping"])).good("note"));
418 res!(res!(ore(&root, &["mark", "four"])).good("mark four"));
419
420 let out = res!(ore(&root, &["revert", &para, "--dry-run"]));
421 let said = fmt!("{}", res!(out.good("revert --dry-run")));
422 assert!(said.contains("wrote inside the bytes being removed"),
423 "the report names the work written into what is going: {}", said);
424 assert!(said.contains("note"),
425 "and the note that will be left on dead content: {}", said);
426 // It is not enough that the words are there; the operations have to be named,
427 // since a reader is being asked to go and look at them.
428 let out = res!(ore(&root, &["who", "a.txt"]));
429 let runs = runs_of(res!(out.good("who")));
430 let outer = res!(runs.first().cloned()
431 .ok_or_else(|| err!("the file has no runs"; Test, Missing)));
432 let inserted = res!(runs.iter().find(|id| **id != para && **id != outer)
433 .cloned()
434 .ok_or_else(|| err!("the inserted text is in no run of its own: {:?}", runs; Test)));
435 assert!(said.contains(&inserted),
436 "the report names {} rather than only counting it: {}", inserted, said);
437 Ok(())
438}
439
440/// Reverting a move puts the run back where it was, as a move, and leaves the
441/// renderer with nothing to complain about.
442///
443/// The last clause is the assertion worth having. The obvious destination for the
444/// inverse -- the gap in front of the run's first byte -- names the run's own
445/// first byte as its right origin, so the move names its own source as its
446/// destination; the renderer breaks that cycle by demoting both origins, lands
447/// the bytes correctly, and raises two flags about it. The bytes being right is
448/// therefore not evidence that the operation is, which is why this test reads the
449/// flags and not only the file.
450#[test]
451fn reverting_a_move_puts_the_run_back_and_flags_nothing() -> Outcome<()> {
452 let scratch = res!(Scratch::new("revert_move"));
453 let root = res!(scratch.sub("work"));
454 res!(res!(ore(&root, &["init"])).good("init"));
455 // The block has to be long enough for capture to record a move rather than a
456 // deletion and an insertion; below that floor there is no move to revert. The
457 // head and the tail are several lines each for a second reason: relocating the
458 // block past a one-line tail is a smaller edit spelled as the tail moving the
459 // other way, and what is being tested is a move of the block.
460 let block = "MOVED LINE ------------------------------------\n".repeat(3);
461 let head = "head line one\nhead line two\nhead line three\n";
462 let tail = "tail line one\ntail line two\ntail line three\n";
463 res!(write(&root, "m.txt", fmt!("{}{}{}", head, block, tail).as_bytes()));
464 res!(res!(ore(&root, &["mark", "base"])).good("mark base"));
465 res!(write(&root, "m.txt", fmt!("{}{}{}", head, tail, block).as_bytes()));
466 let out = res!(ore(&root, &["mark", "moved"]));
467 let text = fmt!("{}", res!(out.good("mark moved")));
468 assert!(text.contains("moved as a block"),
469 "capture recorded a move and not a rewrite, which is what is being reverted: {}",
470 text);
471
472 let out = res!(ore(&root, &["log", "--auto"]));
473 let mv = res!(under_mark(&fmt!("{}", res!(out.good("log"))), "moved"));
474
475 let before = res!(flags(&root));
476 assert!(before.contains("clean"), "nothing is flagged to begin with: {}", before);
477
478 res!(res!(ore(&root, &["revert", &mv])).good("revert"));
479 assert_eq!(res!(bytes_of(&root, "m.txt")),
480 Some(fmt!("{}{}{}", head, block, tail).into_bytes()),
481 "the block is back between the head and the tail");
482
483 let after = res!(flags(&root));
484 assert!(after.contains("clean"),
485 "an inverse move that names its own source as its destination renders the \
486 right bytes and raises two demoted flags; this is the check that it does \
487 not: {}", after);
488
489 // And the content kept its identity, which is what makes this a move rather
490 // than a re-typing: the whole file is one run again, as it was at the start.
491 let out = res!(ore(&root, &["who", "m.txt"]));
492 assert_eq!(runs_of(res!(out.good("who"))).len(), 1,
493 "the file is one run again, so the bytes travelled rather than being \
494 written afresh: {}", res!(out.good("who")));
495 Ok(())
496}
497
498/// Reverting a file's creation says the file goes, says what somebody moved into
499/// it, and says that this is the revert that cannot be reverted.
500///
501/// This is the case the report was silently wrong about. Counted as content
502/// ranges, undoing a creation touches none, so it announced itself as taking
503/// nothing out of any file -- while retiring a file holding a hundred and fifty
504/// bytes of somebody else's moved work.
505#[test]
506fn reverting_a_creation_says_the_file_goes_with_it() -> Outcome<()> {
507 let scratch = res!(Scratch::new("revert_create"));
508 let root = res!(scratch.sub("work"));
509 res!(res!(ore(&root, &["init"])).good("init"));
510 let block = "BLOCK LINE ------------------------------------\n".repeat(3);
511 let begins = "b begins\n";
512 let held = begins.len() + block.len();
513 res!(write(&root, "a.txt", fmt!("keep\n{}keep too\n", block).as_bytes()));
514 res!(write(&root, "b.txt", begins.as_bytes()));
515 res!(res!(ore(&root, &["mark", "base"])).good("mark base"));
516 // The block crosses into b.txt, which is what makes retiring b.txt expensive.
517 res!(write(&root, "a.txt", b"keep\nkeep too\n"));
518 res!(write(&root, "b.txt", fmt!("{}{}", begins, block).as_bytes()));
519 let out = res!(ore(&root, &["mark", "moved"]));
520 assert!(fmt!("{}", res!(out.good("mark moved"))).contains("moved as a block"),
521 "the block crossed as a move");
522
523 // b.txt's creation is the operation its own file is named by, which `ore who`
524 // prints outright.
525 let out = res!(ore(&root, &["who", "b.txt"]));
526 let text = fmt!("{}", res!(out.good("who")));
527 let made = res!(text.lines()
528 .find(|l| l.starts_with("created by"))
529 .and_then(|l| l.split_whitespace().last())
530 .map(|w| fmt!("{}", w))
531 .ok_or_else(|| err!("`ore who` does not say what created b.txt: {}", text; Test)));
532
533 let out = res!(ore(&root, &["revert", &made, "--dry-run"]));
534 let said = fmt!("{}", res!(out.good("revert --dry-run")));
535 assert!(said.contains("this retires the file b.txt"),
536 "the report says the file goes, in those terms: {}", said);
537 assert!(said.contains(&fmt!("{} bytes", held)),
538 "and how much it holds, which is what a person is deciding about: {}", said);
539 assert!(said.contains("cannot itself be reverted"),
540 "and that a deletion has no inverse, so this is one way: {}", said);
541 assert!(said.contains("moved content into a file this retires"),
542 "and names the work that goes dark with it: {}", said);
543 Ok(())
544}