oxedyne/ore/cli/tests/capture.rs
9.4 KiB, 1 run
created by r2848102244:25, 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 a capture costs, which is the whole reason for finding a real |
| 2 | //! difference. |
| 3 | //! |
| 4 | //! The engine's `diff` module has its own tests and its own oracle. What is |
| 5 | //! unproven until the binary is driven is whether the tool hands it the right |
| 6 | //! two versions and authors what comes back: that two edits far apart are two |
| 7 | //! small operations rather than one covering the file between them, that the |
| 8 | //! bytes still come back exactly, and that `who` can then name each region's |
| 9 | //! own author. |
| 10 | |
| 11 | mod support; |
| 12 | |
| 13 | use support::{ |
| 14 | ore, |
| 15 | runs_in, |
| 16 | tree_of, |
| 17 | write, |
| 18 | Scratch, |
| 19 | }; |
| 20 | |
| 21 | use oxedyne_fe2o3_core::prelude::*; |
| 22 | |
| 23 | |
| 24 | /// Builds a file of numbered lines, which is what a source file looks like to a |
| 25 | /// line level difference. |
| 26 | fn lines(n: usize) -> Vec<u8> { |
| 27 | let mut out = Vec::new(); |
| 28 | for i in 0..n { |
| 29 | out.extend_from_slice(fmt!("line {:04} of the file, with enough on it to matter\n", i) |
| 30 | .as_bytes()); |
| 31 | } |
| 32 | out |
| 33 | } |
| 34 | |
| 35 | /// Returns the `edited` line the capture report wrote for a path. |
| 36 | fn edited_line(text: &str, path: &str) |
| 37 | -> Outcome<String> |
| 38 | { |
| 39 | for line in text.lines() { |
| 40 | let trimmed = line.trim_start(); |
| 41 | if let Some(rest) = trimmed.strip_prefix("edited ") { |
| 42 | if rest.starts_with(path) { |
| 43 | return Ok(fmt!("{}", trimmed)); |
| 44 | } |
| 45 | } |
| 46 | } |
| 47 | Err(err!("The report says nothing about editing {:?}: {}", path, text; Test, Missing)) |
| 48 | } |
| 49 | |
| 50 | /// Returns the two counts an `edited` line carries: operations, and bytes |
| 51 | /// inserted. |
| 52 | fn counts(line: &str) |
| 53 | -> Outcome<(usize, usize)> |
| 54 | { |
| 55 | let fields: Vec<&str> = line.split_whitespace().collect(); |
| 56 | // `edited <path> <ops> operation(s), <n> byte(s) inserted` |
| 57 | let at = match fields.iter().position(|f| f.starts_with("operation")) { |
| 58 | Some(i) if i > 0 => i, |
| 59 | _ => return Err(err!("The line {:?} carries no operation count.", line; Test)), |
| 60 | }; |
| 61 | let ops = match fields[at - 1].parse::<usize>() { |
| 62 | Ok(n) => n, |
| 63 | Err(e) => return Err(err!(e, "The line {:?} has no number of operations.", line; Test)), |
| 64 | }; |
| 65 | let at = match fields.iter().position(|f| f.starts_with("byte")) { |
| 66 | Some(i) if i > 0 => i, |
| 67 | _ => return Err(err!("The line {:?} carries no byte count.", line; Test)), |
| 68 | }; |
| 69 | let inserted = match fields[at - 1].parse::<usize>() { |
| 70 | Ok(n) => n, |
| 71 | Err(e) => return Err(err!(e, "The line {:?} has no number of bytes.", line; Test)), |
| 72 | }; |
| 73 | Ok((ops, inserted)) |
| 74 | } |
| 75 | |
| 76 | /// Two edits at opposite ends of a file are two small operations, and between |
| 77 | /// them they insert what was typed rather than what lies between. |
| 78 | #[test] |
| 79 | fn two_distant_edits_are_two_small_operations() -> Outcome<()> { |
| 80 | let scratch = res!(Scratch::new("distant")); |
| 81 | let root = res!(scratch.sub("work")); |
| 82 | res!(res!(ore(&root, &["init"])).good("init")); |
| 83 | |
| 84 | let base = lines(400); |
| 85 | res!(write(&root, "a.txt", &base)); |
| 86 | res!(res!(ore(&root, &["mark", "base"])).good("mark base")); |
| 87 | |
| 88 | // One word changed near the top and one near the bottom, thousands of bytes |
| 89 | // apart and both untouched in between. |
| 90 | let mut edited = fmt!("{}", String::from_utf8_lossy(&base)); |
| 91 | edited = edited.replace("line 0005 of the file", "line 0005 OF THE FILE"); |
| 92 | edited = edited.replace("line 0395 of the file", "line 0395 OF THE FILE"); |
| 93 | assert_ne!(edited.as_bytes(), base.as_slice(), "the edits changed something"); |
| 94 | res!(write(&root, "a.txt", edited.as_bytes())); |
| 95 | |
| 96 | let out = res!(ore(&root, &["mark", "edited"])); |
| 97 | let text = fmt!("{}", res!(out.good("mark edited"))); |
| 98 | let line = res!(edited_line(&text, "a.txt")); |
| 99 | let (ops, inserted) = res!(counts(&line)); |
| 100 | assert_eq!(ops, 2, "one operation per edit, not one for the pair: {}", line); |
| 101 | // The file is over twenty kilobytes and the two edits are a handful of bytes |
| 102 | // each. A capture that trimmed a common prefix and suffix would have carried |
| 103 | // everything between them. |
| 104 | assert!(base.len() > 20_000, "the file is large: {} bytes", base.len()); |
| 105 | assert!(inserted < 200, |
| 106 | "the operations carry the edits and not the file between them: {}", line); |
| 107 | |
| 108 | // And the file is exactly what was written. |
| 109 | let now = res!(tree_of(&root)); |
| 110 | assert_eq!(now.get("a.txt").map(|v| v.as_slice()), Some(edited.as_bytes()), |
| 111 | "the bytes come back exactly"); |
| 112 | let out = res!(ore(&root, &["log"])); |
| 113 | let text = fmt!("{}", res!(out.good("log"))); |
| 114 | assert!(text.contains("captured nothing"), |
| 115 | "and nothing is left over for the next verb: {}", text); |
| 116 | Ok(()) |
| 117 | } |
| 118 | |
| 119 | /// Two edits to two regions are owned by two operations, which is what the |
| 120 | /// finer difference buys a reader of `who`. |
| 121 | #[test] |
| 122 | fn who_attributes_two_regions_to_two_operations() -> Outcome<()> { |
| 123 | let scratch = res!(Scratch::new("regions")); |
| 124 | let root = res!(scratch.sub("work")); |
| 125 | res!(res!(ore(&root, &["init"])).good("init")); |
| 126 | |
| 127 | let base = lines(200); |
| 128 | res!(write(&root, "a.txt", &base)); |
| 129 | res!(res!(ore(&root, &["mark", "base"])).good("mark base")); |
| 130 | |
| 131 | let edited = fmt!("{}", String::from_utf8_lossy(&base)) |
| 132 | .replace("line 0010 of the file", "line 0010 NEAR THE TOP") |
| 133 | .replace("line 0190 of the file", "line 0190 NEAR THE END"); |
| 134 | res!(write(&root, "a.txt", edited.as_bytes())); |
| 135 | res!(res!(ore(&root, &["mark", "edited"])).good("mark edited")); |
| 136 | |
| 137 | let out = res!(ore(&root, &["who", "a.txt"])); |
| 138 | let text = fmt!("{}", res!(out.good("who a.txt"))); |
| 139 | let runs = runs_in(&text); |
| 140 | assert!(runs.len() >= 4, "the file is in several runs now: {}", text); |
| 141 | |
| 142 | let top = match runs.iter().find(|r| r.text.contains("NEAR THE TOP")) { |
| 143 | Some(r) => r, |
| 144 | None => return Err(err!("No run holds the first edit: {}", text; Test, Missing)), |
| 145 | }; |
| 146 | let end = match runs.iter().find(|r| r.text.contains("NEAR THE END")) { |
| 147 | Some(r) => r, |
| 148 | None => return Err(err!("No run holds the second edit: {}", text; Test, Missing)), |
| 149 | }; |
| 150 | assert_ne!(top.op, end.op, |
| 151 | "the two regions are owned by two operations, not one: {}", text); |
| 152 | assert!(top.from < end.from, "and they are where they were typed: {}", text); |
| 153 | |
| 154 | // The lines nobody touched still belong to the operation that first wrote |
| 155 | // them, which is what a splice covering both edits would have taken over. |
| 156 | // The first run is that operation, since the file opens with a line neither |
| 157 | // edit went near. |
| 158 | let base_op = fmt!("{}", runs[0].op); |
| 159 | assert_ne!(base_op, top.op, "the first edit is its own operation: {}", text); |
| 160 | assert_ne!(base_op, end.op, "and so is the second: {}", text); |
| 161 | let held = runs.iter().any(|r| r.op == base_op && r.from > top.from && r.from < end.from); |
| 162 | assert!(held, "and the untouched middle is still the first writer's: {}", text); |
| 163 | Ok(()) |
| 164 | } |
| 165 | |
| 166 | /// A file rewritten from end to end is captured as one splice, which is what |
| 167 | /// the difference falls back to when the two versions have nothing left in |
| 168 | /// common, and the bytes still come back exactly. |
| 169 | #[test] |
| 170 | fn a_whole_file_rewrite_still_round_trips() -> Outcome<()> { |
| 171 | let scratch = res!(Scratch::new("rewrite")); |
| 172 | let root = res!(scratch.sub("work")); |
| 173 | res!(res!(ore(&root, &["init"])).good("init")); |
| 174 | |
| 175 | // Enough distinct lines on both sides that the line level edit script would |
| 176 | // exceed its budget, which is the path that falls back to one splice. |
| 177 | let mut before = Vec::new(); |
| 178 | for i in 0..900 { |
| 179 | before.extend_from_slice(fmt!("before {:04}\n", i).as_bytes()); |
| 180 | } |
| 181 | let mut after = Vec::new(); |
| 182 | for i in 0..900 { |
| 183 | after.extend_from_slice(fmt!("after {:04} entirely\n", i).as_bytes()); |
| 184 | } |
| 185 | res!(write(&root, "a.txt", &before)); |
| 186 | res!(res!(ore(&root, &["mark", "before"])).good("mark before")); |
| 187 | res!(write(&root, "a.txt", &after)); |
| 188 | |
| 189 | let out = res!(ore(&root, &["mark", "after"])); |
| 190 | let text = fmt!("{}", res!(out.good("mark after"))); |
| 191 | let line = res!(edited_line(&text, "a.txt")); |
| 192 | let (ops, _) = res!(counts(&line)); |
| 193 | assert_eq!(ops, 1, "nothing in common is one splice: {}", line); |
| 194 | |
| 195 | let now = res!(tree_of(&root)); |
| 196 | assert_eq!(now.get("a.txt").map(|v| v.as_slice()), Some(after.as_slice()), |
| 197 | "and the rewrite comes back byte for byte"); |
| 198 | |
| 199 | // Both states are still reachable, since the log only grows. |
| 200 | res!(res!(ore(&root, &["back", "before"])).good("back before")); |
| 201 | let now = res!(tree_of(&root)); |
| 202 | assert_eq!(now.get("a.txt").map(|v| v.as_slice()), Some(before.as_slice())); |
| 203 | Ok(()) |
| 204 | } |
| 205 | |
| 206 | /// A small edit to a large file already in the history stores the small edit. |
| 207 | /// |
| 208 | /// This is the storage claim stated as an assertion: the operation that records |
| 209 | /// one appended line carries that line, and the segment the log grew by is a |
| 210 | /// long way short of the file. |
| 211 | #[test] |
| 212 | fn appending_one_line_stores_one_line() -> Outcome<()> { |
| 213 | let scratch = res!(Scratch::new("append")); |
| 214 | let root = res!(scratch.sub("work")); |
| 215 | res!(res!(ore(&root, &["init"])).good("init")); |
| 216 | |
| 217 | let base = lines(2000); |
| 218 | res!(write(&root, "big.txt", &base)); |
| 219 | res!(res!(ore(&root, &["mark", "base"])).good("mark base")); |
| 220 | let grew = res!(log_size(&root)); |
| 221 | |
| 222 | let mut more = base.clone(); |
| 223 | more.extend_from_slice(b"one line appended at the very end\n"); |
| 224 | res!(write(&root, "big.txt", &more)); |
| 225 | let out = res!(ore(&root, &["mark", "appended"])); |
| 226 | let text = fmt!("{}", res!(out.good("mark appended"))); |
| 227 | let line = res!(edited_line(&text, "big.txt")); |
| 228 | let (ops, inserted) = res!(counts(&line)); |
| 229 | assert_eq!(ops, 1, "one appended line is one operation: {}", line); |
| 230 | assert_eq!(inserted, 34, "carrying the line and nothing else: {}", line); |
| 231 | |
| 232 | // The log grew by the line, its record and a mark, not by the file. |
| 233 | let after = res!(log_size(&root)); |
| 234 | let added = after - grew; |
| 235 | assert!(added < 1_000, |
| 236 | "the log grew by {} bytes for a {} byte file", added, base.len()); |
| 237 | Ok(()) |
| 238 | } |
| 239 | |
| 240 | /// Returns the total size of every segment file of a repository. |
| 241 | fn log_size(root: &std::path::Path) |
| 242 | -> Outcome<u64> |
| 243 | { |
| 244 | let mut total = 0u64; |
| 245 | for entry in res!(std::fs::read_dir(root.join(".ore").join("log"))) { |
| 246 | let entry = res!(entry); |
| 247 | total += res!(entry.metadata()).len(); |
| 248 | } |
| 249 | Ok(total) |
| 250 | } |