github.com/bananabytelabs/wazero@v0.0.0-20240105073314-54b22a776da8/imports/assemblyscript/assemblyscript_test.go (about) 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/bananabytelabs/wazero" 16 "github.com/bananabytelabs/wazero/api" 17 . "github.com/bananabytelabs/wazero/experimental" 18 "github.com/bananabytelabs/wazero/experimental/logging" 19 . "github.com/bananabytelabs/wazero/internal/assemblyscript" 20 "github.com/bananabytelabs/wazero/internal/testing/proxy" 21 "github.com/bananabytelabs/wazero/internal/testing/require" 22 "github.com/bananabytelabs/wazero/internal/u64" 23 "github.com/bananabytelabs/wazero/internal/wasm" 24 "github.com/bananabytelabs/wazero/sys" 25 ) 26 27 // testCtx is an arbitrary, non-default context. Non-nil also prevents linter errors. 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()) // nothing output if strings fail to read. 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 // TestFunctionExporter_Trace ensures the trace output is according to configuration. 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, // will attempt to read size from offset -4 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) // set size to too large value 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 // Set context to one that has an experimental listener 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 }