github.com/bananabytelabs/wazero@v0.0.0-20240105073314-54b22a776da8/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/bananabytelabs/wazero"
    21  	"github.com/bananabytelabs/wazero/api"
    22  	experimentalsock "github.com/bananabytelabs/wazero/experimental/sock"
    23  	"github.com/bananabytelabs/wazero/imports/wasi_snapshot_preview1"
    24  	"github.com/bananabytelabs/wazero/internal/fsapi"
    25  	"github.com/bananabytelabs/wazero/internal/fstest"
    26  	internalsys "github.com/bananabytelabs/wazero/internal/sys"
    27  	"github.com/bananabytelabs/wazero/internal/testing/require"
    28  	"github.com/bananabytelabs/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  // wasmGotip is conditionally compiled from testdata/gotip/wasi.go
    44  var wasmGotip []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 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 /* 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 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  	// 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.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()) // clear
   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  // compileAndRunForked executes the test case with the wazero runtime in a separate process, and waits for it to terminate.
   273  //
   274  // Stdout is captured to a buffer, and stderr is dumped to os.Stderr.
   275  // It returns the capture buffer and boolean; the boolean is true if we are running in the outer process.
   276  // If it is false, it means we are running in the subprocess; all the verification logic should be handled
   277  // when the boolean is true.
   278  //
   279  // A typical usage pattern will be:
   280  //
   281  //	if buf, hasRun := compileAndRunForked(...); hasRun {
   282  //		validateContents(buf)
   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  	// We use the technique described in https://go.dev/talks/2014/testing.slide#23
   287  	// We check if we are running forked by ensuring that a "magic" environment variable is set.
   288  	if os.Getenv("_TEST_FORKED") != "1" {
   289  		// If said variable is not set, then we need to exec this same executable, specifying the name of test.
   290  		// We could use t.Name(), but because t may be arbitrarily nested, it is better to get the name of the test
   291  		// from a parameter.
   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  		// This is a test, so in case of success, it will include the "PASS\n" string:
   302  		// we remove that from the output, to return a clean stdout.
   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()) // clear
   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  	// The following test cases replace Stdin with a custom reader.
   327  	// For more precise coverage, see poll_test.go.
   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, // always timeouts
   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  // eofReader is safer than reading from os.DevNull as it can never overrun operating system file descriptors.
   389  type eofReader struct{}
   390  
   391  // Read implements io.Reader
   392  // Note: This doesn't use a pointer reference as it has no state and an empty struct doesn't allocate.
   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  	// Give a little time for _start to complete
   469  	sleepALittle()
   470  
   471  	// Now dial to the initial address, which should be now held by wazero.
   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  	// Nonblocking connections may contain error logging, we ignore those.
   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(). // HTTP middleware uses both clocks
   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  	// Give a little time for _start to complete
   516  	sleepALittle()
   517  
   518  	// Now, send a POST to the address which we had pre-opened.
   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(). // poll_oneoff requires nanotime.
   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  	// This test dumps a large Go source file to stdout. The generated result
   610  	// should be valid code, otherwise it means that stdout is corrupted.
   611  	//
   612  	// The error conditions are more easily reproduced by executing in a subprocess
   613  	// and capturing its stdout.
   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  }