github.com/tetratelabs/wazero@v1.2.1/cmd/wazero/wazero_test.go (about)

     1  package main
     2  
     3  import (
     4  	"bytes"
     5  	_ "embed"
     6  	"flag"
     7  	"fmt"
     8  	"log"
     9  	"os"
    10  	"os/exec"
    11  	"path"
    12  	"path/filepath"
    13  	"runtime"
    14  	"strings"
    15  	"testing"
    16  
    17  	"github.com/tetratelabs/wazero/api"
    18  	"github.com/tetratelabs/wazero/experimental/logging"
    19  	"github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
    20  	"github.com/tetratelabs/wazero/internal/internalapi"
    21  	"github.com/tetratelabs/wazero/internal/platform"
    22  	"github.com/tetratelabs/wazero/internal/testing/require"
    23  	"github.com/tetratelabs/wazero/internal/version"
    24  	"github.com/tetratelabs/wazero/sys"
    25  )
    26  
    27  //go:embed testdata/infinite_loop.wasm
    28  var wasmInfiniteLoop []byte
    29  
    30  //go:embed testdata/wasi_arg.wasm
    31  var wasmWasiArg []byte
    32  
    33  //go:embed testdata/wasi_env.wasm
    34  var wasmWasiEnv []byte
    35  
    36  //go:embed testdata/wasi_fd.wasm
    37  var wasmWasiFd []byte
    38  
    39  //go:embed testdata/wasi_random_get.wasm
    40  var wasmWasiRandomGet []byte
    41  
    42  // wasmCatGo is compiled on demand with `GOARCH=wasm GOOS=js`
    43  var wasmCatGo []byte
    44  
    45  //go:embed testdata/cat/cat-tinygo.wasm
    46  var wasmCatTinygo []byte
    47  
    48  //go:embed testdata/exit_on_start_unstable.wasm
    49  var wasmWasiUnstable []byte
    50  
    51  func TestMain(m *testing.M) {
    52  	// For some reason, riscv64 fails to see directory listings.
    53  	if a := runtime.GOARCH; a == "riscv64" {
    54  		log.Println("main: skipping due to not yet supported GOARCH:", a)
    55  		os.Exit(0)
    56  	}
    57  
    58  	// Notably our scratch containers don't have go, so don't fail tests.
    59  	if err := compileGoJS(); err != nil {
    60  		log.Println("main: Skipping GOARCH=wasm GOOS=js tests due to:", err)
    61  		os.Exit(0)
    62  	}
    63  	os.Exit(m.Run())
    64  }
    65  
    66  func TestCompile(t *testing.T) {
    67  	tmpDir, oldwd := requireChdirToTemp(t)
    68  	defer os.Chdir(oldwd) //nolint
    69  
    70  	wasmPath := filepath.Join(tmpDir, "test.wasm")
    71  	require.NoError(t, os.WriteFile(wasmPath, wasmWasiArg, 0o600))
    72  
    73  	existingDir1 := filepath.Join(tmpDir, "existing1")
    74  	require.NoError(t, os.Mkdir(existingDir1, 0o700))
    75  	existingDir2 := filepath.Join(tmpDir, "existing2")
    76  	require.NoError(t, os.Mkdir(existingDir2, 0o700))
    77  
    78  	cpuProfile := filepath.Join(t.TempDir(), "cpu.out")
    79  	memProfile := filepath.Join(t.TempDir(), "mem.out")
    80  
    81  	tests := []struct {
    82  		name       string
    83  		wazeroOpts []string
    84  		test       func(t *testing.T)
    85  	}{
    86  		{
    87  			name: "no opts",
    88  		},
    89  		{
    90  			name:       "cachedir existing absolute",
    91  			wazeroOpts: []string{"--cachedir=" + existingDir1},
    92  			test: func(t *testing.T) {
    93  				entries, err := os.ReadDir(existingDir1)
    94  				require.NoError(t, err)
    95  				require.True(t, len(entries) > 0)
    96  			},
    97  		},
    98  		{
    99  			name:       "cachedir existing relative",
   100  			wazeroOpts: []string{"--cachedir=existing2"},
   101  			test: func(t *testing.T) {
   102  				entries, err := os.ReadDir(existingDir2)
   103  				require.NoError(t, err)
   104  				require.True(t, len(entries) > 0)
   105  			},
   106  		},
   107  		{
   108  			name:       "cachedir new absolute",
   109  			wazeroOpts: []string{"--cachedir=" + path.Join(tmpDir, "new1")},
   110  			test: func(t *testing.T) {
   111  				entries, err := os.ReadDir("new1")
   112  				require.NoError(t, err)
   113  				require.True(t, len(entries) > 0)
   114  			},
   115  		},
   116  		{
   117  			name:       "cachedir new relative",
   118  			wazeroOpts: []string{"--cachedir=new2"},
   119  			test: func(t *testing.T) {
   120  				entries, err := os.ReadDir("new2")
   121  				require.NoError(t, err)
   122  				require.True(t, len(entries) > 0)
   123  			},
   124  		},
   125  		{
   126  			name:       "enable cpu profiling",
   127  			wazeroOpts: []string{"-cpuprofile=" + cpuProfile},
   128  			test: func(t *testing.T) {
   129  				require.NoError(t, exist(cpuProfile))
   130  			},
   131  		},
   132  		{
   133  			name:       "enable memory profiling",
   134  			wazeroOpts: []string{"-memprofile=" + memProfile},
   135  			test: func(t *testing.T) {
   136  				require.NoError(t, exist(memProfile))
   137  			},
   138  		},
   139  	}
   140  
   141  	for _, tc := range tests {
   142  		tt := tc
   143  		t.Run(tt.name, func(t *testing.T) {
   144  			args := append([]string{"compile"}, tt.wazeroOpts...)
   145  			args = append(args, wasmPath)
   146  			exitCode, stdout, stderr := runMain(t, "", args)
   147  			require.Zero(t, stderr)
   148  			require.Equal(t, 0, exitCode, stderr)
   149  			require.Zero(t, stdout)
   150  			if test := tt.test; test != nil {
   151  				test(t)
   152  			}
   153  		})
   154  	}
   155  }
   156  
   157  func requireChdirToTemp(t *testing.T) (string, string) {
   158  	tmpDir := t.TempDir()
   159  	oldwd, err := os.Getwd()
   160  	require.NoError(t, err)
   161  	require.NoError(t, os.Chdir(tmpDir))
   162  	return tmpDir, oldwd
   163  }
   164  
   165  func TestCompile_Errors(t *testing.T) {
   166  	tmpDir := t.TempDir()
   167  
   168  	wasmPath := filepath.Join(tmpDir, "test.wasm")
   169  	require.NoError(t, os.WriteFile(wasmPath, wasmWasiArg, 0o600))
   170  
   171  	notWasmPath := filepath.Join(tmpDir, "bears.wasm")
   172  	require.NoError(t, os.WriteFile(notWasmPath, []byte("pooh"), 0o600))
   173  
   174  	tests := []struct {
   175  		message string
   176  		args    []string
   177  	}{
   178  		{
   179  			message: "missing path to wasm file",
   180  			args:    []string{},
   181  		},
   182  		{
   183  			message: "error reading wasm binary",
   184  			args:    []string{"non-existent.wasm"},
   185  		},
   186  		{
   187  			message: "error compiling wasm binary",
   188  			args:    []string{notWasmPath},
   189  		},
   190  		{
   191  			message: "invalid cachedir",
   192  			args:    []string{"--cachedir", notWasmPath, wasmPath},
   193  		},
   194  	}
   195  
   196  	for _, tc := range tests {
   197  		tt := tc
   198  		t.Run(tt.message, func(t *testing.T) {
   199  			exitCode, _, stderr := runMain(t, "", append([]string{"compile"}, tt.args...))
   200  
   201  			require.Equal(t, 1, exitCode)
   202  			require.Contains(t, stderr, tt.message)
   203  		})
   204  	}
   205  }
   206  
   207  func TestRun(t *testing.T) {
   208  	tmpDir, oldwd := requireChdirToTemp(t)
   209  	defer os.Chdir(oldwd) //nolint
   210  
   211  	// Restore env logic borrowed from TestClearenv
   212  	defer func(origEnv []string) {
   213  		for _, pair := range origEnv {
   214  			// Environment variables on Windows can begin with =
   215  			// https://blogs.msdn.com/b/oldnewthing/archive/2010/05/06/10008132.aspx
   216  			i := strings.Index(pair[1:], "=") + 1
   217  			if err := os.Setenv(pair[:i], pair[i+1:]); err != nil {
   218  				t.Errorf("Setenv(%q, %q) failed during reset: %v", pair[:i], pair[i+1:], err)
   219  			}
   220  		}
   221  	}(os.Environ())
   222  
   223  	// Clear the environment first, so we can make strict assertions.
   224  	os.Clearenv()
   225  	os.Setenv("ANIMAL", "kitten")
   226  	os.Setenv("INHERITED", "wazero")
   227  
   228  	// We can't rely on the mtime from git because in CI, only the last commit
   229  	// is checked out. Instead, grab the effective mtime.
   230  	bearDir := filepath.Join(oldwd, "testdata", "fs")
   231  	bearPath := filepath.Join(bearDir, "bear.txt")
   232  	bearStat, err := os.Stat(bearPath)
   233  	require.NoError(t, err)
   234  	bearMtime := bearStat.ModTime().UnixMilli()
   235  	bearMtimeNano := bearStat.ModTime().UnixNano()
   236  	// The file is world read, but windows cannot see that and reports world
   237  	// write. Hence, we save off the current interpretation of mode for
   238  	// comparison.
   239  	bearMode := bearStat.Mode()
   240  
   241  	existingDir1 := filepath.Join(tmpDir, "existing1")
   242  	require.NoError(t, os.Mkdir(existingDir1, 0o700))
   243  	existingDir2 := filepath.Join(tmpDir, "existing2")
   244  	require.NoError(t, os.Mkdir(existingDir2, 0o700))
   245  
   246  	cpuProfile := filepath.Join(t.TempDir(), "cpu.out")
   247  	memProfile := filepath.Join(t.TempDir(), "mem.out")
   248  
   249  	type test struct {
   250  		name             string
   251  		wazeroOpts       []string
   252  		workdir          string
   253  		wasm             []byte
   254  		wasmArgs         []string
   255  		expectedStdout   string
   256  		expectedStderr   string
   257  		expectedExitCode int
   258  		test             func(t *testing.T)
   259  	}
   260  
   261  	tests := []test{
   262  		{
   263  			name:     "args",
   264  			wasm:     wasmWasiArg,
   265  			wasmArgs: []string{"hello world"},
   266  			// Executable name is first arg so is printed.
   267  			expectedStdout: "test.wasm\x00hello world\x00",
   268  		},
   269  		{
   270  			name:     "-- args",
   271  			wasm:     wasmWasiArg,
   272  			wasmArgs: []string{"--", "hello world"},
   273  			// Executable name is first arg so is printed.
   274  			expectedStdout: "test.wasm\x00hello world\x00",
   275  		},
   276  		{
   277  			name:           "env",
   278  			wasm:           wasmWasiEnv,
   279  			wazeroOpts:     []string{"--env=ANIMAL=bear", "--env=FOOD=sushi"},
   280  			expectedStdout: "ANIMAL=bear\x00FOOD=sushi\x00",
   281  		},
   282  		{
   283  			name:           "env-inherit",
   284  			wasm:           wasmWasiEnv,
   285  			wazeroOpts:     []string{"-env-inherit"},
   286  			expectedStdout: "ANIMAL=kitten\x00INHERITED=wazero\u0000",
   287  		},
   288  		{
   289  			name:           "env-inherit with env",
   290  			wasm:           wasmWasiEnv,
   291  			wazeroOpts:     []string{"-env-inherit", "--env=ANIMAL=bear"},
   292  			expectedStdout: "ANIMAL=bear\x00INHERITED=wazero\u0000", // not ANIMAL=kitten
   293  		},
   294  		{
   295  			name:           "interpreter",
   296  			wasm:           wasmWasiArg,
   297  			wazeroOpts:     []string{"--interpreter"}, // just test it works
   298  			expectedStdout: "test.wasm\x00",
   299  		},
   300  		{
   301  			name:           "wasi",
   302  			wasm:           wasmWasiFd,
   303  			wazeroOpts:     []string{fmt.Sprintf("--mount=%s:/", bearDir)},
   304  			expectedStdout: "pooh\n",
   305  		},
   306  		{
   307  			name:           "wasi readonly",
   308  			wasm:           wasmWasiFd,
   309  			wazeroOpts:     []string{fmt.Sprintf("--mount=%s:/:ro", bearDir)},
   310  			expectedStdout: "pooh\n",
   311  		},
   312  		{
   313  			name:           "wasi non root",
   314  			wasm:           wasmCatTinygo,
   315  			wazeroOpts:     []string{fmt.Sprintf("--mount=%s:/animals:ro", bearDir)},
   316  			wasmArgs:       []string{"/animals/bear.txt"},
   317  			expectedStdout: "pooh\n",
   318  		},
   319  		{
   320  			name:       "wasi hostlogging=all",
   321  			wasm:       wasmWasiRandomGet,
   322  			wazeroOpts: []string{"--hostlogging=all"},
   323  			expectedStderr: `--> .$1()
   324  	==> wasi_snapshot_preview1.random_get(buf=0,buf_len=1000)
   325  	<== errno=ESUCCESS
   326  <--
   327  `,
   328  		},
   329  		{
   330  			name:       "wasi hostlogging=proc",
   331  			wasm:       wasmCatTinygo,
   332  			wazeroOpts: []string{"--hostlogging=proc", fmt.Sprintf("--mount=%s:/animals:ro", bearDir)},
   333  			wasmArgs:   []string{"/animals/not-bear.txt"},
   334  			expectedStderr: `==> wasi_snapshot_preview1.proc_exit(rval=1)
   335  `, // ^^ proc_exit panics, which short-circuits the logger. Hence, no "<==".
   336  			expectedExitCode: 1,
   337  		},
   338  		{
   339  			name:       "wasi hostlogging=filesystem",
   340  			wasm:       wasmCatTinygo,
   341  			wazeroOpts: []string{"--hostlogging=filesystem", fmt.Sprintf("--mount=%s:/animals:ro", bearDir)},
   342  			wasmArgs:   []string{"/animals/bear.txt"},
   343  			expectedStderr: fmt.Sprintf(`==> wasi_snapshot_preview1.fd_prestat_get(fd=3)
   344  <== (prestat={pr_name_len=8},errno=ESUCCESS)
   345  ==> wasi_snapshot_preview1.fd_prestat_dir_name(fd=3)
   346  <== (path=/animals,errno=ESUCCESS)
   347  ==> wasi_snapshot_preview1.fd_prestat_get(fd=4)
   348  <== (prestat=,errno=EBADF)
   349  ==> wasi_snapshot_preview1.fd_fdstat_get(fd=3)
   350  <== (stat={filetype=DIRECTORY,fdflags=,fs_rights_base=FD_DATASYNC|FDSTAT_SET_FLAGS|FD_SYNC|PATH_CREATE_DIRECTORY|PATH_CREATE_FILE|PATH_LINK_SOURCE|PATH_LINK_TARGET|PATH_OPEN|FD_READDIR|PATH_READLINK,fs_rights_inheriting=FD_DATASYNC|FD_READ|FD_SEEK|FDSTAT_SET_FLAGS|FD_SYNC|FD_TELL|FD_WRITE|FD_ADVISE|FD_ALLOCATE|PATH_CREATE_DIRECTORY|PATH_CREATE_FILE|PATH_LINK_SOURCE|PATH_LINK_TARGET|PATH_OPEN|FD_READDIR|PATH_READLINK},errno=ESUCCESS)
   351  ==> wasi_snapshot_preview1.path_open(fd=3,dirflags=SYMLINK_FOLLOW,path=bear.txt,oflags=,fs_rights_base=FD_READ|FD_SEEK|FDSTAT_SET_FLAGS|FD_SYNC|FD_TELL|FD_ADVISE|PATH_CREATE_DIRECTORY|PATH_CREATE_FILE|PATH_LINK_SOURCE|PATH_LINK_TARGET|PATH_OPEN|FD_READDIR|PATH_READLINK|PATH_RENAME_SOURCE|PATH_RENAME_TARGET|PATH_FILESTAT_GET|PATH_FILESTAT_SET_SIZE|PATH_FILESTAT_SET_TIMES|FD_FILESTAT_GET|FD_FILESTAT_SET_TIMES|PATH_SYMLINK|PATH_REMOVE_DIRECTORY|PATH_UNLINK_FILE|POLL_FD_READWRITE,fs_rights_inheriting=FD_DATASYNC|FD_READ|FD_SEEK|FDSTAT_SET_FLAGS|FD_SYNC|FD_TELL|FD_WRITE|FD_ADVISE|FD_ALLOCATE|PATH_CREATE_DIRECTORY|PATH_CREATE_FILE|PATH_LINK_SOURCE|PATH_LINK_TARGET|PATH_OPEN|FD_READDIR|PATH_READLINK|PATH_RENAME_SOURCE|PATH_RENAME_TARGET|PATH_FILESTAT_GET|PATH_FILESTAT_SET_SIZE|PATH_FILESTAT_SET_TIMES|FD_FILESTAT_GET|FD_FILESTAT_SET_SIZE|FD_FILESTAT_SET_TIMES|PATH_SYMLINK|PATH_REMOVE_DIRECTORY|PATH_UNLINK_FILE|POLL_FD_READWRITE,fdflags=)
   352  <== (opened_fd=4,errno=ESUCCESS)
   353  ==> wasi_snapshot_preview1.fd_filestat_get(fd=4)
   354  <== (filestat={filetype=REGULAR_FILE,size=5,mtim=%d},errno=ESUCCESS)
   355  ==> wasi_snapshot_preview1.fd_read(fd=4,iovs=64776,iovs_len=1)
   356  <== (nread=5,errno=ESUCCESS)
   357  ==> wasi_snapshot_preview1.fd_read(fd=4,iovs=64776,iovs_len=1)
   358  <== (nread=0,errno=ESUCCESS)
   359  ==> wasi_snapshot_preview1.fd_close(fd=4)
   360  <== errno=ESUCCESS
   361  `, bearMtimeNano),
   362  			expectedStdout: "pooh\n",
   363  		},
   364  		{
   365  			name:       "wasi hostlogging=random",
   366  			wasm:       wasmWasiRandomGet,
   367  			wazeroOpts: []string{"--hostlogging=random"},
   368  			expectedStderr: `==> wasi_snapshot_preview1.random_get(buf=0,buf_len=1000)
   369  <== errno=ESUCCESS
   370  `,
   371  		},
   372  		{
   373  			name:           "GOARCH=wasm GOOS=js",
   374  			wasm:           wasmCatGo,
   375  			wazeroOpts:     []string{fmt.Sprintf("--mount=%s:/", bearDir)},
   376  			wasmArgs:       []string{"/bear.txt"},
   377  			expectedStdout: "pooh\n",
   378  		},
   379  		{
   380  			name: "GOARCH=wasm GOOS=js workdir",
   381  			wasm: wasmCatGo,
   382  			wazeroOpts: []string{
   383  				// --mount=X:\:/ on Windows, --mount=/:/ everywhere else
   384  				"--mount=" + filepath.VolumeName(bearDir) + string(os.PathSeparator) + ":/",
   385  			},
   386  			workdir:        bearDir,
   387  			wasmArgs:       []string{"bear.txt"},
   388  			expectedStdout: "pooh\n",
   389  		},
   390  		{
   391  			name:           "GOARCH=wasm GOOS=js readonly",
   392  			wasm:           wasmCatGo,
   393  			wazeroOpts:     []string{fmt.Sprintf("--mount=%s:/:ro", bearDir)},
   394  			wasmArgs:       []string{"/bear.txt"},
   395  			expectedStdout: "pooh\n",
   396  		},
   397  		{
   398  			name:       "GOARCH=wasm GOOS=js hostlogging=proc",
   399  			wasm:       wasmCatGo,
   400  			wazeroOpts: []string{"--hostlogging=proc", fmt.Sprintf("--mount=%s:/animals:ro", bearDir)},
   401  			wasmArgs:   []string{"/animals/not-bear.txt"},
   402  			expectedStderr: `==> go.runtime.wasmExit(code=1)
   403  <==
   404  `,
   405  			expectedExitCode: 1,
   406  		},
   407  		{
   408  			name:           "GOARCH=wasm GOOS=js hostlogging=filesystem",
   409  			wasm:           wasmCatGo,
   410  			wazeroOpts:     []string{"--hostlogging=filesystem", fmt.Sprintf("--mount=%s:/", bearDir)},
   411  			wasmArgs:       []string{"/bear.txt"},
   412  			expectedStdout: "pooh\n",
   413  			expectedStderr: fmt.Sprintf(`==> go.syscall/js.valueCall(fs.open(path=/bear.txt,flags=,perm=----------))
   414  <== (err=<nil>,fd=4)
   415  ==> go.syscall/js.valueCall(fs.fstat(fd=4))
   416  <== (err=<nil>,stat={isDir=false,mode=%[1]s,size=5,mtimeMs=%[2]d})
   417  ==> go.syscall/js.valueCall(fs.fstat(fd=4))
   418  <== (err=<nil>,stat={isDir=false,mode=%[1]s,size=5,mtimeMs=%[2]d})
   419  ==> go.syscall/js.valueCall(fs.read(fd=4,offset=0,byteCount=512,fOffset=<nil>))
   420  <== (err=<nil>,n=5)
   421  ==> go.syscall/js.valueCall(fs.read(fd=4,offset=0,byteCount=507,fOffset=<nil>))
   422  <== (err=<nil>,n=0)
   423  ==> go.syscall/js.valueCall(fs.close(fd=4))
   424  <== (err=<nil>,ok=true)
   425  `, bearMode, bearMtime),
   426  		},
   427  		{
   428  			name:       "cachedir existing absolute",
   429  			wazeroOpts: []string{"--cachedir=" + existingDir1},
   430  			wasm:       wasmWasiArg,
   431  			wasmArgs:   []string{"hello world"},
   432  			// Executable name is first arg so is printed.
   433  			expectedStdout: "test.wasm\x00hello world\x00",
   434  			test: func(t *testing.T) {
   435  				entries, err := os.ReadDir(existingDir1)
   436  				require.NoError(t, err)
   437  				require.True(t, len(entries) > 0)
   438  			},
   439  		},
   440  		{
   441  			name:       "cachedir existing relative",
   442  			wazeroOpts: []string{"--cachedir=existing2"},
   443  			wasm:       wasmWasiArg,
   444  			wasmArgs:   []string{"hello world"},
   445  			// Executable name is first arg so is printed.
   446  			expectedStdout: "test.wasm\x00hello world\x00",
   447  			test: func(t *testing.T) {
   448  				entries, err := os.ReadDir(existingDir2)
   449  				require.NoError(t, err)
   450  				require.True(t, len(entries) > 0)
   451  			},
   452  		},
   453  		{
   454  			name:       "cachedir new absolute",
   455  			wazeroOpts: []string{"--cachedir=" + path.Join(tmpDir, "new1")},
   456  			wasm:       wasmWasiArg,
   457  			wasmArgs:   []string{"hello world"},
   458  			// Executable name is first arg so is printed.
   459  			expectedStdout: "test.wasm\x00hello world\x00",
   460  			test: func(t *testing.T) {
   461  				entries, err := os.ReadDir("new1")
   462  				require.NoError(t, err)
   463  				require.True(t, len(entries) > 0)
   464  			},
   465  		},
   466  		{
   467  			name:       "cachedir new relative",
   468  			wazeroOpts: []string{"--cachedir=new2"},
   469  			wasm:       wasmWasiArg,
   470  			wasmArgs:   []string{"hello world"},
   471  			// Executable name is first arg so is printed.
   472  			expectedStdout: "test.wasm\x00hello world\x00",
   473  			test: func(t *testing.T) {
   474  				entries, err := os.ReadDir("new2")
   475  				require.NoError(t, err)
   476  				require.True(t, len(entries) > 0)
   477  			},
   478  		},
   479  		{
   480  			name:             "timeout: a binary that exceeds the deadline should print an error",
   481  			wazeroOpts:       []string{"-timeout=1ms"},
   482  			wasm:             wasmInfiniteLoop,
   483  			expectedStderr:   "error: module closed with context deadline exceeded (timeout 1ms)\n",
   484  			expectedExitCode: int(sys.ExitCodeDeadlineExceeded),
   485  			test: func(t *testing.T) {
   486  				require.NoError(t, err)
   487  			},
   488  		},
   489  		{
   490  			name:       "timeout: a binary that ends before the deadline should not print a timeout error",
   491  			wazeroOpts: []string{"-timeout=10s"},
   492  			wasm:       wasmWasiRandomGet,
   493  			test: func(t *testing.T) {
   494  				require.NoError(t, err)
   495  			},
   496  		},
   497  		{
   498  			name:             "should run wasi_unstable",
   499  			wasm:             wasmWasiUnstable,
   500  			expectedExitCode: 2,
   501  			test: func(t *testing.T) {
   502  				require.NoError(t, err)
   503  			},
   504  		},
   505  		{
   506  			name:       "enable cpu profiling",
   507  			wazeroOpts: []string{"-cpuprofile=" + cpuProfile},
   508  			wasm:       wasmWasiRandomGet,
   509  			test: func(t *testing.T) {
   510  				require.NoError(t, exist(cpuProfile))
   511  			},
   512  		},
   513  		{
   514  			name:       "enable memory profiling",
   515  			wazeroOpts: []string{"-memprofile=" + memProfile},
   516  			wasm:       wasmWasiRandomGet,
   517  			test: func(t *testing.T) {
   518  				require.NoError(t, exist(memProfile))
   519  			},
   520  		},
   521  	}
   522  
   523  	cryptoTest := test{
   524  		name:       "GOARCH=wasm GOOS=js hostlogging=filesystem,random",
   525  		wasm:       wasmCatGo,
   526  		wazeroOpts: []string{"--hostlogging=filesystem,random"},
   527  		wasmArgs:   []string{"/bear.txt"},
   528  		expectedStderr: `==> go.runtime.getRandomData(r_len=32)
   529  <==
   530  ==> go.runtime.getRandomData(r_len=8)
   531  <==
   532  ==> go.syscall/js.valueCall(fs.open(path=/bear.txt,flags=,perm=----------))
   533  <== (err=function not implemented,fd=0)
   534  `, // Test only shows logging happens in two scopes; it is ok to fail.
   535  		expectedExitCode: 1,
   536  	}
   537  
   538  	for _, tt := range append(tests, cryptoTest) {
   539  		tc := tt
   540  
   541  		if tc.wasm == nil {
   542  			// We should only skip when the runtime is a scratch image.
   543  			require.False(t, platform.CompilerSupported())
   544  			continue
   545  		}
   546  		t.Run(tc.name, func(t *testing.T) {
   547  			wasmPath := filepath.Join(tmpDir, "test.wasm")
   548  			require.NoError(t, os.WriteFile(wasmPath, tc.wasm, 0o700))
   549  
   550  			args := append([]string{"run"}, tc.wazeroOpts...)
   551  			args = append(args, wasmPath)
   552  			args = append(args, tc.wasmArgs...)
   553  			exitCode, stdout, stderr := runMain(t, tc.workdir, args)
   554  
   555  			require.Equal(t, tc.expectedStderr, stderr)
   556  			require.Equal(t, tc.expectedExitCode, exitCode, stderr)
   557  			require.Equal(t, tc.expectedStdout, stdout)
   558  			if test := tc.test; test != nil {
   559  				test(t)
   560  			}
   561  		})
   562  	}
   563  }
   564  
   565  func TestVersion(t *testing.T) {
   566  	exitCode, stdout, stderr := runMain(t, "", []string{"version"})
   567  	require.Equal(t, 0, exitCode)
   568  	require.Equal(t, version.GetWazeroVersion()+"\n", stdout)
   569  	require.Equal(t, "", stderr)
   570  }
   571  
   572  func TestRun_Errors(t *testing.T) {
   573  	wasmPath := filepath.Join(t.TempDir(), "test.wasm")
   574  	require.NoError(t, os.WriteFile(wasmPath, wasmWasiArg, 0o700))
   575  
   576  	notWasmPath := filepath.Join(t.TempDir(), "bears.wasm")
   577  	require.NoError(t, os.WriteFile(notWasmPath, []byte("pooh"), 0o700))
   578  
   579  	tests := []struct {
   580  		message string
   581  		args    []string
   582  	}{
   583  		{
   584  			message: "missing path to wasm file",
   585  			args:    []string{},
   586  		},
   587  		{
   588  			message: "error reading wasm binary",
   589  			args:    []string{"non-existent.wasm"},
   590  		},
   591  		{
   592  			message: "error compiling wasm binary",
   593  			args:    []string{notWasmPath},
   594  		},
   595  		{
   596  			message: "invalid environment variable",
   597  			args:    []string{"--env=ANIMAL", "testdata/wasi_env.wasm"},
   598  		},
   599  		{
   600  			message: "invalid mount", // not found
   601  			args:    []string{"--mount=te", "testdata/wasi_env.wasm"},
   602  		},
   603  		{
   604  			message: "invalid cachedir",
   605  			args:    []string{"--cachedir", notWasmPath, wasmPath},
   606  		},
   607  		{
   608  			message: "timeout duration may not be negative",
   609  			args:    []string{"-timeout=-10s", wasmPath},
   610  		},
   611  	}
   612  
   613  	for _, tc := range tests {
   614  		tt := tc
   615  		t.Run(tt.message, func(t *testing.T) {
   616  			exitCode, _, stderr := runMain(t, "", append([]string{"run"}, tt.args...))
   617  
   618  			require.Equal(t, 1, exitCode)
   619  			require.Contains(t, stderr, tt.message)
   620  		})
   621  	}
   622  }
   623  
   624  var _ api.FunctionDefinition = importer{}
   625  
   626  type importer struct {
   627  	internalapi.WazeroOnlyType
   628  	moduleName, name string
   629  }
   630  
   631  func (i importer) ModuleName() string { return "" }
   632  func (i importer) Index() uint32      { return 0 }
   633  func (i importer) Import() (moduleName, name string, isImport bool) {
   634  	return i.moduleName, i.name, true
   635  }
   636  func (i importer) ExportNames() []string        { return nil }
   637  func (i importer) Name() string                 { return "" }
   638  func (i importer) DebugName() string            { return "" }
   639  func (i importer) GoFunction() interface{}      { return nil }
   640  func (i importer) ParamTypes() []api.ValueType  { return nil }
   641  func (i importer) ParamNames() []string         { return nil }
   642  func (i importer) ResultTypes() []api.ValueType { return nil }
   643  func (i importer) ResultNames() []string        { return nil }
   644  
   645  func Test_detectImports(t *testing.T) {
   646  	tests := []struct {
   647  		message string
   648  		imports []api.FunctionDefinition
   649  		mode    importMode
   650  	}{
   651  		{
   652  			message: "no imports",
   653  		},
   654  		{
   655  			message: "other imports",
   656  			imports: []api.FunctionDefinition{
   657  				importer{internalapi.WazeroOnlyType{}, "env", "emscripten_notify_memory_growth"},
   658  			},
   659  		},
   660  		{
   661  			message: "wasi",
   662  			imports: []api.FunctionDefinition{
   663  				importer{internalapi.WazeroOnlyType{}, wasi_snapshot_preview1.ModuleName, "fd_read"},
   664  			},
   665  			mode: modeWasi,
   666  		},
   667  		{
   668  			message: "unstable_wasi",
   669  			imports: []api.FunctionDefinition{
   670  				importer{internalapi.WazeroOnlyType{}, "wasi_unstable", "fd_read"},
   671  			},
   672  			mode: modeWasiUnstable,
   673  		},
   674  		{
   675  			message: "GOARCH=wasm GOOS=js",
   676  			imports: []api.FunctionDefinition{
   677  				importer{internalapi.WazeroOnlyType{}, "go", "syscall/js.valueCall"},
   678  			},
   679  			mode: modeGo,
   680  		},
   681  	}
   682  
   683  	for _, tc := range tests {
   684  		tt := tc
   685  		t.Run(tt.message, func(t *testing.T) {
   686  			mode := detectImports(tc.imports)
   687  			require.Equal(t, tc.mode, mode)
   688  		})
   689  	}
   690  }
   691  
   692  func Test_logScopesFlag(t *testing.T) {
   693  	tests := []struct {
   694  		name     string
   695  		values   []string
   696  		expected logging.LogScopes
   697  	}{
   698  		{
   699  			name:     "defaults to none",
   700  			expected: logging.LogScopeNone,
   701  		},
   702  		{
   703  			name:     "ignores empty",
   704  			values:   []string{""},
   705  			expected: logging.LogScopeNone,
   706  		},
   707  		{
   708  			name:     "all",
   709  			values:   []string{"all"},
   710  			expected: logging.LogScopeAll,
   711  		},
   712  		{
   713  			name:     "clock",
   714  			values:   []string{"clock"},
   715  			expected: logging.LogScopeClock,
   716  		},
   717  		{
   718  			name:     "filesystem",
   719  			values:   []string{"filesystem"},
   720  			expected: logging.LogScopeFilesystem,
   721  		},
   722  		{
   723  			name:     "memory",
   724  			values:   []string{"memory"},
   725  			expected: logging.LogScopeMemory,
   726  		},
   727  		{
   728  			name:     "poll",
   729  			values:   []string{"poll"},
   730  			expected: logging.LogScopePoll,
   731  		},
   732  		{
   733  			name:     "random",
   734  			values:   []string{"random"},
   735  			expected: logging.LogScopeRandom,
   736  		},
   737  		{
   738  			name:     "clock filesystem poll random",
   739  			values:   []string{"clock", "filesystem", "poll", "random"},
   740  			expected: logging.LogScopeClock | logging.LogScopeFilesystem | logging.LogScopePoll | logging.LogScopeRandom,
   741  		},
   742  		{
   743  			name:     "clock,filesystem poll,random",
   744  			values:   []string{"clock,filesystem", "poll,random"},
   745  			expected: logging.LogScopeClock | logging.LogScopeFilesystem | logging.LogScopePoll | logging.LogScopeRandom,
   746  		},
   747  		{
   748  			name:     "all random",
   749  			values:   []string{"all", "random"},
   750  			expected: logging.LogScopeAll,
   751  		},
   752  	}
   753  
   754  	for _, tt := range tests {
   755  		tc := tt
   756  		t.Run(tc.name, func(t *testing.T) {
   757  			f := logScopesFlag(0)
   758  			for _, v := range tc.values {
   759  				require.NoError(t, f.Set(v))
   760  			}
   761  			require.Equal(t, tc.expected, logging.LogScopes(f))
   762  		})
   763  	}
   764  }
   765  
   766  func TestHelp(t *testing.T) {
   767  	exitCode, _, stderr := runMain(t, "", []string{"-h"})
   768  	require.Equal(t, 0, exitCode)
   769  	fmt.Println(stderr)
   770  	require.Equal(t, `wazero CLI
   771  
   772  Usage:
   773    wazero <command>
   774  
   775  Commands:
   776    compile	Pre-compiles a WebAssembly binary
   777    run		Runs a WebAssembly binary
   778    version	Displays the version of wazero CLI
   779  `, stderr)
   780  }
   781  
   782  func runMain(t *testing.T, workdir string, args []string) (int, string, string) {
   783  	t.Helper()
   784  
   785  	// Use a workdir override if supplied.
   786  	if workdir != "" {
   787  		oldcwd, err := os.Getwd()
   788  		require.NoError(t, err)
   789  
   790  		require.NoError(t, os.Chdir(workdir))
   791  		defer func() {
   792  			require.NoError(t, os.Chdir(oldcwd))
   793  		}()
   794  	}
   795  
   796  	oldArgs := os.Args
   797  	t.Cleanup(func() {
   798  		os.Args = oldArgs
   799  	})
   800  	os.Args = append([]string{"wazero"}, args...)
   801  	flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
   802  
   803  	stdout := new(bytes.Buffer)
   804  	stderr := new(bytes.Buffer)
   805  	exitCode := doMain(stdout, stderr)
   806  	return exitCode, stdout.String(), stderr.String()
   807  }
   808  
   809  // compileGoJS compiles "testdata/cat/cat.go" on demand as the binary generated
   810  // is too big (1.6MB) to check into the source tree.
   811  func compileGoJS() (err error) {
   812  	dir, err := os.Getwd()
   813  	if err != nil {
   814  		return err
   815  	}
   816  
   817  	srcDir := path.Join(dir, "testdata", "cat")
   818  	outPath := path.Join(srcDir, "cat-go.wasm")
   819  
   820  	// This doesn't add "-ldflags=-s -w", as the binary size only changes 28KB.
   821  	cmd := exec.Command("go", "build", "-o", outPath, ".")
   822  	cmd.Dir = srcDir
   823  	cmd.Env = append(os.Environ(), "GOARCH=wasm", "GOOS=js", "GOWASM=satconv,signext")
   824  	if out, err := cmd.CombinedOutput(); err != nil {
   825  		return fmt.Errorf("go build: %v\n%s", err, out)
   826  	}
   827  
   828  	wasmCatGo, err = os.ReadFile(outPath)
   829  	return
   830  }
   831  
   832  func exist(path string) error {
   833  	f, err := os.Open(path)
   834  	if err != nil {
   835  		return err
   836  	}
   837  	return f.Close()
   838  }