Oregami
Repositories/oxedyne/ore

oxedyne/ore/cli/tests/sync.rs

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
16mod support;
17
18use support::{
19 counted,
20 ore,
21 segment_path,
22 tree_of,
23 write,
24 Ran,
25 Scratch,
26};
27
28use ore_store::store::Lock;
29
30use oxedyne_fe2o3_core::prelude::*;
31
32use std::path::{
33 Path,
34 PathBuf,
35};
36
37
38/// Runs `ore sync <other>` in a directory and returns what it said.
39fn 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.
46fn 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.
54fn 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.
62fn 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.
75fn 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.
111fn 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]
131fn 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.
218fn 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]
239fn 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]
261fn 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]
311fn 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]
349fn 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]
380fn 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]
394fn 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]
417fn 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"), &copy.join(".ore")));
424 res!(write(&copy, "f.txt", b"alpha\nbeta\ngamma\n"));
425 res!(write(&copy, "notes.md", b"shared notes\n"));
426
427 let out = res!(sync(&a, &copy));
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]
443fn 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]
484fn 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]
526fn 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.
548fn 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]
577fn 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]
615fn 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]
659fn 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]
721fn 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}