Files
wazero/wasm_test.go
Adrian Cole 76c0bfc33f Combines Store+Engine into Runtime
This simplifies state management and the amount of terminology end-users
need to learn by using one concept `Runtime` instead of two: `Engine`
and `Store`. This bridges the concepts to the specification by still
having `wazero.Runtime` implement `wasm.Store`.

The net result is that we can know for sure which "engine" is used when
decoding. This allows us a lot of flexibility especially pre-compilation
when JIT is possible.

This also changes the default to JIT based on compiler flags so that
downstream projects like wapc-go don't have to do this individually
(including tracking of which OS+Arch have JIT).

Signed-off-by: Adrian Cole <adrian@tetrate.io>
2022-03-02 12:29:13 +08:00

251 lines
6.6 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.DecodeModule(tc.source)
require.NoError(t, err)
require.Equal(t, tc.expectedName, decoded.name)
})
}
}
func TestRuntime_DecodeModule_Errors(t *testing.T) {
tests := []struct {
name string
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",
},
}
r := NewRuntime()
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
_, err := r.DecodeModule(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.DecodeModule([]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.NewModule(base.WithName("1"))
require.NoError(t, err)
require.Nil(t, internal.ModuleContexts["0"])
require.Equal(t, internal.ModuleContexts["1"], m1.(*internalwasm.PublicModule).Context)
m2, err := r.NewModule(base.WithName("2"))
require.NoError(t, err)
require.Nil(t, internal.ModuleContexts["0"])
require.Equal(t, internal.ModuleContexts["2"], m2.(*internalwasm.PublicModule).Context)
}
// TestModuleExports_Memory only covers a couple cases to avoid duplication of internal/wasm/runtime_test.go
func TestModuleExports_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.DecodeModule([]byte(tc.wat))
require.NoError(t, err)
// Instantiate the module and get the export of the above hostFn
module, err := r.NewModule(decoded)
require.NoError(t, err)
mem := module.Memory("memory")
if tc.expected {
require.Equal(t, tc.expectedLen, mem.Size())
} else {
require.Nil(t, mem)
}
})
}
}
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.ModuleContext) 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.DecodeModule(source)
require.NoError(t, err)
module, err := r.NewModule(decoded)
require.NoError(t, err)
// This fails if the function wasn't invoked, or had an unexpected context.
results, err := module.Function(functionName).Call(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.ModuleContext) {
calledStart = true
require.Equal(t, runtimeCtx, ctx.Context())
}
_, err := r.NewHostModule(&HostModuleConfig{Functions: map[string]interface{}{"start": start}})
require.NoError(t, err)
decoded, err := r.DecodeModule([]byte(`(module $runtime_test.go
(import "" "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.NewModule(decoded)
require.NoError(t, err)
require.True(t, calledStart)
}
// requireImportAndExportFunction re-module a host function because only host functions can see the propagated context.
func requireImportAndExportFunction(t *testing.T, r Runtime, hostFn func(ctx wasm.ModuleContext) uint64, functionName string) []byte {
_, err := r.NewHostModule(&HostModuleConfig{
Name: "host", Functions: map[string]interface{}{functionName: hostFn},
})
require.NoError(t, err)
return []byte(fmt.Sprintf(
`(module (import "host" "%[1]s" (func (result i64))) (export "%[1]s" (func 0)))`, functionName,
))
}