snapshot_incremental_test.go RawHistoryFolder

1package daemon
2
3import (
4	"strings"
5	"testing"
6	"time"
7
8	"github.com/stretchr/testify/require"
9
10	vt "github.com/bnema/vev-vt"
11	renderer "github.com/bnema/vev-vt/ansi"
12	"github.com/bnema/vev/internal/domain"
13	snapcodec "github.com/bnema/vev/internal/snapshotcodec"
14)
15
16func TestMarshalSnapshotTailSelectsCanonicalEncoder(t *testing.T) {
17	for _, tt := range []struct {
18		name string
19		fill bool
20	}{
21		{name: "empty tail"},
22		{name: "non-empty tail", fill: true},
23	} {
24		t.Run(tt.name, func(t *testing.T) {
25			history := vt.NewHistory(vt.HistoryConfig{MaxRows: 2, ChunkRows: 2})
26			if tt.fill {
27				appendHistoryRow(t, history, []renderer.Cell{{Rune: 'x'}})
28			}
29			tail := history.SnapshotView().Tail()
30
31			got, err := marshalSnapshotTail(tail)
32			require.NoError(t, err)
33			if tt.fill {
34				want, err := vt.MarshalHistory(tail)
35				require.NoError(t, err)
36				require.Equal(t, want, got)
37				return
38			}
39			want, err := vt.MarshalEmptyHistoryTail()
40			require.NoError(t, err)
41			require.Equal(t, want, got)
42		})
43	}
44}
45
46func TestIncrementalPublicationEncodesAfterPaneUnlock(t *testing.T) {
47	d := New(nil, nil, nil)
48	sess := newSnapshotTestSession(t, "work", false, "/work")
49	capture, ok := d.captureSnapshotState(sess, 1)
50	if !ok {
51		t.Fatal("capture rejected named session")
52	}
53
54	pane := sess.tabs[0].panes["pane-1"]
55	pane.mu.Lock()
56	done := make(chan error, 1)
57	go func() {
58		_, err := d.incrementalPublication(capture)
59		done <- err
60	}()
61	select {
62	case err := <-done:
63		if err != nil {
64			t.Fatalf("encode capture: %v", err)
65		}
66	case <-time.After(testWaitTimeout):
67		pane.mu.Unlock()
68		t.Fatal("recovery transcript encoding waited for pane lock")
69	}
70	pane.mu.Unlock()
71}
72
73func TestSnapshotCaptureOwnsRecoveryTranscriptAcrossLiveMutations(t *testing.T) {
74	d := New(nil, nil, nil)
75	sess := newSnapshotTestSession(t, "work", false, "/work")
76	capture, ok := d.captureSnapshotState(sess, 1)
77	require.True(t, ok)
78
79	pane := sess.tabs[0].panes["pane-1"]
80	pane.mu.Lock()
81	pane.screen.Write([]byte("\x1b[1;1Hchanged"))
82	pane.mu.Unlock()
83
84	publication, err := d.incrementalPublication(capture)
85	require.NoError(t, err)
86	restored, err := sessionFromGeneration(snapshotGeneration(publication))
87	require.NoError(t, err)
88	view, err := vt.UnmarshalHistory(restored.Tabs[0].Panes[0].Transcript)
89	require.NoError(t, err)
90	require.Equal(t, "hello", strings.TrimRight(cellsString(view.Row(0)), " "))
91}
92
93func TestSnapshotChunkCacheResistsWarmedOverCapacityHistoryScans(t *testing.T) {
94	history := vt.NewHistory(vt.HistoryConfig{MaxRows: 6, ChunkRows: 1})
95	for i := range 6 {
96		appendHistoryRow(t, history, []renderer.Cell{{Rune: rune('a' + i)}})
97	}
98	view := history.SnapshotView()
99	require.Equal(t, 6, view.ChunkCount())
100
101	firstPayload, err := vt.MarshalHistoryChunk(view.Chunk(0))
102	require.NoError(t, err)
103	firstObject, err := snapcodec.MarshalObject(snapcodec.HistoryChunk, firstPayload)
104	require.NoError(t, err)
105	cache := newSnapshotChunkCache(2 * len(firstObject.Data))
106	encodes := make(map[*vt.HistoryChunk]int)
107	cache.marshalChunk = func(chunk *vt.HistoryChunk) ([]byte, error) {
108		encodes[chunk]++
109		return vt.MarshalHistoryChunk(chunk)
110	}
111
112	for pass := range 3 {
113		for i := range view.ChunkCount() {
114			_, _, err := cache.objectLocked(view.Chunk(i))
115			require.NoError(t, err)
116		}
117		require.LessOrEqual(t, cache.used, cache.limit)
118		if pass == 0 {
119			require.Len(t, cache.byPtr, 2)
120		}
121	}
122
123	// The cache admits the first bounded working set then rejects later scan
124	// misses. Consequently, each warmed retained chunk is encoded only once
125	// even though the history is repeatedly scanned oldest-to-newest.
126	require.Equal(t, 1, encodes[view.Chunk(0)])
127	require.Equal(t, 1, encodes[view.Chunk(1)])
128	require.Equal(t, 3, encodes[view.Chunk(2)])
129	require.Equal(t, 3, encodes[view.Chunk(5)])
130
131	isolated := newSnapshotChunkCache(cache.limit)
132	isolatedEncodes := 0
133	isolated.marshalChunk = func(chunk *vt.HistoryChunk) ([]byte, error) {
134		isolatedEncodes++
135		return vt.MarshalHistoryChunk(chunk)
136	}
137	_, _, err = isolated.objectLocked(view.Chunk(0))
138	require.NoError(t, err)
139	require.Equal(t, 1, isolatedEncodes, "a session cache must not share encoded history with another session")
140}
141
142func TestMarkSnapshotCaptureObjectsPublishedReleasesCachedEncoding(t *testing.T) {
143	history := vt.NewHistory(vt.HistoryConfig{MaxRows: 2, ChunkRows: 1})
144	appendHistoryRow(t, history, []renderer.Cell{{Rune: 'x'}})
145	view := history.SnapshotView()
146	chunk := view.Chunk(0)
147	transcript := vt.NewScreen(1, 1).RecoveryTranscriptSnapshot()
148	d := New(nil, nil, nil)
149	sess := newSnapshotTestSession(t, "work", false, "/work")
150	capture := &snapshotCapture{session: sess, name: "work", incarnation: domain.IncarnationID{1}, generation: 1, tabs: []snapshotCaptureTab{{stableID: "t", cols: 1, rows: 1, panes: []snapshotCapturePane{{id: "p", stableID: "p", sealed: view, tail: view.Tail(), transcript: transcript}}}}}
151
152	_, err := d.incrementalPublication(capture)
153	require.NoError(t, err)
154	entry, ok := sess.snapshotChunkCache.byPtr[chunk]
155	require.True(t, ok)
156	usedBefore := sess.snapshotChunkCache.used
157	ref := capture.sealedRefs[chunk]
158
159	markSnapshotCaptureObjectsPublished(capture)
160
161	require.NotContains(t, sess.snapshotChunkCache.byPtr, chunk)
162	require.Equal(t, usedBefore-len(entry.object.Data), sess.snapshotChunkCache.used)
163	require.Equal(t, ref, sess.snapshotChunkCache.persisted[chunk])
164	gotRef, object, err := sess.snapshotChunkCache.objectLocked(chunk)
165	require.NoError(t, err)
166	require.Equal(t, ref, gotRef)
167	require.Nil(t, object)
168
169	// Repeated acknowledgement must not release the same bytes twice.
170	markSnapshotCaptureObjectsPublished(capture)
171	require.Equal(t, usedBefore-len(entry.object.Data), sess.snapshotChunkCache.used)
172	require.Equal(t, ref, sess.snapshotChunkCache.persisted[chunk])
173}
174
175func TestIncrementalPublicationReusesSealedChunkObject(t *testing.T) {
176	history := vt.NewHistory(vt.HistoryConfig{MaxRows: 2, ChunkRows: 1})
177	appendHistoryRow(t, history, []renderer.Cell{renderer.BlankCell()})
178	view := history.SnapshotView()
179	transcript := vt.NewScreen(1, 1).RecoveryTranscriptSnapshot()
180	d := New(nil, nil, nil)
181	sess := newSnapshotTestSession(t, "work", false, "/work")
182	capture := &snapshotCapture{session: sess, name: "work", incarnation: domain.IncarnationID{1}, generation: 1, tabs: []snapshotCaptureTab{{stableID: "t", cols: 1, rows: 1, panes: []snapshotCapturePane{{id: "p", stableID: "p", sealed: view, tail: view.Tail(), transcript: transcript}}}}}
183	first, err := d.incrementalPublication(capture)
184	if err != nil {
185		t.Fatal(err)
186	}
187	firstRef := domain.CheckpointRef{Generation: first.Generation, ManifestDigest: snapcodec.ManifestDigest(first.Manifest)}
188	capture.checkpoint = firstRef
189	markSnapshotCaptureObjectsPublished(capture)
190	capture.generation = 2
191	capture.parentCheckpoint = &firstRef
192	second, err := d.incrementalPublication(capture)
193	if err != nil {
194		t.Fatal(err)
195	}
196	if len(first.Objects) == 0 || len(second.Objects) == 0 {
197		t.Fatal("publication omitted required objects")
198	}
199	require.Len(t, second.Objects, 2, "a generation must supply its tail and transcript once without resupplying persisted sealed chunks")
200	kinds := make(map[snapcodec.ObjectKind]int)
201	for _, object := range second.Objects {
202		kind, err := snapcodec.PreflightObject(object.Data)
203		require.NoError(t, err)
204		kinds[kind]++
205	}
206	require.Equal(t, map[snapcodec.ObjectKind]int{snapcodec.HistoryTail: 1, snapcodec.RecoveryTranscript: 1}, kinds)
207	require.Equal(t, &firstRef, second.ParentCheckpoint)
208	manifest, err := snapcodec.UnmarshalManifest(second.Manifest)
209	require.NoError(t, err)
210	require.Equal(t, &firstRef, manifest.ParentCheckpoint)
211	if got, want := sess.snapshotChunkCache.used, sess.snapshotChunkCache.limit; got > want {
212		t.Fatalf("cache bytes = %d, limit = %d", got, want)
213	}
214}

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