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

vmkit_test.gno

8.94 Kb · 312 lines
  1package vmkit
  2
  3import (
  4	"testing"
  5
  6	"gno.land/p/nt/uassert/v0"
  7)
  8
  9// counter is a toy [Machine] used to test the kit itself without dragging in
 10// a real guest VM: it writes one byte per step and halts after `total` of
 11// them. Anything that works here is a property of the ABI, not of brainfuck.
 12type counter struct {
 13	total int64
 14	done  int64
 15	trap  string
 16}
 17
 18func (c *counter) Step(h Host, fuel int64) (int64, Status) {
 19	m := NewMeter(fuel)
 20	var used int64
 21	for c.done < c.total {
 22		if !m.Charge(1) {
 23			return used, Running
 24		}
 25		used++
 26		c.done++
 27		h.Output([]byte{byte('a' + (c.done-1)%26)})
 28	}
 29	return used, Halted
 30}
 31
 32func (c *counter) Snapshot() []byte {
 33	w := NewWriter(24)
 34	w.Int(c.total)
 35	w.Int(c.done)
 36	w.String(c.trap)
 37	return w.Out()
 38}
 39
 40func (c *counter) Restore(b []byte) error {
 41	r := NewReader(b)
 42	total := r.Int()
 43	done := r.Int()
 44	trap := r.String()
 45	if err := r.Err(); err != nil {
 46		return err
 47	}
 48	c.total, c.done, c.trap = total, done, trap
 49	return nil
 50}
 51
 52func (c *counter) Trap() string { return c.trap }
 53
 54func TestStatusString(t *testing.T) {
 55	cases := []struct {
 56		s    Status
 57		want string
 58		done bool
 59	}{
 60		{Running, "running", false},
 61		{Halted, "halted", true},
 62		{Trapped, "trapped", true},
 63		{OutOfFuel, "out of fuel", true},
 64		{Status(99), "unknown", true},
 65	}
 66	for _, tc := range cases {
 67		uassert.Equal(t, tc.want, tc.s.String())
 68		uassert.Equal(t, tc.done, tc.s.Done())
 69	}
 70}
 71
 72func TestMeterCharge(t *testing.T) {
 73	m := NewMeter(10)
 74	uassert.True(t, m.Charge(4))
 75	uassert.Equal(t, int64(4), m.Used())
 76	uassert.Equal(t, int64(6), m.Remaining())
 77
 78	// A charge that does not fit spends nothing, so the caller can stop
 79	// before the instruction it cannot pay for.
 80	uassert.False(t, m.Charge(7))
 81	uassert.Equal(t, int64(4), m.Used())
 82
 83	uassert.True(t, m.Charge(6))
 84	uassert.True(t, m.Exhausted())
 85	uassert.False(t, m.Charge(1))
 86}
 87
 88func TestMeterUnmetered(t *testing.T) {
 89	m := NewMeter(Unmetered)
 90	uassert.True(t, m.Charge(1 << 40))
 91	uassert.False(t, m.Exhausted())
 92	uassert.Equal(t, Unmetered, m.Remaining())
 93}
 94
 95func TestMeterZeroBudget(t *testing.T) {
 96	m := NewMeter(0)
 97	uassert.True(t, m.Exhausted())
 98	uassert.False(t, m.Charge(1))
 99	uassert.Equal(t, int64(0), m.Used())
100}
101
102func TestCodecRoundTrip(t *testing.T) {
103	w := NewWriter(0)
104	w.Byte(0xab)
105	w.Uint32(0xdeadbeef)
106	w.Uint64(0x0102030405060708)
107	w.Int(-1234567)
108	w.Int(0)
109	w.Int(1234567)
110	w.Bytes([]byte{0, 1, 2, 255})
111	w.String("gno.land")
112
113	r := NewReader(w.Out())
114	uassert.Equal(t, uint64(0xab), uint64(r.Byte()))
115	uassert.Equal(t, uint64(0xdeadbeef), uint64(r.Uint32()))
116	uassert.Equal(t, uint64(0x0102030405060708), r.Uint64())
117	uassert.Equal(t, int64(-1234567), r.Int())
118	uassert.Equal(t, int64(0), r.Int())
119	uassert.Equal(t, int64(1234567), r.Int())
120	uassert.Equal(t, 4, len(r.Bytes()))
121	uassert.Equal(t, "gno.land", r.String())
122	uassert.NoError(t, r.Err())
123	uassert.Equal(t, 0, r.Remaining())
124}
125
126func TestCodecIsCanonical(t *testing.T) {
127	// The same state must always produce the same bytes: a snapshot is
128	// consensus state, so two nodes encoding it differently is a fork.
129	build := func() []byte {
130		w := NewWriter(0)
131		w.Int(-7)
132		w.String("x")
133		w.Uint32(9)
134		return w.Out()
135	}
136	uassert.Equal(t, string(build()), string(build()))
137}
138
139func TestCodecTruncated(t *testing.T) {
140	r := NewReader([]byte{1, 2})
141	r.Uint64()
142	uassert.ErrorIs(t, r.Err(), ErrTruncated)
143	// Once latched, every later read is a zero value and the error stands.
144	uassert.Equal(t, "", r.String())
145	uassert.ErrorIs(t, r.Err(), ErrTruncated)
146}
147
148func TestCodecBytesAreCopied(t *testing.T) {
149	w := NewWriter(0)
150	w.Bytes([]byte{1, 2, 3})
151	buf := w.Out()
152	r := NewReader(buf)
153	got := r.Bytes()
154	buf[len(buf)-1] = 99 // mutate the snapshot under the reader
155	uassert.Equal(t, 3, len(got))
156	uassert.Equal(t, uint64(3), uint64(got[2]))
157}
158
159func TestTestHostStorage(t *testing.T) {
160	h := NewTestHost()
161	uassert.Equal(t, 0, len(h.Get([]byte("missing"))))
162
163	h.Set([]byte{0, 1}, []byte("zero-prefixed"))
164	h.Set([]byte("k"), []byte("v"))
165	uassert.Equal(t, "zero-prefixed", string(h.Get([]byte{0, 1})))
166	uassert.Equal(t, "v", string(h.Get([]byte("k"))))
167
168	// Keys are hex so a zero byte survives, and sorted so a test can pin
169	// them.
170	keys := h.Keys()
171	uassert.Equal(t, 2, len(keys))
172	uassert.Equal(t, "0001", keys[0])
173	uassert.Equal(t, "6b", keys[1])
174}
175
176func TestTestHostStorageIsCopied(t *testing.T) {
177	h := NewTestHost()
178	val := []byte("abc")
179	h.Set([]byte("k"), val)
180	val[0] = 'z'
181	uassert.Equal(t, "abc", string(h.Get([]byte("k"))))
182
183	got := h.Get([]byte("k"))
184	got[0] = 'z'
185	uassert.Equal(t, "abc", string(h.Get([]byte("k"))))
186}
187
188func TestTestHostSendNeedsAGrant(t *testing.T) {
189	to := address("g1manfred47kzduec920z88wfr64ylksmdcedlf5")
190
191	h := NewTestHost()
192	uassert.ErrorIs(t, h.Send(to, 100), ErrNotGranted)
193	uassert.Equal(t, 0, len(h.Sends()))
194
195	h.Grant(150)
196	uassert.NoError(t, h.Send(to, 100))
197	uassert.Equal(t, 1, len(h.Sends()))
198	uassert.Equal(t, int64(100), h.Sends()[0].Amount)
199
200	// The grant is a budget, not a switch.
201	uassert.ErrorIs(t, h.Send(to, 100), ErrNotGranted)
202	uassert.NoError(t, h.Send(to, 50))
203}
204
205func TestTestHostEventsAndInput(t *testing.T) {
206	h := NewTestHost().WithInput([]byte("hi")).WithHeight(42).WithTime(1700000000)
207	uassert.Equal(t, "hi", string(h.Input()))
208	uassert.Equal(t, int64(42), h.Height())
209	uassert.Equal(t, int64(1700000000), h.Now())
210
211	h.Emit("run", "id", "1", "status", "halted")
212	h.Emit("odd", "dangling")
213	uassert.Equal(t, 2, len(h.Events()))
214	uassert.Equal(t, "run id=1 status=halted", h.Events()[0])
215	uassert.Equal(t, "odd", h.Events()[1]) // odd trailing element dropped
216}
217
218func TestInstanceRunsToCompletion(t *testing.T) {
219	inst := NewInstance("1", address("g1x"), "counter", []byte("5"), Unmetered)
220	h := NewTestHost()
221	uassert.NoError(t, inst.Run(&counter{total: 5}, h, Unmetered))
222	uassert.Equal(t, "halted", inst.Status.String())
223	uassert.Equal(t, int64(5), inst.FuelUsed)
224	uassert.Equal(t, int64(1), inst.Slices)
225	uassert.Equal(t, "abcde", h.OutString())
226}
227
228func TestInstanceResumesAcrossSlices(t *testing.T) {
229	// The property that makes a guest program a contract: five slices of
230	// one unit must equal one slice of five.
231	inst := NewInstance("1", address("g1x"), "counter", []byte("5"), Unmetered)
232	h := NewTestHost()
233	for i := 0; i < 5; i++ {
234		// A realm holds bytes, not a machine: every slice loads a fresh
235		// one from the program and lets Restore carry the state over.
236		uassert.NoError(t, inst.Run(&counter{total: 5}, h, 1))
237	}
238	uassert.Equal(t, "halted", inst.Status.String())
239	uassert.Equal(t, int64(5), inst.FuelUsed)
240	uassert.Equal(t, int64(5), inst.Slices)
241	uassert.Equal(t, "abcde", h.OutString())
242}
243
244func TestInstanceStopsAtItsBudget(t *testing.T) {
245	inst := NewInstance("1", address("g1x"), "counter", []byte("10"), 3)
246	h := NewTestHost()
247	uassert.NoError(t, inst.Run(&counter{total: 10}, h, Unmetered))
248	uassert.Equal(t, "out of fuel", inst.Status.String())
249	uassert.Equal(t, int64(3), inst.FuelUsed)
250	uassert.Equal(t, int64(0), inst.Remaining())
251	uassert.Equal(t, "abc", h.OutString())
252
253	// A second call has nothing to spend and says so instead of looping.
254	uassert.ErrorIs(t, inst.Run(&counter{}, h, Unmetered), ErrBudgetExhausted)
255}
256
257func TestInstanceSliceClamps(t *testing.T) {
258	inst := NewInstance("1", address("g1x"), "counter", nil, 10)
259	inst.FuelUsed = 7
260	uassert.Equal(t, int64(3), inst.Slice(100))
261	uassert.Equal(t, int64(2), inst.Slice(2))
262	uassert.Equal(t, int64(3), inst.Slice(Unmetered))
263
264	open := NewInstance("2", address("g1x"), "counter", nil, Unmetered)
265	uassert.Equal(t, int64(5), open.Slice(5))
266	uassert.Equal(t, Unmetered, open.Slice(Unmetered))
267}
268
269func TestInstanceRejectsABadSnapshot(t *testing.T) {
270	inst := NewInstance("1", address("g1x"), "counter", []byte("5"), Unmetered)
271	inst.Snapshot = []byte{1, 2, 3} // too short for the counter's three fields
272	err := inst.Run(&counter{}, NewTestHost(), Unmetered)
273	uassert.ErrorIs(t, err, ErrTruncated)
274	// The instance is untouched: a snapshot that cannot be decoded costs
275	// gas but never corrupts the stored program.
276	uassert.Equal(t, int64(0), inst.Slices)
277	uassert.Equal(t, "5", string(inst.Program))
278}
279
280func TestStore(t *testing.T) {
281	s := NewStore()
282	uassert.Equal(t, 0, s.Size())
283	uassert.Equal(t, true, s.Get("nope") == nil)
284
285	s.Set(NewInstance("001", address("g1a"), "counter", nil, Unmetered))
286	s.Set(NewInstance("002", address("g1b"), "counter", nil, Unmetered))
287	s.Set(NewInstance("003", address("g1c"), "counter", nil, Unmetered))
288	uassert.Equal(t, 3, s.Size())
289	uassert.Equal(t, "002", s.Get("002").ID)
290
291	ids := ""
292	s.Iterate(func(i *Instance) bool { ids += i.ID + " "; return false })
293	uassert.Equal(t, "001 002 003 ", ids)
294
295	rev := ""
296	s.ReverseIterate(func(i *Instance) bool { rev += i.ID + " "; return false })
297	uassert.Equal(t, "003 002 001 ", rev)
298
299	// Early stop.
300	first := ""
301	s.Iterate(func(i *Instance) bool { first = i.ID; return true })
302	uassert.Equal(t, "001", first)
303
304	uassert.True(t, s.Remove("002"))
305	uassert.False(t, s.Remove("002"))
306	uassert.Equal(t, 2, s.Size())
307}
308
309func TestTrapReason(t *testing.T) {
310	uassert.Equal(t, "boom", TrapReason(&counter{trap: "boom"}))
311	uassert.Equal(t, "", TrapReason(&counter{}))
312}