Files
wazero/runtime_test.go
Takeshi Yoneda 076d3245e3 Pass context into NewRuntime (#748)
Signed-off-by: Takeshi Yoneda <takeshi@tetrate.io>
2022-08-18 14:47:49 +09:00

551 lines
16 KiB
Go

package wazero
import (
"context"
_ "embed"
"errors"
"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"
binaryformat "github.com/tetratelabs/wazero/internal/wasm/binary"
"github.com/tetratelabs/wazero/sys"
)
var (
binaryNamedZero = binaryformat.EncodeModule(&wasm.Module{NameSection: &wasm.NameSection{ModuleName: "0"}})
// testCtx is an arbitrary, non-default context. Non-nil also prevents linter errors.
testCtx = context.WithValue(context.Background(), struct{}{}, "arbitrary")
)
func TestRuntime_CompileModule(t *testing.T) {
tests := []struct {
name string
runtime Runtime
wasm []byte
expectedName string
}{
{
name: "no name section",
wasm: binaryformat.EncodeModule(&wasm.Module{}),
},
{
name: "empty NameSection.ModuleName",
wasm: binaryformat.EncodeModule(&wasm.Module{NameSection: &wasm.NameSection{}}),
},
{
name: "NameSection.ModuleName",
wasm: binaryformat.EncodeModule(&wasm.Module{NameSection: &wasm.NameSection{ModuleName: "test"}}),
expectedName: "test",
},
}
r := NewRuntime(testCtx)
defer r.Close(testCtx)
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
m, err := r.CompileModule(testCtx, tc.wasm, NewCompileConfig())
require.NoError(t, err)
code := m.(*compiledModule)
if tc.expectedName != "" {
require.Equal(t, tc.expectedName, code.module.NameSection.ModuleName)
}
require.Equal(t, r.(*runtime).store.Engine, code.compiledEngine)
})
}
t.Run("WithMemorySizer", func(t *testing.T) {
testWasm := binaryformat.EncodeModule(&wasm.Module{MemorySection: &wasm.Memory{Min: 1}})
m, err := r.CompileModule(testCtx, testWasm, NewCompileConfig().
WithMemorySizer(func(minPages uint32, maxPages *uint32) (min, capacity, max uint32) {
return 1, 2, 3
}))
require.NoError(t, err)
code := m.(*compiledModule)
require.Equal(t, &wasm.Memory{
Min: 1,
Cap: 2,
Max: 3,
}, code.module.MemorySection)
})
}
func TestRuntime_CompileModule_Errors(t *testing.T) {
tests := []struct {
name string
config CompileConfig
wasm []byte
expectedErr string
}{
{
name: "nil",
expectedErr: "binary == nil",
},
{
name: "invalid binary",
wasm: append(binaryformat.Magic, []byte("yolo")...),
expectedErr: "invalid version header",
},
{
name: "memory cap < min", // only one test to avoid duplicating tests in module_test.go
config: NewCompileConfig().WithMemorySizer(func(minPages uint32, maxPages *uint32) (min, capacity, max uint32) {
return 3, 1, 3
}),
wasm: binaryformat.EncodeModule(&wasm.Module{
MemorySection: &wasm.Memory{Min: 3},
}),
expectedErr: "section memory: 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
config: NewCompileConfig().WithMemorySizer(func(minPages uint32, maxPages *uint32) (min, capacity, max uint32) {
return 3, 2, 3
}),
wasm: binaryformat.EncodeModule(&wasm.Module{
MemorySection: &wasm.Memory{},
ExportSection: []*wasm.Export{
{Name: "memory", Type: api.ExternTypeMemory},
},
}),
expectedErr: "section memory: capacity 2 pages (128 Ki) less than minimum 3 pages (192 Ki)",
},
{
name: "memory has too many pages",
wasm: binaryformat.EncodeModule(&wasm.Module{MemorySection: &wasm.Memory{Min: 2, Cap: 2, Max: 70000, IsMaxEncoded: true}}),
expectedErr: "section memory: max 70000 pages (4 Gi) over limit of 65536 pages (4 Gi)",
},
}
r := NewRuntime(testCtx)
defer r.Close(testCtx)
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
config := tc.config
if config == nil {
config = NewCompileConfig()
}
_, err := r.CompileModule(testCtx, tc.wasm, config)
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
t.Run(tc.name, func(t *testing.T) {
r := NewRuntime(testCtx)
defer r.Close(testCtx)
// Instantiate the module and get the export of the above memory
module, err := tc.builder(r).Instantiate(testCtx, r)
require.NoError(t, err)
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
t.Run(tc.name, func(t *testing.T) {
r := NewRuntime(testCtx).(*runtime)
defer r.Close(testCtx)
var m CompiledModule
if tc.module != nil {
m = &compiledModule{module: tc.module}
} else {
m, _ = tc.builder(r).Compile(testCtx, NewCompileConfig())
}
code := m.(*compiledModule)
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, NewModuleConfig())
require.NoError(t, err)
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 TestRuntime_InstantiateModule_UsesContext(t *testing.T) {
r := NewRuntime(testCtx)
defer r.Close(testCtx)
// 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)
}
_, err := r.NewModuleBuilder("env").
ExportFunction("start", start).
Instantiate(testCtx, r)
require.NoError(t, err)
one := uint32(1)
binary := binaryformat.EncodeModule(&wasm.Module{
TypeSection: []*wasm.FunctionType{{}},
ImportSection: []*wasm.Import{{Module: "env", Name: "start", 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,
})
code, err := r.CompileModule(testCtx, binary, NewCompileConfig())
require.NoError(t, err)
// Instantiate the module, which calls the start function. This will fail if the context wasn't as intended.
mod, err := r.InstantiateModule(testCtx, code, NewModuleConfig())
require.NoError(t, err)
require.True(t, calledStart)
// Closing the module shouldn't remove the compiler cache
require.NoError(t, mod.Close(testCtx))
require.Equal(t, uint32(2), r.(*runtime).store.Engine.CompiledModuleCount())
}
// TestRuntime_InstantiateModuleFromBinary_DoesntEnforce_Start ensures wapc-go work when modules import WASI, but don't
// export "_start".
func TestRuntime_InstantiateModuleFromBinary_DoesntEnforce_Start(t *testing.T) {
r := NewRuntime(testCtx)
defer r.Close(testCtx)
binary := binaryformat.EncodeModule(&wasm.Module{
MemorySection: &wasm.Memory{Min: 1},
ExportSection: []*wasm.Export{{Name: "memory", Type: wasm.ExternTypeMemory, Index: 0}},
})
mod, err := r.InstantiateModuleFromBinary(testCtx, binary)
require.NoError(t, err)
require.NoError(t, mod.Close(testCtx))
}
func TestRuntime_InstantiateModuleFromBinary_ErrorOnStart(t *testing.T) {
tests := []struct {
name, wasm string
}{
{
name: "_start function",
wasm: `(module
(import "" "start" (func $start))
(export "_start" (func $start))
)`,
},
{
name: ".start function",
wasm: `(module
(import "" "start" (func $start))
(start $start)
)`,
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
r := NewRuntime(testCtx)
defer r.Close(testCtx)
start := func(context.Context) {
panic(errors.New("ice cream"))
}
host, err := r.NewModuleBuilder("").
ExportFunction("start", start).
Instantiate(testCtx, r)
require.NoError(t, err)
// Start the module as a WASI command. We expect it to fail.
_, err = r.InstantiateModuleFromBinary(testCtx, []byte(tc.wasm))
require.Error(t, err)
// Close the imported module, which should remove its compiler cache.
require.NoError(t, host.Close(testCtx))
// The compiler cache of the importing module should be removed on error.
require.Zero(t, r.(*runtime).store.Engine.CompiledModuleCount())
})
}
}
// TestRuntime_InstantiateModule_WithName tests that we can pre-validate (cache) a module and instantiate it under
// different names. This pattern is used in wapc-go.
func TestRuntime_InstantiateModule_WithName(t *testing.T) {
r := NewRuntime(testCtx)
defer r.Close(testCtx)
base, err := r.CompileModule(testCtx, binaryNamedZero, NewCompileConfig())
require.NoError(t, err)
require.Equal(t, "0", base.(*compiledModule).module.NameSection.ModuleName)
// Use the same runtime to instantiate multiple modules
internal := r.(*runtime).ns
m1, err := r.InstantiateModule(testCtx, base, NewModuleConfig().WithName("1"))
require.NoError(t, err)
require.Nil(t, internal.Module("0"))
require.Equal(t, internal.Module("1"), m1)
m2, err := r.InstantiateModule(testCtx, base, NewModuleConfig().WithName("2"))
require.NoError(t, err)
require.Nil(t, internal.Module("0"))
require.Equal(t, internal.Module("2"), m2)
}
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))
}
_, err := r.NewModuleBuilder("env").ExportFunction("exit", start).Instantiate(testCtx, r)
require.NoError(t, err)
one := uint32(1)
binary := binaryformat.EncodeModule(&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,
})
code, err := r.CompileModule(testCtx, binary, NewCompileConfig())
require.NoError(t, err)
// Instantiate the module, which calls the start function.
_, err = r.InstantiateModule(testCtx, code, NewModuleConfig().WithName("call-exit"))
// Ensure the exit error propagated and didn't wrap.
require.Equal(t, err, sys.NewExitError("call-exit", 2))
}
func TestRuntime_CloseWithExitCode(t *testing.T) {
bin := binaryformat.EncodeModule(&wasm.Module{
TypeSection: []*wasm.FunctionType{{}},
FunctionSection: []wasm.Index{0},
CodeSection: []*wasm.Code{{Body: []byte{wasm.OpcodeEnd}}},
ExportSection: []*wasm.Export{{Type: wasm.ExternTypeFunc, Index: 0, Name: "func"}},
})
tests := []struct {
name string
exitCode uint32
}{
{
name: "exit code 0",
exitCode: uint32(0),
},
{
name: "exit code 2",
exitCode: uint32(2),
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
r := NewRuntime(testCtx)
code, err := r.CompileModule(testCtx, bin, NewCompileConfig())
require.NoError(t, err)
// Instantiate two modules.
m1, err := r.InstantiateModule(testCtx, code, NewModuleConfig().WithName("mod1"))
require.NoError(t, err)
m2, err := r.InstantiateModule(testCtx, code, NewModuleConfig().WithName("mod2"))
require.NoError(t, err)
func1 := m1.ExportedFunction("func")
func2 := m2.ExportedFunction("func")
// Modules not closed so calls succeed
_, err = func1.Call(testCtx)
require.NoError(t, err)
_, err = func2.Call(testCtx)
require.NoError(t, err)
if tc.exitCode == 0 {
err = r.Close(testCtx)
} else {
err = r.CloseWithExitCode(testCtx, tc.exitCode)
}
require.NoError(t, err)
// Modules closed so calls fail
_, err = func1.Call(testCtx)
require.ErrorIs(t, err, sys.NewExitError("mod1", tc.exitCode))
_, err = func2.Call(testCtx)
require.ErrorIs(t, err, sys.NewExitError("mod2", tc.exitCode))
})
}
}
func TestRuntime_Close_ClosesCompiledModules(t *testing.T) {
engine := &mockEngine{name: "mock", cachedModules: map[*wasm.Module]struct{}{}}
conf := *engineLessConfig
conf.newEngine = func(context.Context, wasm.Features) wasm.Engine {
return engine
}
r := NewRuntimeWithConfig(testCtx, &conf)
defer r.Close(testCtx)
// Normally compiled modules are closed when instantiated but this is never instantiated.
_, err := r.CompileModule(testCtx, binaryNamedZero, NewCompileConfig())
require.NoError(t, err)
require.Equal(t, uint32(1), engine.CompiledModuleCount())
err = r.Close(testCtx)
require.NoError(t, err)
// Closing the runtime should remove the compiler cache
require.Zero(t, engine.CompiledModuleCount())
}
type mockEngine struct {
name string
cachedModules map[*wasm.Module]struct{}
}
// CompileModule implements the same method as documented on wasm.Engine.
func (e *mockEngine) CompileModule(_ context.Context, module *wasm.Module) error {
e.cachedModules[module] = struct{}{}
return nil
}
// CompiledModuleCount implements the same method as documented on wasm.Engine.
func (e *mockEngine) CompiledModuleCount() uint32 {
return uint32(len(e.cachedModules))
}
// DeleteCompiledModule implements the same method as documented on wasm.Engine.
func (e *mockEngine) DeleteCompiledModule(module *wasm.Module) {
delete(e.cachedModules, module)
}
// NewModuleEngine implements the same method as documented on wasm.Engine.
func (e *mockEngine) NewModuleEngine(_ string, _ *wasm.Module, _, _ []*wasm.FunctionInstance, _ []*wasm.TableInstance, _ []wasm.TableInitEntry) (wasm.ModuleEngine, error) {
return nil, nil
}