1//! What a device keeps besides memory: the chat, the parents' log and the copy of the others' lines, the 2//! household and its minutes, the pictures, and a consistent copy of the whole database. 3 4use std::path::PathBuf; 5 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(©).unwrap(); 133 assert!(!copy.with_file_name("copy.db-wal").exists(), "the copy is one file"); 134 let opened = Store::open(©).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}