oxedyne/ore/cli/tests/review.rs
13.8 KiB, 1 run
created by r2848102244:136, 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 review surface: where a flag's content is, what the last sync brought, |
| 2 | //! and which flags have been put down. |
| 3 | //! |
| 4 | //! Each of the three answers something the self-hosting trial found missing -- |
| 5 | //! flag coordinates a reviewer could act on, a delta since the last sync, and a |
| 6 | //! reviewed state so that the flag list stops growing |
| 7 | //! (`self_hosting_trial.md` §2.2, §2.5, §3 item 3). |
| 8 | //! |
| 9 | //! # Coordinates are asserted by construction |
| 10 | //! |
| 11 | //! Every file a coordinate test uses is written here, line by line, so the line |
| 12 | //! a flag should name is known before the tool is run rather than read off what |
| 13 | //! the tool said. A test that copied the output would pass over a coordinate |
| 14 | //! that was off by one in every case. |
| 15 | |
| 16 | mod support; |
| 17 | |
| 18 | use support::{ |
| 19 | ore, |
| 20 | write, |
| 21 | Ran, |
| 22 | Scratch, |
| 23 | }; |
| 24 | |
| 25 | use oxedyne_fe2o3_core::prelude::*; |
| 26 | |
| 27 | use std::fs; |
| 28 | use std::path::{ |
| 29 | Path, |
| 30 | PathBuf, |
| 31 | }; |
| 32 | |
| 33 | |
| 34 | /// Five lines, so that the third is unambiguously the third. |
| 35 | const FIVE: &[u8] = b"line one\nline two\nline three\nline four\nline five\n"; |
| 36 | |
| 37 | /// The same five lines with the third replaced, as one replica writes it. |
| 38 | const FIVE_A: &[u8] = b"line one\nline two\nTHIRD FROM A\nline four\nline five\n"; |
| 39 | |
| 40 | /// And as the other writes it. |
| 41 | const FIVE_B: &[u8] = b"line one\nline two\nthird from b\nline four\nline five\n"; |
| 42 | |
| 43 | |
| 44 | /// Runs `ore sync <other>` and fails the test if it did not succeed. |
| 45 | fn synced(dir: &Path, other: &Path) |
| 46 | -> Outcome<String> |
| 47 | { |
| 48 | let out = res!(ore(dir, &["sync", &fmt!("{}", other.display())])); |
| 49 | Ok(fmt!("{}", res!(out.good("sync")))) |
| 50 | } |
| 51 | |
| 52 | /// Runs a verb and fails the test if it did not succeed. |
| 53 | fn ran(dir: &Path, args: &[&str]) |
| 54 | -> Outcome<String> |
| 55 | { |
| 56 | let out: Ran = res!(ore(dir, args)); |
| 57 | Ok(fmt!("{}", res!(out.good(&args.join(" "))))) |
| 58 | } |
| 59 | |
| 60 | /// Makes two replicas of one repository holding `text` at `file`. |
| 61 | fn twinned(scratch: &Scratch, file: &str, text: &[u8]) |
| 62 | -> Outcome<(PathBuf, PathBuf)> |
| 63 | { |
| 64 | let a = res!(scratch.sub("a")); |
| 65 | let b = res!(scratch.sub("b")); |
| 66 | res!(res!(ore(&a, &["init"])).good("init")); |
| 67 | res!(write(&a, file, text)); |
| 68 | res!(res!(ore(&a, &["mark", "base"])).good("mark")); |
| 69 | res!(res!(ore(&b, &["init"])).good("init")); |
| 70 | res!(synced(&b, &a)); |
| 71 | Ok((a, b)) |
| 72 | } |
| 73 | |
| 74 | |
| 75 | /// A flag says where in the file its content is, and the line it names is the |
| 76 | /// line the edit was planted on. |
| 77 | /// |
| 78 | /// Two replicas replace the third of five lines. One prevails and the other |
| 79 | /// yields, and the reviewer's question is where to look: the answer is the |
| 80 | /// third line of the file that is on the disk, whichever of them prevailed. |
| 81 | #[test] |
| 82 | fn a_flag_names_the_line_its_content_is_on() -> Outcome<()> { |
| 83 | let scratch = res!(Scratch::new("review_line")); |
| 84 | let (a, b) = res!(twinned(&scratch, "src/util.rs", FIVE)); |
| 85 | res!(write(&a, "src/util.rs", FIVE_A)); |
| 86 | res!(write(&b, "src/util.rs", FIVE_B)); |
| 87 | res!(synced(&b, &a)); |
| 88 | |
| 89 | let text = res!(ran(&b, &["flags"])); |
| 90 | assert!(text.contains("yielded"), "one of the two edits yielded: {}", text); |
| 91 | assert!(text.contains("the region as the file now holds it is at src/util.rs:3"), |
| 92 | "the contended region is the third line, which is where it was planted: {}", text); |
| 93 | // And the file really does hold one writer's third line there, so the |
| 94 | // coordinate is not merely a plausible number. |
| 95 | let held = res!(fs::read(b.join("src/util.rs"))); |
| 96 | let third = match String::from_utf8_lossy(&held).lines().nth(2) { |
| 97 | Some(l) => fmt!("{}", l), |
| 98 | None => return Err(err!("The file has no third line."; Test, Missing)), |
| 99 | }; |
| 100 | assert!(third == "THIRD FROM A" || third == "third from b", |
| 101 | "the third line is one writer's whole line, not a coordinate pointing at \ |
| 102 | nothing: {:?}", third); |
| 103 | |
| 104 | // The other end says the same thing, a flag being a function of the operation |
| 105 | // set and a coordinate a function of the render. |
| 106 | let theirs = res!(ran(&a, &["flags"])); |
| 107 | assert!(theirs.contains("the region as the file now holds it is at src/util.rs:3"), |
| 108 | "both ends put the region in the same place: {}", theirs); |
| 109 | Ok(()) |
| 110 | } |
| 111 | |
| 112 | /// Content that renders nowhere is said to render nowhere, rather than being |
| 113 | /// omitted or given the location of a file it is not in. |
| 114 | /// |
| 115 | /// Two authors who rewrite one region both delete the old bytes, so the content |
| 116 | /// their overlap reports is dead. The flag has no file to be filed under, and the |
| 117 | /// coordinate has no line to give. |
| 118 | #[test] |
| 119 | fn dead_content_says_no_file_shows_it() -> Outcome<()> { |
| 120 | let scratch = res!(Scratch::new("review_dead")); |
| 121 | let (a, b) = res!(twinned(&scratch, "src/util.rs", FIVE)); |
| 122 | res!(write(&a, "src/util.rs", FIVE_A)); |
| 123 | res!(write(&b, "src/util.rs", FIVE_B)); |
| 124 | res!(synced(&b, &a)); |
| 125 | |
| 126 | let text = res!(ran(&b, &["flags"])); |
| 127 | assert!(text.contains("overlap"), "the raw fact is reported: {}", text); |
| 128 | // The overlap names the old bytes, which both replicas deleted. |
| 129 | let mut said: Option<&str> = None; |
| 130 | let mut lines = text.lines(); |
| 131 | while let Some(line) = lines.next() { |
| 132 | if line.trim_start().starts_with("overlap ") { |
| 133 | said = lines.next(); |
| 134 | break; |
| 135 | } |
| 136 | } |
| 137 | let said = match said { |
| 138 | Some(s) => s.trim(), |
| 139 | None => return Err(err!( |
| 140 | "No line follows the overlap flag: {}", text; Test, Missing)), |
| 141 | }; |
| 142 | assert_eq!(said, "names bytes no file shows", |
| 143 | "the flag's content is dead, and that is what is said of it"); |
| 144 | Ok(()) |
| 145 | } |
| 146 | |
| 147 | /// An edit that landed in a concurrently deleted file is given the line it |
| 148 | /// landed on, and told that the file is gone. |
| 149 | #[test] |
| 150 | fn an_edit_in_a_deleted_file_keeps_its_line() -> Outcome<()> { |
| 151 | let scratch = res!(Scratch::new("review_deleted")); |
| 152 | let (a, b) = res!(twinned(&scratch, "doomed.txt", FIVE)); |
| 153 | res!(fs::remove_file(a.join("doomed.txt"))); |
| 154 | res!(res!(ore(&a, &["mark", "gone"])).good("mark")); |
| 155 | res!(write(&b, "doomed.txt", FIVE_B)); |
| 156 | res!(synced(&b, &a)); |
| 157 | |
| 158 | let text = res!(ran(&b, &["flags"])); |
| 159 | assert!(text.contains("swallowed"), "the race is flagged: {}", text); |
| 160 | assert!(text.contains("at doomed.txt:3, in a file that has been deleted"), |
| 161 | "the edit was planted on the third line, and the file it is in is gone: {}", text); |
| 162 | Ok(()) |
| 163 | } |
| 164 | |
| 165 | /// A file holding bytes no line number would mean anything about is given a |
| 166 | /// byte range instead. |
| 167 | #[test] |
| 168 | fn a_file_with_no_lines_is_given_a_byte_range() -> Outcome<()> { |
| 169 | let scratch = res!(Scratch::new("review_binary")); |
| 170 | // Sixteen bytes with a NUL in them, so the file has no lines to count. |
| 171 | let base: &[u8] = b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"; |
| 172 | let (a, b) = res!(twinned(&scratch, "blob.bin", base)); |
| 173 | let mut mine = base.to_vec(); |
| 174 | mine[8] = 0xaa; |
| 175 | let mut theirs = base.to_vec(); |
| 176 | theirs[8] = 0xbb; |
| 177 | res!(write(&a, "blob.bin", &mine)); |
| 178 | res!(write(&b, "blob.bin", &theirs)); |
| 179 | res!(synced(&b, &a)); |
| 180 | |
| 181 | let text = res!(ran(&b, &["flags"])); |
| 182 | assert!(text.contains("yielded"), "the two writes collide: {}", text); |
| 183 | assert!(text.contains("blob.bin bytes "), |
| 184 | "a file with no lines is given bytes: {}", text); |
| 185 | assert!(!text.contains("blob.bin:"), |
| 186 | "and is given no line number at all: {}", text); |
| 187 | Ok(()) |
| 188 | } |
| 189 | |
| 190 | |
| 191 | /// After a sync, `log --arrived` names exactly what that sync delivered, and |
| 192 | /// nothing else. |
| 193 | #[test] |
| 194 | fn arrived_names_what_the_last_sync_delivered() -> Outcome<()> { |
| 195 | let scratch = res!(Scratch::new("review_arrived")); |
| 196 | let (a, b) = res!(twinned(&scratch, "shared.txt", FIVE)); |
| 197 | |
| 198 | // Work at each end, none of it seen by the other. |
| 199 | res!(write(&a, "from-a.txt", b"a line only A wrote\n")); |
| 200 | res!(res!(ore(&a, &["mark", "a-work"])).good("mark")); |
| 201 | res!(write(&b, "from-b.txt", b"a line only B wrote\n")); |
| 202 | res!(res!(ore(&b, &["mark", "b-work"])).good("mark")); |
| 203 | res!(synced(&b, &a)); |
| 204 | |
| 205 | // B asked for the sync, so what arrived there is A's work and only A's. |
| 206 | let text = res!(ran(&b, &["log", "--arrived"])); |
| 207 | assert!(text.contains("it brought 3 operations"), |
| 208 | "a file created, the line written into it, and the mark: {}", text); |
| 209 | assert!(text.contains("from-a.txt"), "the file A created is named: {}", text); |
| 210 | assert!(!text.contains("from-b.txt"), |
| 211 | "and B's own work is not something that arrived: {}", text); |
| 212 | assert!(text.contains("\"a-work\""), "the mark that crossed is named: {}", text); |
| 213 | assert!(!text.contains("\"b-work\""), "and B's own mark is not: {}", text); |
| 214 | assert!(text.contains("1 file created, 1 edit, 1 mark"), |
| 215 | "the operations are counted by kind: {}", text); |
| 216 | assert!(text.contains("flags none"), |
| 217 | "two files nobody else touched raise nothing: {}", text); |
| 218 | |
| 219 | // A was synced with rather than syncing, and it absorbed B's work, so its |
| 220 | // answer is the mirror image. |
| 221 | let theirs = res!(ran(&a, &["log", "--arrived"])); |
| 222 | assert!(theirs.contains("it brought 3 operations"), "both ends absorb: {}", theirs); |
| 223 | assert!(theirs.contains("from-b.txt") && !theirs.contains("from-a.txt"), |
| 224 | "and each end's answer is about what it did not already hold: {}", theirs); |
| 225 | |
| 226 | // A sync with nothing to carry says so, which is the answer that stops a |
| 227 | // reader reading the last one twice. |
| 228 | res!(synced(&b, &a)); |
| 229 | let again = res!(ran(&b, &["log", "--arrived"])); |
| 230 | assert!(again.contains("it brought nothing"), |
| 231 | "a no-op sync delivered nothing: {}", again); |
| 232 | assert!(!again.contains("from-a.txt"), |
| 233 | "and the previous sync's delta is not repeated: {}", again); |
| 234 | assert!(again.contains("sync this repository has recorded, and only the last is kept"), |
| 235 | "and the reader is told that only the last is kept: {}", again); |
| 236 | |
| 237 | // Deleting the record costs the answer and nothing else. |
| 238 | res!(fs::remove_file(b.join(".ore").join("arrived"))); |
| 239 | let bare = res!(ran(&b, &["log", "--arrived"])); |
| 240 | assert!(bare.contains("no sync has brought anything into this repository"), |
| 241 | "the record is deletable: {}", bare); |
| 242 | Ok(()) |
| 243 | } |
| 244 | |
| 245 | /// The delta names the flags the arrival raised, and not the ones the renderer |
| 246 | /// was already raising. |
| 247 | #[test] |
| 248 | fn arrived_names_the_flags_the_arrival_raised() -> Outcome<()> { |
| 249 | let scratch = res!(Scratch::new("review_arrived_flags")); |
| 250 | let (a, b) = res!(twinned(&scratch, "src/util.rs", FIVE)); |
| 251 | res!(write(&a, "src/util.rs", FIVE_A)); |
| 252 | res!(write(&b, "src/util.rs", FIVE_B)); |
| 253 | res!(synced(&b, &a)); |
| 254 | |
| 255 | let text = res!(ran(&b, &["log", "--arrived"])); |
| 256 | assert!(text.contains("flags 2 raised by the arrival"), |
| 257 | "the collision is what the arrival raised: {}", text); |
| 258 | assert!(text.contains("overlap") && text.contains("yielded"), |
| 259 | "and both flags are named: {}", text); |
| 260 | assert!(text.contains("the region as the file now holds it is at src/util.rs:3"), |
| 261 | "with the coordinate the flag list gives: {}", text); |
| 262 | Ok(()) |
| 263 | } |
| 264 | |
| 265 | |
| 266 | /// A flag can be put down: `--reviewed` marks it, `--new` stops showing it, and |
| 267 | /// deleting the record brings it back. |
| 268 | #[test] |
| 269 | fn a_reviewed_flag_stops_being_new() -> Outcome<()> { |
| 270 | let scratch = res!(Scratch::new("review_new")); |
| 271 | let (a, b) = res!(twinned(&scratch, "src/util.rs", FIVE)); |
| 272 | res!(write(&a, "src/util.rs", FIVE_A)); |
| 273 | res!(write(&b, "src/util.rs", FIVE_B)); |
| 274 | res!(synced(&b, &a)); |
| 275 | |
| 276 | // Everything is new until somebody says otherwise. |
| 277 | let text = res!(ran(&b, &["flags", "--new"])); |
| 278 | assert!(text.contains("2 new flags, and 0 marked reviewed"), |
| 279 | "nothing has been put down yet: {}", text); |
| 280 | |
| 281 | // The operation that yielded names both of its flags, which is what a person |
| 282 | // means by naming it. |
| 283 | let which = match yielded_op(&res!(ran(&b, &["flags"]))) { |
| 284 | Some(id) => id, |
| 285 | None => return Err(err!( |
| 286 | "No yielded flag names an operation."; Test, Missing)), |
| 287 | }; |
| 288 | let text = res!(ran(&b, &["flags", "--reviewed", &which])); |
| 289 | assert!(text.contains("marked 2 flags reviewed, 2 of 2 in all"), |
| 290 | "naming the edit marks every flag about it: {}", text); |
| 291 | assert!(text.contains("(reviewed)"), |
| 292 | "and the full list says which of them are down: {}", text); |
| 293 | |
| 294 | let text = res!(ran(&b, &["flags", "--new"])); |
| 295 | assert!(text.contains("nothing new: all 2 flags are marked reviewed"), |
| 296 | "and --new shows nothing: {}", text); |
| 297 | |
| 298 | // A re-render does not raise them again: the record names the operations the |
| 299 | // flags name, so it survives anything that does not change those. |
| 300 | res!(write(&b, "unrelated.txt", b"more work, elsewhere\n")); |
| 301 | res!(res!(ore(&b, &["mark", "later"])).good("mark")); |
| 302 | let text = res!(ran(&b, &["flags", "--new"])); |
| 303 | assert!(text.contains("nothing new"), |
| 304 | "work elsewhere does not make an old flag new: {}", text); |
| 305 | |
| 306 | // And the record is this working copy's own: deleting it makes everything new |
| 307 | // again, which is the whole of what it costs. |
| 308 | res!(fs::remove_file(b.join(".ore").join("reviewed"))); |
| 309 | let text = res!(ran(&b, &["flags", "--new"])); |
| 310 | assert!(text.contains("2 new flags"), |
| 311 | "deleting the record makes every flag new again: {}", text); |
| 312 | |
| 313 | // The other replica has an opinion of its own, and this one's does not reach |
| 314 | // it: reviewing is the tool's business and syncs with nothing. |
| 315 | let theirs = res!(ran(&a, &["flags", "--new"])); |
| 316 | assert!(theirs.contains("2 new flags"), |
| 317 | "a reviewed flag is not something a peer inherits: {}", theirs); |
| 318 | assert!(!a.join(".ore").join("reviewed").is_file(), |
| 319 | "and nothing wrote a reviewed record there"); |
| 320 | Ok(()) |
| 321 | } |
| 322 | |
| 323 | /// Marking every flag at once is one word, and naming an operation no flag names |
| 324 | /// says so rather than claiming to have done something. |
| 325 | #[test] |
| 326 | fn all_marks_every_flag_and_a_stranger_marks_none() -> Outcome<()> { |
| 327 | let scratch = res!(Scratch::new("review_all")); |
| 328 | let (a, b) = res!(twinned(&scratch, "src/util.rs", FIVE)); |
| 329 | res!(write(&a, "src/util.rs", FIVE_A)); |
| 330 | res!(write(&b, "src/util.rs", FIVE_B)); |
| 331 | res!(synced(&b, &a)); |
| 332 | |
| 333 | let text = res!(ran(&b, &["flags", "--reviewed", "r1:99999"])); |
| 334 | assert!(text.contains("nothing was marked reviewed"), |
| 335 | "an operation no flag names marks nothing: {}", text); |
| 336 | assert!(text.contains("2 flags in all"), |
| 337 | "and the flags are still all there: {}", text); |
| 338 | |
| 339 | let text = res!(ran(&b, &["flags", "--reviewed", "all"])); |
| 340 | assert!(text.contains("marked 2 flags reviewed"), "all means all: {}", text); |
| 341 | let text = res!(ran(&b, &["flags", "--new"])); |
| 342 | assert!(text.contains("nothing new"), "and nothing is left new: {}", text); |
| 343 | Ok(()) |
| 344 | } |
| 345 | |
| 346 | |
| 347 | /// Returns the operation a yielded flag names, as `ore flags` prints it. |
| 348 | fn yielded_op(text: &str) -> Option<String> { |
| 349 | for line in text.lines() { |
| 350 | let rest = match line.trim_start().strip_prefix("yielded ") { |
| 351 | Some(r) => r, |
| 352 | None => continue, |
| 353 | }; |
| 354 | return rest.split_whitespace().next().map(|s| fmt!("{}", s)); |
| 355 | } |
| 356 | None |
| 357 | } |