Files
wazero/wasm_test.go
Crypt Keeper 8f461f6f12 Makes memory limit configurable and a compile error (#419)
This allows users to reduce the memory limit per module below 4 Gi. This
is often needed because Wasm routinely leaves off the max, which implies
spec max (4 Gi). This uses Ki Gi etc in error messages because the spec
chooses to, though we can change to make it less awkward.

This also fixes an issue where we instantiated an engine inside config.

Signed-off-by: Adrian Cole <adrian@tetrate.io>
2022-03-31 08:57:28 +08:00

353 lines
9.7 KiB
Go

package wazero
import (
"context"
"fmt"
"math"
"testing"
"github.com/stretchr/testify/require"
internalwasm "github.com/tetratelabs/wazero/internal/wasm"
"github.com/tetratelabs/wazero/internal/wasm/binary"
"github.com/tetratelabs/wazero/wasm"
)
func TestRuntime_DecodeModule(t *testing.T) {
tests := []struct {
name string
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(&internalwasm.Module{}),
},
{
name: "binary - empty NameSection.ModuleName",
source: binary.EncodeModule(&internalwasm.Module{NameSection: &internalwasm.NameSection{}}),
},
{
name: "binary - NameSection.ModuleName",
source: binary.EncodeModule(&internalwasm.Module{NameSection: &internalwasm.NameSection{ModuleName: "test"}}),
expectedName: "test",
},
}
r := NewRuntime()
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
decoded, err := r.CompileModule(tc.source)
require.NoError(t, err)
require.Equal(t, tc.expectedName, decoded.name)
})
}
}
func TestRuntime_DecodeModule_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.memoryMaxPage too large",
runtime: NewRuntimeWithConfig(NewRuntimeConfig().WithMemoryMaxPages(math.MaxUint32)),
source: []byte(`(module)`),
expectedErr: "memoryMaxPages 4294967295 (3 Ti) > specification max 65536 (4 Gi)",
},
{
name: "memory has too many pages - text",
runtime: NewRuntimeWithConfig(NewRuntimeConfig().WithMemoryMaxPages(2)),
source: []byte(`(module (memory 3))`),
expectedErr: "1:17: min 3 pages (192 Ki) outside range of 2 pages (128 Ki) in module.memory[0]",
},
{
name: "memory has too many pages - binary",
runtime: NewRuntimeWithConfig(NewRuntimeConfig().WithMemoryMaxPages(2)),
source: binary.EncodeModule(&internalwasm.Module{MemorySection: &internalwasm.Memory{Min: 2, Max: 3}}),
expectedErr: "section memory: max 3 pages (192 Ki) outside range 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(tc.source)
require.EqualError(t, err, tc.expectedErr)
})
}
}
// TestDecodedModule_WithName tests that we can pre-validate (cache) a module and instantiate it under different
// names. This pattern is used in wapc-go.
func TestDecodedModule_WithName(t *testing.T) {
r := NewRuntime()
base, err := r.CompileModule([]byte(`(module $0 (memory 1))`))
require.NoError(t, err)
require.Equal(t, "0", base.name)
// Use the same runtime to instantiate multiple modules
internal := r.(*runtime).store
m1, err := r.InstantiateModule(base.WithName("1"))
require.NoError(t, err)
require.Nil(t, internal.Module("0"))
require.Equal(t, internal.Module("1"), m1)
m2, err := r.InstantiateModule(base.WithName("2"))
require.NoError(t, err)
require.Nil(t, internal.Module("0"))
require.Equal(t, internal.Module("2"), m2)
}
// 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, wat string
expected bool
expectedLen uint32
}{
{
name: "no memory",
wat: `(module)`,
},
{
name: "memory exported, one page",
wat: `(module (memory $mem 1) (export "memory" (memory $mem)))`,
expected: true,
expectedLen: 65536,
},
}
for _, tt := range tests {
tc := tt
r := NewRuntime()
t.Run(tc.name, func(t *testing.T) {
decoded, err := r.CompileModule([]byte(tc.wat))
require.NoError(t, err)
// Instantiate the module and get the export of the above hostFn
module, err := r.InstantiateModule(decoded)
require.NoError(t, err)
mem := module.ExportedMemory("memory")
if tc.expected {
require.Equal(t, tc.expectedLen, mem.Size())
} 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) {
tests := []struct {
name string
module *internalwasm.Module // module as wat doesn't yet support globals
expected, expectedMutable bool
}{
{
name: "no global",
module: &internalwasm.Module{},
},
{
name: "global not exported",
module: &internalwasm.Module{
GlobalSection: []*internalwasm.Global{
{
Type: &internalwasm.GlobalType{ValType: internalwasm.ValueTypeI64, Mutable: true},
Init: &internalwasm.ConstantExpression{Opcode: internalwasm.OpcodeI64Const, Data: []byte{1}},
},
},
},
},
{
name: "global exported",
module: &internalwasm.Module{
GlobalSection: []*internalwasm.Global{
{
Type: &internalwasm.GlobalType{ValType: internalwasm.ValueTypeI64},
Init: &internalwasm.ConstantExpression{Opcode: internalwasm.OpcodeI64Const, Data: []byte{1}},
},
},
ExportSection: map[string]*internalwasm.Export{
"global": {Type: internalwasm.ExternTypeGlobal, Name: "global"},
},
},
expected: true,
},
{
name: "global exported and mutable",
module: &internalwasm.Module{
GlobalSection: []*internalwasm.Global{
{
Type: &internalwasm.GlobalType{ValType: internalwasm.ValueTypeI64, Mutable: true},
Init: &internalwasm.ConstantExpression{Opcode: internalwasm.OpcodeI64Const, Data: []byte{1}},
},
},
ExportSection: map[string]*internalwasm.Export{
"global": {Type: internalwasm.ExternTypeGlobal, Name: "global"},
},
},
expected: true,
expectedMutable: true,
},
}
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 global
module, err := r.InstantiateModule(&Module{module: tc.module})
require.NoError(t, err)
global := module.ExportedGlobal("global")
if !tc.expected {
require.Nil(t, global)
return
}
require.Equal(t, uint64(1), global.Get())
mutable, ok := global.(wasm.MutableGlobal)
require.Equal(t, tc.expectedMutable, ok)
if ok {
mutable.Set(2)
require.Equal(t, uint64(2), global.Get())
}
})
}
}
func TestFunction_Context(t *testing.T) {
type key string
runtimeCtx := context.WithValue(context.Background(), key("wa"), "zero")
config := NewRuntimeConfig().WithContext(runtimeCtx)
notStoreCtx := context.WithValue(context.Background(), key("wazer"), "o")
tests := []struct {
name string
ctx context.Context
expected context.Context
}{
{
name: "nil defaults to runtime context",
ctx: nil,
expected: runtimeCtx,
},
{
name: "set overrides runtime context",
ctx: notStoreCtx,
expected: notStoreCtx,
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
r := NewRuntimeWithConfig(config)
// 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 wasm.Module) uint64 {
require.Equal(t, tc.expected, ctx.Context())
return expectedResult
}
source := requireImportAndExportFunction(t, r, hostFn, functionName)
// Instantiate the module and get the export of the above hostFn
decoded, err := r.CompileModule(source)
require.NoError(t, err)
module, err := r.InstantiateModule(decoded)
require.NoError(t, err)
// This fails if the function wasn't invoked, or had an unexpected context.
results, err := module.ExportedFunction(functionName).Call(module.WithContext(tc.ctx))
require.NoError(t, err)
require.Equal(t, expectedResult, results[0])
})
}
}
func TestRuntime_NewModule_UsesStoreContext(t *testing.T) {
type key string
runtimeCtx := context.WithValue(context.Background(), key("wa"), "zero")
config := NewRuntimeConfig().WithContext(runtimeCtx)
r := NewRuntimeWithConfig(config)
// Define a function that will be set as the start function
var calledStart bool
start := func(ctx wasm.Module) {
calledStart = true
require.Equal(t, runtimeCtx, ctx.Context())
}
_, err := r.NewModuleBuilder("env").ExportFunction("start", start).Instantiate()
require.NoError(t, err)
decoded, err := r.CompileModule([]byte(`(module $runtime_test.go
(import "env" "start" (func $start))
(start $start)
)`))
require.NoError(t, err)
// Instantiate the module, which calls the start function. This will fail if the context wasn't as intended.
_, err = r.InstantiateModule(decoded)
require.NoError(t, err)
require.True(t, calledStart)
}
// 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 wasm.Module) uint64, functionName string) []byte {
_, err := r.NewModuleBuilder("host").ExportFunction(functionName, hostFn).Instantiate()
require.NoError(t, err)
return []byte(fmt.Sprintf(
`(module (import "host" "%[1]s" (func (result i64))) (export "%[1]s" (func 0)))`, functionName,
))
}