hubs.rsannotatedhubs.rssource376 lines · 18.8 KB · raw

The three hubs: memory, household, chat.

3use ::log::info;
4use whiskers_core::{ChatState, Household, IconId, MemorySnapshot, Visibility};
5use whiskers_ports::{Hub, run_ready};
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.

84fn away(gid: &str, hide_at: u64) -> whiskers_core::Fact {
85    let mut f = fact(gid, "she has a toy bunny");
86    f.visibility = Visibility::default().hidden_after(hide_at);
87    f
88}
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).

320pub fn chat_hub_whose_storage_works<H: Hub<Document = ChatState>>(fresh: &impl Fn() -> H, restart: &impl Fn(H) -> H) {
321    info!("conformance: chat hub");
322    chat_the_newer_copy_wins_whole(fresh());
323    chat_merge_twice_changes_nothing(fresh());
324    chat_survives_a_restart(fresh, restart);
325}

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}