This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / knot2 / crates / knot-index / tests / lifecycle.rs
11 kB 357 lines
1use std::sync::Arc; 2use std::sync::atomic::{AtomicBool, Ordering}; 3 4use knot_cob::{CobHome, CobStore}; 5use knot_cobs::{CollaboratorsChange, CollaboratorsCob, Grant, MembersChange}; 6use knot_git::Repo; 7use knot_index::{Coverage, IndexCoverage, IndexError, OfferedKey, Resolved}; 8 9mod common; 10use common::{World, acc, at, grant, meta_home, own, repo_did, rkey}; 11 12#[test] 13fn rebuild_folds_members_and_registry_and_collaborators_fold_on_access() { 14 let world = World::new(); 15 let repo = repo_did("squid"); 16 world.seed_members(); 17 world.seed_registry(&repo); 18 world.seed_collaborator(&repo, "lyna"); 19 20 let index = world.index(); 21 index.rebuild().unwrap(); 22 23 assert_eq!(index.is_member(&acc("nel")), Resolved::Ready(true)); 24 assert_eq!(index.is_member(&acc("olaren")), Resolved::Ready(true)); 25 assert_eq!(index.is_member(&acc("teq")), Resolved::Ready(false)); 26 assert_eq!( 27 index.resolve_repo(&own("nel"), &rkey("anemone")), 28 Resolved::Ready(Some(repo.clone())) 29 ); 30 assert_eq!( 31 index.resolve_repo(&own("nel"), &rkey("nautilus")), 32 Resolved::Ready(None) 33 ); 34 assert_eq!( 35 index.is_collaborator(&repo, &acc("lyna")), 36 Resolved::Warming, 37 "rebuild doesn't fold collaborators, so roster reads warming until first access" 38 ); 39 40 index.ensure_collaborators(&repo).unwrap(); 41 assert_eq!( 42 index.is_collaborator(&repo, &acc("lyna")), 43 Resolved::Ready(true) 44 ); 45 assert_eq!( 46 index.is_collaborator(&repo, &acc("bailey")), 47 Resolved::Ready(false) 48 ); 49 assert_eq!( 50 index.coverage(), 51 IndexCoverage { 52 members: Coverage::Ready, 53 blocklist: Coverage::Ready, 54 collaborators: Coverage::Ready, 55 registry: Coverage::Ready, 56 keys: Coverage::Warming, 57 } 58 ); 59} 60 61#[test] 62fn every_accessor_fails_closed_while_warming() { 63 let world = World::new(); 64 world.seed_members(); 65 world.seed_collaborator(&repo_did("squid"), "lyna"); 66 let index = world.index(); 67 68 assert_eq!(index.is_member(&acc("nel")), Resolved::Warming); 69 assert_eq!(index.member_entries(), Resolved::Warming); 70 assert_eq!( 71 index.is_collaborator(&repo_did("squid"), &acc("lyna")), 72 Resolved::Warming 73 ); 74 assert_eq!( 75 index.collaborator_entries(&repo_did("squid")), 76 Resolved::Warming, 77 "roster that has never been folded fails closed" 78 ); 79 assert_eq!( 80 index.resolve_repo(&own("nel"), &rkey("anemone")), 81 Resolved::Warming 82 ); 83 assert_eq!( 84 index.owner_of(&repo_did("squid")), 85 Resolved::Warming, 86 "repo lookup before rebuild fails closed" 87 ); 88 assert_eq!(index.coverage().members, Coverage::Warming); 89 90 assert_eq!( 91 index.owner_of_key(&OfferedKey::from_bytes(vec![1, 2, 3]), at(0)), 92 Resolved::Ready(None), 93 "a key lookup answers from the first request, even while the key set is warming" 94 ); 95} 96 97#[test] 98fn member_entries_record_provenance() { 99 let world = World::new(); 100 world.seed_members(); 101 let index = world.index(); 102 index.rebuild().unwrap(); 103 assert_eq!( 104 index.member_entries(), 105 Resolved::Ready(vec![grant("nel", "nel", 1), grant("olaren", "nel", 2)]) 106 ); 107} 108 109#[test] 110fn a_re_added_member_keeps_the_first_provenance() { 111 let world = World::new(); 112 let members = world.seed_members(); 113 let index = world.index(); 114 index.rebuild().unwrap(); 115 116 let meta = Repo::open(&world.meta_path).unwrap(); 117 let store = CobStore::new(&meta); 118 store 119 .update( 120 &meta_home(), 121 members, 122 &MembersChange::Add(grant("olaren", "teq", 9)), 123 &world.signer, 124 at(9), 125 ) 126 .unwrap(); 127 index.refresh_members().unwrap(); 128 129 assert_eq!( 130 index.member_entries(), 131 Resolved::Ready(vec![grant("nel", "nel", 1), grant("olaren", "nel", 2)]), 132 "duplicate add never rewrites original provenance, matching canonical roster" 133 ); 134} 135 136#[test] 137fn collaborator_entries_match_the_canonical_roster() { 138 let world = World::new(); 139 let repo = repo_did("squid"); 140 world.seed_registry(&repo); 141 let object = world.seed_collaborator(&repo, "lyna"); 142 let git = world.layout.open(&repo).unwrap(); 143 let store = CobStore::new(&git); 144 store 145 .update( 146 &CobHome::from(&repo), 147 object, 148 &CollaboratorsChange::Add(grant("bailey", "olaren", 2)), 149 &world.signer, 150 at(2), 151 ) 152 .unwrap(); 153 world.remove_collaborator(&repo, object, "lyna", 3); 154 store 155 .update( 156 &CobHome::from(&repo), 157 object, 158 &CollaboratorsChange::Add(grant("lyna", "teq", 5)), 159 &world.signer, 160 at(5), 161 ) 162 .unwrap(); 163 164 let index = world.index(); 165 index.rebuild().unwrap(); 166 index.ensure_collaborators(&repo).unwrap(); 167 168 let canonical = store.get::<CollaboratorsCob>(object).unwrap(); 169 let expected: Vec<Grant> = canonical 170 .state() 171 .entries() 172 .map(|(subject, entry)| Grant { 173 subject: subject.clone(), 174 added_by: entry.added_by.clone(), 175 created_at: entry.created_at, 176 }) 177 .collect(); 178 assert_eq!( 179 index.collaborator_entries(&repo), 180 Resolved::Ready(expected), 181 "projected entries disagree with canonical Evaluate fold" 182 ); 183} 184 185#[test] 186fn a_folded_repo_serves_while_unaccessed_repos_stay_warming() { 187 let world = World::new(); 188 let present = repo_did("squid"); 189 let absent = repo_did("kelp"); 190 let registry = world.seed_registry(&present); 191 world.register_extra(&absent, "barnacle", registry); 192 world.seed_collaborator(&present, "lyna"); 193 194 let index = world.index(); 195 index.rebuild().unwrap(); 196 assert_eq!( 197 index.coverage().collaborators, 198 Coverage::Ready, 199 "collaborators projection is operational from boot" 200 ); 201 index.ensure_collaborators(&present).unwrap(); 202 203 assert_eq!( 204 index.is_collaborator(&present, &acc("lyna")), 205 Resolved::Ready(true) 206 ); 207 assert_eq!( 208 index 209 .collaborator_entries(&present) 210 .map(|grants| grants.len()), 211 Resolved::Ready(1), 212 "folded repo serves its roster" 213 ); 214 assert_eq!( 215 index.collaborator_entries(&absent), 216 Resolved::Warming, 217 "registered repo that was never accessed stays fail-closed until folded" 218 ); 219 assert_eq!( 220 index.is_collaborator(&repo_did("conch"), &acc("lyna")), 221 Resolved::Warming, 222 "repo the index never folded cannot answer, so it fails closed" 223 ); 224} 225 226#[test] 227fn an_ambiguous_meta_cob_fails_refresh_and_rebuild() { 228 let world = World::new(); 229 let meta = Repo::open(&world.meta_path).unwrap(); 230 let store = CobStore::new(&meta); 231 store 232 .create( 233 &meta_home(), 234 &MembersChange::Add(grant("nel", "nel", 1)), 235 &world.signer, 236 at(1), 237 ) 238 .unwrap(); 239 store 240 .create( 241 &meta_home(), 242 &MembersChange::Add(grant("olaren", "olaren", 2)), 243 &world.signer, 244 at(2), 245 ) 246 .unwrap(); 247 248 let index = world.index(); 249 assert!(matches!( 250 index.refresh_members(), 251 Err(IndexError::Ambiguous { count: 2, .. }) 252 )); 253 assert!( 254 matches!(index.rebuild(), Err(IndexError::Ambiguous { .. })), 255 "broken meta COB fails whole boot instead of reporting partial one" 256 ); 257} 258 259#[test] 260fn a_repo_missing_on_disk_does_not_fail_the_boot_and_isolates_its_fold() { 261 let world = World::new(); 262 let present = repo_did("squid"); 263 let absent = repo_did("kelp"); 264 world.seed_members(); 265 let registry = world.seed_registry(&present); 266 world.register_extra(&absent, "barnacle", registry); 267 world.seed_collaborator(&present, "lyna"); 268 269 let index = world.index(); 270 index 271 .rebuild() 272 .expect("boot folds members and registry only, so missing repo dir never fails it"); 273 274 index.ensure_collaborators(&present).unwrap(); 275 assert_eq!( 276 index.is_collaborator(&present, &acc("lyna")), 277 Resolved::Ready(true) 278 ); 279 assert!( 280 index.ensure_collaborators(&absent).is_err(), 281 "folding repo with no dir on disk fails for that repo alone" 282 ); 283 assert_eq!( 284 index.is_collaborator(&absent, &acc("lyna")), 285 Resolved::Warming, 286 "repo the index couldn't fold stays fail-closed" 287 ); 288} 289 290#[test] 291fn concurrent_refreshes_of_distinct_repos_all_land() { 292 let world = World::new(); 293 let repos = ["squid", "clam", "whelk", "conch"]; 294 repos.iter().for_each(|repo| { 295 world.seed_collaborator(&repo_did(repo), "lyna"); 296 }); 297 298 let index = Arc::new(world.index()); 299 std::thread::scope(|scope| { 300 repos.iter().for_each(|repo| { 301 let index = Arc::clone(&index); 302 let repo = repo_did(repo); 303 scope.spawn(move || index.refresh_collaborators(&repo).unwrap()); 304 }); 305 }); 306 307 repos.iter().for_each(|repo| { 308 assert_eq!( 309 index.is_collaborator(&repo_did(repo), &acc("lyna")), 310 Resolved::Ready(true) 311 ); 312 }); 313} 314 315#[test] 316fn a_refresh_is_eventually_consistent_not_an_atomic_snapshot() { 317 let world = World::new(); 318 let members = world.seed_members(); 319 let index = Arc::new(world.index()); 320 index.rebuild().unwrap(); 321 322 (0..32).for_each(|i| world.add_member(members, &format!("m{i}"), 10 + i as i64)); 323 324 let done = Arc::new(AtomicBool::new(false)); 325 std::thread::scope(|scope| { 326 let writer = Arc::clone(&index); 327 let writer_done = Arc::clone(&done); 328 scope.spawn(move || { 329 writer.refresh_members().unwrap(); 330 writer_done.store(true, Ordering::Release); 331 }); 332 333 let reader = Arc::clone(&index); 334 let reader_done = Arc::clone(&done); 335 scope.spawn(move || { 336 while !reader_done.load(Ordering::Acquire) { 337 assert_eq!( 338 reader.is_member(&acc("nel")), 339 Resolved::Ready(true), 340 "stable member stays visible and read never blocks on writer" 341 ); 342 assert!( 343 !reader.is_member(&acc("m0")).is_warming(), 344 "already-ready projection serves reads mid-refresh, it never re-warms" 345 ); 346 } 347 }); 348 }); 349 350 (0..32).for_each(|i| { 351 assert_eq!( 352 index.is_member(&acc(&format!("m{i}"))), 353 Resolved::Ready(true), 354 "once writer returns, whole delta has converged" 355 ); 356 }); 357}