machine_test.gno
6.87 Kb · 236 lines
1package bf
2
3import (
4 "testing"
5
6 "gno.land/p/moul/x/vm/vmkit/v0"
7 "gno.land/p/nt/uassert/v0"
8)
9
10// oneShot runs src to completion and returns what it wrote.
11func oneShot(t *testing.T, src string) string {
12 t.Helper()
13 h := vmkit.NewTestHost()
14 status, err := Run(src, h)
15 uassert.NoError(t, err)
16 uassert.Equal(t, "halted", status.String())
17 return h.OutString()
18}
19
20// TestSlicedRunEqualsOneShot is the property that makes a guest program a
21// contract rather than a function call: chopping the execution into fuel
22// slices, snapshotting between each, must produce exactly the single run.
23func TestSlicedRunEqualsOneShot(t *testing.T) {
24 for _, slice := range []int64{1, 2, 7, 64} {
25 prog, err := CompileDefault(hello)
26 uassert.NoError(t, err)
27
28 h := vmkit.NewTestHost()
29 snap := []byte(nil)
30 status := vmkit.Running
31 slices := 0
32 for status == vmkit.Running && slices < 10000 {
33 m := NewMachine(prog) // a realm holds bytes, not a machine
34 if snap != nil {
35 uassert.NoError(t, m.Restore(snap))
36 }
37 _, status = m.Step(h, slice)
38 snap = m.Snapshot()
39 slices++
40 }
41 uassert.Equal(t, "halted", status.String())
42 uassert.Equal(t, "Hello World", h.OutString())
43 }
44}
45
46func TestSlicedRunAgreesAcrossTheCorpus(t *testing.T) {
47 for _, tc := range corpus {
48 prog, err := CompileDefault(tc.src)
49 uassert.NoError(t, err)
50 if prog == nil {
51 continue
52 }
53 h := vmkit.NewTestHost()
54 snap := []byte(nil)
55 status := vmkit.Running
56 n := 0
57 for status == vmkit.Running && n < 20000 {
58 m := NewMachine(prog)
59 if snap != nil {
60 uassert.NoError(t, m.Restore(snap))
61 }
62 _, status = m.Step(h, 3)
63 snap = m.Snapshot()
64 n++
65 }
66 if h.OutString() != tc.want {
67 t.Errorf("%s sliced: got %q, want %q", tc.name, h.OutString(), tc.want)
68 }
69 }
70}
71
72func TestFuelStopsTheMachine(t *testing.T) {
73 prog, err := CompileDefault(hello)
74 uassert.NoError(t, err)
75 m := NewMachine(prog)
76 h := vmkit.NewTestHost()
77
78 used, status := m.Step(h, 5)
79 uassert.Equal(t, int64(5), used)
80 uassert.Equal(t, "running", status.String())
81 uassert.Equal(t, "", h.OutString()) // not far enough to print anything
82}
83
84// TestScanIsChargedPerCell pins the thing that would otherwise be a way to
85// buy unbounded work for one unit: a scan walks one cell per unit of fuel,
86// and stopping mid-scan resumes the same walk.
87func TestScanIsChargedPerCell(t *testing.T) {
88 // 20 non-zero cells, then scan left back to the zero at cell 0.
89 src := ">+>+>+>+>+>+>+>+>+>+>+>+>+>+>+>+>+>+>+>+[<]"
90 prog, err := CompileDefault(src)
91 uassert.NoError(t, err)
92
93 m := NewMachine(prog)
94 h := vmkit.NewTestHost()
95 used, status := m.Step(h, 45)
96 uassert.Equal(t, int64(45), used)
97 uassert.Equal(t, "running", status.String())
98 uassert.True(t, m.Pointer() > 0) // stopped part way down the scan
99
100 // Resuming finishes the same walk and lands on the zero cell.
101 _, status = m.Step(h, vmkit.Unmetered)
102 uassert.Equal(t, "halted", status.String())
103 uassert.Equal(t, 0, m.Pointer())
104}
105
106func TestSnapshotRoundTrip(t *testing.T) {
107 prog, err := CompileDefault(hello)
108 uassert.NoError(t, err)
109
110 a := NewMachine(prog)
111 h := vmkit.NewTestHost()
112 a.Step(h, 40)
113
114 b := NewMachine(prog)
115 uassert.NoError(t, b.Restore(a.Snapshot()))
116 uassert.Equal(t, a.PC(), b.PC())
117 uassert.Equal(t, a.Pointer(), b.Pointer())
118 uassert.Equal(t, a.Touched(), b.Touched())
119 for i := 0; i < a.Touched(); i++ {
120 uassert.Equal(t, uint64(a.Cell(i)), uint64(b.Cell(i)))
121 }
122 uassert.Equal(t, string(a.Snapshot()), string(b.Snapshot()))
123}
124
125// TestSnapshotIsTrimmed is the continuation kill criterion in test form: if a
126// pause carried the whole 30,000 byte tape, resuming would cost more than
127// re-running and continuations would be theater.
128func TestSnapshotIsTrimmed(t *testing.T) {
129 m, err := Load(hello)
130 uassert.NoError(t, err)
131 h := vmkit.NewTestHost()
132 m.Step(h, vmkit.Unmetered)
133
134 uassert.Equal(t, 5, m.Touched()) // hello world uses five cells
135 snap := m.Snapshot()
136 // 4 magic + 1 version + 3x8 zig-zag ints + 1 status + 4 empty trap
137 // + 4 length + 5 tape. The README quotes this number.
138 uassert.Equal(t, 43, len(snap))
139 uassert.True(t, len(snap) < TapeSize/100)
140}
141
142func TestRestoreRejectsJunk(t *testing.T) {
143 prog, err := CompileDefault(hello)
144 uassert.NoError(t, err)
145 good := NewMachine(prog)
146 good.Step(vmkit.NewTestHost(), 20)
147 snap := good.Snapshot()
148
149 cases := []struct {
150 name string
151 b []byte
152 }{
153 {"empty", []byte{}},
154 {"truncated", snap[:len(snap)-3]},
155 {"bad magic", append([]byte{0, 0, 0, 0}, snap[4:]...)},
156 }
157 for _, tc := range cases {
158 m := NewMachine(prog)
159 if err := m.Restore(tc.b); err == nil {
160 t.Errorf("Restore(%s) should have failed", tc.name)
161 }
162 }
163
164 // A snapshot with a wrong version byte is refused rather than decoded
165 // into a plausible-looking wrong state.
166 bad := make([]byte, len(snap))
167 copy(bad, snap)
168 bad[4] = 99
169 m := NewMachine(prog)
170 uassert.ErrorIs(t, m.Restore(bad), vmkit.ErrBadSnapshot)
171}
172
173func TestRestoreRejectsAnOutOfRangePC(t *testing.T) {
174 small, err := CompileDefault("+")
175 uassert.NoError(t, err)
176 big, err := CompileDefault(hello)
177 uassert.NoError(t, err)
178
179 m := NewMachine(big)
180 m.Step(vmkit.NewTestHost(), 30)
181 snap := m.Snapshot()
182
183 // The same snapshot against a two-op program points past the end.
184 other := NewMachine(small)
185 uassert.ErrorIs(t, other.Restore(snap), vmkit.ErrBadSnapshot)
186}
187
188func TestHaltedMachineStaysHalted(t *testing.T) {
189 m, err := Load("+++.")
190 uassert.NoError(t, err)
191 h := vmkit.NewTestHost()
192 m.Step(h, vmkit.Unmetered)
193 uassert.Equal(t, "halted", m.Status().String())
194
195 used, status := m.Step(h, vmkit.Unmetered)
196 uassert.Equal(t, int64(0), used)
197 uassert.Equal(t, "halted", status.String())
198 uassert.Equal(t, "\x03", h.OutString()) // still only one write
199}
200
201func TestLoadRejectsBadSource(t *testing.T) {
202 _, err := Load("[[")
203 uassert.Error(t, err)
204 status, err := Run("]", vmkit.NewTestHost())
205 uassert.Error(t, err)
206 uassert.Equal(t, "trapped", status.String())
207}
208
209// TestInstanceIntegration wires the machine through vmkit's realm-facing
210// plumbing, which is the shape the demo realm uses.
211func TestInstanceIntegration(t *testing.T) {
212 prog, err := CompileDefault(hello)
213 uassert.NoError(t, err)
214
215 inst := vmkit.NewInstance("001", address("g1x"), VMName, []byte(hello), vmkit.Unmetered)
216 h := vmkit.NewTestHost()
217 for i := 0; i < 200 && inst.Status == vmkit.Running; i++ {
218 uassert.NoError(t, inst.Run(NewMachine(prog), h, 8))
219 }
220 uassert.Equal(t, "halted", inst.Status.String())
221 uassert.Equal(t, "Hello World", h.OutString())
222 uassert.True(t, inst.Slices > 1)
223 uassert.True(t, inst.FuelUsed > 0)
224}
225
226func TestInstanceHonoursItsBudget(t *testing.T) {
227 prog, err := CompileDefault(hello)
228 uassert.NoError(t, err)
229
230 inst := vmkit.NewInstance("002", address("g1x"), VMName, []byte(hello), 10)
231 h := vmkit.NewTestHost()
232 uassert.NoError(t, inst.Run(NewMachine(prog), h, vmkit.Unmetered))
233 uassert.Equal(t, "out of fuel", inst.Status.String())
234 uassert.Equal(t, int64(10), inst.FuelUsed)
235 uassert.ErrorIs(t, inst.Run(NewMachine(prog), h, vmkit.Unmetered), vmkit.ErrBudgetExhausted)
236}