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 | |
| 13 | mod support; |
| 14 | |
| 15 | use support::{ |
| 16 | ore, |
| 17 | write, |
| 18 | Scratch, |
| 19 | }; |
| 20 | |
| 21 | use oxedyne_fe2o3_core::prelude::*; |
| 22 | |
| 23 | use std::fs; |
| 24 | use std::path::Path; |
| 25 | use 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. |
| 36 | fn 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. |
| 53 | fn 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? |
| 72 | fn 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] |
| 93 | fn 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] |
| 130 | fn 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] |
| 147 | fn 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] |
| 176 | fn 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] |
| 198 | fn 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] |
| 230 | fn 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] |
| 266 | fn 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] |
| 308 | fn 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] |
| 367 | fn 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] |
| 399 | fn 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] |
| 414 | fn 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] |
| 443 | fn 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 | } |