Avoids returning ExitError on exit code zero, and optimizes for no allocations (#1284)
Fixes #1283 Signed-off-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
@@ -459,7 +459,7 @@ func TestRun(t *testing.T) {
|
||||
name: "timeout: a binary that exceeds the deadline should print an error",
|
||||
wazeroOpts: []string{"-timeout=1ms"},
|
||||
wasm: wasmInfiniteLoop,
|
||||
expectedStderr: "error: module \"\" closed with context deadline exceeded (timeout 1ms)\n",
|
||||
expectedStderr: "error: module closed with context deadline exceeded (timeout 1ms)\n",
|
||||
expectedExitCode: int(sys.ExitCodeDeadlineExceeded),
|
||||
test: func(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -45,7 +45,7 @@ func ExampleRuntimeConfig_WithCloseOnContextDone_context_timeout() {
|
||||
fmt.Println(err)
|
||||
|
||||
// Output:
|
||||
// module "malicious_wasm" closed with context deadline exceeded
|
||||
// module closed with context deadline exceeded
|
||||
}
|
||||
|
||||
// ExampleRuntimeConfig_WithCloseOnContextDone_context_cancel demonstrates how to ensure the termination
|
||||
@@ -82,7 +82,7 @@ func ExampleRuntimeConfig_WithCloseOnContextDone_context_cancel() {
|
||||
fmt.Println(err)
|
||||
|
||||
// Output:
|
||||
// module "malicious_wasm" closed with context canceled
|
||||
// module closed with context canceled
|
||||
}
|
||||
|
||||
// ExampleRuntimeConfig_WithCloseOnContextDone_moduleClose demonstrates how to ensure the termination
|
||||
@@ -120,5 +120,5 @@ func ExampleRuntimeConfig_WithCloseOnContextDone_moduleClose() {
|
||||
fmt.Println(err)
|
||||
|
||||
// Output:
|
||||
// module "malicious_wasm" closed with exit_code(1)
|
||||
// module closed with exit_code(1)
|
||||
}
|
||||
|
||||
@@ -177,7 +177,7 @@ func abort(ctx context.Context, mod api.Module, _ []uint64) {
|
||||
_ = mod.CloseWithExitCode(ctx, exitCode)
|
||||
|
||||
// Prevent any code from executing after this function.
|
||||
panic(sys.NewExitError(mod.Name(), exitCode))
|
||||
panic(sys.NewExitError(exitCode))
|
||||
}
|
||||
|
||||
// traceDisabled ignores the input.
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"github.com/tetratelabs/wazero/experimental/logging"
|
||||
"github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
|
||||
"github.com/tetratelabs/wazero/internal/testing/require"
|
||||
"github.com/tetratelabs/wazero/sys"
|
||||
)
|
||||
|
||||
// growWasm was compiled from testdata/grow.cc
|
||||
@@ -45,10 +44,9 @@ func TestGrow(t *testing.T) {
|
||||
_, err := Instantiate(ctx, r)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Emscripten exits main with zero by default
|
||||
// Emscripten exits main with zero by default, which coerces to nul.
|
||||
_, err = r.Instantiate(ctx, growWasm)
|
||||
require.Error(t, err)
|
||||
require.Zero(t, err.(*sys.ExitError).ExitCode())
|
||||
require.Nil(t, err)
|
||||
|
||||
// We expect the memory no-op memory growth hook to be invoked as wasm.
|
||||
require.Contains(t, log.String(), "==> env.emscripten_notify_memory_growth(memory_index=0)")
|
||||
|
||||
@@ -37,7 +37,7 @@ func procExitFn(ctx context.Context, mod api.Module, params []uint64) {
|
||||
// Prevent any code from executing after this function. For example, LLVM
|
||||
// inserts unreachable instructions after calls to exit.
|
||||
// See: https://github.com/emscripten-core/emscripten/issues/12322
|
||||
panic(sys.NewExitError(mod.Name(), exitCode))
|
||||
panic(sys.NewExitError(exitCode))
|
||||
}
|
||||
|
||||
// procRaise is stubbed and will never be supported, as it was removed.
|
||||
|
||||
@@ -72,7 +72,7 @@ func TestNewFunctionExporter(t *testing.T) {
|
||||
_, err = r.Instantiate(testCtx, exitOnStartWasm)
|
||||
|
||||
// Ensure the modified function was used!
|
||||
require.Zero(t, err.(*sys.ExitError).ExitCode())
|
||||
require.Nil(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ func Test_argsAndEnv(t *testing.T) {
|
||||
return moduleConfig.WithEnv("c", "d").WithEnv("a", "b"), config.NewConfig()
|
||||
})
|
||||
|
||||
require.EqualError(t, err, `module "" closed with exit_code(0)`)
|
||||
require.EqualError(t, err, `module closed with exit_code(0)`)
|
||||
require.Zero(t, stderr)
|
||||
require.Equal(t, `
|
||||
args 0 = test
|
||||
|
||||
@@ -20,7 +20,7 @@ func Test_crypto(t *testing.T) {
|
||||
stdout, stderr, err := compileAndRun(loggingCtx, "crypto", defaultConfig)
|
||||
|
||||
require.Zero(t, stderr)
|
||||
require.EqualError(t, err, `module "" closed with exit_code(0)`)
|
||||
require.EqualError(t, err, `module closed with exit_code(0)`)
|
||||
require.Equal(t, `7a0c9f9f0d
|
||||
`, stdout)
|
||||
require.Equal(t, `==> go.runtime.getRandomData(r_len=32)
|
||||
|
||||
@@ -20,7 +20,7 @@ func Test_fs(t *testing.T) {
|
||||
})
|
||||
|
||||
require.Zero(t, stderr)
|
||||
require.EqualError(t, err, `module "" closed with exit_code(0)`)
|
||||
require.EqualError(t, err, `module closed with exit_code(0)`)
|
||||
require.Equal(t, `sub mode drwxr-xr-x
|
||||
/animals.txt mode -rw-r--r--
|
||||
animals.txt mode -rw-r--r--
|
||||
@@ -50,7 +50,7 @@ func Test_testfs(t *testing.T) {
|
||||
})
|
||||
|
||||
require.Zero(t, stderr)
|
||||
require.EqualError(t, err, `module "" closed with exit_code(0)`)
|
||||
require.EqualError(t, err, `module closed with exit_code(0)`)
|
||||
require.Zero(t, stdout)
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ func Test_writefs(t *testing.T) {
|
||||
})
|
||||
|
||||
require.Zero(t, stderr)
|
||||
require.EqualError(t, err, `module "" closed with exit_code(0)`)
|
||||
require.EqualError(t, err, `module closed with exit_code(0)`)
|
||||
|
||||
if platform.CompilerSupported() {
|
||||
// Note: as of Go 1.19, only the Sec field is set on update in fs_js.go.
|
||||
|
||||
@@ -46,7 +46,7 @@ func Test_http(t *testing.T) {
|
||||
return moduleConfig.WithEnv("BASE_URL", "http://host"), config
|
||||
})
|
||||
|
||||
require.EqualError(t, err, `module "" closed with exit_code(0)`)
|
||||
require.EqualError(t, err, `module closed with exit_code(0)`)
|
||||
require.Zero(t, stderr)
|
||||
require.Equal(t, `Get "http://host/error": net/http: fetch() failed: error
|
||||
1
|
||||
|
||||
@@ -23,7 +23,7 @@ func Test_exit(t *testing.T) {
|
||||
|
||||
stdout, stderr, err := compileAndRun(loggingCtx, "exit", defaultConfig)
|
||||
|
||||
require.EqualError(t, err, `module "" closed with exit_code(255)`)
|
||||
require.EqualError(t, err, `module closed with exit_code(255)`)
|
||||
require.Zero(t, stderr)
|
||||
require.Zero(t, stdout)
|
||||
require.Equal(t, `==> go.runtime.wasmExit(code=255)
|
||||
@@ -36,7 +36,7 @@ func Test_goroutine(t *testing.T) {
|
||||
|
||||
stdout, stderr, err := compileAndRun(testCtx, "goroutine", defaultConfig)
|
||||
|
||||
require.EqualError(t, err, `module "" closed with exit_code(0)`)
|
||||
require.EqualError(t, err, `module closed with exit_code(0)`)
|
||||
require.Zero(t, stderr)
|
||||
require.Equal(t, `producer
|
||||
consumer
|
||||
@@ -52,7 +52,7 @@ func Test_mem(t *testing.T) {
|
||||
|
||||
stdout, stderr, err := compileAndRun(loggingCtx, "mem", defaultConfig)
|
||||
|
||||
require.EqualError(t, err, `module "" closed with exit_code(0)`)
|
||||
require.EqualError(t, err, `module closed with exit_code(0)`)
|
||||
require.Zero(t, stderr)
|
||||
require.Zero(t, stdout)
|
||||
|
||||
@@ -71,7 +71,7 @@ func Test_stdio(t *testing.T) {
|
||||
})
|
||||
|
||||
require.Equal(t, "stderr 6\n", stderr)
|
||||
require.EqualError(t, err, `module "" closed with exit_code(0)`)
|
||||
require.EqualError(t, err, `module closed with exit_code(0)`)
|
||||
require.Equal(t, "stdout 6\n", stdout)
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ func Test_stdio_large(t *testing.T) {
|
||||
return defaultConfig(moduleConfig.WithStdin(bytes.NewReader(input)))
|
||||
})
|
||||
|
||||
require.EqualError(t, err, `module "" closed with exit_code(0)`)
|
||||
require.EqualError(t, err, `module closed with exit_code(0)`)
|
||||
require.Equal(t, fmt.Sprintf("stderr %d\n", size), stderr)
|
||||
require.Equal(t, fmt.Sprintf("stdout %d\n", size), stdout)
|
||||
|
||||
@@ -107,7 +107,7 @@ func Test_gc(t *testing.T) {
|
||||
|
||||
stdout, stderr, err := compileAndRun(testCtx, "gc", defaultConfig)
|
||||
|
||||
require.EqualError(t, err, `module "" closed with exit_code(0)`)
|
||||
require.EqualError(t, err, `module closed with exit_code(0)`)
|
||||
require.Equal(t, "", stderr)
|
||||
require.Equal(t, "before gc\nafter gc\n", stdout)
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ func Test_process(t *testing.T) {
|
||||
})
|
||||
|
||||
require.Zero(t, stderr)
|
||||
require.EqualError(t, err, `module "" closed with exit_code(0)`)
|
||||
require.EqualError(t, err, `module closed with exit_code(0)`)
|
||||
require.Equal(t, `syscall.Getpid()=1
|
||||
syscall.Getppid()=0
|
||||
syscall.Getuid()=0
|
||||
|
||||
@@ -19,7 +19,7 @@ func Test_time(t *testing.T) {
|
||||
|
||||
stdout, stderr, err := compileAndRun(loggingCtx, "time", defaultConfig)
|
||||
|
||||
require.EqualError(t, err, `module "" closed with exit_code(0)`)
|
||||
require.EqualError(t, err, `module closed with exit_code(0)`)
|
||||
require.Zero(t, stderr)
|
||||
require.Equal(t, `Local
|
||||
1ms
|
||||
|
||||
@@ -3,7 +3,6 @@ package adhoc
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"testing"
|
||||
@@ -115,7 +114,7 @@ func testEnsureTerminationOnClose(t *testing.T, r wazero.Runtime) {
|
||||
}()
|
||||
_, err = infinite.Call(ctx)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), fmt.Sprintf("module \"%s\" closed with context canceled", t.Name()))
|
||||
require.Contains(t, err.Error(), "module closed with context canceled")
|
||||
})
|
||||
|
||||
t.Run("context cancel in advance", func(t *testing.T) {
|
||||
@@ -124,7 +123,7 @@ func testEnsureTerminationOnClose(t *testing.T, r wazero.Runtime) {
|
||||
cancel()
|
||||
_, err = infinite.Call(ctx)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), fmt.Sprintf("module \"%s\" closed with context canceled", t.Name()))
|
||||
require.Contains(t, err.Error(), "module closed with context canceled")
|
||||
})
|
||||
|
||||
t.Run("context timeout", func(t *testing.T) {
|
||||
@@ -133,7 +132,7 @@ func testEnsureTerminationOnClose(t *testing.T, r wazero.Runtime) {
|
||||
defer cancel()
|
||||
_, err = infinite.Call(ctx)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), fmt.Sprintf("module \"%s\" closed with context deadline exceeded", t.Name()))
|
||||
require.Contains(t, err.Error(), "module closed with context deadline exceeded")
|
||||
})
|
||||
|
||||
t.Run("explicit close of module", func(t *testing.T) {
|
||||
@@ -144,7 +143,7 @@ func testEnsureTerminationOnClose(t *testing.T, r wazero.Runtime) {
|
||||
}()
|
||||
_, err = infinite.Call(context.Background())
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), fmt.Sprintf("module \"%s\" closed with exit_code(2)", t.Name()))
|
||||
require.Contains(t, err.Error(), "module closed with exit_code(2)")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -688,11 +687,11 @@ func testCloseInFlight(t *testing.T, r wazero.Runtime) {
|
||||
var expectedErr error
|
||||
if tc.closeImported != 0 && tc.closeImporting != 0 {
|
||||
// When both modules are closed, importing is the better one to choose in the error message.
|
||||
expectedErr = sys.NewExitError(importing.Name(), tc.closeImporting)
|
||||
expectedErr = sys.NewExitError(tc.closeImporting)
|
||||
} else if tc.closeImported != 0 {
|
||||
expectedErr = sys.NewExitError(imported.Name(), tc.closeImported)
|
||||
expectedErr = sys.NewExitError(tc.closeImported)
|
||||
} else if tc.closeImporting != 0 {
|
||||
expectedErr = sys.NewExitError(importing.Name(), tc.closeImporting)
|
||||
expectedErr = sys.NewExitError(tc.closeImporting)
|
||||
} else {
|
||||
t.Fatal("invalid test case")
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ func closeModuleWhileInUse(t *testing.T, r wazero.Runtime, closeFn func(imported
|
||||
i := importing // pin the module used inside goroutines
|
||||
hammer.NewHammer(t, P, 1).Run(func(name string) {
|
||||
// In all cases, the importing module is closed, so the error should have that as its module name.
|
||||
requireFunctionCallExits(t, i.Name(), i.ExportedFunction("call_return_input"))
|
||||
requireFunctionCallExits(t, i.ExportedFunction("call_return_input"))
|
||||
}, func() { // When all functions are in-flight, re-assign the modules.
|
||||
imported, importing = closeFn(imported, importing)
|
||||
// Unblock all the calls
|
||||
@@ -122,7 +122,7 @@ func requireFunctionCall(t *testing.T, fn api.Function) {
|
||||
require.Equal(t, uint64(3), res[0])
|
||||
}
|
||||
|
||||
func requireFunctionCallExits(t *testing.T, moduleName string, fn api.Function) {
|
||||
func requireFunctionCallExits(t *testing.T, fn api.Function) {
|
||||
_, err := fn.Call(testCtx, 3)
|
||||
require.Equal(t, sys.NewExitError(moduleName, 0), err)
|
||||
require.Equal(t, sys.NewExitError(0), err)
|
||||
}
|
||||
|
||||
@@ -42,6 +42,10 @@ func Contains(t TestingT, s, substr string, formatWithArgs ...interface{}) {
|
||||
//
|
||||
// - formatWithArgs are optional. When the first is a string that contains '%', it is treated like fmt.Sprintf.
|
||||
func Equal(t TestingT, expected, actual interface{}, formatWithArgs ...interface{}) {
|
||||
if expected == nil {
|
||||
Nil(t, actual)
|
||||
return
|
||||
}
|
||||
if equal(expected, actual) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ type CallContext struct {
|
||||
// FailIfClosed returns a sys.ExitError if CloseWithExitCode was called.
|
||||
func (m *CallContext) FailIfClosed() (err error) {
|
||||
if closed := atomic.LoadUint64(&m.Closed); closed != 0 {
|
||||
return sys.NewExitError(m.module.Name, uint32(closed>>32)) // Unpack the high order bits as the exit code.
|
||||
return sys.NewExitError(uint32(closed >> 32)) // Unpack the high order bits as the exit code.
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -271,7 +271,7 @@ func TestCallContext_CloseModuleOnCanceledOrTimeout(t *testing.T) {
|
||||
defer done()
|
||||
|
||||
err := cc.FailIfClosed()
|
||||
require.EqualError(t, err, "module \"test\" closed with context deadline exceeded")
|
||||
require.EqualError(t, err, "module closed with context deadline exceeded")
|
||||
})
|
||||
|
||||
t.Run("cancel", func(t *testing.T) {
|
||||
@@ -286,7 +286,7 @@ func TestCallContext_CloseModuleOnCanceledOrTimeout(t *testing.T) {
|
||||
|
||||
time.Sleep(time.Second)
|
||||
err := cc.FailIfClosed()
|
||||
require.EqualError(t, err, "module \"test\" closed with context canceled")
|
||||
require.EqualError(t, err, "module closed with context canceled")
|
||||
})
|
||||
|
||||
t.Run("timeout over cancel", func(t *testing.T) {
|
||||
@@ -316,7 +316,7 @@ func TestCallContext_CloseModuleOnCanceledOrTimeout(t *testing.T) {
|
||||
|
||||
time.Sleep(time.Second)
|
||||
err := cc.FailIfClosed()
|
||||
require.EqualError(t, err, "module \"test\" closed with context canceled")
|
||||
require.EqualError(t, err, "module closed with context canceled")
|
||||
})
|
||||
|
||||
t.Run("cancel works", func(t *testing.T) {
|
||||
@@ -359,7 +359,7 @@ func TestCallContext_CloseWithCtxErr(t *testing.T) {
|
||||
cc.CloseWithCtxErr(ctx)
|
||||
|
||||
err := cc.FailIfClosed()
|
||||
require.EqualError(t, err, "module \"test\" closed with context canceled")
|
||||
require.EqualError(t, err, "module closed with context canceled")
|
||||
})
|
||||
|
||||
t.Run("context timeout", func(t *testing.T) {
|
||||
@@ -373,7 +373,7 @@ func TestCallContext_CloseWithCtxErr(t *testing.T) {
|
||||
cc.CloseWithCtxErr(ctx)
|
||||
|
||||
err := cc.FailIfClosed()
|
||||
require.EqualError(t, err, "module \"test\" closed with context deadline exceeded")
|
||||
require.EqualError(t, err, "module closed with context deadline exceeded")
|
||||
})
|
||||
|
||||
t.Run("no error", func(t *testing.T) {
|
||||
|
||||
39
runtime.go
39
runtime.go
@@ -34,11 +34,14 @@ type Runtime interface {
|
||||
//
|
||||
// mod, _ := r.Instantiate(ctx, wasm)
|
||||
//
|
||||
// See InstantiateWithConfig for configuration overrides.
|
||||
// # Notes
|
||||
//
|
||||
// - See notes on InstantiateModule for error scenarios.
|
||||
// - See InstantiateWithConfig for configuration overrides.
|
||||
Instantiate(ctx context.Context, source []byte) (api.Module, error)
|
||||
|
||||
// InstantiateWithConfig instantiates a module from the WebAssembly binary
|
||||
// (%.wasm) or errs if invalid.
|
||||
// (%.wasm) or errs for reasons including exit or validation.
|
||||
//
|
||||
// Here's an example:
|
||||
// ctx := context.Background()
|
||||
@@ -50,11 +53,12 @@ type Runtime interface {
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - See notes on InstantiateModule for error scenarios.
|
||||
// - If you aren't overriding defaults, use Instantiate.
|
||||
// - This is a convenience utility that chains CompileModule with
|
||||
// InstantiateModule. To instantiate the same source multiple times,
|
||||
// use CompileModule as InstantiateModule avoids redundant decoding
|
||||
// and/or compilation.
|
||||
// - If you aren't overriding defaults, use Instantiate.
|
||||
InstantiateWithConfig(ctx context.Context, source []byte, config ModuleConfig) (api.Module, error)
|
||||
|
||||
// NewHostModuleBuilder lets you create modules out of functions defined in Go.
|
||||
@@ -87,15 +91,25 @@ type Runtime interface {
|
||||
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#name-section%E2%91%A0
|
||||
CompileModule(ctx context.Context, binary []byte) (CompiledModule, error)
|
||||
|
||||
// InstantiateModule instantiates the module or errs if the configuration was invalid.
|
||||
// InstantiateModule instantiates the module or errs for reasons including
|
||||
// exit or validation.
|
||||
//
|
||||
// Here's an example:
|
||||
// mod, _ := n.InstantiateModule(ctx, compiled, wazero.NewModuleConfig().WithName("prod"))
|
||||
// mod, _ := n.InstantiateModule(ctx, compiled, wazero.NewModuleConfig().
|
||||
// WithName("prod"))
|
||||
//
|
||||
// While CompiledModule is pre-validated, there are a few situations which can cause an error:
|
||||
// # Errors
|
||||
//
|
||||
// While CompiledModule is pre-validated, there are a few situations which
|
||||
// can cause an error:
|
||||
// - The module name is already in use.
|
||||
// - The module has a table element initializer that resolves to an index outside the Table minimum size.
|
||||
// - The module has a table element initializer that resolves to an index
|
||||
// outside the Table minimum size.
|
||||
// - The module has a start function, and it failed to execute.
|
||||
// - The module was compiled to WASI and exited with a non-zero exit
|
||||
// code, you'll receive a sys.ExitError.
|
||||
// - RuntimeConfig.WithCloseOnContextDone was enabled and a context
|
||||
// cancellation or deadline triggered before a start function returned.
|
||||
InstantiateModule(ctx context.Context, compiled CompiledModule, config ModuleConfig) (api.Module, error)
|
||||
|
||||
// CloseWithExitCode closes all the modules that have been initialized in this Runtime with the provided exit code.
|
||||
@@ -294,6 +308,11 @@ func (r *runtime) InstantiateModule(
|
||||
if code.closeWithModule {
|
||||
_ = code.Close(ctx) // don't overwrite the error
|
||||
}
|
||||
if se, ok := err.(*sys.ExitError); ok {
|
||||
if se.ExitCode() == 0 { // Don't err on success.
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -310,7 +329,11 @@ func (r *runtime) InstantiateModule(
|
||||
}
|
||||
if _, err = start.Call(ctx); err != nil {
|
||||
_ = mod.Close(ctx) // Don't leak the module on error.
|
||||
if _, ok := err.(*sys.ExitError); ok {
|
||||
|
||||
if se, ok := err.(*sys.ExitError); ok {
|
||||
if se.ExitCode() == 0 { // Don't err on success.
|
||||
err = nil
|
||||
}
|
||||
return // Don't wrap an exit error
|
||||
}
|
||||
err = fmt.Errorf("module[%s] function[%s] failed: %w", name, fn, err)
|
||||
|
||||
@@ -471,32 +471,73 @@ func TestRuntime_InstantiateModule_ExitError(t *testing.T) {
|
||||
r := NewRuntime(testCtx)
|
||||
defer r.Close(testCtx)
|
||||
|
||||
start := func(ctx context.Context, m api.Module) {
|
||||
require.NoError(t, m.CloseWithExitCode(ctx, 2))
|
||||
tests := []struct {
|
||||
name string
|
||||
exitCode uint32
|
||||
export bool
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
name: "start: exit code 0",
|
||||
exitCode: 0,
|
||||
},
|
||||
{
|
||||
name: "start: exit code 2",
|
||||
exitCode: 2,
|
||||
expectedErr: sys.NewExitError(2),
|
||||
},
|
||||
{
|
||||
name: "_start: exit code 0",
|
||||
exitCode: 0,
|
||||
export: true,
|
||||
},
|
||||
{
|
||||
name: "_start: exit code 2",
|
||||
exitCode: 2,
|
||||
export: true,
|
||||
expectedErr: sys.NewExitError(2),
|
||||
},
|
||||
}
|
||||
|
||||
_, err := r.NewHostModuleBuilder("env").
|
||||
for _, tt := range tests {
|
||||
tc := tt
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
start := func(ctx context.Context, m api.Module) {
|
||||
require.NoError(t, m.CloseWithExitCode(ctx, tc.exitCode))
|
||||
}
|
||||
|
||||
env, err := r.NewHostModuleBuilder("env").
|
||||
NewFunctionBuilder().WithFunc(start).Export("exit").
|
||||
Instantiate(testCtx)
|
||||
require.NoError(t, err)
|
||||
defer env.Close(testCtx)
|
||||
|
||||
one := uint32(1)
|
||||
binary := binaryencoding.EncodeModule(&wasm.Module{
|
||||
mod := &wasm.Module{
|
||||
TypeSection: []wasm.FunctionType{{}},
|
||||
ImportSection: []wasm.Import{{Module: "env", Name: "exit", Type: wasm.ExternTypeFunc, DescFunc: 0}},
|
||||
FunctionSection: []wasm.Index{0},
|
||||
CodeSection: []wasm.Code{
|
||||
{Body: []byte{wasm.OpcodeCall, 0, wasm.OpcodeEnd}}, // Call the imported env.start.
|
||||
},
|
||||
StartSection: &one,
|
||||
})
|
||||
}
|
||||
if tc.export {
|
||||
mod.ExportSection = []wasm.Export{
|
||||
{Name: "_start", Type: wasm.ExternTypeFunc, Index: 1},
|
||||
}
|
||||
} else {
|
||||
one := uint32(1)
|
||||
mod.StartSection = &one
|
||||
}
|
||||
binary := binaryencoding.EncodeModule(mod)
|
||||
|
||||
// Instantiate the module, which calls the start function.
|
||||
_, err = r.InstantiateWithConfig(testCtx, binary,
|
||||
NewModuleConfig().WithName("call-exit"))
|
||||
|
||||
// Ensure the exit error propagated and didn't wrap.
|
||||
require.Equal(t, err, sys.NewExitError("call-exit", 2))
|
||||
require.Equal(t, tc.expectedErr, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuntime_CloseWithExitCode(t *testing.T) {
|
||||
@@ -559,10 +600,10 @@ func TestRuntime_CloseWithExitCode(t *testing.T) {
|
||||
|
||||
// Modules closed so calls fail
|
||||
_, err = func1.Call(testCtx)
|
||||
require.ErrorIs(t, err, sys.NewExitError("mod1", tc.exitCode))
|
||||
require.ErrorIs(t, err, sys.NewExitError(tc.exitCode))
|
||||
|
||||
_, err = func2.Call(testCtx)
|
||||
require.ErrorIs(t, err, sys.NewExitError("mod2", tc.exitCode))
|
||||
require.ErrorIs(t, err, sys.NewExitError(tc.exitCode))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
25
sys/error.go
25
sys/error.go
@@ -25,7 +25,7 @@ const (
|
||||
// main := module.ExportedFunction("main")
|
||||
// if err := main(ctx); err != nil {
|
||||
// if exitErr, ok := err.(*sys.ExitError); ok {
|
||||
// // If your main function expects to exit, this could be ok if Code == 0
|
||||
// // This means your module exited with non-zero code!
|
||||
// }
|
||||
// --snip--
|
||||
//
|
||||
@@ -37,17 +37,18 @@ const (
|
||||
//
|
||||
// Note: In the case of context cancellation or timeout, the api.Module from which the api.Function created is closed.
|
||||
type ExitError struct {
|
||||
moduleName string
|
||||
// Note: this is a struct not a uint32 type as it was originally one and
|
||||
// we don't want to break call-sites that cast into it.
|
||||
exitCode uint32
|
||||
}
|
||||
|
||||
func NewExitError(moduleName string, exitCode uint32) *ExitError {
|
||||
return &ExitError{moduleName: moduleName, exitCode: exitCode}
|
||||
}
|
||||
var exitZero = &ExitError{}
|
||||
|
||||
// ModuleName is the api.Module that was closed.
|
||||
func (e *ExitError) ModuleName() string {
|
||||
return e.moduleName
|
||||
func NewExitError(exitCode uint32) *ExitError {
|
||||
if exitCode == 0 {
|
||||
return exitZero
|
||||
}
|
||||
return &ExitError{exitCode: exitCode}
|
||||
}
|
||||
|
||||
// ExitCode returns zero on success, and an arbitrary value otherwise.
|
||||
@@ -59,18 +60,18 @@ func (e *ExitError) ExitCode() uint32 {
|
||||
func (e *ExitError) Error() string {
|
||||
switch e.exitCode {
|
||||
case ExitCodeContextCanceled:
|
||||
return fmt.Sprintf("module %q closed with %s", e.moduleName, context.Canceled)
|
||||
return fmt.Sprintf("module closed with %s", context.Canceled)
|
||||
case ExitCodeDeadlineExceeded:
|
||||
return fmt.Sprintf("module %q closed with %s", e.moduleName, context.DeadlineExceeded)
|
||||
return fmt.Sprintf("module closed with %s", context.DeadlineExceeded)
|
||||
default:
|
||||
return fmt.Sprintf("module %q closed with exit_code(%d)", e.moduleName, e.exitCode)
|
||||
return fmt.Sprintf("module closed with exit_code(%d)", e.exitCode)
|
||||
}
|
||||
}
|
||||
|
||||
// Is allows use via errors.Is
|
||||
func (e *ExitError) Is(err error) bool {
|
||||
if target, ok := err.(*ExitError); ok {
|
||||
return e.moduleName == target.moduleName && e.exitCode == target.exitCode
|
||||
return e.exitCode == target.exitCode
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
)
|
||||
|
||||
type notExitError struct {
|
||||
moduleName string
|
||||
exitCode uint32
|
||||
}
|
||||
|
||||
@@ -17,7 +16,7 @@ func (e *notExitError) Error() string {
|
||||
}
|
||||
|
||||
func TestIs(t *testing.T) {
|
||||
err := NewExitError("some module", 2)
|
||||
err := NewExitError(2)
|
||||
tests := []struct {
|
||||
name string
|
||||
target error
|
||||
@@ -28,25 +27,14 @@ func TestIs(t *testing.T) {
|
||||
target: err,
|
||||
matches: true,
|
||||
},
|
||||
{
|
||||
name: "same content",
|
||||
target: NewExitError("some module", 2),
|
||||
matches: true,
|
||||
},
|
||||
{
|
||||
name: "different module name",
|
||||
target: NewExitError("not some module", 2),
|
||||
matches: false,
|
||||
},
|
||||
{
|
||||
name: "different exit code",
|
||||
target: NewExitError("some module", 0),
|
||||
target: NewExitError(1),
|
||||
matches: false,
|
||||
},
|
||||
{
|
||||
name: "different type",
|
||||
target: ¬ExitError{
|
||||
moduleName: "some module",
|
||||
exitCode: 2,
|
||||
},
|
||||
matches: false,
|
||||
@@ -64,18 +52,18 @@ func TestIs(t *testing.T) {
|
||||
|
||||
func TestExitError_Error(t *testing.T) {
|
||||
t.Run("timeout", func(t *testing.T) {
|
||||
err := NewExitError("foo", ExitCodeDeadlineExceeded)
|
||||
err := NewExitError(ExitCodeDeadlineExceeded)
|
||||
require.Equal(t, ExitCodeDeadlineExceeded, err.ExitCode())
|
||||
require.EqualError(t, err, "module \"foo\" closed with context deadline exceeded")
|
||||
require.EqualError(t, err, "module closed with context deadline exceeded")
|
||||
})
|
||||
t.Run("cancel", func(t *testing.T) {
|
||||
err := NewExitError("foo", ExitCodeContextCanceled)
|
||||
err := NewExitError(ExitCodeContextCanceled)
|
||||
require.Equal(t, ExitCodeContextCanceled, err.ExitCode())
|
||||
require.EqualError(t, err, "module \"foo\" closed with context canceled")
|
||||
require.EqualError(t, err, "module closed with context canceled")
|
||||
})
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
err := NewExitError("foo", 123)
|
||||
err := NewExitError(123)
|
||||
require.Equal(t, uint32(123), err.ExitCode())
|
||||
require.EqualError(t, err, "module \"foo\" closed with exit_code(123)")
|
||||
require.EqualError(t, err, "module closed with exit_code(123)")
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user