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
32
33 var sleepALittle = func() { time.Sleep(500 * time.Millisecond) }
34
35
36
37
38
39
40
41 var wasmCargoWasi []byte
42
43
44 var wasmGotip []byte
45
46
47
48
49 var wasmTinyGo []byte
50
51
52
53
54 var wasmZigCc []byte
55
56
57
58
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 wasmGotip != nil {
69 toolchains["gotip"] = wasmGotip
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
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)
164 actual = actual[1:]
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 wasmGotip != nil {
182 toolchains["gotip"] = wasmGotip
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
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
243 var consoleBuf bytes.Buffer
244
245 r := wazero.NewRuntime(ctx)
246 defer r.Close(ctx)
247
248 _, err := wasi_snapshot_preview1.Instantiate(ctx, r)
249 require.NoError(t, err)
250
251 mod, err := r.InstantiateWithConfig(ctx, bin, config.
252 WithStdout(&consoleBuf).
253 WithStderr(&consoleBuf).
254 WithStartFunctions())
255 require.NoError(t, err)
256
257 if preStart != nil {
258 preStart(t, mod)
259 }
260
261 _, err = mod.ExportedFunction("_start").Call(ctx)
262 if exitErr, ok := err.(*sys.ExitError); ok {
263 require.Zero(t, exitErr.ExitCode(), consoleBuf.String())
264 } else {
265 require.NoError(t, err, consoleBuf.String())
266 }
267
268 console = consoleBuf.String()
269 return
270 }
271
272
273
274
275
276
277
278
279
280
281
282
283
284 func compileAndRunForked(t *testing.T, ctx context.Context, config wazero.ModuleConfig, tname string, bin []byte) ([]byte, bool) {
285 var buf bytes.Buffer
286
287
288 if os.Getenv("_TEST_FORKED") != "1" {
289
290
291
292 cmd := exec.Command(os.Args[0], "-test.run", tname)
293 cmd.Stdout = &buf
294 cmd.Stderr = os.Stderr
295 cmd.Env = append(os.Environ(), "_TEST_FORKED=1")
296 err := cmd.Run()
297 if e, ok := err.(*exec.ExitError); ok && !e.Success() {
298 require.NoError(t, e, "The test quit with an error code: %v\n", e)
299 }
300 res := buf.Bytes()
301
302
303 return res[0 : len(res)-len("PASS\n")], true
304 }
305
306 r := wazero.NewRuntime(ctx)
307 defer r.Close(ctx)
308
309 _, err := wasi_snapshot_preview1.Instantiate(ctx, r)
310 require.NoError(t, err)
311
312 mod, err := r.InstantiateWithConfig(ctx, bin, config.
313 WithStartFunctions())
314 require.NoError(t, err)
315
316 _, err = mod.ExportedFunction("_start").Call(ctx)
317 if exitErr, ok := err.(*sys.ExitError); ok {
318 require.Zero(t, exitErr.ExitCode())
319 } else {
320 require.NoError(t, err)
321 }
322 return nil, false
323 }
324
325 func Test_Poll(t *testing.T) {
326
327
328
329 tests := []struct {
330 name string
331 args []string
332 stdin fsapi.File
333 expectedOutput string
334 expectedTimeout time.Duration
335 }{
336 {
337 name: "custom reader, data ready, not tty",
338 args: []string{"wasi", "poll"},
339 stdin: &internalsys.StdinFile{Reader: strings.NewReader("test")},
340 expectedOutput: "STDIN",
341 expectedTimeout: 0 * time.Millisecond,
342 },
343 {
344 name: "custom reader, data ready, not tty, .5sec",
345 args: []string{"wasi", "poll", "0", "500"},
346 stdin: &internalsys.StdinFile{Reader: strings.NewReader("test")},
347 expectedOutput: "STDIN",
348 expectedTimeout: 0 * time.Millisecond,
349 },
350 {
351 name: "custom reader, data ready, tty, .5sec",
352 args: []string{"wasi", "poll", "0", "500"},
353 stdin: &ttyStdinFile{StdinFile: internalsys.StdinFile{Reader: strings.NewReader("test")}},
354 expectedOutput: "STDIN",
355 expectedTimeout: 0 * time.Millisecond,
356 },
357 {
358 name: "custom, blocking reader, no data, tty, .5sec",
359 args: []string{"wasi", "poll", "0", "500"},
360 stdin: &neverReadyTtyStdinFile{StdinFile: internalsys.StdinFile{Reader: newBlockingReader(t)}},
361 expectedOutput: "NOINPUT",
362 expectedTimeout: 500 * time.Millisecond,
363 },
364 {
365 name: "eofReader, not tty, .5sec",
366 args: []string{"wasi", "poll", "0", "500"},
367 stdin: &ttyStdinFile{StdinFile: internalsys.StdinFile{Reader: eofReader{}}},
368 expectedOutput: "STDIN",
369 expectedTimeout: 0 * time.Millisecond,
370 },
371 }
372
373 for _, tt := range tests {
374 tc := tt
375 t.Run(tc.name, func(t *testing.T) {
376 start := time.Now()
377 console := compileAndRunWithPreStart(t, testCtx, wazero.NewModuleConfig().WithArgs(tc.args...), wasmZigCc,
378 func(t *testing.T, mod api.Module) {
379 setStdin(t, mod, tc.stdin)
380 })
381 elapsed := time.Since(start)
382 require.True(t, elapsed >= tc.expectedTimeout)
383 require.Equal(t, tc.expectedOutput+"\n", console)
384 })
385 }
386 }
387
388
389 type eofReader struct{}
390
391
392
393 func (eofReader) Read([]byte) (int, error) {
394 return 0, io.EOF
395 }
396
397 func Test_Sleep(t *testing.T) {
398 moduleConfig := wazero.NewModuleConfig().WithArgs("wasi", "sleepmillis", "100").WithSysNanosleep()
399 start := time.Now()
400 console := compileAndRun(t, testCtx, moduleConfig, wasmZigCc)
401 require.True(t, time.Since(start) >= 100*time.Millisecond)
402 require.Equal(t, "OK\n", console)
403 }
404
405 func Test_Open(t *testing.T) {
406 for toolchain, bin := range map[string][]byte{
407 "zig-cc": wasmZigCc,
408 } {
409 toolchain := toolchain
410 bin := bin
411 t.Run(toolchain, func(t *testing.T) {
412 testOpenReadOnly(t, bin)
413 testOpenWriteOnly(t, bin)
414 })
415 }
416 }
417
418 func testOpenReadOnly(t *testing.T, bin []byte) {
419 testOpen(t, "rdonly", bin)
420 }
421
422 func testOpenWriteOnly(t *testing.T, bin []byte) {
423 testOpen(t, "wronly", bin)
424 }
425
426 func testOpen(t *testing.T, cmd string, bin []byte) {
427 t.Run(cmd, func(t *testing.T) {
428 moduleConfig := wazero.NewModuleConfig().
429 WithArgs("wasi", "open-"+cmd).
430 WithFSConfig(wazero.NewFSConfig().WithDirMount(t.TempDir(), "/"))
431
432 console := compileAndRun(t, testCtx, moduleConfig, bin)
433 require.Equal(t, "OK", strings.TrimSpace(console))
434 })
435 }
436
437 func Test_Sock(t *testing.T) {
438 toolchains := map[string][]byte{
439 "cargo-wasi": wasmCargoWasi,
440 "zig-cc": wasmZigCc,
441 }
442 if wasmGotip != nil {
443 toolchains["gotip"] = wasmGotip
444 }
445
446 for toolchain, bin := range toolchains {
447 toolchain := toolchain
448 bin := bin
449 t.Run(toolchain, func(t *testing.T) {
450 testSock(t, bin)
451 })
452 }
453 }
454
455 func testSock(t *testing.T, bin []byte) {
456 sockCfg := experimentalsock.NewConfig().WithTCPListener("127.0.0.1", 0)
457 ctx := experimentalsock.WithConfig(testCtx, sockCfg)
458 moduleConfig := wazero.NewModuleConfig().WithArgs("wasi", "sock")
459 tcpAddrCh := make(chan *net.TCPAddr, 1)
460 ch := make(chan string, 1)
461 go func() {
462 ch <- compileAndRunWithPreStart(t, ctx, moduleConfig, bin, func(t *testing.T, mod api.Module) {
463 tcpAddrCh <- requireTCPListenerAddr(t, mod)
464 })
465 }()
466 tcpAddr := <-tcpAddrCh
467
468
469 sleepALittle()
470
471
472 conn, err := net.Dial("tcp", tcpAddr.String())
473 require.NoError(t, err)
474 defer conn.Close()
475
476 n, err := conn.Write([]byte("wazero"))
477 console := <-ch
478 require.NotEqual(t, 0, n)
479 require.NoError(t, err)
480
481 require.Equal(t, "wazero\n", console[len(console)-7:])
482 }
483
484 func Test_HTTP(t *testing.T) {
485 toolchains := map[string][]byte{}
486 if wasmGotip != nil {
487 toolchains["gotip"] = wasmGotip
488 }
489
490 for toolchain, bin := range toolchains {
491 toolchain := toolchain
492 bin := bin
493 t.Run(toolchain, func(t *testing.T) {
494 testHTTP(t, bin)
495 })
496 }
497 }
498
499 func testHTTP(t *testing.T, bin []byte) {
500 sockCfg := experimentalsock.NewConfig().WithTCPListener("127.0.0.1", 0)
501 ctx := experimentalsock.WithConfig(testCtx, sockCfg)
502
503 moduleConfig := wazero.NewModuleConfig().
504 WithSysWalltime().WithSysNanotime().
505 WithArgs("wasi", "http")
506 tcpAddrCh := make(chan *net.TCPAddr, 1)
507 ch := make(chan string, 1)
508 go func() {
509 ch <- compileAndRunWithPreStart(t, ctx, moduleConfig, bin, func(t *testing.T, mod api.Module) {
510 tcpAddrCh <- requireTCPListenerAddr(t, mod)
511 })
512 }()
513 tcpAddr := <-tcpAddrCh
514
515
516 sleepALittle()
517
518
519 body := bytes.NewReader([]byte("wazero"))
520 req, err := http.NewRequest(http.MethodPost, "http://"+tcpAddr.String(), body)
521 require.NoError(t, err)
522
523 resp, err := http.DefaultClient.Do(req)
524 require.NoError(t, err)
525 defer resp.Body.Close()
526
527 require.Equal(t, 200, resp.StatusCode)
528 b, err := io.ReadAll(resp.Body)
529 require.NoError(t, err)
530 require.Equal(t, "wazero\n", string(b))
531
532 console := <-ch
533 require.Equal(t, "", console)
534 }
535
536 func Test_Stdin(t *testing.T) {
537 toolchains := map[string][]byte{}
538 if wasmGotip != nil {
539 toolchains["gotip"] = wasmGotip
540 }
541
542 for toolchain, bin := range toolchains {
543 toolchain := toolchain
544 bin := bin
545 t.Run(toolchain, func(t *testing.T) {
546 testStdin(t, bin)
547 })
548 }
549 }
550
551 func testStdin(t *testing.T, bin []byte) {
552 stdinReader, stdinWriter, err := os.Pipe()
553 require.NoError(t, err)
554 stdoutReader, stdoutWriter, err := os.Pipe()
555 require.NoError(t, err)
556 defer func() {
557 stdinReader.Close()
558 stdinWriter.Close()
559 stdoutReader.Close()
560 stdoutReader.Close()
561 }()
562 require.NoError(t, err)
563 moduleConfig := wazero.NewModuleConfig().
564 WithSysNanotime().
565 WithArgs("wasi", "stdin").
566 WithStdin(stdinReader).
567 WithStdout(stdoutWriter)
568 ch := make(chan struct{}, 1)
569 go func() {
570 defer close(ch)
571
572 r := wazero.NewRuntime(testCtx)
573 defer r.Close(testCtx)
574 _, err := wasi_snapshot_preview1.Instantiate(testCtx, r)
575 require.NoError(t, err)
576 _, err = r.InstantiateWithConfig(testCtx, bin, moduleConfig)
577 require.NoError(t, err)
578 }()
579
580 time.Sleep(1 * time.Second)
581 buf := make([]byte, 21)
582 _, _ = stdoutReader.Read(buf)
583 require.Equal(t, "waiting for stdin...\n", string(buf))
584 _, _ = stdinWriter.WriteString("foo")
585 _ = stdinWriter.Close()
586 buf = make([]byte, 3)
587 _, _ = stdoutReader.Read(buf)
588 require.Equal(t, "foo", string(buf))
589 <-ch
590 }
591
592 func Test_LargeStdout(t *testing.T) {
593 toolchains := map[string][]byte{}
594 if wasmGotip != nil {
595 toolchains["gotip"] = wasmGotip
596 }
597
598 for toolchain, bin := range toolchains {
599 toolchain := toolchain
600 bin := bin
601 name := t.Name()
602 t.Run(toolchain, func(t *testing.T) {
603 testLargeStdout(t, name, bin)
604 })
605 }
606 }
607
608 func testLargeStdout(t *testing.T, tname string, bin []byte) {
609
610
611
612
613
614 if buf, hasRun := compileAndRunForked(t, testCtx, wazero.NewModuleConfig().
615 WithArgs("wasi", "largestdout").
616 WithStdout(os.Stdout), tname, bin); hasRun {
617
618 tempDir := t.TempDir()
619 temp, err := os.Create(joinPath(tempDir, "out.go"))
620 require.NoError(t, err)
621 defer temp.Close()
622
623 require.NoError(t, err)
624 _, _ = temp.Write(buf)
625 _ = temp.Close()
626
627 gotipBin, err := findGotipBin()
628 require.NoError(t, err)
629
630 cmd := exec.CommandContext(testCtx, gotipBin, "build", "-o",
631 joinPath(tempDir, "outbin"), temp.Name())
632 require.NoError(t, err)
633 output, err := cmd.CombinedOutput()
634 require.NoError(t, err, string(output))
635 }
636 }
637
View as plain text