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