From c4caa1ea9b8ac8500db4600fd918a5aa0ee47d2a Mon Sep 17 00:00:00 2001 From: Crypt Keeper <64215+codefromthecrypt@users.noreply.github.com> Date: Fri, 15 Apr 2022 15:51:27 +0800 Subject: [PATCH] Changes how examples are tested, and fixes ExitError bug (#468) 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. Signed-off-by: Adrian Cole --- example_test.go | 43 ++++++++++++++ examples/basic/add_test.go | 24 ++++---- examples/import-go/age-calculator_test.go | 31 +++++----- .../multiple-results/multiple-results_test.go | 25 +++++---- .../replace-import/replace-import_test.go | 18 +++--- examples/wasi/cat_test.go | 23 ++++---- internal/testing/maintester/maintester.go | 56 +++++++++++++++++++ sys/sys.go | 2 +- wasi/example_test.go | 46 +++++++++++++-- wasm.go | 10 +++- wasm_test.go | 14 +++++ 11 files changed, 221 insertions(+), 71 deletions(-) create mode 100644 example_test.go create mode 100644 internal/testing/maintester/maintester.go 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() { +func Test_main(t *testing.T) { + // Set ENV to ensure this test doesn't need maintenance every year. + t.Setenv("CURRENT_YEAR", "2021") - // 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() - - // 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: +import ( + "testing" + + "github.com/tetratelabs/wazero/internal/testing/maintester" + "github.com/tetratelabs/wazero/internal/testing/require" +) + +// Test_main ensures the following will work: // // go run multiple-results.go -func Example_main() { - - main() - - // Output: - // result-offset/wasm: age=37 - // result-offset/host: age=37 - // multi-value/wasm: age=37 - // multi-value/host: age=37 +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: +import ( + "testing" + + "github.com/tetratelabs/wazero/internal/testing/maintester" + "github.com/tetratelabs/wazero/internal/testing/require" +) + +// Test_main ensures the following will work: // // go run replace-import.go -func Example_main() { - - main() - - // Output: - // module "needs-import" closed with exit_code(255) +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/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/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.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 } 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()