Repository: tetratelabs/wazero
Pull request #468 (2022-04-15 06:48:43)

Changes how examples are tested, and fixes ExitError bug
Before, we tested the examples/ directory using "ExampleXX", but this is
not ideal because it literally embeds the call to `main` into example
godoc output. This stops doing that for a different infrastructure.

This also makes sure there's a godoc example for both the main package
and wasi, so that people looking at https://pkg.go.dev see something and
also a link to our real examples directory.



Patch:
diff --git a/sys/sys.go b/sys/sys.go
index f0fd231b..28539889 100644
--- a/sys/sys.go
+++ b/sys/sys.go
@@ -11,7 +11,7 @@ import (
 // Here's an example of how to get the exit code:
 //	main := module.ExportedFunction("main")
 //	if err := main(nil); err != nil {
-//		if exitErr, ok := err.(*wazero.ExitError); ok {
+//		if exitErr, ok := err.(*sys.ExitError); ok {
 //			// If your main function expects to exit, this could be ok if Code == 0
 //		}
 //	--snip--
diff --git a/wasm.go b/wasm.go
index c348189d..758c2d24 100644
--- a/wasm.go
+++ b/wasm.go
@@ -10,6 +10,7 @@ import (
 	"github.com/tetratelabs/wazero/internal/wasm"
 	"github.com/tetratelabs/wazero/internal/wasm/binary"
 	"github.com/tetratelabs/wazero/internal/wasm/text"
+	"github.com/tetratelabs/wazero/sys"
 )
 
 // Runtime allows embedding of WebAssembly 1.0 (20191205) modules.
@@ -195,8 +196,8 @@ func (r *runtime) InstantiateModule(code *CompiledCode) (mod api.Module, err err
 
 // InstantiateModuleWithConfig implements Runtime.InstantiateModuleWithConfig
 func (r *runtime) InstantiateModuleWithConfig(code *CompiledCode, config *ModuleConfig) (mod api.Module, err error) {
-	var sys *wasm.SysContext
-	if sys, err = config.toSysContext(); err != nil {
+	var sysCtx *wasm.SysContext
+	if sysCtx, err = config.toSysContext(); err != nil {
 		return
 	}
 
@@ -207,7 +208,7 @@ func (r *runtime) InstantiateModuleWithConfig(code *CompiledCode, config *Module
 
 	module := config.replaceImports(code.module)
 
-	mod, err = r.store.Instantiate(r.ctx, module, name, sys)
+	mod, err = r.store.Instantiate(r.ctx, module, name, sysCtx)
 	if err != nil {
 		return
 	}
@@ -218,6 +219,9 @@ func (r *runtime) InstantiateModuleWithConfig(code *CompiledCode, config *Module
 			continue
 		}
 		if _, err = start.Call(mod.WithContext(r.ctx)); err != nil {
+			if _, ok := err.(*sys.ExitError); ok {
+				return
+			}
 			err = fmt.Errorf("module[%s] function[%s] failed: %w", name, fn, err)
 			return
 		}


Test patch:
diff --git a/example_test.go b/example_test.go
new file mode 100644
index 00000000..730ced0a
--- /dev/null
+++ b/example_test.go
@@ -0,0 +1,43 @@
+package wazero
+
+import (
+	_ "embed"
+	"fmt"
+	"log"
+)
+
+// This is an example of how to use WebAssembly via adding two numbers.
+//
+// See https://github.com/tetratelabs/wazero/tree/main/examples for more examples.
+func Example() {
+	// Create a new WebAssembly Runtime.
+	r := NewRuntime()
+
+	// Add a module to the runtime named "wasm/math" which exports one function "add", implemented in WebAssembly.
+	mod, err := r.InstantiateModuleFromCode([]byte(`(module $wasm/math
+    (func $add (param i32 i32) (result i32)
+        local.get 0
+        local.get 1
+        i32.add
+    )
+    (export "add" (func $add))
+)`))
+	if err != nil {
+		log.Fatal(err)
+	}
+	defer mod.Close()
+
+	// Get a function that can be reused until its module is closed:
+	add := mod.ExportedFunction("add")
+
+	x, y := uint64(1), uint64(2)
+	results, err := add.Call(nil, x, y)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	fmt.Printf("%s: %d + %d = %d\n", mod.Name(), x, y, results[0])
+
+	// Output:
+	// wasm/math: 1 + 2 = 3
+}
diff --git a/examples/basic/add_test.go b/examples/basic/add_test.go
index 00164866..2a118b9f 100644
--- a/examples/basic/add_test.go
+++ b/examples/basic/add_test.go
@@ -1,22 +1,18 @@
 package add
 
 import (
-	"os"
+	"testing"
+
+	"github.com/tetratelabs/wazero/internal/testing/maintester"
+	"github.com/tetratelabs/wazero/internal/testing/require"
 )
 
-// Example_main ensures the following will work:
+// Test_main ensures the following will work:
 //
 //	go run add.go 7 9
-func Example_main() {
-
-	// Save the old os.Args and replace with our example input.
-	oldArgs := os.Args
-	os.Args = []string{"add", "7", "9"}
-	defer func() { os.Args = oldArgs }()
-
-	main()
-
-	// Output:
-	// wasm/math: 7 + 9 = 16
-	// host/math: 7 + 9 = 16
+func Test_main(t *testing.T) {
+	stdout, _ := maintester.TestMain(t, main, "add", "7", "9")
+	require.Equal(t, `wasm/math: 7 + 9 = 16
+host/math: 7 + 9 = 16
+`, stdout)
 }
diff --git a/examples/import-go/age-calculator_test.go b/examples/import-go/age-calculator_test.go
index a7c04d3c..4fab3932 100644
--- a/examples/import-go/age-calculator_test.go
+++ b/examples/import-go/age-calculator_test.go
@@ -1,24 +1,21 @@
 package age_calculator
 
-import "os"
+import (
+	"testing"
 
-// Example_main ensures the following will work:
+	"github.com/tetratelabs/wazero/internal/testing/maintester"
+	"github.com/tetratelabs/wazero/internal/testing/require"
+)
+
+// Test_main ensures the following will work:
 //
 //	go run age-calculator.go 2000
-func Example_main() {
-
-	// Save the old os.Args and replace with our example input.
-	oldArgs := os.Args
-	_ = os.Setenv("CURRENT_YEAR", "2021")
-	os.Args = []string{"age-calculator", "2000"}
-	defer func() {
-		os.Args = oldArgs
-		_ = os.Unsetenv("CURRENT_YEAR")
-	}()
-
-	main()
+func Test_main(t *testing.T) {
+	// Set ENV to ensure this test doesn't need maintenance every year.
+	t.Setenv("CURRENT_YEAR", "2021")
 
-	// Output:
-	// println >> 21
-	// log_i32 >> 21
+	stdout, _ := maintester.TestMain(t, main, "age-calculator", "2000")
+	require.Equal(t, `println >> 21
+log_i32 >> 21
+`, stdout)
 }
diff --git a/examples/multiple-results/multiple-results_test.go b/examples/multiple-results/multiple-results_test.go
index 36fd2952..409db0be 100644
--- a/examples/multiple-results/multiple-results_test.go
+++ b/examples/multiple-results/multiple-results_test.go
@@ -1,15 +1,20 @@
 package multiple_results
 
-// Example_main ensures the following will work:
-//
-//	go run multiple-results.go
-func Example_main() {
+import (
+	"testing"
 
-	main()
+	"github.com/tetratelabs/wazero/internal/testing/maintester"
+	"github.com/tetratelabs/wazero/internal/testing/require"
+)
 
-	// Output:
-	// result-offset/wasm: age=37
-	// result-offset/host: age=37
-	// multi-value/wasm: age=37
-	// multi-value/host: age=37
+// Test_main ensures the following will work:
+//
+//	go run multiple-results.go
+func Test_main(t *testing.T) {
+	stdout, _ := maintester.TestMain(t, main, "multiple-results")
+	require.Equal(t, `result-offset/wasm: age=37
+result-offset/host: age=37
+multi-value/wasm: age=37
+multi-value/host: age=37
+`, stdout)
 }
diff --git a/examples/replace-import/replace-import_test.go b/examples/replace-import/replace-import_test.go
index d365bc7a..49834449 100644
--- a/examples/replace-import/replace-import_test.go
+++ b/examples/replace-import/replace-import_test.go
@@ -1,12 +1,16 @@
 package replace_import
 
-// Example_main ensures the following will work:
-//
-//	go run replace-import.go
-func Example_main() {
+import (
+	"testing"
 
-	main()
+	"github.com/tetratelabs/wazero/internal/testing/maintester"
+	"github.com/tetratelabs/wazero/internal/testing/require"
+)
 
-	// Output:
-	// module "needs-import" closed with exit_code(255)
+// Test_main ensures the following will work:
+//
+//	go run replace-import.go
+func Test_main(t *testing.T) {
+	stdout, _ := maintester.TestMain(t, main, "replace-import")
+	require.Equal(t, "module \"needs-import\" closed with exit_code(255)\n", stdout)
 }
diff --git a/examples/wasi/cat_test.go b/examples/wasi/cat_test.go
index d57a2dbe..d4dc035a 100644
--- a/examples/wasi/cat_test.go
+++ b/examples/wasi/cat_test.go
@@ -1,19 +1,16 @@
 package wasi_example
 
-import "os"
+import (
+	"testing"
 
-// Example_main ensures the following will work:
+	"github.com/tetratelabs/wazero/internal/testing/maintester"
+	"github.com/tetratelabs/wazero/internal/testing/require"
+)
+
+// Test_main ensures the following will work:
 //
 //	go run cat.go ./test.txt
-func Example_main() {
-
-	// Save the old os.Args and replace with our example input.
-	oldArgs := os.Args
-	os.Args = []string{"cat", "./test.txt"}
-	defer func() { os.Args = oldArgs }()
-
-	main()
-
-	// Output:
-	// hello filesystem
+func Test_main(t *testing.T) {
+	stdout, _ := maintester.TestMain(t, main, "cat", "./test.txt")
+	require.Equal(t, "hello filesystem\n", stdout)
 }
diff --git a/internal/testing/maintester/maintester.go b/internal/testing/maintester/maintester.go
new file mode 100644
index 00000000..5ed844f2
--- /dev/null
+++ b/internal/testing/maintester/maintester.go
@@ -0,0 +1,56 @@
+package maintester
+
+import (
+	"os"
+	"path"
+	"strings"
+	"testing"
+
+	"github.com/tetratelabs/wazero/internal/testing/require"
+)
+
+func TestMain(t *testing.T, main func(), args ...string) (stdout, stderr string) {
+	// Setup files to capture stdout and stderr
+	tmp := t.TempDir()
+
+	stdoutPath := path.Join(tmp, "stdout.txt")
+	stdoutF, err := os.Create(stdoutPath)
+	require.NoError(t, err)
+
+	stderrPath := path.Join(tmp, "stderr.txt")
+	stderrF, err := os.Create(stderrPath)
+	require.NoError(t, err)
+
+	// Save the old os.XXX and revert regardless of the outcome.
+	oldArgs := os.Args
+	os.Args = args
+	oldStdout := os.Stdout
+	os.Stdout = stdoutF
+	oldStderr := os.Stderr
+	os.Stderr = stderrF
+	revertOS := func() {
+		os.Args = oldArgs
+		_ = stdoutF.Close()
+		os.Stdout = oldStdout
+		_ = stderrF.Close()
+		os.Stderr = oldStderr
+	}
+	defer revertOS()
+
+	// Run the main command.
+	main()
+
+	// Revert os.XXX so that test output is visible on failure.
+	revertOS()
+
+	// Capture any output and return it in a portable way (ex without windows newlines)
+	stdoutB, err := os.ReadFile(stdoutPath)
+	require.NoError(t, err)
+	stdout = strings.ReplaceAll(string(stdoutB), "\r\n", "\n")
+
+	stderrB, err := os.ReadFile(stderrPath)
+	require.NoError(t, err)
+	stderr = strings.ReplaceAll(string(stderrB), "\r\n", "\n")
+
+	return
+}
diff --git a/wasi/example_test.go b/wasi/example_test.go
index 43196bf4..93f5b15a 100644
--- a/wasi/example_test.go
+++ b/wasi/example_test.go
@@ -1,14 +1,48 @@
 package wasi
 
-import "github.com/tetratelabs/wazero"
+import (
+	"fmt"
+	"log"
+	"os"
 
-var r = wazero.NewRuntime()
+	"github.com/tetratelabs/wazero"
+	"github.com/tetratelabs/wazero/sys"
+)
 
-// Example_InstantiateSnapshotPreview1 shows how to instantiate ModuleSnapshotPreview1, which allows other modules to
-// import functions such as "wasi_snapshot_preview1" "fd_write".
-func Example_instantiateSnapshotPreview1() {
-	wm, _ := InstantiateSnapshotPreview1(r)
+// This is an example of how to use WebAssembly System Interface (WASI) with its simplest function: "proc_exit".
+//
+// See https://github.com/tetratelabs/wazero/tree/main/examples/wasi for another example.
+func Example() {
+	r := wazero.NewRuntime()
+
+	// Instantiate WASI, which implements system I/O such as console output.
+	wm, err := InstantiateSnapshotPreview1(r)
+	if err != nil {
+		log.Fatal(err)
+	}
 	defer wm.Close()
 
+	// Override default configuration (which discards stdout).
+	config := wazero.NewModuleConfig().WithStdout(os.Stdout)
+
+	// InstantiateModuleFromCodeWithConfig runs the "_start" function which is like a "main" function.
+	_, err = r.InstantiateModuleFromCodeWithConfig([]byte(`
+(module
+  (import "wasi_snapshot_preview1" "proc_exit" (func $wasi.proc_exit (param $rval i32)))
+
+  (func $main
+     i32.const 2           ;; push $rval onto the stack
+     call $wasi.proc_exit  ;; return a sys.ExitError to the caller
+  )
+  (export "_start" (func $main))
+)
+`), config.WithName("wasi-demo"))
+
+	// Print the exit code
+	if exitErr, ok := err.(*sys.ExitError); ok {
+		fmt.Printf("exit_code: %d\n", exitErr.ExitCode())
+	}
+
 	// Output:
+	// exit_code: 2
 }
diff --git a/wasm_test.go b/wasm_test.go
index e47ddba7..e2a8edc0 100644
--- a/wasm_test.go
+++ b/wasm_test.go
@@ -13,6 +13,7 @@ import (
 	"github.com/tetratelabs/wazero/internal/testing/require"
 	"github.com/tetratelabs/wazero/internal/wasm"
 	"github.com/tetratelabs/wazero/internal/wasm/binary"
+	"github.com/tetratelabs/wazero/sys"
 )
 
 func TestRuntime_DecodeModule(t *testing.T) {
@@ -401,6 +402,19 @@ func TestInstantiateModuleWithConfig_WithName(t *testing.T) {
 	require.Equal(t, internal.Module("2"), m2)
 }
 
+func TestInstantiateModuleWithConfig_ExitError(t *testing.T) {
+	r := NewRuntime()
+
+	start := func(m api.Module) {
+		require.NoError(t, m.CloseWithExitCode(2))
+	}
+
+	_, err := r.NewModuleBuilder("env").ExportFunction("_start", start).Instantiate()
+
+	// Ensure the exit error propagated and didn't wrap.
+	require.Equal(t, err, sys.NewExitError("env", 2))
+}
+
 // requireImportAndExportFunction re-exports a host function because only host functions can see the propagated context.
 func requireImportAndExportFunction(t *testing.T, r Runtime, hostFn func(ctx api.Module) uint64, functionName string) ([]byte, func() error) {
 	mod, err := r.NewModuleBuilder("host").ExportFunction(functionName, hostFn).Instantiate()