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

The SQLite adapters held to the same laws as the in-memory one (whiskers-conformance), plus what only real threads and files show.

4mod fixtures;
6use std::sync::Arc;
7
8use fixtures::{SqliteFixtures, scratch};
9use whiskers_conformance::suite::fixture::*;
10use whiskers_conformance::suite::{self};
11use whiskers_core::TokenLimit;
12use whiskers_ports::{Allowance, Appended, DeviceCursor, DeviceName, EntryLine, Hub, Journal, LogCursor, Millis, PictureShelf, Put, SafePictureId, Settlement, run_ready};
13use whiskers_store::{SqliteAllowance, SqliteJournal, SqliteMemoryHub, SqlitePictureShelf, Store};

The store's adapters are held to every law there is: if a capability gains a law and a fixture trait, this stops compiling until the fixtures have it.

17const _EVERY_STORAGE_LAW_IS_RUN: fn(SqliteFixtures) -> Suite<SqliteFixtures> = |f| Suite::new(f).memory().household().chat().chat_write_faults().journal().pictures().allowance().speech_cache();
19#[test]
20fn the_sqlite_memory_hub_obeys_its_contract() {
21    Suite::new(SqliteFixtures).memory();
22}
23
24#[test]
25fn the_sqlite_household_hub_obeys_its_contract() {
26    Suite::new(SqliteFixtures).household();
27}
28
29#[test]
30fn the_sqlite_chat_hub_obeys_its_contract() {
31    Suite::new(SqliteFixtures).chat().chat_write_faults();
32}
33
34#[test]
35fn the_sqlite_journal_obeys_its_contract() {
36    Suite::new(SqliteFixtures).journal();
37}
38
39#[test]
40fn the_sqlite_picture_shelf_obeys_its_contract() {
41    Suite::new(SqliteFixtures).pictures();
42}
43
44#[test]
45fn the_sqlite_allowance_obeys_its_contract() {
46    Suite::new(SqliteFixtures).allowance();
47}

What only real threads show.

51fn at(ms: u64) -> Millis {
52    Millis::new(ms)
53}
55#[test]
56fn eight_lines_asked_at_once_cannot_pass_a_cap_with_room_for_one() {
57    let a = Arc::new(SqliteAllowance::open(Store::open(&scratch("race-voice")).unwrap()));
58    let threads: Vec<_> = (0..8)
59        .map(|_| {
60            let a = a.clone();
61            std::thread::spawn(move || run_ready(a.reserve_voice(at(1000), 6, 10)).map(|r| run_ready(a.settle_voice(r, Settlement::Spoken)).unwrap()))
62        })
63        .collect();
64    let results: Vec<_> = threads.into_iter().map(|t| t.join().unwrap()).collect();
65    assert_eq!(results.iter().filter(|r| r.is_ok()).count(), 1, "exactly one line fits");
66    assert_eq!(run_ready(a.voice_spend(at(1001))).unwrap().spent_today, 6);
67}
68
69#[test]
70fn many_threads_pushing_at_one_cursor_take_exactly_one_push() {
71    let j = Arc::new(SqliteJournal::new(Store::open(&scratch("race-journal")).unwrap()));
72    let device = DeviceName::new("phone1").unwrap();
73    let accepted: usize = (0..8)
74        .map(|_| {
75            let (j, device) = (j.clone(), device.clone());
76            std::thread::spawn(move || {
77                let lines = [EntryLine::parse(r#"{"n":1}"#).unwrap(), EntryLine::parse(r#"{"n":2}"#).unwrap()];
78                matches!(run_ready(j.append(&device, DeviceCursor::new(0), &lines)).unwrap(), Appended::Accepted { .. })
79            })
80        })
81        .collect::<Vec<_>>()
82        .into_iter()
83        .map(|t| usize::from(t.join().unwrap()))
84        .sum();
85    assert_eq!(accepted, 1);
86    assert_eq!(run_ready(j.pull(LogCursor::START)).unwrap().total.get(), 2);
87}
88
89#[test]
90fn merges_that_arrive_together_all_land_and_no_fact_is_numbered_twice() {
91    let hub = Arc::new(SqliteMemoryHub::open(Store::open(&scratch("race-memory")).unwrap()).unwrap());
92    let threads: Vec<_> = (0..8)
93        .map(|i| {
94            let hub = hub.clone();
95            std::thread::spawn(move || {
96                let doc = suite::build::snapshot(vec![suite::build::fact(&format!("g{i}"), &format!("fact number {i}"))], &[]);
97                run_ready(hub.merge(doc)).unwrap();
98            })
99        })
100        .collect();
101    for t in threads {
102        t.join().unwrap();
103    }
104    let doc = run_ready(hub.current()).unwrap();
105    assert_eq!(doc.facts.len(), 8, "no merge lost another's work");
106    let ids: std::collections::BTreeSet<u64> = doc.facts.iter().map(|f| f.id).collect();
107    assert_eq!(ids.len(), 8, "and no id was handed out twice");
108}
109
110#[test]
111fn eight_puts_of_one_picture_store_it_once_and_it_is_never_seen_half_written() {
112    let shelf = Arc::new(SqlitePictureShelf::new(Store::open(&scratch("race-pictures")).unwrap()));
113    let id = SafePictureId::parse("a.jpg").unwrap();
114    let body = vec![7u8; 1 << 20];
115    let threads: Vec<_> = (0..8)
116        .map(|_| {
117            let (shelf, id, body) = (shelf.clone(), id.clone(), body.clone());
118            std::thread::spawn(move || {
119                let put = run_ready(shelf.put(&id, &body)).unwrap();
120                if let Some(bytes) = run_ready(shelf.get(&id)).unwrap() {
121                    assert_eq!(bytes.len(), body.len());
122                }
123                put
124            })
125        })
126        .collect();
127    let puts: Vec<Put> = threads.into_iter().map(|t| t.join().unwrap()).collect();
128    assert_eq!(puts.iter().filter(|p| **p == Put::Stored).count(), 1);
129    assert_eq!(puts.iter().filter(|p| **p == Put::AlreadyKept).count(), 7);
130}
131
132#[test]
133fn the_ledger_and_the_spend_are_found_again_by_the_next_process() {
134    let path = scratch("restart-spend");
135    let a = SqliteAllowance::open(Store::open(&path).unwrap());
136    run_ready(a.charge_thinking(at(10), whiskers_ports::Tokens::new(700))).unwrap();
137    let r = run_ready(a.reserve_voice(at(10), 4, 10)).unwrap();
138    run_ready(a.settle_voice(r, Settlement::Spoken)).unwrap();
139    drop(a);
140    let again = SqliteAllowance::open(Store::open(&path).unwrap());
141    let limit = TokenLimit::new(Some(1000), 5).unwrap();
142    assert_eq!(run_ready(again.thinking_usage(at(20), &limit)).unwrap().used.get(), 700);
143    assert_eq!(run_ready(again.voice_spend(at(20))).unwrap().spent_today, 4);
144}
145
146#[test]
147fn the_sqlite_speech_cache_obeys_its_contract() {
148    Suite::new(SqliteFixtures).speech_cache();
149}