The device's memory on SQLite, held to what the file-backed one was: it survives a restart, ids are never reused, devices converge in any order, and hiding, the picture and the card's star are registers.
4use std::path::{Path, PathBuf};
6use whiskers_core::*; 7use whiskers_store::{SqliteMemory, Store}; 8 9fn dir(name: &str) -> PathBuf { 10 use std::sync::atomic::{AtomicUsize, Ordering}; 11 static N: AtomicUsize = AtomicUsize::new(0); 12 let d = std::env::temp_dir().join(format!("whiskers-store-mem-{name}-{}-{}", std::process::id(), N.fetch_add(1, Ordering::SeqCst))); 13 let _ = std::fs::remove_dir_all(&d); 14 std::fs::create_dir_all(&d).unwrap(); 15 d 16} 17 18fn open(path: &Path, device: &str) -> SqliteMemory { 19 SqliteMemory::open(Store::open(path).unwrap(), device).unwrap() 20} 21 22#[test] 23fn memory_survives_a_restart_and_ids_never_repeat() { 24 let d = dir("memory"); 25 let path = d.join("memory.db"); 26 let mut m = open(&path, "phone"); 27 let new = |t: &str| NewFact { text: t.into(), ..NewFact::default() }; 28 let a = m.add(new("one"), 1).unwrap(); 29 let b = m.add(new("two"), 2).unwrap(); 30 m.forget(b.id, 9).unwrap(); 31 let c = m.add(new("three"), 3).unwrap(); 32 assert!(c.id > b.id && c.id > a.id); 33 let again = open(&path, "phone"); 34 assert_eq!(again.facts().iter().map(|f| f.text.as_str()).collect::<Vec<_>>(), ["one", "three"]); 35} 36 37 38#[test] 39fn an_embedding_and_the_rest_of_a_fact_survive_a_restart() { 40 let d = dir("embedding"); 41 let path = d.join("memory.db"); 42 let mut m = open(&path, "phone"); 43 let f = m 44 .add( 45 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())], turn: String::new() }, 46 5, 47 ) 48 .unwrap(); 49 m.set_embedding(f.id, vec![0.5, 0.25]).unwrap(); 50 let again = open(&path, "phone").facts().remove(0); 51 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()))); 52}
memory shared between devices
63fn two_devices(name: &str) -> (SqliteMemory, SqliteMemory) { 64 let d = dir(name); 65 (open(&d.join("phone.db"), "phone"), open(&d.join("tablet.db"), "tablet")) 66} 67 68fn learn(m: &mut SqliteMemory, text: &str) { 69 m.add(NewFact { text: text.into(), ..NewFact::default() }, 1).unwrap(); 70} 71 72fn exchange(a: &mut SqliteMemory, b: &mut SqliteMemory) { 73 let (sa, sb) = (a.snapshot(), b.snapshot()); 74 a.merge(sb).unwrap(); 75 b.merge(sa).unwrap(); 76} 77 78#[test] 79fn what_one_device_learned_reaches_the_other() { 80 let (mut phone, mut tablet) = two_devices("share"); 81 learn(&mut phone, "Biscuit is her toy bunny"); 82 learn(&mut tablet, "Her friend Omar has a green dinosaur"); 83 exchange(&mut phone, &mut tablet); 84 assert_eq!(texts(&phone), texts(&tablet)); 85 assert_eq!(texts(&phone).len(), 2); 86} 87 88#[test] 89fn merging_twice_or_in_either_order_changes_nothing() { 90 let (mut a, mut b) = two_devices("idempotent"); 91 learn(&mut a, "one"); 92 learn(&mut b, "two"); 93 exchange(&mut a, &mut b); 94 let (sa, sb) = (a.snapshot(), b.snapshot()); 95 assert_eq!(a.merge(sb.clone()).unwrap(), 0); 96 assert_eq!(b.merge(sa.clone()).unwrap(), 0); 97 assert_eq!(texts(&a), texts(&b)); 98} 99 100#[test] 101fn a_fact_the_parents_forget_stays_forgotten_everywhere() { 102 let (mut phone, mut tablet) = two_devices("forget"); 103 learn(&mut phone, "Biscuit is her toy bunny"); 104 exchange(&mut phone, &mut tablet); 105 let id = tablet.facts()[0].id; 106 tablet.forget(id, 9).unwrap(); 107 exchange(&mut phone, &mut tablet); 108 assert!(phone.facts().is_empty(), "forgotten on the other device too"); 109 // A device that still held it (it was offline) cannot bring it back. 110 let stale = MemorySnapshot::default(); 111 phone.merge(stale).unwrap(); 112 assert!(phone.facts().is_empty()); 113} 114 115#[test] 116fn the_same_thing_learned_on_two_devices_becomes_one_fact_on_both() { 117 let (mut phone, mut tablet) = two_devices("dup"); 118 learn(&mut phone, "Biscuit is her toy bunny"); 119 learn(&mut tablet, " biscuit is her TOY bunny "); 120 exchange(&mut phone, &mut tablet); 121 exchange(&mut phone, &mut tablet); 122 assert_eq!(phone.facts().len(), 1); 123 assert_eq!(tablet.facts().len(), 1); 124 assert_eq!(phone.facts()[0].gid, tablet.facts()[0].gid, "the same survivor everywhere"); 125} 126 127#[test] 128fn an_embedding_computed_on_one_side_serves_both() { 129 let (mut phone, mut tablet) = two_devices("embedding-shared"); 130 learn(&mut phone, "Biscuit is her toy bunny"); 131 exchange(&mut phone, &mut tablet); 132 let id = phone.facts()[0].id; 133 phone.set_embedding(id, vec![1.0, 2.0]).unwrap(); 134 exchange(&mut phone, &mut tablet); 135 assert_eq!(tablet.facts()[0].embedding, vec![1.0, 2.0]); 136}
putting a fact away (hide) is not forgetting
141#[test] 142fn a_fact_she_puts_away_stays_for_the_parents_and_is_away_on_every_device() { 143 let (mut phone, mut tablet) = two_devices("hide"); 144 learn(&mut phone, "Biscuit is her toy bunny"); 145 exchange(&mut phone, &mut tablet); 146 let id = tablet.facts()[0].id; 147 tablet.hide(id, 100).unwrap(); 148 assert_eq!(tablet.facts().len(), 1, "not deleted"); 149 assert!(tablet.facts()[0].visibility.is_hidden()); 150 exchange(&mut phone, &mut tablet); 151 assert_eq!(phone.facts().len(), 1); 152 assert!(phone.facts()[0].visibility.is_hidden(), "away on the phone too"); 153 assert!(phone.snapshot().forgotten.is_empty(), "it was never forgotten"); 154}
156#[test] 157fn a_parents_restore_beats_the_earlier_hide_everywhere_and_a_later_hide_beats_the_restore() { 158 let (mut phone, mut tablet) = two_devices("restore"); 159 learn(&mut phone, "Biscuit is her toy bunny"); 160 exchange(&mut phone, &mut tablet); 161 tablet.hide(tablet.facts()[0].id, 1_000).unwrap(); 162 exchange(&mut phone, &mut tablet); 163 // The parents' phone clock is far behind the tablet's: the restore still wins. 164 phone.restore(phone.facts()[0].id, 5).unwrap(); 165 exchange(&mut phone, &mut tablet); 166 assert!(!tablet.facts()[0].visibility.is_hidden()); 167 assert!(!phone.facts()[0].visibility.is_hidden()); 168 tablet.hide(tablet.facts()[0].id, 2_000).unwrap(); 169 exchange(&mut phone, &mut tablet); 170 assert!(phone.facts()[0].visibility.is_hidden(), "she can put it away again"); 171} 172 173#[test] 174fn what_the_parents_forget_wins_over_a_hide_and_nothing_brings_it_back() { 175 let (mut phone, mut tablet) = two_devices("hide-forget"); 176 learn(&mut phone, "Biscuit is her toy bunny"); 177 exchange(&mut phone, &mut tablet); 178 let hidden = { 179 tablet.hide(tablet.facts()[0].id, 50).unwrap(); 180 tablet.snapshot() 181 }; 182 phone.forget(phone.facts()[0].id, 9).unwrap(); 183 phone.merge(hidden).unwrap(); 184 assert!(phone.facts().is_empty()); 185 exchange(&mut phone, &mut tablet); 186 assert!(tablet.facts().is_empty()); 187 assert!(tablet.restore(1, 99).is_err(), "there is nothing left to restore"); 188} 189 190#[test] 191fn hiding_twice_or_restoring_what_is_not_away_changes_nothing() { 192 let (mut phone, _) = two_devices("hide-twice"); 193 learn(&mut phone, "a"); 194 let id = phone.facts()[0].id; 195 let before = phone.facts()[0].clone(); 196 phone.restore(id, 10).unwrap(); 197 assert_eq!(phone.facts()[0], before); 198 phone.hide(id, 20).unwrap(); 199 let once = phone.facts()[0].clone(); 200 phone.hide(id, 30).unwrap(); 201 assert_eq!(phone.facts()[0], once); 202 assert!(phone.hide(999, 1).is_err()); 203} 204 205#[test] 206fn a_hidden_fact_is_kept_across_a_restart_and_an_untouched_fact_is_what_it_was() { 207 let d = dir("hide-file"); 208 let path = d.join("m.db"); 209 { 210 let mut m = open(&path, "phone"); 211 learn(&mut m, "one"); 212 learn(&mut m, "two"); 213 m.hide(1, 7).unwrap(); 214 } 215 let m = open(&path, "phone"); 216 assert!(m.facts()[0].visibility.is_hidden(), "kept across a restart"); 217 assert!(!m.facts()[1].visibility.is_hidden()); 218 assert_eq!(open(&path, "phone").facts()[1].visibility, Visibility::default(), "an untouched fact is shown, as it always was"); 219}
the picture of a fact
223#[test] 224fn a_fact_keeps_its_picture_and_the_first_choice_stands() { 225 let d = dir("icon"); 226 let path = d.join("m.db"); 227 let mut m = open(&path, "phone"); 228 learn(&mut m, "Biscuit is her toy bunny"); 229 assert_eq!(m.facts()[0].icon, None); 230 m.give_icon(1, IconId::new("rabbit-white").unwrap()).unwrap(); 231 m.give_icon(1, IconId::new("pumpkin").unwrap()).unwrap(); 232 drop(m); 233 let m = open(&path, "phone"); 234 assert_eq!(m.facts()[0].icon.as_ref().map(IconId::as_str), Some("rabbit-white")); 235 assert!(open(&path, "phone").give_icon(99, IconId::new("cat").unwrap()).is_err()); 236}
238#[test] 239fn pictures_chosen_on_two_devices_meet_at_one_on_both() { 240 let (mut phone, mut tablet) = two_devices("icon-meet"); 241 learn(&mut phone, "Biscuit is her toy bunny"); 242 exchange(&mut phone, &mut tablet); 243 phone.give_icon(1, IconId::new("rabbit-white").unwrap()).unwrap(); 244 tablet.give_icon(1, IconId::new("rabbit-grey").unwrap()).unwrap(); 245 exchange(&mut phone, &mut tablet); 246 exchange(&mut phone, &mut tablet); 247 assert_eq!(phone.facts()[0].icon, tablet.facts()[0].icon); 248 assert_eq!(phone.facts()[0].icon.as_ref().unwrap().as_str(), "rabbit-grey", "the smaller name"); 249 assert_eq!(phone.merge(tablet.snapshot()).unwrap(), 0); 250}
258fn learn_with_pictures(m: &mut SqliteMemory, text: &str, names: &[&str]) { 259 m.add(NewFact { text: text.into(), pictures: names.iter().map(|n| pic(n)).collect(), ..NewFact::default() }, 1).unwrap(); 260} 261 262#[test] 263fn the_card_shows_the_first_picture_until_one_is_starred_and_a_stranger_is_refused() { 264 let d = dir("cover"); 265 let path = d.join("m.db"); 266 let mut m = open(&path, "phone"); 267 learn_with_pictures(&mut m, "Biscuit is her toy bunny", &["a.jpg", "b.jpg"]); 268 learn(&mut m, "she likes purple"); 269 assert_eq!(m.facts()[0].card_picture(), Some(&pic("a.jpg"))); 270 assert_eq!(m.facts()[1].card_picture(), None, "no pictures, so the card shows its icon"); 271 assert!(m.choose_cover(1, &pic("c.jpg"), 5).is_err(), "a picture the fact lacks is refused"); 272 assert!(m.choose_cover(2, &pic("a.jpg"), 5).is_err()); 273 assert!(m.choose_cover(99, &pic("a.jpg"), 5).is_err()); 274 m.choose_cover(1, &pic("b.jpg"), 5).unwrap(); 275 drop(m); 276 let m = open(&path, "phone"); 277 assert_eq!(m.facts()[0].card_picture(), Some(&pic("b.jpg")), "kept across a restart"); 278} 279 280#[test] 281fn two_devices_starring_different_pictures_meet_at_the_later_star_and_the_wire_round_trips() { 282 let (mut phone, mut tablet) = two_devices("cover-meet"); 283 learn_with_pictures(&mut phone, "Biscuit is her toy bunny", &["a.jpg", "b.jpg", "c.jpg"]); 284 exchange(&mut phone, &mut tablet); 285 phone.choose_cover(1, &pic("b.jpg"), 100).unwrap(); 286 tablet.choose_cover(1, &pic("c.jpg"), 200).unwrap(); 287 exchange(&mut phone, &mut tablet); 288 exchange(&mut phone, &mut tablet); 289 assert_eq!(phone.facts()[0].card_picture(), Some(&pic("c.jpg")), "the later star wins"); 290 assert_eq!(tablet.facts()[0].card_picture(), Some(&pic("c.jpg"))); 291 assert_eq!(phone.merge(tablet.snapshot()).unwrap(), 0); 292 // Starring again, from a clock that is behind, still beats what it follows. 293 phone.choose_cover(1, &pic("a.jpg"), 1).unwrap(); 294 exchange(&mut phone, &mut tablet); 295 assert_eq!(tablet.facts()[0].card_picture(), Some(&pic("a.jpg"))); 296 // The wire form round-trips, and a fact nobody starred has no cover on it. 297 let wire = serde_json::to_string(&phone.snapshot()).unwrap(); 298 let back: MemorySnapshot = serde_json::from_str(&wire).unwrap(); 299 assert_eq!(back, phone.snapshot()); 300 learn(&mut phone, "she likes purple"); 301 let v: serde_json::Value = serde_json::to_value(phone.snapshot()).unwrap(); 302 assert!(v["facts"][0].get("cover").is_some() && v["facts"][1].get("cover").is_none()); 303} 304 305#[test] 306fn a_star_does_not_change_hiding_or_forgetting() { 307 let (mut phone, mut tablet) = two_devices("cover-hide"); 308 learn_with_pictures(&mut phone, "Biscuit is her toy bunny", &["a.jpg", "b.jpg"]); 309 exchange(&mut phone, &mut tablet); 310 tablet.hide(1, 50).unwrap(); 311 phone.choose_cover(1, &pic("b.jpg"), 60).unwrap(); 312 exchange(&mut phone, &mut tablet); 313 for m in [&phone, &tablet] { 314 assert!(m.facts()[0].visibility.is_hidden()); 315 assert_eq!(m.facts()[0].card_picture(), Some(&pic("b.jpg"))); 316 } 317 phone.forget(1, 9).unwrap(); 318 exchange(&mut phone, &mut tablet); 319 assert!(tablet.facts().is_empty(), "forgetting still wins"); 320}