1package snapshot
2
3import (
4 "context"
5 "crypto/sha256"
6 "fmt"
7 "os"
8 "path/filepath"
9 "testing"
10
11 "github.com/bnema/vev/internal/domain"
12 "github.com/bnema/vev/internal/ports"
13 codec "github.com/bnema/vev/internal/usecase/snapshot"
14)
15
16func TestRepositoryPublishRevalidatesUnchangedTenThousandObjectHistory(t *testing.T) {
17 const historyObjects = 10_000
18
19 repo := NewRepository(privateDir(t))
20 first, second := largeIncrementalPublications(t, "named", historyObjects)
21 seedCompletePublication(t, repo, first)
22
23 if err := repo.Publish(context.Background(), second); err != nil {
24 t.Fatal(err)
25 }
26
27 // Replaying the current generation revalidates every referenced immutable
28 // object before accepting it. That is required for a forward orphan retry:
29 // the catalogue must not advance to a checkpoint whose files were damaged
30 // after its original publication.
31 if err := repo.Publish(context.Background(), second); err != nil {
32 t.Fatal(err)
33 }
34
35 got, err := loadPublication(context.Background(), repo, second)
36 if err != nil {
37 t.Fatal(err)
38 }
39 if got.Generation != second.Generation {
40 t.Fatalf("published generation = %d, want %d", got.Generation, second.Generation)
41 }
42}
43
44func BenchmarkRepositoryPublishUnchangedTenThousandObjectHistory(b *testing.B) {
45 first, second := largeIncrementalPublications(b, "named", 10_000)
46 repo := NewRepository(filepath.Join(b.TempDir(), "vev"))
47 seedCompletePublication(b, repo, first)
48 b.ReportAllocs()
49 b.ResetTimer()
50 for b.Loop() {
51 if err := repo.Publish(context.Background(), second); err != nil {
52 b.Fatal(err)
53 }
54 }
55 b.StopTimer()
56}
57
58func largeIncrementalPublications(t testing.TB, name string, count int) (ports.SnapshotPublication, ports.SnapshotPublication) {
59 t.Helper()
60 id := testIncarnationID(name)
61 sealed := make([]codec.ObjectRef, 0, count)
62 objects := make([]ports.SnapshotObject, 0, count+2)
63 for i := range count {
64 object, err := codec.MarshalObject(codec.HistoryChunk, canonicalHistoryBlob(t, fmt.Sprintf("history-%d", i)))
65 if err != nil {
66 t.Fatal(err)
67 }
68 sealed = append(sealed, codec.ObjectRef{Kind: codec.HistoryChunk, Digest: object.Digest, Size: uint32(len(object.Data))})
69 objects = append(objects, object)
70 }
71 makePublication := func(generation uint64, parent *domain.CheckpointRef, tailPayload, transcriptPayload string) ports.SnapshotPublication {
72 t.Helper()
73 tail, err := codec.MarshalObject(codec.HistoryTail, canonicalHistoryBlob(t, tailPayload))
74 if err != nil {
75 t.Fatal(err)
76 }
77 transcript, err := codec.MarshalObject(codec.RecoveryTranscript, canonicalHistoryBlob(t, transcriptPayload))
78 if err != nil {
79 t.Fatal(err)
80 }
81 manifest, err := codec.MarshalManifest(codec.Manifest{Generation: generation, IncarnationID: id, ParentCheckpoint: parent, Name: name, Tabs: []codec.ManifestTab{{Cols: 1, Rows: 1, Panes: []codec.ManifestPane{{ID: "p", Sealed: sealed, Tail: codec.ObjectRef{Kind: codec.HistoryTail, Digest: tail.Digest, Size: uint32(len(tail.Data))}, Transcript: codec.ObjectRef{Kind: codec.RecoveryTranscript, Digest: transcript.Digest, Size: uint32(len(transcript.Data))}}}}}})
82 if err != nil {
83 t.Fatal(err)
84 }
85 return ports.SnapshotPublication{IncarnationID: id, Name: name, Generation: generation, ParentCheckpoint: parent, Manifest: manifest, Objects: append(append([]ports.SnapshotObject(nil), objects...), tail, transcript)}
86 }
87 first := makePublication(1, nil, "tail-1", "transcript-1")
88 parent := &domain.CheckpointRef{Generation: 1, ManifestDigest: sha256.Sum256(first.Manifest)}
89 return first, makePublication(2, parent, "tail-2", "transcript-2")
90}
91
92func seedCompletePublication(t testing.TB, repo *Repository, publication ports.SnapshotPublication) {
93 t.Helper()
94 if err := repo.ensureSession(publication.IncarnationID); err != nil {
95 t.Fatal(err)
96 }
97 manifest, err := codec.UnmarshalManifest(publication.Manifest)
98 if err != nil {
99 t.Fatal(err)
100 }
101 for _, object := range publication.Objects {
102 if err := os.MkdirAll(filepath.Dir(repo.objectPath(publication.IncarnationID, object.Digest)), 0o700); err != nil {
103 t.Fatal(err)
104 }
105 if err := os.WriteFile(repo.objectPath(publication.IncarnationID, object.Digest), object.Data, 0o600); err != nil {
106 t.Fatal(err)
107 }
108 }
109 if err := os.WriteFile(repo.manifestPath(publication.IncarnationID, publication.Generation), publication.Manifest, 0o600); err != nil {
110 t.Fatal(err)
111 }
112 if err := os.WriteFile(repo.headPath(publication.IncarnationID), marshalHead(publication.Generation, sha256.Sum256(publication.Manifest)), 0o600); err != nil {
113 t.Fatal(err)
114 }
115 if refs := manifestRefs(manifest); refs == nil {
116 t.Fatal("invalid seeded manifest")
117 }
118}