manifest_test.go RawHistoryFolder

1package snapshotcodec
2
3import (
4	"bytes"
5	"crypto/sha256"
6	"encoding/binary"
7	"errors"
8	"math"
9	"testing"
10
11	"github.com/bnema/vev/internal/domain"
12	"github.com/bnema/vev/internal/domain/layout"
13	"github.com/bnema/vev/internal/ports"
14)
15
16func TestManifestIncarnation(t *testing.T) {
17	manifest := manifestWithTailSize(minObjectEnvelopeSize)
18	manifest.Generation = 2
19	manifest.IncarnationID = domain.IncarnationID{1}
20	manifest.ParentCheckpoint = &domain.CheckpointRef{Generation: 1, ManifestDigest: [32]byte{2}}
21
22	encoded, err := MarshalManifest(manifest)
23	if err != nil {
24		t.Fatal(err)
25	}
26	decoded, err := UnmarshalManifest(encoded)
27	if err != nil {
28		t.Fatal(err)
29	}
30	if decoded.IncarnationID != manifest.IncarnationID || decoded.ParentCheckpoint == nil || *decoded.ParentCheckpoint != *manifest.ParentCheckpoint {
31		t.Fatalf("round trip = %#v", decoded)
32	}
33	reencoded, err := MarshalManifest(decoded)
34	if err != nil || !bytes.Equal(encoded, reencoded) {
35		t.Fatalf("manifest bytes are not canonical: %v", err)
36	}
37	if got := binary.BigEndian.Uint16(encoded[4:6]); got != ManifestVersion {
38		t.Fatalf("manifest version = %d", got)
39	}
40	for n := range len(encoded) {
41		if _, err := UnmarshalManifest(encoded[:n]); err == nil {
42			t.Fatalf("prefix %d accepted", n)
43		}
44	}
45	if _, err := UnmarshalManifest(append(encoded, 0)); err == nil {
46		t.Fatal("trailing data accepted")
47	}
48	versionOne := append([]byte(nil), encoded...)
49	binary.BigEndian.PutUint16(versionOne[4:6], 1)
50	if _, err := UnmarshalManifest(versionOne); !errors.Is(err, ErrBadVersion) {
51		t.Fatalf("manifest v1 error = %v", err)
52	}
53	manifest.IncarnationID = domain.IncarnationID{}
54	if _, err := MarshalManifest(manifest); err == nil {
55		t.Fatal("zero incarnation accepted")
56	}
57}
58
59func TestManifestCodecUsesGenerationAndOrderedTypedReferences(t *testing.T) {
60	chunk, err := MarshalObject(HistoryChunk, []byte("history"))
61	if err != nil {
62		t.Fatal(err)
63	}
64	tail, err := MarshalObject(HistoryTail, []byte("tail"))
65	if err != nil {
66		t.Fatal(err)
67	}
68	transcript, err := MarshalObject(RecoveryTranscript, []byte("transcript"))
69	if err != nil {
70		t.Fatal(err)
71	}
72	manifest := Manifest{Generation: 7, IncarnationID: domain.IncarnationID{1}, Name: "named", CreatedAt: 9, Active: 0, Tabs: []ManifestTab{{
73		StableID: "tab", Cols: 80, Rows: 24, NextPaneID: 2, Focus: "pane", Tree: layout.NewTree("pane"),
74		Panes: []ManifestPane{{ID: "pane", StableID: "pane-stable", Cwd: "/tmp",
75			Sealed:     []ObjectRef{{Kind: HistoryChunk, Digest: chunk.Digest, Size: uint32(len(chunk.Data))}},
76			Tail:       ObjectRef{Kind: HistoryTail, Digest: tail.Digest, Size: uint32(len(tail.Data))},
77			Transcript: ObjectRef{Kind: RecoveryTranscript, Digest: transcript.Digest, Size: uint32(len(transcript.Data))},
78		}},
79	}}}
80	encoded, err := MarshalManifest(manifest)
81	if err != nil {
82		t.Fatal(err)
83	}
84	got, err := UnmarshalManifest(encoded)
85	if err != nil {
86		t.Fatal(err)
87	}
88	if got.Generation != manifest.Generation || got.Tabs[0].Panes[0].Tail != manifest.Tabs[0].Panes[0].Tail {
89		t.Fatalf("round trip = %#v", got)
90	}
91	gotTranscript := got.Tabs[0].Panes[0].Transcript
92	if gotTranscript.Kind != RecoveryTranscript || gotTranscript.Digest != transcript.Digest || gotTranscript.Size != uint32(len(transcript.Data)) {
93		t.Fatalf("transcript round trip = %#v", gotTranscript)
94	}
95	for n := range len(encoded) {
96		if _, err := UnmarshalManifest(encoded[:n]); err == nil {
97			t.Fatalf("prefix %d accepted", n)
98		}
99	}
100	if _, err := UnmarshalManifest(append(encoded, 0)); !errors.Is(err, ErrTrailingBytes) {
101		t.Fatalf("trailing data error = %v", err)
102	}
103
104	head := Head{Generation: 7, ManifestDigest: sha256.Sum256(encoded)}
105	if head.Generation != manifest.Generation || head.ManifestDigest != ManifestDigest(encoded) {
106		t.Fatalf("head = %#v", head)
107	}
108}
109
110func TestObjectCodecReturnsContentAddressedObjectAndPayload(t *testing.T) {
111	object, err := MarshalObject(RecoveryTranscript, []byte("canonical-vt"))
112	if err != nil {
113		t.Fatal(err)
114	}
115	if object.Digest != sha256.Sum256(object.Data) {
116		t.Fatalf("digest does not address VEVO bytes")
117	}
118	kind, payload, err := UnmarshalObject(object.Data)
119	if err != nil || kind != RecoveryTranscript || !bytes.Equal(payload, []byte("canonical-vt")) {
120		t.Fatalf("round trip = %v %q %v", kind, payload, err)
121	}
122	kind, err = PreflightObject(object.Data)
123	if err != nil || kind != RecoveryTranscript {
124		t.Fatalf("preflight = %v %v", kind, err)
125	}
126	for n := range len(object.Data) {
127		if _, _, err := UnmarshalObject(object.Data[:n]); err == nil {
128			t.Fatalf("prefix %d accepted", n)
129		}
130	}
131	if _, _, err := UnmarshalObject(append(object.Data, 0)); !errors.Is(err, ErrTrailingBytes) {
132		t.Fatalf("trailing data error = %v", err)
133	}
134}
135
136func TestManifestDimensionBoundaries(t *testing.T) {
137	ref := ObjectRef{Kind: HistoryTail, Digest: ports.SnapshotDigest{1}, Size: minObjectEnvelopeSize}
138	transcript := ObjectRef{Kind: RecoveryTranscript, Digest: ports.SnapshotDigest{2}, Size: minObjectEnvelopeSize}
139	for _, tc := range []struct {
140		name       string
141		cols, rows uint16
142		wantErr    bool
143	}{
144		{"max cols safe rows", math.MaxUint16, 1, false},
145		{"max rows safe cols", 1, math.MaxUint16, false},
146		{"hostile product", math.MaxUint16, math.MaxUint16, true},
147	} {
148		t.Run(tc.name, func(t *testing.T) {
149			m := Manifest{Generation: 1, IncarnationID: domain.IncarnationID{1}, Name: "safe", Tabs: []ManifestTab{{Cols: tc.cols, Rows: tc.rows, Panes: []ManifestPane{{ID: "p", Tail: ref, Transcript: transcript}}}}}
150			_, err := MarshalManifest(m)
151			if (err != nil) != tc.wantErr {
152				t.Fatalf("MarshalManifest() error = %v, want error=%v", err, tc.wantErr)
153			}
154		})
155	}
156}
157
158func TestManifestRejectsImpossibleObjectEnvelopeSize(t *testing.T) {
159	manifest := Manifest{Generation: 1, IncarnationID: domain.IncarnationID{1}, Name: "named", Tabs: []ManifestTab{{Cols: 1, Rows: 1, Panes: []ManifestPane{{
160		ID:         "pane",
161		Tail:       ObjectRef{Kind: HistoryTail, Digest: SnapshotDigest{1}, Size: 1},
162		Transcript: ObjectRef{Kind: RecoveryTranscript, Digest: SnapshotDigest{2}, Size: minObjectEnvelopeSize},
163	}}}}}
164	if _, err := MarshalManifest(manifest); !errors.Is(err, ErrInvalidData) {
165		t.Fatalf("MarshalManifest() error = %v, want invalid data", err)
166	}
167}
168
169func TestObjectRefEnvelopeSizeBoundaries(t *testing.T) {
170	const (
171		minimum = manifestHeaderSize + 1 + 4 + 1
172		maximum = manifestHeaderSize + maxDecodedBodySize
173	)
174	if minObjectEnvelopeSize != minimum || maxObjectEnvelopeSize != maximum {
175		t.Fatalf("object envelope range = [%d, %d], want [%d, %d]", minObjectEnvelopeSize, maxObjectEnvelopeSize, minimum, maximum)
176	}
177	object, err := MarshalObject(HistoryTail, []byte{0})
178	if err != nil {
179		t.Fatalf("MarshalObject() error = %v", err)
180	}
181	// The smallest payload is still a complete zlib stream, so real objects sit
182	// above the declared lower bound the manifest accepts.
183	if len(object.Data) < minimum {
184		t.Fatalf("MarshalObject() size = %d, want >= %d", len(object.Data), minimum)
185	}
186
187	for _, tc := range []struct {
188		name  string
189		size  uint32
190		valid bool
191	}{
192		{name: "one below minimum", size: minimum - 1},
193		{name: "minimum", size: minimum, valid: true},
194		{name: "maximum declaration", size: maximum, valid: true},
195		{name: "one above maximum", size: maximum + 1},
196	} {
197		t.Run(tc.name, func(t *testing.T) {
198			manifest := manifestWithTailSize(tc.size)
199			encoded, err := MarshalManifest(manifest)
200			if tc.valid {
201				if err != nil {
202					t.Fatalf("MarshalManifest() error = %v", err)
203				}
204				if _, err := UnmarshalManifest(encoded); err != nil {
205					t.Fatalf("UnmarshalManifest() error = %v", err)
206				}
207			} else if !errors.Is(err, ErrInvalidData) {
208				t.Fatalf("MarshalManifest() error = %v, want invalid data", err)
209			}
210
211			var w payloadWriter
212			writeObjectRef(&w, ObjectRef{Kind: HistoryTail, Digest: SnapshotDigest{1}, Size: tc.size})
213			err = preflightObjectRef(&payloadReader{b: w.b}, HistoryTail)
214			if tc.valid && err != nil {
215				t.Fatalf("preflightObjectRef() error = %v", err)
216			}
217			if !tc.valid && !errors.Is(err, ErrInvalidData) {
218				t.Fatalf("preflightObjectRef() error = %v, want invalid data", err)
219			}
220		})
221	}
222}
223
224func manifestWithTailSize(size uint32) Manifest {
225	return Manifest{Generation: 1, IncarnationID: domain.IncarnationID{1}, Name: "named", Tabs: []ManifestTab{{
226		Cols: 1,
227		Rows: 1,
228		Panes: []ManifestPane{{
229			ID:         "pane",
230			Tail:       ObjectRef{Kind: HistoryTail, Digest: SnapshotDigest{1}, Size: size},
231			Transcript: ObjectRef{Kind: RecoveryTranscript, Digest: SnapshotDigest{2}, Size: minObjectEnvelopeSize},
232		}},
233	}}}
234}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close