manifest_preflight_test.go RawHistoryFolder

1package snapshotcodec
2
3import (
4	"encoding/binary"
5	"errors"
6	"hash/crc32"
7	"testing"
8
9	"github.com/bnema/vev/internal/domain"
10	"github.com/bnema/vev/internal/domain/layout"
11)
12
13func TestManifestGrammarRoundTrip(t *testing.T) {
14	for _, tc := range []struct {
15		name string
16		tree *layout.Tree
17		proc *Process
18	}{
19		{"split without process", &layout.Tree{Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{layout.NewLeaf("one"), layout.NewLeaf("two")}}}, nil},
20		{"stack with process argv", &layout.Tree{Root: &layout.Node{Kind: layout.Stack, Expanded: "one", Children: []*layout.Node{layout.NewLeaf("one"), layout.NewLeaf("two")}}}, &Process{Argv: []string{"sh", "-c", "echo"}, Strategy: "restore", Opts: ProcessOpts{AgentSessionID: "agent"}}},
21	} {
22		t.Run(tc.name, func(t *testing.T) {
23			encoded, err := MarshalManifest(testManifest(tc.tree, tc.proc))
24			if err != nil {
25				t.Fatal(err)
26			}
27			got, err := UnmarshalManifest(encoded)
28			if err != nil {
29				t.Fatal(err)
30			}
31			if got.Tabs[0].Tree.Root.Kind != tc.tree.Root.Kind || (got.Tabs[0].Panes[0].Process == nil) != (tc.proc == nil) {
32				t.Fatalf("decoded tree/process = %#v/%#v", got.Tabs[0].Tree, got.Tabs[0].Panes[0].Process)
33			}
34		})
35	}
36}
37
38func TestManifestEnvelopePreflightParity(t *testing.T) {
39	encoded, err := MarshalManifest(testManifest(layout.NewTree("one"), nil))
40	if err != nil {
41		t.Fatal(err)
42	}
43	nameOffset, nodeKindOffset := manifestFieldOffsets(t, encoded)
44	mutations := []struct {
45		name string
46		edit func([]byte) []byte
47	}{
48		{"valid", func(b []byte) []byte { return b }},
49		{"magic", func(b []byte) []byte { b[0] ^= 1; return b }},
50		{"version", func(b []byte) []byte { binary.BigEndian.PutUint16(b[4:6], ManifestVersion+1); return b }},
51		{"flags", func(b []byte) []byte { b[7] = 1; return b }},
52		{"body length", func(b []byte) []byte { binary.BigEndian.PutUint32(b[8:12], uint32(len(b))); return b }},
53		{"crc", func(b []byte) []byte { b[12] ^= 1; return b }},
54		{"trailing", func(b []byte) []byte { return append(b, 0) }},
55		{"body name", func(b []byte) []byte {
56			b[nameOffset] = 0
57			binary.BigEndian.PutUint32(b[12:16], checksum(b[16:]))
58			return b
59		}},
60		{"body generation", func(b []byte) []byte {
61			clear(b[manifestHeaderSize : manifestHeaderSize+8])
62			binary.BigEndian.PutUint32(b[12:16], checksum(b[16:]))
63			return b
64		}},
65		{"body node kind", func(b []byte) []byte {
66			b[nodeKindOffset] = 99
67			binary.BigEndian.PutUint32(b[12:16], checksum(b[16:]))
68			return b
69		}},
70	}
71	for _, tc := range mutations {
72		t.Run(tc.name, func(t *testing.T) {
73			candidate := tc.edit(append([]byte(nil), encoded...))
74			_, decodeErr := UnmarshalManifest(candidate)
75			preflightErr := preflightManifestEnvelope(candidate)
76			uncheckedErr := decodeManifestEnvelope(candidate)
77			if (decodeErr == nil) != (preflightErr == nil) || (preflightErr == nil) != (uncheckedErr == nil) {
78				t.Fatalf("preflight error = %v, unchecked decode error = %v, decode error = %v", preflightErr, uncheckedErr, decodeErr)
79			}
80		})
81	}
82}
83
84func manifestFieldOffsets(t *testing.T, encoded []byte) (nameOffset, nodeKindOffset int) {
85	t.Helper()
86	r := payloadReader{b: encoded[manifestHeaderSize:]}
87	must := func(err error) {
88		if err != nil {
89			t.Fatal(err)
90		}
91	}
92	_, err := r.getUint64()
93	must(err)
94	if len(r.b) < len(domain.IncarnationID{}) {
95		t.Fatalf("manifest body has %d incarnation bytes, want at least %d", len(r.b), len(domain.IncarnationID{}))
96	}
97	r.b = r.b[len(domain.IncarnationID{}):]
98	_, err = r.getUint8()
99	must(err)
100	nameLengthOffset := len(encoded) - len(r.b)
101	name, err := r.getString()
102	must(err)
103	nameOffset = nameLengthOffset + 2
104	if len(name) == 0 {
105		t.Fatal("test manifest has empty name")
106	}
107	_, err = r.getUint64()
108	must(err)
109	_, err = r.getUint16()
110	must(err)
111	_, err = r.getUint16()
112	must(err)
113	_, err = r.getString()
114	must(err)
115	_, err = r.getUint16()
116	must(err)
117	_, err = r.getUint16()
118	must(err)
119	_, err = r.getUint64()
120	must(err)
121	_, err = r.getString()
122	must(err)
123	nodeKindOffset = len(encoded) - len(r.b)
124	const wantName = "named"
125	if nameOffset < 0 || nameOffset+len(wantName) > len(encoded) || string(encoded[nameOffset:nameOffset+len(wantName)]) != wantName {
126		t.Fatalf("name offset %d does not point to %q", nameOffset, wantName)
127	}
128	if nodeKindOffset < 0 || nodeKindOffset >= len(encoded) || encoded[nodeKindOffset] != manifestNodeLeaf {
129		t.Fatalf("node kind offset %d does not point to leaf kind", nodeKindOffset)
130	}
131	return nameOffset, nodeKindOffset
132}
133
134func TestManifestObjectRefKinds(t *testing.T) {
135	for _, tc := range []struct {
136		name  string
137		got   ObjectKind
138		want  ObjectKind
139		valid bool
140	}{
141		{"chunk", HistoryChunk, HistoryChunk, true},
142		{"tail", HistoryTail, HistoryTail, true},
143		{"transcript", RecoveryTranscript, RecoveryTranscript, true},
144		{"invalid", ObjectKind(99), RecoveryTranscript, false},
145		{"wrong role", HistoryTail, RecoveryTranscript, false},
146	} {
147		t.Run(tc.name, func(t *testing.T) {
148			var w payloadWriter
149			writeObjectRef(&w, ObjectRef{Kind: tc.got, Digest: SnapshotDigest{1}, Size: minObjectEnvelopeSize})
150			err := preflightObjectRef(&payloadReader{b: w.b}, tc.want)
151			if (err == nil) != tc.valid {
152				t.Fatalf("preflightObjectRef() error = %v, valid=%v", err, tc.valid)
153			}
154		})
155	}
156}
157
158func TestManifestPreflightHostileDeclarations(t *testing.T) {
159	t.Run("tab count", func(t *testing.T) {
160		var w payloadWriter
161		w.putUint64(1)
162		if err := w.putString("name"); err != nil {
163			t.Fatalf("putString(name): %v", err)
164		}
165		w.putUint64(1)
166		w.putUint16(0)
167		w.putUint16(^uint16(0))
168		if err := preflightManifest(w.b); !errors.Is(err, ErrInvalidData) {
169			t.Fatalf("preflightManifest() error = %v", err)
170		}
171	})
172	for _, tc := range []struct {
173		name string
174		fn   func(*payloadReader, *manifestPreflightBudget) error
175		body []byte
176	}{
177		{"string length", func(r *payloadReader, b *manifestPreflightBudget) error { return skipManifestString(r, b) }, []byte{0, 1}},
178		{"tree children", func(r *payloadReader, b *manifestPreflightBudget) error {
179			return preflightManifestNode(r, 0, true, b, nil)
180		}, []byte{manifestNodeSplit, byte(layout.Horizontal), 0xff, 0xff}},
181		{"nil child", func(r *payloadReader, b *manifestPreflightBudget) error {
182			return preflightManifestNode(r, 1, false, b, nil)
183		}, []byte{manifestNodeNil}},
184		{"process declaration", func(r *payloadReader, b *manifestPreflightBudget) error { return preflightManifestProcess(r, b) }, []byte{processPresent, 0, 1, 0, 2, 'x'}},
185	} {
186		t.Run(tc.name, func(t *testing.T) {
187			err := tc.fn(&payloadReader{b: tc.body}, &manifestPreflightBudget{})
188			if err == nil {
189				t.Fatal("hostile declaration accepted")
190			}
191		})
192	}
193}
194
195func TestSkipManifestStringDoesNotAllocate(t *testing.T) {
196	raw := make([]byte, 2+1024)
197	binary.BigEndian.PutUint16(raw, 1024)
198	if allocations := testing.AllocsPerRun(100, func() {
199		r := payloadReader{b: raw}
200		if err := skipManifestString(&r, &manifestPreflightBudget{}); err != nil {
201			t.Fatal(err)
202		}
203	}); allocations != 0 {
204		t.Fatalf("skipManifestString allocations = %v, want 0", allocations)
205	}
206}
207
208func preflightManifestEnvelope(encoded []byte) error {
209	body, err := unmarshalManifestEnvelope(encoded, manifestMagic)
210	if err != nil {
211		return err
212	}
213	return preflightManifest(body)
214}
215
216func decodeManifestEnvelope(encoded []byte) error {
217	body, err := unmarshalManifestEnvelope(encoded, manifestMagic)
218	if err != nil {
219		return err
220	}
221	_, err = decodeManifest(body)
222	return err
223}
224
225func checksum(body []byte) uint32 {
226	return crc32.ChecksumIEEE(body)
227}
228
229func testManifest(tree *layout.Tree, process *Process) Manifest {
230	return Manifest{Generation: 1, IncarnationID: domain.IncarnationID{1}, Name: "named", Tabs: []ManifestTab{{
231		StableID: "tab", Cols: 80, Rows: 24, Focus: "one", Tree: tree,
232		Panes: []ManifestPane{
233			{ID: "one", StableID: "one", Cwd: "/one", Sealed: []ObjectRef{{Kind: HistoryChunk, Digest: SnapshotDigest{1}, Size: minObjectEnvelopeSize}}, Tail: ObjectRef{Kind: HistoryTail, Digest: SnapshotDigest{2}, Size: minObjectEnvelopeSize}, Transcript: ObjectRef{Kind: RecoveryTranscript, Digest: SnapshotDigest{3}, Size: minObjectEnvelopeSize}, Process: process},
234			{ID: "two", StableID: "two", Cwd: "/two", Tail: ObjectRef{Kind: HistoryTail, Digest: SnapshotDigest{4}, Size: minObjectEnvelopeSize}, Transcript: ObjectRef{Kind: RecoveryTranscript, Digest: SnapshotDigest{5}, Size: minObjectEnvelopeSize}},
235		},
236	}}}
237}

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