1use std::path::PathBuf; 2 3use whiskers_core::*; 4 5fn dir(name: &str) -> PathBuf { 6 let d = std::env::temp_dir().join(format!("whiskers-{name}-{}", std::process::id())); 7 let _ = std::fs::remove_dir_all(&d); 8 std::fs::create_dir_all(&d).unwrap(); 9 d 10} 11 12#[test] 13fn the_jsonl_log_round_trips_and_reads_back_past_a_torn_line() { 14 let d = dir("log"); 15 let path = d.join("log.jsonl"); 16 let mut log = JsonlLog::open(&path).unwrap(); 17 let e = Entry { at_ms: 7, event: Event::Heard { text: "toy bunny".into(), pictures: vec![] } }; 18 log.append(&e).unwrap(); 19 log.append(&e).unwrap(); 20 std::fs::OpenOptions::new().append(true).open(&path).unwrap().write_all_torn(); 21 let (entries, unreadable) = read_log(&path).unwrap(); 22 assert_eq!(entries, vec![e.clone(), e]); 23 assert_eq!(unreadable, 1); 24} 25 26trait Torn { 27 fn write_all_torn(self); 28} 29impl Torn for std::fs::File { 30 fn write_all_torn(mut self) { 31 use std::io::Write; 32 self.write_all(b"{\"at_ms\":9,\"event\":{\"Hea").unwrap(); 33 } 34} 35 36#[test] 37fn memory_survives_a_restart_and_ids_never_repeat() { 38 let d = dir("memory"); 39 let path = d.join("memory.json"); 40 let mut m = JsonMemory::open(&path).unwrap(); 41 let new = |t: &str| NewFact { text: t.into(), ..NewFact::default() }; 42 let a = m.add(new("one"), 1).unwrap(); 43 let b = m.add(new("two"), 2).unwrap(); 44 m.forget(b.id).unwrap(); 45 let c = m.add(new("three"), 3).unwrap(); 46 assert!(c.id > b.id && c.id > a.id); 47 let again = JsonMemory::open(&path).unwrap(); 48 assert_eq!(again.facts().iter().map(|f| f.text.as_str()).collect::<Vec<_>>(), ["one", "three"]); 49} 50 51#[test] 52fn a_damaged_memory_file_is_an_error_not_an_empty_memory() { 53 let d = dir("damaged"); 54 let path = d.join("memory.json"); 55 std::fs::write(&path, "{ not json").unwrap(); 56 assert!(JsonMemory::open(&path).is_err()); 57} 58 59#[test] 60fn pictures_are_kept_as_distinct_files() { 61 let d = dir("pictures"); 62 let mut p = DirPictures::open(&d).unwrap(); 63 let img = Image { media_type: "image/jpeg".into(), bytes: vec![1, 2, 3] }; 64 let a = p.save(&img).unwrap(); 65 let b = p.save(&img).unwrap(); 66 assert_ne!(a, b); 67 assert_eq!(std::fs::read(p.path_of(&a)).unwrap(), vec![1, 2, 3]); 68 assert!(a.0.ends_with(".jpg")); 69} 70 71#[test] 72fn a_picture_id_cannot_point_outside_the_directory() { 73 let d = dir("escape"); 74 let p = DirPictures::open(&d).unwrap(); 75 let path = p.path_of(&PictureId("../../etc/passwd".into())); 76 assert!(path.starts_with(&d)); 77} 78 79#[test] 80fn a_digest_orders_the_day_and_puts_needs_a_grown_up_first() { 81 let h = |at, text: &str| Entry { at_ms: at, event: Event::Heard { text: text.into(), pictures: vec![] } }; 82 let said = |at, text: &str, outcome| Entry { at_ms: at, event: Event::Said { text: text.into(), outcome } }; 83 let entries = vec![ 84 h(10, "my bunny is Biscuit"), 85 said(11, "Lovely!", Outcome::Answered), 86 h(20, "my tummy hurts"), 87 Entry { at_ms: 21, event: Event::Guarded { direction: Direction::FromChild, verdict: Verdict::Refuse { reason: "hurt".into(), kind: RefusalKind::NeedsAGrownUp } } }, 88 said(22, "Let's find Mommy.", Outcome::Fallback(Fallback::NeedsAGrownUp)), 89 Entry { at_ms: 23, event: Event::Remembered { fact: "bunny is Biscuit".into() } }, 90 h(500, "tomorrow"), 91 said(501, "ok", Outcome::Answered), 92 ]; 93 let d = Digest::between(&entries, 0, 100); 94 assert_eq!(d.exchanges.len(), 2); 95 assert_eq!(d.counts(), (1, 1)); 96 let urgent = d.needs_a_grown_up(); 97 assert_eq!(urgent.len(), 1); 98 assert_eq!(urgent[0].heard, "my tummy hurts"); 99 assert_eq!(urgent[0].notes.len(), 1); 100 assert_eq!(d.facts_learned, vec!["bunny is Biscuit".to_owned()]); 101 let ada = Audience::new(Some(&Child { name: ChildName::new("Ada").unwrap(), age: Age::new(6).unwrap() })); 102 assert!(d.transcript(&ada).contains("Ada: my bunny is Biscuit")); 103 assert!(d.transcript(&Audience::new(None)).contains("Child: my bunny is Biscuit")); 104} 105 106#[test] 107fn a_turn_the_log_never_finished_is_shown_not_hidden() { 108 let entries = vec![Entry { at_ms: 5, event: Event::Heard { text: "cut off".into(), pictures: vec![] } }]; 109 let d = Digest::between(&entries, 0, 100); 110 assert_eq!(d.exchanges.len(), 1); 111 assert_ne!(d.exchanges[0].outcome, Outcome::Answered); 112} 113 114#[test] 115fn an_embedding_and_the_rest_of_a_fact_survive_a_restart() { 116 let d = dir("embedding"); 117 let path = d.join("memory.json"); 118 let mut m = JsonMemory::open(&path).unwrap(); 119 let f = m 120 .add( 121 NewFact { text: "Biscuit is a bunny".into(), kind: Kind::Toy, who: vec!["Biscuit".into()], place: Some("Nana's".into()), when: None, pictures: vec![PictureId("p.jpg".into())] }, 122 5, 123 ) 124 .unwrap(); 125 m.set_embedding(f.id, vec![0.5, 0.25]).unwrap(); 126 let again = JsonMemory::open(&path).unwrap().facts().remove(0); 127 assert_eq!((again.kind, again.embedding, again.who, again.place), (Kind::Toy, vec![0.5, 0.25], vec!["Biscuit".to_string()], Some("Nana's".into()))); 128} 129 130#[test] 131fn a_fact_without_its_optional_fields_opens_with_their_defaults() { 132 let d = dir("optional"); 133 let path = d.join("memory.json"); 134 std::fs::write(&path, r#"{"next_id":2,"facts":[{"id":1,"text":"A fact","learned_at_ms":3,"gid":"g-1"}]}"#).unwrap(); 135 let f = JsonMemory::open(&path).unwrap().facts().remove(0); 136 assert_eq!((f.kind, f.embedding.len()), (Kind::Other, 0)); 137} 138 139#[test] 140fn a_fact_without_an_identity_is_a_damaged_file() { 141 let d = dir("no-gid"); 142 let path = d.join("memory.json"); 143 std::fs::write(&path, r#"{"next_id":2,"facts":[{"id":1,"text":"A fact","learned_at_ms":3}]}"#).unwrap(); 144 assert!(JsonMemory::open(&path).err().unwrap().0.contains("damaged")); 145} 146 147#[test] 148fn the_chat_is_saved_atomically_and_a_damaged_file_is_an_error() { 149 let d = dir("chat"); 150 let path = d.join("chat.json"); 151 let mut store = JsonChat::open(&path); 152 let mut s = ChatState::default(); 153 s.summary = "They talked about bunnies.".into(); 154 s.turns.push(ChatTurn { speaker: Speaker::Child, text: "hi".into() }); 155 s.last_active_ms = 9; 156 store.save(&s).unwrap(); 157 assert_eq!(JsonChat::open(&path).load().unwrap(), s); 158 std::fs::write(&path, "{ nope").unwrap(); 159 assert!(JsonChat::open(&path).load().is_err()); 160 assert_eq!(JsonChat::open(&d.join("absent.json")).load().unwrap(), ChatState::default()); 161} 162 163#[test] 164fn cosine_is_zero_for_anything_it_cannot_compare() { 165 assert_eq!(cosine(&[], &[]), 0.0); 166 assert_eq!(cosine(&[1.0, 0.0], &[1.0]), 0.0); 167 assert_eq!(cosine(&[0.0, 0.0], &[1.0, 0.0]), 0.0); 168 assert!((cosine(&[1.0, 2.0], &[2.0, 4.0]) - 1.0).abs() < 1e-6); 169} 170 171#[test] 172fn an_entry_after_a_torn_last_line_is_not_glued_onto_it() { 173 let d = dir("torn-then-append"); 174 let path = d.join("log.jsonl"); 175 let e = Entry { at_ms: 7, event: Event::Heard { text: "toy bunny".into(), pictures: vec![] } }; 176 JsonlLog::open(&path).unwrap().append(&e).unwrap(); 177 std::fs::OpenOptions::new().append(true).open(&path).unwrap().write_all_torn(); 178 JsonlLog::open(&path).unwrap().append(&e).unwrap(); 179 let (entries, unreadable) = read_log(&path).unwrap(); 180 assert_eq!(entries, vec![e.clone(), e], "the entry written after the crash is readable"); 181 assert_eq!(unreadable, 1, "only the fragment is lost"); 182} 183 184#[test] 185fn a_log_cut_inside_a_character_still_reads() { 186 let d = dir("cut-char"); 187 let path = d.join("log.jsonl"); 188 let e = Entry { at_ms: 7, event: Event::Heard { text: "toy bunny".into(), pictures: vec![] } }; 189 JsonlLog::open(&path).unwrap().append(&e).unwrap(); 190 // The first byte of a two-byte character, then nothing. 191 std::fs::OpenOptions::new().append(true).open(&path).unwrap().write_all_bytes(&[0xC3]); 192 let (entries, _) = read_log(&path).unwrap(); 193 assert_eq!(entries, vec![e]); 194} 195 196trait Bytes { 197 fn write_all_bytes(self, b: &[u8]); 198} 199impl Bytes for std::fs::File { 200 fn write_all_bytes(mut self, b: &[u8]) { 201 use std::io::Write; 202 self.write_all(b).unwrap(); 203 } 204} 205 206#[test] 207fn a_damaged_memory_or_chat_file_is_kept_aside_and_the_app_starts_clean() { 208 let d = dir("aside"); 209 let (mem, chat) = (d.join("memory.json"), d.join("chat.json")); 210 std::fs::write(&mem, "").unwrap(); // what a power cut leaves 211 std::fs::write(&chat, "{ nope").unwrap(); 212 let (m, kept) = JsonMemory::open_or_set_aside(&mem).unwrap(); 213 assert!(m.facts().is_empty()); 214 assert!(kept.unwrap().exists(), "the evidence is kept"); 215 let (c, kept) = JsonChat::open_or_set_aside(&chat).unwrap(); 216 assert_eq!(c.load().unwrap(), ChatState::default()); 217 assert!(kept.unwrap().exists()); 218 // An undamaged file is left alone. 219 assert!(JsonMemory::open_or_set_aside(&mem).unwrap().1.is_none()); 220} 221 222#[test] 223fn the_greeting_never_closes_a_turn_a_crash_left_open() { 224 let at = |ms, event| Entry { at_ms: ms, event }; 225 let entries = vec![ 226 at(1, Event::Heard { text: "my tummy hurts".into(), pictures: vec![] }), 227 at(5, Event::Greeted { text: "Hi!".into() }), 228 ]; 229 let d = Digest::between(&entries, 0, 10); 230 assert_eq!(d.exchanges.len(), 1); 231 assert_ne!(d.exchanges[0].outcome, Outcome::Answered, "she never got an answer"); 232 assert!(d.exchanges[0].said.is_empty()); 233} 234 235 236// ---- memory shared between devices -------------------------------------------- 237 238fn texts(m: &JsonMemory) -> Vec<String> { 239 let mut v: Vec<String> = m.facts().into_iter().map(|f| f.text).collect(); 240 v.sort(); 241 v 242} 243 244fn two_devices(name: &str) -> (JsonMemory, JsonMemory) { 245 let d = dir(name); 246 (JsonMemory::open(&d.join("phone.json")).unwrap(), JsonMemory::open(&d.join("tablet.json")).unwrap()) 247} 248 249fn learn(m: &mut JsonMemory, text: &str) { 250 m.add(NewFact { text: text.into(), ..NewFact::default() }, 1).unwrap(); 251} 252 253fn exchange(a: &mut JsonMemory, b: &mut JsonMemory) { 254 let (sa, sb) = (a.snapshot(), b.snapshot()); 255 a.merge(sb).unwrap(); 256 b.merge(sa).unwrap(); 257} 258 259#[test] 260fn what_one_device_learned_reaches_the_other() { 261 let (mut phone, mut tablet) = two_devices("share"); 262 learn(&mut phone, "Biscuit is her toy bunny"); 263 learn(&mut tablet, "Her friend Omar has a green dinosaur"); 264 exchange(&mut phone, &mut tablet); 265 assert_eq!(texts(&phone), texts(&tablet)); 266 assert_eq!(texts(&phone).len(), 2); 267} 268 269#[test] 270fn merging_twice_or_in_either_order_changes_nothing() { 271 let (mut a, mut b) = two_devices("idempotent"); 272 learn(&mut a, "one"); 273 learn(&mut b, "two"); 274 exchange(&mut a, &mut b); 275 let (sa, sb) = (a.snapshot(), b.snapshot()); 276 assert_eq!(a.merge(sb.clone()).unwrap(), 0); 277 assert_eq!(b.merge(sa.clone()).unwrap(), 0); 278 assert_eq!(texts(&a), texts(&b)); 279} 280 281#[test] 282fn a_fact_the_parents_forget_stays_forgotten_everywhere() { 283 let (mut phone, mut tablet) = two_devices("forget"); 284 learn(&mut phone, "Biscuit is her toy bunny"); 285 exchange(&mut phone, &mut tablet); 286 let id = tablet.facts()[0].id; 287 tablet.forget(id).unwrap(); 288 exchange(&mut phone, &mut tablet); 289 assert!(phone.facts().is_empty(), "forgotten on the other device too"); 290 // A device that still held it (it was offline) cannot bring it back. 291 let stale = MemorySnapshot { facts: vec![], forgotten: vec![] }; 292 phone.merge(stale).unwrap(); 293 assert!(phone.facts().is_empty()); 294} 295 296#[test] 297fn the_same_thing_learned_on_two_devices_becomes_one_fact_on_both() { 298 let (mut phone, mut tablet) = two_devices("dup"); 299 learn(&mut phone, "Biscuit is her toy bunny"); 300 learn(&mut tablet, " biscuit is her TOY bunny "); 301 exchange(&mut phone, &mut tablet); 302 exchange(&mut phone, &mut tablet); 303 assert_eq!(phone.facts().len(), 1); 304 assert_eq!(tablet.facts().len(), 1); 305 assert_eq!(phone.facts()[0].gid, tablet.facts()[0].gid, "the same survivor everywhere"); 306} 307 308#[test] 309fn an_embedding_computed_on_one_side_serves_both() { 310 let (mut phone, mut tablet) = two_devices("embedding-shared"); 311 learn(&mut phone, "Biscuit is her toy bunny"); 312 exchange(&mut phone, &mut tablet); 313 let id = phone.facts()[0].id; 314 phone.set_embedding(id, vec![1.0, 2.0]).unwrap(); 315 exchange(&mut phone, &mut tablet); 316 assert_eq!(tablet.facts()[0].embedding, vec![1.0, 2.0]); 317} 318 319#[test] 320fn only_plain_picture_names_are_trusted() { 321 for ok in ["0000001790000000-0000.jpg", "a-1_b.png"] { 322 assert!(PictureId(ok.into()).is_safe(), "{ok}"); 323 } 324 for bad in ["", "../secret.jpg", "a/b.jpg", ".hidden.jpg", "a..b.jpg", "x.sh", "x", &"a".repeat(70)] { 325 assert!(!PictureId(bad.into()).is_safe(), "{bad}"); 326 } 327} 328 329#[test] 330fn a_picture_from_another_device_is_kept_once_under_its_name() { 331 let d = dir("picture-store"); 332 let p = DirPictures::open(&d).unwrap(); 333 let id = PictureId("0000001790000000-0000.jpg".into()); 334 assert!(!p.has(&id)); 335 p.store(&id, &[1, 2, 3]).unwrap(); 336 p.store(&id, &[9, 9]).unwrap(); // the same name is the same picture: not replaced 337 assert_eq!(p.read(&id), Some(vec![1, 2, 3])); 338 assert!(p.store(&PictureId("../x.jpg".into()), &[1]).is_err()); 339 assert_eq!(p.read(&PictureId("../x.jpg".into())), None); 340} 341 342// ---- putting a fact away (hide) is not forgetting --------------------------------- 343 344#[test] 345fn a_fact_she_puts_away_stays_for_the_parents_and_is_away_on_every_device() { 346 let (mut phone, mut tablet) = two_devices("hide"); 347 learn(&mut phone, "Biscuit is her toy bunny"); 348 exchange(&mut phone, &mut tablet); 349 let id = tablet.facts()[0].id; 350 tablet.hide(id, 100).unwrap(); 351 assert_eq!(tablet.facts().len(), 1, "not deleted"); 352 assert!(tablet.facts()[0].visibility.is_hidden()); 353 exchange(&mut phone, &mut tablet); 354 assert_eq!(phone.facts().len(), 1); 355 assert!(phone.facts()[0].visibility.is_hidden(), "away on the phone too"); 356 assert!(phone.snapshot().forgotten.is_empty(), "it was never forgotten"); 357} 358 359#[test] 360fn a_parents_restore_beats_the_earlier_hide_everywhere_and_a_later_hide_beats_the_restore() { 361 let (mut phone, mut tablet) = two_devices("restore"); 362 learn(&mut phone, "Biscuit is her toy bunny"); 363 exchange(&mut phone, &mut tablet); 364 tablet.hide(tablet.facts()[0].id, 1_000).unwrap(); 365 exchange(&mut phone, &mut tablet); 366 // The parents' phone clock is far behind the tablet's: the restore still wins. 367 phone.restore(phone.facts()[0].id, 5).unwrap(); 368 exchange(&mut phone, &mut tablet); 369 assert!(!tablet.facts()[0].visibility.is_hidden()); 370 assert!(!phone.facts()[0].visibility.is_hidden()); 371 tablet.hide(tablet.facts()[0].id, 2_000).unwrap(); 372 exchange(&mut phone, &mut tablet); 373 assert!(phone.facts()[0].visibility.is_hidden(), "she can put it away again"); 374} 375 376#[test] 377fn what_the_parents_forget_wins_over_a_hide_and_nothing_brings_it_back() { 378 let (mut phone, mut tablet) = two_devices("hide-forget"); 379 learn(&mut phone, "Biscuit is her toy bunny"); 380 exchange(&mut phone, &mut tablet); 381 let hidden = { 382 tablet.hide(tablet.facts()[0].id, 50).unwrap(); 383 tablet.snapshot() 384 }; 385 phone.forget(phone.facts()[0].id).unwrap(); 386 phone.merge(hidden).unwrap(); 387 assert!(phone.facts().is_empty()); 388 exchange(&mut phone, &mut tablet); 389 assert!(tablet.facts().is_empty()); 390 assert!(tablet.restore(1, 99).is_err(), "there is nothing left to restore"); 391} 392 393#[test] 394fn hiding_twice_or_restoring_what_is_not_away_changes_nothing() { 395 let (mut phone, _) = two_devices("hide-twice"); 396 learn(&mut phone, "a"); 397 let id = phone.facts()[0].id; 398 let before = phone.facts()[0].clone(); 399 phone.restore(id, 10).unwrap(); 400 assert_eq!(phone.facts()[0], before); 401 phone.hide(id, 20).unwrap(); 402 let once = phone.facts()[0].clone(); 403 phone.hide(id, 30).unwrap(); 404 assert_eq!(phone.facts()[0], once); 405 assert!(phone.hide(999, 1).is_err()); 406} 407 408#[test] 409fn a_hidden_fact_is_kept_in_the_file_and_an_untouched_fact_is_byte_for_byte_what_it_was() { 410 let d = dir("hide-file"); 411 let path = d.join("m.json"); 412 { 413 let mut m = JsonMemory::open(&path).unwrap(); 414 learn(&mut m, "one"); 415 learn(&mut m, "two"); 416 let plain = std::fs::read_to_string(&path).unwrap(); 417 assert!(!plain.contains("visibility"), "an untouched fact carries no new field: {plain}"); 418 m.hide(1, 7).unwrap(); 419 } 420 let m = JsonMemory::open(&path).unwrap(); 421 assert!(m.facts()[0].visibility.is_hidden(), "kept across a restart"); 422 assert!(!m.facts()[1].visibility.is_hidden()); 423 // and a file written before the field existed still opens 424 let old = r#"{"next_id":2,"facts":[{"id":1,"text":"x","learned_at_ms":1,"gid":"g1"}]}"#; 425 std::fs::write(&path, old).unwrap(); 426 assert_eq!(JsonMemory::open(&path).unwrap().facts()[0].visibility, Visibility::default()); 427} 428 429// ---- the picture of a fact ----------------------------------------------------------- 430 431#[test] 432fn a_fact_keeps_its_picture_and_the_first_choice_stands() { 433 let d = dir("icon"); 434 let path = d.join("m.json"); 435 let mut m = JsonMemory::open(&path).unwrap(); 436 learn(&mut m, "Biscuit is her toy bunny"); 437 assert_eq!(m.facts()[0].icon, None); 438 assert!(!std::fs::read_to_string(&path).unwrap().contains("icon"), "no field until there is a picture"); 439 m.give_icon(1, IconId::new("rabbit-white").unwrap()).unwrap(); 440 m.give_icon(1, IconId::new("pumpkin").unwrap()).unwrap(); 441 drop(m); 442 let m = JsonMemory::open(&path).unwrap(); 443 assert_eq!(m.facts()[0].icon.as_ref().map(IconId::as_str), Some("rabbit-white")); 444 assert!(JsonMemory::open(&path).unwrap().give_icon(99, IconId::new("cat").unwrap()).is_err()); 445} 446 447#[test] 448fn a_name_this_build_does_not_admit_reads_as_no_picture_and_never_damages_the_memory() { 449 let d = dir("icon-unknown"); 450 let path = d.join("m.json"); 451 for name in ["not-an-icon", "beer", "../cat", ""] { 452 let text = format!(r#"{{"next_id":2,"facts":[{{"id":1,"text":"x","learned_at_ms":1,"gid":"g1","icon":"{name}"}}]}}"#); 453 std::fs::write(&path, text).unwrap(); 454 let m = JsonMemory::open(&path).expect("an unknown icon is not a damaged file"); 455 assert_eq!(m.facts()[0].icon, None, "{name}"); 456 } 457} 458 459#[test] 460fn pictures_chosen_on_two_devices_meet_at_one_on_both() { 461 let (mut phone, mut tablet) = two_devices("icon-meet"); 462 learn(&mut phone, "Biscuit is her toy bunny"); 463 exchange(&mut phone, &mut tablet); 464 phone.give_icon(1, IconId::new("rabbit-white").unwrap()).unwrap(); 465 tablet.give_icon(1, IconId::new("rabbit-grey").unwrap()).unwrap(); 466 exchange(&mut phone, &mut tablet); 467 exchange(&mut phone, &mut tablet); 468 assert_eq!(phone.facts()[0].icon, tablet.facts()[0].icon); 469 assert_eq!(phone.facts()[0].icon.as_ref().unwrap().as_str(), "rabbit-grey", "the smaller name"); 470 assert_eq!(phone.merge(tablet.snapshot()).unwrap(), 0); 471} 472 473/// A tiny deterministic generator, so the property needs no dependency and fails the same way twice. 474struct Lcg(u64); 475impl Lcg { 476 fn next(&mut self, below: u64) -> u64 { 477 self.0 = self.0.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407); 478 (self.0 >> 33) % below 479 } 480} 481 482fn gist(d: &MemoryDoc) -> Vec<(String, Visibility)> { 483 let mut v: Vec<_> = d.snapshot().facts.into_iter().map(|f| (f.gid, f.visibility)).collect(); 484 v.sort_by(|a, b| a.0.cmp(&b.0)); 485 v 486} 487 488fn replica(gids: &[&str], acts: &[(usize, bool, u64)]) -> MemorySnapshot { 489 // One replica's view: it knows the facts and applied some hide/restore acts to them. 490 let facts = gids 491 .iter() 492 .map(|g| { 493 let mut v = Visibility::default(); 494 for &(i, hide, t) in acts { 495 if gids[i] == *g { 496 v = if hide { v.hidden_after(t) } else { v.restored_after(t) }; 497 } 498 } 499 Fact { 500 id: 0, text: format!("fact {g}"), learned_at_ms: 1, kind: Kind::Other, who: vec![], place: None, when: None, 501 pictures: vec![], embedding: vec![], gid: (*g).to_owned(), visibility: v, icon: None, 502 } 503 }) 504 .collect(); 505 MemorySnapshot { facts, forgotten: vec![] } 506} 507 508#[test] 509fn merging_replicas_in_any_order_any_number_of_times_gives_the_same_visibility() { 510 let gids = ["g1", "g2", "g3"]; 511 let mut rng = Lcg(42); 512 for _round in 0..200 { 513 let snaps: Vec<MemorySnapshot> = (0..4) 514 .map(|_| { 515 let acts: Vec<(usize, bool, u64)> = 516 (0..rng.next(5)).map(|_| (rng.next(3) as usize, rng.next(2) == 0, rng.next(6))).collect(); 517 replica(&gids, &acts) 518 }) 519 .collect(); 520 let mut expected = None; 521 for _try in 0..6 { 522 // a random order, with repeats 523 let mut doc = MemoryDoc::default(); 524 let mut order: Vec<usize> = (0..snaps.len()).collect(); 525 for i in (1..order.len()).rev() { 526 order.swap(i, rng.next(i as u64 + 1) as usize); 527 } 528 for &i in &order { 529 doc.merge(snaps[i].clone()); 530 if rng.next(2) == 0 { 531 doc.merge(snaps[i].clone()); 532 } 533 } 534 let g = gist(&doc); 535 match &expected { 536 None => expected = Some(g), 537 Some(e) => assert_eq!(&g, e, "order must not matter"), 538 } 539 let again = doc.merge(snaps[0].clone()); 540 assert_eq!(again, 0, "a second merge changes nothing"); 541 } 542 } 543}