332 lines
8.8 KiB
Go
332 lines
8.8 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.Module("0"))
|
|
require.Equal(t, internal.Module("1"), m1.(*internalwasm.PublicModule))
|
|
|
|
m2, err := r.NewModule(base.WithName("2"))
|
|
require.NoError(t, err)
|
|
require.Nil(t, internal.Module("0"))
|
|
require.Equal(t, internal.Module("2"), m2.(*internalwasm.PublicModule))
|
|
}
|
|
|
|
// 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.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)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// 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.NewModule(&DecodedModule{module: tc.module})
|
|
require.NoError(t, err)
|
|
|
|
global := module.Global("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.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,
|
|
))
|
|
}
|