This repository has no description
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}