okami.mom/ultraviolet

git clone https://git.nkp.pet/okami.mom/ultraviolet

Kieran Klukasfix(conformance): keep decoding after an op a zero-height screen cannot take5e21978

main
6.2 KiB203 linesraw
1package conformance
2
3import (
4	"os"
5	"path/filepath"
6	"strconv"
7	"strings"
8	"testing"
9)
10
11// corpusProgram decodes a saved fuzz entry the way the fuzz target would.
12func corpusProgram(t *testing.T, path string) (Program, bool) {
13	t.Helper()
14	raw, err := os.ReadFile(path)
15	if err != nil {
16		t.Fatalf("read %s: %v", path, err)
17	}
18	for _, line := range strings.Split(string(raw), "\n") {
19		line = strings.TrimSpace(line)
20		if !strings.HasPrefix(line, "[]byte(") {
21			continue
22		}
23		quoted := strings.TrimSuffix(strings.TrimPrefix(line, "[]byte("), ")")
24		data, err := strconv.Unquote(quoted)
25		if err != nil {
26			continue
27		}
28		return DecodeProgram([]byte(data)), true
29	}
30	return Program{}, false
31}
32
33// TestCorpusDecodesStably pins what every saved entry decodes to.
34//
35// A saved entry is only a regression test for as long as it keeps performing
36// the operations it was minimised for. Choosing an operation by a count that
37// grows re-pointed every byte the moment a kind was added, so entries kept
38// passing while testing something else entirely. This is the alarm for that:
39// if it fires, the decoder's mapping moved, and the entries have to be
40// re-minted deliberately rather than left to drift.
41func TestCorpusDecodesStably(t *testing.T) {
42	golden := map[string]string{}
43	raw, err := os.ReadFile("testdata/corpus_decode.txt")
44	if err != nil {
45		t.Fatalf("read golden: %v", err)
46	}
47	for _, line := range strings.Split(strings.TrimSpace(string(raw)), "\n") {
48		if line == "" || strings.HasPrefix(line, "#") {
49			continue
50		}
51		name, ops, found := strings.Cut(line, "\t")
52		if !found {
53			t.Fatalf("malformed golden line %q", line)
54		}
55		golden[name] = ops
56	}
57
58	seeds, err := filepath.Glob("testdata/fuzz/*/*")
59	if err != nil {
60		t.Fatal(err)
61	}
62	seen := map[string]bool{}
63	for _, path := range seeds {
64		p, ok := corpusProgram(t, path)
65		if !ok {
66			continue
67		}
68		name := filepath.Join(filepath.Base(filepath.Dir(path)), filepath.Base(path))
69		var kinds []string
70		for _, op := range p.Ops {
71			kinds = append(kinds, op.Kind.String())
72		}
73		got := strings.Join(kinds, ",")
74		seen[name] = true
75		want, recorded := golden[name]
76		if !recorded {
77			t.Errorf("%s is not in testdata/corpus_decode.txt; add it with:\n\t%s\t%s", name, name, got)
78			continue
79		}
80		if got != want {
81			t.Errorf("%s decodes to a different program than it was minimised for\n got: %s\nwant: %s", name, got, want)
82		}
83	}
84	for name := range golden {
85		if !seen[name] {
86			t.Errorf("%s is recorded in testdata/corpus_decode.txt but no longer exists", name)
87		}
88	}
89}
90
91// TestOpTableCoversEveryKind keeps the table honest: a kind the table never
92// names is a kind the fuzzer can never generate, which is a silent hole in
93// coverage rather than a failure anyone would notice.
94func TestOpTableCoversEveryKind(t *testing.T) {
95	seen := map[OpKind]int{}
96	for _, kind := range opTable {
97		seen[kind]++
98	}
99	for kind := OpKind(0); kind < opKindCount; kind++ {
100		if seen[kind] == 0 {
101			t.Errorf("%s is never reachable: no slot in opTable names it", kind)
102		}
103	}
104	if len(seen) != int(opKindCount) {
105		t.Errorf("opTable names %d kinds, but %d exist", len(seen), opKindCount)
106	}
107}
108
109// TestSeedsDecodeToTheProgramsTheyDescribe checks that the hand-written seeds
110// still mean what their comments say.
111//
112// A seed is a literal byte slice, so nothing connects it to the program it is
113// meant to express. Two silent divergences have already happened: the op bytes
114// were the enum's numeric values, which stopped selecting those ops when the
115// decoder moved to a slot table, and the resize heights were encoded against the
116// fullscreen low bound for the inline modes too, so every inline resize in them
117// asked for a different size than intended. Both left the seeds passing while
118// covering less than they claimed.
119func TestSeedsDecodeToTheProgramsTheyDescribe(t *testing.T) {
120	for i, seed := range Seeds() {
121		p := DecodeProgram(seed)
122		if len(p.Ops) == 0 {
123			t.Errorf("seed %d decodes to no operations at all", i)
124			continue
125		}
126
127		// Every seed exists to reach a render; one that cannot is inert.
128		renders := 0
129		for _, op := range p.Ops {
130			if op.Kind == OpRender || op.Kind == OpRedraw {
131				renders++
132			}
133		}
134		if renders == 0 {
135			t.Errorf("seed %d never renders, so nothing it sets up is ever tested: %v", i, kindsOf(p))
136		}
137
138		// A seed that resizes has to land inside the bounds the decoder
139		// promises, or it is exercising a clamp rather than the case it names.
140		for _, op := range p.Ops {
141			if op.Kind != OpResize {
142				continue
143			}
144			minH := MinResizeH
145			if p.Inline {
146				minH = 0
147			}
148			if op.W < MinResizeW || op.W > MaxResizeW {
149				t.Errorf("seed %d resizes to width %d, outside [%d,%d]", i, op.W, MinResizeW, MaxResizeW)
150			}
151			if op.H < minH || op.H > MaxResizeH {
152				t.Errorf("seed %d (inline=%v) resizes to height %d, outside [%d,%d]",
153					i, p.Inline, op.H, minH, MaxResizeH)
154			}
155		}
156	}
157}
158
159func kindsOf(p Program) []string {
160	var out []string
161	for _, op := range p.Ops {
162		out = append(out, op.Kind.String())
163	}
164	return out
165}
166
167// TestCollapseThenDrawIsReachable checks that an op a collapsed frame cannot
168// express does not end the program.
169//
170// Drawing needs a row, so a frame collapsed to nothing cannot take a DrawLine.
171// That used to end decoding, which made everything after a collapse unreachable:
172// the fuzzer could shrink an inline frame to zero but never reach the redraw
173// that proves it recovered, and the collapse is the case inline erasing is most
174// likely to get wrong.
175func TestCollapseThenDrawIsReachable(t *testing.T) {
176	width := byte((20 - MinResizeW) % (MaxResizeW - MinResizeW + 1))
177	program := []byte{
178		14, 2, // 20x4
179		4,                          // inline
180		opByte(OpResize), width, 0, // collapse to no rows
181		opByte(OpDrawLine), 0, 0, byte(len(corpusAlphabet)), // cannot be expressed
182		opByte(OpResize), width, 3, // grow back
183		opByte(OpRender), // has to still be reachable
184	}
185
186	p := DecodeProgram(program)
187
188	var collapsed, renderedAfter bool
189	for _, op := range p.Ops {
190		if op.Kind == OpResize && op.H == 0 {
191			collapsed = true
192		}
193		if collapsed && op.Kind == OpRender {
194			renderedAfter = true
195		}
196	}
197	if !collapsed {
198		t.Fatalf("expected the program to collapse the frame, got %v", kindsOf(p))
199	}
200	if !renderedAfter {
201		t.Errorf("decoding stopped at the collapsed frame, so its recovery cannot be fuzzed: %v", kindsOf(p))
202	}
203}