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