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 | |
| 23 | mod support; |
| 24 | |
| 25 | use support::{ |
| 26 | ore, |
| 27 | tree_of, |
| 28 | write, |
| 29 | Scratch, |
| 30 | }; |
| 31 | |
| 32 | use oxedyne_fe2o3_core::prelude::*; |
| 33 | |
| 34 | use 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. |
| 42 | fn 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. |
| 63 | fn 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. |
| 94 | fn 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. |
| 118 | fn 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. |
| 126 | fn 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] |
| 137 | fn 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] |
| 186 | fn 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", ¬e, "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] |
| 242 | fn 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] |
| 314 | fn 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] |
| 358 | fn 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] |
| 400 | fn 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", ¶, "--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] |
| 451 | fn 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] |
| 506 | fn 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 | } |