Now let's take a silly one
0

Configure Feed

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

at main 33 kB View raw
1mod coverage; 2mod error; 3mod intern; 4mod projections; 5 6pub use coverage::{Coverage, Resolved}; 7pub use error::IndexError; 8pub use knot_types::OfferedKey; 9 10use std::path::PathBuf; 11use std::sync::atomic::{AtomicU64, Ordering}; 12 13use knot_cob::{ChangePayload, CobStore}; 14use knot_cobs::{ 15 BlocklistChange, BlocklistCob, CollaboratorsChange, CollaboratorsCob, Grant, MembersChange, 16 MembersCob, RegistryChange, RepoRegistryCob, 17}; 18use knot_git::{Layout, Repo}; 19use knot_types::{AccountDid, OwnerDid, RepoDid, RepoRkey}; 20 21use intern::Interner; 22use projections::{CollaboratorsProjection, GrantSetProjection, KeyProjection, RegistryProjection}; 23 24#[derive(Debug, Clone, Copy, PartialEq, Eq)] 25pub struct IndexCoverage { 26 pub members: Coverage, 27 pub blocklist: Coverage, 28 pub collaborators: Coverage, 29 pub registry: Coverage, 30 pub keys: Coverage, 31} 32 33pub struct Index { 34 meta_path: PathBuf, 35 layout: Layout, 36 interner: Interner, 37 members: GrantSetProjection<MembersCob>, 38 blocklist: GrantSetProjection<BlocklistCob>, 39 collaborators: CollaboratorsProjection, 40 registry: RegistryProjection, 41 keys: KeyProjection, 42 generation: AtomicU64, 43} 44 45impl Index { 46 pub fn new(meta_path: impl Into<PathBuf>, layout: Layout) -> Self { 47 Self { 48 meta_path: meta_path.into(), 49 layout, 50 interner: Interner::new(), 51 members: GrantSetProjection::new(), 52 blocklist: GrantSetProjection::new(), 53 collaborators: CollaboratorsProjection::new(), 54 registry: RegistryProjection::new(), 55 keys: KeyProjection::new(), 56 generation: AtomicU64::new(0), 57 } 58 } 59 60 pub fn generation(&self) -> u64 { 61 self.generation.load(Ordering::Acquire) 62 } 63 64 fn bump_generation(&self) { 65 self.generation.fetch_add(1, Ordering::Release); 66 } 67 68 pub fn rebuild(&self) -> Result<(), IndexError> { 69 self.refresh_members()?; 70 self.refresh_blocklist()?; 71 self.refresh_registry()?; 72 Ok(()) 73 } 74 75 pub fn warm_collaborators(&self) { 76 self.hosted_repos().iter().for_each(|repo| { 77 let _ = self.ensure_collaborators(repo); 78 }); 79 } 80 81 pub fn refresh_members(&self) -> Result<(), IndexError> { 82 let meta = Repo::open(&self.meta_path)?; 83 let store = CobStore::new(&meta); 84 match store.list::<MembersCob>()?.as_slice() { 85 [] => self.members.reset(), 86 [object] => self.members.refresh(&self.interner, &store, *object)?, 87 many => { 88 return Err(IndexError::Ambiguous { 89 type_name: MembersChange::type_name(), 90 count: many.len(), 91 }); 92 } 93 } 94 self.bump_generation(); 95 Ok(()) 96 } 97 98 pub fn refresh_blocklist(&self) -> Result<(), IndexError> { 99 let meta = Repo::open(&self.meta_path)?; 100 let store = CobStore::new(&meta); 101 match store.list::<BlocklistCob>()?.as_slice() { 102 [] => self.blocklist.reset(), 103 [object] => self.blocklist.refresh(&self.interner, &store, *object)?, 104 many => { 105 return Err(IndexError::Ambiguous { 106 type_name: BlocklistChange::type_name(), 107 count: many.len(), 108 }); 109 } 110 } 111 self.bump_generation(); 112 Ok(()) 113 } 114 115 pub fn refresh_registry(&self) -> Result<(), IndexError> { 116 let meta = Repo::open(&self.meta_path)?; 117 let store = CobStore::new(&meta); 118 let evacuated = match store.list::<RepoRegistryCob>()?.as_slice() { 119 [] => self.registry.reset(&self.interner), 120 [object] => self.registry.refresh(&self.interner, &store, *object)?, 121 many => { 122 return Err(IndexError::Ambiguous { 123 type_name: RegistryChange::type_name(), 124 count: many.len(), 125 }); 126 } 127 }; 128 evacuated.iter().for_each(|repo| { 129 if let Some(key) = self.interner.repo(repo) { 130 self.collaborators.drop_repo(key); 131 } 132 }); 133 self.bump_generation(); 134 Ok(()) 135 } 136 137 pub fn ensure_collaborators(&self, repo: &RepoDid) -> Result<(), IndexError> { 138 if self.collaborators.is_folded(&self.interner, repo) { 139 return Ok(()); 140 } 141 self.refresh_collaborators(repo) 142 } 143 144 pub fn refresh_collaborators(&self, repo: &RepoDid) -> Result<(), IndexError> { 145 let git = self.layout.open(repo)?; 146 let store = CobStore::new(&git); 147 let repo_key = self.interner.intern_repo(repo); 148 match store.list::<CollaboratorsCob>()?.as_slice() { 149 [] => self.collaborators.mark_repo_empty(repo_key), 150 [object] => { 151 self.collaborators 152 .refresh_repo(&self.interner, &store, repo_key, *object)? 153 } 154 many => { 155 return Err(IndexError::Ambiguous { 156 type_name: CollaboratorsChange::type_name(), 157 count: many.len(), 158 }); 159 } 160 } 161 self.bump_generation(); 162 Ok(()) 163 } 164 165 pub fn is_member(&self, did: &AccountDid) -> Resolved<bool> { 166 self.members.contains(&self.interner, did) 167 } 168 169 pub fn member_entries(&self) -> Resolved<Vec<Grant>> { 170 self.members.entries(&self.interner) 171 } 172 173 pub fn is_blocked(&self, did: &AccountDid) -> Resolved<bool> { 174 self.blocklist.contains(&self.interner, did) 175 } 176 177 pub fn blocked_entries(&self) -> Resolved<Vec<Grant>> { 178 self.blocklist.entries(&self.interner) 179 } 180 181 pub fn is_collaborator(&self, repo: &RepoDid, did: &AccountDid) -> Resolved<bool> { 182 self.collaborators.contains(&self.interner, repo, did) 183 } 184 185 pub fn collaborator_entries(&self, repo: &RepoDid) -> Resolved<Vec<Grant>> { 186 self.collaborators.entries(&self.interner, repo) 187 } 188 189 pub fn collaborators_of(&self, repo: &RepoDid) -> Resolved<Vec<AccountDid>> { 190 self.collaborator_entries(repo) 191 .map(|entries| entries.into_iter().map(|grant| grant.subject).collect()) 192 } 193 194 pub fn resolve_repo(&self, owner: &OwnerDid, rkey: &RepoRkey) -> Resolved<Option<RepoDid>> { 195 self.registry.resolve(&self.interner, owner, rkey) 196 } 197 198 pub fn owner_of(&self, repo: &RepoDid) -> Resolved<Option<OwnerDid>> { 199 self.registry.owner_of(&self.interner, repo) 200 } 201 202 pub fn rkey_of(&self, repo: &RepoDid) -> Resolved<Option<RepoRkey>> { 203 self.registry.rkey_of(&self.interner, repo) 204 } 205 206 pub fn hosted_repos(&self) -> Vec<RepoDid> { 207 self.registry.hosted_repos(&self.interner) 208 } 209 210 pub fn owner_of_key(&self, key: &OfferedKey) -> Resolved<Option<AccountDid>> { 211 self.keys.owner(&self.interner, key) 212 } 213 214 pub fn cache_key(&self, key: OfferedKey, did: &AccountDid) { 215 self.keys.cache(&self.interner, key, did); 216 } 217 218 pub fn coverage(&self) -> IndexCoverage { 219 IndexCoverage { 220 members: self.members.coverage(), 221 blocklist: self.blocklist.coverage(), 222 collaborators: self.collaborators.coverage(), 223 registry: self.registry.coverage(), 224 keys: self.keys.coverage(), 225 } 226 } 227} 228 229#[cfg(test)] 230mod tests { 231 use super::*; 232 use knot_cob::{CobHome, CobId, CobStore}; 233 use knot_cobs::{Grant, Registration, Removal, Rename, RepoRef}; 234 use knot_git::Repo; 235 use knot_runtime::{K256Signer, SeededEntropy}; 236 use knot_types::{KnotId, RepoName, UnixSeconds}; 237 use tempfile::TempDir; 238 239 fn acc(suffix: &str) -> AccountDid { 240 AccountDid::new(format!("did:plc:{suffix}")).unwrap() 241 } 242 243 fn meta_home() -> CobHome { 244 CobHome::from(&KnotId::new("did:web:knot.nel.pet").unwrap()) 245 } 246 247 fn own(suffix: &str) -> OwnerDid { 248 OwnerDid::new(format!("did:plc:{suffix}")).unwrap() 249 } 250 251 fn repo_did(suffix: &str) -> RepoDid { 252 RepoDid::new(format!("did:plc:{suffix}")).unwrap() 253 } 254 255 fn rkey(value: &str) -> RepoRkey { 256 RepoRkey::new(value).unwrap() 257 } 258 259 fn registration(owner_id: &str, key: &str, repo: &RepoDid, seconds: i64) -> Registration { 260 Registration { 261 owner: own(owner_id), 262 rkey: rkey(key), 263 name: RepoName::new(key).unwrap(), 264 repo: repo.clone(), 265 created_at: at(seconds), 266 } 267 } 268 269 fn at(seconds: i64) -> UnixSeconds { 270 UnixSeconds::new(seconds) 271 } 272 273 fn grant(subject: &str, added_by: &str, seconds: i64) -> Grant { 274 Grant { 275 subject: acc(subject), 276 added_by: acc(added_by), 277 created_at: at(seconds), 278 } 279 } 280 281 struct World { 282 _dir: TempDir, 283 meta_path: PathBuf, 284 layout: Layout, 285 signer: K256Signer, 286 } 287 288 impl World { 289 fn new() -> Self { 290 let dir = tempfile::tempdir().unwrap(); 291 let meta_path = dir.path().join("meta"); 292 Repo::create(&meta_path).unwrap(); 293 let layout = Layout::new(dir.path().join("repos")); 294 let signer = K256Signer::generate(&SeededEntropy::new(1)); 295 Self { 296 _dir: dir, 297 meta_path, 298 layout, 299 signer, 300 } 301 } 302 303 fn index(&self) -> Index { 304 Index::new(&self.meta_path, self.layout.clone()) 305 } 306 307 fn seed_members(&self) -> CobId { 308 let meta = Repo::open(&self.meta_path).unwrap(); 309 let store = CobStore::new(&meta); 310 let created = store 311 .create( 312 &meta_home(), 313 &MembersChange::Add(grant("nel", "nel", 1)), 314 &self.signer, 315 at(1), 316 ) 317 .unwrap(); 318 store 319 .update( 320 &meta_home(), 321 created.object, 322 &MembersChange::Add(grant("olaren", "nel", 2)), 323 &self.signer, 324 at(2), 325 ) 326 .unwrap(); 327 created.object 328 } 329 330 fn add_member(&self, object: CobId, subject: &str, seconds: i64) { 331 let meta = Repo::open(&self.meta_path).unwrap(); 332 let store = CobStore::new(&meta); 333 store 334 .update( 335 &meta_home(), 336 object, 337 &MembersChange::Add(grant(subject, "nel", seconds)), 338 &self.signer, 339 at(seconds), 340 ) 341 .unwrap(); 342 } 343 344 fn seed_registry(&self, repo: &RepoDid) -> CobId { 345 let meta = Repo::open(&self.meta_path).unwrap(); 346 let store = CobStore::new(&meta); 347 store 348 .create( 349 &meta_home(), 350 &RegistryChange::Register(registration("nel", "anemone", repo, 1)), 351 &self.signer, 352 at(1), 353 ) 354 .unwrap() 355 .object 356 } 357 358 fn register_extra(&self, repo: &RepoDid, key: &str, registry: CobId) { 359 let meta = Repo::open(&self.meta_path).unwrap(); 360 let store = CobStore::new(&meta); 361 store 362 .update( 363 &meta_home(), 364 registry, 365 &RegistryChange::Register(registration("nel", key, repo, 2)), 366 &self.signer, 367 at(2), 368 ) 369 .unwrap(); 370 } 371 372 fn seed_collaborator(&self, repo: &RepoDid, subject: &str) -> CobId { 373 let git = self.layout.create(repo).unwrap(); 374 let store = CobStore::new(&git); 375 store 376 .create( 377 &CobHome::from(repo), 378 &CollaboratorsChange::Add(grant(subject, "nel", 1)), 379 &self.signer, 380 at(1), 381 ) 382 .unwrap() 383 .object 384 } 385 386 fn remove_collaborator(&self, repo: &RepoDid, object: CobId, subject: &str, seconds: i64) { 387 let git = self.layout.open(repo).unwrap(); 388 let store = CobStore::new(&git); 389 store 390 .update( 391 &CobHome::from(repo), 392 object, 393 &CollaboratorsChange::Remove(Removal { 394 subject: acc(subject), 395 }), 396 &self.signer, 397 at(seconds), 398 ) 399 .unwrap(); 400 } 401 } 402 403 #[test] 404 fn rebuild_folds_members_and_registry_and_collaborators_fold_on_access() { 405 let world = World::new(); 406 let repo = repo_did("squid"); 407 world.seed_members(); 408 world.seed_registry(&repo); 409 world.seed_collaborator(&repo, "lyna"); 410 411 let index = world.index(); 412 index.rebuild().unwrap(); 413 414 assert_eq!(index.is_member(&acc("nel")), Resolved::Ready(true)); 415 assert_eq!(index.is_member(&acc("olaren")), Resolved::Ready(true)); 416 assert_eq!(index.is_member(&acc("teq")), Resolved::Ready(false)); 417 assert_eq!( 418 index.resolve_repo(&own("nel"), &rkey("anemone")), 419 Resolved::Ready(Some(repo.clone())) 420 ); 421 assert_eq!( 422 index.resolve_repo(&own("nel"), &rkey("nautilus")), 423 Resolved::Ready(None) 424 ); 425 assert_eq!( 426 index.is_collaborator(&repo, &acc("lyna")), 427 Resolved::Warming, 428 "rebuild does not fold collaborators, so roster reads warming until first access" 429 ); 430 431 index.ensure_collaborators(&repo).unwrap(); 432 assert_eq!( 433 index.is_collaborator(&repo, &acc("lyna")), 434 Resolved::Ready(true) 435 ); 436 assert_eq!( 437 index.is_collaborator(&repo, &acc("bailey")), 438 Resolved::Ready(false) 439 ); 440 assert_eq!( 441 index.coverage(), 442 IndexCoverage { 443 members: Coverage::Ready, 444 blocklist: Coverage::Ready, 445 collaborators: Coverage::Ready, 446 registry: Coverage::Ready, 447 keys: Coverage::Ready, 448 } 449 ); 450 } 451 452 #[test] 453 fn a_warming_projection_fails_closed() { 454 let world = World::new(); 455 world.seed_members(); 456 let index = world.index(); 457 458 assert_eq!(index.is_member(&acc("nel")), Resolved::Warming); 459 assert_eq!( 460 index.is_collaborator(&repo_did("squid"), &acc("lyna")), 461 Resolved::Warming 462 ); 463 assert_eq!( 464 index.resolve_repo(&own("nel"), &rkey("anemone")), 465 Resolved::Warming 466 ); 467 assert_eq!(index.coverage().members, Coverage::Warming); 468 469 assert_eq!( 470 index.owner_of_key(&OfferedKey::from_bytes(vec![1, 2, 3])), 471 Resolved::Ready(None), 472 "key cache is operational from boot, never warming" 473 ); 474 } 475 476 #[test] 477 fn a_member_added_after_boot_is_delta_applied() { 478 let world = World::new(); 479 let members = world.seed_members(); 480 let index = world.index(); 481 index.rebuild().unwrap(); 482 assert_eq!(index.is_member(&acc("teq")), Resolved::Ready(false)); 483 484 world.add_member(members, "teq", 3); 485 index.refresh_members().unwrap(); 486 assert_eq!(index.is_member(&acc("teq")), Resolved::Ready(true)); 487 488 index.refresh_members().unwrap(); 489 assert_eq!(index.is_member(&acc("teq")), Resolved::Ready(true)); 490 assert_eq!(index.is_member(&acc("nel")), Resolved::Ready(true)); 491 } 492 493 #[test] 494 fn owner_of_resolves_a_repo_to_its_registered_owner() { 495 let world = World::new(); 496 let repo = repo_did("squid"); 497 world.seed_registry(&repo); 498 499 let index = world.index(); 500 assert_eq!( 501 index.owner_of(&repo), 502 Resolved::Warming, 503 "repo lookup before rebuild fails closed" 504 ); 505 506 index.refresh_registry().unwrap(); 507 assert_eq!( 508 index.owner_of(&repo), 509 Resolved::Ready(Some(own("nel"))), 510 "registered repo resolves to owner it was registered under" 511 ); 512 assert_eq!( 513 index.owner_of(&repo_did("conch")), 514 Resolved::Ready(None), 515 "unregistered repo has no owner" 516 ); 517 } 518 519 #[test] 520 fn a_removed_collaborator_is_gone() { 521 let world = World::new(); 522 let repo = repo_did("clam"); 523 world.seed_registry(&repo); 524 let object = world.seed_collaborator(&repo, "lyna"); 525 world.remove_collaborator(&repo, object, "lyna", 2); 526 527 let index = world.index(); 528 index.rebuild().unwrap(); 529 index.ensure_collaborators(&repo).unwrap(); 530 assert_eq!( 531 index.is_collaborator(&repo, &acc("lyna")), 532 Resolved::Ready(false) 533 ); 534 } 535 536 #[test] 537 fn an_unaccessed_repo_is_warming_even_when_a_folded_one_is_ready() { 538 let world = World::new(); 539 let repo = repo_did("whelk"); 540 world.seed_registry(&repo); 541 world.seed_collaborator(&repo, "lyna"); 542 543 let index = world.index(); 544 index.rebuild().unwrap(); 545 assert_eq!( 546 index.collaborators.coverage(), 547 Coverage::Ready, 548 "collaborators projection is operational from boot" 549 ); 550 index.ensure_collaborators(&repo).unwrap(); 551 assert_eq!( 552 index.is_collaborator(&repo, &acc("lyna")), 553 Resolved::Ready(true) 554 ); 555 assert_eq!( 556 index.is_collaborator(&repo_did("conch"), &acc("lyna")), 557 Resolved::Warming, 558 "repo the index never folded cannot answer, so it fails closed" 559 ); 560 } 561 562 #[test] 563 fn rebuild_is_byte_identical_across_restarts() { 564 let world = World::new(); 565 let repo = repo_did("squid"); 566 world.seed_members(); 567 world.seed_registry(&repo); 568 world.seed_collaborator(&repo, "lyna"); 569 570 let first = world.index(); 571 first.rebuild().unwrap(); 572 first.ensure_collaborators(&repo).unwrap(); 573 let second = world.index(); 574 second.rebuild().unwrap(); 575 second.ensure_collaborators(&repo).unwrap(); 576 577 ["nel", "olaren", "lyna", "stranger"] 578 .into_iter() 579 .for_each(|subject| { 580 assert_eq!( 581 first.is_member(&acc(subject)), 582 second.is_member(&acc(subject)) 583 ); 584 assert_eq!( 585 first.is_collaborator(&repo, &acc(subject)), 586 second.is_collaborator(&repo, &acc(subject)) 587 ); 588 }); 589 assert_eq!( 590 first.resolve_repo(&own("nel"), &rkey("anemone")), 591 second.resolve_repo(&own("nel"), &rkey("anemone")) 592 ); 593 } 594 595 #[test] 596 fn the_key_cache_round_trips() { 597 let world = World::new(); 598 let index = world.index(); 599 let key = OfferedKey::from_bytes(vec![9, 9, 9]); 600 601 assert_eq!(index.owner_of_key(&key), Resolved::Ready(None)); 602 index.cache_key(key.clone(), &acc("nel")); 603 assert_eq!(index.owner_of_key(&key), Resolved::Ready(Some(acc("nel")))); 604 } 605 606 #[test] 607 fn member_entries_carry_provenance_and_fail_closed_while_warming() { 608 let world = World::new(); 609 world.seed_members(); 610 let index = world.index(); 611 assert_eq!(index.member_entries(), Resolved::Warming); 612 613 index.rebuild().unwrap(); 614 assert_eq!( 615 index.member_entries(), 616 Resolved::Ready(vec![grant("nel", "nel", 1), grant("olaren", "nel", 2)]) 617 ); 618 } 619 620 #[test] 621 fn a_re_added_member_keeps_the_first_provenance() { 622 let world = World::new(); 623 let members = world.seed_members(); 624 let index = world.index(); 625 index.rebuild().unwrap(); 626 627 let meta = Repo::open(&world.meta_path).unwrap(); 628 let store = CobStore::new(&meta); 629 store 630 .update( 631 &meta_home(), 632 members, 633 &MembersChange::Add(grant("olaren", "teq", 9)), 634 &world.signer, 635 at(9), 636 ) 637 .unwrap(); 638 index.refresh_members().unwrap(); 639 640 assert_eq!( 641 index.member_entries(), 642 Resolved::Ready(vec![grant("nel", "nel", 1), grant("olaren", "nel", 2)]), 643 "duplicate add never rewrites original provenance, matching canonical roster" 644 ); 645 } 646 647 #[test] 648 fn collaborator_entries_match_the_canonical_roster() { 649 let world = World::new(); 650 let repo = repo_did("squid"); 651 world.seed_registry(&repo); 652 let object = world.seed_collaborator(&repo, "lyna"); 653 let git = world.layout.open(&repo).unwrap(); 654 let store = CobStore::new(&git); 655 store 656 .update( 657 &CobHome::from(&repo), 658 object, 659 &CollaboratorsChange::Add(grant("bailey", "olaren", 2)), 660 &world.signer, 661 at(2), 662 ) 663 .unwrap(); 664 world.remove_collaborator(&repo, object, "lyna", 3); 665 store 666 .update( 667 &CobHome::from(&repo), 668 object, 669 &CollaboratorsChange::Add(grant("lyna", "teq", 5)), 670 &world.signer, 671 at(5), 672 ) 673 .unwrap(); 674 675 let index = world.index(); 676 index.rebuild().unwrap(); 677 index.ensure_collaborators(&repo).unwrap(); 678 679 let canonical = store.get::<CollaboratorsCob>(object).unwrap(); 680 let expected: Vec<Grant> = canonical 681 .state() 682 .entries() 683 .map(|(subject, entry)| Grant { 684 subject: subject.clone(), 685 added_by: entry.added_by.clone(), 686 created_at: entry.created_at, 687 }) 688 .collect(); 689 assert_eq!( 690 index.collaborator_entries(&repo), 691 Resolved::Ready(expected), 692 "projected entries disagree with canonical Evaluate fold" 693 ); 694 } 695 696 #[test] 697 fn collaborator_entries_fails_closed_while_unfolded() { 698 let world = World::new(); 699 world.seed_collaborator(&repo_did("squid"), "lyna"); 700 let index = world.index(); 701 assert_eq!( 702 index.collaborator_entries(&repo_did("squid")), 703 Resolved::Warming, 704 "roster that has never been folded fails closed" 705 ); 706 } 707 708 #[test] 709 fn the_forward_path_serves_a_folded_repo_while_an_unaccessed_one_stays_warming() { 710 let world = World::new(); 711 let present = repo_did("squid"); 712 let absent = repo_did("kelp"); 713 let registry = world.seed_registry(&present); 714 world.register_extra(&absent, "barnacle", registry); 715 world.seed_collaborator(&present, "lyna"); 716 717 let index = world.index(); 718 index.rebuild().unwrap(); 719 index.ensure_collaborators(&present).unwrap(); 720 721 assert_eq!( 722 index 723 .collaborator_entries(&present) 724 .map(|grants| grants.len()), 725 Resolved::Ready(1), 726 "folded repo serves its roster" 727 ); 728 assert_eq!( 729 index.collaborator_entries(&absent), 730 Resolved::Warming, 731 "registered repo that was never accessed stays fail-closed until folded" 732 ); 733 } 734 735 #[test] 736 fn two_objects_of_one_type_are_ambiguous() { 737 let world = World::new(); 738 let meta = Repo::open(&world.meta_path).unwrap(); 739 let store = CobStore::new(&meta); 740 store 741 .create( 742 &meta_home(), 743 &MembersChange::Add(grant("nel", "nel", 1)), 744 &world.signer, 745 at(1), 746 ) 747 .unwrap(); 748 store 749 .create( 750 &meta_home(), 751 &MembersChange::Add(grant("olaren", "olaren", 2)), 752 &world.signer, 753 at(2), 754 ) 755 .unwrap(); 756 757 let index = world.index(); 758 assert!(matches!( 759 index.refresh_members(), 760 Err(IndexError::Ambiguous { count: 2, .. }) 761 )); 762 } 763 764 #[test] 765 fn a_repo_missing_on_disk_does_not_fail_the_boot_and_isolates_its_fold() { 766 let world = World::new(); 767 let present = repo_did("squid"); 768 let absent = repo_did("kelp"); 769 world.seed_members(); 770 let registry = world.seed_registry(&present); 771 world.register_extra(&absent, "barnacle", registry); 772 world.seed_collaborator(&present, "lyna"); 773 774 let index = world.index(); 775 index 776 .rebuild() 777 .expect("boot folds members and registry only, so missing repo dir never fails it"); 778 779 index.ensure_collaborators(&present).unwrap(); 780 assert_eq!( 781 index.is_collaborator(&present, &acc("lyna")), 782 Resolved::Ready(true) 783 ); 784 assert!( 785 index.ensure_collaborators(&absent).is_err(), 786 "folding repo with no dir on disk fails for that repo alone" 787 ); 788 assert_eq!( 789 index.is_collaborator(&absent, &acc("lyna")), 790 Resolved::Warming, 791 "repo the index could not fold stays fail-closed" 792 ); 793 } 794 795 #[test] 796 fn concurrent_refreshes_of_distinct_repos_all_land() { 797 use std::sync::Arc; 798 799 let world = World::new(); 800 let repos = ["squid", "clam", "whelk", "conch"]; 801 repos.iter().for_each(|repo| { 802 world.seed_collaborator(&repo_did(repo), "lyna"); 803 }); 804 805 let index = Arc::new(world.index()); 806 std::thread::scope(|scope| { 807 repos.iter().for_each(|repo| { 808 let index = Arc::clone(&index); 809 let repo = repo_did(repo); 810 scope.spawn(move || index.refresh_collaborators(&repo).unwrap()); 811 }); 812 }); 813 814 repos.iter().for_each(|repo| { 815 assert_eq!( 816 index.is_collaborator(&repo_did(repo), &acc("lyna")), 817 Resolved::Ready(true) 818 ); 819 }); 820 } 821 822 #[test] 823 fn collaborators_fold_matches_canonical_evaluate() { 824 let world = World::new(); 825 let repo = repo_did("squid"); 826 let object = world.seed_collaborator(&repo, "lyna"); 827 let git = world.layout.open(&repo).unwrap(); 828 let store = CobStore::new(&git); 829 store 830 .update( 831 &CobHome::from(&repo), 832 object, 833 &CollaboratorsChange::Add(grant("bailey", "nel", 2)), 834 &world.signer, 835 at(2), 836 ) 837 .unwrap(); 838 world.remove_collaborator(&repo, object, "lyna", 3); 839 840 let index = world.index(); 841 index.refresh_collaborators(&repo).unwrap(); 842 843 let canonical = store.get::<CollaboratorsCob>(object).unwrap(); 844 let roster = canonical.state(); 845 ["lyna", "bailey", "nel"].into_iter().for_each(|who| { 846 assert_eq!( 847 index.is_collaborator(&repo, &acc(who)), 848 Resolved::Ready(roster.contains(&acc(who))), 849 "collaborator fold disagrees with canonical Evaluate for {who}" 850 ); 851 }); 852 } 853 854 #[test] 855 fn registry_fold_matches_canonical_evaluate() { 856 let world = World::new(); 857 let registry = world.seed_registry(&repo_did("squid")); 858 world.register_extra(&repo_did("clam"), "barnacle", registry); 859 860 let meta = Repo::open(&world.meta_path).unwrap(); 861 let store = CobStore::new(&meta); 862 store 863 .update( 864 &meta_home(), 865 registry, 866 &RegistryChange::Rename(Rename { 867 owner: own("nel"), 868 rkey: rkey("nautilus"), 869 name: RepoName::new("nautilus").unwrap(), 870 repo: repo_did("squid"), 871 }), 872 &world.signer, 873 at(3), 874 ) 875 .unwrap(); 876 store 877 .update( 878 &meta_home(), 879 registry, 880 &RegistryChange::Register(Registration { 881 owner: own("nel"), 882 rkey: rkey("anemone"), 883 name: RepoName::new("anemone").unwrap(), 884 repo: repo_did("whelk"), 885 created_at: at(4), 886 }), 887 &world.signer, 888 at(4), 889 ) 890 .unwrap(); 891 store 892 .update( 893 &meta_home(), 894 registry, 895 &RegistryChange::Deregister(RepoRef { 896 owner: own("nel"), 897 rkey: rkey("barnacle"), 898 }), 899 &world.signer, 900 at(5), 901 ) 902 .unwrap(); 903 904 let index = world.index(); 905 index.refresh_registry().unwrap(); 906 907 let canonical = store.get::<RepoRegistryCob>(registry).unwrap(); 908 let reg = canonical.state(); 909 [ 910 ("nel", "anemone"), 911 ("nel", "barnacle"), 912 ("nel", "nautilus"), 913 ("nel", "kelp"), 914 ] 915 .into_iter() 916 .for_each(|(owner, key)| { 917 assert_eq!( 918 index.resolve_repo(&own(owner), &rkey(key)), 919 Resolved::Ready(reg.resolve(&own(owner), &rkey(key)).cloned()), 920 "registry fold disagrees with canonical Evaluate for {owner}/{key}" 921 ); 922 }); 923 [repo_did("squid"), repo_did("clam"), repo_did("whelk")] 924 .iter() 925 .for_each(|repo| { 926 assert_eq!( 927 index.owner_of(repo), 928 Resolved::Ready(reg.owner_of(repo)), 929 "owner fold disagrees with canonical Evaluate for {repo}" 930 ); 931 assert_eq!( 932 index.rkey_of(repo), 933 Resolved::Ready(reg.record_of(repo).map(|record| record.rkey.clone())), 934 "canonical rkey fold disagrees with canonical Evaluate for {repo}" 935 ); 936 }); 937 } 938 939 #[test] 940 fn rebuild_aborts_on_a_broken_meta_cob() { 941 let world = World::new(); 942 let meta = Repo::open(&world.meta_path).unwrap(); 943 let store = CobStore::new(&meta); 944 store 945 .create( 946 &meta_home(), 947 &MembersChange::Add(grant("nel", "nel", 1)), 948 &world.signer, 949 at(1), 950 ) 951 .unwrap(); 952 store 953 .create( 954 &meta_home(), 955 &MembersChange::Add(grant("olaren", "olaren", 2)), 956 &world.signer, 957 at(2), 958 ) 959 .unwrap(); 960 961 let index = world.index(); 962 assert!( 963 matches!(index.rebuild(), Err(IndexError::Ambiguous { .. })), 964 "broken meta COB fails whole boot instead of reporting partial one" 965 ); 966 } 967 968 #[test] 969 fn a_refresh_is_eventually_consistent_not_an_atomic_snapshot() { 970 use std::sync::Arc; 971 use std::sync::atomic::{AtomicBool, Ordering}; 972 973 let world = World::new(); 974 let members = world.seed_members(); 975 let index = Arc::new(world.index()); 976 index.rebuild().unwrap(); 977 978 (0..32).for_each(|i| world.add_member(members, &format!("m{i}"), 10 + i as i64)); 979 980 let done = Arc::new(AtomicBool::new(false)); 981 std::thread::scope(|scope| { 982 let writer = Arc::clone(&index); 983 let writer_done = Arc::clone(&done); 984 scope.spawn(move || { 985 writer.refresh_members().unwrap(); 986 writer_done.store(true, Ordering::Release); 987 }); 988 989 let reader = Arc::clone(&index); 990 let reader_done = Arc::clone(&done); 991 scope.spawn(move || { 992 while !reader_done.load(Ordering::Acquire) { 993 assert_eq!( 994 reader.is_member(&acc("nel")), 995 Resolved::Ready(true), 996 "stable member stays visible and read never blocks on writer" 997 ); 998 assert!( 999 !reader.is_member(&acc("m0")).is_warming(), 1000 "already-ready projection serves reads mid-refresh, it never re-warms" 1001 ); 1002 } 1003 }); 1004 }); 1005 1006 (0..32).for_each(|i| { 1007 assert_eq!( 1008 index.is_member(&acc(&format!("m{i}"))), 1009 Resolved::Ready(true), 1010 "once writer returns, whole delta has converged" 1011 ); 1012 }); 1013 } 1014}