What a device keeps besides memory: the chat, the parents' log and the copy of the others' lines, the household and its minutes, the pictures, and a consistent copy of the whole database.

4use std::path::PathBuf;
6use whiskers_core::*;
7use whiskers_store::{SqliteChat, SqliteHousehold, SqliteLog, SqliteMemory, SqlitePictures, Store};
8
9fn path(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-device-{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.join("device.db")
16}
17
18fn turn(who: Speaker, text: &str) -> ChatTurn {
19    ChatTurn { speaker: who, text: text.into() }
20}
21
22#[test]
23fn the_chat_is_kept_appended_folded_and_replaced_and_found_again() {
24    let p = path("chat");
25    let mut chat = SqliteChat::new(Store::open(&p).unwrap());
26    assert_eq!(chat.load().unwrap(), ChatState::default(), "nothing kept is an empty session");
27    let mut s = ChatState { summary: String::new(), turns: vec![turn(Speaker::Child, "hi"), turn(Speaker::Whiskers, "hello")], last_active_ms: 5, version: 1, scrubbed: vec![] };
28    chat.save(&s).unwrap();
29    s.turns.push(turn(Speaker::Child, "toy bunny"));
30    s.version = 2;
31    chat.save(&s).unwrap();
32    assert_eq!(chat.load().unwrap(), s);
33    // The oldest turns are folded into the summary.
34    s.turns.drain(..2);
35    s.summary = "they said hello".into();
36    s.version = 3;
37    chat.save(&s).unwrap();
38    assert_eq!(chat.load().unwrap(), s);
39    // Another device's copy shares nothing with this one: it replaces it.
40    let theirs = ChatState { summary: "other".into(), turns: vec![turn(Speaker::Child, "different")], last_active_ms: 9, version: 7, scrubbed: vec![] };
41    chat.save(&theirs).unwrap();
42    drop(chat);
43    assert_eq!(SqliteChat::new(Store::open(&p).unwrap()).load().unwrap(), theirs, "and it is still there after a restart");
44}
45
46#[test]
47fn the_parents_log_keeps_every_line_and_the_copy_of_the_others_continues_where_it_ended() {
48    let p = path("log");
49    let store = Store::open(&p).unwrap();
50    let mut log = SqliteLog::new(store.clone(), "phone");
51    let heard = |n: u64| Entry::new(n, Event::Heard { text: format!("line {n}"), pictures: vec![] });
52    for n in 1..=3 {
53        log.append(&heard(n)).unwrap();
54    }
55    assert_eq!(log.own_count().unwrap(), 3);
56    assert_eq!(log.own_lines(1, 10).unwrap().len(), 2, "from the second line on");
57    let theirs = |dev: &str, n: u64| format!(r#"{{"device":"{dev}","entry":{}}}"#, serde_json::to_string(&heard(n)).unwrap());
58    // The service's log: one of the tablet's, one of this phone's own, one more of the tablet's.
59    let page = vec![theirs("tablet", 10), theirs("phone", 1), theirs("tablet", 11)];
60    assert_eq!(log.take(0, &page).unwrap(), 2, "the phone's own line is already here");
61    assert_eq!(log.pulled().unwrap(), 3);
62    assert_eq!(log.take(0, &page).unwrap(), 0, "lines that do not continue where the copy ends are not taken");
63    assert_eq!(log.take(3, &[theirs("tablet", 12)]).unwrap(), 1);
64    let (groups, unreadable) = log.read_groups().unwrap();
65    assert_eq!((groups.len(), groups[0].len(), groups[1].len(), unreadable), (2, 3, 3, 0), "this device's group first, then the tablet's");
66    assert_eq!(groups[1].iter().map(|e| e.at_ms).collect::<Vec<_>>(), [10, 11, 12], "in the tablet's order");
67    log.rebuild_replica().unwrap();
68    assert_eq!((log.pulled().unwrap(), log.read_groups().unwrap().0.len()), (0, 1));
69    assert_eq!(log.own_count().unwrap(), 3, "a rebuilt copy never touches this device's own lines");
70    // An entry's keys come back in the order they were written.
71    let e = log.own_lines(0, 1).unwrap().remove(0);
72    assert_eq!(e, serde_json::to_string(&heard(1)).unwrap());
73}
74
75#[test]
76fn two_devices_minutes_are_their_own_rows_and_only_rise() {
77    let p = path("household");
78    let store = Store::open(&p).unwrap();
79    let mut phone = TimeKeeper::new(Box::new(SqliteHousehold::new(store.clone())), "phone");
80    phone.set_limits(Limits::new(Some(2), None).unwrap(), 10);
81    phone.tick(4, 5_000);
82    phone.tick(4, 5_000);
83    // A second device on its own database brings its own count to the same day.
84    let tablet_store = Store::open(&path("household-tablet")).unwrap();
85    let mut tablet = TimeKeeper::new(Box::new(SqliteHousehold::new(tablet_store)), "tablet");
86    tablet.merge(&phone.household());
87    tablet.tick(4, 5_000);
88    phone.merge(&tablet.household());
89    assert_eq!(phone.used_minutes(4), 0);
90    let conn = rusqlite::Connection::open(&p).unwrap();
91    let rows: Vec<(String, i64, i64)> = conn
92        .prepare("SELECT device, day, used_ms FROM time_spent ORDER BY device")
93        .unwrap()
94        .query_map([], |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?)))
95        .unwrap()
96        .collect::<Result<_, _>>()
97        .unwrap();
98    assert_eq!(rows, [("phone".to_owned(), 4, 10_000), ("tablet".to_owned(), 4, 5_000)], "a row for each device, whose sum is the day's time");
99    drop(phone);
100    let mut again = TimeKeeper::new(Box::new(SqliteHousehold::new(Store::open(&p).unwrap())), "phone");
101    assert_eq!(again.limits().daily_minutes(), Some(2));
102    assert_eq!(again.status(4, 600), TimeStatus::Open { minutes_left: Some(2) }, "15 of 120 seconds used between the two devices, rounded up");
103    let settings: i64 = conn.query_row("SELECT COUNT(*) FROM household", [], |r| r.get(0)).unwrap();
104    assert!(settings > 0, "each setting is a row");
105}
106
107#[test]
108fn pictures_are_kept_once_under_their_name_and_a_made_name_is_new_each_time() {
109    let store = Store::open(&path("pictures")).unwrap();
110    let mut p = SqlitePictures::new(store);
111    let id = PictureId("0000001790000000-0000.jpg".into());
112    assert!(!p.has(&id));
113    p.store(&id, &[1, 2, 3]).unwrap();
114    p.store(&id, &[9, 9]).unwrap(); // the same name is the same picture: not replaced
115    assert_eq!(p.read(&id), Some(vec![1, 2, 3]));
116    assert!(p.store(&PictureId("../x.jpg".into()), &[1]).is_err());
117    assert_eq!(p.read(&PictureId("../x.jpg".into())), None);
118    let img = Image { media_type: "image/jpeg".into(), bytes: vec![5; 10] };
119    let (a, b) = (p.save(&img).unwrap(), p.save(&img).unwrap());
120    assert_ne!(a, b);
121    assert!(a.0.ends_with(".jpg") && p.read(&a) == Some(vec![5; 10]));
122}
123
124#[test]
125fn a_backup_copy_is_a_whole_database_that_opens_by_itself() {
126    let p = path("backup");
127    let store = Store::open(&p).unwrap();
128    let mut memory = SqliteMemory::open(store.clone(), "phone").unwrap();
129    memory.add(NewFact { text: "Biscuit is her bunny".into(), ..NewFact::default() }, 5).unwrap();
130    SqliteLog::new(store.clone(), "phone").append(&Entry::new(1, Event::Greeted { text: "hi".into() })).unwrap();
131    let copy = p.with_file_name("copy.db");
132    store.backup_to(&copy).unwrap();
133    assert!(!copy.with_file_name("copy.db-wal").exists(), "the copy is one file");
134    let opened = Store::open(&copy).unwrap();
135    assert_eq!(SqliteMemory::open(opened.clone(), "phone").unwrap().facts().len(), 1);
136    assert_eq!(SqliteLog::new(opened, "phone").own_count().unwrap(), 1);
137}
138
139#[test]
140fn the_character_and_its_voice_are_rows_kept_and_found_again_and_an_old_household_reads_as_before() {
141    let p = path("character");
142    // A database at version 4, with a household written before characters existed.
143    {
144        let old = Store::open(&p).unwrap();
145        let mut keeper = TimeKeeper::new(Box::new(SqliteHousehold::new(old)), "phone");
146        keeper.set_config(HouseholdConfig { cat_theme: CatTheme::Ginger, ..keeper.config() }, 10);
147    }
148    {
149        let keeper = TimeKeeper::new(Box::new(SqliteHousehold::new(Store::open(&p).unwrap())), "phone");
150        assert_eq!(keeper.character(), CharacterId::new("ginger-cat").unwrap(), "nothing changes for a household that never chose");
151        assert_eq!(keeper.config().character, None);
152    }
153    let (panda, fox) = (CharacterId::new("panda").unwrap(), CharacterId::new("fox").unwrap());
154    let bunny = CharacterId::new("bunny").unwrap();
155    {
156        let mut keeper = TimeKeeper::new(Box::new(SqliteHousehold::new(Store::open(&p).unwrap())), "phone");
157        keeper.choose_character(bunny, Some(VoiceId::new("vb").unwrap()), 50);
158    }
159    {
160        let keeper = TimeKeeper::new(Box::new(SqliteHousehold::new(Store::open(&p).unwrap())), "phone");
161        assert_eq!(keeper.character(), bunny, "a bunny choice survives the store");
162        assert_eq!(keeper.character_voice(bunny), Some(VoiceId::new("vb").unwrap()));
163    }
164    {
165        let mut keeper = TimeKeeper::new(Box::new(SqliteHousehold::new(Store::open(&p).unwrap())), "phone");
166        keeper.choose_character(panda, Some(VoiceId::new("vp").unwrap()), 100);
167        keeper.choose_character(fox, Some(VoiceId::new("vf").unwrap()), 200);
168    }
169    let keeper = TimeKeeper::new(Box::new(SqliteHousehold::new(Store::open(&p).unwrap())), "phone");
170    assert_eq!(keeper.character(), fox);
171    assert_eq!(keeper.character_voice(panda), Some(VoiceId::new("vp").unwrap()), "each character keeps its own");
172    assert_eq!(keeper.character_voice(fox), Some(VoiceId::new("vf").unwrap()));
173}