| 1 | use std::collections::BTreeMap; |
| 2 | |
| 3 | use super::*; |
| 4 | |
| 5 | fn doc(key: &str, room: &str, ts: u64, text: &str) -> Doc { |
| 6 | Doc { |
| 7 | key: key.to_owned(), |
| 8 | room: room.to_owned(), |
| 9 | sender: "@a:x".to_owned(), |
| 10 | ts, |
| 11 | content: key.to_owned(), |
| 12 | content_ts: ts, |
| 13 | text: text.to_owned(), |
| 14 | provisional: false, |
| 15 | } |
| 16 | } |
| 17 | |
| 18 | fn keys(hits: &[Hit]) -> Vec<&str> { |
| 19 | hits.iter().map(|hit| hit.key.as_str()).collect() |
| 20 | } |
| 21 | |
| 22 | /// the order `Service::commit` uses, the manifest saved with the blobs before `committed` |
| 23 | fn commit(index: &mut KeywordIndex, store: &mut BTreeMap<String, Vec<u8>>) { |
| 24 | let changes = index.changes().unwrap(); |
| 25 | for key in &changes.deletes { |
| 26 | store.remove(key); |
| 27 | } |
| 28 | for (key, value) in &changes.puts { |
| 29 | store.insert(key.clone(), value.clone()); |
| 30 | } |
| 31 | store.insert("meta".to_owned(), changes.manifest.to_string().into_bytes()); |
| 32 | index.committed(&changes); |
| 33 | } |
| 34 | |
| 35 | fn reload(store: &BTreeMap<String, Vec<u8>>) -> KeywordIndex { |
| 36 | let manifest: serde_json::Value = serde_json::from_slice(&store["meta"]).unwrap(); |
| 37 | KeywordIndex::load(&manifest, |key| store.get(key).cloned()).unwrap() |
| 38 | } |
| 39 | |
| 40 | fn found(index: &mut KeywordIndex, query: &str) -> Vec<String> { |
| 41 | let (hits, _) = index.search(query, None, None, 100).unwrap(); |
| 42 | let mut found: Vec<_> = keys(&hits).into_iter().map(str::to_owned).collect(); |
| 43 | found.sort(); |
| 44 | found |
| 45 | } |
| 46 | |
| 47 | #[test] |
| 48 | fn pages_run_newest_first_without_repeats_across_segments() { |
| 49 | let mut index = KeywordIndex::new(false); |
| 50 | for ts in 0..30 { |
| 51 | index.put(doc(&format!("$e{ts}"), "!r:x", ts, "hello there")); |
| 52 | if ts % 7 == 0 { |
| 53 | index.maintain().unwrap(); |
| 54 | } |
| 55 | } |
| 56 | let mut seen = Vec::new(); |
| 57 | let mut before = None; |
| 58 | loop { |
| 59 | let (hits, _) = index.search("hello", None, before, 8).unwrap(); |
| 60 | let Some(last) = hits.last() else { break }; |
| 61 | before = Some((last.ts, last.tie)); |
| 62 | seen.extend(hits.iter().map(|hit| hit.ts)); |
| 63 | } |
| 64 | assert_eq!(seen, (0..30).rev().collect::<Vec<_>>()); |
| 65 | } |
| 66 | |
| 67 | #[test] |
| 68 | fn a_replaced_doc_is_found_once_by_its_new_text() { |
| 69 | let mut index = KeywordIndex::new(false); |
| 70 | index.put(doc("$a", "!r:x", 1, "before")); |
| 71 | index.maintain().unwrap(); |
| 72 | index.put(doc("$a", "!r:x", 1, "after")); |
| 73 | assert!(found(&mut index, "before").is_empty()); |
| 74 | assert_eq!(found(&mut index, "after"), vec!["$a"]); |
| 75 | } |
| 76 | |
| 77 | #[test] |
| 78 | fn rooms_filter_the_results() { |
| 79 | let mut index = KeywordIndex::new(false); |
| 80 | index.put(doc("$a", "!one:x", 1, "word")); |
| 81 | index.put(doc("$b", "!two:x", 2, "word")); |
| 82 | let rooms = vec!["!one:x".to_owned()]; |
| 83 | assert_eq!( |
| 84 | keys(&index.search("word", Some(&rooms), None, 10).unwrap().0), |
| 85 | vec!["$a"] |
| 86 | ); |
| 87 | assert!( |
| 88 | index |
| 89 | .search("word", Some(&[]), None, 10) |
| 90 | .unwrap() |
| 91 | .0 |
| 92 | .is_empty() |
| 93 | ); |
| 94 | } |
| 95 | |
| 96 | #[test] |
| 97 | fn provisional_docs_stay_out_of_results() { |
| 98 | let mut index = KeywordIndex::new(false); |
| 99 | let mut pending = doc(&provisional_key("$o", "@a:x"), "!r:x", 5, "early edit"); |
| 100 | pending.provisional = true; |
| 101 | index.put(pending); |
| 102 | assert!(found(&mut index, "early").is_empty()); |
| 103 | assert!(index.remove_provisional("$o")); |
| 104 | assert!(index.get(&provisional_key("$o", "@a:x")).is_none()); |
| 105 | } |
| 106 | |
| 107 | /// the manifest used to be read before `committed`, one commit stale, so a restart lost |
| 108 | /// the last flush and merge, and a bumped delete generation failed the whole load |
| 109 | #[test] |
| 110 | fn every_commit_reloads_to_what_was_committed() { |
| 111 | let mut store = BTreeMap::new(); |
| 112 | let mut index = KeywordIndex::new(true); |
| 113 | let count = MERGE_FACTOR as u64 + 2; |
| 114 | for ts in 0..count { |
| 115 | index.put(doc(&format!("$e{ts}"), "!r:x", ts, "kept")); |
| 116 | commit(&mut index, &mut store); |
| 117 | assert_eq!(found(&mut reload(&store), "kept").len(), ts as usize + 1); |
| 118 | } |
| 119 | index.remove("$e0"); |
| 120 | commit(&mut index, &mut store); |
| 121 | index.remove("$e1"); |
| 122 | commit(&mut index, &mut store); |
| 123 | index.put(doc("$e2", "!r:x", 2, "edited")); |
| 124 | commit(&mut index, &mut store); |
| 125 | let mut reloaded = reload(&store); |
| 126 | let expected: Vec<String> = (3..count).map(|ts| format!("$e{ts}")).collect(); |
| 127 | assert_eq!(found(&mut reloaded, "kept"), expected); |
| 128 | assert_eq!(found(&mut reloaded, "edited"), vec!["$e2"]); |
| 129 | let mut referenced = reloaded.referenced(); |
| 130 | referenced.push("meta".to_owned()); |
| 131 | referenced.sort(); |
| 132 | let stored: Vec<_> = store.keys().cloned().collect(); |
| 133 | assert_eq!(stored, referenced, "a commit left blobs nothing points at"); |
| 134 | } |
| 135 | |
| 136 | #[test] |
| 137 | fn unspaced_text_matches_inside_a_run() { |
| 138 | let mut index = KeywordIndex::new(false); |
| 139 | index.put(doc("$a", "!r:x", 1, "明日東京都庁に行きます")); |
| 140 | assert_eq!(found(&mut index, "東京"), vec!["$a"]); |
| 141 | assert_eq!(found(&mut index, "\"都庁\""), vec!["$a"]); |
| 142 | assert!(found(&mut index, "大阪").is_empty()); |
| 143 | } |
| 144 | |
| 145 | #[test] |
| 146 | fn packed_segments_reject_truncation() { |
| 147 | let packed = pack(&[("a".to_owned(), OwnedBytes::new(vec![1, 2, 3]))]); |
| 148 | assert!(unpack(&packed).is_some()); |
| 149 | assert!(unpack(&packed[..packed.len() - 1]).is_none()); |
| 150 | } |