github.com/tetratelabs/wazero@v1.2.1/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 "io/fs" 9 "net" 10 "net/http" 11 "runtime" 12 "strconv" 13 "strings" 14 "testing" 15 "testing/fstest" 16 "time" 17 18 "github.com/tetratelabs/wazero" 19 "github.com/tetratelabs/wazero/api" 20 experimentalsock "github.com/tetratelabs/wazero/experimental/sock" 21 "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1" 22 "github.com/tetratelabs/wazero/internal/fsapi" 23 internalsys "github.com/tetratelabs/wazero/internal/sys" 24 "github.com/tetratelabs/wazero/internal/testing/require" 25 "github.com/tetratelabs/wazero/sys" 26 ) 27 28 // sleepALittle directly slows down test execution. So, use this sparingly and 29 // only when so where proper signals are unavailable. 30 var sleepALittle = func() { time.Sleep(500 * time.Millisecond) } 31 32 // This file ensures that the behavior we've implemented not only the wasi 33 // spec, but also at least two compilers use of sdks. 34 35 // wasmCargoWasi was compiled from testdata/cargo-wasi/wasi.rs 36 // 37 //go:embed testdata/cargo-wasi/wasi.wasm 38 var wasmCargoWasi []byte 39 40 // wasmZigCc was compiled from testdata/zig-cc/wasi.c 41 // 42 //go:embed testdata/zig-cc/wasi.wasm 43 var wasmZigCc []byte 44 45 // wasmZig was compiled from testdata/zig/wasi.c 46 // 47 //go:embed testdata/zig/wasi.wasm 48 var wasmZig []byte 49 50 // wasmGotip is conditionally compiled from testdata/gotip/wasi.go 51 var wasmGotip []byte 52 53 func Test_fdReaddir_ls(t *testing.T) { 54 for toolchain, bin := range map[string][]byte{ 55 "cargo-wasi": wasmCargoWasi, 56 "zig-cc": wasmZigCc, 57 "zig": wasmZig, 58 } { 59 toolchain := toolchain 60 bin := bin 61 t.Run(toolchain, func(t *testing.T) { 62 expectDots := toolchain == "zig-cc" 63 testFdReaddirLs(t, bin, expectDots) 64 }) 65 } 66 } 67 68 func testFdReaddirLs(t *testing.T, bin []byte, expectDots bool) { 69 // TODO: make a subfs 70 moduleConfig := wazero.NewModuleConfig(). 71 WithFS(fstest.MapFS{ 72 "-": {}, 73 "a-": {Mode: fs.ModeDir}, 74 "ab-": {}, 75 }) 76 77 t.Run("empty directory", func(t *testing.T) { 78 console := compileAndRun(t, testCtx, moduleConfig.WithArgs("wasi", "ls", "./a-"), bin) 79 80 requireLsOut(t, "\n", expectDots, console) 81 }) 82 83 t.Run("not a directory", func(t *testing.T) { 84 console := compileAndRun(t, testCtx, moduleConfig.WithArgs("wasi", "ls", "-"), bin) 85 86 require.Equal(t, ` 87 ENOTDIR 88 `, "\n"+console) 89 }) 90 91 t.Run("directory with entries", func(t *testing.T) { 92 console := compileAndRun(t, testCtx, moduleConfig.WithArgs("wasi", "ls", "."), bin) 93 requireLsOut(t, ` 94 ./- 95 ./a- 96 ./ab- 97 `, expectDots, console) 98 }) 99 100 t.Run("directory with entries - read twice", func(t *testing.T) { 101 console := compileAndRun(t, testCtx, moduleConfig.WithArgs("wasi", "ls", ".", "repeat"), bin) 102 if expectDots { 103 require.Equal(t, ` 104 ./. 105 ./.. 106 ./- 107 ./a- 108 ./ab- 109 ./. 110 ./.. 111 ./- 112 ./a- 113 ./ab- 114 `, "\n"+console) 115 } else { 116 require.Equal(t, ` 117 ./- 118 ./a- 119 ./ab- 120 ./- 121 ./a- 122 ./ab- 123 `, "\n"+console) 124 } 125 }) 126 127 t.Run("directory with tons of entries", func(t *testing.T) { 128 testFS := fstest.MapFS{} 129 count := 8096 130 for i := 0; i < count; i++ { 131 testFS[strconv.Itoa(i)] = &fstest.MapFile{} 132 } 133 config := wazero.NewModuleConfig().WithFS(testFS).WithArgs("wasi", "ls", ".") 134 console := compileAndRun(t, testCtx, config, bin) 135 136 lines := strings.Split(console, "\n") 137 expected := count + 1 /* trailing newline */ 138 if expectDots { 139 expected += 2 140 } 141 require.Equal(t, expected, len(lines)) 142 }) 143 } 144 145 func requireLsOut(t *testing.T, expected string, expectDots bool, console string) { 146 dots := ` 147 ./. 148 ./.. 149 ` 150 if expectDots { 151 expected = dots + expected[1:] 152 } 153 require.Equal(t, expected, "\n"+console) 154 } 155 156 func Test_fdReaddir_stat(t *testing.T) { 157 for toolchain, bin := range map[string][]byte{ 158 "cargo-wasi": wasmCargoWasi, 159 "zig-cc": wasmZigCc, 160 "zig": wasmZig, 161 } { 162 toolchain := toolchain 163 bin := bin 164 t.Run(toolchain, func(t *testing.T) { 165 testFdReaddirStat(t, bin) 166 }) 167 } 168 } 169 170 func testFdReaddirStat(t *testing.T, bin []byte) { 171 moduleConfig := wazero.NewModuleConfig().WithArgs("wasi", "stat") 172 173 console := compileAndRun(t, testCtx, moduleConfig.WithFS(fstest.MapFS{}), bin) 174 175 // TODO: switch this to a real stat test 176 require.Equal(t, ` 177 stdin isatty: false 178 stdout isatty: false 179 stderr isatty: false 180 / isatty: false 181 `, "\n"+console) 182 } 183 184 func Test_preopen(t *testing.T) { 185 for toolchain, bin := range map[string][]byte{ 186 "zig": wasmZig, 187 } { 188 toolchain := toolchain 189 bin := bin 190 t.Run(toolchain, func(t *testing.T) { 191 testPreopen(t, bin) 192 }) 193 } 194 } 195 196 func testPreopen(t *testing.T, bin []byte) { 197 moduleConfig := wazero.NewModuleConfig().WithArgs("wasi", "preopen") 198 199 console := compileAndRun(t, testCtx, moduleConfig. 200 WithFSConfig(wazero.NewFSConfig(). 201 WithDirMount(".", "/"). 202 WithFSMount(fstest.MapFS{}, "/tmp")), bin) 203 204 require.Equal(t, ` 205 0: stdin 206 1: stdout 207 2: stderr 208 3: / 209 4: /tmp 210 `, "\n"+console) 211 } 212 213 func compileAndRun(t *testing.T, ctx context.Context, config wazero.ModuleConfig, bin []byte) (console string) { 214 return compileAndRunWithPreStart(t, ctx, config, bin, nil) 215 } 216 217 func compileAndRunWithPreStart(t *testing.T, ctx context.Context, config wazero.ModuleConfig, bin []byte, preStart func(t *testing.T, mod api.Module)) (console string) { 218 // same for console and stderr as sometimes the stack trace is in one or the other. 219 var consoleBuf bytes.Buffer 220 221 r := wazero.NewRuntime(ctx) 222 defer r.Close(ctx) 223 224 _, err := wasi_snapshot_preview1.Instantiate(ctx, r) 225 require.NoError(t, err) 226 227 mod, err := r.InstantiateWithConfig(ctx, bin, config. 228 WithStdout(&consoleBuf). 229 WithStderr(&consoleBuf). 230 WithStartFunctions()) // clear 231 require.NoError(t, err) 232 233 if preStart != nil { 234 preStart(t, mod) 235 } 236 237 _, err = mod.ExportedFunction("_start").Call(ctx) 238 if exitErr, ok := err.(*sys.ExitError); ok { 239 require.Zero(t, exitErr.ExitCode(), consoleBuf.String()) 240 } else { 241 require.NoError(t, err, consoleBuf.String()) 242 } 243 244 console = consoleBuf.String() 245 return 246 } 247 248 func Test_Poll(t *testing.T) { 249 // The following test cases replace Stdin with a custom reader. 250 // For more precise coverage, see poll_test.go. 251 252 tests := []struct { 253 name string 254 args []string 255 stdin fsapi.File 256 expectedOutput string 257 expectedTimeout time.Duration 258 }{ 259 { 260 name: "custom reader, data ready, not tty", 261 args: []string{"wasi", "poll"}, 262 stdin: &internalsys.StdinFile{Reader: strings.NewReader("test")}, 263 expectedOutput: "STDIN", 264 expectedTimeout: 0 * time.Millisecond, 265 }, 266 { 267 name: "custom reader, data ready, not tty, .5sec", 268 args: []string{"wasi", "poll", "0", "500"}, 269 stdin: &internalsys.StdinFile{Reader: strings.NewReader("test")}, 270 expectedOutput: "STDIN", 271 expectedTimeout: 0 * time.Millisecond, 272 }, 273 { 274 name: "custom reader, data ready, tty, .5sec", 275 args: []string{"wasi", "poll", "0", "500"}, 276 stdin: &ttyStdinFile{StdinFile: internalsys.StdinFile{Reader: strings.NewReader("test")}}, 277 expectedOutput: "STDIN", 278 expectedTimeout: 0 * time.Millisecond, 279 }, 280 { 281 name: "custom, blocking reader, no data, tty, .5sec", 282 args: []string{"wasi", "poll", "0", "500"}, 283 stdin: &neverReadyTtyStdinFile{StdinFile: internalsys.StdinFile{Reader: newBlockingReader(t)}}, 284 expectedOutput: "NOINPUT", 285 expectedTimeout: 500 * time.Millisecond, // always timeouts 286 }, 287 { 288 name: "eofReader, not tty, .5sec", 289 args: []string{"wasi", "poll", "0", "500"}, 290 stdin: &ttyStdinFile{StdinFile: internalsys.StdinFile{Reader: eofReader{}}}, 291 expectedOutput: "STDIN", 292 expectedTimeout: 0 * time.Millisecond, 293 }, 294 } 295 296 for _, tt := range tests { 297 tc := tt 298 t.Run(tc.name, func(t *testing.T) { 299 start := time.Now() 300 console := compileAndRunWithPreStart(t, testCtx, wazero.NewModuleConfig().WithArgs(tc.args...), wasmZigCc, 301 func(t *testing.T, mod api.Module) { 302 setStdin(t, mod, tc.stdin) 303 }) 304 elapsed := time.Since(start) 305 require.True(t, elapsed >= tc.expectedTimeout) 306 require.Equal(t, tc.expectedOutput+"\n", console) 307 }) 308 } 309 } 310 311 // eofReader is safer than reading from os.DevNull as it can never overrun operating system file descriptors. 312 type eofReader struct{} 313 314 // Read implements io.Reader 315 // Note: This doesn't use a pointer reference as it has no state and an empty struct doesn't allocate. 316 func (eofReader) Read([]byte) (int, error) { 317 return 0, io.EOF 318 } 319 320 func Test_Sleep(t *testing.T) { 321 moduleConfig := wazero.NewModuleConfig().WithArgs("wasi", "sleepmillis", "100").WithSysNanosleep() 322 start := time.Now() 323 console := compileAndRun(t, testCtx, moduleConfig, wasmZigCc) 324 require.True(t, time.Since(start) >= 100*time.Millisecond) 325 require.Equal(t, "OK\n", console) 326 } 327 328 func Test_Open(t *testing.T) { 329 for toolchain, bin := range map[string][]byte{ 330 "zig-cc": wasmZigCc, 331 } { 332 toolchain := toolchain 333 bin := bin 334 t.Run(toolchain, func(t *testing.T) { 335 testOpenReadOnly(t, bin) 336 testOpenWriteOnly(t, bin) 337 }) 338 } 339 } 340 341 func testOpenReadOnly(t *testing.T, bin []byte) { 342 testOpen(t, "rdonly", bin) 343 } 344 345 func testOpenWriteOnly(t *testing.T, bin []byte) { 346 testOpen(t, "wronly", bin) 347 } 348 349 func testOpen(t *testing.T, cmd string, bin []byte) { 350 t.Run(cmd, func(t *testing.T) { 351 moduleConfig := wazero.NewModuleConfig(). 352 WithArgs("wasi", "open-"+cmd). 353 WithFSConfig(wazero.NewFSConfig().WithDirMount(t.TempDir(), "/")) 354 355 console := compileAndRun(t, testCtx, moduleConfig, bin) 356 require.Equal(t, "OK", strings.TrimSpace(console)) 357 }) 358 } 359 360 func Test_Sock(t *testing.T) { 361 toolchains := map[string][]byte{ 362 "cargo-wasi": wasmCargoWasi, 363 "zig-cc": wasmZigCc, 364 } 365 if wasmGotip != nil { 366 toolchains["gotip"] = wasmGotip 367 } 368 369 for toolchain, bin := range toolchains { 370 toolchain := toolchain 371 bin := bin 372 t.Run(toolchain, func(t *testing.T) { 373 testSock(t, bin) 374 }) 375 } 376 } 377 378 func testSock(t *testing.T, bin []byte) { 379 sockCfg := experimentalsock.NewConfig().WithTCPListener("127.0.0.1", 0) 380 ctx := experimentalsock.WithConfig(testCtx, sockCfg) 381 moduleConfig := wazero.NewModuleConfig().WithArgs("wasi", "sock") 382 tcpAddrCh := make(chan *net.TCPAddr, 1) 383 ch := make(chan string, 1) 384 go func() { 385 ch <- compileAndRunWithPreStart(t, ctx, moduleConfig, bin, func(t *testing.T, mod api.Module) { 386 tcpAddrCh <- requireTCPListenerAddr(t, mod) 387 }) 388 }() 389 tcpAddr := <-tcpAddrCh 390 391 // Give a little time for _start to complete 392 sleepALittle() 393 394 // Now dial to the initial address, which should be now held by wazero. 395 conn, err := net.Dial("tcp", tcpAddr.String()) 396 require.NoError(t, err) 397 defer conn.Close() 398 399 n, err := conn.Write([]byte("wazero")) 400 console := <-ch 401 require.NotEqual(t, 0, n) 402 require.NoError(t, err) 403 require.Equal(t, "wazero\n", console) 404 } 405 406 func Test_HTTP(t *testing.T) { 407 if runtime.GOOS == "windows" { 408 t.Skip("syscall.Nonblocking() is not supported on wasip1+windows.") 409 } 410 toolchains := map[string][]byte{} 411 if wasmGotip != nil { 412 toolchains["gotip"] = wasmGotip 413 } 414 415 for toolchain, bin := range toolchains { 416 toolchain := toolchain 417 bin := bin 418 t.Run(toolchain, func(t *testing.T) { 419 testHTTP(t, bin) 420 }) 421 } 422 } 423 424 func testHTTP(t *testing.T, bin []byte) { 425 sockCfg := experimentalsock.NewConfig().WithTCPListener("127.0.0.1", 0) 426 ctx := experimentalsock.WithConfig(testCtx, sockCfg) 427 428 moduleConfig := wazero.NewModuleConfig(). 429 WithSysWalltime().WithSysNanotime(). // HTTP middleware uses both clocks 430 WithArgs("wasi", "http") 431 tcpAddrCh := make(chan *net.TCPAddr, 1) 432 ch := make(chan string, 1) 433 go func() { 434 ch <- compileAndRunWithPreStart(t, ctx, moduleConfig, bin, func(t *testing.T, mod api.Module) { 435 tcpAddrCh <- requireTCPListenerAddr(t, mod) 436 }) 437 }() 438 tcpAddr := <-tcpAddrCh 439 440 // Give a little time for _start to complete 441 sleepALittle() 442 443 // Now, send a POST to the address which we had pre-opened. 444 body := bytes.NewReader([]byte("wazero")) 445 req, err := http.NewRequest(http.MethodPost, "http://"+tcpAddr.String(), body) 446 require.NoError(t, err) 447 448 resp, err := http.DefaultClient.Do(req) 449 require.NoError(t, err) 450 defer resp.Body.Close() 451 452 require.Equal(t, 200, resp.StatusCode) 453 b, err := io.ReadAll(resp.Body) 454 require.NoError(t, err) 455 require.Equal(t, "wazero\n", string(b)) 456 457 console := <-ch 458 require.Equal(t, "", console) 459 }