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  }