oxedyne/fe2o3/fe2o3_ore/src/seq/forget_tests.rs
10.5 KiB, 3 runs
created by r1870400018:38194, 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 | //! Forgetting: an operation loses its content and keeps its shape, and every |
| 2 | //! replica renders the same whether it still holds the bytes or only the stub. |
| 3 | //! |
| 4 | //! The property under test is the one the whole design rests on -- that the |
| 5 | //! render is a function of the operation set, forgets included, and of nothing |
| 6 | //! else. So every case is run under every delivery order, and the case that |
| 7 | //! matters most puts an original on one replica and a repack's stub on another |
| 8 | //! and asks for byte equality. |
| 9 | |
| 10 | use crate::id::{ |
| 11 | OpId, |
| 12 | ReplicaId, |
| 13 | }; |
| 14 | use crate::op::{ |
| 15 | Header, |
| 16 | Op, |
| 17 | Placing, |
| 18 | Stub, |
| 19 | }; |
| 20 | use crate::seq::tests::{ |
| 21 | case, |
| 22 | converge, |
| 23 | seed, |
| 24 | Replica, |
| 25 | }; |
| 26 | use crate::seq::Sequence; |
| 27 | |
| 28 | use oxedyne_fe2o3_core::prelude::*; |
| 29 | |
| 30 | |
| 31 | /// A forget of an operation, as a command would author it: the shape read off |
| 32 | /// the operation itself. |
| 33 | fn forget_of(rep: &mut Replica, targets: &[(OpId, &Op)]) |
| 34 | -> Outcome<(Header, Op)> |
| 35 | { |
| 36 | let mut of: Vec<Stub> = Vec::new(); |
| 37 | for (id, op) in targets { |
| 38 | let placing = res!(op.stub_of().ok_or_else(|| err!( |
| 39 | "{} has nothing to forget", op.name(); Test))); |
| 40 | of.push(Stub { id: *id, placing }); |
| 41 | } |
| 42 | of.sort_by_key(|s| s.id); |
| 43 | rep.author(Op::Forget { of, reason: b"a test".to_vec(), time: 1_755_000_000 }) |
| 44 | } |
| 45 | |
| 46 | /// A forgotten insertion's bytes are gone from every state, and an edit made |
| 47 | /// inside it afterwards still renders where it was made. |
| 48 | #[test] |
| 49 | fn a_forgotten_insertion_is_buried_and_what_was_anchored_in_it_survives() -> Outcome<()> { |
| 50 | let mut st = res!(seed(b"one two three\n", 1)); |
| 51 | let rep = &mut st.reps[0]; |
| 52 | // The secret goes in, and a word is then written inside it. |
| 53 | let secret = res!(rep.insert(4, b"SECRET ")); |
| 54 | let inside = res!(rep.insert(4 + 3, b"[x]")); |
| 55 | assert_eq!(res!(rep.view()).text_lossy(), "one SEC[x]RET two three\n"); |
| 56 | let forget = res!(forget_of(rep, &[(secret.0.id(), &secret.1)])); |
| 57 | let mut ops = st.ops.clone(); |
| 58 | ops.extend([secret, inside, forget]); |
| 59 | // The secret's bytes are gone; the word written inside it stands where the |
| 60 | // secret stood, since its anchors still name the buried bytes. |
| 61 | let got = res!(case(st.file, "one [x]two three\n", &ops)); |
| 62 | assert!(!got.bytes().windows(6).any(|w| w == b"SECRET"), "the bytes are gone"); |
| 63 | Ok(()) |
| 64 | } |
| 65 | |
| 66 | /// A replica holding the original beside the forget and one holding only the |
| 67 | /// stub a repack wrote render byte for byte the same, in every delivery order. |
| 68 | #[test] |
| 69 | fn a_stub_and_an_original_render_the_same() -> Outcome<()> { |
| 70 | let mut st = res!(seed(b"alpha beta\n", 1)); |
| 71 | let rep = &mut st.reps[0]; |
| 72 | let secret = res!(rep.insert(6, b"KEY=abc ")); |
| 73 | let after = res!(rep.insert(6 + 8, b"gamma ")); |
| 74 | let forget = res!(forget_of(rep, &[(secret.0.id(), &secret.1)])); |
| 75 | let placing = res!(secret.1.stub_of().ok_or_else(|| err!("no shape"; Test))); |
| 76 | |
| 77 | let mut with_original = st.ops.clone(); |
| 78 | with_original.extend([secret.clone(), after.clone(), forget.clone()]); |
| 79 | let mut with_stub = st.ops.clone(); |
| 80 | with_stub.extend([ |
| 81 | (secret.0.clone(), Op::Forgotten { placing }), |
| 82 | after, |
| 83 | forget, |
| 84 | ]); |
| 85 | let a = res!(case(st.file, "alpha gamma beta\n", &with_original)); |
| 86 | let b = res!(case(st.file, "alpha gamma beta\n", &with_stub)); |
| 87 | assert_eq!(a.bytes(), b.bytes()); |
| 88 | assert_eq!(a.runs(), b.runs(), "and the same provenance runs"); |
| 89 | Ok(()) |
| 90 | } |
| 91 | |
| 92 | /// A forgotten file has no path and is not live, its content is gone, and an |
| 93 | /// edit into it afterwards is complete rather than an error. |
| 94 | #[test] |
| 95 | fn a_forgotten_file_is_dead_from_birth() -> Outcome<()> { |
| 96 | let mut st = res!(seed(b"public\n", 1)); |
| 97 | let rep = &mut st.reps[0]; |
| 98 | let create = res!(rep.author(Op::FileCreate { path: b"keys/prod.pem".to_vec() })); |
| 99 | let key = create.0.id(); |
| 100 | let fill = { |
| 101 | let repo = res!(rep.seq.render()); |
| 102 | let f = res!(repo.file(key).ok_or_else(|| err!("no file"; Test))); |
| 103 | let op = res!(f.splice(0, 0, b"-----BEGIN".to_vec())); |
| 104 | res!(rep.author(op)) |
| 105 | }; |
| 106 | let forget = res!(forget_of(rep, &[(key, &create.1), (fill.0.id(), &fill.1)])); |
| 107 | let late = { |
| 108 | let repo = res!(rep.seq.render()); |
| 109 | let f = res!(repo.file(key).ok_or_else(|| err!("no file after forget"; Test))); |
| 110 | let op = res!(f.splice(0, 0, b"late".to_vec())); |
| 111 | res!(rep.author(op)) |
| 112 | }; |
| 113 | let mut ops = st.ops.clone(); |
| 114 | ops.extend([create, fill, forget, late]); |
| 115 | let repo = res!(converge(&ops)); |
| 116 | let f = res!(repo.file(key).ok_or_else(|| err!("the file is still a file"; Test))); |
| 117 | assert!(!f.is_live(), "and not a live one"); |
| 118 | assert!(f.path().is_empty(), "with no path to be laid out under"); |
| 119 | assert!(!f.bytes().windows(5).any(|w| w == b"BEGIN"), "its content gone"); |
| 120 | let public = res!(repo.file(st.file).ok_or_else(|| err!("no public file"; Test))); |
| 121 | assert_eq!(public.text_lossy(), "public\n", "the other file untouched"); |
| 122 | Ok(()) |
| 123 | } |
| 124 | |
| 125 | /// A forget arriving before the operation it names holds the shape ready, and |
| 126 | /// the original arriving later is held in that shape and not as itself. |
| 127 | #[test] |
| 128 | fn a_forget_arriving_first_still_shapes_what_comes_after() -> Outcome<()> { |
| 129 | let mut st = res!(seed(b"ab\n", 1)); |
| 130 | let rep = &mut st.reps[0]; |
| 131 | let secret = res!(rep.insert(1, b"XX")); |
| 132 | let forget = res!(forget_of(rep, &[(secret.0.id(), &secret.1)])); |
| 133 | let mut seq = Sequence::new(); |
| 134 | for (h, o) in &st.ops { |
| 135 | res!(seq.apply(h.clone(), o.clone())); |
| 136 | } |
| 137 | res!(seq.apply(forget.0.clone(), forget.1.clone())); |
| 138 | res!(seq.apply(secret.0.clone(), secret.1.clone())); |
| 139 | match seq.get(&secret.0.id()) { |
| 140 | Some(Op::Forgotten { .. }) => (), |
| 141 | other => return Err(err!( |
| 142 | "the original is held as {:?}, not as its stub", other.map(|o| o.name()); Test)), |
| 143 | } |
| 144 | let repo = res!(seq.render()); |
| 145 | let f = res!(repo.file(st.file).ok_or_else(|| err!("no file"; Test))); |
| 146 | assert_eq!(f.text_lossy(), "ab\n"); |
| 147 | Ok(()) |
| 148 | } |
| 149 | |
| 150 | /// Two forgets disagreeing about the shape one operation keeps are refused, |
| 151 | /// because a forgotten operation keeps one shape everywhere. |
| 152 | #[test] |
| 153 | fn two_forgets_disagreeing_on_a_shape_are_refused() -> Outcome<()> { |
| 154 | let mut st = res!(seed(b"ab\n", 2)); |
| 155 | let secret = res!(st.reps[0].insert(1, b"XX")); |
| 156 | res!(st.reps[1].recv(secret.clone())); |
| 157 | let honest = res!(forget_of(&mut st.reps[0], &[(secret.0.id(), &secret.1)])); |
| 158 | // The second replica claims a different shape for the same operation. |
| 159 | let dishonest = res!(st.reps[1].author(Op::Forget { |
| 160 | of: vec![Stub { id: secret.0.id(), placing: Placing::Void }], |
| 161 | reason: Vec::new(), |
| 162 | time: 0, |
| 163 | })); |
| 164 | let mut seq = Sequence::new(); |
| 165 | for (h, o) in st.ops.iter().chain([&secret, &honest]) { |
| 166 | res!(seq.apply(h.clone(), o.clone())); |
| 167 | } |
| 168 | assert!(seq.apply(dishonest.0.clone(), dishonest.1.clone()).is_err()); |
| 169 | // And the other way about, through absorb. |
| 170 | let mut other = Sequence::new(); |
| 171 | for (h, o) in st.ops.iter().chain([&secret, &dishonest]) { |
| 172 | res!(other.apply(h.clone(), o.clone())); |
| 173 | } |
| 174 | assert!(seq.absorb(&other).is_err()); |
| 175 | Ok(()) |
| 176 | } |
| 177 | |
| 178 | /// The source of a move can be forgotten, and the moved bytes are gone from the |
| 179 | /// destination they were carried to, in every delivery order. |
| 180 | #[test] |
| 181 | fn qa_forgetting_a_moved_ranges_source_buries_it_at_the_destination() -> Outcome<()> { |
| 182 | let mut st = res!(seed(b"one two three\n", 1)); |
| 183 | let rep = &mut st.reps[0]; |
| 184 | // The secret goes in, is then carried to the front, and is then forgotten. |
| 185 | let secret = res!(rep.insert(4, b"SECRET ")); |
| 186 | assert_eq!(res!(rep.view()).text_lossy(), "one SECRET two three\n"); |
| 187 | let mv = { |
| 188 | let op = res!(res!(rep.view()).move_range(4, 7, 0)); |
| 189 | res!(rep.author(op)) |
| 190 | }; |
| 191 | assert_eq!(res!(rep.view()).text_lossy(), "SECRET one two three\n"); |
| 192 | let forget = res!(forget_of(rep, &[(secret.0.id(), &secret.1)])); |
| 193 | let mut ops = st.ops.clone(); |
| 194 | ops.extend([secret, mv, forget]); |
| 195 | // The bytes are buried wherever they came to rest, so the file is what it was. |
| 196 | let got = res!(case(st.file, "one two three\n", &ops)); |
| 197 | assert!(!got.bytes().windows(6).any(|w| w == b"SECRET"), "the bytes are gone"); |
| 198 | Ok(()) |
| 199 | } |
| 200 | |
| 201 | /// A deletion that spans the junction of a forgotten insertion and the text after |
| 202 | /// it keeps the part of the cut that fell outside the buried bytes. |
| 203 | #[test] |
| 204 | fn qa_a_cut_across_a_forgotten_boundary_still_removes_the_outside_byte() -> Outcome<()> { |
| 205 | let mut st = res!(seed(b"abc\n", 1)); |
| 206 | let rep = &mut st.reps[0]; |
| 207 | let secret = res!(rep.insert(1, b"SECRET")); |
| 208 | assert_eq!(res!(rep.view()).text_lossy(), "aSECRETbc\n"); |
| 209 | // Cut "Tb": the last byte of the secret and the first original byte after it. |
| 210 | let cut = { |
| 211 | let op = res!(res!(rep.view()).splice(6, 2, Vec::new())); |
| 212 | res!(rep.author(op)) |
| 213 | }; |
| 214 | assert_eq!(res!(rep.view()).text_lossy(), "aSECREc\n"); |
| 215 | let forget = res!(forget_of(rep, &[(secret.0.id(), &secret.1)])); |
| 216 | let mut ops = st.ops.clone(); |
| 217 | ops.extend([secret, cut, forget]); |
| 218 | // The secret is buried whole; the cut's removal of the original 'b' still |
| 219 | // stands, since forgetting bytes does not bring back what a later edit took. |
| 220 | res!(case(st.file, "ac\n", &ops)); |
| 221 | Ok(()) |
| 222 | } |
| 223 | |
| 224 | /// The bytes two concurrent moves both claim can be forgotten, and conservation |
| 225 | /// holds although one move yields the run to the other, in every delivery order. |
| 226 | #[test] |
| 227 | fn qa_forgetting_a_run_two_moves_contend_for_conserves() -> Outcome<()> { |
| 228 | let mut st = res!(seed(b"- Eggs\n- Milk\n- Cheese\n", 2)); |
| 229 | let seed_op = st.ops[1].clone(); // the splice that wrote the list |
| 230 | let (r1, r2) = st.reps.split_at_mut(1); |
| 231 | // Both replicas move "- Milk\n" (seed bytes 7..14); one wins, one yields. |
| 232 | let lost = { |
| 233 | let op = res!(res!(r1[0].view()).move_range(7, 7, 0)); |
| 234 | res!(r1[0].author(op)) |
| 235 | }; |
| 236 | let won = { |
| 237 | let op = res!(res!(r2[0].view()).move_range(7, 7, 22)); |
| 238 | res!(r2[0].author(op)) |
| 239 | }; |
| 240 | let forget = res!(forget_of(&mut r1[0], &[(seed_op.0.id(), &seed_op.1)])); |
| 241 | let mut ops = st.ops.clone(); |
| 242 | ops.extend([lost, won, forget]); |
| 243 | // Every byte the seed wrote is buried, so the file is empty; the overlap of the |
| 244 | // two moves must still balance under conservation, which `case` checks in every |
| 245 | // delivery order. |
| 246 | let got = res!(case(st.file, "", &ops)); |
| 247 | assert!(got.bytes().is_empty(), "the whole list is buried"); |
| 248 | Ok(()) |
| 249 | } |
| 250 | |
| 251 | /// What each kind of operation keeps when forgotten, and which have nothing to |
| 252 | /// forget. |
| 253 | #[test] |
| 254 | fn each_kind_keeps_the_shape_the_rule_says() -> Outcome<()> { |
| 255 | let r = ReplicaId::new(1); |
| 256 | let file = OpId::new(r, 1); |
| 257 | for op in crate::op::tests::samples() { |
| 258 | let kept = op.stub_of(); |
| 259 | match &op { |
| 260 | Op::FileCreate { .. } => assert_eq!(kept, Some(Placing::File), "{}", op.name()), |
| 261 | Op::Splice { left, right, remove, insert } => assert_eq!(kept, Some(Placing::Splice { |
| 262 | left: left.clone(), right: right.clone(), remove: remove.clone(), |
| 263 | len: insert.len() as u64, |
| 264 | }), "{}", op.name()), |
| 265 | Op::FileRename { .. } | Op::Mark { .. } | Op::Note { .. } | Op::Proposal { .. } |
| 266 | | Op::Said { .. } | Op::Amended { .. } |
| 267 | => assert_eq!(kept, Some(Placing::Void), "{}", op.name()), |
| 268 | _ => assert_eq!(kept, None, "{} has nothing to forget", op.name()), |
| 269 | } |
| 270 | } |
| 271 | let _ = file; |
| 272 | Ok(()) |
| 273 | } |