detached 5041353

systemview_asset_cache_test.go RawHistoryFolder

1package cef
2
3// P2.1 tests: the bundle loads its WASM exactly once (success or failure),
4// with the hardened semantics — compressed authoritative, raw fallback only
5// on absence, raw reads bounded, failures cached.
6
7import (
8	"errors"
9	"io"
10	"io/fs"
11	"sync"
12	"testing"
13	"testing/fstest"
14
15	"github.com/stretchr/testify/require"
16)
17
18// countingFS counts Open and ReadFile calls per name. Note MapFS
19// implements ReadFileFS, so fs.ReadFile bypasses Open: both entry points
20// must be counted explicitly.
21type countingFS struct {
22	inner fstest.MapFS
23	mu    sync.Mutex
24	opens map[string]int
25}
26
27func (c *countingFS) record(name string) {
28	c.mu.Lock()
29	defer c.mu.Unlock()
30	if c.opens == nil {
31		c.opens = map[string]int{}
32	}
33	c.opens[name]++
34}
35
36func (c *countingFS) Open(name string) (fs.File, error) {
37	c.record("open:" + name)
38	return c.inner.Open(name)
39}
40
41func (c *countingFS) ReadFile(name string) ([]byte, error) {
42	c.record("readfile:" + name)
43	return c.inner.ReadFile(name)
44}
45
46// compressedReads counts compressed WASM reads through either entry
47// point: the loader may Open the file directly (bounded reads) or use
48// the ReadFileFS shortcut, depending on the hardened path.
49func (c *countingFS) compressedReads() int {
50	c.mu.Lock()
51	defer c.mu.Unlock()
52	name := systemviewsWASMPath + ".br"
53	return c.opens["readfile:"+name] + c.opens["open:"+name]
54}
55
56// denyFS fails every Open with a fixed error (e.g. permission denied).
57type denyFS struct{ err error }
58
59func (d denyFS) Open(name string) (fs.File, error) {
60	return nil, &fs.PathError{Op: "open", Path: name, Err: d.err}
61}
62
63func TestSystemviewAssetCache_DecodesOnceUnderConcurrency(t *testing.T) {
64	t.Parallel()
65
66	wasm := []byte("\x00asm-bundle-once")
67	fsys := &countingFS{inner: fstest.MapFS{
68		"systemviews/systemviews.wasm.br": {Data: brotliCompressForTest(t, wasm)},
69	}}
70	b := newSystemviewAssetBundle(fsys)
71
72	const readers = 8
73	got := make([][]byte, readers)
74	errs := make([]error, readers)
75	var wg sync.WaitGroup
76	for i := range got {
77		wg.Add(1)
78		go func(i int) {
79			defer wg.Done()
80			got[i], errs[i] = b.WASM()
81		}(i)
82	}
83	wg.Wait()
84	for i := range got {
85		require.NoError(t, errs[i])
86		require.Equal(t, wasm, got[i])
87		require.Same(t, &got[0][0], &got[i][0], "waiters must share one immutable buffer")
88	}
89	require.Equal(t, 1, fsys.compressedReads(), "bundle must decode exactly once")
90}
91
92func TestSystemviewAssetCache_CachesFailureWithoutRetryStorm(t *testing.T) {
93	t.Parallel()
94
95	fsys := &countingFS{inner: fstest.MapFS{
96		"systemviews/systemviews.wasm.br": {Data: []byte("not-brotli")},
97		"systemviews/systemviews.wasm":    {Data: []byte("\x00asm-valid-raw")},
98	}}
99	b := newSystemviewAssetBundle(fsys)
100
101	_, err := b.WASM()
102	require.Error(t, err, "corrupt compressed WASM must fail even with valid raw present")
103	firstReads := fsys.compressedReads()
104	_, err = b.WASM()
105	require.Error(t, err)
106	require.Equal(t, firstReads, fsys.compressedReads(), "cached failure must not re-read")
107}
108
109func TestSystemviewAssetCache_CompressedReadErrorIsExplicit(t *testing.T) {
110	t.Parallel()
111
112	// Permission (or any non-absence) failures reading the compressed file
113	// must not silently fall back to raw.
114	b := newSystemviewAssetBundle(denyFS{err: fs.ErrPermission})
115
116	_, err := b.WASM()
117	require.Error(t, err)
118	require.ErrorIs(t, err, fs.ErrPermission, "must surface the read failure")
119	require.NotErrorIs(t, err, fs.ErrNotExist)
120}
121
122func TestSystemviewAssetCache_RawFallbackOnlyOnAbsence(t *testing.T) {
123	t.Parallel()
124
125	raw := []byte("\x00asm-raw-fallback")
126	b := newSystemviewAssetBundle(fstest.MapFS{
127		"systemviews/systemviews.wasm": {Data: raw},
128	})
129
130	data, err := b.WASM()
131	require.NoError(t, err)
132	require.Equal(t, raw, data)
133}
134
135func TestSystemviewAssetCache_RawOversizedRejected(t *testing.T) {
136	t.Parallel()
137
138	big := make([]byte, maxSystemviewsWASMBytes+1)
139	b := newSystemviewAssetBundle(fstest.MapFS{
140		"systemviews/systemviews.wasm": {Data: big},
141	})
142
143	_, err := b.WASM()
144	require.Error(t, err, "raw WASM past the bound must fail like the compressed path")
145}
146
147func TestSystemviewAssetCache_MissingBothReportsNotExist(t *testing.T) {
148	t.Parallel()
149
150	b := newSystemviewAssetBundle(fstest.MapFS{})
151
152	_, err := b.WASM()
153	require.Error(t, err)
154	require.ErrorIs(t, err, fs.ErrNotExist)
155}
156
157func TestSystemviewAssetCache_NilFSFailsCleanly(t *testing.T) {
158	t.Parallel()
159
160	b := newSystemviewAssetBundle(nil)
161	_, err := b.WASM()
162	require.Error(t, err)
163}
164
165// zeroFile yields size bytes of zeros without allocating them, proving
166// input bounds without a 64 MiB fixture in memory.
167type zeroFile struct {
168	remaining int64
169}
170
171func (f *zeroFile) Read(p []byte) (int, error) {
172	if f.remaining <= 0 {
173		return 0, io.EOF
174	}
175	n := min(int64(len(p)), f.remaining)
176	clear(p[:n])
177	f.remaining -= n
178	return int(n), nil
179}
180
181func (f *zeroFile) Close() error { return nil }
182
183func (f *zeroFile) Stat() (fs.FileInfo, error) { return nil, errors.New("zeroFile has no stat") }
184
185// oversizedInputFS serves an unbounded-looking compressed file: the
186// bundle must reject it on size before allocation.
187type oversizedInputFS struct {
188	fstest.MapFS
189}
190
191func (o oversizedInputFS) Open(name string) (fs.File, error) {
192	if name == systemviewsWASMPath+".br" {
193		return &zeroFile{remaining: int64(maxSystemviewsWASMBytes) + 1}, nil
194	}
195	return o.MapFS.Open(name)
196}
197
198func TestSystemviewAssetCache_OversizedCompressedInputRejected(t *testing.T) {
199	t.Parallel()
200
201	b := newSystemviewAssetBundle(oversizedInputFS{fstest.MapFS{
202		"systemviews/systemviews.wasm": {Data: []byte("\x00asm-valid-raw")},
203	}})
204	_, err := b.WASM()
205	require.Error(t, err, "oversized compressed input must fail before allocation")
206	require.Contains(t, err.Error(), "exceeds")
207}
208
209func TestSystemviewAssetCache_DigestCachedAtLoad(t *testing.T) {
210	t.Parallel()
211
212	wasm := []byte("\x00asm-digest")
213	b := newSystemviewAssetBundle(fstest.MapFS{
214		"systemviews/systemviews.wasm.br": {Data: brotliCompressForTest(t, wasm)},
215	})
216	digest, err := b.WASMDigest()
217	require.NoError(t, err)
218	require.Equal(t, sha256Hex(wasm), digest, "cached digest must match the decoded bytes")
219
220	// A failing bundle reports the load error instead of an empty digest.
221	bad := newSystemviewAssetBundle(denyFS{err: fs.ErrPermission})
222	_, err = bad.WASMDigest()
223	require.Error(t, err)
224}

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 8

Tags 42

↑↓ moveenter openesc close