oxedyne/ore/cli/tests/basics.rs
13.9 KiB, 20 runs
created by r2848102244:23, 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 | //! The verbs, driven as a person drives them: through the compiled binary, in a |
| 2 | //! directory of its own. |
| 3 | //! |
| 4 | //! These tests treat the binary as the unit, because it is. Everything below it |
| 5 | //! -- the log, the sequence, the two formats -- has its own tests in the engine; |
| 6 | //! what is unproven until it is run this way is whether the tool assembles them |
| 7 | //! into something that keeps a working copy. |
| 8 | |
| 9 | mod support; |
| 10 | |
| 11 | use support::{ |
| 12 | marks, |
| 13 | operations, |
| 14 | ore, |
| 15 | tree_of, |
| 16 | write, |
| 17 | Scratch, |
| 18 | }; |
| 19 | |
| 20 | use oxedyne_fe2o3_core::prelude::*; |
| 21 | |
| 22 | use std::fs; |
| 23 | |
| 24 | |
| 25 | /// The bytes the first mark holds. |
| 26 | const FIRST_A: &[u8] = b"hello\nworld\n"; |
| 27 | /// The bytes the second mark holds. |
| 28 | const SECOND_A: &[u8] = b"hello\nbrave\nworld\n"; |
| 29 | /// A second file, unchanged throughout, so that a verb that touches everything |
| 30 | /// is caught doing it. |
| 31 | const B: &[u8] = b"one\ntwo\nthree\n"; |
| 32 | |
| 33 | |
| 34 | /// A repository, two marks, a journey back to the first and forward to the |
| 35 | /// second, and an edit after the journey. |
| 36 | #[test] |
| 37 | fn marks_and_time_travel() -> Outcome<()> { |
| 38 | let scratch = res!(Scratch::new("marks")); |
| 39 | let root = res!(scratch.sub("work")); |
| 40 | |
| 41 | res!(res!(ore(&root, &["init"])).good("init")); |
| 42 | res!(write(&root, "a.txt", FIRST_A)); |
| 43 | res!(write(&root, "b.txt", B)); |
| 44 | res!(write(&root, "sub/c.txt", b"deep\n")); |
| 45 | let out = res!(ore(&root, &["mark", "first"])); |
| 46 | let text = fmt!("{}", res!(out.good("mark first"))); |
| 47 | assert!(text.contains("3 created"), "the first mark captures three files: {}", text); |
| 48 | |
| 49 | res!(write(&root, "a.txt", SECOND_A)); |
| 50 | let out = res!(ore(&root, &["mark", "second"])); |
| 51 | let text = fmt!("{}", res!(out.good("mark second"))); |
| 52 | assert!(text.contains("1 edited"), "the second mark captures one edit: {}", text); |
| 53 | |
| 54 | // The log names both marks, newest first. |
| 55 | let out = res!(ore(&root, &["log"])); |
| 56 | let text = fmt!("{}", res!(out.good("log"))); |
| 57 | let at_first = match text.find("\"first\"") { |
| 58 | Some(i) => i, |
| 59 | None => return Err(err!("The log does not name the first mark: {}", text; Test)), |
| 60 | }; |
| 61 | let at_second = match text.find("\"second\"") { |
| 62 | Some(i) => i, |
| 63 | None => return Err(err!("The log does not name the second mark: {}", text; Test)), |
| 64 | }; |
| 65 | assert!(at_second < at_first, "the log is newest first: {}", text); |
| 66 | // What the history holds before the journey, so that the claim below is that |
| 67 | // the log grew and not that it reached a particular number. |
| 68 | let was = res!(operations(&text)); |
| 69 | |
| 70 | // Back to the first mark, and the bytes are exactly what they were. |
| 71 | res!(res!(ore(&root, &["back", "first"])).good("back first")); |
| 72 | let now = res!(tree_of(&root)); |
| 73 | assert_eq!(now.get("a.txt").map(|v| v.as_slice()), Some(FIRST_A)); |
| 74 | assert_eq!(now.get("b.txt").map(|v| v.as_slice()), Some(B)); |
| 75 | assert_eq!(now.get("sub/c.txt").map(|v| v.as_slice()), Some(&b"deep\n"[..])); |
| 76 | |
| 77 | // And forward to the second, which is the state it was left in. |
| 78 | res!(res!(ore(&root, &["back", "second"])).good("back second")); |
| 79 | let now = res!(tree_of(&root)); |
| 80 | assert_eq!(now.get("a.txt").map(|v| v.as_slice()), Some(SECOND_A)); |
| 81 | |
| 82 | // An edit made after a journey is captured like any other. |
| 83 | res!(write(&root, "a.txt", b"hello\nbrave\nnew\nworld\n")); |
| 84 | let out = res!(ore(&root, &["mark", "third"])); |
| 85 | let text = fmt!("{}", res!(out.good("mark third"))); |
| 86 | assert!(text.contains("1 edited"), "the edit after the journey is captured: {}", text); |
| 87 | let out = res!(ore(&root, &["log"])); |
| 88 | let text = fmt!("{}", res!(out.good("log"))); |
| 89 | assert!(text.contains("captured nothing"), |
| 90 | "nothing is left to capture straight after a mark: {}", text); |
| 91 | for name in ["\"first\"", "\"second\"", "\"third\""] { |
| 92 | assert!(text.contains(name), "the log names {}: {}", name, text); |
| 93 | } |
| 94 | // Nothing was rewritten: the journey and the mark after it added operations |
| 95 | // and took none away. A total is not written down here because every command |
| 96 | // that appends anything now ends by naming the point it reached, so the total |
| 97 | // is a function of how many commands the test happens to run; what is being |
| 98 | // claimed is that the log only grows, and that is a comparison. |
| 99 | let now = res!(operations(&text)); |
| 100 | assert!(now > was, |
| 101 | "the log only grows: it held {} operations before the journey and {} after: {}", |
| 102 | was, now, text); |
| 103 | Ok(()) |
| 104 | } |
| 105 | |
| 106 | /// The log shows the marks a person named, and `--auto` shows the ones the tool |
| 107 | /// wrote as well. |
| 108 | /// |
| 109 | /// Every command that appends anything ends by naming the point it reached, so |
| 110 | /// the plain listing would otherwise bury the handful somebody chose under |
| 111 | /// several hundred nobody did. What is asserted here is the distinction itself: |
| 112 | /// the named mark is in both listings, the automatic one is in exactly one of |
| 113 | /// them, and the plain listing says how many it is not showing. |
| 114 | #[test] |
| 115 | fn the_log_shows_named_marks_and_auto_shows_the_rest() -> Outcome<()> { |
| 116 | let scratch = res!(Scratch::new("log_auto")); |
| 117 | let root = res!(scratch.sub("work")); |
| 118 | res!(res!(ore(&root, &["init"])).good("init")); |
| 119 | res!(write(&root, "a.txt", b"alpha\n")); |
| 120 | res!(res!(ore(&root, &["mark", "release"])).good("mark")); |
| 121 | // Three commands that append and name nothing, so there are marks to hide. |
| 122 | for i in 0..3 { |
| 123 | res!(write(&root, "a.txt", &fmt!("alpha\nline {}\n", i).into_bytes())); |
| 124 | res!(res!(ore(&root, &["note", "a.txt:1", "a remark"])).good("note")); |
| 125 | } |
| 126 | |
| 127 | let out = res!(ore(&root, &["log"])); |
| 128 | let plain = fmt!("{}", res!(out.good("log"))); |
| 129 | let (named, automatic) = marks(&plain); |
| 130 | assert_eq!(named, 1, "the mark somebody named is listed: {}", plain); |
| 131 | assert_eq!(automatic, 0, "and the ones the tool wrote are not: {}", plain); |
| 132 | assert!(plain.contains("`ore log --auto` lists those too"), |
| 133 | "the listing says what it is not showing, rather than leaving it to be \ |
| 134 | noticed: {}", plain); |
| 135 | |
| 136 | let out = res!(ore(&root, &["log", "--auto"])); |
| 137 | let all = fmt!("{}", res!(out.good("log --auto"))); |
| 138 | let (named, automatic) = marks(&all); |
| 139 | assert_eq!(named, 1, "the named mark is in both listings: {}", all); |
| 140 | assert!(automatic >= 3, |
| 141 | "and --auto shows one for each command that appended anything: {}", all); |
| 142 | assert!(!all.contains("`ore log --auto` lists those too"), |
| 143 | "a listing showing everything has nothing to offer: {}", all); |
| 144 | Ok(()) |
| 145 | } |
| 146 | |
| 147 | |
| 148 | /// A file deleted from the working copy is recorded as gone, and comes back on |
| 149 | /// the journey to a mark that had it. |
| 150 | #[test] |
| 151 | fn a_deletion_is_an_operation() -> Outcome<()> { |
| 152 | let scratch = res!(Scratch::new("delete")); |
| 153 | let root = res!(scratch.sub("work")); |
| 154 | res!(res!(ore(&root, &["init"])).good("init")); |
| 155 | res!(write(&root, "keep.txt", b"kept\n")); |
| 156 | res!(write(&root, "gone.txt", b"going\n")); |
| 157 | res!(res!(ore(&root, &["mark", "both"])).good("mark both")); |
| 158 | |
| 159 | res!(fs::remove_file(root.join("gone.txt"))); |
| 160 | let out = res!(ore(&root, &["mark", "one"])); |
| 161 | let text = fmt!("{}", res!(out.good("mark one"))); |
| 162 | assert!(text.contains("1 deleted"), "the deletion is captured: {}", text); |
| 163 | assert!(!root.join("gone.txt").exists()); |
| 164 | |
| 165 | res!(res!(ore(&root, &["back", "both"])).good("back both")); |
| 166 | let now = res!(tree_of(&root)); |
| 167 | assert_eq!(now.get("gone.txt").map(|v| v.as_slice()), Some(&b"going\n"[..])); |
| 168 | Ok(()) |
| 169 | } |
| 170 | |
| 171 | /// The ignore file keeps files out of the history, and a dotfile it says |
| 172 | /// nothing about goes in like any other. |
| 173 | #[test] |
| 174 | fn ignored_files_are_not_captured() -> Outcome<()> { |
| 175 | let scratch = res!(Scratch::new("ignore")); |
| 176 | let root = res!(scratch.sub("work")); |
| 177 | res!(res!(ore(&root, &["init"])).good("init")); |
| 178 | res!(write(&root, ".oreignore", b"build/\n*.tmp\nnotes.txt\n")); |
| 179 | res!(write(&root, "kept.txt", b"kept\n")); |
| 180 | res!(write(&root, "notes.txt", b"private\n")); |
| 181 | res!(write(&root, "scratch.tmp", b"rubbish\n")); |
| 182 | res!(write(&root, "build/out.bin", b"artefact\n")); |
| 183 | res!(write(&root, ".hidden", b"dotfile\n")); |
| 184 | let out = res!(ore(&root, &["mark", "one"])); |
| 185 | let text = fmt!("{}", res!(out.good("mark one"))); |
| 186 | // The ignore file itself is captured, so that its rules travel with the |
| 187 | // repository; everything it names stays out; and a dotfile no rule mentions |
| 188 | // is content like anything else. |
| 189 | assert!(text.contains("3 created"), |
| 190 | "the kept file, the dotfile and the rules: {}", text); |
| 191 | assert!(text.contains("kept.txt"), "the kept file is the right one: {}", text); |
| 192 | assert!(text.contains(".oreignore"), "and the rules are recorded: {}", text); |
| 193 | assert!(text.contains(".hidden"), "and the dotfile with them: {}", text); |
| 194 | // The files left alone are still where they were. |
| 195 | assert!(root.join("notes.txt").exists()); |
| 196 | assert!(root.join("build/out.bin").exists()); |
| 197 | Ok(()) |
| 198 | } |
| 199 | |
| 200 | /// `who` accounts for every byte of a file, run by run. |
| 201 | #[test] |
| 202 | fn who_accounts_for_the_bytes() -> Outcome<()> { |
| 203 | let scratch = res!(Scratch::new("who")); |
| 204 | let root = res!(scratch.sub("work")); |
| 205 | res!(res!(ore(&root, &["init"])).good("init")); |
| 206 | res!(write(&root, "a.txt", FIRST_A)); |
| 207 | res!(res!(ore(&root, &["mark", "first"])).good("mark first")); |
| 208 | res!(write(&root, "a.txt", SECOND_A)); |
| 209 | res!(res!(ore(&root, &["mark", "second"])).good("mark second")); |
| 210 | |
| 211 | let out = res!(ore(&root, &["who", "a.txt"])); |
| 212 | let text = fmt!("{}", res!(out.good("who a.txt"))); |
| 213 | assert!(text.contains("18 bytes in 3 runs"), |
| 214 | "the middle of the file came from the second edit: {}", text); |
| 215 | assert!(text.contains("hello\\n"), "the runs show their content: {}", text); |
| 216 | assert!(text.contains("brave\\n"), "including the inserted stretch: {}", text); |
| 217 | // One replica wrote all of it, so every run names the same one. A second |
| 218 | // replica's runs are proven by the import tests, where git's authors supply |
| 219 | // the second writer. |
| 220 | let mut replicas = std::collections::BTreeSet::new(); |
| 221 | for line in text.lines() { |
| 222 | if let Some(at) = line.find(" r") { |
| 223 | let rest = &line[at + 2..]; |
| 224 | let name = rest.split(':').next().unwrap_or(""); |
| 225 | replicas.insert(fmt!("{}", name)); |
| 226 | } |
| 227 | } |
| 228 | assert_eq!(replicas.len(), 1, "one writer, one replica: {:?}", replicas); |
| 229 | Ok(()) |
| 230 | } |
| 231 | |
| 232 | /// `flags` says the history is clean when nothing concurrent has happened. |
| 233 | #[test] |
| 234 | fn flags_are_clean_on_a_linear_history() -> Outcome<()> { |
| 235 | let scratch = res!(Scratch::new("clean")); |
| 236 | let root = res!(scratch.sub("work")); |
| 237 | res!(res!(ore(&root, &["init"])).good("init")); |
| 238 | res!(write(&root, "a.txt", FIRST_A)); |
| 239 | res!(res!(ore(&root, &["mark", "first"])).good("mark first")); |
| 240 | res!(write(&root, "a.txt", SECOND_A)); |
| 241 | res!(res!(ore(&root, &["mark", "second"])).good("mark second")); |
| 242 | let out = res!(ore(&root, &["flags"])); |
| 243 | let text = fmt!("{}", res!(out.good("flags"))); |
| 244 | assert!(text.contains("clean"), "a linear history flags nothing: {}", text); |
| 245 | Ok(()) |
| 246 | } |
| 247 | |
| 248 | /// Initialising twice is refused, since a second replica identifier for one |
| 249 | /// working copy would let one writer spend another's counters. |
| 250 | #[test] |
| 251 | fn a_second_init_is_refused() -> Outcome<()> { |
| 252 | let scratch = res!(Scratch::new("twice")); |
| 253 | let root = res!(scratch.sub("work")); |
| 254 | res!(res!(ore(&root, &["init"])).good("init")); |
| 255 | let out = res!(ore(&root, &["init"])); |
| 256 | assert!(!out.ok, "the second init fails"); |
| 257 | assert!(out.err.contains("already an Ore repository"), |
| 258 | "and says why: {}", out.err); |
| 259 | Ok(()) |
| 260 | } |
| 261 | |
| 262 | /// A verb outside a repository says so, and says what to do about it. |
| 263 | #[test] |
| 264 | fn a_verb_outside_a_repository_says_so() -> Outcome<()> { |
| 265 | let scratch = res!(Scratch::new("nowhere")); |
| 266 | let root = res!(scratch.sub("work")); |
| 267 | for verb in [ |
| 268 | vec!["log"], |
| 269 | vec!["flags"], |
| 270 | vec!["mark", "x"], |
| 271 | vec!["who", "a.txt"], |
| 272 | vec!["back", "x"], |
| 273 | vec!["undo"], |
| 274 | ] { |
| 275 | let out = res!(ore(&root, &verb)); |
| 276 | assert!(!out.ok, "`ore {}` outside a repository fails", verb.join(" ")); |
| 277 | assert!(out.err.contains("ore init"), |
| 278 | "and says to run init: {}", out.err); |
| 279 | } |
| 280 | Ok(()) |
| 281 | } |
| 282 | |
| 283 | /// A segment cut short is an error that names the file it read. |
| 284 | #[test] |
| 285 | fn a_truncated_segment_names_itself() -> Outcome<()> { |
| 286 | let scratch = res!(Scratch::new("truncated")); |
| 287 | let root = res!(scratch.sub("work")); |
| 288 | res!(res!(ore(&root, &["init"])).good("init")); |
| 289 | res!(write(&root, "a.txt", FIRST_A)); |
| 290 | res!(res!(ore(&root, &["mark", "first"])).good("mark first")); |
| 291 | |
| 292 | let path = root.join(".ore").join("log").join("000000.seg"); |
| 293 | let bytes = res!(fs::read(&path)); |
| 294 | assert!(bytes.len() > 8, "the segment holds records"); |
| 295 | res!(fs::write(&path, &bytes[..bytes.len() - 3])); |
| 296 | |
| 297 | let out = res!(ore(&root, &["log"])); |
| 298 | assert!(!out.ok, "a truncated segment is an error"); |
| 299 | assert!(out.err.contains("000000.seg"), |
| 300 | "and the error names the file: {}", out.err); |
| 301 | Ok(()) |
| 302 | } |
| 303 | |
| 304 | /// A log longer than one segment is written across several and replayed from |
| 305 | /// all of them. |
| 306 | #[test] |
| 307 | fn segments_rotate_and_replay() -> Outcome<()> { |
| 308 | let scratch = res!(Scratch::new("rotate")); |
| 309 | let root = res!(scratch.sub("work")); |
| 310 | res!(res!(ore(&root, &["init"])).good("init")); |
| 311 | // One splice carrying more than the segment threshold, so the next append |
| 312 | // has to start a second file. |
| 313 | let big: Vec<u8> = (0..(1usize << 20) + 4096).map(|i| (i % 251) as u8).collect(); |
| 314 | res!(write(&root, "big.dat", &big)); |
| 315 | res!(res!(ore(&root, &["mark", "big"])).good("mark big")); |
| 316 | res!(write(&root, "small.txt", b"after the rotation\n")); |
| 317 | res!(res!(ore(&root, &["mark", "after"])).good("mark after")); |
| 318 | |
| 319 | let dir = root.join(".ore").join("log"); |
| 320 | let mut names: Vec<String> = Vec::new(); |
| 321 | for entry in res!(fs::read_dir(&dir)) { |
| 322 | names.push(res!(entry).file_name().to_string_lossy().into_owned()); |
| 323 | } |
| 324 | names.sort(); |
| 325 | assert!(names.len() >= 2, "the log rotated onto a second segment: {:?}", names); |
| 326 | |
| 327 | // Everything replays: both marks are there and the big file is intact. |
| 328 | let out = res!(ore(&root, &["log"])); |
| 329 | let text = fmt!("{}", res!(out.good("log"))); |
| 330 | assert!(text.contains("\"big\"") && text.contains("\"after\""), |
| 331 | "both marks survive the rotation: {}", text); |
| 332 | let now = res!(tree_of(&root)); |
| 333 | assert_eq!(now.get("big.dat").map(|v| v.len()), Some(big.len())); |
| 334 | assert_eq!(now.get("big.dat").map(|v| v.as_slice()), Some(big.as_slice())); |
| 335 | Ok(()) |
| 336 | } |
| 337 | |
| 338 | /// A mark nobody made is an error that lists the marks that were made. |
| 339 | #[test] |
| 340 | fn an_unknown_mark_lists_the_known_ones() -> Outcome<()> { |
| 341 | let scratch = res!(Scratch::new("nomark")); |
| 342 | let root = res!(scratch.sub("work")); |
| 343 | res!(res!(ore(&root, &["init"])).good("init")); |
| 344 | res!(write(&root, "a.txt", FIRST_A)); |
| 345 | res!(res!(ore(&root, &["mark", "first"])).good("mark first")); |
| 346 | let out = res!(ore(&root, &["back", "second"])); |
| 347 | assert!(!out.ok, "going back to a mark nobody made fails"); |
| 348 | assert!(out.err.contains("\"first\""), "and lists what there is: {}", out.err); |
| 349 | Ok(()) |
| 350 | } |
| 351 |