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  }