1 package assemblyscript
2
3 import (
4 "bytes"
5 "context"
6 _ "embed"
7 "encoding/hex"
8 "errors"
9 "io"
10 "strings"
11 "testing"
12 "testing/iotest"
13 "unicode/utf16"
14
15 "github.com/tetratelabs/wazero"
16 "github.com/tetratelabs/wazero/api"
17 . "github.com/tetratelabs/wazero/experimental"
18 "github.com/tetratelabs/wazero/experimental/logging"
19 . "github.com/tetratelabs/wazero/internal/assemblyscript"
20 "github.com/tetratelabs/wazero/internal/testing/proxy"
21 "github.com/tetratelabs/wazero/internal/testing/require"
22 "github.com/tetratelabs/wazero/internal/u64"
23 "github.com/tetratelabs/wazero/internal/wasm"
24 "github.com/tetratelabs/wazero/sys"
25 )
26
27
28 var testCtx = context.WithValue(context.Background(), struct{}{}, "arbitrary")
29
30 func TestAbort(t *testing.T) {
31 tests := []struct {
32 name string
33 exporter FunctionExporter
34 expected string
35 }{
36 {
37 name: "enabled",
38 exporter: NewFunctionExporter(),
39 expected: "message at filename:1:2\n",
40 },
41 {
42 name: "disabled",
43 exporter: NewFunctionExporter().WithAbortMessageDisabled(),
44 expected: "",
45 },
46 }
47
48 for _, tt := range tests {
49 tc := tt
50
51 t.Run(tc.name, func(t *testing.T) {
52 var stderr bytes.Buffer
53 mod, r, log := requireProxyModule(t, tc.exporter, wazero.NewModuleConfig().WithStderr(&stderr), logging.LogScopeProc)
54 defer r.Close(testCtx)
55
56 messageOff, filenameOff := writeAbortMessageAndFileName(t, mod.Memory(), encodeUTF16("message"), encodeUTF16("filename"))
57
58 _, err := mod.ExportedFunction(AbortName).
59 Call(testCtx, uint64(messageOff), uint64(filenameOff), uint64(1), uint64(2))
60 require.Error(t, err)
61 sysErr, ok := err.(*sys.ExitError)
62 require.True(t, ok, err)
63 require.Equal(t, uint32(255), sysErr.ExitCode())
64 require.Equal(t, `
65 ==> env.~lib/builtins/abort(message=4,fileName=22,lineNumber=1,columnNumber=2)
66 `, "\n"+log.String())
67
68 require.Equal(t, tc.expected, stderr.String())
69 })
70 }
71 }
72
73 func TestAbort_Error(t *testing.T) {
74 tests := []struct {
75 name string
76 messageUTF16 []byte
77 fileNameUTF16 []byte
78 expectedLog string
79 }{
80 {
81 name: "bad message",
82 messageUTF16: encodeUTF16("message")[:5],
83 fileNameUTF16: encodeUTF16("filename"),
84 expectedLog: `
85 ==> env.~lib/builtins/abort(message=4,fileName=13,lineNumber=1,columnNumber=2)
86 `,
87 },
88 {
89 name: "bad filename",
90 messageUTF16: encodeUTF16("message"),
91 fileNameUTF16: encodeUTF16("filename")[:5],
92 expectedLog: `
93 ==> env.~lib/builtins/abort(message=4,fileName=22,lineNumber=1,columnNumber=2)
94 `,
95 },
96 }
97
98 for _, tt := range tests {
99 tc := tt
100
101 t.Run(tc.name, func(t *testing.T) {
102 var stderr bytes.Buffer
103 mod, r, log := requireProxyModule(t, NewFunctionExporter(), wazero.NewModuleConfig().WithStderr(&stderr), logging.LogScopeAll)
104 defer r.Close(testCtx)
105
106 messageOff, filenameOff := writeAbortMessageAndFileName(t, mod.Memory(), tc.messageUTF16, tc.fileNameUTF16)
107
108 _, err := mod.ExportedFunction(AbortName).
109 Call(testCtx, uint64(messageOff), uint64(filenameOff), uint64(1), uint64(2))
110 require.Error(t, err)
111 sysErr, ok := err.(*sys.ExitError)
112 require.True(t, ok, err)
113 require.Equal(t, uint32(255), sysErr.ExitCode())
114 require.Equal(t, tc.expectedLog, "\n"+log.String())
115
116 require.Equal(t, "", stderr.String())
117 })
118 }
119 }
120
121 func TestSeed(t *testing.T) {
122 tests := []struct {
123 name string
124 scopes logging.LogScopes
125 expectedLog string
126 }{
127 {
128 name: "logs to crypto scope",
129 scopes: logging.LogScopeRandom,
130 expectedLog: `
131 ==> env.~lib/builtins/seed()
132 <== rand=4.958153677776298e-175
133 `,
134 },
135 {
136 name: "doesn't log to filesystem scope",
137 scopes: logging.LogScopeFilesystem,
138 expectedLog: "\n",
139 },
140 }
141
142 for _, tt := range tests {
143 tc := tt
144
145 t.Run(tc.name, func(t *testing.T) {
146 mod, r, log := requireProxyModule(t, NewFunctionExporter(), wazero.NewModuleConfig(), tc.scopes)
147 defer r.Close(testCtx)
148
149 ret, err := mod.ExportedFunction(SeedName).Call(testCtx)
150 require.NoError(t, err)
151 require.Equal(t, tc.expectedLog, "\n"+log.String())
152
153 require.Equal(t, "538c7f96b164bf1b", hex.EncodeToString(u64.LeBytes(ret[0])))
154 })
155 }
156 }
157
158 func TestSeed_error(t *testing.T) {
159 tests := []struct {
160 name string
161 source io.Reader
162 expectedErr string
163 }{
164 {
165 name: "not 8 bytes",
166 source: bytes.NewReader([]byte{0, 1}),
167 expectedErr: `error reading random seed: unexpected EOF (recovered by wazero)
168 wasm stack trace:
169 env.~lib/builtins/seed() f64
170 internal/testing/proxy/proxy.go.seed() f64`,
171 },
172 {
173 name: "error reading",
174 source: iotest.ErrReader(errors.New("ice cream")),
175 expectedErr: `error reading random seed: ice cream (recovered by wazero)
176 wasm stack trace:
177 env.~lib/builtins/seed() f64
178 internal/testing/proxy/proxy.go.seed() f64`,
179 },
180 }
181
182 for _, tt := range tests {
183 tc := tt
184
185 t.Run(tc.name, func(t *testing.T) {
186 mod, r, log := requireProxyModule(t, NewFunctionExporter(), wazero.NewModuleConfig().WithRandSource(tc.source), logging.LogScopeAll)
187 defer r.Close(testCtx)
188
189 _, err := mod.ExportedFunction(SeedName).Call(testCtx)
190 require.EqualError(t, err, tc.expectedErr)
191 require.Equal(t, `
192 ==> env.~lib/builtins/seed()
193 `, "\n"+log.String())
194 })
195 }
196 }
197
198
199 func TestFunctionExporter_Trace(t *testing.T) {
200 noArgs := []uint64{4, 0, 0, 0, 0, 0, 0}
201 noArgsLog := `
202 ==> env.~lib/builtins/trace(message=4,nArgs=0,arg0=0,arg1=0,arg2=0,arg3=0,arg4=0)
203 <==
204 `
205
206 tests := []struct {
207 name string
208 exporter FunctionExporter
209 params []uint64
210 message []byte
211 outErr bool
212 expected, expectedLog string
213 }{
214 {
215 name: "disabled",
216 exporter: NewFunctionExporter(),
217 params: noArgs,
218 expected: "",
219 expectedLog: noArgsLog,
220 },
221 {
222 name: "ToStderr",
223 exporter: NewFunctionExporter().WithTraceToStderr(),
224 params: noArgs,
225 expected: "trace: hello\n",
226 expectedLog: noArgsLog,
227 },
228 {
229 name: "ToStdout - no args",
230 exporter: NewFunctionExporter().WithTraceToStdout(),
231 params: noArgs,
232 expected: "trace: hello\n",
233 expectedLog: noArgsLog,
234 },
235 {
236 name: "ToStdout - one arg",
237 exporter: NewFunctionExporter().WithTraceToStdout(),
238 params: []uint64{4, 1, api.EncodeF64(1), 0, 0, 0, 0},
239 expected: "trace: hello 1\n",
240 expectedLog: `
241 ==> env.~lib/builtins/trace(message=4,nArgs=1,arg0=1,arg1=0,arg2=0,arg3=0,arg4=0)
242 <==
243 `,
244 },
245 {
246 name: "ToStdout - two args",
247 exporter: NewFunctionExporter().WithTraceToStdout(),
248 params: []uint64{4, 2, api.EncodeF64(1), api.EncodeF64(2), 0, 0, 0},
249 expected: "trace: hello 1,2\n",
250 expectedLog: `
251 ==> env.~lib/builtins/trace(message=4,nArgs=2,arg0=1,arg1=2,arg2=0,arg3=0,arg4=0)
252 <==
253 `,
254 },
255 {
256 name: "ToStdout - five args",
257 exporter: NewFunctionExporter().WithTraceToStdout(),
258 params: []uint64{
259 4,
260 5,
261 api.EncodeF64(1),
262 api.EncodeF64(2),
263 api.EncodeF64(3.3),
264 api.EncodeF64(4.4),
265 api.EncodeF64(5),
266 },
267 expected: "trace: hello 1,2,3.3,4.4,5\n",
268 expectedLog: `
269 ==> env.~lib/builtins/trace(message=4,nArgs=5,arg0=1,arg1=2,arg2=3.3,arg3=4.4,arg4=5)
270 <==
271 `,
272 },
273 {
274 name: "not 8 bytes",
275 exporter: NewFunctionExporter().WithTraceToStderr(),
276 message: encodeUTF16("hello")[:5],
277 params: noArgs,
278 expectedLog: noArgsLog,
279 },
280 {
281 name: "error writing",
282 exporter: NewFunctionExporter().WithTraceToStderr(),
283 outErr: true,
284 params: noArgs,
285 expectedLog: noArgsLog,
286 },
287 }
288
289 for _, tt := range tests {
290 tc := tt
291
292 t.Run(tc.name, func(t *testing.T) {
293 var out bytes.Buffer
294
295 config := wazero.NewModuleConfig()
296 if strings.Contains("ToStderr", tc.name) {
297 config = config.WithStderr(&out)
298 } else {
299 config = config.WithStdout(&out)
300 }
301 if tc.outErr {
302 config = config.WithStderr(&errWriter{err: errors.New("ice cream")})
303 }
304
305 mod, r, log := requireProxyModule(t, tc.exporter, config, logging.LogScopeAll)
306 defer r.Close(testCtx)
307
308 message := tc.message
309 if message == nil {
310 message = encodeUTF16("hello")
311 }
312 ok := mod.Memory().WriteUint32Le(0, uint32(len(message)))
313 require.True(t, ok)
314 ok = mod.Memory().Write(uint32(4), message)
315 require.True(t, ok)
316
317 _, err := mod.ExportedFunction(TraceName).Call(testCtx, tc.params...)
318 require.NoError(t, err)
319 require.Equal(t, tc.expected, out.String())
320 require.Equal(t, tc.expectedLog, "\n"+log.String())
321 })
322 }
323 }
324
325 func Test_readAssemblyScriptString(t *testing.T) {
326 tests := []struct {
327 name string
328 memory func(api.Memory)
329 offset int
330 expected string
331 expectedOk bool
332 }{
333 {
334 name: "success",
335 memory: func(memory api.Memory) {
336 memory.WriteUint32Le(0, 10)
337 b := encodeUTF16("hello")
338 memory.Write(4, b)
339 },
340 offset: 4,
341 expected: "hello",
342 expectedOk: true,
343 },
344 {
345 name: "can't read size",
346 memory: func(memory api.Memory) {
347 b := encodeUTF16("hello")
348 memory.Write(0, b)
349 },
350 offset: 0,
351 expectedOk: false,
352 },
353 {
354 name: "odd size",
355 memory: func(memory api.Memory) {
356 memory.WriteUint32Le(0, 9)
357 b := encodeUTF16("hello")
358 memory.Write(4, b)
359 },
360 offset: 4,
361 expectedOk: false,
362 },
363 {
364 name: "can't read string",
365 memory: func(memory api.Memory) {
366 memory.WriteUint32Le(0, 10_000_000)
367 b := encodeUTF16("hello")
368 memory.Write(4, b)
369 },
370 offset: 4,
371 expectedOk: false,
372 },
373 }
374
375 for _, tt := range tests {
376 tc := tt
377
378 t.Run(tc.name, func(t *testing.T) {
379 mem := wasm.NewMemoryInstance(&wasm.Memory{Min: 1, Cap: 1, Max: 1})
380 tc.memory(mem)
381
382 s, ok := readAssemblyScriptString(mem, uint32(tc.offset))
383 require.Equal(t, tc.expectedOk, ok)
384 require.Equal(t, tc.expected, s)
385 })
386 }
387 }
388
389 func writeAbortMessageAndFileName(t *testing.T, mem api.Memory, messageUTF16, fileNameUTF16 []byte) (uint32, uint32) {
390 off := uint32(0)
391 ok := mem.WriteUint32Le(off, uint32(len(messageUTF16)))
392 require.True(t, ok)
393 off += 4
394 messageOff := off
395 ok = mem.Write(off, messageUTF16)
396 require.True(t, ok)
397 off += uint32(len(messageUTF16))
398 ok = mem.WriteUint32Le(off, uint32(len(fileNameUTF16)))
399 require.True(t, ok)
400 off += 4
401 filenameOff := off
402 ok = mem.Write(off, fileNameUTF16)
403 require.True(t, ok)
404 return messageOff, filenameOff
405 }
406
407 func encodeUTF16(s string) []byte {
408 runes := utf16.Encode([]rune(s))
409 b := make([]byte, len(runes)*2)
410 for i, r := range runes {
411 b[i*2] = byte(r)
412 b[i*2+1] = byte(r >> 8)
413 }
414 return b
415 }
416
417 type errWriter struct {
418 err error
419 }
420
421 func (w *errWriter) Write([]byte) (int, error) {
422 return 0, w.err
423 }
424
425 func requireProxyModule(t *testing.T, fns FunctionExporter, config wazero.ModuleConfig, scopes logging.LogScopes) (api.Module, api.Closer, *bytes.Buffer) {
426 var log bytes.Buffer
427
428
429 ctx := context.WithValue(testCtx, FunctionListenerFactoryKey{},
430 proxy.NewLoggingListenerFactory(&log, scopes))
431
432 r := wazero.NewRuntime(ctx)
433
434 builder := r.NewHostModuleBuilder("env")
435 fns.ExportFunctions(builder)
436
437 envCompiled, err := builder.Compile(ctx)
438 require.NoError(t, err)
439
440 _, err = r.InstantiateModule(ctx, envCompiled, config)
441 require.NoError(t, err)
442
443 proxyBin := proxy.NewModuleBinary("env", envCompiled)
444
445 proxyCompiled, err := r.CompileModule(ctx, proxyBin)
446 require.NoError(t, err)
447
448 mod, err := r.InstantiateModule(ctx, proxyCompiled, config)
449 require.NoError(t, err)
450 return mod, r, &log
451 }
452
View as plain text