github.com/tetratelabs/wazero@v1.2.1/cmd/wazero/wazero_test.go (about) 1 package main 2 3 import ( 4 "bytes" 5 _ "embed" 6 "flag" 7 "fmt" 8 "log" 9 "os" 10 "os/exec" 11 "path" 12 "path/filepath" 13 "runtime" 14 "strings" 15 "testing" 16 17 "github.com/tetratelabs/wazero/api" 18 "github.com/tetratelabs/wazero/experimental/logging" 19 "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1" 20 "github.com/tetratelabs/wazero/internal/internalapi" 21 "github.com/tetratelabs/wazero/internal/platform" 22 "github.com/tetratelabs/wazero/internal/testing/require" 23 "github.com/tetratelabs/wazero/internal/version" 24 "github.com/tetratelabs/wazero/sys" 25 ) 26 27 //go:embed testdata/infinite_loop.wasm 28 var wasmInfiniteLoop []byte 29 30 //go:embed testdata/wasi_arg.wasm 31 var wasmWasiArg []byte 32 33 //go:embed testdata/wasi_env.wasm 34 var wasmWasiEnv []byte 35 36 //go:embed testdata/wasi_fd.wasm 37 var wasmWasiFd []byte 38 39 //go:embed testdata/wasi_random_get.wasm 40 var wasmWasiRandomGet []byte 41 42 // wasmCatGo is compiled on demand with `GOARCH=wasm GOOS=js` 43 var wasmCatGo []byte 44 45 //go:embed testdata/cat/cat-tinygo.wasm 46 var wasmCatTinygo []byte 47 48 //go:embed testdata/exit_on_start_unstable.wasm 49 var wasmWasiUnstable []byte 50 51 func TestMain(m *testing.M) { 52 // For some reason, riscv64 fails to see directory listings. 53 if a := runtime.GOARCH; a == "riscv64" { 54 log.Println("main: skipping due to not yet supported GOARCH:", a) 55 os.Exit(0) 56 } 57 58 // Notably our scratch containers don't have go, so don't fail tests. 59 if err := compileGoJS(); err != nil { 60 log.Println("main: Skipping GOARCH=wasm GOOS=js tests due to:", err) 61 os.Exit(0) 62 } 63 os.Exit(m.Run()) 64 } 65 66 func TestCompile(t *testing.T) { 67 tmpDir, oldwd := requireChdirToTemp(t) 68 defer os.Chdir(oldwd) //nolint 69 70 wasmPath := filepath.Join(tmpDir, "test.wasm") 71 require.NoError(t, os.WriteFile(wasmPath, wasmWasiArg, 0o600)) 72 73 existingDir1 := filepath.Join(tmpDir, "existing1") 74 require.NoError(t, os.Mkdir(existingDir1, 0o700)) 75 existingDir2 := filepath.Join(tmpDir, "existing2") 76 require.NoError(t, os.Mkdir(existingDir2, 0o700)) 77 78 cpuProfile := filepath.Join(t.TempDir(), "cpu.out") 79 memProfile := filepath.Join(t.TempDir(), "mem.out") 80 81 tests := []struct { 82 name string 83 wazeroOpts []string 84 test func(t *testing.T) 85 }{ 86 { 87 name: "no opts", 88 }, 89 { 90 name: "cachedir existing absolute", 91 wazeroOpts: []string{"--cachedir=" + existingDir1}, 92 test: func(t *testing.T) { 93 entries, err := os.ReadDir(existingDir1) 94 require.NoError(t, err) 95 require.True(t, len(entries) > 0) 96 }, 97 }, 98 { 99 name: "cachedir existing relative", 100 wazeroOpts: []string{"--cachedir=existing2"}, 101 test: func(t *testing.T) { 102 entries, err := os.ReadDir(existingDir2) 103 require.NoError(t, err) 104 require.True(t, len(entries) > 0) 105 }, 106 }, 107 { 108 name: "cachedir new absolute", 109 wazeroOpts: []string{"--cachedir=" + path.Join(tmpDir, "new1")}, 110 test: func(t *testing.T) { 111 entries, err := os.ReadDir("new1") 112 require.NoError(t, err) 113 require.True(t, len(entries) > 0) 114 }, 115 }, 116 { 117 name: "cachedir new relative", 118 wazeroOpts: []string{"--cachedir=new2"}, 119 test: func(t *testing.T) { 120 entries, err := os.ReadDir("new2") 121 require.NoError(t, err) 122 require.True(t, len(entries) > 0) 123 }, 124 }, 125 { 126 name: "enable cpu profiling", 127 wazeroOpts: []string{"-cpuprofile=" + cpuProfile}, 128 test: func(t *testing.T) { 129 require.NoError(t, exist(cpuProfile)) 130 }, 131 }, 132 { 133 name: "enable memory profiling", 134 wazeroOpts: []string{"-memprofile=" + memProfile}, 135 test: func(t *testing.T) { 136 require.NoError(t, exist(memProfile)) 137 }, 138 }, 139 } 140 141 for _, tc := range tests { 142 tt := tc 143 t.Run(tt.name, func(t *testing.T) { 144 args := append([]string{"compile"}, tt.wazeroOpts...) 145 args = append(args, wasmPath) 146 exitCode, stdout, stderr := runMain(t, "", args) 147 require.Zero(t, stderr) 148 require.Equal(t, 0, exitCode, stderr) 149 require.Zero(t, stdout) 150 if test := tt.test; test != nil { 151 test(t) 152 } 153 }) 154 } 155 } 156 157 func requireChdirToTemp(t *testing.T) (string, string) { 158 tmpDir := t.TempDir() 159 oldwd, err := os.Getwd() 160 require.NoError(t, err) 161 require.NoError(t, os.Chdir(tmpDir)) 162 return tmpDir, oldwd 163 } 164 165 func TestCompile_Errors(t *testing.T) { 166 tmpDir := t.TempDir() 167 168 wasmPath := filepath.Join(tmpDir, "test.wasm") 169 require.NoError(t, os.WriteFile(wasmPath, wasmWasiArg, 0o600)) 170 171 notWasmPath := filepath.Join(tmpDir, "bears.wasm") 172 require.NoError(t, os.WriteFile(notWasmPath, []byte("pooh"), 0o600)) 173 174 tests := []struct { 175 message string 176 args []string 177 }{ 178 { 179 message: "missing path to wasm file", 180 args: []string{}, 181 }, 182 { 183 message: "error reading wasm binary", 184 args: []string{"non-existent.wasm"}, 185 }, 186 { 187 message: "error compiling wasm binary", 188 args: []string{notWasmPath}, 189 }, 190 { 191 message: "invalid cachedir", 192 args: []string{"--cachedir", notWasmPath, wasmPath}, 193 }, 194 } 195 196 for _, tc := range tests { 197 tt := tc 198 t.Run(tt.message, func(t *testing.T) { 199 exitCode, _, stderr := runMain(t, "", append([]string{"compile"}, tt.args...)) 200 201 require.Equal(t, 1, exitCode) 202 require.Contains(t, stderr, tt.message) 203 }) 204 } 205 } 206 207 func TestRun(t *testing.T) { 208 tmpDir, oldwd := requireChdirToTemp(t) 209 defer os.Chdir(oldwd) //nolint 210 211 // Restore env logic borrowed from TestClearenv 212 defer func(origEnv []string) { 213 for _, pair := range origEnv { 214 // Environment variables on Windows can begin with = 215 // https://blogs.msdn.com/b/oldnewthing/archive/2010/05/06/10008132.aspx 216 i := strings.Index(pair[1:], "=") + 1 217 if err := os.Setenv(pair[:i], pair[i+1:]); err != nil { 218 t.Errorf("Setenv(%q, %q) failed during reset: %v", pair[:i], pair[i+1:], err) 219 } 220 } 221 }(os.Environ()) 222 223 // Clear the environment first, so we can make strict assertions. 224 os.Clearenv() 225 os.Setenv("ANIMAL", "kitten") 226 os.Setenv("INHERITED", "wazero") 227 228 // We can't rely on the mtime from git because in CI, only the last commit 229 // is checked out. Instead, grab the effective mtime. 230 bearDir := filepath.Join(oldwd, "testdata", "fs") 231 bearPath := filepath.Join(bearDir, "bear.txt") 232 bearStat, err := os.Stat(bearPath) 233 require.NoError(t, err) 234 bearMtime := bearStat.ModTime().UnixMilli() 235 bearMtimeNano := bearStat.ModTime().UnixNano() 236 // The file is world read, but windows cannot see that and reports world 237 // write. Hence, we save off the current interpretation of mode for 238 // comparison. 239 bearMode := bearStat.Mode() 240 241 existingDir1 := filepath.Join(tmpDir, "existing1") 242 require.NoError(t, os.Mkdir(existingDir1, 0o700)) 243 existingDir2 := filepath.Join(tmpDir, "existing2") 244 require.NoError(t, os.Mkdir(existingDir2, 0o700)) 245 246 cpuProfile := filepath.Join(t.TempDir(), "cpu.out") 247 memProfile := filepath.Join(t.TempDir(), "mem.out") 248 249 type test struct { 250 name string 251 wazeroOpts []string 252 workdir string 253 wasm []byte 254 wasmArgs []string 255 expectedStdout string 256 expectedStderr string 257 expectedExitCode int 258 test func(t *testing.T) 259 } 260 261 tests := []test{ 262 { 263 name: "args", 264 wasm: wasmWasiArg, 265 wasmArgs: []string{"hello world"}, 266 // Executable name is first arg so is printed. 267 expectedStdout: "test.wasm\x00hello world\x00", 268 }, 269 { 270 name: "-- args", 271 wasm: wasmWasiArg, 272 wasmArgs: []string{"--", "hello world"}, 273 // Executable name is first arg so is printed. 274 expectedStdout: "test.wasm\x00hello world\x00", 275 }, 276 { 277 name: "env", 278 wasm: wasmWasiEnv, 279 wazeroOpts: []string{"--env=ANIMAL=bear", "--env=FOOD=sushi"}, 280 expectedStdout: "ANIMAL=bear\x00FOOD=sushi\x00", 281 }, 282 { 283 name: "env-inherit", 284 wasm: wasmWasiEnv, 285 wazeroOpts: []string{"-env-inherit"}, 286 expectedStdout: "ANIMAL=kitten\x00INHERITED=wazero\u0000", 287 }, 288 { 289 name: "env-inherit with env", 290 wasm: wasmWasiEnv, 291 wazeroOpts: []string{"-env-inherit", "--env=ANIMAL=bear"}, 292 expectedStdout: "ANIMAL=bear\x00INHERITED=wazero\u0000", // not ANIMAL=kitten 293 }, 294 { 295 name: "interpreter", 296 wasm: wasmWasiArg, 297 wazeroOpts: []string{"--interpreter"}, // just test it works 298 expectedStdout: "test.wasm\x00", 299 }, 300 { 301 name: "wasi", 302 wasm: wasmWasiFd, 303 wazeroOpts: []string{fmt.Sprintf("--mount=%s:/", bearDir)}, 304 expectedStdout: "pooh\n", 305 }, 306 { 307 name: "wasi readonly", 308 wasm: wasmWasiFd, 309 wazeroOpts: []string{fmt.Sprintf("--mount=%s:/:ro", bearDir)}, 310 expectedStdout: "pooh\n", 311 }, 312 { 313 name: "wasi non root", 314 wasm: wasmCatTinygo, 315 wazeroOpts: []string{fmt.Sprintf("--mount=%s:/animals:ro", bearDir)}, 316 wasmArgs: []string{"/animals/bear.txt"}, 317 expectedStdout: "pooh\n", 318 }, 319 { 320 name: "wasi hostlogging=all", 321 wasm: wasmWasiRandomGet, 322 wazeroOpts: []string{"--hostlogging=all"}, 323 expectedStderr: `--> .$1() 324 ==> wasi_snapshot_preview1.random_get(buf=0,buf_len=1000) 325 <== errno=ESUCCESS 326 <-- 327 `, 328 }, 329 { 330 name: "wasi hostlogging=proc", 331 wasm: wasmCatTinygo, 332 wazeroOpts: []string{"--hostlogging=proc", fmt.Sprintf("--mount=%s:/animals:ro", bearDir)}, 333 wasmArgs: []string{"/animals/not-bear.txt"}, 334 expectedStderr: `==> wasi_snapshot_preview1.proc_exit(rval=1) 335 `, // ^^ proc_exit panics, which short-circuits the logger. Hence, no "<==". 336 expectedExitCode: 1, 337 }, 338 { 339 name: "wasi hostlogging=filesystem", 340 wasm: wasmCatTinygo, 341 wazeroOpts: []string{"--hostlogging=filesystem", fmt.Sprintf("--mount=%s:/animals:ro", bearDir)}, 342 wasmArgs: []string{"/animals/bear.txt"}, 343 expectedStderr: fmt.Sprintf(`==> wasi_snapshot_preview1.fd_prestat_get(fd=3) 344 <== (prestat={pr_name_len=8},errno=ESUCCESS) 345 ==> wasi_snapshot_preview1.fd_prestat_dir_name(fd=3) 346 <== (path=/animals,errno=ESUCCESS) 347 ==> wasi_snapshot_preview1.fd_prestat_get(fd=4) 348 <== (prestat=,errno=EBADF) 349 ==> wasi_snapshot_preview1.fd_fdstat_get(fd=3) 350 <== (stat={filetype=DIRECTORY,fdflags=,fs_rights_base=FD_DATASYNC|FDSTAT_SET_FLAGS|FD_SYNC|PATH_CREATE_DIRECTORY|PATH_CREATE_FILE|PATH_LINK_SOURCE|PATH_LINK_TARGET|PATH_OPEN|FD_READDIR|PATH_READLINK,fs_rights_inheriting=FD_DATASYNC|FD_READ|FD_SEEK|FDSTAT_SET_FLAGS|FD_SYNC|FD_TELL|FD_WRITE|FD_ADVISE|FD_ALLOCATE|PATH_CREATE_DIRECTORY|PATH_CREATE_FILE|PATH_LINK_SOURCE|PATH_LINK_TARGET|PATH_OPEN|FD_READDIR|PATH_READLINK},errno=ESUCCESS) 351 ==> wasi_snapshot_preview1.path_open(fd=3,dirflags=SYMLINK_FOLLOW,path=bear.txt,oflags=,fs_rights_base=FD_READ|FD_SEEK|FDSTAT_SET_FLAGS|FD_SYNC|FD_TELL|FD_ADVISE|PATH_CREATE_DIRECTORY|PATH_CREATE_FILE|PATH_LINK_SOURCE|PATH_LINK_TARGET|PATH_OPEN|FD_READDIR|PATH_READLINK|PATH_RENAME_SOURCE|PATH_RENAME_TARGET|PATH_FILESTAT_GET|PATH_FILESTAT_SET_SIZE|PATH_FILESTAT_SET_TIMES|FD_FILESTAT_GET|FD_FILESTAT_SET_TIMES|PATH_SYMLINK|PATH_REMOVE_DIRECTORY|PATH_UNLINK_FILE|POLL_FD_READWRITE,fs_rights_inheriting=FD_DATASYNC|FD_READ|FD_SEEK|FDSTAT_SET_FLAGS|FD_SYNC|FD_TELL|FD_WRITE|FD_ADVISE|FD_ALLOCATE|PATH_CREATE_DIRECTORY|PATH_CREATE_FILE|PATH_LINK_SOURCE|PATH_LINK_TARGET|PATH_OPEN|FD_READDIR|PATH_READLINK|PATH_RENAME_SOURCE|PATH_RENAME_TARGET|PATH_FILESTAT_GET|PATH_FILESTAT_SET_SIZE|PATH_FILESTAT_SET_TIMES|FD_FILESTAT_GET|FD_FILESTAT_SET_SIZE|FD_FILESTAT_SET_TIMES|PATH_SYMLINK|PATH_REMOVE_DIRECTORY|PATH_UNLINK_FILE|POLL_FD_READWRITE,fdflags=) 352 <== (opened_fd=4,errno=ESUCCESS) 353 ==> wasi_snapshot_preview1.fd_filestat_get(fd=4) 354 <== (filestat={filetype=REGULAR_FILE,size=5,mtim=%d},errno=ESUCCESS) 355 ==> wasi_snapshot_preview1.fd_read(fd=4,iovs=64776,iovs_len=1) 356 <== (nread=5,errno=ESUCCESS) 357 ==> wasi_snapshot_preview1.fd_read(fd=4,iovs=64776,iovs_len=1) 358 <== (nread=0,errno=ESUCCESS) 359 ==> wasi_snapshot_preview1.fd_close(fd=4) 360 <== errno=ESUCCESS 361 `, bearMtimeNano), 362 expectedStdout: "pooh\n", 363 }, 364 { 365 name: "wasi hostlogging=random", 366 wasm: wasmWasiRandomGet, 367 wazeroOpts: []string{"--hostlogging=random"}, 368 expectedStderr: `==> wasi_snapshot_preview1.random_get(buf=0,buf_len=1000) 369 <== errno=ESUCCESS 370 `, 371 }, 372 { 373 name: "GOARCH=wasm GOOS=js", 374 wasm: wasmCatGo, 375 wazeroOpts: []string{fmt.Sprintf("--mount=%s:/", bearDir)}, 376 wasmArgs: []string{"/bear.txt"}, 377 expectedStdout: "pooh\n", 378 }, 379 { 380 name: "GOARCH=wasm GOOS=js workdir", 381 wasm: wasmCatGo, 382 wazeroOpts: []string{ 383 // --mount=X:\:/ on Windows, --mount=/:/ everywhere else 384 "--mount=" + filepath.VolumeName(bearDir) + string(os.PathSeparator) + ":/", 385 }, 386 workdir: bearDir, 387 wasmArgs: []string{"bear.txt"}, 388 expectedStdout: "pooh\n", 389 }, 390 { 391 name: "GOARCH=wasm GOOS=js readonly", 392 wasm: wasmCatGo, 393 wazeroOpts: []string{fmt.Sprintf("--mount=%s:/:ro", bearDir)}, 394 wasmArgs: []string{"/bear.txt"}, 395 expectedStdout: "pooh\n", 396 }, 397 { 398 name: "GOARCH=wasm GOOS=js hostlogging=proc", 399 wasm: wasmCatGo, 400 wazeroOpts: []string{"--hostlogging=proc", fmt.Sprintf("--mount=%s:/animals:ro", bearDir)}, 401 wasmArgs: []string{"/animals/not-bear.txt"}, 402 expectedStderr: `==> go.runtime.wasmExit(code=1) 403 <== 404 `, 405 expectedExitCode: 1, 406 }, 407 { 408 name: "GOARCH=wasm GOOS=js hostlogging=filesystem", 409 wasm: wasmCatGo, 410 wazeroOpts: []string{"--hostlogging=filesystem", fmt.Sprintf("--mount=%s:/", bearDir)}, 411 wasmArgs: []string{"/bear.txt"}, 412 expectedStdout: "pooh\n", 413 expectedStderr: fmt.Sprintf(`==> go.syscall/js.valueCall(fs.open(path=/bear.txt,flags=,perm=----------)) 414 <== (err=<nil>,fd=4) 415 ==> go.syscall/js.valueCall(fs.fstat(fd=4)) 416 <== (err=<nil>,stat={isDir=false,mode=%[1]s,size=5,mtimeMs=%[2]d}) 417 ==> go.syscall/js.valueCall(fs.fstat(fd=4)) 418 <== (err=<nil>,stat={isDir=false,mode=%[1]s,size=5,mtimeMs=%[2]d}) 419 ==> go.syscall/js.valueCall(fs.read(fd=4,offset=0,byteCount=512,fOffset=<nil>)) 420 <== (err=<nil>,n=5) 421 ==> go.syscall/js.valueCall(fs.read(fd=4,offset=0,byteCount=507,fOffset=<nil>)) 422 <== (err=<nil>,n=0) 423 ==> go.syscall/js.valueCall(fs.close(fd=4)) 424 <== (err=<nil>,ok=true) 425 `, bearMode, bearMtime), 426 }, 427 { 428 name: "cachedir existing absolute", 429 wazeroOpts: []string{"--cachedir=" + existingDir1}, 430 wasm: wasmWasiArg, 431 wasmArgs: []string{"hello world"}, 432 // Executable name is first arg so is printed. 433 expectedStdout: "test.wasm\x00hello world\x00", 434 test: func(t *testing.T) { 435 entries, err := os.ReadDir(existingDir1) 436 require.NoError(t, err) 437 require.True(t, len(entries) > 0) 438 }, 439 }, 440 { 441 name: "cachedir existing relative", 442 wazeroOpts: []string{"--cachedir=existing2"}, 443 wasm: wasmWasiArg, 444 wasmArgs: []string{"hello world"}, 445 // Executable name is first arg so is printed. 446 expectedStdout: "test.wasm\x00hello world\x00", 447 test: func(t *testing.T) { 448 entries, err := os.ReadDir(existingDir2) 449 require.NoError(t, err) 450 require.True(t, len(entries) > 0) 451 }, 452 }, 453 { 454 name: "cachedir new absolute", 455 wazeroOpts: []string{"--cachedir=" + path.Join(tmpDir, "new1")}, 456 wasm: wasmWasiArg, 457 wasmArgs: []string{"hello world"}, 458 // Executable name is first arg so is printed. 459 expectedStdout: "test.wasm\x00hello world\x00", 460 test: func(t *testing.T) { 461 entries, err := os.ReadDir("new1") 462 require.NoError(t, err) 463 require.True(t, len(entries) > 0) 464 }, 465 }, 466 { 467 name: "cachedir new relative", 468 wazeroOpts: []string{"--cachedir=new2"}, 469 wasm: wasmWasiArg, 470 wasmArgs: []string{"hello world"}, 471 // Executable name is first arg so is printed. 472 expectedStdout: "test.wasm\x00hello world\x00", 473 test: func(t *testing.T) { 474 entries, err := os.ReadDir("new2") 475 require.NoError(t, err) 476 require.True(t, len(entries) > 0) 477 }, 478 }, 479 { 480 name: "timeout: a binary that exceeds the deadline should print an error", 481 wazeroOpts: []string{"-timeout=1ms"}, 482 wasm: wasmInfiniteLoop, 483 expectedStderr: "error: module closed with context deadline exceeded (timeout 1ms)\n", 484 expectedExitCode: int(sys.ExitCodeDeadlineExceeded), 485 test: func(t *testing.T) { 486 require.NoError(t, err) 487 }, 488 }, 489 { 490 name: "timeout: a binary that ends before the deadline should not print a timeout error", 491 wazeroOpts: []string{"-timeout=10s"}, 492 wasm: wasmWasiRandomGet, 493 test: func(t *testing.T) { 494 require.NoError(t, err) 495 }, 496 }, 497 { 498 name: "should run wasi_unstable", 499 wasm: wasmWasiUnstable, 500 expectedExitCode: 2, 501 test: func(t *testing.T) { 502 require.NoError(t, err) 503 }, 504 }, 505 { 506 name: "enable cpu profiling", 507 wazeroOpts: []string{"-cpuprofile=" + cpuProfile}, 508 wasm: wasmWasiRandomGet, 509 test: func(t *testing.T) { 510 require.NoError(t, exist(cpuProfile)) 511 }, 512 }, 513 { 514 name: "enable memory profiling", 515 wazeroOpts: []string{"-memprofile=" + memProfile}, 516 wasm: wasmWasiRandomGet, 517 test: func(t *testing.T) { 518 require.NoError(t, exist(memProfile)) 519 }, 520 }, 521 } 522 523 cryptoTest := test{ 524 name: "GOARCH=wasm GOOS=js hostlogging=filesystem,random", 525 wasm: wasmCatGo, 526 wazeroOpts: []string{"--hostlogging=filesystem,random"}, 527 wasmArgs: []string{"/bear.txt"}, 528 expectedStderr: `==> go.runtime.getRandomData(r_len=32) 529 <== 530 ==> go.runtime.getRandomData(r_len=8) 531 <== 532 ==> go.syscall/js.valueCall(fs.open(path=/bear.txt,flags=,perm=----------)) 533 <== (err=function not implemented,fd=0) 534 `, // Test only shows logging happens in two scopes; it is ok to fail. 535 expectedExitCode: 1, 536 } 537 538 for _, tt := range append(tests, cryptoTest) { 539 tc := tt 540 541 if tc.wasm == nil { 542 // We should only skip when the runtime is a scratch image. 543 require.False(t, platform.CompilerSupported()) 544 continue 545 } 546 t.Run(tc.name, func(t *testing.T) { 547 wasmPath := filepath.Join(tmpDir, "test.wasm") 548 require.NoError(t, os.WriteFile(wasmPath, tc.wasm, 0o700)) 549 550 args := append([]string{"run"}, tc.wazeroOpts...) 551 args = append(args, wasmPath) 552 args = append(args, tc.wasmArgs...) 553 exitCode, stdout, stderr := runMain(t, tc.workdir, args) 554 555 require.Equal(t, tc.expectedStderr, stderr) 556 require.Equal(t, tc.expectedExitCode, exitCode, stderr) 557 require.Equal(t, tc.expectedStdout, stdout) 558 if test := tc.test; test != nil { 559 test(t) 560 } 561 }) 562 } 563 } 564 565 func TestVersion(t *testing.T) { 566 exitCode, stdout, stderr := runMain(t, "", []string{"version"}) 567 require.Equal(t, 0, exitCode) 568 require.Equal(t, version.GetWazeroVersion()+"\n", stdout) 569 require.Equal(t, "", stderr) 570 } 571 572 func TestRun_Errors(t *testing.T) { 573 wasmPath := filepath.Join(t.TempDir(), "test.wasm") 574 require.NoError(t, os.WriteFile(wasmPath, wasmWasiArg, 0o700)) 575 576 notWasmPath := filepath.Join(t.TempDir(), "bears.wasm") 577 require.NoError(t, os.WriteFile(notWasmPath, []byte("pooh"), 0o700)) 578 579 tests := []struct { 580 message string 581 args []string 582 }{ 583 { 584 message: "missing path to wasm file", 585 args: []string{}, 586 }, 587 { 588 message: "error reading wasm binary", 589 args: []string{"non-existent.wasm"}, 590 }, 591 { 592 message: "error compiling wasm binary", 593 args: []string{notWasmPath}, 594 }, 595 { 596 message: "invalid environment variable", 597 args: []string{"--env=ANIMAL", "testdata/wasi_env.wasm"}, 598 }, 599 { 600 message: "invalid mount", // not found 601 args: []string{"--mount=te", "testdata/wasi_env.wasm"}, 602 }, 603 { 604 message: "invalid cachedir", 605 args: []string{"--cachedir", notWasmPath, wasmPath}, 606 }, 607 { 608 message: "timeout duration may not be negative", 609 args: []string{"-timeout=-10s", wasmPath}, 610 }, 611 } 612 613 for _, tc := range tests { 614 tt := tc 615 t.Run(tt.message, func(t *testing.T) { 616 exitCode, _, stderr := runMain(t, "", append([]string{"run"}, tt.args...)) 617 618 require.Equal(t, 1, exitCode) 619 require.Contains(t, stderr, tt.message) 620 }) 621 } 622 } 623 624 var _ api.FunctionDefinition = importer{} 625 626 type importer struct { 627 internalapi.WazeroOnlyType 628 moduleName, name string 629 } 630 631 func (i importer) ModuleName() string { return "" } 632 func (i importer) Index() uint32 { return 0 } 633 func (i importer) Import() (moduleName, name string, isImport bool) { 634 return i.moduleName, i.name, true 635 } 636 func (i importer) ExportNames() []string { return nil } 637 func (i importer) Name() string { return "" } 638 func (i importer) DebugName() string { return "" } 639 func (i importer) GoFunction() interface{} { return nil } 640 func (i importer) ParamTypes() []api.ValueType { return nil } 641 func (i importer) ParamNames() []string { return nil } 642 func (i importer) ResultTypes() []api.ValueType { return nil } 643 func (i importer) ResultNames() []string { return nil } 644 645 func Test_detectImports(t *testing.T) { 646 tests := []struct { 647 message string 648 imports []api.FunctionDefinition 649 mode importMode 650 }{ 651 { 652 message: "no imports", 653 }, 654 { 655 message: "other imports", 656 imports: []api.FunctionDefinition{ 657 importer{internalapi.WazeroOnlyType{}, "env", "emscripten_notify_memory_growth"}, 658 }, 659 }, 660 { 661 message: "wasi", 662 imports: []api.FunctionDefinition{ 663 importer{internalapi.WazeroOnlyType{}, wasi_snapshot_preview1.ModuleName, "fd_read"}, 664 }, 665 mode: modeWasi, 666 }, 667 { 668 message: "unstable_wasi", 669 imports: []api.FunctionDefinition{ 670 importer{internalapi.WazeroOnlyType{}, "wasi_unstable", "fd_read"}, 671 }, 672 mode: modeWasiUnstable, 673 }, 674 { 675 message: "GOARCH=wasm GOOS=js", 676 imports: []api.FunctionDefinition{ 677 importer{internalapi.WazeroOnlyType{}, "go", "syscall/js.valueCall"}, 678 }, 679 mode: modeGo, 680 }, 681 } 682 683 for _, tc := range tests { 684 tt := tc 685 t.Run(tt.message, func(t *testing.T) { 686 mode := detectImports(tc.imports) 687 require.Equal(t, tc.mode, mode) 688 }) 689 } 690 } 691 692 func Test_logScopesFlag(t *testing.T) { 693 tests := []struct { 694 name string 695 values []string 696 expected logging.LogScopes 697 }{ 698 { 699 name: "defaults to none", 700 expected: logging.LogScopeNone, 701 }, 702 { 703 name: "ignores empty", 704 values: []string{""}, 705 expected: logging.LogScopeNone, 706 }, 707 { 708 name: "all", 709 values: []string{"all"}, 710 expected: logging.LogScopeAll, 711 }, 712 { 713 name: "clock", 714 values: []string{"clock"}, 715 expected: logging.LogScopeClock, 716 }, 717 { 718 name: "filesystem", 719 values: []string{"filesystem"}, 720 expected: logging.LogScopeFilesystem, 721 }, 722 { 723 name: "memory", 724 values: []string{"memory"}, 725 expected: logging.LogScopeMemory, 726 }, 727 { 728 name: "poll", 729 values: []string{"poll"}, 730 expected: logging.LogScopePoll, 731 }, 732 { 733 name: "random", 734 values: []string{"random"}, 735 expected: logging.LogScopeRandom, 736 }, 737 { 738 name: "clock filesystem poll random", 739 values: []string{"clock", "filesystem", "poll", "random"}, 740 expected: logging.LogScopeClock | logging.LogScopeFilesystem | logging.LogScopePoll | logging.LogScopeRandom, 741 }, 742 { 743 name: "clock,filesystem poll,random", 744 values: []string{"clock,filesystem", "poll,random"}, 745 expected: logging.LogScopeClock | logging.LogScopeFilesystem | logging.LogScopePoll | logging.LogScopeRandom, 746 }, 747 { 748 name: "all random", 749 values: []string{"all", "random"}, 750 expected: logging.LogScopeAll, 751 }, 752 } 753 754 for _, tt := range tests { 755 tc := tt 756 t.Run(tc.name, func(t *testing.T) { 757 f := logScopesFlag(0) 758 for _, v := range tc.values { 759 require.NoError(t, f.Set(v)) 760 } 761 require.Equal(t, tc.expected, logging.LogScopes(f)) 762 }) 763 } 764 } 765 766 func TestHelp(t *testing.T) { 767 exitCode, _, stderr := runMain(t, "", []string{"-h"}) 768 require.Equal(t, 0, exitCode) 769 fmt.Println(stderr) 770 require.Equal(t, `wazero CLI 771 772 Usage: 773 wazero <command> 774 775 Commands: 776 compile Pre-compiles a WebAssembly binary 777 run Runs a WebAssembly binary 778 version Displays the version of wazero CLI 779 `, stderr) 780 } 781 782 func runMain(t *testing.T, workdir string, args []string) (int, string, string) { 783 t.Helper() 784 785 // Use a workdir override if supplied. 786 if workdir != "" { 787 oldcwd, err := os.Getwd() 788 require.NoError(t, err) 789 790 require.NoError(t, os.Chdir(workdir)) 791 defer func() { 792 require.NoError(t, os.Chdir(oldcwd)) 793 }() 794 } 795 796 oldArgs := os.Args 797 t.Cleanup(func() { 798 os.Args = oldArgs 799 }) 800 os.Args = append([]string{"wazero"}, args...) 801 flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ContinueOnError) 802 803 stdout := new(bytes.Buffer) 804 stderr := new(bytes.Buffer) 805 exitCode := doMain(stdout, stderr) 806 return exitCode, stdout.String(), stderr.String() 807 } 808 809 // compileGoJS compiles "testdata/cat/cat.go" on demand as the binary generated 810 // is too big (1.6MB) to check into the source tree. 811 func compileGoJS() (err error) { 812 dir, err := os.Getwd() 813 if err != nil { 814 return err 815 } 816 817 srcDir := path.Join(dir, "testdata", "cat") 818 outPath := path.Join(srcDir, "cat-go.wasm") 819 820 // This doesn't add "-ldflags=-s -w", as the binary size only changes 28KB. 821 cmd := exec.Command("go", "build", "-o", outPath, ".") 822 cmd.Dir = srcDir 823 cmd.Env = append(os.Environ(), "GOARCH=wasm", "GOOS=js", "GOWASM=satconv,signext") 824 if out, err := cmd.CombinedOutput(); err != nil { 825 return fmt.Errorf("go build: %v\n%s", err, out) 826 } 827 828 wasmCatGo, err = os.ReadFile(outPath) 829 return 830 } 831 832 func exist(path string) error { 833 f, err := os.Open(path) 834 if err != nil { 835 return err 836 } 837 return f.Close() 838 }