11.0 KiB, 12 runs
created by r2848102244:43, 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 undo`, driven as a person drives it. |
| 2 | //! |
| 3 | //! Undo here is a new edit that restores an earlier state, so what these tests |
| 4 | //! insist on is that the bytes come back exactly, that the history grew rather |
| 5 | //! than shrank while they did, and that running the verb a second time puts |
| 6 | //! back what the first one took away -- which is the whole of the redo story. |
| 7 | |
| 8 | mod support; |
| 9 | |
| 10 | use support::{ |
| 11 | git, |
| 12 | git_init, |
| 13 | marks, |
| 14 | operations, |
| 15 | ore, |
| 16 | tree_of, |
| 17 | write, |
| 18 | Scratch, |
| 19 | }; |
| 20 | |
| 21 | use oxedyne_fe2o3_core::prelude::*; |
| 22 | |
| 23 | |
| 24 | /// The bytes the first mark holds. |
| 25 | const FIRST: &[u8] = b"alpha\nbeta\ngamma\n"; |
| 26 | /// The bytes the second mark holds. |
| 27 | const SECOND: &[u8] = b"alpha\nBETA-changed\ngamma\n"; |
| 28 | |
| 29 | |
| 30 | /// Returns the bytes of one file of the working copy. |
| 31 | fn bytes_of(root: &std::path::Path, path: &str) |
| 32 | -> Outcome<Vec<u8>> |
| 33 | { |
| 34 | let now = res!(tree_of(root)); |
| 35 | match now.get(path) { |
| 36 | Some(b) => Ok(b.clone()), |
| 37 | None => Err(err!("The working copy has no {:?}.", path; Test, Missing)), |
| 38 | } |
| 39 | } |
| 40 | |
| 41 | |
| 42 | /// Undo puts back the state from before the last command, and undoing again |
| 43 | /// puts back what that took away. |
| 44 | #[test] |
| 45 | fn undo_restores_and_undoing_again_redoes() -> Outcome<()> { |
| 46 | let scratch = res!(Scratch::new("undo")); |
| 47 | let root = res!(scratch.sub("work")); |
| 48 | res!(res!(ore(&root, &["init"])).good("init")); |
| 49 | res!(write(&root, "a.txt", FIRST)); |
| 50 | res!(write(&root, "b.txt", b"untouched\n")); |
| 51 | res!(res!(ore(&root, &["mark", "A"])).good("mark A")); |
| 52 | res!(write(&root, "a.txt", SECOND)); |
| 53 | res!(res!(ore(&root, &["mark", "B"])).good("mark B")); |
| 54 | |
| 55 | let out = res!(ore(&root, &["log"])); |
| 56 | let before = res!(operations(&fmt!("{}", res!(out.good("log"))))); |
| 57 | |
| 58 | // The first undo goes back to the state before `ore mark B`, which is A's. |
| 59 | let out = res!(ore(&root, &["undo"])); |
| 60 | let text = fmt!("{}", res!(out.good("undo"))); |
| 61 | assert!(text.contains("the state from before `ore mark B`"), |
| 62 | "undo says what it put back: {}", text); |
| 63 | assert_eq!(res!(bytes_of(&root, "a.txt")), FIRST.to_vec(), |
| 64 | "and the bytes are exactly what they were"); |
| 65 | assert_eq!(res!(bytes_of(&root, "b.txt")), b"untouched\n".to_vec(), |
| 66 | "and the file nobody touched is untouched"); |
| 67 | |
| 68 | // The second undo goes back to the state before the first undo, which is B's. |
| 69 | let out = res!(ore(&root, &["undo"])); |
| 70 | let text = fmt!("{}", res!(out.good("undo"))); |
| 71 | assert!(text.contains("the state from before `ore undo`"), |
| 72 | "the second undo undoes the first: {}", text); |
| 73 | assert_eq!(res!(bytes_of(&root, "a.txt")), SECOND.to_vec(), |
| 74 | "undoing twice is redoing"); |
| 75 | |
| 76 | // And the history only grew: the operations that made both states are still |
| 77 | // there, and so are the ones that put them back. |
| 78 | // Every mark, because what is being counted is what each undo left behind and |
| 79 | // the plain listing shows only what a person named. |
| 80 | let out = res!(ore(&root, &["log", "--auto"])); |
| 81 | let text = fmt!("{}", res!(out.good("log --auto"))); |
| 82 | let after = res!(operations(&text)); |
| 83 | assert_eq!(after, before + 4, |
| 84 | "each undo is one splice, and each command that appends anything ends by \ |
| 85 | naming the point it reached, so two undos are four operations: {}", text); |
| 86 | let (named, automatic) = marks(&text); |
| 87 | assert_eq!(named, 2, "the marks a person made are still the marks: {}", text); |
| 88 | assert_eq!(automatic, 2, "and each undo named the point it left: {}", text); |
| 89 | assert!(!text.contains("operations since the last mark"), |
| 90 | "the history ends at a mark, so no work is left standing unnamed: {}", text); |
| 91 | assert!(text.contains("\"A\"") && text.contains("\"B\""), |
| 92 | "with both marks still named: {}", text); |
| 93 | |
| 94 | // Nothing about any of it is concurrent, so nothing is flagged. |
| 95 | let out = res!(ore(&root, &["flags"])); |
| 96 | let text = fmt!("{}", res!(out.good("flags"))); |
| 97 | assert!(text.contains("clean"), "an undo is an ordinary edit: {}", text); |
| 98 | Ok(()) |
| 99 | } |
| 100 | |
| 101 | /// Undo with a mark restores that mark's state, and records the arrival, which |
| 102 | /// is what distinguishes it from `back`. |
| 103 | #[test] |
| 104 | fn undo_to_a_mark_records_the_arrival() -> Outcome<()> { |
| 105 | let scratch = res!(Scratch::new("undo_mark")); |
| 106 | let root = res!(scratch.sub("work")); |
| 107 | res!(res!(ore(&root, &["init"])).good("init")); |
| 108 | res!(write(&root, "a.txt", FIRST)); |
| 109 | res!(res!(ore(&root, &["mark", "A"])).good("mark A")); |
| 110 | res!(write(&root, "a.txt", SECOND)); |
| 111 | res!(res!(ore(&root, &["mark", "B"])).good("mark B")); |
| 112 | res!(write(&root, "a.txt", b"alpha\nBETA-again\ngamma\n")); |
| 113 | res!(res!(ore(&root, &["mark", "C"])).good("mark C")); |
| 114 | |
| 115 | let out = res!(ore(&root, &["undo", "A"])); |
| 116 | let text = fmt!("{}", res!(out.good("undo A"))); |
| 117 | assert!(text.contains("the state at mark \"A\""), "undo names the mark: {}", text); |
| 118 | assert_eq!(res!(bytes_of(&root, "a.txt")), FIRST.to_vec(), |
| 119 | "and puts back what it held"); |
| 120 | |
| 121 | // `back` would have left the arrival for the next verb to notice. `undo` |
| 122 | // recorded it, so the next verb has nothing to capture. |
| 123 | let out = res!(ore(&root, &["log"])); |
| 124 | let text = fmt!("{}", res!(out.good("log"))); |
| 125 | assert!(text.contains("captured nothing"), |
| 126 | "undo committed the visit rather than leaving it: {}", text); |
| 127 | Ok(()) |
| 128 | } |
| 129 | |
| 130 | /// Undo with changes nobody has recorded yet undoes those changes. |
| 131 | #[test] |
| 132 | fn undo_puts_back_what_was_not_yet_recorded() -> Outcome<()> { |
| 133 | let scratch = res!(Scratch::new("undo_dirty")); |
| 134 | let root = res!(scratch.sub("work")); |
| 135 | res!(res!(ore(&root, &["init"])).good("init")); |
| 136 | res!(write(&root, "a.txt", FIRST)); |
| 137 | res!(res!(ore(&root, &["mark", "A"])).good("mark A")); |
| 138 | |
| 139 | // Edited, and no command has run since. |
| 140 | res!(write(&root, "a.txt", SECOND)); |
| 141 | let out = res!(ore(&root, &["undo"])); |
| 142 | let text = fmt!("{}", res!(out.good("undo"))); |
| 143 | assert!(text.contains("the state from before the 1 operation just captured"), |
| 144 | "the changes just captured are what there is to undo: {}", text); |
| 145 | assert_eq!(res!(bytes_of(&root, "a.txt")), FIRST.to_vec(), |
| 146 | "and the edit is undone"); |
| 147 | |
| 148 | // The edit is still in the history, undone rather than removed. |
| 149 | let out = res!(ore(&root, &["who", "a.txt"])); |
| 150 | let text = fmt!("{}", res!(out.good("who"))); |
| 151 | assert!(!text.contains("BETA-changed"), |
| 152 | "the file does not hold the undone edit: {}", text); |
| 153 | let out = res!(ore(&root, &["log"])); |
| 154 | let text = fmt!("{}", res!(out.good("log"))); |
| 155 | assert!(res!(operations(&text)) >= 5, |
| 156 | "but the history holds it, and the undo of it: {}", text); |
| 157 | Ok(()) |
| 158 | } |
| 159 | |
| 160 | /// A file created and then undone is gone from the working copy, and a file |
| 161 | /// deleted and then undone comes back. |
| 162 | #[test] |
| 163 | fn undo_reaches_the_lifecycle_as_well_as_the_bytes() -> Outcome<()> { |
| 164 | let scratch = res!(Scratch::new("undo_files")); |
| 165 | let root = res!(scratch.sub("work")); |
| 166 | res!(res!(ore(&root, &["init"])).good("init")); |
| 167 | res!(write(&root, "keep.txt", b"kept\n")); |
| 168 | res!(write(&root, "gone.txt", b"going\n")); |
| 169 | res!(res!(ore(&root, &["mark", "both"])).good("mark both")); |
| 170 | |
| 171 | // A file arrives and another leaves, in one command. |
| 172 | res!(write(&root, "new.txt", b"arrived\n")); |
| 173 | res!(std::fs::remove_file(root.join("gone.txt"))); |
| 174 | res!(res!(ore(&root, &["mark", "changed"])).good("mark changed")); |
| 175 | |
| 176 | res!(res!(ore(&root, &["undo"])).good("undo")); |
| 177 | let now = res!(tree_of(&root)); |
| 178 | assert!(!now.contains_key("new.txt"), "the created file is gone again: {:?}", |
| 179 | now.keys().collect::<Vec<&String>>()); |
| 180 | assert_eq!(now.get("gone.txt").map(|v| v.as_slice()), Some(&b"going\n"[..]), |
| 181 | "and the deleted file is back"); |
| 182 | assert_eq!(now.get("keep.txt").map(|v| v.as_slice()), Some(&b"kept\n"[..])); |
| 183 | |
| 184 | // The bytes came back; the identity did not. There is no operation that |
| 185 | // revives a deleted file, so what the undo recorded is a create and a splice, |
| 186 | // and `ore who` dates the restored file from the undo. |
| 187 | let out = res!(ore(&root, &["who", "gone.txt"])); |
| 188 | let text = fmt!("{}", res!(out.good("who gone.txt"))); |
| 189 | assert!(text.contains("going"), "the restored file holds what it held: {}", text); |
| 190 | |
| 191 | // And undoing again puts both back the other way. |
| 192 | res!(res!(ore(&root, &["undo"])).good("undo again")); |
| 193 | let now = res!(tree_of(&root)); |
| 194 | assert_eq!(now.get("new.txt").map(|v| v.as_slice()), Some(&b"arrived\n"[..]), |
| 195 | "the redo brings the created file back"); |
| 196 | assert!(!now.contains_key("gone.txt"), "and takes the deleted one away again"); |
| 197 | Ok(()) |
| 198 | } |
| 199 | |
| 200 | /// Undo with nothing to undo says so, and does nothing. |
| 201 | #[test] |
| 202 | fn undo_with_nothing_to_undo_says_so() -> Outcome<()> { |
| 203 | let scratch = res!(Scratch::new("undo_empty")); |
| 204 | let root = res!(scratch.sub("work")); |
| 205 | res!(res!(ore(&root, &["init"])).good("init")); |
| 206 | let out = res!(ore(&root, &["undo"])); |
| 207 | let text = fmt!("{}", res!(out.good("undo"))); |
| 208 | assert!(text.contains("nothing to undo"), "a fresh repository has none: {}", text); |
| 209 | |
| 210 | // A mark nobody made is an error that lists the marks that were made. |
| 211 | res!(write(&root, "a.txt", FIRST)); |
| 212 | res!(res!(ore(&root, &["mark", "A"])).good("mark A")); |
| 213 | let out = res!(ore(&root, &["undo", "nowhere"])); |
| 214 | assert!(!out.ok, "undoing to a mark nobody made fails"); |
| 215 | assert!(out.err.contains("\"A\""), "and lists what there is: {}", out.err); |
| 216 | Ok(()) |
| 217 | } |
| 218 | |
| 219 | /// Undo works on an imported history, where every operation was written by |
| 220 | /// somebody else. |
| 221 | #[test] |
| 222 | fn undo_works_after_an_import() -> Outcome<()> { |
| 223 | let scratch = res!(Scratch::new("undo_import")); |
| 224 | let src = res!(scratch.sub("src")); |
| 225 | let into = res!(scratch.sub("ore")); |
| 226 | |
| 227 | res!(git_init(&src)); |
| 228 | res!(write(&src, "f.txt", b"alpha\nbeta\ngamma\n")); |
| 229 | res!(git(&src, &["add", "."])); |
| 230 | res!(git(&src, &["commit", "--quiet", "--no-verify", "-m", "base"])); |
| 231 | res!(write(&src, "f.txt", b"alpha\nbeta\ngamma\ndelta\n")); |
| 232 | res!(git(&src, &["add", "."])); |
| 233 | res!(git(&src, &["commit", "--quiet", "--no-verify", "-m", "one more line"])); |
| 234 | |
| 235 | res!(res!(ore(&into, &["init"])).good("init")); |
| 236 | res!(res!(ore(&into, &["import", &fmt!("{}", src.display())])).good("import")); |
| 237 | let imported = res!(bytes_of(&into, "f.txt")); |
| 238 | |
| 239 | // An edit of this replica's own, on top of somebody else's history. |
| 240 | res!(write(&into, "f.txt", b"alpha\nbeta\ngamma\ndelta\nepsilon\n")); |
| 241 | res!(res!(ore(&into, &["mark", "mine"])).good("mark mine")); |
| 242 | |
| 243 | res!(res!(ore(&into, &["undo"])).good("undo")); |
| 244 | assert_eq!(res!(bytes_of(&into, "f.txt")), imported, |
| 245 | "the imported state comes back exactly"); |
| 246 | |
| 247 | res!(res!(ore(&into, &["undo"])).good("undo again")); |
| 248 | assert_eq!(res!(bytes_of(&into, "f.txt")), b"alpha\nbeta\ngamma\ndelta\nepsilon\n".to_vec(), |
| 249 | "and the redo puts this replica's edit back"); |
| 250 | Ok(()) |
| 251 | } |
| 252 | |
| 253 | /// An undo of a small edit to a large file costs a small edit. |
| 254 | #[test] |
| 255 | fn undo_costs_what_the_edit_cost() -> Outcome<()> { |
| 256 | let scratch = res!(Scratch::new("undo_cost")); |
| 257 | let root = res!(scratch.sub("work")); |
| 258 | res!(res!(ore(&root, &["init"])).good("init")); |
| 259 | |
| 260 | let mut base = Vec::new(); |
| 261 | for i in 0..1000 { |
| 262 | base.extend_from_slice(fmt!("line {:04} of a file that is quite long\n", i).as_bytes()); |
| 263 | } |
| 264 | res!(write(&root, "big.txt", &base)); |
| 265 | res!(res!(ore(&root, &["mark", "base"])).good("mark base")); |
| 266 | |
| 267 | let mut more = base.clone(); |
| 268 | more.extend_from_slice(b"and one line more\n"); |
| 269 | res!(write(&root, "big.txt", &more)); |
| 270 | res!(res!(ore(&root, &["mark", "appended"])).good("mark appended")); |
| 271 | |
| 272 | let out = res!(ore(&root, &["undo"])); |
| 273 | let text = fmt!("{}", res!(out.good("undo"))); |
| 274 | assert!(text.contains("1 operation, 0 bytes inserted"), |
| 275 | "undoing an append removes bytes and inserts none: {}", text); |
| 276 | assert_eq!(res!(bytes_of(&root, "big.txt")), base, |
| 277 | "and the file is what it was"); |
| 278 | Ok(()) |
| 279 | } |