oxedyne/fe2o3/fe2o3_o3db_sync/tests/scan.rs
6.9 KiB, 9 runs
created by r1870400018:10314, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | //! Ozone scan integration test. |
| 2 | //! |
| 3 | //! Populates a fresh database with a mix of key prefixes, then |
| 4 | //! exercises [`Database::scan`] through several option shapes |
| 5 | //! (all, prefix, limit, overwrite) and verifies the expected |
| 6 | //! entries come back. |
| 7 | //! |
| 8 | //! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\ |
| 9 | //! Anthropic Claude |
| 10 | |
| 11 | use oxedyne_fe2o3_core::{ |
| 12 | prelude::*, |
| 13 | alt::Override, |
| 14 | }; |
| 15 | use oxedyne_fe2o3_crypto::enc::EncryptionScheme; |
| 16 | use oxedyne_fe2o3_hash::{ |
| 17 | csum::ChecksumScheme, |
| 18 | hash::HashScheme, |
| 19 | }; |
| 20 | use oxedyne_fe2o3_iop_db::api::{ |
| 21 | Database, |
| 22 | RestSchemesOverride, |
| 23 | ScanOpts, |
| 24 | }; |
| 25 | use oxedyne_fe2o3_jdat::prelude::*; |
| 26 | use oxedyne_fe2o3_o3db_sync::{ |
| 27 | data::core::RestSchemesInput, |
| 28 | test::setup, |
| 29 | }; |
| 30 | |
| 31 | use std::{ |
| 32 | collections::HashSet, |
| 33 | path::Path, |
| 34 | thread, |
| 35 | time::Duration, |
| 36 | }; |
| 37 | |
| 38 | pub fn test_scan(_filter: &'static str) -> Outcome<()> { |
| 39 | |
| 40 | let db_root = res!(Path::new("./test_db_scan").canonicalize().or_else(|_| { |
| 41 | ok!(std::fs::create_dir_all("./test_db_scan")); |
| 42 | Path::new("./test_db_scan").canonicalize() |
| 43 | })); |
| 44 | |
| 45 | // Fixed key so the test is deterministic. |
| 46 | let enckey = [0x42u8; 32]; |
| 47 | let aes_gcm = res!(EncryptionScheme::new_aes_256_gcm_with_key(&enckey[..])); |
| 48 | let crc32 = ChecksumScheme::new_crc32(); |
| 49 | let schms2: RestSchemesOverride<EncryptionScheme, HashScheme> = |
| 50 | RestSchemesOverride::default() |
| 51 | .set_encrypter(Override::Default(aes_gcm.clone())); |
| 52 | let schms2 = Some(&schms2); |
| 53 | let user = setup::Uid::default(); |
| 54 | |
| 55 | let schms_input = RestSchemesInput::new( |
| 56 | Some(aes_gcm.clone()), |
| 57 | None::<HashScheme>, |
| 58 | None::<HashScheme>, |
| 59 | Some(crc32.clone()), |
| 60 | ); |
| 61 | |
| 62 | let mut cfg = res!(setup::default_cfg()); |
| 63 | cfg.num_zones = 3; |
| 64 | cfg.num_cbots_per_zone = 2; |
| 65 | cfg.num_igbots_per_zone = 2; |
| 66 | cfg.data_file_max_bytes = 200_000; |
| 67 | // Keep every zone inside the per-test db root so this test |
| 68 | // cannot inherit or contaminate state from sibling tests |
| 69 | // (basic.rs uses "../test_db_zone_container" for zone 1). |
| 70 | cfg.zone_overrides = mapdat!{ |
| 71 | 1u16 => mapdat!{ "dir" => "", "max_size" => 10_000_000u64 }, |
| 72 | 2u16 => mapdat!{ "dir" => "", "max_size" => 10_000_000u64 }, |
| 73 | 3u16 => mapdat!{ "dir" => "", "max_size" => 10_000_000u64 }, |
| 74 | }.get_map().unwrap(); |
| 75 | |
| 76 | test!(sync_log::stream(), "+---------------------------------------------+"); |
| 77 | test!(sync_log::stream(), "| SCAN TEST |"); |
| 78 | test!(sync_log::stream(), "+---------------------------------------------+"); |
| 79 | |
| 80 | let mut db = res!(setup::start_db( |
| 81 | db_root.clone(), |
| 82 | Some(cfg.clone()), |
| 83 | schms_input.clone(), |
| 84 | None, |
| 85 | true, // gc on |
| 86 | true, // wipe |
| 87 | )); |
| 88 | |
| 89 | thread::sleep(Duration::from_secs(1)); |
| 90 | |
| 91 | // 1. Populate with a mix of prefixes. |
| 92 | // - 10 under "user:" |
| 93 | // - 5 under "sess:" |
| 94 | // - 3 under "cfg:" |
| 95 | test!(sync_log::stream(), "Populating 18 keys across three prefixes."); |
| 96 | for i in 0..10 { |
| 97 | res!(db.insert( |
| 98 | dat!(fmt!("user:{:03}", i)), |
| 99 | dat!(fmt!("profile_{}", i)), |
| 100 | user, |
| 101 | schms2, |
| 102 | )); |
| 103 | } |
| 104 | for i in 0..5 { |
| 105 | res!(db.insert( |
| 106 | dat!(fmt!("sess:{:03}", i)), |
| 107 | dat!(fmt!("token_{}", i)), |
| 108 | user, |
| 109 | schms2, |
| 110 | )); |
| 111 | } |
| 112 | for i in 0..3 { |
| 113 | res!(db.insert( |
| 114 | dat!(fmt!("cfg:{:03}", i)), |
| 115 | dat!(fmt!("setting_{}", i)), |
| 116 | user, |
| 117 | schms2, |
| 118 | )); |
| 119 | } |
| 120 | |
| 121 | thread::sleep(Duration::from_millis(500)); |
| 122 | |
| 123 | // 2. Scan everything. 18 entries expected. |
| 124 | test!(sync_log::stream(), "Scan all: expecting 18 entries."); |
| 125 | let all = res!(db.scan(&ScanOpts::all(), schms2)); |
| 126 | if all.len() != 18 { |
| 127 | return Err(err!( |
| 128 | "Expected 18 entries from scan-all, got {}.", all.len(); |
| 129 | Test, Mismatch)); |
| 130 | } |
| 131 | let all_keys: HashSet<String> = all.iter() |
| 132 | .filter_map(|(k, _, _)| match k { |
| 133 | Dat::Str(s) => Some(s.clone()), |
| 134 | _ => None, |
| 135 | }) |
| 136 | .collect(); |
| 137 | if all_keys.len() != 18 { |
| 138 | return Err(err!( |
| 139 | "Expected 18 distinct Str keys, got {}.", all_keys.len(); |
| 140 | Test, Mismatch)); |
| 141 | } |
| 142 | |
| 143 | // 3. Prefix scan "user:". 10 expected. |
| 144 | test!(sync_log::stream(), "Prefix scan 'user:' expecting 10 entries."); |
| 145 | let user_only = res!(db.scan( |
| 146 | &ScanOpts::with_str_prefix("user:"), |
| 147 | schms2, |
| 148 | )); |
| 149 | if user_only.len() != 10 { |
| 150 | return Err(err!( |
| 151 | "Expected 10 'user:' entries, got {}.", user_only.len(); |
| 152 | Test, Mismatch)); |
| 153 | } |
| 154 | for (k, _, _) in &user_only { |
| 155 | match k { |
| 156 | Dat::Str(s) => { |
| 157 | if !s.starts_with("user:") { |
| 158 | return Err(err!( |
| 159 | "Prefix scan returned non-matching key {:?}.", k; |
| 160 | Test, Mismatch)); |
| 161 | } |
| 162 | }, |
| 163 | other => return Err(err!( |
| 164 | "Expected Dat::Str key, got {:?}.", other; |
| 165 | Test, Mismatch)), |
| 166 | } |
| 167 | } |
| 168 | |
| 169 | // 4. Prefix scan "sess:". 5 expected. |
| 170 | test!(sync_log::stream(), "Prefix scan 'sess:' expecting 5 entries."); |
| 171 | let sess_only = res!(db.scan( |
| 172 | &ScanOpts::with_str_prefix("sess:"), |
| 173 | schms2, |
| 174 | )); |
| 175 | if sess_only.len() != 5 { |
| 176 | return Err(err!( |
| 177 | "Expected 5 'sess:' entries, got {}.", sess_only.len(); |
| 178 | Test, Mismatch)); |
| 179 | } |
| 180 | |
| 181 | // 5. Prefix scan "nope:". 0 expected. |
| 182 | test!(sync_log::stream(), "Prefix scan 'nope:' expecting 0 entries."); |
| 183 | let nope_only = res!(db.scan( |
| 184 | &ScanOpts::with_str_prefix("nope:"), |
| 185 | schms2, |
| 186 | )); |
| 187 | if nope_only.len() != 0 { |
| 188 | return Err(err!( |
| 189 | "Expected 0 'nope:' entries, got {}.", nope_only.len(); |
| 190 | Test, Mismatch)); |
| 191 | } |
| 192 | |
| 193 | // 6. Limit. 5 expected from a scan-all capped at 5. |
| 194 | test!(sync_log::stream(), "Scan all with limit=5 expecting 5 entries."); |
| 195 | let limited = res!(db.scan( |
| 196 | &ScanOpts::all().limit(5), |
| 197 | schms2, |
| 198 | )); |
| 199 | if limited.len() != 5 { |
| 200 | return Err(err!( |
| 201 | "Expected 5 entries under limit=5, got {}.", limited.len(); |
| 202 | Test, Mismatch)); |
| 203 | } |
| 204 | |
| 205 | // 7. Overwrite. Re-insert "user:000" and expect scan still to |
| 206 | // return 18 entries total -- one per distinct key -- not 19. |
| 207 | test!(sync_log::stream(), "Overwrite 'user:000' and rescan; still expecting 18 entries."); |
| 208 | res!(db.insert( |
| 209 | dat!("user:000"), |
| 210 | dat!("profile_0_v2"), |
| 211 | user, |
| 212 | schms2, |
| 213 | )); |
| 214 | thread::sleep(Duration::from_millis(500)); |
| 215 | let after_overwrite = res!(db.scan(&ScanOpts::all(), schms2)); |
| 216 | if after_overwrite.len() != 18 { |
| 217 | return Err(err!( |
| 218 | "Expected 18 entries after overwrite, got {}.", after_overwrite.len(); |
| 219 | Test, Mismatch)); |
| 220 | } |
| 221 | |
| 222 | test!(sync_log::stream(), "Scan test passed."); |
| 223 | thread::sleep(Duration::from_secs(1)); |
| 224 | res!(db.shutdown()); |
| 225 | Ok(()) |
| 226 | } |