bfdemo_test.gno
7.21 Kb · 232 lines
1package bfdemo
2
3import (
4 "strings"
5 "testing"
6
7 "gno.land/p/moul/x/vm/vmkit/v0"
8 "gno.land/p/nt/avl/v0"
9 "gno.land/p/nt/seqid/v0"
10 "gno.land/p/nt/testutils/v0"
11 "gno.land/p/nt/uassert/v0"
12)
13
14const hello = "++++++++++[>+++++++>++++++++++>+++>+<<<<-]>++.>+.+++++++..+++.>++.<<+++++++++++++++.>.+++.------.--------."
15
16// resetState puts the realm globals back where init() left them. Realm state
17// persists for the whole test binary, so every test that asserts on ids or on
18// the rendered instance list has to start from here.
19func resetState() {
20 store = vmkit.NewStore()
21 inputs = avl.NewTree()
22 idgen = seqid.ID(0)
23 count = 0
24}
25
26func TestUploadAndRunToCompletion(cur realm, t *testing.T) {
27 resetState()
28 alice := testutils.TestAddress("alice")
29 testing.SetRealm(testing.NewUserRealm(alice))
30
31 id := Upload(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 World", string(inst.Output))
36 uassert.Equal(t, int64(1), inst.Slices)
37 uassert.Equal(t, alice, inst.Owner)
38}
39
40// TestOneProgramAcrossManyTransactions is what the realm exists to
41// demonstrate: the same computation, finished over several calls, each paying
42// for its own slice.
43func TestOneProgramAcrossManyTransactions(cur realm, t *testing.T) {
44 resetState()
45 testing.SetRealm(testing.NewUserRealm(testutils.TestAddress("alice")))
46
47 id := Upload(cross(cur), hello, "", 0)
48 status := ""
49 slices := 0
50 for slices < 500 {
51 status = Step(cross(cur), id, 7)
52 slices++
53 if status != "running" {
54 break
55 }
56 }
57
58 inst := store.Get(id)
59 uassert.Equal(t, "halted", status)
60 uassert.Equal(t, "Hello World", string(inst.Output))
61 uassert.True(t, inst.Slices > 1, "took more than one slice")
62}
63
64func TestInputIsReadThroughTheHost(cur realm, t *testing.T) {
65 resetState()
66 testing.SetRealm(testing.NewUserRealm(testutils.TestAddress("alice")))
67
68 // ",[.,]" echoes its input back.
69 id := Upload(cross(cur), ",[.,]", "gno.land", 0)
70 uassert.Equal(t, "halted", Step(cross(cur), id, 0))
71 uassert.Equal(t, "gno.land", string(store.Get(id).Output))
72}
73
74func TestBudgetStopsAProgram(cur realm, t *testing.T) {
75 resetState()
76 testing.SetRealm(testing.NewUserRealm(testutils.TestAddress("alice")))
77
78 id := Upload(cross(cur), hello, "", 12)
79 uassert.Equal(t, "out of fuel", Step(cross(cur), id, 0))
80 uassert.Equal(t, int64(12), store.Get(id).FuelUsed)
81
82 // And it stays stopped rather than quietly continuing.
83 uassert.AbortsContains(t, cur, "instance is out of fuel", func() {
84 Step(cross(cur), id, 0)
85 })
86}
87
88func TestUploadRejectsAMalformedProgram(cur realm, t *testing.T) {
89 resetState()
90 testing.SetRealm(testing.NewUserRealm(testutils.TestAddress("alice")))
91
92 // Rejected by the transaction that submitted it, not by whoever pays
93 // for the first slice later.
94 uassert.AbortsContains(t, cur, "unmatched", func() {
95 Upload(cross(cur), "+++[", "", 0)
96 })
97 uassert.Equal(t, 0, count)
98}
99
100func TestRemoveIsOwnerOnly(cur realm, t *testing.T) {
101 resetState()
102 alice := testutils.TestAddress("alice")
103 bob := testutils.TestAddress("bob")
104
105 testing.SetRealm(testing.NewUserRealm(alice))
106 id := Upload(cross(cur), hello, "", 0)
107
108 testing.SetRealm(testing.NewUserRealm(bob))
109 uassert.AbortsContains(t, cur, "not your instance", func() {
110 Remove(cross(cur), id)
111 })
112
113 // Anyone may pay for a slice, though: the program and its budget were
114 // both fixed at upload.
115 uassert.Equal(t, "halted", Step(cross(cur), id, 0))
116
117 testing.SetRealm(testing.NewUserRealm(alice))
118 Remove(cross(cur), id)
119 uassert.Equal(t, 0, count)
120 uassert.True(t, store.Get(id) == nil)
121}
122
123func TestOutputIsCappedAndTrapped(cur realm, t *testing.T) {
124 resetState()
125 testing.SetRealm(testing.NewUserRealm(testutils.TestAddress("alice")))
126
127 // A cell set to 1 and printed forever: an infinite writer.
128 id := Upload(cross(cur), "+[.]", "", 0)
129 status := Step(cross(cur), id, 0)
130
131 inst := store.Get(id)
132 uassert.Equal(t, "trapped", status)
133 uassert.Equal(t, "output limit reached", inst.Trap)
134 uassert.Equal(t, MaxOutput, len(inst.Output))
135}
136
137func TestRenderUnknownInstance(t *testing.T) {
138 resetState()
139 uassert.True(t, strings.Contains(Render("/nope"), "No such instance"))
140}
141
142// TestRenderEscapesGuestOutput pins the rule that a realm cannot be fixed in
143// place once it is live: everything a caller controls is escaped before it
144// reaches the page.
145func TestRenderEscapesGuestOutput(cur realm, t *testing.T) {
146 resetState()
147 testing.SetRealm(testing.NewUserRealm(testutils.TestAddress("alice")))
148
149 // Print a backtick (96), which would otherwise close the code fence.
150 src := strings.Repeat("+", 96) + "."
151 id := Upload(cross(cur), src, "", 0)
152 Step(cross(cur), id, 0)
153
154 page := Render("/" + id)
155 uassert.Equal(t, "`", string(store.Get(id).Output))
156 uassert.True(t, strings.Contains(page, "\\x60"), "the backtick is escaped")
157
158 // The comment text in a program never reaches the page at all: only
159 // the eight operators do.
160 uassert.Equal(t, "+++[-]<>", operatorsOnly("+++ this is a comment [-] </script>"))
161}
162
163// Output sits in a code span, where markdown escapes show verbatim: only the
164// pipe is escaped there, and nothing else.
165func TestCodeCell(t *testing.T) {
166 uassert.Equal(t, "`Hello, gno.land\\n`", codeCell("Hello, gno.land\\n"))
167 uassert.Equal(t, "`a\\|b`", codeCell("a|b"))
168}
169
170// emit returns a program that prints s, one byte at a time, so a test can put
171// any text in the output column without hand-writing Brainfuck.
172func emit(s string) string {
173 var sb strings.Builder
174 for i := 0; i < len(s); i++ {
175 sb.WriteString("[-]" + strings.Repeat("+", int(s[i])) + ".")
176 }
177 return sb.String()
178}
179
180// instanceRow returns the first instance row of the home page.
181func instanceRow(t *testing.T) string {
182 for _, line := range strings.Split(Render(""), "\n") {
183 if strings.HasPrefix(line, "| [") {
184 return line
185 }
186 }
187 t.Fatalf("no instance row on the home page")
188 return ""
189}
190
191// The output column is pinned on the rendered page, not only through the
192// helper, so the call site cannot drift back to ui.Cell inside backticks
193// (which would show `a\.b`) with the suite green.
194func TestRenderOutputCell(cur realm, t *testing.T) {
195 cases := []struct {
196 name, out, cell string
197 }{
198 {"an escapable character shows as written", "a.b", "| `a.b` |"},
199 {"no output is an empty cell, not two backticks", "", "| 0 | |"},
200 {"a space at each end is padded in the markdown, past CommonMark's strip", " a ", "| ` a ` |"},
201 {"a guest pipe stays inside its cell", "a|b", "| `a\\|b` |"},
202 }
203 for _, c := range cases {
204 resetState()
205 testing.SetRealm(testing.NewUserRealm(testutils.TestAddress("alice")))
206 id := Upload(cross(cur), emit(c.out), "", 0)
207 if c.out != "" {
208 Step(cross(cur), id, 0)
209 uassert.Equal(t, c.out, string(store.Get(id).Output))
210 }
211 row := instanceRow(t)
212 uassert.True(t, strings.HasSuffix(row, c.cell), c.name+": "+row)
213 }
214}
215
216// codeCell follows the three rules of a code span, not only the pipe: empty
217// content is not a span, a space at either end needs a pad, and the fence
218// outscans any backtick run in the content.
219func TestCodeCellRules(t *testing.T) {
220 cases := []struct{ in, want string }{
221 {"", ""},
222 {"Hello World", "`Hello World`"},
223 {"a|b", "`a\\|b`"},
224 {" hi ", "` hi `"},
225 {" hi", "` hi `"},
226 {"hi ", "` hi `"},
227 {"a`b", "``a`b``"},
228 }
229 for _, c := range cases {
230 uassert.Equal(t, c.want, codeCell(c.in), "codeCell("+c.in+")")
231 }
232}