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 / projections.rs
16 kB 567 lines
1use std::sync::Arc; 2use std::sync::atomic::{AtomicBool, Ordering}; 3 4use knot_cob::{ChangePayload, CobHome, CobId, CobStore}; 5use knot_cobs::{CollaboratorsChange, MembersChange, RegistryChange, Removal, Rename, RepoRef}; 6use knot_git::{RefUpdate, Repo}; 7use knot_index::{Coverage, IndexError, OfferedKey, Resolved}; 8use knot_types::{RefName, RepoName}; 9use serde::{Deserialize, Serialize}; 10 11mod common; 12use common::{World, acc, at, grant, meta_home, own, registration, repo_did, rkey}; 13 14#[derive(Serialize, Deserialize)] 15#[serde(tag = "op", content = "data", rename_all = "snake_case")] 16enum BadMembers { 17 Explode(u8), 18} 19impl ChangePayload for BadMembers { 20 const TYPE: &'static str = "sh.tangled.knot.member"; 21} 22 23#[test] 24fn a_concurrent_reader_never_sees_a_net_absent_subject() { 25 let world = World::new(); 26 let object = world.seed_members(); 27 let index = Arc::new(world.index()); 28 index.rebuild().unwrap(); 29 assert_eq!(index.is_member(&acc("teq")), Resolved::Ready(false)); 30 31 let meta = Repo::open(&world.meta_path).unwrap(); 32 let store = CobStore::new(&meta); 33 store 34 .update( 35 &meta_home(), 36 object, 37 &MembersChange::Add(grant("teq", "nel", 10)), 38 &world.signer, 39 at(10), 40 ) 41 .unwrap(); 42 (0..1000).for_each(|i| { 43 store 44 .update( 45 &meta_home(), 46 object, 47 &MembersChange::Add(grant(&format!("f{i}"), "nel", 11 + i)), 48 &world.signer, 49 at(11 + i), 50 ) 51 .unwrap(); 52 }); 53 store 54 .update( 55 &meta_home(), 56 object, 57 &MembersChange::Remove(Removal { 58 subject: acc("teq"), 59 }), 60 &world.signer, 61 at(20_000), 62 ) 63 .unwrap(); 64 65 let done = Arc::new(AtomicBool::new(false)); 66 let leaked = Arc::new(AtomicBool::new(false)); 67 std::thread::scope(|scope| { 68 let reader = Arc::clone(&index); 69 let reader_done = Arc::clone(&done); 70 let reader_leaked = Arc::clone(&leaked); 71 scope.spawn(move || { 72 while !reader_done.load(Ordering::Acquire) { 73 if reader.is_member(&acc("teq")) == Resolved::Ready(true) { 74 reader_leaked.store(true, Ordering::Release); 75 } 76 } 77 }); 78 index.refresh_members().unwrap(); 79 done.store(true, Ordering::Release); 80 }); 81 82 assert!( 83 !leaked.load(Ordering::Acquire), 84 "net-absent subject is never written, so no reader can observe it mid-delta" 85 ); 86 assert_eq!(index.is_member(&acc("teq")), Resolved::Ready(false)); 87 assert_eq!(index.is_member(&acc("f0")), Resolved::Ready(true)); 88 assert_eq!(index.is_member(&acc("f999")), Resolved::Ready(true)); 89} 90 91#[test] 92fn a_concurrent_reader_never_sees_a_collaborator_roster_emptied_mid_refresh() { 93 let world = World::new(); 94 let repo = repo_did("squid"); 95 let git = world.layout.create(&repo).unwrap(); 96 let store = CobStore::new(&git); 97 let object = store 98 .create( 99 &CobHome::from(&repo), 100 &CollaboratorsChange::Add(grant("anchor", "nel", 1)), 101 &world.signer, 102 at(1), 103 ) 104 .unwrap() 105 .object; 106 (0..128).for_each(|i| { 107 store 108 .update( 109 &CobHome::from(&repo), 110 object, 111 &CollaboratorsChange::Add(grant(&format!("c{i}"), "nel", 2 + i)), 112 &world.signer, 113 at(2 + i), 114 ) 115 .unwrap(); 116 }); 117 118 let index = Arc::new(world.index()); 119 index.rebuild().unwrap(); 120 index.ensure_collaborators(&repo).unwrap(); 121 assert_eq!( 122 index.is_collaborator(&repo, &acc("anchor")), 123 Resolved::Ready(true) 124 ); 125 126 let done = Arc::new(AtomicBool::new(false)); 127 let leaked = Arc::new(AtomicBool::new(false)); 128 std::thread::scope(|scope| { 129 let reader = Arc::clone(&index); 130 let reader_done = Arc::clone(&done); 131 let reader_leaked = Arc::clone(&leaked); 132 let target = repo.clone(); 133 scope.spawn(move || { 134 while !reader_done.load(Ordering::Acquire) { 135 if reader.is_collaborator(&target, &acc("anchor")) != Resolved::Ready(true) { 136 reader_leaked.store(true, Ordering::Release); 137 } 138 } 139 }); 140 (0..500).for_each(|_| index.refresh_collaborators(&repo).unwrap()); 141 done.store(true, Ordering::Release); 142 }); 143 144 assert!( 145 !leaked.load(Ordering::Acquire), 146 "in-place mem::take runs under per-repo lock, so anchor present in both \ 147 pre- and post-refresh roster is never observed absent or warming mid-refresh" 148 ); 149 assert_eq!( 150 index.is_collaborator(&repo, &acc("anchor")), 151 Resolved::Ready(true) 152 ); 153} 154 155fn cob_ref(type_name: &str, object: CobId) -> RefName { 156 RefName::new(format!("refs/cobs/{type_name}/{}", object.oid())).unwrap() 157} 158 159#[test] 160fn a_diverged_collaborators_tip_purges_the_roster_instead_of_serving_it_stale() { 161 let world = World::new(); 162 let repo = repo_did("squid"); 163 let git = world.layout.create(&repo).unwrap(); 164 let store = CobStore::new(&git); 165 let created = store 166 .create( 167 &CobHome::from(&repo), 168 &CollaboratorsChange::Add(grant("lyna", "nel", 1)), 169 &world.signer, 170 at(1), 171 ) 172 .unwrap(); 173 let tip = store 174 .update( 175 &CobHome::from(&repo), 176 created.object, 177 &CollaboratorsChange::Add(grant("bailey", "nel", 2)), 178 &world.signer, 179 at(2), 180 ) 181 .unwrap(); 182 183 let index = world.index(); 184 index.rebuild().unwrap(); 185 index.ensure_collaborators(&repo).unwrap(); 186 assert_eq!( 187 index.is_collaborator(&repo, &acc("lyna")), 188 Resolved::Ready(true) 189 ); 190 191 git.update_ref(&RefUpdate::Update { 192 name: cob_ref(CollaboratorsChange::TYPE, created.object), 193 old: tip.oid(), 194 new: created.object.oid(), 195 }) 196 .unwrap(); 197 198 assert!( 199 index.refresh_collaborators(&repo).is_err(), 200 "tip that no longer descends from folded tip is structural error" 201 ); 202 assert_eq!( 203 index.is_collaborator(&repo, &acc("lyna")), 204 Resolved::Warming, 205 "diverged COB tip purges roster and fails closed, it does not serve \ 206 pre-divergence collaborators" 207 ); 208} 209 210#[test] 211fn a_diverged_members_tip_fails_closed_to_warming() { 212 let world = World::new(); 213 let object = world.seed_members(); 214 let meta = Repo::open(&world.meta_path).unwrap(); 215 let store = CobStore::new(&meta); 216 let tip = store 217 .update( 218 &meta_home(), 219 object, 220 &MembersChange::Add(grant("teq", "nel", 3)), 221 &world.signer, 222 at(3), 223 ) 224 .unwrap(); 225 226 let index = world.index(); 227 index.rebuild().unwrap(); 228 assert_eq!(index.is_member(&acc("nel")), Resolved::Ready(true)); 229 230 meta.update_ref(&RefUpdate::Update { 231 name: cob_ref(MembersChange::TYPE, object), 232 old: tip.oid(), 233 new: object.oid(), 234 }) 235 .unwrap(); 236 237 assert!( 238 index.refresh_members().is_err(), 239 "tip that no longer descends from folded tip is structural error" 240 ); 241 assert_eq!(index.coverage().members, Coverage::Warming); 242 assert_eq!( 243 index.is_member(&acc("nel")), 244 Resolved::Warming, 245 "diverged members COB fails projection closed instead of serving stale members" 246 ); 247} 248 249#[test] 250fn an_undecodable_change_fails_closed_with_no_partial_apply() { 251 let world = World::new(); 252 let object = world.seed_members(); 253 let index = world.index(); 254 index.rebuild().unwrap(); 255 256 let meta = Repo::open(&world.meta_path).unwrap(); 257 let store = CobStore::new(&meta); 258 store 259 .update( 260 &meta_home(), 261 object, 262 &MembersChange::Add(grant("teq", "nel", 3)), 263 &world.signer, 264 at(3), 265 ) 266 .unwrap(); 267 store 268 .update( 269 &meta_home(), 270 object, 271 &BadMembers::Explode(0), 272 &world.signer, 273 at(4), 274 ) 275 .unwrap(); 276 store 277 .update( 278 &meta_home(), 279 object, 280 &MembersChange::Remove(Removal { 281 subject: acc("teq"), 282 }), 283 &world.signer, 284 at(5), 285 ) 286 .unwrap(); 287 288 assert!(matches!( 289 index.refresh_members(), 290 Err(IndexError::Decode { .. }) 291 )); 292 293 assert_eq!(index.coverage().members, Coverage::Warming); 294 assert_eq!( 295 index.is_member(&acc("teq")), 296 Resolved::Warming, 297 "no partial apply: teq from pre-error change was never committed" 298 ); 299 assert_eq!( 300 index.is_member(&acc("nel")), 301 Resolved::Warming, 302 "structurally broken COB fails whole projection closed" 303 ); 304 305 assert!(matches!( 306 index.refresh_members(), 307 Err(IndexError::Decode { .. }) 308 )); 309 assert_eq!(index.coverage().members, Coverage::Warming); 310} 311 312#[test] 313fn deregister_purges_collaborators_fail_closed() { 314 let world = World::new(); 315 let repo = repo_did("clam"); 316 317 let meta = Repo::open(&world.meta_path).unwrap(); 318 let store = CobStore::new(&meta); 319 let registry = store 320 .create( 321 &meta_home(), 322 &RegistryChange::Register(registration("nel", "anemone", &repo, 1)), 323 &world.signer, 324 at(1), 325 ) 326 .unwrap() 327 .object; 328 329 let git = world.layout.create(&repo).unwrap(); 330 let cstore = CobStore::new(&git); 331 cstore 332 .create( 333 &CobHome::from(&repo), 334 &CollaboratorsChange::Add(grant("lyna", "nel", 1)), 335 &world.signer, 336 at(1), 337 ) 338 .unwrap(); 339 340 let index = world.index(); 341 index.rebuild().unwrap(); 342 index.warm_collaborators(); 343 assert_eq!( 344 index.is_collaborator(&repo, &acc("lyna")), 345 Resolved::Ready(true) 346 ); 347 348 store 349 .update( 350 &meta_home(), 351 registry, 352 &RegistryChange::Deregister(RepoRef { 353 owner: own("nel"), 354 rkey: rkey("anemone"), 355 }), 356 &world.signer, 357 at(2), 358 ) 359 .unwrap(); 360 index.refresh_registry().unwrap(); 361 362 assert_eq!( 363 index.resolve_repo(&own("nel"), &rkey("anemone")), 364 Resolved::Ready(None) 365 ); 366 assert_eq!( 367 index.is_collaborator(&repo, &acc("lyna")), 368 Resolved::Warming, 369 "deregistered repo's collaborators are purged and fail closed, not served stale" 370 ); 371} 372 373#[test] 374fn a_renamed_repo_keeps_both_rkeys_and_its_collaborators() { 375 let world = World::new(); 376 let repo = repo_did("squid"); 377 378 let meta = Repo::open(&world.meta_path).unwrap(); 379 let store = CobStore::new(&meta); 380 let registry = store 381 .create( 382 &meta_home(), 383 &RegistryChange::Register(registration("nel", "anemone", &repo, 1)), 384 &world.signer, 385 at(1), 386 ) 387 .unwrap() 388 .object; 389 390 let git = world.layout.create(&repo).unwrap(); 391 CobStore::new(&git) 392 .create( 393 &CobHome::from(&repo), 394 &CollaboratorsChange::Add(grant("lyna", "nel", 1)), 395 &world.signer, 396 at(1), 397 ) 398 .unwrap(); 399 400 let index = world.index(); 401 index.rebuild().unwrap(); 402 index.warm_collaborators(); 403 assert_eq!(index.rkey_of(&repo), Resolved::Ready(Some(rkey("anemone")))); 404 405 store 406 .update( 407 &meta_home(), 408 registry, 409 &RegistryChange::Rename(Rename { 410 owner: own("nel"), 411 rkey: rkey("barnacle"), 412 name: RepoName::new("barnacle").unwrap(), 413 repo: repo.clone(), 414 }), 415 &world.signer, 416 at(2), 417 ) 418 .unwrap(); 419 index.refresh_registry().unwrap(); 420 421 assert_eq!( 422 index.resolve_repo(&own("nel"), &rkey("anemone")), 423 Resolved::Ready(Some(repo.clone())), 424 "prior rkey keeps resolving as alias after rename is delta-applied" 425 ); 426 assert_eq!( 427 index.resolve_repo(&own("nel"), &rkey("barnacle")), 428 Resolved::Ready(Some(repo.clone())) 429 ); 430 assert_eq!( 431 index.rkey_of(&repo), 432 Resolved::Ready(Some(rkey("barnacle"))), 433 "new rkey is canonical" 434 ); 435 assert_eq!( 436 index.is_collaborator(&repo, &acc("lyna")), 437 Resolved::Ready(true), 438 "rename never evacuates repo, so its collaborators survive" 439 ); 440 441 store 442 .update( 443 &meta_home(), 444 registry, 445 &RegistryChange::Deregister(RepoRef { 446 owner: own("nel"), 447 rkey: rkey("anemone"), 448 }), 449 &world.signer, 450 at(3), 451 ) 452 .unwrap(); 453 index.refresh_registry().unwrap(); 454 assert_eq!( 455 index.resolve_repo(&own("nel"), &rkey("barnacle")), 456 Resolved::Ready(None), 457 "deregistering through retained alias removes repo and every alias" 458 ); 459 assert_eq!( 460 index.is_collaborator(&repo, &acc("lyna")), 461 Resolved::Warming, 462 "deregistered repo's collaborators are evacuated" 463 ); 464} 465 466#[test] 467fn a_repo_moved_within_one_delta_is_not_evacuated() { 468 let world = World::new(); 469 let repo = repo_did("squid"); 470 471 let meta = Repo::open(&world.meta_path).unwrap(); 472 let store = CobStore::new(&meta); 473 let registry = store 474 .create( 475 &meta_home(), 476 &RegistryChange::Register(registration("nel", "anemone", &repo, 1)), 477 &world.signer, 478 at(1), 479 ) 480 .unwrap() 481 .object; 482 483 let git = world.layout.create(&repo).unwrap(); 484 CobStore::new(&git) 485 .create( 486 &CobHome::from(&repo), 487 &CollaboratorsChange::Add(grant("lyna", "nel", 1)), 488 &world.signer, 489 at(1), 490 ) 491 .unwrap(); 492 493 let index = world.index(); 494 index.rebuild().unwrap(); 495 index.warm_collaborators(); 496 assert_eq!( 497 index.is_collaborator(&repo, &acc("lyna")), 498 Resolved::Ready(true) 499 ); 500 501 store 502 .update( 503 &meta_home(), 504 registry, 505 &RegistryChange::Deregister(RepoRef { 506 owner: own("nel"), 507 rkey: rkey("anemone"), 508 }), 509 &world.signer, 510 at(2), 511 ) 512 .unwrap(); 513 store 514 .update( 515 &meta_home(), 516 registry, 517 &RegistryChange::Register(registration("nel", "barnacle", &repo, 3)), 518 &world.signer, 519 at(3), 520 ) 521 .unwrap(); 522 index.refresh_registry().unwrap(); 523 524 assert_eq!( 525 index.resolve_repo(&own("nel"), &rkey("anemone")), 526 Resolved::Ready(None) 527 ); 528 assert_eq!( 529 index.resolve_repo(&own("nel"), &rkey("barnacle")), 530 Resolved::Ready(Some(repo.clone())) 531 ); 532 assert_eq!( 533 index.is_collaborator(&repo, &acc("lyna")), 534 Resolved::Ready(true), 535 "deregister and re-register within single delta leaves repo hosted, so its collaborators survive" 536 ); 537} 538 539#[test] 540fn key_cache_evicts_least_recently_used() { 541 const CAP: u32 = 16_384; 542 let world = World::new(); 543 let index = world.index(); 544 let key = |i: u32| OfferedKey::from_bytes(i.to_le_bytes().to_vec()); 545 546 (0..CAP).for_each(|i| index.cache_key(key(i), &acc("nel"))); 547 assert_eq!( 548 index.owner_of_key(&key(0)), 549 Resolved::Ready(Some(acc("nel"))) 550 ); 551 index.cache_key(key(CAP), &acc("nel")); 552 553 assert_eq!( 554 index.owner_of_key(&key(1)), 555 Resolved::Ready(None), 556 "least-recently-used key is evicted" 557 ); 558 assert_eq!( 559 index.owner_of_key(&key(0)), 560 Resolved::Ready(Some(acc("nel"))), 561 "recently-used key survives despite being inserted first" 562 ); 563 assert_eq!( 564 index.owner_of_key(&key(CAP)), 565 Resolved::Ready(Some(acc("nel"))) 566 ); 567}