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:
43
example_test.go
Normal file
43
example_test.go
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
56
internal/testing/maintester/maintester.go
Normal file
56
internal/testing/maintester/maintester.go
Normal 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
|
||||
}
|
||||
@@ -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--
|
||||
|
||||
@@ -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
10
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
|
||||
}
|
||||
|
||||
14
wasm_test.go
14
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()
|
||||
|
||||
Reference in New Issue
Block a user