Forgetting reaches the conversation a memory was told in. A turn has an identity, on every line of it and on the memory's mentions; forgetting writes the identities (never words) and the schema empties those turns, here and on every device the forgetting reaches, and in lines of them that arrive later. What waits to be thought about is dropped, and the bytes of the database and of its write-ahead log are searched for every word.
6use std::path::{Path, PathBuf};
8use whiskers_core::{Entry, Event, Log, Memory, MemoryDoc, NewFact, Outcome, PictureId}; 9use whiskers_core::{ReflectInput, Verdict}; 10use whiskers_store::{SqliteLog, SqliteMemory, SqlitePictures, SqliteReflections, Store}; 11 12const HEARD: &str = "HEARD-WORDS-3391-QX"; 13const MODEL: &str = "MODEL-WORDS-4402-QX"; 14const SAID: &str = "SAID-WORDS-5513-QX"; 15const LOOK: &str = "LOOKED-LIKE-6624-QX"; 16const PROPOSED: &str = "PROPOSED-FACT-7735-QX"; 17const AGAIN: &str = "SAID-AGAIN-8846-QX"; 18const UNRELATED: &str = "UNRELATED-WORDS-9957-QX"; 19const WAITING: &str = "WAITING-WORDS-1068-QX"; 20const SHOWN: &[u8] = b"SHOWN-PICTURE-BYTES-2179-QX-0123456789-0123456789"; 21const TURN: &str = "0000000000001aaaaaaaaaaaaaaaa1"; 22const AGAIN_TURN: &str = "0000000000002bbbbbbbbbbbbbbbb2"; 23const OTHER_TURN: &str = "0000000000003cccccccccccccccc3"; 24 25fn dir(name: &str) -> PathBuf { 26 use std::sync::atomic::{AtomicUsize, Ordering}; 27 static N: AtomicUsize = AtomicUsize::new(0); 28 let d = std::env::temp_dir().join(format!("whiskers-store-turns-{name}-{}-{}", std::process::id(), N.fetch_add(1, Ordering::SeqCst))); 29 let _ = std::fs::remove_dir_all(&d); 30 std::fs::create_dir_all(&d).unwrap(); 31 d 32} 33 34fn on_disk(db: &Path) -> (Vec<u8>, Vec<u8>) { 35 let wal = db.with_file_name(format!("{}-wal", db.file_name().unwrap().to_string_lossy())); 36 (std::fs::read(db).unwrap(), std::fs::read(wal).unwrap_or_default()) 37} 38 39fn contains(haystack: &[u8], needle: &[u8]) -> bool { 40 haystack.windows(needle.len()).any(|w| w == needle) 41} 42 43fn gone_from_disk(db: &Path, what: &[(&str, &[u8])]) { 44 let (file, wal) = on_disk(db); 45 for (name, n) in what { 46 assert!(!contains(&file, n), "{name} is still in the database file"); 47 assert!(!contains(&wal, n), "{name} is still in the write-ahead log"); 48 } 49} 50 51fn on_disk_somewhere(db: &Path, what: &[(&str, &[u8])]) { 52 let (file, wal) = on_disk(db); 53 for (name, n) in what { 54 assert!(contains(&file, n) || contains(&wal, n), "{name} should be on disk before it is forgotten"); 55 } 56} 57 58fn line(turn: &str, at: u64, event: Event) -> Entry { 59 Entry::in_turn(turn, at, event) 60}
The lines of one turn, as the conversation writes them: what she said and showed, what a look found, what the model wrote, what was said, and a memory proposed from it that was kept out.
64fn the_turn(picture: &PictureId) -> Vec<Entry> { 65 vec![ 66 line(TURN, 100, Event::Heard { text: HEARD.into(), pictures: vec![picture.clone()] }), 67 line(TURN, 101, Event::PictureSeen { description: LOOK.into(), pictures: vec![picture.clone()] }), 68 line(TURN, 102, Event::Guarded { direction: whiskers_core::Direction::FromChild, verdict: Verdict::Allow }), 69 line(TURN, 103, Event::ModelWrote { text: MODEL.into() }), 70 line(TURN, 104, Event::Said { text: SAID.into(), outcome: Outcome::Answered }), 71 line(TURN, 105, Event::NotRemembered { fact: PROPOSED.into(), why: "too long".into() }), 72 ] 73}
75fn texts_of(entries: &[Entry]) -> Vec<String> { 76 entries 77 .iter() 78 .map(|e| match &e.event { 79 Event::Heard { text, .. } | Event::ModelWrote { text } | Event::Said { text, .. } => text.clone(), 80 Event::PictureSeen { description, .. } => description.clone(), 81 Event::NotRemembered { fact, .. } => fact.clone(), 82 _ => String::new(), 83 }) 84 .collect() 85} 86 87fn waiting(turn: &str, words: &str) -> ReflectInput { 88 ReflectInput { turn: turn.into(), heard: words.into(), said: "fine".into(), description: None, pictures: vec![] } 89} 90 91#[test] 92fn forgetting_a_memory_empties_the_turns_it_was_told_in_and_nothing_else() { 93 let d = dir("device"); 94 let db = d.join("device.db"); 95 let store = Store::open(&db).unwrap(); 96 let pictures = SqlitePictures::new(store.clone()); 97 let picture = PictureId("0000000000000009-0000.jpg".into()); 98 pictures.store(&picture, SHOWN).unwrap(); 99 let mut log = SqliteLog::new(store.clone(), "phone"); 100 for e in the_turn(&picture) { 101 log.append(&e).unwrap(); 102 } 103 // The memory was told again in a second turn, and an unrelated turn and the hello are in the log too. 104 log.append(&line(AGAIN_TURN, 200, Event::Heard { text: AGAIN.into(), pictures: vec![] })).unwrap(); 105 log.append(&line(AGAIN_TURN, 201, Event::Said { text: format!("{AGAIN} again"), outcome: Outcome::Answered })).unwrap(); 106 log.append(&line(OTHER_TURN, 300, Event::Heard { text: UNRELATED.into(), pictures: vec![] })).unwrap(); 107 log.append(&Entry::new(1, Event::Greeted { text: "Hi!".into() })).unwrap(); 108 109 let mut memory = SqliteMemory::open(store.clone(), "phone").unwrap(); 110 let f = memory.add(NewFact { text: "Biscuit is her bunny".into(), turn: TURN.into(), ..NewFact::default() }, 100).unwrap(); 111 memory.mention(f.id, 200, AGAIN_TURN).unwrap(); 112 // Exchanges wait: one of the turn the memory came from, one of an unrelated turn. 113 let queue = SqliteReflections::new(store.clone()); 114 queue.push(&waiting(TURN, WAITING), 106, 8).unwrap(); 115 queue.push(&waiting(OTHER_TURN, UNRELATED), 301, 8).unwrap(); 116 117 // The turns are on the memory, by identity, and survive a restart. 118 drop(memory); 119 let mut memory = SqliteMemory::open(store.clone(), "phone").unwrap(); 120 let mentions: Vec<String> = memory.facts()[0].mentions.iter().map(|m| m.turn.clone()).collect(); 121 assert_eq!(mentions, [TURN, AGAIN_TURN]); 122 123 let words: Vec<(&str, &[u8])> = vec![ 124 ("what she said", HEARD.as_bytes()), 125 ("what a look at her picture found", LOOK.as_bytes()), 126 ("what the model wrote", MODEL.as_bytes()), 127 ("what was said back", SAID.as_bytes()), 128 ("a fact proposed from it", PROPOSED.as_bytes()), 129 ("what she said again", AGAIN.as_bytes()), 130 ("the picture she showed", SHOWN), 131 ("the exchange waiting to be thought about", WAITING.as_bytes()), 132 ]; 133 on_disk_somewhere(&db, &words); 134 assert_eq!(queue.len().unwrap(), 2); 135 136 memory.forget(f.id, 999).unwrap(); 137 138 gone_from_disk(&db, &words); 139 let (_, wal) = on_disk(&db); 140 assert!(wal.is_empty(), "the write-ahead log was truncated: {} bytes", wal.len()); 141 // The lines stay, with their times and how the turn ended; what was said in them is empty. 142 let kept = log.own_entries().unwrap(); 143 assert_eq!(kept.len(), 6 + 2 + 1 + 1, "no line was removed"); 144 let turn: Vec<&Entry> = kept.iter().filter(|e| e.turn == TURN).collect(); 145 assert_eq!(turn.iter().map(|e| e.at_ms).collect::<Vec<_>>(), [100, 101, 102, 103, 104, 105]); 146 assert!(texts_of(&turn.iter().map(|e| (*e).clone()).collect::<Vec<_>>()).iter().all(String::is_empty)); 147 assert!(matches!(&turn[0].event, Event::Heard { pictures, .. } if pictures.is_empty()), "the picture is let go"); 148 assert!(matches!(&turn[4].event, Event::Said { outcome: Outcome::Answered, .. }), "and how it ended stays"); 149 assert!(matches!(&turn[5].event, Event::NotRemembered { why, .. } if why == "too long")); 150 assert!(kept.iter().filter(|e| e.turn == AGAIN_TURN).all(|e| texts_of(&[(*e).clone()])[0].is_empty()), "the turn she said it again in is emptied too"); 151 let unrelated = kept.iter().find(|e| e.turn == OTHER_TURN).unwrap(); 152 assert!(matches!(&unrelated.event, Event::Heard { text, .. } if text == UNRELATED), "an unrelated turn is untouched"); 153 assert!(kept.iter().any(|e| matches!(&e.event, Event::Greeted { text } if text == "Hi!"))); 154 // Every exchange that waited was dropped: any of them might say the memory again and learn it back. 155 assert!(queue.is_empty().unwrap(), "waiting exchanges are dropped, the unrelated one too"); 156 assert!(!queue.has_turn(OTHER_TURN).unwrap()); 157 // The picture only that turn showed went with it. 158 assert!(!pictures.has(&picture)); 159 // What stays of the memory is its identity, when and by whom, and the identities of its turns. 160 let conn = rusqlite::Connection::open(&db).unwrap(); 161 let count = |sql: &str| conn.query_row(sql, [], |r| r.get::<_, i64>(0)).unwrap(); 162 assert_eq!(count(&format!("SELECT COUNT(*) FROM tombstone_turn WHERE gid = '{}'", f.gid)), 2); 163 assert_eq!(count("SELECT COUNT(*) FROM journal_picture"), 0); 164 drop((memory, store)); 165 let again = SqliteMemory::open(Store::open(&db).unwrap(), "phone").unwrap(); 166 assert_eq!(again.snapshot().forgotten_when[0].turns, [TURN, AGAIN_TURN], "and they survive a restart, to be sent on"); 167} 168 169#[test] 170fn a_line_of_a_forgotten_turn_that_arrives_late_is_emptied_as_it_is_written() { 171 let d = dir("late"); 172 let db = d.join("device.db"); 173 let store = Store::open(&db).unwrap(); 174 let mut memory = SqliteMemory::open(store.clone(), "phone").unwrap(); 175 let f = memory.add(NewFact { text: "Biscuit is her bunny".into(), turn: TURN.into(), ..NewFact::default() }, 100).unwrap(); 176 memory.forget(f.id, 999).unwrap(); 177 // The tablet had not heard of it, and its lines of that turn reach this device afterwards. 178 let picture = PictureId("0000000000000009-0000.jpg".into()); 179 let lines: Vec<String> = the_turn(&picture).iter().map(|e| format!(r#"{{"device":"tablet","entry":{}}}"#, serde_json::to_string(e).unwrap())).collect(); 180 let log = SqliteLog::new(store.clone(), "phone"); 181 assert_eq!(log.take(0, &lines).unwrap(), 6); 182 store.scrub().unwrap(); 183 gone_from_disk(&db, &[("what she said", HEARD.as_bytes()), ("what the model wrote", MODEL.as_bytes()), ("what was said back", SAID.as_bytes()), ("a fact proposed", PROPOSED.as_bytes())]); 184 let (groups, _) = log.read_groups().unwrap(); 185 assert!(groups.iter().flatten().all(|e| texts_of(std::slice::from_ref(e))[0].is_empty())); 186 let conn = rusqlite::Connection::open(&db).unwrap(); 187 assert_eq!(conn.query_row("SELECT COUNT(*) FROM journal_picture", [], |r| r.get::<_, i64>(0)).unwrap(), 0, "no picture is kept for a line that no longer names one"); 188} 189 190#[test] 191fn a_device_that_never_saw_the_memory_empties_the_turns_when_the_forgetting_arrives() { 192 // Phone learned and forgot it; the tablet only has the phone's lines of that turn, taken from the service. 193 let phone_dir = dir("phone"); 194 let mut phone = SqliteMemory::open(Store::open(&phone_dir.join("p.db")).unwrap(), "phone").unwrap(); 195 let f = phone.add(NewFact { text: "Biscuit is her bunny".into(), turn: TURN.into(), ..NewFact::default() }, 100).unwrap(); 196 let before_forgetting: MemoryDoc = MemoryDoc::default(); 197 let _ = before_forgetting; 198 phone.forget(f.id, 999).unwrap(); 199 200 let tablet_dir = dir("tablet"); 201 let db = tablet_dir.join("t.db"); 202 let store = Store::open(&db).unwrap(); 203 let picture = PictureId("0000000000000009-0000.jpg".into()); 204 let lines: Vec<String> = the_turn(&picture).iter().map(|e| format!(r#"{{"device":"phone","entry":{}}}"#, serde_json::to_string(e).unwrap())).collect(); 205 let tablet_log = SqliteLog::new(store.clone(), "tablet"); 206 tablet_log.take(0, &lines).unwrap(); 207 let queue = SqliteReflections::new(store.clone()); 208 queue.push(&waiting(OTHER_TURN, WAITING), 1, 8).unwrap(); 209 on_disk_somewhere(&db, &[("what she said", HEARD.as_bytes()), ("what the model wrote", MODEL.as_bytes())]); 210 211 let mut tablet = SqliteMemory::open(store.clone(), "tablet").unwrap(); 212 tablet.merge(phone.snapshot()).unwrap(); 213 214 gone_from_disk(&db, &[("what she said", HEARD.as_bytes()), ("what the model wrote", MODEL.as_bytes()), ("what was said back", SAID.as_bytes()), ("the waiting exchange", WAITING.as_bytes())]); 215 assert!(queue.is_empty().unwrap()); 216 // Merging again changes nothing, and is not a second forgetting: nothing waiting is dropped twice. 217 queue.push(&waiting(OTHER_TURN, "later"), 2, 8).unwrap(); 218 tablet.merge(phone.snapshot()).unwrap(); 219 assert_eq!(queue.len().unwrap(), 1, "the same forgetting arriving again drops nothing"); 220} 221 222#[test] 223fn a_copy_dropped_as_a_duplicate_does_not_drop_what_waits_or_empty_its_turns() { 224 let d = dir("duplicate"); 225 let store = Store::open(&d.join("d.db")).unwrap(); 226 let mut a = SqliteMemory::open(store.clone(), "phone").unwrap(); 227 let kept = a.add(NewFact { text: "Biscuit is her bunny".into(), turn: TURN.into(), ..NewFact::default() }, 10).unwrap(); 228 let log = SqliteLog::new(store.clone(), "phone"); 229 log.clone().append(&line(TURN, 10, Event::Heard { text: HEARD.into(), pictures: vec![] })).unwrap(); 230 let queue = SqliteReflections::new(store.clone()); 231 queue.push(&waiting(OTHER_TURN, WAITING), 11, 8).unwrap(); 232 // The same thing learned on another device arrives as a second fact and is dropped, which writes a tombstone. 233 let mut other = MemoryDoc::default(); 234 other.add(NewFact { text: "biscuit is her bunny".into(), turn: OTHER_TURN.into(), ..NewFact::default() }, 90, "tablet", "zz-other".into()); 235 a.merge(other.snapshot()).unwrap(); 236 assert_eq!(a.facts().len(), 1); 237 assert_eq!(a.snapshot().forgotten.len(), 1, "a tombstone was written for the copy"); 238 assert!(a.snapshot().forgotten_when.is_empty(), "but it is not a forgetting: it has no time"); 239 assert_eq!(queue.len().unwrap(), 1, "nothing waiting was dropped"); 240 let e = log.own_entries().unwrap(); 241 assert!(matches!(&e[0].event, Event::Heard { text, .. } if text == HEARD), "and nothing was emptied"); 242 assert_eq!(a.facts()[0].id, kept.id); 243}