detached ece7682

kv_test.go RawHistoryFolder

1package kv
2
3import (
4	"encoding/binary"
5	"errors"
6	"os"
7	"path/filepath"
8	"testing"
9
10	"github.com/stretchr/testify/require"
11)
12
13func TestSyncCrashBoundaries(t *testing.T) {
14	t.Parallel()
15	tests := []struct {
16		name      string
17		leaveTmp  bool
18		corrupt   bool
19		wantOpen  bool
20		wantValue string
21	}{
22		{name: "clean file opens", wantOpen: true, wantValue: "v1"},
23		{name: "stray tmp is ignored", leaveTmp: true, wantOpen: true, wantValue: "v1"},
24		{name: "corrupt file fails closed", corrupt: true},
25	}
26	for _, tt := range tests {
27		t.Run(tt.name, func(t *testing.T) {
28			t.Parallel()
29			path := privateStorePath(t)
30			store, err := Open(path)
31			require.NoError(t, err)
32			require.NoError(t, store.Set([]byte("k"), []byte("v1")))
33			require.NoError(t, store.Sync())
34			require.NoError(t, store.Close())
35
36			if tt.leaveTmp {
37				require.NoError(t, os.WriteFile(path+".tmp", []byte("half written"), 0o600))
38			}
39			if tt.corrupt {
40				data, err := os.ReadFile(path)
41				require.NoError(t, err)
42				data[len(data)-1] ^= 0xff
43				require.NoError(t, os.WriteFile(path, data, 0o600))
44			}
45
46			reopened, err := Open(path)
47			if !tt.wantOpen {
48				require.ErrorIs(t, err, ErrCorrupt)
49				return
50			}
51			require.NoError(t, err)
52			t.Cleanup(func() { _ = reopened.Close() })
53			got, ok := reopened.Get([]byte("k"))
54			require.True(t, ok)
55			require.Equal(t, tt.wantValue, string(got))
56		})
57	}
58}
59
60func TestSyncIsAtomicAcrossKeys(t *testing.T) {
61	t.Parallel()
62	path := privateStorePath(t)
63	store, err := Open(path)
64	require.NoError(t, err)
65	require.NoError(t, store.Set([]byte("old"), []byte("x")))
66	require.NoError(t, store.Sync())
67	require.NoError(t, store.Delete([]byte("old")))
68	require.NoError(t, store.Set([]byte("new"), []byte("x")))
69	require.NoError(t, store.Sync())
70	require.NoError(t, store.Close())
71
72	reopened, err := Open(path)
73	require.NoError(t, err)
74	t.Cleanup(func() { _ = reopened.Close() })
75	_, hadOld := reopened.Get([]byte("old"))
76	_, hasNew := reopened.Get([]byte("new"))
77	require.False(t, hadOld)
78	require.True(t, hasNew)
79}
80
81func TestOpenFailsClosedWithoutMutation(t *testing.T) {
82	t.Parallel()
83	tests := []struct {
84		name string
85		raw  []byte
86	}{
87		{name: "empty", raw: []byte{}},
88		{name: "short magic", raw: []byte("VE")},
89		{name: "unknown magic", raw: func() []byte {
90			payload := append([]byte(nil), validEmptyFile()[:filePrefixLen]...)
91			copy(payload[:4], "NOPE")
92			return appendCRC(payload)
93		}()},
94		{name: "unknown version", raw: func() []byte {
95			payload := append([]byte(nil), validEmptyFile()[:filePrefixLen]...)
96			binary.BigEndian.PutUint16(payload[4:6], 99)
97			return appendCRC(payload)
98		}()},
99		{name: "truncated entry", raw: func() []byte {
100			payload := append([]byte(nil), validEmptyFile()[:filePrefixLen]...)
101			binary.BigEndian.PutUint32(payload[6:10], 1)
102			payload = append(payload, 0, 0, 0, 1)
103			return appendCRC(payload)
104		}()},
105	}
106	for _, tt := range tests {
107		t.Run(tt.name, func(t *testing.T) {
108			t.Parallel()
109			path := privateStorePath(t)
110			require.NoError(t, os.WriteFile(path, tt.raw, 0o600))
111			_, err := Open(path)
112			require.ErrorIs(t, err, ErrCorrupt)
113			after, readErr := os.ReadFile(path)
114			require.NoError(t, readErr)
115			require.Equal(t, tt.raw, after)
116		})
117	}
118}
119
120func TestCloseSyncsDirtyStore(t *testing.T) {
121	t.Parallel()
122	path := privateStorePath(t)
123	store, err := Open(path)
124	require.NoError(t, err)
125	require.NoError(t, store.Set([]byte("k"), []byte("v")))
126	require.NoError(t, store.Close())
127
128	reopened, err := Open(path)
129	require.NoError(t, err)
130	t.Cleanup(func() { _ = reopened.Close() })
131	got, ok := reopened.Get([]byte("k"))
132	require.True(t, ok)
133	require.Equal(t, []byte("v"), got)
134}
135
136func TestStoreCopiesValuesAndRangesInKeyOrder(t *testing.T) {
137	t.Parallel()
138	store, err := Open(privateStorePath(t))
139	require.NoError(t, err)
140	t.Cleanup(func() { _ = store.Close() })
141	value := []byte("two")
142	require.NoError(t, store.Set([]byte("b"), value))
143	value[0] = 'x'
144	require.NoError(t, store.Set([]byte("a"), []byte("one")))
145	got, ok := store.Get([]byte("b"))
146	require.True(t, ok)
147	require.Equal(t, []byte("two"), got)
148	got[0] = 'x'
149	gotAgain, _ := store.Get([]byte("b"))
150	require.Equal(t, []byte("two"), gotAgain)
151
152	var keys []string
153	store.Range(func(k, v []byte) bool {
154		keys = append(keys, string(k))
155		return true
156	})
157	require.Equal(t, []string{"a", "b"}, keys)
158}
159
160func TestStoreLockAndClosedErrors(t *testing.T) {
161	t.Parallel()
162	path := privateStorePath(t)
163	store, err := Open(path)
164	require.NoError(t, err)
165	_, err = Open(path)
166	require.Error(t, err)
167	require.NoError(t, store.Close())
168	require.NoError(t, store.Close())
169	require.ErrorIs(t, store.Set([]byte("k"), []byte("v")), os.ErrClosed)
170	require.ErrorIs(t, store.Delete([]byte("k")), os.ErrClosed)
171	require.ErrorIs(t, store.Sync(), os.ErrClosed)
172}
173
174func privateStorePath(t *testing.T) string {
175	t.Helper()
176	dir := filepath.Join(t.TempDir(), "vev")
177	require.NoError(t, os.Mkdir(dir, 0o700))
178	return filepath.Join(dir, "sessions.kv")
179}
180
181func validEmptyFile() []byte {
182	return encodeFile(map[string][]byte{})
183}
184
185func TestDecodeRejectsDuplicateKeysAndTrailingData(t *testing.T) {
186	t.Parallel()
187	payload := make([]byte, 0)
188	payload = append(payload, fileMagic[:]...)
189	payload = binary.BigEndian.AppendUint16(payload, fileVersion)
190	payload = binary.BigEndian.AppendUint32(payload, 2)
191	for range 2 {
192		payload = binary.BigEndian.AppendUint32(payload, 1)
193		payload = append(payload, 'k')
194		payload = binary.BigEndian.AppendUint32(payload, 1)
195		payload = append(payload, 'v')
196	}
197	raw := appendCRC(payload)
198	_, err := decodeFile(raw)
199	require.ErrorIs(t, err, ErrCorrupt)
200
201	payload = append([]byte(nil), validEmptyFile()[:filePrefixLen]...)
202	payload = append(payload, 0)
203	_, err = decodeFile(appendCRC(payload))
204	require.True(t, errors.Is(err, ErrCorrupt))
205}
206
207func TestCloseWithoutSyncPreventsLaterCloseFromSyncing(t *testing.T) {
208	t.Parallel()
209	path := privateStorePath(t)
210	store, err := Open(path)
211	require.NoError(t, err)
212	require.NoError(t, store.Set([]byte("rejected"), []byte("value")))
213	require.NoError(t, store.CloseWithoutSync())
214	require.NoError(t, store.Close(), "a later normal Close must remain a no-op")
215
216	reopened, err := Open(path)
217	require.NoError(t, err)
218	t.Cleanup(func() { _ = reopened.Close() })
219	_, ok := reopened.Get([]byte("rejected"))
220	require.False(t, ok)
221}

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