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 <adrian@tetrate.io>
This commit is contained in:
Crypt Keeper
2022-04-15 15:51:27 +08:00
committed by GitHub
parent 26398f5263
commit c4caa1ea9b
11 changed files with 221 additions and 71 deletions

43
example_test.go Normal file
View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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--

View File

@@ -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
}

10
wasm.go
View File

@@ -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
}

View File

@@ -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()