whiskers.git / crates / whiskers-store / tests / device_memory.rs

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

57fn texts(m: &SqliteMemory) -> Vec<String> {
58    let mut v: Vec<String> = m.facts().into_iter().map(|f| f.text).collect();
59    v.sort();
60    v
61}
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}

which picture is the card's

254fn pic(n: &str) -> PictureId {
255    PictureId(n.to_owned())
256}
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}