riscvdemo_test.gno
5.91 Kb · 193 lines
1package riscvdemo
2
3import (
4 "strings"
5 "testing"
6
7 "gno.land/p/moul/x/vm/riscv/v0"
8 "gno.land/p/moul/x/vm/vmkit/v0"
9 "gno.land/p/nt/avl/v0"
10 "gno.land/p/nt/seqid/v0"
11 "gno.land/p/nt/testutils/v0"
12 "gno.land/p/nt/uassert/v0"
13)
14
15// resetState puts the realm globals back where init() left them. Realm state
16// persists for the whole test binary, so every test that asserts on ids or on
17// the rendered instance list has to start from here.
18func resetState() {
19 store = vmkit.NewStore()
20 inputs = avl.NewTree()
21 idgen = seqid.ID(0)
22 count = 0
23}
24
25func alice() address { return testutils.TestAddress("alice") }
26
27func TestUploadAndRunToCompletion(cur realm, t *testing.T) {
28 resetState()
29 testing.SetRealm(testing.NewUserRealm(alice()))
30
31 id := UploadSample(cross(cur), "hello", "", 0)
32 uassert.Equal(t, "halted", Step(cross(cur), id, 0))
33
34 inst := store.Get(id)
35 uassert.Equal(t, "Hello, gno.land\n", string(inst.Output))
36 uassert.Equal(t, int64(1), inst.Slices)
37 uassert.Equal(t, alice(), inst.Owner)
38 uassert.Equal(t, "riscv32im", inst.VM)
39}
40
41// Every sample on the front page has to reach its documented answer, or the
42// page offers a button that traps when pressed. The words are assembled
43// outside this repo, so this is the test that catches a mistyped one.
44func TestEverySampleReachesItsAnswer(cur realm, t *testing.T) {
45 resetState()
46 testing.SetRealm(testing.NewUserRealm(alice()))
47
48 cases := []struct {
49 slug string
50 reg int
51 want uint32
52 output string
53 }{
54 {"hello", 0, 0, "Hello, gno.land\n"},
55 {"sum100", 5, 5050, ""},
56 {"heavy", 5, 1250025000, ""},
57 {"mext", 5, 3000000021, ""},
58 {"clang", 0, 0, "fb7ffba0"},
59 {"ledger", 0, 0, "alice 70\nbob 80\n"},
60 }
61 for _, c := range cases {
62 id := UploadSample(cross(cur), c.slug, sampleByName(c.slug).input, 0)
63 status := ""
64 for i := 0; i < 20; i++ {
65 status = Step(cross(cur), id, 0)
66 if status != "running" {
67 break
68 }
69 }
70 uassert.Equal(t, "halted", status, c.slug+" must halt")
71 inst := store.Get(id)
72 uassert.Equal(t, c.output, string(inst.Output), c.slug+" output")
73 if c.reg != 0 {
74 var m riscv.Machine
75 uassert.NoError(t, m.Restore(inst.Snapshot))
76 uassert.Equal(t, uint64(c.want), uint64(m.Registers()[c.reg]), c.slug+" result")
77 }
78 }
79}
80
81// TestOneProgramAcrossManyTransactions is what the realm exists to
82// demonstrate: the same computation, finished over several calls, each paying
83// for its own slice.
84func TestOneProgramAcrossManyTransactions(cur realm, t *testing.T) {
85 resetState()
86 testing.SetRealm(testing.NewUserRealm(alice()))
87
88 id := UploadSample(cross(cur), "heavy", "", 0)
89 status := ""
90 for i := 0; i < 50; i++ {
91 status = Step(cross(cur), id, 80000)
92 if status != "running" {
93 break
94 }
95 }
96 inst := store.Get(id)
97 uassert.Equal(t, "halted", status)
98 uassert.True(t, inst.Slices > 1, "a 200k instruction program must not finish in one slice")
99 uassert.Equal(t, int64(200006), inst.FuelUsed)
100}
101
102func TestUploadTakesHex(cur realm, t *testing.T) {
103 resetState()
104 testing.SetRealm(testing.NewUserRealm(alice()))
105
106 // addi a7, zero, 93 ; ecall
107 id := Upload(cross(cur), "0x9308d005 73000000", "", 0)
108 _ = id
109}
110
111func TestUploadRejectsBadImages(cur realm, t *testing.T) {
112 resetState()
113 testing.SetRealm(testing.NewUserRealm(alice()))
114
115 // Called without cross(): a panic raised through a crossing call cannot be
116 // recovered, so uassert would never see it.
117 uassert.PanicsWithMessage(t, cur, "riscvdemo: image is not valid hex", func() {
118 Upload(cur, "zzzz", "", 0)
119 })
120 uassert.PanicsWithMessage(t, cur, "riscvdemo: image is not valid hex", func() {
121 Upload(cur, "abc", "", 0)
122 })
123 uassert.PanicsWithMessage(t, cur, "riscvdemo: no such sample", func() {
124 UploadSample(cur, "nope", "", 0)
125 })
126 uassert.PanicsWithMessage(t, cur,
127 "riscvdemo: riscv: image length is not a whole number of instructions",
128 func() { Upload(cur, "0011", "", 0) })
129}
130
131func TestDecodeHex(t *testing.T) {
132 b, ok := decodeHex("0xDEADbeef")
133 uassert.True(t, ok)
134 uassert.Equal(t, 4, len(b))
135 uassert.Equal(t, uint64(0xDE), uint64(b[0]))
136 uassert.Equal(t, uint64(0xEF), uint64(b[3]))
137
138 _, ok = decodeHex("")
139 uassert.False(t, ok)
140 _, ok = decodeHex("0x")
141 uassert.False(t, ok)
142 // Whitespace is skipped, because a pasted hexdump has it.
143 b, ok = decodeHex("00 11\n22\t33")
144 uassert.True(t, ok)
145 uassert.Equal(t, 4, len(b))
146 // An odd number of digits is still a refusal, whitespace or not.
147 _, ok = decodeHex("00 1")
148 uassert.False(t, ok)
149}
150
151func TestRenderHomeOffersEverySample(cur realm, t *testing.T) {
152 resetState()
153 out := Render("")
154 uassert.True(t, strings.Contains(out, "RV32IM, on chain"), "the title")
155 for _, s := range samples {
156 uassert.True(t, strings.Contains(out, s.name), "home must offer "+s.slug)
157 }
158 uassert.True(t, strings.Contains(out, "No instances yet"), "the empty state")
159}
160
161// The register file is the interesting half of this VM: a program that writes
162// nothing still computed something, and the snapshot is the only place it is
163// legible between transactions.
164func TestRenderInstanceShowsTheRegisterFile(cur realm, t *testing.T) {
165 resetState()
166 testing.SetRealm(testing.NewUserRealm(alice()))
167
168 id := UploadSample(cross(cur), "sum100", "", 0)
169 Step(cross(cur), id, 0)
170
171 out := Render("/" + id)
172 uassert.True(t, strings.Contains(out, "## Registers"), "the section")
173 uassert.True(t, strings.Contains(out, "5050"), "t0 must show the answer")
174 uassert.True(t, strings.Contains(out, "halted"), "the status")
175}
176
177func TestRenderUnknownInstance(t *testing.T) {
178 uassert.True(t, strings.Contains(Render("/nope"), "No such instance"))
179}
180
181// ExampleRender pins the realm's output for a path whose content cannot drift
182// with the instance list. The home page and the instance page both produce
183// consecutive blank lines, which an example block cannot represent, so they
184// are asserted in the tests above instead.
185func ExampleRender() {
186 print(Render("/missing"))
187 // Output:
188 // # Instance missing
189 //
190 // [← all instances](/r/moul/x/vm/riscvdemo/v0)
191 //
192 // **No such instance.** It was never uploaded, or its owner removed it.
193}