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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

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}