The three hubs: memory, household, chat.
7use super::build::*;
Runs every memory-hub law.
10pub fn memory_hub<H: Hub<Document = MemorySnapshot>>(fresh: &impl Fn() -> H, restart: &impl Fn(H) -> H) { 11 info!("conformance: memory hub"); 12 memory_takes_facts_and_numbers_them(fresh()); 13 memory_merge_twice_changes_nothing(fresh()); 14 memory_merge_is_order_independent(fresh); 15 memory_forgotten_wins_and_stays_forgotten(fresh); 16 memory_ignores_facts_with_no_identity(fresh()); 17 memory_the_same_words_twice_are_one_fact(fresh()); 18 memory_an_embedding_computed_anywhere_serves_everywhere(fresh); 19 memory_current_changes_nothing(fresh()); 20 memory_a_hide_is_kept_and_the_later_write_wins(fresh); 21 memory_a_hidden_fact_is_not_a_forgotten_one(fresh); 22 memory_a_picture_chosen_anywhere_serves_everywhere(fresh); 23 memory_survives_a_restart(fresh, restart); 24}
26pub fn memory_takes_facts_and_numbers_them<H: Hub<Document = MemorySnapshot>>(hub: H) { 27 let m = run_ready(hub.merge(snapshot(vec![fact("g1", "Biscuit is a bunny"), fact("g2", "she loves purple")], &[]))).unwrap(); 28 assert_eq!(m.changed, 2, "two new facts"); 29 assert_eq!(m.document.facts.len(), 2); 30 let ids: std::collections::BTreeSet<u64> = m.document.facts.iter().map(|f| f.id).collect(); 31 assert_eq!(ids.len(), 2, "the hub numbers each fact differently"); 32 assert!(!ids.contains(&0), "and never with zero"); 33} 34 35pub fn memory_merge_twice_changes_nothing<H: Hub<Document = MemorySnapshot>>(hub: H) { 36 let theirs = snapshot(vec![fact("g1", "a bunny"), fact("g2", "purple")], &["g9"]); 37 let first = run_ready(hub.merge(theirs.clone())).unwrap(); 38 let second = run_ready(hub.merge(theirs)).unwrap(); 39 assert_eq!(second.changed, 0, "the hub already held everything sent"); 40 assert_eq!(gist(&first.document), gist(&second.document)); 41 assert_eq!(first.document, second.document, "not even the numbering moved"); 42} 43 44pub fn memory_merge_is_order_independent<H: Hub<Document = MemorySnapshot>>(fresh: &impl Fn() -> H) { 45 let a = snapshot(vec![fact("g1", "Biscuit is a bunny"), fact("g2", "she loves purple")], &[]); 46 // B learned the same thing as g1 in different spacing and case, and forgot g2. 47 let b = snapshot(vec![fact("g3", "BISCUIT is a bunny")], &["g2"]); 48 let c = snapshot(vec![with_embedding(fact("g1", "Biscuit is a bunny"), &[0.5]), fact("g4", "Nana lives by the sea")], &[]); 49 let mut expected = None; 50 for order in permutations(&[a, b, c]) { 51 let hub = fresh(); 52 let mut last = None; 53 for doc in order { 54 last = Some(run_ready(hub.merge(doc)).unwrap().document); 55 } 56 let g = gist(&last.unwrap()); 57 match &expected { 58 None => expected = Some(g), 59 Some(e) => assert_eq!(&g, e, "every order of the same documents converges to the same memory"), 60 } 61 } 62 let (facts, forgotten) = expected.unwrap(); 63 let gids: Vec<&str> = facts.iter().map(|(g, _, _)| g.as_str()).collect(); 64 assert_eq!(gids, ["g1", "g4"], "g2 was forgotten, and g3 was g1 learned twice"); 65 assert!(forgotten.contains("g2") && forgotten.contains("g3")); 66} 67 68pub fn memory_forgotten_wins_and_stays_forgotten<H: Hub<Document = MemorySnapshot>>(fresh: &impl Fn() -> H) { 69 let has = snapshot(vec![fact("g1", "a bunny")], &[]); 70 let forgets = snapshot(vec![], &["g1"]); 71 for order in [[has.clone(), forgets.clone()], [forgets, has.clone()]] { 72 let hub = fresh(); 73 for doc in order { 74 run_ready(hub.merge(doc)).unwrap(); 75 } 76 let again = run_ready(hub.merge(has.clone())).unwrap(); 77 assert!(again.document.facts.is_empty(), "a forgotten fact does not come back from a device that still has it"); 78 assert_eq!(again.changed, 0); 79 assert!(again.document.forgotten.contains(&"g1".to_owned())); 80 } 81}
The fact as a device holds it after she put it away at hide_at.
90fn restored(gid: &str, hide_at: u64, restore_at: u64) -> whiskers_core::Fact { 91 let mut f = fact(gid, "she has a toy bunny"); 92 f.visibility = Visibility::default().hidden_after(hide_at).restored_after(restore_at); 93 f 94} 95 96fn visibility_of(s: &MemorySnapshot, gid: &str) -> Visibility { 97 s.facts.iter().find(|f| f.gid == gid).expect("the fact is there").visibility 98}
Putting a fact away is a last-writer-wins register on the fact: kept, converging in every order, and a parent's restore (made after the hide, even by a slow clock) beats it while a later hide beats the restore.
102pub fn memory_a_hide_is_kept_and_the_later_write_wins<H: Hub<Document = MemorySnapshot>>(fresh: &impl Fn() -> H) { 103 let plain = snapshot(vec![fact("g1", "she has a toy bunny")], &[]); 104 let hid = snapshot(vec![away("g1", 100)], &[]); 105 let back = snapshot(vec![restored("g1", 100, 5)], &[]); // the parents' clock is behind: still later 106 let again = snapshot(vec![away("g1", 100)], &[]); 107 let rehid = { 108 let mut f = restored("g1", 100, 5); 109 f.visibility = f.visibility.hidden_after(1); 110 snapshot(vec![f], &[]) 111 }; 112 // The hide reaches a hub that holds the plain fact, in either order. 113 for order in [[plain.clone(), hid.clone()], [hid.clone(), plain.clone()]] { 114 let hub = fresh(); 115 let mut last = None; 116 for d in order { 117 last = Some(run_ready(hub.merge(d)).unwrap()); 118 } 119 let doc = last.unwrap().document; 120 assert!(visibility_of(&doc, "g1").is_hidden(), "a hide is kept whatever order it arrives in"); 121 assert_eq!(doc.facts.len(), 1, "and the fact is not deleted"); 122 assert_eq!(run_ready(hub.merge(hid.clone())).unwrap().changed, 0, "repeating it changes nothing"); 123 } 124 // The restore beats the hide it follows; a later hide beats the restore. 125 for order in permutations(&[plain.clone(), hid.clone(), back.clone(), again]) { 126 let hub = fresh(); 127 let mut last = None; 128 for d in order { 129 last = Some(run_ready(hub.merge(d)).unwrap().document); 130 } 131 assert!(!visibility_of(&last.unwrap(), "g1").is_hidden(), "the restore is the latest write in every order"); 132 } 133 for order in permutations(&[hid.clone(), back, rehid]) { 134 let hub = fresh(); 135 let mut last = None; 136 for d in order { 137 last = Some(run_ready(hub.merge(d)).unwrap().document); 138 } 139 assert!(visibility_of(&last.unwrap(), "g1").is_hidden(), "and her hiding it again is later still"); 140 } 141}
A fact's picture chosen on any device serves all of them, and two choices meet at the smaller name whatever order they arrive in.
145pub fn memory_a_picture_chosen_anywhere_serves_everywhere<H: Hub<Document = MemorySnapshot>>(fresh: &impl Fn() -> H) { 146 let pictured = |name: &str| { 147 let mut f = fact("g1", "she has a toy bunny"); 148 f.icon = Some(IconId::new(name).unwrap()); 149 snapshot(vec![f], &[]) 150 }; 151 let plain = snapshot(vec![fact("g1", "she has a toy bunny")], &[]); 152 let hub = fresh(); 153 run_ready(hub.merge(plain.clone())).unwrap(); 154 let m = run_ready(hub.merge(pictured("rabbit-white"))).unwrap(); 155 assert_eq!(m.changed, 1); 156 assert_eq!(m.document.facts[0].icon.as_ref().map(IconId::as_str), Some("rabbit-white")); 157 let m = run_ready(hub.merge(plain.clone())).unwrap(); 158 assert_eq!((m.changed, m.document.facts[0].icon.is_some()), (0, true), "a copy with no picture does not take it away"); 159 for order in permutations(&[plain, pictured("rabbit-white"), pictured("rabbit-grey")]) { 160 let hub = fresh(); 161 let mut last = None; 162 for d in order { 163 last = Some(run_ready(hub.merge(d)).unwrap().document); 164 } 165 assert_eq!(last.unwrap().facts[0].icon.as_ref().map(IconId::as_str), Some("rabbit-grey"), "the smaller name, in every order"); 166 } 167}
What she puts away is not what the parents forget: forgetting wins over everything and is permanent, hiding never tombstones.
171pub fn memory_a_hidden_fact_is_not_a_forgotten_one<H: Hub<Document = MemorySnapshot>>(fresh: &impl Fn() -> H) { 172 let hub = fresh(); 173 let m = run_ready(hub.merge(snapshot(vec![away("g1", 10)], &[]))).unwrap(); 174 assert!(m.document.forgotten.is_empty(), "hiding does not forget"); 175 assert!(m.document.facts[0].visibility.is_hidden()); 176 // Forgotten beats a hide that arrives after it, and one that arrives before. 177 for order in [[snapshot(vec![away("g1", 10)], &[]), snapshot(vec![], &["g1"])], [snapshot(vec![], &["g1"]), snapshot(vec![away("g1", 99)], &[])]] { 178 let hub = fresh(); 179 let mut last = None; 180 for d in order { 181 last = Some(run_ready(hub.merge(d)).unwrap().document); 182 } 183 let doc = last.unwrap(); 184 assert!(doc.facts.is_empty() && doc.forgotten.contains(&"g1".to_owned())); 185 } 186}
188pub fn memory_ignores_facts_with_no_identity<H: Hub<Document = MemorySnapshot>>(hub: H) { 189 let m = run_ready(hub.merge(snapshot(vec![fact("", "no identity yet")], &[]))).unwrap(); 190 assert_eq!((m.changed, m.document.facts.len()), (0, 0)); 191} 192 193pub fn memory_the_same_words_twice_are_one_fact<H: Hub<Document = MemorySnapshot>>(hub: H) { 194 let m = run_ready(hub.merge(snapshot(vec![fact("g8", "Likes CARROTS"), fact("g2", "likes carrots")], &[]))).unwrap(); 195 assert_eq!(m.document.facts.len(), 1); 196 assert_eq!(m.document.facts[0].gid, "g2", "the smaller identity survives, on every device"); 197 assert!(m.document.forgotten.contains(&"g8".to_owned()), "and the other is forgotten everywhere"); 198} 199 200pub fn memory_an_embedding_computed_anywhere_serves_everywhere<H: Hub<Document = MemorySnapshot>>(fresh: &impl Fn() -> H) { 201 let hub = fresh(); 202 run_ready(hub.merge(snapshot(vec![fact("g1", "a bunny")], &[]))).unwrap(); 203 let m = run_ready(hub.merge(snapshot(vec![with_embedding(fact("g1", "a bunny"), &[1.0, 2.0])], &[]))).unwrap(); 204 assert_eq!(m.changed, 1); 205 assert_eq!(m.document.facts[0].embedding, vec![1.0, 2.0]); 206 let m = run_ready(hub.merge(snapshot(vec![with_embedding(fact("g1", "a bunny"), &[9.0])], &[]))).unwrap(); 207 assert_eq!(m.document.facts[0].embedding, vec![1.0, 2.0], "the first embedding is kept"); 208} 209 210pub fn memory_current_changes_nothing<H: Hub<Document = MemorySnapshot>>(hub: H) { 211 assert_eq!(run_ready(hub.current()).unwrap(), MemorySnapshot::default(), "an empty hub holds an empty memory"); 212 let m = run_ready(hub.merge(snapshot(vec![fact("g1", "a bunny")], &[]))).unwrap(); 213 assert_eq!(run_ready(hub.current()).unwrap(), m.document); 214 assert_eq!(run_ready(hub.current()).unwrap(), m.document, "reading twice reads the same"); 215} 216 217pub fn memory_survives_a_restart<H: Hub<Document = MemorySnapshot>>(fresh: &impl Fn() -> H, restart: &impl Fn(H) -> H) { 218 let hub = fresh(); 219 let m = run_ready(hub.merge(snapshot(vec![fact("g1", "a bunny")], &["g7"]))).unwrap(); 220 let hub = restart(hub); 221 assert_eq!(run_ready(hub.current()).unwrap(), m.document, "what a merge reported is what is still there"); 222 // And the numbering carries on rather than starting again. 223 let m2 = run_ready(hub.merge(snapshot(vec![fact("g2", "purple")], &[]))).unwrap(); 224 let ids: std::collections::BTreeSet<u64> = m2.document.facts.iter().map(|f| f.id).collect(); 225 assert_eq!(ids.len(), 2, "a restarted hub does not hand out an id twice"); 226}
Runs every household-hub law.
229pub fn household_hub<H: Hub<Document = Household>>(fresh: &impl Fn() -> H, restart: &impl Fn(H) -> H) { 230 info!("conformance: household hub"); 231 household_takes_the_later_choice(fresh); 232 household_merge_twice_changes_nothing(fresh()); 233 household_merge_is_order_independent(fresh); 234 household_time_is_summed_across_devices_and_a_later_day_wins(fresh()); 235 household_survives_a_restart(fresh, restart); 236}
238pub fn household_takes_the_later_choice<H: Hub<Document = Household>>(fresh: &impl Fn() -> H) { 239 let hub = fresh(); 240 let newer = household(10, 150, 3, 1, &[]); 241 let m = run_ready(hub.merge(newer.clone())).unwrap(); 242 assert_eq!(m.changed, 1); 243 assert_eq!(m.document, newer); 244 let older = household(5, 999, 9, 1, &[]); 245 let m = run_ready(hub.merge(older)).unwrap(); 246 assert_eq!(m.changed, 0, "an older choice changes nothing"); 247 assert_eq!(m.document.config().tokens.per_window(), Some(150)); 248 assert_eq!(m.document.config().voice_daily_chars, 3); 249} 250 251pub fn household_merge_twice_changes_nothing<H: Hub<Document = Household>>(hub: H) { 252 let theirs = household(10, 150, 3, 1, &[("a", 1000)]); 253 let first = run_ready(hub.merge(theirs.clone())).unwrap(); 254 let second = run_ready(hub.merge(theirs)).unwrap(); 255 assert_eq!(second.changed, 0, "the hub already held everything sent"); 256 assert_eq!(first.document, second.document); 257} 258 259pub fn household_merge_is_order_independent<H: Hub<Document = Household>>(fresh: &impl Fn() -> H) { 260 let docs = [ 261 household(10, 150, 3, 1, &[("phone", 4000)]), 262 household(20, 500, 7, 1, &[("tablet", 9000), ("phone", 1000)]), 263 household(15, 300, 5, 2, &[("phone", 100)]), 264 ]; 265 let mut expected: Option<Household> = None; 266 for order in permutations(&docs) { 267 let hub = fresh(); 268 let mut last = None; 269 for d in order { 270 last = Some(run_ready(hub.merge(d)).unwrap().document); 271 } 272 let last = last.unwrap(); 273 match &expected { 274 None => expected = Some(last), 275 Some(e) => assert_eq!(&last, e, "every order of the same documents converges to the same household"), 276 } 277 } 278 let e = expected.unwrap(); 279 assert_eq!(e.config().voice_daily_chars, 7, "the latest choice"); 280 assert_eq!(e.config().tokens.per_window(), Some(500)); 281} 282 283pub fn household_time_is_summed_across_devices_and_a_later_day_wins<H: Hub<Document = Household>>(hub: H) { 284 run_ready(hub.merge(household(1, 150, 3, 4, &[("phone", 4000)]))).unwrap(); 285 let m = run_ready(hub.merge(household(1, 150, 3, 4, &[("tablet", 9000), ("phone", 1000)]))).unwrap(); 286 assert_eq!(m.document, household(1, 150, 3, 4, &[("phone", 4000), ("tablet", 9000)]), "per device the larger count, never the sum of the same count twice"); 287 let m = run_ready(hub.merge(household(1, 150, 3, 5, &[("phone", 10)]))).unwrap(); 288 assert_eq!(m.document, household(1, 150, 3, 5, &[("phone", 10)]), "a new day leaves the old time behind"); 289 let m = run_ready(hub.merge(household(1, 150, 3, 4, &[("phone", 99_000)]))).unwrap(); 290 assert_eq!(m.changed, 0, "and an earlier day cannot bring it back"); 291} 292 293pub fn household_survives_a_restart<H: Hub<Document = Household>>(fresh: &impl Fn() -> H, restart: &impl Fn(H) -> H) { 294 let hub = fresh(); 295 let m = run_ready(hub.merge(household(10, 150, 3, 1, &[("a", 1000)]))).unwrap(); 296 let hub = restart(hub); 297 assert_eq!(run_ready(hub.current()).unwrap(), m.document); 298}
How a test makes a hub's storage refuse writes (true) and take them again (false). Every adapter
has its own way to fail; the law only needs to be able to ask.
302pub type WriteSwitch = Box<dyn Fn(bool)>;
Runs every chat-hub law. broken makes a hub whose storage can be told to refuse writes (it may be
a different type from fresh's, since it is the same adapter on a storage that can fail), and
restart_broken is its restart.
307pub fn chat_hub<H: Hub<Document = ChatState>, B: Hub<Document = ChatState>>( 308 fresh: &impl Fn() -> H, 309 restart: &impl Fn(H) -> H, 310 broken: &impl Fn() -> (B, WriteSwitch), 311 restart_broken: &impl Fn(B) -> B, 312) { 313 chat_hub_whose_storage_works(fresh, restart); 314 chat_a_copy_that_cannot_be_kept_is_an_error_and_changes_nothing(broken, restart_broken); 315}
The chat-hub laws that need nothing but a hub and a restart: every chat-hub law except the one that makes the storage refuse writes (an adapter that cannot make its storage fail, as a black box over HTTP cannot, runs these and not that one).
A merge that cannot be kept is an Err, never an Ok carrying a copy the hub does not hold. What
the hub reports from then on is what it durably saved, and a restart finds the same; the device
that sent the copy can send it again once storage works.
330pub fn chat_a_copy_that_cannot_be_kept_is_an_error_and_changes_nothing<H: Hub<Document = ChatState>>( 331 broken: &impl Fn() -> (H, WriteSwitch), 332 restart: &impl Fn(H) -> H, 333) { 334 let (hub, refuse_writes) = broken(); 335 let two = chat(2, 5, "kept"); 336 assert_eq!(run_ready(hub.merge(two.clone())).unwrap().changed, 1); 337 338 refuse_writes(true); 339 assert!(run_ready(hub.merge(chat(3, 9, "lost"))).is_err(), "a copy that cannot be saved is not answered as if it had been"); 340 assert_eq!(run_ready(hub.current()).unwrap(), two, "memory holds what was saved, not what was refused"); 341 let m = run_ready(hub.merge(chat(1, 99, "older"))).unwrap(); 342 assert_eq!((m.changed, m.document), (0, two.clone()), "a copy that needs no write needs no storage"); 343 344 let hub = restart(hub); 345 assert_eq!(run_ready(hub.current()).unwrap(), two, "and a restart finds the same"); 346 347 refuse_writes(false); 348 let m = run_ready(hub.merge(chat(3, 9, "again"))).unwrap(); 349 assert_eq!((m.changed, m.document.summary.as_str()), (1, "again"), "once storage works the same copy is taken"); 350}
352pub fn chat_the_newer_copy_wins_whole<H: Hub<Document = ChatState>>(hub: H) { 353 assert_eq!(run_ready(hub.current()).unwrap(), ChatState::default()); 354 let two = chat(2, 5, "s"); 355 let m = run_ready(hub.merge(two.clone())).unwrap(); 356 assert_eq!((m.changed, m.document.clone()), (1, two.clone())); 357 let m = run_ready(hub.merge(chat(1, 99, "old"))).unwrap(); 358 assert_eq!((m.changed, m.document), (0, two.clone()), "a copy with fewer changes loses even if it was active later"); 359 let m = run_ready(hub.merge(chat(2, 9, "same version, later"))).unwrap(); 360 assert_eq!((m.changed, m.document.summary.as_str()), (1, "same version, later"), "on a tie the more recently active wins"); 361} 362 363pub fn chat_merge_twice_changes_nothing<H: Hub<Document = ChatState>>(hub: H) { 364 let c = chat(3, 7, "s"); 365 let first = run_ready(hub.merge(c.clone())).unwrap(); 366 let second = run_ready(hub.merge(c)).unwrap(); 367 assert_eq!((first.changed, second.changed), (1, 0)); 368 assert_eq!(first.document, second.document); 369} 370 371pub fn chat_survives_a_restart<H: Hub<Document = ChatState>>(fresh: &impl Fn() -> H, restart: &impl Fn(H) -> H) { 372 let hub = fresh(); 373 let m = run_ready(hub.merge(chat(4, 8, "kept"))).unwrap(); 374 let hub = restart(hub); 375 assert_eq!(run_ready(hub.current()).unwrap(), m.document); 376}