This repository has no description
18 kB
657 lines
1use std::path::{Path, PathBuf};
2
3use knot_cob::{CobError, CobHome, CobId, CobStore};
4use knot_cobs::{
5 CollaboratorsChange, Grant, MembersChange, MembersCob, Registration, RegistryChange,
6 RepoRegistryCob, add_member, register_repo,
7};
8use knot_git::{
9 EntryKind, Identity, Layout, NewCommit, RefUpdate, Repo, StagedAction, StagedChange,
10};
11use knot_index::Index;
12use knot_runtime::{K256Signer, SeededEntropy};
13use knot_types::{
14 AccountDid, AuthorName, Email, KnotId, Oid, OwnerDid, RefName, RepoDid, RepoName, RepoRkey,
15 UnixSeconds,
16};
17use tempfile::TempDir;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub struct CommitCount(u32);
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub struct PathCount(u32);
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub struct ChurnCount(u32);
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub struct ChangeCount(u32);
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub struct RepoCount(u64);
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub struct RefCount(u32);
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub struct RosterCount(u32);
39
40impl CommitCount {
41 pub fn new(value: u32) -> Self {
42 Self(value.max(1))
43 }
44 fn get(self) -> u32 {
45 self.0
46 }
47}
48
49impl PathCount {
50 pub fn new(value: u32) -> Self {
51 Self(value.max(1))
52 }
53 fn get(self) -> u32 {
54 self.0
55 }
56}
57
58impl ChurnCount {
59 pub fn new(value: u32) -> Self {
60 Self(value.max(1))
61 }
62 fn get(self) -> u32 {
63 self.0
64 }
65}
66
67impl ChangeCount {
68 pub fn new(value: u32) -> Self {
69 Self(value.max(1))
70 }
71 fn get(self) -> u32 {
72 self.0
73 }
74}
75
76impl RepoCount {
77 pub fn new(value: u64) -> Self {
78 Self(value.max(1))
79 }
80 pub fn get(self) -> u64 {
81 self.0
82 }
83}
84
85impl RefCount {
86 pub fn new(value: u32) -> Self {
87 Self(value.max(1))
88 }
89 fn get(self) -> u32 {
90 self.0
91 }
92}
93
94impl RosterCount {
95 pub fn new(value: u32) -> Self {
96 Self(value.max(1))
97 }
98 fn get(self) -> u32 {
99 self.0
100 }
101}
102
103#[derive(Debug, Clone, Copy)]
104pub struct HistorySpec {
105 pub commits: CommitCount,
106 pub paths: PathCount,
107 pub churn: ChurnCount,
108}
109
110const CONTENT_BYTES: usize = 128;
111const GENESIS_SECONDS: i64 = 1_700_000_000;
112
113fn splitmix(seed: u64) -> u64 {
114 let z = seed.wrapping_add(0x9e37_79b9_7f4a_7c15);
115 let z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
116 let z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
117 z ^ (z >> 31)
118}
119
120knot_types::scalar_newtype! {
121 struct PathIndex(u32);
122 struct Revision(u32);
123}
124
125fn blob_content(path_index: PathIndex, revision: Revision) -> Vec<u8> {
126 let seed = splitmix(u64::from(path_index.get()) ^ u64::from(revision.get()).rotate_left(32));
127 (0..CONTENT_BYTES)
128 .scan(seed, |state, _| {
129 *state = splitmix(*state);
130 Some((*state & 0xff) as u8)
131 })
132 .collect()
133}
134
135fn path_at(path_index: PathIndex) -> knot_types::RepoPath {
136 let path_index = path_index.get();
137 knot_types::RepoPath::new(format!(
138 "src/m{:04}/f{:06}.dat",
139 path_index / 256,
140 path_index
141 ))
142 .expect("generated fixture path is well-formed")
143}
144
145fn identity(revision: Revision) -> Identity {
146 Identity {
147 name: AuthorName::new("nel"),
148 email: Email::new("nel@oyster.cafe"),
149 time: UnixSeconds::new(GENESIS_SECONDS + i64::from(revision.get())),
150 offset_seconds: 0,
151 }
152}
153
154fn put(path_index: PathIndex, revision: Revision) -> StagedChange {
155 StagedChange {
156 path: path_at(path_index),
157 action: StagedAction::Put {
158 content: blob_content(path_index, revision),
159 kind: EntryKind::Blob,
160 },
161 }
162}
163
164fn churn_indices(revision: Revision, spec: HistorySpec) -> impl Iterator<Item = PathIndex> {
165 let span = u64::from(spec.paths.get());
166 let base = u64::from(revision.get()).wrapping_mul(u64::from(spec.churn.get()));
167 (0..spec.churn.get())
168 .map(move |offset| PathIndex::new(((base + u64::from(offset)) % span) as u32))
169}
170
171pub struct BuiltHistory {
172 _dir: TempDir,
173 repo: Repo,
174 tip: Oid,
175}
176
177impl BuiltHistory {
178 pub fn repo(&self) -> &Repo {
179 &self.repo
180 }
181 pub fn tip(&self) -> Oid {
182 self.tip
183 }
184 pub fn tips(&self) -> Vec<Oid> {
185 vec![self.tip]
186 }
187}
188
189pub fn build_history(spec: HistorySpec) -> BuiltHistory {
190 let dir = tempfile::tempdir().expect("tempdir");
191 let repo = Repo::create(dir.path().join("repo.git")).expect("create repo");
192 let tip = write_history(&repo, spec);
193 BuiltHistory {
194 _dir: dir,
195 repo,
196 tip,
197 }
198}
199
200pub fn write_history(repo: &Repo, spec: HistorySpec) -> Oid {
201 let empty_tree = Oid::from(repo.git().empty_tree().id().detach());
202
203 let genesis_changes: Vec<StagedChange> = (0..spec.paths.get())
204 .map(|index| put(PathIndex::new(index), Revision::new(0)))
205 .collect();
206 let genesis_tree = repo
207 .write_staged_tree(empty_tree, &genesis_changes)
208 .expect("genesis tree");
209 let genesis_commit = repo
210 .write_commit(&NewCommit {
211 tree: genesis_tree,
212 parents: Vec::new(),
213 author: identity(Revision::new(0)),
214 committer: identity(Revision::new(0)),
215 message: "genesis".to_string(),
216 extra_headers: Vec::new(),
217 })
218 .expect("genesis commit");
219
220 let (_, tip) = (1..spec.commits.get())
221 .try_fold(
222 (genesis_tree, genesis_commit),
223 |(prev_tree, prev_commit), revision| -> Result<(Oid, Oid), knot_git::GitError> {
224 let revision = Revision::new(revision);
225 let changes: Vec<StagedChange> = churn_indices(revision, spec)
226 .map(|index| put(index, revision))
227 .collect();
228 let tree = repo.write_staged_tree(prev_tree, &changes)?;
229 let commit = repo.write_commit(&NewCommit {
230 tree,
231 parents: vec![prev_commit],
232 author: identity(revision),
233 committer: identity(revision),
234 message: format!("revision {}", revision.get()),
235 extra_headers: Vec::new(),
236 })?;
237 Ok((tree, commit))
238 },
239 )
240 .expect("commit chain");
241
242 let main = RefName::new("refs/heads/main").expect("main ref name");
243 repo.update_ref(&RefUpdate::Create {
244 name: main.clone(),
245 new: tip,
246 })
247 .expect("create main");
248 repo.set_head(&main).expect("set head");
249
250 tip
251}
252
253pub struct BuiltRefs {
254 _dir: TempDir,
255 repo: Repo,
256}
257
258impl BuiltRefs {
259 pub fn repo(&self) -> &Repo {
260 &self.repo
261 }
262}
263
264pub fn build_many_refs(count: RefCount) -> BuiltRefs {
265 let dir = tempfile::tempdir().expect("tempdir");
266 let repo = Repo::create(dir.path().join("repo.git")).expect("create repo");
267 let tip = write_history(
268 &repo,
269 HistorySpec {
270 commits: CommitCount::new(1),
271 paths: PathCount::new(1),
272 churn: ChurnCount::new(1),
273 },
274 );
275 (0..count.get()).for_each(|index| {
276 let name = RefName::new(format!("refs/heads/branch{index:06}")).expect("ref name");
277 repo.update_ref(&RefUpdate::Create { name, new: tip })
278 .expect("create ref");
279 });
280 BuiltRefs { _dir: dir, repo }
281}
282
283fn synthetic_account(seed: u64) -> AccountDid {
284 AccountDid::new(format!("did:plc:acct{seed:012}")).expect("account did")
285}
286
287fn synthetic_repo_did(index: u64) -> RepoDid {
288 RepoDid::new(format!("did:plc:repo{index:012}")).expect("repo did")
289}
290
291fn registry_owner() -> OwnerDid {
292 OwnerDid::new("did:plc:nel").expect("owner did")
293}
294
295fn knot_home() -> CobHome {
296 CobHome::from(&KnotId::new("did:web:knot.nel.pet").expect("knot did"))
297}
298
299fn registration(index: u64) -> Registration {
300 let rkey = format!("repo{index:012}");
301 Registration {
302 owner: registry_owner(),
303 rkey: RepoRkey::new(&rkey).expect("rkey"),
304 name: RepoName::new(&rkey).expect("repo name"),
305 repo: synthetic_repo_did(index),
306 created_at: UnixSeconds::new(GENESIS_SECONDS + index as i64),
307 }
308}
309
310fn grant(seed: u64) -> Grant {
311 Grant {
312 subject: synthetic_account(seed),
313 added_by: synthetic_account(0),
314 created_at: UnixSeconds::new(GENESIS_SECONDS),
315 }
316}
317
318pub struct BuiltRegistry {
319 _dir: TempDir,
320 meta_path: PathBuf,
321 layout: Layout,
322 dids: Vec<RepoDid>,
323}
324
325impl BuiltRegistry {
326 pub fn index(&self) -> Index {
327 Index::new(&self.meta_path, self.layout.clone())
328 }
329 pub fn dids(&self) -> &[RepoDid] {
330 &self.dids
331 }
332 pub fn alias(&self, index: u64) -> (OwnerDid, RepoRkey) {
333 let reg = registration(index);
334 (reg.owner, reg.rkey)
335 }
336}
337
338fn seed_members(meta_path: &Path, signer: &dyn knot_runtime::Signer) {
339 let meta = Repo::open(meta_path).expect("open meta");
340 let store = CobStore::new(&meta);
341 store
342 .create(
343 &knot_home(),
344 &knot_cobs::MembersChange::Add(grant(1)),
345 signer,
346 UnixSeconds::new(GENESIS_SECONDS),
347 )
348 .expect("seed members");
349}
350
351pub fn build_registry(repos: RepoCount) -> BuiltRegistry {
352 let dir = tempfile::tempdir().expect("tempdir");
353 let meta_path = dir.path().join("meta.git");
354 Repo::create(&meta_path).expect("create meta");
355 let layout = Layout::new(dir.path().join("repos"));
356 let signer = K256Signer::generate(&SeededEntropy::new(7));
357
358 seed_members(&meta_path, &signer);
359
360 let meta = Repo::open(&meta_path).expect("open meta");
361 let store = CobStore::new(&meta);
362 let registry_object = store
363 .create(
364 &knot_home(),
365 &RegistryChange::Register(registration(0)),
366 &signer,
367 UnixSeconds::new(GENESIS_SECONDS),
368 )
369 .expect("create registry")
370 .object;
371 (1..repos.get()).for_each(|index| {
372 store
373 .update(
374 &knot_home(),
375 registry_object,
376 &RegistryChange::Register(registration(index)),
377 &signer,
378 UnixSeconds::new(GENESIS_SECONDS + index as i64),
379 )
380 .expect("register repo");
381 });
382
383 let dids: Vec<RepoDid> = (0..repos.get()).map(synthetic_repo_did).collect();
384
385 dids.iter().enumerate().for_each(|(index, did)| {
386 let git = layout.create(did).expect("create repo dir");
387 let collab = CobStore::new(&git);
388 collab
389 .create(
390 &CobHome::from(did),
391 &CollaboratorsChange::Add(grant(index as u64 + 2)),
392 &signer,
393 UnixSeconds::new(GENESIS_SECONDS),
394 )
395 .expect("seed collaborator");
396 });
397
398 BuiltRegistry {
399 _dir: dir,
400 meta_path,
401 layout,
402 dids,
403 }
404}
405
406pub struct BuiltRoster {
407 _dir: TempDir,
408 meta_path: PathBuf,
409 layout: Layout,
410 repo: RepoDid,
411}
412
413impl BuiltRoster {
414 pub fn index(&self) -> Index {
415 Index::new(&self.meta_path, self.layout.clone())
416 }
417 pub fn repo(&self) -> &RepoDid {
418 &self.repo
419 }
420}
421
422pub fn build_collaborator_roster(collaborators: RosterCount) -> BuiltRoster {
423 let dir = tempfile::tempdir().expect("tempdir");
424 let meta_path = dir.path().join("meta.git");
425 Repo::create(&meta_path).expect("create meta");
426 let layout = Layout::new(dir.path().join("repos"));
427 let signer = K256Signer::generate(&SeededEntropy::new(9));
428
429 seed_members(&meta_path, &signer);
430
431 let repo = synthetic_repo_did(0);
432 let git = layout.create(&repo).expect("create repo dir");
433 let store = CobStore::new(&git);
434 let home = CobHome::from(&repo);
435 let object = store
436 .create(
437 &home,
438 &CollaboratorsChange::Add(grant(2)),
439 &signer,
440 UnixSeconds::new(GENESIS_SECONDS),
441 )
442 .expect("seed collaborator")
443 .object;
444 (1..collaborators.get()).for_each(|index| {
445 store
446 .update(
447 &home,
448 object,
449 &CollaboratorsChange::Add(grant(u64::from(index) + 2)),
450 &signer,
451 UnixSeconds::new(GENESIS_SECONDS + i64::from(index)),
452 )
453 .expect("add collaborator");
454 });
455
456 BuiltRoster {
457 _dir: dir,
458 meta_path,
459 layout,
460 repo,
461 }
462}
463
464pub struct BuiltRegistryWriter {
465 _dir: TempDir,
466 meta_path: PathBuf,
467 object: CobId,
468 signer: K256Signer,
469}
470
471impl BuiltRegistryWriter {
472 pub fn probe(&self) {
473 let meta = Repo::open(&self.meta_path).expect("reopen meta");
474 let store = CobStore::new(&meta);
475 register_repo(
476 &store,
477 &knot_home(),
478 self.object,
479 registration(0),
480 &self.signer,
481 UnixSeconds::new(GENESIS_SECONDS),
482 )
483 .expect("idempotent re-register folds registry");
484 }
485
486 pub fn full_fold(&self) {
487 let meta = Repo::open(&self.meta_path).expect("reopen meta");
488 let store = CobStore::new(&meta);
489 store
490 .get::<RepoRegistryCob>(self.object)
491 .expect("full fold of registry");
492 }
493}
494
495pub fn build_registry_checkpointed(repos: RepoCount) -> BuiltRegistryWriter {
496 let dir = tempfile::tempdir().expect("tempdir");
497 let meta_path = dir.path().join("meta.git");
498 Repo::create(&meta_path).expect("create meta");
499 let signer = K256Signer::generate(&SeededEntropy::new(13));
500
501 let meta = Repo::open(&meta_path).expect("open meta");
502 let store = CobStore::new(&meta);
503 let object = store
504 .create(
505 &knot_home(),
506 &RegistryChange::Register(registration(0)),
507 &signer,
508 UnixSeconds::new(GENESIS_SECONDS),
509 )
510 .expect("create registry")
511 .object;
512 (1..repos.get()).for_each(|index| {
513 register_repo(
514 &store,
515 &knot_home(),
516 object,
517 registration(index),
518 &signer,
519 UnixSeconds::new(GENESIS_SECONDS + index as i64),
520 )
521 .expect("register repo");
522 });
523
524 BuiltRegistryWriter {
525 _dir: dir,
526 meta_path,
527 object,
528 signer,
529 }
530}
531
532pub struct BuiltMembersWriter {
533 _dir: TempDir,
534 meta_path: PathBuf,
535 object: CobId,
536 signer: K256Signer,
537}
538
539impl BuiltMembersWriter {
540 pub fn probe(&self) {
541 let meta = Repo::open(&self.meta_path).expect("reopen meta");
542 let store = CobStore::new(&meta);
543 store
544 .update_maybe_checkpointed::<MembersCob, CobError>(
545 &knot_home(),
546 self.object,
547 &self.signer,
548 UnixSeconds::new(GENESIS_SECONDS),
549 |roster| {
550 Ok(if roster.contains(&grant(0).subject) {
551 None
552 } else {
553 Some(MembersChange::Add(grant(0)))
554 })
555 },
556 )
557 .expect("idempotent re-add folds the bounded suffix");
558 }
559
560 pub fn full_fold(&self) {
561 let meta = Repo::open(&self.meta_path).expect("reopen meta");
562 let store = CobStore::new(&meta);
563 store
564 .get::<MembersCob>(self.object)
565 .expect("full fold of members");
566 }
567}
568
569pub fn build_members_checkpointed(members: RosterCount) -> BuiltMembersWriter {
570 let dir = tempfile::tempdir().expect("tempdir");
571 let meta_path = dir.path().join("meta.git");
572 Repo::create(&meta_path).expect("create meta");
573 let signer = K256Signer::generate(&SeededEntropy::new(17));
574
575 let meta = Repo::open(&meta_path).expect("open meta");
576 let store = CobStore::new(&meta);
577 let object = store
578 .create(
579 &knot_home(),
580 &MembersChange::Add(grant(0)),
581 &signer,
582 UnixSeconds::new(GENESIS_SECONDS),
583 )
584 .expect("create members")
585 .object;
586 (1..members.get()).for_each(|index| {
587 add_member(
588 &store,
589 &knot_home(),
590 object,
591 grant(u64::from(index) + 1),
592 &signer,
593 UnixSeconds::new(GENESIS_SECONDS + i64::from(index)),
594 )
595 .expect("add member");
596 });
597
598 BuiltMembersWriter {
599 _dir: dir,
600 meta_path,
601 object,
602 signer,
603 }
604}
605
606pub struct BuiltLinearCob {
607 _dir: TempDir,
608 repo: Repo,
609 object: CobId,
610}
611
612impl BuiltLinearCob {
613 pub fn fold(&self) -> usize {
614 let store = CobStore::new(&self.repo);
615 store
616 .get::<RepoRegistryCob>(self.object)
617 .expect("fold registry")
618 .state()
619 .len()
620 }
621}
622
623pub fn build_linear_cob(changes: ChangeCount) -> BuiltLinearCob {
624 let dir = tempfile::tempdir().expect("tempdir");
625 let meta_path = dir.path().join("meta.git");
626 Repo::create(&meta_path).expect("create meta");
627 let signer = K256Signer::generate(&SeededEntropy::new(11));
628
629 let meta = Repo::open(&meta_path).expect("open meta");
630 let store = CobStore::new(&meta);
631 let object = store
632 .create(
633 &knot_home(),
634 &RegistryChange::Register(registration(0)),
635 &signer,
636 UnixSeconds::new(GENESIS_SECONDS),
637 )
638 .expect("create registry")
639 .object;
640 (1..changes.get()).for_each(|index| {
641 store
642 .update(
643 &knot_home(),
644 object,
645 &RegistryChange::Register(registration(u64::from(index))),
646 &signer,
647 UnixSeconds::new(GENESIS_SECONDS + i64::from(index)),
648 )
649 .expect("append change");
650 });
651
652 BuiltLinearCob {
653 _dir: dir,
654 repo: Repo::open(&meta_path).expect("reopen meta"),
655 object,
656 }
657}