Repository: tetratelabs/wazero
Pull request #1235 (2023-03-14 01:08:07)

cli: changes experimental workdir flag to inherit
This changes the experimental flag introduced yesterday to `-experimental-workdir-inherit=true`, to reduce edge cases needed to produce the same behavior as go's wasm test runner.

After this, we have the same failure count, though the latter is likely related to my machine:

```bash
$ wasm=$PWD/os.wasm; (cd $(go env GOROOT)/src/os; wazero run -mount=/:/ --experimental-workdir-inherit=true $wasm)
--- FAIL: TestMkdirAllAtSlash (0.00s)
    path_test.go:111: MkdirAll "/_go_os_test/dir": mkdir /_go_os_test: I/O error, I/O error
--- FAIL: TestOpenFileLimit (0.01s)
    rlimit_test.go:31: open rlimit.go: I/O error
FAIL
```

See https://github.com/golang/go/blob/master/misc/wasm/wasm_exec_node.js#L24

codefromthecrypt: my workstation is working so I'll look into this fail

Patch:
diff --git a/cmd/wazero/wazero.go b/cmd/wazero/wazero.go
index c0be1f9b..ac2a8d4a 100644
--- a/cmd/wazero/wazero.go
+++ b/cmd/wazero/wazero.go
@@ -125,18 +125,20 @@ func doRun(args []string, stdOut io.Writer, stdErr logging.Writer, exit func(cod
 
 	var envInherit bool
 	flags.BoolVar(&envInherit, "env-inherit", false,
-		"inherits any environment variables from the calling process."+
+		"inherits any environment variables from the calling process. "+
 			"Variables specified with the <env> flag are appended to the inherited list.")
 
-	var workdir string
-	flags.StringVar(&workdir, "experimental-workdir", "",
-		"inherits the working directory from the calling process."+
-			"Note: This only applies to wasm compiled with `GOARCH=wasm GOOS=js` a.k.a. gojs.")
+	var workdirInherit bool
+	flags.BoolVar(&workdirInherit, "experimental-workdir-inherit", false,
+		"inherits the working directory from the calling process. "+
+			"Note: This only applies to wasm compiled with `GOARCH=wasm GOOS=js` a.k.a. gojs. "+
+			"In windows, the working directory must be on the same volume as the root mount.")
 
 	var mounts sliceFlag
 	flags.Var(&mounts, "mount",
 		"filesystem path to expose to the binary in the form of <path>[:<wasm path>][:ro]. "+
 			"This may be specified multiple times. When <wasm path> is unset, <path> is used. "+
+			"For example, -mount=/:/ or c:\\:/ makes the entire host volume writeable by wasm. "+
 			"For read-only mounts, append the suffix ':ro'.")
 
 	var timeout time.Duration
@@ -224,8 +226,11 @@ func doRun(args []string, stdOut io.Writer, stdErr logging.Writer, exit func(cod
 		exit(1)
 	}
 
-	if workdir != "" {
-		ctx = gojs.WithWorkdir(ctx, workdir)
+	if workdirInherit {
+		if ctx, err = gojs.WithOSWorkDir(ctx); err != nil {
+			fmt.Fprintf(stdErr, "cannot read current working directory: %v\n", err)
+			exit(1)
+		}
 	}
 
 	rt := wazero.NewRuntimeWithConfig(ctx, rtc)
diff --git a/experimental/gojs/gojs.go b/experimental/gojs/gojs.go
index 671ae510..358a78a5 100644
--- a/experimental/gojs/gojs.go
+++ b/experimental/gojs/gojs.go
@@ -16,12 +16,14 @@ package gojs
 import (
 	"context"
 	"net/http"
+	"os"
 	"path/filepath"
 
 	"github.com/tetratelabs/wazero"
 	"github.com/tetratelabs/wazero/api"
 	. "github.com/tetratelabs/wazero/internal/gojs"
 	. "github.com/tetratelabs/wazero/internal/gojs/run"
+	"github.com/tetratelabs/wazero/internal/platform"
 	"github.com/tetratelabs/wazero/internal/wasm"
 )
 
@@ -107,18 +109,30 @@ func WithRoundTripper(ctx context.Context, rt http.RoundTripper) context.Context
 	return context.WithValue(ctx, RoundTripperKey{}, rt)
 }
 
-// WithWorkdir sets the initial working directory used to Run Wasm. This
-// defaults to root "/".
+// WithOSWorkDir sets the initial working directory used to Run Wasm to
+// the value of os.Getwd instead of the default of root "/".
 //
 // Here's an example that overrides this to "/usr/local/go/src/os".
 //
-//	ctx = gojs.WithWorkdir(ctx, "/usr/local/go/src/os")
+//	ctx = gojs.WithOSWorkDir(ctx)
 //	err = gojs.Run(ctx, r, compiled, config)
-func WithWorkdir(ctx context.Context, workdir string) context.Context {
+//
+// Note: To use this feature requires mounting the real root directory via
+// wazero.FSConfig `WithDirMount`. On windows, this root must be the same drive
+// as the value of os.Getwd. For example, it would be an error to mount `C:\`
+// as the guest path "", while the current directory is inside `D:\`.
+func WithOSWorkDir(ctx context.Context) (context.Context, error) {
+	workdir, err := os.Getwd()
+	if err != nil {
+		return nil, err
+	}
+
 	// Ensure if used on windows, the input path is translated to a POSIX one.
-	workdir = workdir[len(filepath.VolumeName(workdir)):] // trim volume prefix (C:) on Windows
-	workdir = filepath.ToSlash(workdir)                   // convert \ to /
-	return context.WithValue(ctx, WorkdirKey{}, workdir)
+	workdir = platform.ToPosixPath(workdir)
+	// Strip the volume of the path, for example C:\
+	workdir = workdir[len(filepath.VolumeName(workdir)):]
+
+	return context.WithValue(ctx, WorkdirKey{}, workdir), nil
 }
 
 // Run instantiates a new module and calls "run" with the given config.


Test patch:
diff --git a/cmd/wazero/wazero_test.go b/cmd/wazero/wazero_test.go
index a55ba5d2..89a89901 100644
--- a/cmd/wazero/wazero_test.go
+++ b/cmd/wazero/wazero_test.go
@@ -125,7 +125,7 @@ func TestCompile(t *testing.T) {
 		t.Run(tt.name, func(t *testing.T) {
 			args := append([]string{"compile"}, tt.wazeroOpts...)
 			args = append(args, wasmPath)
-			exitCode, stdout, stderr := runMain(t, args)
+			exitCode, stdout, stderr := runMain(t, "", args)
 			require.Zero(t, stderr)
 			require.Equal(t, 0, exitCode, stderr)
 			require.Zero(t, stdout)
@@ -178,7 +178,7 @@ func TestCompile_Errors(t *testing.T) {
 	for _, tc := range tests {
 		tt := tc
 		t.Run(tt.message, func(t *testing.T) {
-			exitCode, _, stderr := runMain(t, append([]string{"compile"}, tt.args...))
+			exitCode, _, stderr := runMain(t, "", append([]string{"compile"}, tt.args...))
 
 			require.Equal(t, 1, exitCode)
 			require.Contains(t, stderr, tt.message)
@@ -228,6 +228,7 @@ func TestRun(t *testing.T) {
 	type test struct {
 		name             string
 		wazeroOpts       []string
+		workdir          string
 		wasm             []byte
 		wasmArgs         []string
 		expectedStdout   string
@@ -350,8 +351,9 @@ func TestRun(t *testing.T) {
 			wazeroOpts: []string{
 				// --mount=X:\:/ on Windows, --mount=/:/ everywhere else
 				"--mount=" + filepath.VolumeName(bearDir) + string(os.PathSeparator) + ":/",
-				fmt.Sprintf("--experimental-workdir=%s", bearDir[len(filepath.VolumeName(bearDir)):]),
+				"--experimental-workdir-inherit=true",
 			},
+			workdir:        bearDir,
 			wasmArgs:       []string{"bear.txt"},
 			expectedStdout: "pooh\n",
 		},
@@ -502,7 +504,7 @@ func TestRun(t *testing.T) {
 			args := append([]string{"run"}, tc.wazeroOpts...)
 			args = append(args, wasmPath)
 			args = append(args, tc.wasmArgs...)
-			exitCode, stdout, stderr := runMain(t, args)
+			exitCode, stdout, stderr := runMain(t, tc.workdir, args)
 
 			require.Equal(t, tc.expectedStderr, stderr)
 			require.Equal(t, tc.expectedExitCode, exitCode, stderr)
@@ -515,7 +517,7 @@ func TestRun(t *testing.T) {
 }
 
 func TestVersion(t *testing.T) {
-	exitCode, stdout, stderr := runMain(t, []string{"version"})
+	exitCode, stdout, stderr := runMain(t, "", []string{"version"})
 	require.Equal(t, 0, exitCode)
 	require.Equal(t, version.GetWazeroVersion()+"\n", stdout)
 	require.Equal(t, "", stderr)
@@ -565,7 +567,7 @@ func TestRun_Errors(t *testing.T) {
 	for _, tc := range tests {
 		tt := tc
 		t.Run(tt.message, func(t *testing.T) {
-			exitCode, _, stderr := runMain(t, append([]string{"run"}, tt.args...))
+			exitCode, _, stderr := runMain(t, "", append([]string{"run"}, tt.args...))
 
 			require.Equal(t, 1, exitCode)
 			require.Contains(t, stderr, tt.message)
@@ -705,7 +707,7 @@ func Test_logScopesFlag(t *testing.T) {
 }
 
 func TestHelp(t *testing.T) {
-	exitCode, _, stderr := runMain(t, []string{"-h"})
+	exitCode, _, stderr := runMain(t, "", []string{"-h"})
 	require.Equal(t, 0, exitCode)
 	fmt.Println(stderr)
 	require.Equal(t, `wazero CLI
@@ -720,8 +722,20 @@ Commands:
 `, stderr)
 }
 
-func runMain(t *testing.T, args []string) (int, string, string) {
+func runMain(t *testing.T, workdir string, args []string) (int, string, string) {
 	t.Helper()
+
+	// Use a workdir override if supplied.
+	if workdir != "" {
+		oldcwd, err := os.Getwd()
+		require.NoError(t, err)
+
+		require.NoError(t, os.Chdir(workdir))
+		defer func() {
+			require.NoError(t, os.Chdir(oldcwd))
+		}()
+	}
+
 	oldArgs := os.Args
 	t.Cleanup(func() {
 		os.Args = oldArgs
@@ -729,8 +743,7 @@ func runMain(t *testing.T, args []string) (int, string, string) {
 	os.Args = append([]string{"wazero"}, args...)
 
 	var exitCode int
-	stdout := &bytes.Buffer{}
-	stderr := &bytes.Buffer{}
+	var stdout, stderr bytes.Buffer
 	var exited bool
 	func() {
 		defer func() {
@@ -739,9 +752,9 @@ func runMain(t *testing.T, args []string) (int, string, string) {
 			}
 		}()
 		flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
-		doMain(stdout, stderr, func(code int) {
+		doMain(&stdout, &stderr, func(code int) {
 			exitCode = code
-			panic(code)
+			panic(code) // to exit the func and set the exit status.
 		})
 	}()
 
diff --git a/experimental/gojs/gojs_test.go b/experimental/gojs/gojs_test.go
new file mode 100644
index 00000000..bb1ef995
--- /dev/null
+++ b/experimental/gojs/gojs_test.go
@@ -0,0 +1,22 @@
+package gojs
+
+import (
+	"context"
+	"strings"
+	"testing"
+
+	"github.com/tetratelabs/wazero/internal/gojs"
+	"github.com/tetratelabs/wazero/internal/testing/require"
+)
+
+func TestWithOSWorkdir(t *testing.T) {
+	t.Parallel()
+
+	ctx, err := WithOSWorkDir(context.Background())
+	require.NoError(t, err)
+	actual := ctx.Value(gojs.WorkdirKey{}).(string)
+
+	// Check c:\ or d:\ aren't retained.
+	require.Equal(t, -1, strings.IndexByte(actual, '\\'))
+	require.Equal(t, -1, strings.IndexByte(actual, ':'))
+}
diff --git a/internal/platform/path_test.go b/internal/platform/path_test.go
index 73456a68..e306c4c1 100644
--- a/internal/platform/path_test.go
+++ b/internal/platform/path_test.go
@@ -1,14 +1,42 @@
 package platform
 
 import (
-	"path/filepath"
+	"runtime"
+	"strings"
 	"testing"
 
 	"github.com/tetratelabs/wazero/internal/testing/require"
 )
 
 func TestToPosixPath(t *testing.T) {
-	orig := filepath.Join("a", "b", "c")
-	fixed := ToPosixPath(orig)
-	require.Equal(t, "a/b/c", fixed)
+	t.Parallel()
+
+	tests := []struct {
+		input    string
+		expected string
+	}{
+		{},
+		{input: ".", expected: "."},
+		{input: "/", expected: `/`},
+		{input: `/`, expected: `/`},
+		{input: "/foo/bar", expected: `/foo/bar`},
+		{input: `\foo\bar`, expected: `/foo/bar`},
+		{input: "foo/bar", expected: `foo/bar`},
+		{input: `foo\bar`, expected: `foo/bar`},
+		{input: "c:/foo/bar", expected: `c:/foo/bar`},
+		{input: `c:\foo\bar`, expected: `c:/foo/bar`},
+	}
+
+	for _, tt := range tests {
+		tc := tt
+
+		// We don't expect to translate backslashes unless we are on windows.
+		if strings.IndexByte(tc.input, '\\') != -1 && runtime.GOOS != "windows" {
+			continue
+		}
+
+		t.Run(tc.input, func(t *testing.T) {
+			require.Equal(t, tc.expected, ToPosixPath(tc.input))
+		})
+	}
 }