Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}