WebAssembly Core Working Draft 1 recently came out. Before that, we had a toe-hold feature bucked called FinishedFeatures. This replaces `RuntimeConfig.WithFinishedFeatures` with `RuntimeConfig.WithWasmCore2`. This also adds `WithWasmCore1` for those who want to lock into 1.0 features as opposed to relying on defaults. This also fixes some design debt where we hadn't finished migrating public types that require constructor functions (NewXxx) to interfaces. By using interfaces, we prevent people from accidentally initializing key configuration directly (via &Xxx), causing nil field refs. This also helps prevent confusion about how to use the type (ex pointer or not) as there's only one way (as an interface). See https://github.com/tetratelabs/wazero/issues/516 Signed-off-by: Adrian Cole <adrian@tetrate.io>
535 lines
15 KiB
Go
535 lines
15 KiB
Go
package wazero
|
|
|
|
import (
|
|
"context"
|
|
_ "embed"
|
|
"fmt"
|
|
"math"
|
|
"testing"
|
|
|
|
"github.com/tetratelabs/wazero/api"
|
|
"github.com/tetratelabs/wazero/internal/leb128"
|
|
"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"
|
|
)
|
|
|
|
// testCtx is an arbitrary, non-default context. Non-nil also prevents linter errors.
|
|
var testCtx = context.WithValue(context.Background(), struct{}{}, "arbitrary")
|
|
|
|
func TestNewRuntimeWithConfig_PanicsOnWrongImpl(t *testing.T) {
|
|
// It is too burdensome to define an impl of RuntimeConfig in tests just to verify the error when it is wrong.
|
|
// Instead, we pass nil which is implicitly the wrong type, as that's less work!
|
|
err := require.CapturePanic(func() {
|
|
NewRuntimeWithConfig(nil)
|
|
})
|
|
|
|
require.EqualError(t, err, "unsupported wazero.RuntimeConfig implementation: <nil>")
|
|
}
|
|
|
|
func TestRuntime_CompileModule(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
runtime Runtime
|
|
source []byte
|
|
expectedName string
|
|
}{
|
|
{
|
|
name: "text - no name",
|
|
source: []byte(`(module)`),
|
|
},
|
|
{
|
|
name: "text - empty name",
|
|
source: []byte(`(module $)`),
|
|
},
|
|
{
|
|
name: "text - name",
|
|
source: []byte(`(module $test)`),
|
|
expectedName: "test",
|
|
},
|
|
{
|
|
name: "binary - no name section",
|
|
source: binary.EncodeModule(&wasm.Module{}),
|
|
},
|
|
{
|
|
name: "binary - empty NameSection.ModuleName",
|
|
source: binary.EncodeModule(&wasm.Module{NameSection: &wasm.NameSection{}}),
|
|
},
|
|
{
|
|
name: "binary - NameSection.ModuleName",
|
|
source: binary.EncodeModule(&wasm.Module{NameSection: &wasm.NameSection{ModuleName: "test"}}),
|
|
expectedName: "test",
|
|
},
|
|
}
|
|
|
|
r := NewRuntime()
|
|
for _, tt := range tests {
|
|
tc := tt
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
code, err := r.CompileModule(testCtx, tc.source)
|
|
require.NoError(t, err)
|
|
defer code.Close(testCtx)
|
|
if tc.expectedName != "" {
|
|
require.Equal(t, tc.expectedName, code.module.NameSection.ModuleName)
|
|
}
|
|
require.Equal(t, r.(*runtime).store.Engine, code.compiledEngine)
|
|
})
|
|
}
|
|
|
|
t.Run("text - memory", func(t *testing.T) {
|
|
r := NewRuntimeWithConfig(NewRuntimeConfig().
|
|
WithMemoryCapacityPages(func(minPages uint32, maxPages *uint32) uint32 { return 2 }))
|
|
|
|
source := []byte(`(module (memory 1 3))`)
|
|
|
|
code, err := r.CompileModule(testCtx, source)
|
|
require.NoError(t, err)
|
|
defer code.Close(testCtx)
|
|
|
|
require.Equal(t, &wasm.Memory{
|
|
Min: 1,
|
|
Cap: 2, // Uses capacity function
|
|
Max: 3,
|
|
IsMaxEncoded: true,
|
|
}, code.module.MemorySection)
|
|
})
|
|
}
|
|
|
|
func TestRuntime_CompileModule_Errors(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
runtime Runtime
|
|
source []byte
|
|
expectedErr string
|
|
}{
|
|
{
|
|
name: "nil",
|
|
expectedErr: "source == nil",
|
|
},
|
|
{
|
|
name: "invalid binary",
|
|
source: append(binary.Magic, []byte("yolo")...),
|
|
expectedErr: "invalid version header",
|
|
},
|
|
{
|
|
name: "invalid text",
|
|
source: []byte(`(modular)`),
|
|
expectedErr: "1:2: unexpected field: modular",
|
|
},
|
|
{
|
|
name: "RuntimeConfig.memoryLimitPages too large",
|
|
runtime: NewRuntimeWithConfig(NewRuntimeConfig().WithMemoryLimitPages(math.MaxUint32)),
|
|
source: []byte(`(module)`),
|
|
expectedErr: "memoryLimitPages 4294967295 (3 Ti) > specification max 65536 (4 Gi)",
|
|
},
|
|
{
|
|
name: "memory has too many pages - text",
|
|
runtime: NewRuntimeWithConfig(NewRuntimeConfig().WithMemoryLimitPages(2)),
|
|
source: []byte(`(module (memory 3))`),
|
|
expectedErr: "1:17: min 3 pages (192 Ki) over limit of 2 pages (128 Ki) in module.memory[0]",
|
|
},
|
|
{
|
|
name: "memory cap < min", // only one test to avoid duplicating tests in module_test.go
|
|
runtime: NewRuntimeWithConfig(NewRuntimeConfig().
|
|
WithMemoryCapacityPages(func(minPages uint32, maxPages *uint32) uint32 { return 1 })),
|
|
source: []byte(`(module (memory 3))`),
|
|
expectedErr: "memory[0] capacity 1 pages (64 Ki) less than minimum 3 pages (192 Ki)",
|
|
},
|
|
{
|
|
name: "memory cap < min - exported", // only one test to avoid duplicating tests in module_test.go
|
|
runtime: NewRuntimeWithConfig(NewRuntimeConfig().
|
|
WithMemoryCapacityPages(func(minPages uint32, maxPages *uint32) uint32 { return 1 })),
|
|
source: []byte(`(module (memory 3) (export "memory" (memory 0)))`),
|
|
expectedErr: "memory[memory] capacity 1 pages (64 Ki) less than minimum 3 pages (192 Ki)",
|
|
},
|
|
{
|
|
name: "memory has too many pages - binary",
|
|
runtime: NewRuntimeWithConfig(NewRuntimeConfig().WithMemoryLimitPages(2)),
|
|
source: binary.EncodeModule(&wasm.Module{MemorySection: &wasm.Memory{Min: 2, Max: 3, IsMaxEncoded: true}}),
|
|
expectedErr: "section memory: max 3 pages (192 Ki) over limit of 2 pages (128 Ki)",
|
|
},
|
|
}
|
|
|
|
r := NewRuntime()
|
|
for _, tt := range tests {
|
|
tc := tt
|
|
|
|
if tc.runtime == nil {
|
|
tc.runtime = r
|
|
}
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
_, err := tc.runtime.CompileModule(testCtx, tc.source)
|
|
require.EqualError(t, err, tc.expectedErr)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRuntime_setMemoryCapacity(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
runtime *runtime
|
|
mem *wasm.Memory
|
|
expectedErr string
|
|
}{
|
|
{
|
|
name: "cap ok",
|
|
runtime: &runtime{memoryCapacityPages: func(minPages uint32, maxPages *uint32) uint32 {
|
|
return 3
|
|
}, memoryLimitPages: 3},
|
|
mem: &wasm.Memory{Min: 2},
|
|
},
|
|
{
|
|
name: "cap < min",
|
|
runtime: &runtime{memoryCapacityPages: func(minPages uint32, maxPages *uint32) uint32 {
|
|
return 1
|
|
}, memoryLimitPages: 3},
|
|
mem: &wasm.Memory{Min: 2},
|
|
expectedErr: "memory[memory] capacity 1 pages (64 Ki) less than minimum 2 pages (128 Ki)",
|
|
},
|
|
{
|
|
name: "cap > maxLimit",
|
|
runtime: &runtime{memoryCapacityPages: func(minPages uint32, maxPages *uint32) uint32 {
|
|
return 4
|
|
}, memoryLimitPages: 3},
|
|
mem: &wasm.Memory{Min: 2},
|
|
expectedErr: "memory[memory] capacity 4 pages (256 Ki) over limit of 3 pages (192 Ki)",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tc := tt
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
err := tc.runtime.setMemoryCapacity("memory", tc.mem)
|
|
if tc.expectedErr == "" {
|
|
require.NoError(t, err)
|
|
} else {
|
|
require.EqualError(t, err, tc.expectedErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestModule_Memory only covers a couple cases to avoid duplication of internal/wasm/runtime_test.go
|
|
func TestModule_Memory(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
builder func(Runtime) ModuleBuilder
|
|
expected bool
|
|
expectedLen uint32
|
|
}{
|
|
{
|
|
name: "no memory",
|
|
builder: func(r Runtime) ModuleBuilder {
|
|
return r.NewModuleBuilder(t.Name())
|
|
},
|
|
},
|
|
{
|
|
name: "memory exported, one page",
|
|
builder: func(r Runtime) ModuleBuilder {
|
|
return r.NewModuleBuilder(t.Name()).ExportMemory("memory", 1)
|
|
},
|
|
expected: true,
|
|
expectedLen: 65536,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tc := tt
|
|
|
|
r := NewRuntime()
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
// Instantiate the module and get the export of the above memory
|
|
module, err := tc.builder(r).Instantiate(testCtx)
|
|
require.NoError(t, err)
|
|
defer module.Close(testCtx)
|
|
|
|
mem := module.ExportedMemory("memory")
|
|
if tc.expected {
|
|
require.Equal(t, tc.expectedLen, mem.Size(testCtx))
|
|
} else {
|
|
require.Nil(t, mem)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestModule_Global only covers a couple cases to avoid duplication of internal/wasm/global_test.go
|
|
func TestModule_Global(t *testing.T) {
|
|
globalVal := int64(100) // intentionally a value that differs in signed vs unsigned encoding
|
|
|
|
tests := []struct {
|
|
name string
|
|
module *wasm.Module // module as wat doesn't yet support globals
|
|
builder func(Runtime) ModuleBuilder
|
|
expected, expectedMutable bool
|
|
}{
|
|
{
|
|
name: "no global",
|
|
module: &wasm.Module{},
|
|
},
|
|
{
|
|
name: "global not exported",
|
|
module: &wasm.Module{
|
|
GlobalSection: []*wasm.Global{
|
|
{
|
|
Type: &wasm.GlobalType{ValType: wasm.ValueTypeI64, Mutable: true},
|
|
Init: &wasm.ConstantExpression{Opcode: wasm.OpcodeI64Const, Data: leb128.EncodeInt64(globalVal)},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "global exported",
|
|
builder: func(r Runtime) ModuleBuilder {
|
|
return r.NewModuleBuilder(t.Name()).ExportGlobalI64("global", globalVal)
|
|
},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "global exported and mutable",
|
|
module: &wasm.Module{
|
|
GlobalSection: []*wasm.Global{
|
|
{
|
|
Type: &wasm.GlobalType{ValType: wasm.ValueTypeI64, Mutable: true},
|
|
Init: &wasm.ConstantExpression{Opcode: wasm.OpcodeI64Const, Data: leb128.EncodeInt64(globalVal)},
|
|
},
|
|
},
|
|
ExportSection: []*wasm.Export{
|
|
{Type: wasm.ExternTypeGlobal, Name: "global"},
|
|
},
|
|
},
|
|
expected: true,
|
|
expectedMutable: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tc := tt
|
|
|
|
r := NewRuntime().(*runtime)
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
var code *CompiledCode
|
|
if tc.module != nil {
|
|
code = &CompiledCode{module: tc.module}
|
|
} else {
|
|
code, _ = tc.builder(r).Build(testCtx)
|
|
}
|
|
|
|
err := r.store.Engine.CompileModule(testCtx, code.module)
|
|
require.NoError(t, err)
|
|
|
|
// Instantiate the module and get the export of the above global
|
|
module, err := r.InstantiateModule(testCtx, code)
|
|
require.NoError(t, err)
|
|
defer module.Close(testCtx)
|
|
|
|
global := module.ExportedGlobal("global")
|
|
if !tc.expected {
|
|
require.Nil(t, global)
|
|
return
|
|
}
|
|
require.Equal(t, uint64(globalVal), global.Get(testCtx))
|
|
|
|
mutable, ok := global.(api.MutableGlobal)
|
|
require.Equal(t, tc.expectedMutable, ok)
|
|
if ok {
|
|
mutable.Set(testCtx, 2)
|
|
require.Equal(t, uint64(2), global.Get(testCtx))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFunction_Context(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
ctx context.Context
|
|
expected context.Context
|
|
}{
|
|
{
|
|
name: "nil defaults to context.Background",
|
|
ctx: nil,
|
|
expected: context.Background(),
|
|
},
|
|
{
|
|
name: "set context",
|
|
ctx: testCtx,
|
|
expected: testCtx,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tc := tt
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
r := NewRuntime()
|
|
|
|
// Define a host function so that we can catch the context propagated from a module function call
|
|
functionName := "fn"
|
|
expectedResult := uint64(math.MaxUint64)
|
|
hostFn := func(ctx context.Context) uint64 {
|
|
require.Equal(t, tc.expected, ctx)
|
|
return expectedResult
|
|
}
|
|
source, closer := requireImportAndExportFunction(t, r, hostFn, functionName)
|
|
defer closer(testCtx) // nolint
|
|
|
|
// Instantiate the module and get the export of the above hostFn
|
|
module, err := r.InstantiateModuleFromCodeWithConfig(tc.ctx, source, NewModuleConfig().WithName(t.Name()))
|
|
require.NoError(t, err)
|
|
defer module.Close(testCtx)
|
|
|
|
// This fails if the function wasn't invoked, or had an unexpected context.
|
|
results, err := module.ExportedFunction(functionName).Call(tc.ctx)
|
|
require.NoError(t, err)
|
|
require.Equal(t, expectedResult, results[0])
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRuntime_InstantiateModule_UsesContext(t *testing.T) {
|
|
r := NewRuntime()
|
|
|
|
// Define a function that will be set as the start function
|
|
var calledStart bool
|
|
start := func(ctx context.Context) {
|
|
calledStart = true
|
|
require.Equal(t, testCtx, ctx)
|
|
}
|
|
|
|
env, err := r.NewModuleBuilder("env").
|
|
ExportFunction("start", start).
|
|
Instantiate(testCtx)
|
|
require.NoError(t, err)
|
|
defer env.Close(testCtx)
|
|
|
|
code, err := r.CompileModule(testCtx, []byte(`(module $runtime_test.go
|
|
(import "env" "start" (func $start))
|
|
(start $start)
|
|
)`))
|
|
require.NoError(t, err)
|
|
defer code.Close(testCtx)
|
|
|
|
// Instantiate the module, which calls the start function. This will fail if the context wasn't as intended.
|
|
m, err := r.InstantiateModule(testCtx, code)
|
|
require.NoError(t, err)
|
|
defer m.Close(testCtx)
|
|
|
|
require.True(t, calledStart)
|
|
}
|
|
|
|
// TestInstantiateModuleFromCode_DoesntEnforce_Start ensures wapc-go work when modules import WASI, but don't export "_start".
|
|
func TestInstantiateModuleFromCode_DoesntEnforce_Start(t *testing.T) {
|
|
r := NewRuntime()
|
|
|
|
mod, err := r.InstantiateModuleFromCode(testCtx, []byte(`(module $wasi_test.go
|
|
(memory 1)
|
|
(export "memory" (memory 0))
|
|
)`))
|
|
require.NoError(t, err)
|
|
require.NoError(t, mod.Close(testCtx))
|
|
}
|
|
|
|
func TestRuntime_InstantiateModuleFromCode_UsesContext(t *testing.T) {
|
|
r := NewRuntime()
|
|
|
|
// Define a function that will be re-exported as the WASI function: _start
|
|
var calledStart bool
|
|
start := func(ctx context.Context) {
|
|
calledStart = true
|
|
require.Equal(t, testCtx, ctx)
|
|
}
|
|
|
|
host, err := r.NewModuleBuilder("").
|
|
ExportFunction("start", start).
|
|
Instantiate(testCtx)
|
|
require.NoError(t, err)
|
|
defer host.Close(testCtx)
|
|
|
|
// Start the module as a WASI command. This will fail if the context wasn't as intended.
|
|
mod, err := r.InstantiateModuleFromCode(testCtx, []byte(`(module $start
|
|
(import "" "start" (func $start))
|
|
(memory 1)
|
|
(export "_start" (func $start))
|
|
(export "memory" (memory 0))
|
|
)`))
|
|
require.NoError(t, err)
|
|
defer mod.Close(testCtx)
|
|
|
|
require.True(t, calledStart)
|
|
}
|
|
|
|
// TestInstantiateModuleWithConfig_WithName tests that we can pre-validate (cache) a module and instantiate it under
|
|
// different names. This pattern is used in wapc-go.
|
|
func TestInstantiateModuleWithConfig_WithName(t *testing.T) {
|
|
r := NewRuntime()
|
|
base, err := r.CompileModule(testCtx, []byte(`(module $0 (memory 1))`))
|
|
require.NoError(t, err)
|
|
defer base.Close(testCtx)
|
|
|
|
require.Equal(t, "0", base.module.NameSection.ModuleName)
|
|
|
|
// Use the same runtime to instantiate multiple modules
|
|
internal := r.(*runtime).store
|
|
m1, err := r.InstantiateModuleWithConfig(testCtx, base, NewModuleConfig().WithName("1"))
|
|
require.NoError(t, err)
|
|
defer m1.Close(testCtx)
|
|
|
|
require.Nil(t, internal.Module("0"))
|
|
require.Equal(t, internal.Module("1"), m1)
|
|
|
|
m2, err := r.InstantiateModuleWithConfig(testCtx, base, NewModuleConfig().WithName("2"))
|
|
require.NoError(t, err)
|
|
defer m2.Close(testCtx)
|
|
|
|
require.Nil(t, internal.Module("0"))
|
|
require.Equal(t, internal.Module("2"), m2)
|
|
}
|
|
|
|
func TestInstantiateModuleWithConfig_ExitError(t *testing.T) {
|
|
r := NewRuntime()
|
|
|
|
start := func(ctx context.Context, m api.Module) {
|
|
require.NoError(t, m.CloseWithExitCode(ctx, 2))
|
|
}
|
|
|
|
_, err := r.NewModuleBuilder("env").ExportFunction("_start", start).Instantiate(testCtx)
|
|
|
|
// 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 context.Context) uint64, functionName string) ([]byte, func(context.Context) error) {
|
|
mod, err := r.NewModuleBuilder("host").ExportFunction(functionName, hostFn).Instantiate(testCtx)
|
|
require.NoError(t, err)
|
|
|
|
return []byte(fmt.Sprintf(
|
|
`(module (import "host" "%[1]s" (func (result i64))) (export "%[1]s" (func 0)))`, functionName,
|
|
)), mod.Close
|
|
}
|
|
|
|
type mockEngine struct {
|
|
name string
|
|
cachedModules map[*wasm.Module]struct{}
|
|
}
|
|
|
|
// NewModuleEngine implements the same method as documented on wasm.Engine.
|
|
func (e *mockEngine) NewModuleEngine(_ string, _ *wasm.Module, _, _ []*wasm.FunctionInstance, _ *wasm.TableInstance, _ map[wasm.Index]wasm.Index) (wasm.ModuleEngine, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
// DeleteCompiledModule implements the same method as documented on wasm.Engine.
|
|
func (e *mockEngine) DeleteCompiledModule(module *wasm.Module) {
|
|
delete(e.cachedModules, module)
|
|
}
|
|
|
|
func (e *mockEngine) CompileModule(_ context.Context, module *wasm.Module) error {
|
|
e.cachedModules[module] = struct{}{}
|
|
return nil
|
|
}
|