github.com/tetratelabs/wazero@v1.7.3-0.20240513003603-48f702e154b5/imports/wasi_snapshot_preview1/wasi_stdlib_test.go (about) 1 package wasi_snapshot_preview1_test 2 3 import ( 4 "bytes" 5 "context" 6 _ "embed" 7 "io" 8 "net" 9 "net/http" 10 "os" 11 "os/exec" 12 "path" 13 "sort" 14 "strconv" 15 "strings" 16 "testing" 17 gofstest "testing/fstest" 18 "time" 19 20 "github.com/tetratelabs/wazero" 21 "github.com/tetratelabs/wazero/api" 22 experimentalsock "github.com/tetratelabs/wazero/experimental/sock" 23 "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1" 24 "github.com/tetratelabs/wazero/internal/fsapi" 25 "github.com/tetratelabs/wazero/internal/fstest" 26 internalsys "github.com/tetratelabs/wazero/internal/sys" 27 "github.com/tetratelabs/wazero/internal/testing/require" 28 "github.com/tetratelabs/wazero/sys" 29 ) 30 31 // sleepALittle directly slows down test execution. So, use this sparingly and 32 // only when so where proper signals are unavailable. 33 var sleepALittle = func() { time.Sleep(500 * time.Millisecond) } 34 35 // This file ensures that the behavior we've implemented not only the wasi 36 // spec, but also at least two compilers use of sdks. 37 38 // wasmCargoWasi was compiled from testdata/cargo-wasi/wasi.rs 39 // 40 //go:embed testdata/cargo-wasi/wasi.wasm 41 var wasmCargoWasi []byte 42 43 // wasmGo is conditionally compiled from testdata/go/wasi.go 44 var wasmGo []byte 45 46 // wasmTinyGo was compiled from testdata/tinygo/wasi.go 47 // 48 //go:embed testdata/tinygo/wasi.wasm 49 var wasmTinyGo []byte 50 51 // wasmZigCc was compiled from testdata/zig-cc/wasi.c 52 // 53 //go:embed testdata/zig-cc/wasi.wasm 54 var wasmZigCc []byte 55 56 // wasmZig was compiled from testdata/zig/wasi.c 57 // 58 //go:embed testdata/zig/wasi.wasm 59 var wasmZig []byte 60 61 func Test_fdReaddir_ls(t *testing.T) { 62 toolchains := map[string][]byte{ 63 "cargo-wasi": wasmCargoWasi, 64 "tinygo": wasmTinyGo, 65 "zig-cc": wasmZigCc, 66 "zig": wasmZig, 67 } 68 if wasmGo != nil { 69 toolchains["go"] = wasmGo 70 } 71 72 tmpDir := t.TempDir() 73 require.NoError(t, fstest.WriteTestFiles(tmpDir)) 74 75 tons := path.Join(tmpDir, "tons") 76 require.NoError(t, os.Mkdir(tons, 0o0777)) 77 for i := 0; i < direntCountTons; i++ { 78 require.NoError(t, os.WriteFile(path.Join(tons, strconv.Itoa(i)), nil, 0o0666)) 79 } 80 81 for toolchain, bin := range toolchains { 82 toolchain := toolchain 83 bin := bin 84 t.Run(toolchain, func(t *testing.T) { 85 var expectDots int 86 if toolchain == "zig-cc" { 87 expectDots = 1 88 } 89 testFdReaddirLs(t, bin, toolchain, tmpDir, expectDots) 90 }) 91 } 92 } 93 94 const direntCountTons = 8096 95 96 func testFdReaddirLs(t *testing.T, bin []byte, toolchain, rootDir string, expectDots int) { 97 t.Helper() 98 99 moduleConfig := wazero.NewModuleConfig(). 100 WithFSConfig(wazero.NewFSConfig(). 101 WithReadOnlyDirMount(path.Join(rootDir, "dir"), "/")) 102 103 t.Run("empty directory", func(t *testing.T) { 104 console := compileAndRun(t, testCtx, moduleConfig.WithArgs("wasi", "ls", "./a-"), bin) 105 106 requireLsOut(t, nil, expectDots, console) 107 }) 108 109 t.Run("not a directory", func(t *testing.T) { 110 console := compileAndRun(t, testCtx, moduleConfig.WithArgs("wasi", "ls", "-"), bin) 111 112 require.Equal(t, ` 113 ENOTDIR 114 `, "\n"+console) 115 }) 116 117 t.Run("directory with entries", func(t *testing.T) { 118 console := compileAndRun(t, testCtx, moduleConfig.WithArgs("wasi", "ls", "."), bin) 119 requireLsOut(t, []string{ 120 "./-", 121 "./a-", 122 "./ab-", 123 }, expectDots, console) 124 }) 125 126 t.Run("directory with entries - read twice", func(t *testing.T) { 127 if toolchain == "tinygo" { 128 t.Skip("https://github.com/tinygo-org/tinygo/issues/3823") 129 } 130 131 console := compileAndRun(t, testCtx, moduleConfig.WithArgs("wasi", "ls", ".", "repeat"), bin) 132 requireLsOut(t, []string{ 133 "./-", 134 "./a-", 135 "./ab-", 136 "./-", 137 "./a-", 138 "./ab-", 139 }, expectDots*2, console) 140 }) 141 142 t.Run("directory with tons of entries", func(t *testing.T) { 143 moduleConfig = wazero.NewModuleConfig(). 144 WithFSConfig(wazero.NewFSConfig(). 145 WithReadOnlyDirMount(path.Join(rootDir, "tons"), "/")). 146 WithArgs("wasi", "ls", ".") 147 148 console := compileAndRun(t, testCtx, moduleConfig, bin) 149 150 lines := strings.Split(console, "\n") 151 expected := direntCountTons + 1 /* trailing newline */ 152 expected += expectDots * 2 153 require.Equal(t, expected, len(lines)) 154 }) 155 } 156 157 func requireLsOut(t *testing.T, expected []string, expectDots int, console string) { 158 for i := 0; i < expectDots; i++ { 159 expected = append(expected, "./.", "./..") 160 } 161 162 actual := strings.Split(console, "\n") 163 sort.Strings(actual) // os directories are not lexicographic order 164 actual = actual[1:] // trailing newline 165 166 sort.Strings(expected) 167 if len(actual) == 0 { 168 require.Nil(t, expected) 169 } else { 170 require.Equal(t, expected, actual) 171 } 172 } 173 174 func Test_fdReaddir_stat(t *testing.T) { 175 toolchains := map[string][]byte{ 176 "cargo-wasi": wasmCargoWasi, 177 "tinygo": wasmTinyGo, 178 "zig-cc": wasmZigCc, 179 "zig": wasmZig, 180 } 181 if wasmGo != nil { 182 toolchains["go"] = wasmGo 183 } 184 185 for toolchain, bin := range toolchains { 186 toolchain := toolchain 187 bin := bin 188 t.Run(toolchain, func(t *testing.T) { 189 testFdReaddirStat(t, bin) 190 }) 191 } 192 } 193 194 func testFdReaddirStat(t *testing.T, bin []byte) { 195 moduleConfig := wazero.NewModuleConfig().WithArgs("wasi", "stat") 196 197 console := compileAndRun(t, testCtx, moduleConfig.WithFS(gofstest.MapFS{}), bin) 198 199 // TODO: switch this to a real stat test 200 require.Equal(t, ` 201 stdin isatty: false 202 stdout isatty: false 203 stderr isatty: false 204 / isatty: false 205 `, "\n"+console) 206 } 207 208 func Test_preopen(t *testing.T) { 209 for toolchain, bin := range map[string][]byte{ 210 "zig": wasmZig, 211 } { 212 toolchain := toolchain 213 bin := bin 214 t.Run(toolchain, func(t *testing.T) { 215 testPreopen(t, bin) 216 }) 217 } 218 } 219 220 func testPreopen(t *testing.T, bin []byte) { 221 moduleConfig := wazero.NewModuleConfig().WithArgs("wasi", "preopen") 222 223 console := compileAndRun(t, testCtx, moduleConfig. 224 WithFSConfig(wazero.NewFSConfig(). 225 WithDirMount(".", "/"). 226 WithFSMount(gofstest.MapFS{}, "/tmp")), bin) 227 228 require.Equal(t, ` 229 0: stdin 230 1: stdout 231 2: stderr 232 3: / 233 4: /tmp 234 `, "\n"+console) 235 } 236 237 func compileAndRun(t *testing.T, ctx context.Context, config wazero.ModuleConfig, bin []byte) (console string) { 238 return compileAndRunWithPreStart(t, ctx, config, bin, nil) 239 } 240 241 func compileAndRunWithPreStart(t *testing.T, ctx context.Context, config wazero.ModuleConfig, bin []byte, preStart func(t *testing.T, mod api.Module)) (console string) { 242 // same for console and stderr as sometimes the stack trace is in one or the other. 243 var consoleBuf bytes.Buffer 244 245 r := wazero.NewRuntimeWithConfig(ctx, runtimeCfg) 246 defer r.Close(ctx) 247 248 _, err := wasi_snapshot_preview1.Instantiate(ctx, r) 249 require.NoError(t, err) 250 251 compiled, err := r.CompileModule(ctx, bin) 252 require.NoError(t, err) 253 254 mod, err := r.InstantiateModule(ctx, compiled, config. 255 WithStdout(&consoleBuf). 256 WithStderr(&consoleBuf). 257 WithStartFunctions()) // clear 258 require.NoError(t, err) 259 260 if preStart != nil { 261 preStart(t, mod) 262 } 263 264 _, err = mod.ExportedFunction("_start").Call(ctx) 265 if exitErr, ok := err.(*sys.ExitError); ok { 266 require.Zero(t, exitErr.ExitCode(), consoleBuf.String()) 267 } else { 268 require.NoError(t, err, consoleBuf.String()) 269 } 270 271 console = consoleBuf.String() 272 return 273 } 274 275 func Test_Poll(t *testing.T) { 276 // The following test cases replace Stdin with a custom reader. 277 // For more precise coverage, see poll_test.go. 278 279 tests := []struct { 280 name string 281 args []string 282 stdin fsapi.File 283 expectedOutput string 284 expectedTimeout time.Duration 285 }{ 286 { 287 name: "custom reader, data ready, not tty", 288 args: []string{"wasi", "poll"}, 289 stdin: &internalsys.StdinFile{Reader: strings.NewReader("test")}, 290 expectedOutput: "STDIN", 291 expectedTimeout: 0 * time.Millisecond, 292 }, 293 { 294 name: "custom reader, data ready, not tty, .5sec", 295 args: []string{"wasi", "poll", "0", "500"}, 296 stdin: &internalsys.StdinFile{Reader: strings.NewReader("test")}, 297 expectedOutput: "STDIN", 298 expectedTimeout: 0 * time.Millisecond, 299 }, 300 { 301 name: "custom reader, data ready, tty, .5sec", 302 args: []string{"wasi", "poll", "0", "500"}, 303 stdin: &ttyStdinFile{StdinFile: internalsys.StdinFile{Reader: strings.NewReader("test")}}, 304 expectedOutput: "STDIN", 305 expectedTimeout: 0 * time.Millisecond, 306 }, 307 { 308 name: "custom, blocking reader, no data, tty, .5sec", 309 args: []string{"wasi", "poll", "0", "500"}, 310 stdin: &neverReadyTtyStdinFile{StdinFile: internalsys.StdinFile{Reader: newBlockingReader(t)}}, 311 expectedOutput: "NOINPUT", 312 expectedTimeout: 500 * time.Millisecond, // always timeouts 313 }, 314 { 315 name: "eofReader, not tty, .5sec", 316 args: []string{"wasi", "poll", "0", "500"}, 317 stdin: &ttyStdinFile{StdinFile: internalsys.StdinFile{Reader: eofReader{}}}, 318 expectedOutput: "STDIN", 319 expectedTimeout: 0 * time.Millisecond, 320 }, 321 } 322 323 for _, tt := range tests { 324 tc := tt 325 t.Run(tc.name, func(t *testing.T) { 326 start := time.Now() 327 console := compileAndRunWithPreStart(t, testCtx, wazero.NewModuleConfig().WithArgs(tc.args...), wasmZigCc, 328 func(t *testing.T, mod api.Module) { 329 setStdin(t, mod, tc.stdin) 330 }) 331 elapsed := time.Since(start) 332 require.True(t, elapsed >= tc.expectedTimeout) 333 require.Equal(t, tc.expectedOutput+"\n", console) 334 }) 335 } 336 } 337 338 // eofReader is safer than reading from os.DevNull as it can never overrun operating system file descriptors. 339 type eofReader struct{} 340 341 // Read implements io.Reader 342 // Note: This doesn't use a pointer reference as it has no state and an empty struct doesn't allocate. 343 func (eofReader) Read([]byte) (int, error) { 344 return 0, io.EOF 345 } 346 347 func Test_Sleep(t *testing.T) { 348 moduleConfig := wazero.NewModuleConfig().WithArgs("wasi", "sleepmillis", "100").WithSysNanosleep() 349 start := time.Now() 350 console := compileAndRun(t, testCtx, moduleConfig, wasmZigCc) 351 require.True(t, time.Since(start) >= 100*time.Millisecond) 352 require.Equal(t, "OK\n", console) 353 } 354 355 func Test_Open(t *testing.T) { 356 for toolchain, bin := range map[string][]byte{ 357 "zig-cc": wasmZigCc, 358 } { 359 toolchain := toolchain 360 bin := bin 361 t.Run(toolchain, func(t *testing.T) { 362 testOpenReadOnly(t, bin) 363 testOpenWriteOnly(t, bin) 364 }) 365 } 366 } 367 368 func testOpenReadOnly(t *testing.T, bin []byte) { 369 testOpen(t, "rdonly", bin) 370 } 371 372 func testOpenWriteOnly(t *testing.T, bin []byte) { 373 testOpen(t, "wronly", bin) 374 } 375 376 func testOpen(t *testing.T, cmd string, bin []byte) { 377 t.Run(cmd, func(t *testing.T) { 378 moduleConfig := wazero.NewModuleConfig(). 379 WithArgs("wasi", "open-"+cmd). 380 WithFSConfig(wazero.NewFSConfig().WithDirMount(t.TempDir(), "/")) 381 382 console := compileAndRun(t, testCtx, moduleConfig, bin) 383 require.Equal(t, "OK", strings.TrimSpace(console)) 384 }) 385 } 386 387 func Test_Sock(t *testing.T) { 388 toolchains := map[string][]byte{ 389 "cargo-wasi": wasmCargoWasi, 390 "zig-cc": wasmZigCc, 391 } 392 if wasmGo != nil { 393 toolchains["go"] = wasmGo 394 } 395 396 for toolchain, bin := range toolchains { 397 toolchain := toolchain 398 bin := bin 399 t.Run(toolchain, func(t *testing.T) { 400 testSock(t, bin) 401 }) 402 } 403 } 404 405 func testSock(t *testing.T, bin []byte) { 406 sockCfg := experimentalsock.NewConfig().WithTCPListener("127.0.0.1", 0) 407 ctx := experimentalsock.WithConfig(testCtx, sockCfg) 408 moduleConfig := wazero.NewModuleConfig().WithArgs("wasi", "sock") 409 tcpAddrCh := make(chan *net.TCPAddr, 1) 410 ch := make(chan string, 1) 411 go func() { 412 ch <- compileAndRunWithPreStart(t, ctx, moduleConfig, bin, func(t *testing.T, mod api.Module) { 413 tcpAddrCh <- requireTCPListenerAddr(t, mod) 414 }) 415 }() 416 tcpAddr := <-tcpAddrCh 417 418 // Give a little time for _start to complete 419 sleepALittle() 420 421 // Now dial to the initial address, which should be now held by wazero. 422 conn, err := net.Dial("tcp", tcpAddr.String()) 423 require.NoError(t, err) 424 defer conn.Close() 425 426 n, err := conn.Write([]byte("wazero")) 427 console := <-ch 428 require.NotEqual(t, 0, n) 429 require.NoError(t, err) 430 // Nonblocking connections may contain error logging, we ignore those. 431 require.Equal(t, "wazero\n", console[len(console)-7:]) 432 } 433 434 func Test_HTTP(t *testing.T) { 435 toolchains := map[string][]byte{} 436 if wasmGo != nil { 437 toolchains["go"] = wasmGo 438 } 439 440 for toolchain, bin := range toolchains { 441 toolchain := toolchain 442 bin := bin 443 t.Run(toolchain, func(t *testing.T) { 444 testHTTP(t, bin) 445 }) 446 } 447 } 448 449 func testHTTP(t *testing.T, bin []byte) { 450 sockCfg := experimentalsock.NewConfig().WithTCPListener("127.0.0.1", 0) 451 ctx := experimentalsock.WithConfig(testCtx, sockCfg) 452 453 moduleConfig := wazero.NewModuleConfig(). 454 WithSysWalltime().WithSysNanotime(). // HTTP middleware uses both clocks 455 WithArgs("wasi", "http") 456 tcpAddrCh := make(chan *net.TCPAddr, 1) 457 ch := make(chan string, 1) 458 go func() { 459 ch <- compileAndRunWithPreStart(t, ctx, moduleConfig, bin, func(t *testing.T, mod api.Module) { 460 tcpAddrCh <- requireTCPListenerAddr(t, mod) 461 }) 462 }() 463 tcpAddr := <-tcpAddrCh 464 465 // Give a little time for _start to complete 466 sleepALittle() 467 468 // Now, send a POST to the address which we had pre-opened. 469 body := bytes.NewReader([]byte("wazero")) 470 req, err := http.NewRequest(http.MethodPost, "http://"+tcpAddr.String(), body) 471 require.NoError(t, err) 472 473 resp, err := http.DefaultClient.Do(req) 474 require.NoError(t, err) 475 defer resp.Body.Close() 476 477 require.Equal(t, 200, resp.StatusCode) 478 b, err := io.ReadAll(resp.Body) 479 require.NoError(t, err) 480 require.Equal(t, "wazero\n", string(b)) 481 482 console := <-ch 483 require.Equal(t, "", console) 484 } 485 486 func Test_Stdin(t *testing.T) { 487 toolchains := map[string][]byte{} 488 if wasmGo != nil { 489 toolchains["go"] = wasmGo 490 } 491 492 for toolchain, bin := range toolchains { 493 toolchain := toolchain 494 bin := bin 495 t.Run(toolchain, func(t *testing.T) { 496 testStdin(t, bin) 497 }) 498 } 499 } 500 501 func testStdin(t *testing.T, bin []byte) { 502 stdinReader, stdinWriter, err := os.Pipe() 503 require.NoError(t, err) 504 stdoutReader, stdoutWriter, err := os.Pipe() 505 require.NoError(t, err) 506 defer func() { 507 stdinReader.Close() 508 stdinWriter.Close() 509 stdoutReader.Close() 510 stdoutReader.Close() 511 }() 512 require.NoError(t, err) 513 moduleConfig := wazero.NewModuleConfig(). 514 WithSysNanotime(). // poll_oneoff requires nanotime. 515 WithArgs("wasi", "stdin"). 516 WithStdin(stdinReader). 517 WithStdout(stdoutWriter) 518 ch := make(chan struct{}, 1) 519 go func() { 520 defer close(ch) 521 r := wazero.NewRuntimeWithConfig(testCtx, runtimeCfg) 522 defer func() { 523 require.NoError(t, r.Close(testCtx)) 524 }() 525 526 _, err := wasi_snapshot_preview1.Instantiate(testCtx, r) 527 require.NoError(t, err) 528 529 compiled, err := r.CompileModule(testCtx, wasmGo) 530 require.NoError(t, err) 531 532 _, err = r.InstantiateModule(testCtx, compiled, moduleConfig) // clear 533 require.NoError(t, err) 534 }() 535 536 time.Sleep(1 * time.Second) 537 buf := make([]byte, 21) 538 _, _ = stdoutReader.Read(buf) 539 require.Equal(t, "waiting for stdin...\n", string(buf)) 540 _, _ = stdinWriter.WriteString("foo") 541 _ = stdinWriter.Close() 542 buf = make([]byte, 3) 543 _, _ = stdoutReader.Read(buf) 544 require.Equal(t, "foo", string(buf)) 545 <-ch 546 } 547 548 func Test_LargeStdout(t *testing.T) { 549 if wasmGo != nil { 550 var buf bytes.Buffer 551 r := wazero.NewRuntimeWithConfig(testCtx, runtimeCfg) 552 defer func() { 553 require.NoError(t, r.Close(testCtx)) 554 }() 555 556 _, err := wasi_snapshot_preview1.Instantiate(testCtx, r) 557 require.NoError(t, err) 558 559 compiled, err := r.CompileModule(testCtx, wasmGo) 560 require.NoError(t, err) 561 562 _, err = r.InstantiateModule(testCtx, compiled, wazero.NewModuleConfig(). 563 WithArgs("wasi", "largestdout"). 564 WithStdout(&buf)) // clear 565 require.NoError(t, err) 566 567 tempDir := t.TempDir() 568 temp, err := os.Create(joinPath(tempDir, "out.go")) 569 require.NoError(t, err) 570 571 // Check if the output Go source code is valid. 572 _, _ = temp.Write(buf.Bytes()) 573 require.NoError(t, temp.Close()) 574 cmd := exec.CommandContext(testCtx, "go", "build", "-o", 575 joinPath(tempDir, "outbin"), temp.Name()) 576 require.NoError(t, err) 577 output, err := cmd.CombinedOutput() 578 require.NoError(t, err, string(output)) 579 } 580 }