32.1 KiB, 50 runs
created by r2848102244:41, 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 sync`: two repositories that have written apart, brought into |
| 2 | //! agreement. |
| 3 | //! |
| 4 | //! # How a second replica is made here |
| 5 | //! |
| 6 | //! By syncing into a fresh one, which is the only way this tool offers and the |
| 7 | //! only one that is safe. Copying a working copy's `.ore` directory would be |
| 8 | //! quicker and would copy the replica identifier with it, and two repositories |
| 9 | //! of one replica name mint the same operation identifiers for different |
| 10 | //! operations -- a collision nothing afterwards can undo. The verb refuses that |
| 11 | //! pairing outright, and [`a_copied_repository_is_refused`] is the test of it. |
| 12 | //! |
| 13 | //! So the first sync of every test below is the clone: a fresh `ore init` with |
| 14 | //! nothing in it, and a sync that brings the whole history across. |
| 15 | |
| 16 | mod support; |
| 17 | |
| 18 | use support::{ |
| 19 | counted, |
| 20 | ore, |
| 21 | segment_path, |
| 22 | tree_of, |
| 23 | write, |
| 24 | Ran, |
| 25 | Scratch, |
| 26 | }; |
| 27 | |
| 28 | use ore_store::store::Lock; |
| 29 | |
| 30 | use oxedyne_fe2o3_core::prelude::*; |
| 31 | |
| 32 | use std::path::{ |
| 33 | Path, |
| 34 | PathBuf, |
| 35 | }; |
| 36 | |
| 37 | |
| 38 | /// Runs `ore sync <other>` in a directory and returns what it said. |
| 39 | fn sync(dir: &Path, other: &Path) |
| 40 | -> Outcome<Ran> |
| 41 | { |
| 42 | ore(dir, &["sync", &fmt!("{}", other.display())]) |
| 43 | } |
| 44 | |
| 45 | /// Runs `ore sync <other>` and fails the test if it did not succeed. |
| 46 | fn synced(dir: &Path, other: &Path) |
| 47 | -> Outcome<String> |
| 48 | { |
| 49 | let out = res!(sync(dir, other)); |
| 50 | Ok(fmt!("{}", res!(out.good("sync")))) |
| 51 | } |
| 52 | |
| 53 | /// Runs `ore sync <other> --pull-only` and fails the test if it did not succeed. |
| 54 | fn pulled_only(dir: &Path, other: &Path) |
| 55 | -> Outcome<String> |
| 56 | { |
| 57 | let out = res!(ore(dir, &["sync", &fmt!("{}", other.display()), "--pull-only"])); |
| 58 | Ok(fmt!("{}", res!(out.good("sync --pull-only")))) |
| 59 | } |
| 60 | |
| 61 | /// Runs `ore sync <other> --dry-run` and fails the test if it did not succeed. |
| 62 | fn rehearsed(dir: &Path, other: &Path) |
| 63 | -> Outcome<String> |
| 64 | { |
| 65 | let out = res!(ore(dir, &["sync", &fmt!("{}", other.display()), "--dry-run"])); |
| 66 | Ok(fmt!("{}", res!(out.good("sync --dry-run")))) |
| 67 | } |
| 68 | |
| 69 | /// What `ore log` says a repository holds: how many operations, and the |
| 70 | /// frontier. |
| 71 | /// |
| 72 | /// The frontier is the part that matters when two replicas are compared. A count |
| 73 | /// can match by coincidence; a frontier names the operations themselves, and two |
| 74 | /// repositories printing the same one hold the same history. |
| 75 | fn history(dir: &Path) |
| 76 | -> Outcome<(usize, String)> |
| 77 | { |
| 78 | let out = res!(ore(dir, &["log"])); |
| 79 | let text = fmt!("{}", res!(out.good("log"))); |
| 80 | let mut count: Option<usize> = None; |
| 81 | let mut frontier: Option<String> = None; |
| 82 | for line in text.lines() { |
| 83 | if let Some(rest) = line.strip_prefix("frontier ") { |
| 84 | frontier = Some(fmt!("{}", rest.trim())); |
| 85 | continue; |
| 86 | } |
| 87 | // The summary line, which is the one that says where the operations are; |
| 88 | // the others count operations since a mark and name no repository. |
| 89 | if !line.contains(" in ") || !line.contains(" on replica ") { |
| 90 | continue; |
| 91 | } |
| 92 | let first = match line.split_whitespace().next() { |
| 93 | Some(f) => f, |
| 94 | None => continue, |
| 95 | }; |
| 96 | count = match first.parse::<usize>() { |
| 97 | Ok(n) => Some(n), |
| 98 | Err(_) => continue, |
| 99 | }; |
| 100 | } |
| 101 | match (count, frontier) { |
| 102 | (Some(n), Some(f)) => Ok((n, f)), |
| 103 | _ => Err(err!( |
| 104 | "`ore log` in {:?} said neither how many operations nor which frontier: {}", |
| 105 | dir, text; |
| 106 | Test, Missing)), |
| 107 | } |
| 108 | } |
| 109 | |
| 110 | /// Makes a repository with a little history, and a second one holding it. |
| 111 | fn twinned(scratch: &Scratch) |
| 112 | -> Outcome<(PathBuf, PathBuf)> |
| 113 | { |
| 114 | let a = res!(scratch.sub("a")); |
| 115 | let b = res!(scratch.sub("b")); |
| 116 | res!(res!(ore(&a, &["init"])).good("init")); |
| 117 | res!(write(&a, "f.txt", b"alpha\nbeta\ngamma\n")); |
| 118 | res!(write(&a, "notes.md", b"shared notes\n")); |
| 119 | res!(res!(ore(&a, &["mark", "base"])).good("mark")); |
| 120 | res!(res!(ore(&b, &["init"])).good("init")); |
| 121 | let text = res!(synced(&b, &a)); |
| 122 | assert!(text.contains("received 5 operations"), |
| 123 | "the clone brings the whole history across: {}", text); |
| 124 | Ok((a, b)) |
| 125 | } |
| 126 | |
| 127 | |
| 128 | /// Two replicas that edited the same line, and one sync: both logs end holding |
| 129 | /// the same operations, and both say the same thing about them. |
| 130 | #[test] |
| 131 | fn a_divergence_converges_at_both_ends() -> Outcome<()> { |
| 132 | let scratch = res!(Scratch::new("sync_two")); |
| 133 | let (a, b) = res!(twinned(&scratch)); |
| 134 | |
| 135 | // Each writes a file of its own, and both replace the same middle line |
| 136 | // knowing nothing of the other. That last part is the genuine concurrency. |
| 137 | res!(write(&a, "f.txt", b"alpha\nBETA from A\ngamma\n")); |
| 138 | res!(write(&a, "only-a.txt", b"a line only A wrote\n")); |
| 139 | res!(write(&b, "f.txt", b"alpha\nBETA from B\ngamma\n")); |
| 140 | res!(write(&b, "only-b.txt", b"a line only B wrote\n")); |
| 141 | |
| 142 | let text = res!(synced(&a, &b)); |
| 143 | assert!(text.contains("mode "), "the mode is named: {}", text); |
| 144 | assert!(text.contains("sent 3 operations"), "and what went out: {}", text); |
| 145 | assert!(text.contains("received 3 operations"), "and what came in: {}", text); |
| 146 | assert!(text.contains("shared 5 operations both already held"), |
| 147 | "and what neither had to send: {}", text); |
| 148 | // Where both ended, read out of the report rather than written down here: the |
| 149 | // sync names the point it left the two of them at, which is one operation |
| 150 | // beyond what the exchange carried, and the total is therefore a function of |
| 151 | // the command and not a constant. What is worth insisting on is that the |
| 152 | // number the report gives is true of both ends, which is the two assertions |
| 153 | // below. |
| 154 | let held = res!(counted(&text, "both repositories now hold ")); |
| 155 | |
| 156 | // Both logs hold the same operations, which is what the frontier says. |
| 157 | let here = res!(history(&a)); |
| 158 | let there = res!(history(&b)); |
| 159 | assert_eq!(here, there, "the two replicas hold different histories"); |
| 160 | assert_eq!(here.0, held, |
| 161 | "the sync said both ends hold {} operations and this end holds {}: {}", |
| 162 | held, here.0, text); |
| 163 | |
| 164 | // The working copy holds the arbitrated merge. Two concurrent replacements of |
| 165 | // one line are one overlap group: the member highest in op order prevails and |
| 166 | // the other yields, its insertion buried whole, so the line reads as one |
| 167 | // writer wrote it rather than as both of them at once. Which writer is decided |
| 168 | // by op order, and is not what is being asserted here. |
| 169 | let merged = res!(std::fs::read(a.join("f.txt"))); |
| 170 | let shown = fmt!("{}", String::from_utf8_lossy(&merged)); |
| 171 | assert!(shown == "alpha\nBETA from A\ngamma\n" || shown == "alpha\nBETA from B\ngamma\n", |
| 172 | "the contended line is one writer's, whole: {:?}", shown); |
| 173 | assert_eq!(res!(std::fs::read(b.join("f.txt"))), merged, |
| 174 | "and the other working copy renders the same bytes"); |
| 175 | |
| 176 | // Each side has the other's file, and neither lost its own. Arbitration |
| 177 | // reaches the contended region and nothing else. |
| 178 | assert_eq!(res!(std::fs::read(a.join("only-b.txt"))), b"a line only B wrote\n".to_vec()); |
| 179 | assert_eq!(res!(std::fs::read(b.join("only-a.txt"))), b"a line only A wrote\n".to_vec()); |
| 180 | |
| 181 | // A sync that arbitrated two concurrent edits of one region says so, rather |
| 182 | // than leaving it to be found. And it says the same thing at both ends, the |
| 183 | // arbitration being a function of the operation set and not of who ran what. |
| 184 | assert!(text.contains("overlap"), |
| 185 | "the sync reports the concurrent edit it merged: {}", text); |
| 186 | assert!(text.contains("yielded"), |
| 187 | "and what it did about it: {}", text); |
| 188 | assert!(text.contains("f.txt"), "and which file it was in: {}", text); |
| 189 | let mine = fmt!("{}", res!(res!(ore(&a, &["flags"])).good("flags"))); |
| 190 | let theirs = fmt!("{}", res!(res!(ore(&b, &["flags"])).good("flags"))); |
| 191 | assert_eq!(mine, theirs, "the two replicas flag different things"); |
| 192 | assert!(mine.contains("overlap"), "the raw fact is reported: {}", mine); |
| 193 | assert!(mine.contains("yielded") && mine.contains("prevailed"), |
| 194 | "and so is the arbitration, naming what prevailed: {}", mine); |
| 195 | assert!(mine.contains("2 flags in all"), "and nothing else is: {}", mine); |
| 196 | |
| 197 | // The buried version is beside each working copy, identically: derived data |
| 198 | // that is not history, and what a reviewer with no forge diffs against. |
| 199 | let one = res!(spilled(&a)); |
| 200 | assert_eq!(one, res!(spilled(&b)), "the two spills differ"); |
| 201 | assert_eq!(one.len(), 1, "one version yielded, so one is spilled"); |
| 202 | let held = fmt!("{}", String::from_utf8_lossy(&one[0])); |
| 203 | assert!(held == "BETA from A" || held == "BETA from B", |
| 204 | "the spill holds the whole of what the buried operation wrote: {:?}", held); |
| 205 | assert!(!shown.contains(&held), "which the file does not hold: {:?}", held); |
| 206 | assert!(mine.contains(".ore/collisions"), |
| 207 | "and the summary says where it is: {}", mine); |
| 208 | |
| 209 | // It is derived and it says so: deleting the whole directory costs nothing, |
| 210 | // because the next render puts back exactly what was there. |
| 211 | res!(std::fs::remove_dir_all(a.join(".ore").join("collisions"))); |
| 212 | res!(res!(ore(&a, &["flags"])).good("flags")); |
| 213 | assert_eq!(res!(spilled(&a)), one, "the next render did not rebuild the spill"); |
| 214 | Ok(()) |
| 215 | } |
| 216 | |
| 217 | /// The buried versions a repository has spilled, in name order. |
| 218 | fn spilled(root: &Path) |
| 219 | -> Outcome<Vec<Vec<u8>>> |
| 220 | { |
| 221 | let dir = root.join(".ore").join("collisions"); |
| 222 | if !dir.is_dir() { |
| 223 | return Ok(Vec::new()); |
| 224 | } |
| 225 | let mut names: Vec<PathBuf> = Vec::new(); |
| 226 | for entry in res!(std::fs::read_dir(&dir)) { |
| 227 | names.push(res!(entry).path()); |
| 228 | } |
| 229 | names.sort(); |
| 230 | let mut out: Vec<Vec<u8>> = Vec::new(); |
| 231 | for at in names { |
| 232 | out.push(res!(std::fs::read(at))); |
| 233 | } |
| 234 | Ok(out) |
| 235 | } |
| 236 | |
| 237 | /// A sync of two repositories that already agree exchanges nothing, and says so. |
| 238 | #[test] |
| 239 | fn a_second_sync_has_nothing_to_do() -> Outcome<()> { |
| 240 | let scratch = res!(Scratch::new("sync_again")); |
| 241 | let (a, b) = res!(twinned(&scratch)); |
| 242 | res!(write(&a, "f.txt", b"alpha\nchanged\ngamma\n")); |
| 243 | res!(synced(&a, &b)); |
| 244 | |
| 245 | let text = res!(synced(&a, &b)); |
| 246 | assert!(text.contains("nothing to exchange: both repositories already held the same"), |
| 247 | "the second sync has nothing to carry: {}", text); |
| 248 | assert!(!text.contains("sent "), "and says nothing about sending: {}", text); |
| 249 | assert!(text.contains("nothing moved; it was already that state"), |
| 250 | "and the working copy is where it was: {}", text); |
| 251 | // From the other end too, which has not run a verb of its own since. |
| 252 | let text = res!(synced(&b, &a)); |
| 253 | assert!(text.contains("nothing to exchange"), |
| 254 | "and neither has the other end: {}", text); |
| 255 | assert_eq!(res!(history(&a)), res!(history(&b))); |
| 256 | Ok(()) |
| 257 | } |
| 258 | |
| 259 | /// Three replicas, three pairwise syncs, one history. |
| 260 | #[test] |
| 261 | fn three_replicas_converge_on_one_frontier() -> Outcome<()> { |
| 262 | let scratch = res!(Scratch::new("sync_three")); |
| 263 | let (a, b) = res!(twinned(&scratch)); |
| 264 | let c = res!(scratch.sub("c")); |
| 265 | res!(res!(ore(&c, &["init"])).good("init")); |
| 266 | res!(synced(&c, &a)); |
| 267 | |
| 268 | // Each of the three writes a file of its own and edits the shared line. |
| 269 | res!(write(&a, "f.txt", b"alpha\nBETA from A\ngamma\n")); |
| 270 | res!(write(&a, "from-a.txt", b"A\n")); |
| 271 | res!(write(&b, "f.txt", b"alpha\nBETA from B\ngamma\n")); |
| 272 | res!(write(&b, "from-b.txt", b"B\n")); |
| 273 | res!(write(&c, "f.txt", b"alpha\nBETA from C\ngamma\n")); |
| 274 | res!(write(&c, "from-c.txt", b"C\n")); |
| 275 | |
| 276 | res!(synced(&a, &b)); |
| 277 | res!(synced(&b, &c)); |
| 278 | res!(synced(&a, &c)); |
| 279 | |
| 280 | let here = res!(history(&a)); |
| 281 | assert_eq!(res!(history(&b)), here, "the second replica is elsewhere"); |
| 282 | assert_eq!(res!(history(&c)), here, "the third replica is elsewhere"); |
| 283 | |
| 284 | // And all three working copies render that one history identically. Three |
| 285 | // concurrent replacements of one line are one overlap group, so one of them |
| 286 | // prevails and the other two yield: the line is one writer's, whole, and the |
| 287 | // other two versions are in the log and flagged. |
| 288 | let merged = res!(std::fs::read(a.join("f.txt"))); |
| 289 | let shown = fmt!("{}", String::from_utf8_lossy(&merged)); |
| 290 | let versions: Vec<String> = ["A", "B", "C"].iter() |
| 291 | .map(|who| fmt!("alpha\nBETA from {}\ngamma\n", who)) |
| 292 | .collect(); |
| 293 | assert!(versions.contains(&shown), |
| 294 | "the contended line is one writer's, whole: {:?}", shown); |
| 295 | assert_eq!(res!(std::fs::read(b.join("f.txt"))), merged); |
| 296 | assert_eq!(res!(std::fs::read(c.join("f.txt"))), merged); |
| 297 | // The two that did not prevail are spilled, at every replica alike. |
| 298 | assert_eq!(res!(spilled(&a)).len(), 2, "two writers yielded"); |
| 299 | assert_eq!(res!(spilled(&b)), res!(spilled(&a))); |
| 300 | assert_eq!(res!(spilled(&c)), res!(spilled(&a))); |
| 301 | for name in ["from-a.txt", "from-b.txt", "from-c.txt"] { |
| 302 | assert!(a.join(name).is_file() && b.join(name).is_file() && c.join(name).is_file(), |
| 303 | "every replica holds {}", name); |
| 304 | } |
| 305 | Ok(()) |
| 306 | } |
| 307 | |
| 308 | /// Two repositories that share no history at all still merge, and a path they |
| 309 | /// both created is the clash the layout policy already knows what to do with. |
| 310 | #[test] |
| 311 | fn repositories_that_share_nothing_merge_as_a_clash() -> Outcome<()> { |
| 312 | let scratch = res!(Scratch::new("sync_disjoint")); |
| 313 | let a = res!(scratch.sub("a")); |
| 314 | let b = res!(scratch.sub("b")); |
| 315 | res!(res!(ore(&a, &["init"])).good("init")); |
| 316 | res!(res!(ore(&b, &["init"])).good("init")); |
| 317 | res!(write(&a, "notes.md", b"notes from A\n")); |
| 318 | res!(write(&b, "notes.md", b"notes from B\n")); |
| 319 | |
| 320 | let text = res!(synced(&a, &b)); |
| 321 | assert!(text.contains("shared 0 operations both already held"), |
| 322 | "two repositories that share nothing share nothing: {}", text); |
| 323 | assert!(text.contains("clash at notes.md: 2 files hold that path"), |
| 324 | "and each created a file at one path: {}", text); |
| 325 | assert_eq!(res!(history(&a)), res!(history(&b)), "and both hold the union"); |
| 326 | |
| 327 | // Both files are in the working copy: one at the path, one at a derived name. |
| 328 | let mut found: Vec<Vec<u8>> = Vec::new(); |
| 329 | for entry in res!(std::fs::read_dir(&a)) { |
| 330 | let entry = res!(entry); |
| 331 | let name = entry.file_name().to_string_lossy().into_owned(); |
| 332 | if name.starts_with("notes.md") { |
| 333 | found.push(res!(std::fs::read(entry.path()))); |
| 334 | } |
| 335 | } |
| 336 | found.sort(); |
| 337 | let mut want = vec![b"notes from A\n".to_vec(), b"notes from B\n".to_vec()]; |
| 338 | want.sort(); |
| 339 | assert_eq!(found, want, "neither file was chosen over the other"); |
| 340 | Ok(()) |
| 341 | } |
| 342 | |
| 343 | /// A small divergence over a large shared history goes by sketch, and the |
| 344 | /// sketch holds. |
| 345 | /// |
| 346 | /// That is the whole reason the mode exists: the walk is loose where both sides |
| 347 | /// have written, and would carry both entire logs to move four operations. |
| 348 | #[test] |
| 349 | fn a_small_divergence_over_a_large_history_uses_the_sketch() -> Outcome<()> { |
| 350 | let scratch = res!(Scratch::new("sync_sketch")); |
| 351 | let a = res!(scratch.sub("a")); |
| 352 | let b = res!(scratch.sub("b")); |
| 353 | res!(res!(ore(&a, &["init"])).good("init")); |
| 354 | for i in 0..60 { |
| 355 | res!(write(&a, &fmt!("f{:02}.txt", i), fmt!("file number {}\n", i).as_bytes())); |
| 356 | } |
| 357 | res!(res!(ore(&a, &["mark", "base"])).good("mark")); |
| 358 | res!(res!(ore(&b, &["init"])).good("init")); |
| 359 | let text = res!(synced(&b, &a)); |
| 360 | assert!(text.contains("received 121 operations"), |
| 361 | "the clone carries the whole history: {}", text); |
| 362 | |
| 363 | // One line at each end, four operations of difference in all. |
| 364 | res!(write(&a, "f00.txt", b"file number 0\na tail from A\n")); |
| 365 | res!(write(&b, "f01.txt", b"file number 1\na tail from B\n")); |
| 366 | let text = res!(synced(&a, &b)); |
| 367 | assert!(text.contains("mode sketch"), |
| 368 | "a small difference over a large history is what a sketch is for: {}", text); |
| 369 | assert!(!text.contains("fell back"), |
| 370 | "and the estimate held, so no round trip was spent on the walk: {}", text); |
| 371 | assert!(text.contains("shared 121 operations both already held"), |
| 372 | "and the shared history was not carried: {}", text); |
| 373 | assert_eq!(res!(history(&a)), res!(history(&b))); |
| 374 | Ok(()) |
| 375 | } |
| 376 | |
| 377 | /// Syncing a repository with itself is nothing, and says so rather than |
| 378 | /// pretending to have done something. |
| 379 | #[test] |
| 380 | fn syncing_with_itself_does_nothing() -> Outcome<()> { |
| 381 | let scratch = res!(Scratch::new("sync_self")); |
| 382 | let (a, _b) = res!(twinned(&scratch)); |
| 383 | let before = res!(history(&a)); |
| 384 | let text = fmt!("{}", res!(res!(ore(&a, &["sync", "."])).good("sync ."))); |
| 385 | assert!(text.contains("is this repository, so there is nothing to sync with"), |
| 386 | "a sync needs two repositories: {}", text); |
| 387 | assert_eq!(res!(history(&a)), before, "and nothing was written"); |
| 388 | Ok(()) |
| 389 | } |
| 390 | |
| 391 | /// A path that is not a repository is refused by name, and so is one that is not |
| 392 | /// there at all. |
| 393 | #[test] |
| 394 | fn a_path_that_is_not_a_repository_is_refused() -> Outcome<()> { |
| 395 | let scratch = res!(Scratch::new("sync_not_repo")); |
| 396 | let (a, _b) = res!(twinned(&scratch)); |
| 397 | let plain = res!(scratch.sub("plain")); |
| 398 | res!(write(&plain, "unrelated.txt", b"nothing to do with Ore\n")); |
| 399 | |
| 400 | let out = res!(sync(&a, &plain)); |
| 401 | assert!(!out.ok, "a directory that is not a repository is not a peer"); |
| 402 | assert!(out.err.contains("is not an Ore repository"), |
| 403 | "and the reason says so: {}", out.err); |
| 404 | assert!(out.err.contains(".ore/config"), |
| 405 | "and names what it looked for: {}", out.err); |
| 406 | |
| 407 | let out = res!(ore(&a, &["sync", "nowhere-at-all"])); |
| 408 | assert!(!out.ok, "a path that is not there is not a peer either"); |
| 409 | assert!(out.err.contains("could not be resolved"), |
| 410 | "and the reason says so: {}", out.err); |
| 411 | Ok(()) |
| 412 | } |
| 413 | |
| 414 | /// Two repositories carrying one replica identifier are refused, because their |
| 415 | /// operation identifiers would collide. |
| 416 | #[test] |
| 417 | fn a_copied_repository_is_refused() -> Outcome<()> { |
| 418 | let scratch = res!(Scratch::new("sync_copied")); |
| 419 | let (a, _b) = res!(twinned(&scratch)); |
| 420 | // What a person reaching for `cp -r` would end up with. |
| 421 | let copy = scratch.path.join("copy"); |
| 422 | res!(std::fs::create_dir_all(copy.join(".ore"))); |
| 423 | res!(copy_tree(&a.join(".ore"), ©.join(".ore"))); |
| 424 | res!(write(©, "f.txt", b"alpha\nbeta\ngamma\n")); |
| 425 | res!(write(©, "notes.md", b"shared notes\n")); |
| 426 | |
| 427 | let out = res!(sync(&a, ©)); |
| 428 | assert!(!out.ok, "two repositories of one replica name must not be synced"); |
| 429 | assert!(out.err.contains("both replica"), "and the reason says so: {}", out.err); |
| 430 | assert!(out.err.contains("`ore init`"), |
| 431 | "and says how a second repository is properly made: {}", out.err); |
| 432 | Ok(()) |
| 433 | } |
| 434 | |
| 435 | /// A working copy edited after a sync updated its history keeps what was typed |
| 436 | /// into it. |
| 437 | /// |
| 438 | /// The sync writes only the working copy it was run from and leaves a marker in |
| 439 | /// the other. The next verb there writes the merged state out -- unless somebody |
| 440 | /// has been editing in the meantime, in which case their bytes are what the |
| 441 | /// working copy is, and they are captured rather than overwritten. |
| 442 | #[test] |
| 443 | fn a_working_copy_edited_after_a_sync_keeps_its_edits() -> Outcome<()> { |
| 444 | let scratch = res!(Scratch::new("sync_overtaken")); |
| 445 | let (a, b) = res!(twinned(&scratch)); |
| 446 | res!(write(&a, "f.txt", b"alpha\nBETA from A\ngamma\n")); |
| 447 | let text = res!(synced(&a, &b)); |
| 448 | assert!(text.contains("its working copy is written the next time a verb runs there"), |
| 449 | "the sync says what it left behind: {}", text); |
| 450 | // The other end's files have not moved, whatever its log now holds. |
| 451 | assert_eq!(res!(std::fs::read(b.join("f.txt"))), b"alpha\nbeta\ngamma\n".to_vec(), |
| 452 | "the sync wrote into the other working copy"); |
| 453 | |
| 454 | // Somebody types into it before running anything. |
| 455 | res!(write(&b, "f.txt", b"typed at B after the sync\n")); |
| 456 | let text = fmt!("{}", res!(res!(ore(&b, &["log"])).good("log"))); |
| 457 | assert!(text.contains("was edited after a sync"), |
| 458 | "the verb says why it did not write the working copy out: {}", text); |
| 459 | assert_eq!(res!(std::fs::read(b.join("f.txt"))), b"typed at B after the sync\n".to_vec(), |
| 460 | "what was typed was overwritten"); |
| 461 | assert!(text.contains("captured 1 operation") || text.contains("captured 2 operations"), |
| 462 | "and what was typed was captured: {}", text); |
| 463 | |
| 464 | // Left alone instead, the merged state is what the next verb writes. |
| 465 | let c = res!(scratch.sub("c")); |
| 466 | res!(res!(ore(&c, &["init"])).good("init")); |
| 467 | res!(synced(&c, &a)); |
| 468 | res!(write(&a, "f.txt", b"alpha\nBETA from A again\ngamma\n")); |
| 469 | res!(synced(&a, &c)); |
| 470 | let text = fmt!("{}", res!(res!(ore(&c, &["log"])).good("log"))); |
| 471 | assert!(text.contains("the working copy is now the merged state"), |
| 472 | "a working copy nobody touched is written forward: {}", text); |
| 473 | assert_eq!(res!(std::fs::read(c.join("f.txt"))), res!(std::fs::read(a.join("f.txt"))), |
| 474 | "and it renders what the other end renders"); |
| 475 | Ok(()) |
| 476 | } |
| 477 | |
| 478 | /// One command writes to a repository at a time, and a second says who has it. |
| 479 | /// |
| 480 | /// The lock is held here by this test process rather than fabricated on disk, |
| 481 | /// because that is the only way to ask the question the fix turns on: a file is no |
| 482 | /// longer what excludes anybody, so a file alone must not. |
| 483 | #[test] |
| 484 | fn a_held_lock_stops_a_second_command() -> Outcome<()> { |
| 485 | let scratch = res!(Scratch::new("sync_lock")); |
| 486 | let (a, b) = res!(twinned(&scratch)); |
| 487 | let held = res!(Lock::take(&a.join(".ore"))); |
| 488 | |
| 489 | let out = res!(ore(&a, &["log"])); |
| 490 | assert!(!out.ok, "a locked repository is not read while another command holds it"); |
| 491 | assert!(out.err.contains("Another `ore` command is working in"), |
| 492 | "and the reason says so: {}", out.err); |
| 493 | assert!(out.err.contains(&fmt!("process {} ", std::process::id())), |
| 494 | "and names what holds the lock: {}", out.err); |
| 495 | |
| 496 | // A sync is refused by the other end's lock in the same way. |
| 497 | drop(held); |
| 498 | let far = res!(Lock::take(&b.join(".ore"))); |
| 499 | let out = res!(sync(&a, &b)); |
| 500 | assert!(!out.ok, "the far end's lock stops a sync"); |
| 501 | assert!(out.err.contains("Another `ore` command is working in"), |
| 502 | "and the reason says so: {}", out.err); |
| 503 | drop(far); |
| 504 | |
| 505 | // And a command that ran normally leaves nobody holding it. The file stays -- |
| 506 | // it is where the lock is kept, not the lock -- and carries no name. |
| 507 | let lock = a.join(".ore").join("lock"); |
| 508 | res!(res!(ore(&a, &["log"])).good("log")); |
| 509 | assert!(res!(std::fs::read_to_string(&lock)).trim().is_empty(), |
| 510 | "the command that took the lock left its name in it"); |
| 511 | res!(synced(&a, &b)); |
| 512 | assert!(res!(std::fs::read_to_string(b.join(".ore").join("lock"))).trim().is_empty(), |
| 513 | "the far end's sync left its name in the lock"); |
| 514 | Ok(()) |
| 515 | } |
| 516 | |
| 517 | /// A command that was killed leaves the lock file behind, and the next command in |
| 518 | /// that tree runs. |
| 519 | /// |
| 520 | /// It did not, and the cost was not theoretical: the git hook that marks an Ore |
| 521 | /// replica beside every commit was written on 2026-08-22 with no `timeout` around |
| 522 | /// `ore`, deliberately, because a killed `ore` bricked the tree until somebody |
| 523 | /// deleted `.ore/lock` by hand. A hook that might hang was the better of two bad |
| 524 | /// outcomes. Neither is on offer now. |
| 525 | #[test] |
| 526 | fn a_killed_command_leaves_the_next_one_free() -> Outcome<()> { |
| 527 | let scratch = res!(Scratch::new("sync_killed")); |
| 528 | let (a, _b) = res!(twinned(&scratch)); |
| 529 | let lock = a.join(".ore").join("lock"); |
| 530 | // Exactly what a killed process leaves: the file, its note, and no hold on it. |
| 531 | res!(std::fs::write(&lock, "process 999999 since 1700000000\n")); |
| 532 | |
| 533 | let out = res!(ore(&a, &["log"])); |
| 534 | assert!(out.ok, |
| 535 | "a lock a killed command left behind still refuses the next one: {}", out.err); |
| 536 | assert!(!out.err.contains("Another `ore` command"), |
| 537 | "and it is not merely quieter about it: {}", out.err); |
| 538 | // A verb that writes, not only one that reads. |
| 539 | res!(res!(ore(&a, &["mark", "after-the-kill", "took the lock back"])).good("mark")); |
| 540 | assert!(res!(std::fs::read_to_string(&lock)).trim().is_empty(), |
| 541 | "the dead process's name is still in the lock"); |
| 542 | Ok(()) |
| 543 | } |
| 544 | |
| 545 | |
| 546 | /// Copies a directory tree, which is how the copied-repository test makes the |
| 547 | /// mistake it is about. |
| 548 | fn copy_tree(from: &Path, to: &Path) |
| 549 | -> Outcome<()> |
| 550 | { |
| 551 | res!(std::fs::create_dir_all(to)); |
| 552 | for entry in res!(std::fs::read_dir(from)) { |
| 553 | let entry = res!(entry); |
| 554 | let kind = res!(entry.file_type()); |
| 555 | let there = to.join(entry.file_name()); |
| 556 | if kind.is_dir() { |
| 557 | res!(copy_tree(&entry.path(), &there)); |
| 558 | } else if kind.is_file() { |
| 559 | res!(std::fs::copy(entry.path(), &there)); |
| 560 | } |
| 561 | } |
| 562 | Ok(()) |
| 563 | } |
| 564 | |
| 565 | |
| 566 | /// A mark name means one state on every replica, whatever order each heard about |
| 567 | /// the marks in. |
| 568 | /// |
| 569 | /// Marking is a person naming where they have got to, so two replicas working |
| 570 | /// apart can reach for the same name, and after a sync both hold both marks. A |
| 571 | /// name then has to be arbitrated. Answering with the last one the log happens to |
| 572 | /// hold answers by arrival order, which two replicas holding the very same |
| 573 | /// operations legitimately disagree about, because a batch is placed to a |
| 574 | /// fixpoint however it was shuffled. Each would land on the other's state, both |
| 575 | /// reporting success. Operation order is shared, so it is what decides. |
| 576 | #[test] |
| 577 | fn a_mark_name_resolves_alike_on_every_replica() -> Outcome<()> { |
| 578 | let scratch = res!(Scratch::new("sync_mark_name")); |
| 579 | let (a, b) = res!(twinned(&scratch)); |
| 580 | |
| 581 | // Each names the point it has reached, with the one name, knowing nothing of |
| 582 | // the other. That last part is what makes the two marks concurrent. |
| 583 | res!(write(&a, "f.txt", b"alpha\nA WAS HERE\ngamma\n")); |
| 584 | res!(res!(ore(&a, &["mark", "release"])).good("mark")); |
| 585 | res!(write(&b, "f.txt", b"alpha\nB WAS HERE\ngamma\n")); |
| 586 | res!(res!(ore(&b, &["mark", "release"])).good("mark")); |
| 587 | |
| 588 | res!(synced(&a, &b)); |
| 589 | |
| 590 | // Both hold one history, so anything they disagree about below is a |
| 591 | // disagreement about reading it rather than about what it holds. |
| 592 | let here = res!(history(&a)); |
| 593 | let there = res!(history(&b)); |
| 594 | assert_eq!(here, there, "the two replicas hold different histories"); |
| 595 | |
| 596 | res!(res!(ore(&a, &["back", "release"])).good("back")); |
| 597 | res!(res!(ore(&b, &["back", "release"])).good("back")); |
| 598 | |
| 599 | assert_eq!(res!(tree_of(&a)), res!(tree_of(&b)), |
| 600 | "one mark name put two replicas holding one history at two states"); |
| 601 | Ok(()) |
| 602 | } |
| 603 | |
| 604 | |
| 605 | /// Taking without giving leaves the other repository exactly as it was, the work |
| 606 | /// its owner had not recorded included. |
| 607 | /// |
| 608 | /// An ordinary sync over a path does two things to the other end, and a person |
| 609 | /// looking at somebody else's repository intends neither. The operations cross |
| 610 | /// both ways, so their history acquires yours. And it is captured first, so |
| 611 | /// whatever they had deliberately not recorded becomes history under their own |
| 612 | /// name, authored by somebody else's command. Both were measured before this |
| 613 | /// option existed. |
| 614 | #[test] |
| 615 | fn taking_without_giving_leaves_the_other_repository_alone() -> Outcome<()> { |
| 616 | let scratch = res!(Scratch::new("sync_pull_only")); |
| 617 | let (a, b) = res!(twinned(&scratch)); |
| 618 | |
| 619 | // Each end does some work of its own and records it. |
| 620 | res!(write(&a, "mine.txt", b"work at this end\n")); |
| 621 | res!(res!(ore(&a, &["flags"])).good("flags")); |
| 622 | res!(write(&b, "theirs.txt", b"work at the other end\n")); |
| 623 | res!(res!(ore(&b, &["flags"])).good("flags")); |
| 624 | |
| 625 | // And the other end has something half done that it has not recorded, which |
| 626 | // is the thing an ordinary sync would decide to record on its behalf. |
| 627 | res!(write(&b, "wip.txt", b"a thought, not finished\n")); |
| 628 | // Read without running the tool there. Every verb but `init` captures, so |
| 629 | // asking `ore log` for a before-reading would commit the very work in |
| 630 | // progress this test exists to protect, and the sync would then correctly |
| 631 | // report having captured nothing. |
| 632 | let was_there = res!(std::fs::read(segment_path(&b))); |
| 633 | |
| 634 | let said = res!(pulled_only(&a, &b)); |
| 635 | assert!(said.contains("left alone"), |
| 636 | "it says the other end is untouched: {}", said); |
| 637 | assert!(said.contains("offered nothing, by request"), |
| 638 | "and that it handed nothing over: {}", said); |
| 639 | assert!(said.contains("kept back"), |
| 640 | "and what stayed behind: {}", said); |
| 641 | |
| 642 | // This end took what was offered. |
| 643 | assert_eq!(res!(std::fs::read(a.join("theirs.txt"))), b"work at the other end\n".to_vec(), |
| 644 | "the other end's recorded work did not arrive"); |
| 645 | |
| 646 | // And the other end is as it was, byte for byte: it did not receive, and its |
| 647 | // unfinished thought is still its own to record or discard. |
| 648 | assert_eq!(res!(std::fs::read(segment_path(&b))), was_there, |
| 649 | "the other repository's log was written to"); |
| 650 | assert!(!b.join("mine.txt").exists(), |
| 651 | "this end's work was written into the other repository"); |
| 652 | assert!(said.contains("wip.txt") == false, |
| 653 | "the other end was captured, so its unfinished work crossed: {}", said); |
| 654 | Ok(()) |
| 655 | } |
| 656 | |
| 657 | /// A rehearsal over a path touches neither repository. |
| 658 | #[test] |
| 659 | fn a_rehearsal_over_a_path_touches_neither_end() -> Outcome<()> { |
| 660 | let scratch = res!(Scratch::new("sync_dry_path")); |
| 661 | let (a, b) = res!(twinned(&scratch)); |
| 662 | |
| 663 | res!(write(&b, "theirs.txt", b"work at the other end\n")); |
| 664 | res!(res!(ore(&b, &["flags"])).good("flags")); |
| 665 | res!(write(&b, "wip.txt", b"a thought, not finished\n")); |
| 666 | |
| 667 | let here = res!(std::fs::read(segment_path(&a))); |
| 668 | let there = res!(std::fs::read(segment_path(&b))); |
| 669 | |
| 670 | let said = res!(rehearsed(&a, &b)); |
| 671 | // How much would arrive, read out rather than written down: what the other end |
| 672 | // has to hand over depends on how many commands were run there, each of which |
| 673 | // ends by naming the point it reached. What is insisted on instead is that the |
| 674 | // rehearsal counts the same in its summary as in its description -- one number |
| 675 | // said twice, and a rehearsal that disagreed with itself would be worthless. |
| 676 | // |
| 677 | // It is deliberately *not* compared with what the sync afterwards brings. A |
| 678 | // rehearsal leaves the other end alone and so never sees its unrecorded work; |
| 679 | // the sync captures it, and brings more. That difference is the subject of the |
| 680 | // assertion about `wip.txt` below and is not a fault in either count. |
| 681 | let would = res!(counted(&said, "would take ")); |
| 682 | assert!(would > 0, "the rehearsal says what would arrive: {}", said); |
| 683 | assert_eq!(res!(counted(&said, "it would bring ")), would, |
| 684 | "the rehearsal counts one way in the summary and another in the \ |
| 685 | description: {}", said); |
| 686 | assert!(said.contains("theirs.txt"), |
| 687 | "naming the file that would appear: {}", said); |
| 688 | assert!(said.contains("nothing was written: this was a rehearsal"), |
| 689 | "and that it kept none of it: {}", said); |
| 690 | |
| 691 | assert!(!said.contains("wip.txt"), |
| 692 | "the other end was captured, so its unfinished work crossed: {}", said); |
| 693 | assert_eq!(res!(std::fs::read(segment_path(&a))), here, |
| 694 | "the rehearsal wrote at this end"); |
| 695 | assert_eq!(res!(std::fs::read(segment_path(&b))), there, |
| 696 | "the rehearsal wrote at the other end"); |
| 697 | assert!(!a.join("theirs.txt").exists(), |
| 698 | "the rehearsal wrote a file into this working copy"); |
| 699 | |
| 700 | // And the sync afterwards brings what was described, and more besides: it |
| 701 | // captures the other end first, so the unfinished work the rehearsal was not |
| 702 | // allowed to see crosses too. |
| 703 | let ran = res!(synced(&a, &b)); |
| 704 | let brought = res!(counted(&ran, "received ")); |
| 705 | assert!(brought >= would, |
| 706 | "the sync brought {} where the rehearsal said {} would arrive, and a sync \ |
| 707 | brings everything a rehearsal saw: {}", brought, would, ran); |
| 708 | assert_eq!(res!(std::fs::read(a.join("theirs.txt"))), b"work at the other end\n".to_vec(), |
| 709 | "what the rehearsal described did not arrive when the sync ran"); |
| 710 | Ok(()) |
| 711 | } |
| 712 | |
| 713 | |
| 714 | /// A rehearsal says what taking somebody's work would cost this replica, which |
| 715 | /// is the edits of its own the arbitration would bury. |
| 716 | /// |
| 717 | /// The count says how much is coming and the flag delta says what it costs. No |
| 718 | /// system that stores states can answer this before the merge: the answer is a |
| 719 | /// property of the operations, and two versions of a file do not hold it. |
| 720 | #[test] |
| 721 | fn a_rehearsal_says_which_of_your_own_edits_would_be_buried() -> Outcome<()> { |
| 722 | let scratch = res!(Scratch::new("sync_dry_cost")); |
| 723 | let (a, b) = res!(twinned(&scratch)); |
| 724 | |
| 725 | // The other end writes twice, so its second operation takes a counter one |
| 726 | // past its first. A counter is one past the highest anybody has written, so |
| 727 | // this end's single edit is outranked whatever the replica numbers happen to |
| 728 | // be, and the arbitration is decided rather than a coin toss. |
| 729 | res!(write(&b, "elsewhere.txt", b"unrelated work\n")); |
| 730 | res!(res!(ore(&b, &["flags"])).good("flags")); |
| 731 | res!(write(&b, "f.txt", b"alpha\nBETA from the other end\ngamma\n")); |
| 732 | res!(res!(ore(&b, &["flags"])).good("flags")); |
| 733 | |
| 734 | // This end replaces the same line, knowing nothing of that. |
| 735 | res!(write(&a, "f.txt", b"alpha\nBETA from this end\ngamma\n")); |
| 736 | res!(res!(ore(&a, &["flags"])).good("flags")); |
| 737 | |
| 738 | let said = res!(rehearsed(&a, &b)); |
| 739 | assert!(said.contains("1 edit of yours: in the log, not in the file"), |
| 740 | "the rehearsal prices the arrival in this replica's own work: {}", said); |
| 741 | assert!(said.contains("f.txt"), |
| 742 | "and says where it is: {}", said); |
| 743 | |
| 744 | // The same merge, rehearsed from the other end, costs it nothing: the one |
| 745 | // yield in the merged state is not its own. Without this the assertion above |
| 746 | // would pass just as well against a count of every yield there is, and the |
| 747 | // whole point of the line is whose work it is. |
| 748 | let theirs = res!(rehearsed(&b, &a)); |
| 749 | assert!(theirs.contains("flags"), |
| 750 | "the other end's rehearsal reaches the flag delta at all: {}", theirs); |
| 751 | assert!(!theirs.contains("of yours"), |
| 752 | "the other end is told its own work would be buried, and it is not: {}", theirs); |
| 753 | |
| 754 | // The rehearsal kept none of it, so the line is still this end's own. |
| 755 | assert_eq!(res!(std::fs::read(a.join("f.txt"))), b"alpha\nBETA from this end\ngamma\n".to_vec(), |
| 756 | "the rehearsal wrote the merged state into the working copy"); |
| 757 | |
| 758 | // And taking it does what the rehearsal said it would. |
| 759 | res!(synced(&a, &b)); |
| 760 | assert_eq!(res!(std::fs::read(a.join("f.txt"))), b"alpha\nBETA from the other end\ngamma\n".to_vec(), |
| 761 | "the sync did not bury the edit the rehearsal said it would"); |
| 762 | Ok(()) |
| 763 | } |