Files
wazero/internal/wasm/store_test.go
Takeshi Yoneda b63d4e6dcd Deletes namespace API (#1018)
Formerly, we introduced `wazero.Namespace` to help avoid module name or import conflicts while still sharing the runtime's compilation cache. Now that we've introduced `CompilationCache` `wazero.Namespace` is no longer necessary. By removing it, we reduce the conceptual load on end users as well internal complexity. Since most users don't use namespace, the change isn't very impactful.

Users who are only trying to avoid module name conflict can generate a name like below instead of using multiple runtimes:

```go
moduleName := fmt.Sprintf("%d", atomic.AddUint64(&m.instanceCounter, 1))
module, err := runtime.InstantiateModule(ctx, compiled, config.WithName(moduleName))
```

For `HostModuleBuilder` users, we no longer take `Namespace` as the last parameter of `Instantiate` method: 

```diff
 	// log to the console.
 	_, err := r.NewHostModuleBuilder("env").
 		NewFunctionBuilder().WithFunc(logString).Export("log").
-		Instantiate(ctx, r)
+		Instantiate(ctx)
 	if err != nil {
 		log.Panicln(err)
 	}
```


The following is an example diff a use of namespace can use to keep compilation cache while also ensuring their modules don't conflict:

```diff

 func useMultipleRuntimes(ctx context.Context, cache) {
-	r := wazero.NewRuntime(ctx)
+	cache := wazero.NewCompilationCache()
 
 	for i := 0; i < N; i++ {
-		// Create a new namespace to instantiate modules into.
-		ns := r.NewNamespace(ctx) // Note: this is closed when the Runtime is
+		r := wazero.NewRuntimeWithConfig(ctx, wazero.NewRuntimeConfig().WithCompilationCache(cache))
 
 		// Instantiate a new "env" module which exports a stateful function.
 		_, err := r.NewHostModuleBuilder("env").
```

Signed-off-by: Takeshi Yoneda <takeshi@tetrate.io>
2023-01-10 14:11:46 +09:00

913 lines
29 KiB
Go

package wasm
import (
"context"
"errors"
"fmt"
"math"
"strconv"
"testing"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/experimental"
"github.com/tetratelabs/wazero/internal/leb128"
"github.com/tetratelabs/wazero/internal/sys"
"github.com/tetratelabs/wazero/internal/testing/hammer"
"github.com/tetratelabs/wazero/internal/testing/require"
"github.com/tetratelabs/wazero/internal/u64"
)
func TestModuleInstance_Memory(t *testing.T) {
tests := []struct {
name string
input *Module
expected bool
expectedLen uint32
}{
{
name: "no memory",
input: &Module{},
},
{
name: "memory not exported, one page",
input: &Module{
MemorySection: &Memory{Min: 1, Cap: 1},
MemoryDefinitionSection: []*MemoryDefinition{{}},
},
},
{
name: "memory exported, different name",
input: &Module{
MemorySection: &Memory{Min: 1, Cap: 1},
MemoryDefinitionSection: []*MemoryDefinition{{}},
ExportSection: []*Export{{Type: ExternTypeMemory, Name: "momory", Index: 0}},
},
},
{
name: "memory exported, but zero length",
input: &Module{
MemorySection: &Memory{},
MemoryDefinitionSection: []*MemoryDefinition{{}},
ExportSection: []*Export{{Type: ExternTypeMemory, Name: "memory", Index: 0}},
},
expected: true,
},
{
name: "memory exported, one page",
input: &Module{
MemorySection: &Memory{Min: 1, Cap: 1},
MemoryDefinitionSection: []*MemoryDefinition{{}},
ExportSection: []*Export{{Type: ExternTypeMemory, Name: "memory", Index: 0}},
},
expected: true,
expectedLen: 65536,
},
{
name: "memory exported, two pages",
input: &Module{
MemorySection: &Memory{Min: 2, Cap: 2},
MemoryDefinitionSection: []*MemoryDefinition{{}},
ExportSection: []*Export{{Type: ExternTypeMemory, Name: "memory", Index: 0}},
},
expected: true,
expectedLen: 65536 * 2,
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
s := newStore()
instance, err := s.Instantiate(testCtx, tc.input, "test", nil)
require.NoError(t, err)
mem := instance.ExportedMemory("memory")
if tc.expected {
require.Equal(t, tc.expectedLen, mem.Size())
} else {
require.Nil(t, mem)
}
})
}
}
func TestNewStore(t *testing.T) {
s := NewStore(api.CoreFeaturesV1, &mockEngine{shouldCompileFail: false, callFailIndex: -1})
// Ensures that a newly created store has the pre allocated type IDs.
for k, v := range preAllocatedTypeIDs {
actual, ok := s.typeIDs[k]
require.True(t, ok)
require.Equal(t, v, actual)
}
}
func TestStore_Instantiate(t *testing.T) {
s := newStore()
m, err := NewHostModule("", map[string]interface{}{"fn": func() {}}, map[string]*HostFuncNames{"fn": {}}, api.CoreFeaturesV1)
require.NoError(t, err)
sysCtx := sys.DefaultContext(nil)
mod, err := s.Instantiate(testCtx, m, "", sysCtx)
require.NoError(t, err)
defer mod.Close(testCtx)
t.Run("CallContext defaults", func(t *testing.T) {
require.Equal(t, s.nameToNode[""].module, mod.module)
require.Equal(t, s.nameToNode[""].module.Memory, mod.memory)
require.Equal(t, s, mod.s)
require.Equal(t, sysCtx, mod.Sys)
})
}
func TestStore_CloseWithExitCode(t *testing.T) {
const importedModuleName = "imported"
const importingModuleName = "test"
tests := []struct {
name string
testClosed bool
}{
{
name: "nothing closed",
testClosed: false,
},
{
name: "partially closed",
testClosed: true,
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
s := newStore()
_, err := s.Instantiate(testCtx, &Module{
TypeSection: []*FunctionType{v_v},
FunctionSection: []uint32{0},
CodeSection: []*Code{{Body: []byte{OpcodeEnd}}},
ExportSection: []*Export{{Type: ExternTypeFunc, Index: 0, Name: "fn"}},
FunctionDefinitionSection: []*FunctionDefinition{{funcType: v_v}},
}, importedModuleName, nil)
require.NoError(t, err)
m2, err := s.Instantiate(testCtx, &Module{
TypeSection: []*FunctionType{v_v},
ImportSection: []*Import{{Type: ExternTypeFunc, Module: importedModuleName, Name: "fn", DescFunc: 0}},
MemorySection: &Memory{Min: 1, Cap: 1},
MemoryDefinitionSection: []*MemoryDefinition{{}},
GlobalSection: []*Global{{Type: &GlobalType{}, Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: const1}}},
TableSection: []*Table{{Min: 10}},
}, importingModuleName, nil)
require.NoError(t, err)
if tc.testClosed {
err = m2.CloseWithExitCode(testCtx, 2)
require.NoError(t, err)
}
err = s.CloseWithExitCode(testCtx, 2)
require.NoError(t, err)
// If Store.CloseWithExitCode was dispatched properly, modules should be empty
require.Nil(t, s.moduleList)
// Store state zeroed
require.Zero(t, len(s.typeIDs))
})
}
}
func TestStore_hammer(t *testing.T) {
const importedModuleName = "imported"
m, err := NewHostModule(importedModuleName, map[string]interface{}{"fn": func() {}}, map[string]*HostFuncNames{"fn": {}}, api.CoreFeaturesV1)
require.NoError(t, err)
s := newStore()
imported, err := s.Instantiate(testCtx, m, importedModuleName, nil)
require.NoError(t, err)
_, ok := s.nameToNode[imported.Name()]
require.True(t, ok)
importingModule := &Module{
TypeSection: []*FunctionType{v_v},
FunctionSection: []uint32{0},
CodeSection: []*Code{{Body: []byte{OpcodeEnd}}},
MemorySection: &Memory{Min: 1, Cap: 1},
MemoryDefinitionSection: []*MemoryDefinition{{}},
GlobalSection: []*Global{{
Type: &GlobalType{ValType: ValueTypeI32},
Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: leb128.EncodeInt32(1)},
}},
TableSection: []*Table{{Min: 10}},
ImportSection: []*Import{
{Type: ExternTypeFunc, Module: importedModuleName, Name: "fn", DescFunc: 0},
},
}
importingModule.BuildFunctionDefinitions()
// Concurrent instantiate, close should test if locks work on the ns. If they don't, we should see leaked modules
// after all of these complete, or an error raised.
P := 8 // max count of goroutines
N := 1000 // work per goroutine
if testing.Short() { // Adjust down if `-test.short`
P = 4
N = 100
}
hammer.NewHammer(t, P, N).Run(func(name string) {
mod, instantiateErr := s.Instantiate(testCtx, importingModule, name, sys.DefaultContext(nil))
require.NoError(t, instantiateErr)
require.NoError(t, mod.Close(testCtx))
}, nil)
if t.Failed() {
return // At least one test failed, so return now.
}
// Close the imported module.
require.NoError(t, imported.Close(testCtx))
// All instances are freed.
require.Nil(t, s.moduleList)
}
func TestStore_Instantiate_Errors(t *testing.T) {
const importedModuleName = "imported"
const importingModuleName = "test"
m, err := NewHostModule(importedModuleName, map[string]interface{}{"fn": func() {}}, map[string]*HostFuncNames{"fn": {}}, api.CoreFeaturesV1)
require.NoError(t, err)
t.Run("Fails if module name already in use", func(t *testing.T) {
s := newStore()
_, err = s.Instantiate(testCtx, m, importedModuleName, nil)
require.NoError(t, err)
// Trying to register it again should fail
_, err = s.Instantiate(testCtx, m, importedModuleName, nil)
require.EqualError(t, err, "module[imported] has already been instantiated")
})
t.Run("fail resolve import", func(t *testing.T) {
s := newStore()
_, err = s.Instantiate(testCtx, m, importedModuleName, nil)
require.NoError(t, err)
hm := s.nameToNode[importedModuleName]
require.NotNil(t, hm)
_, err = s.Instantiate(testCtx, &Module{
TypeSection: []*FunctionType{v_v},
ImportSection: []*Import{
// The first import resolve succeeds -> increment hm.dependentCount.
{Type: ExternTypeFunc, Module: importedModuleName, Name: "fn", DescFunc: 0},
// But the second one tries to import uninitialized-module ->
{Type: ExternTypeFunc, Module: "non-exist", Name: "fn", DescFunc: 0},
},
}, importingModuleName, nil)
require.EqualError(t, err, "module[non-exist] not instantiated")
})
t.Run("creating engine failed", func(t *testing.T) {
s := newStore()
_, err = s.Instantiate(testCtx, m, importedModuleName, nil)
require.NoError(t, err)
hm := s.nameToNode[importedModuleName]
require.NotNil(t, hm)
engine := s.Engine.(*mockEngine)
engine.shouldCompileFail = true
importingModule := &Module{
TypeSection: []*FunctionType{v_v},
FunctionSection: []uint32{0, 0},
CodeSection: []*Code{
{Body: []byte{OpcodeEnd}},
{Body: []byte{OpcodeEnd}},
},
ImportSection: []*Import{
{Type: ExternTypeFunc, Module: importedModuleName, Name: "fn", DescFunc: 0},
},
}
importingModule.BuildFunctionDefinitions()
_, err = s.Instantiate(testCtx, importingModule, importingModuleName, nil)
require.EqualError(t, err, "some engine creation error")
})
t.Run("start func failed", func(t *testing.T) {
s := newStore()
engine := s.Engine.(*mockEngine)
engine.callFailIndex = 1
_, err = s.Instantiate(testCtx, m, importedModuleName, nil)
require.NoError(t, err)
hm := s.nameToNode[importedModuleName]
require.NotNil(t, hm)
startFuncIndex := uint32(1)
importingModule := &Module{
TypeSection: []*FunctionType{v_v},
FunctionSection: []uint32{0},
CodeSection: []*Code{{Body: []byte{OpcodeEnd}}},
StartSection: &startFuncIndex,
ImportSection: []*Import{
{Type: ExternTypeFunc, Module: importedModuleName, Name: "fn", DescFunc: 0},
},
}
importingModule.BuildFunctionDefinitions()
_, err = s.Instantiate(testCtx, importingModule, importingModuleName, nil)
require.EqualError(t, err, "start function[1] failed: call failed")
})
}
type mockEngine struct {
shouldCompileFail bool
callFailIndex int
}
type mockModuleEngine struct {
name string
callFailIndex int
functionRefs map[Index]Reference
}
type mockCallEngine struct {
f *FunctionInstance
callFailIndex int
}
func newStore() *Store {
return NewStore(api.CoreFeaturesV1, &mockEngine{shouldCompileFail: false, callFailIndex: -1})
}
// CompileModule implements the same method as documented on wasm.Engine.
func (e *mockEngine) Close() error {
return nil
}
// CompileModule implements the same method as documented on wasm.Engine.
func (e *mockEngine) CompileModule(context.Context, *Module, []experimental.FunctionListener) error {
return nil
}
// LookupFunction implements the same method as documented on wasm.Engine.
func (e *mockModuleEngine) LookupFunction(*TableInstance, FunctionTypeID, Index) (Index, error) {
return 0, nil
}
// CompiledModuleCount implements the same method as documented on wasm.Engine.
func (e *mockEngine) CompiledModuleCount() uint32 { return 0 }
// DeleteCompiledModule implements the same method as documented on wasm.Engine.
func (e *mockEngine) DeleteCompiledModule(*Module) {}
// NewModuleEngine implements the same method as documented on wasm.Engine.
func (e *mockEngine) NewModuleEngine(_ string, _ *Module, _ []FunctionInstance) (ModuleEngine, error) {
if e.shouldCompileFail {
return nil, fmt.Errorf("some engine creation error")
}
return &mockModuleEngine{callFailIndex: e.callFailIndex}, nil
}
// FunctionInstanceReference implements the same method as documented on wasm.ModuleEngine.
func (e *mockModuleEngine) FunctionInstanceReference(i Index) Reference {
return e.functionRefs[i]
}
// NewCallEngine implements the same method as documented on wasm.ModuleEngine.
func (e *mockModuleEngine) NewCallEngine(callCtx *CallContext, f *FunctionInstance) (CallEngine, error) {
return &mockCallEngine{f: f, callFailIndex: e.callFailIndex}, nil
}
// CreateFuncElementInstance implements the same method as documented on wasm.ModuleEngine.
func (e *mockModuleEngine) CreateFuncElementInstance([]*Index) *ElementInstance {
return nil
}
// InitializeFuncrefGlobals implements the same method as documented on wasm.ModuleEngine.
func (e *mockModuleEngine) InitializeFuncrefGlobals(globals []*GlobalInstance) {}
// Name implements the same method as documented on wasm.ModuleEngine.
func (e *mockModuleEngine) Name() string {
return e.name
}
// Close implements the same method as documented on wasm.ModuleEngine.
func (e *mockModuleEngine) Close(context.Context) {
}
// Call implements the same method as documented on wasm.ModuleEngine.
func (ce *mockCallEngine) Call(ctx context.Context, callCtx *CallContext, _ []uint64) (results []uint64, err error) {
if ce.callFailIndex >= 0 && ce.f.Definition.Index() == Index(ce.callFailIndex) {
err = errors.New("call failed")
return
}
return
}
func TestStore_getFunctionTypeID(t *testing.T) {
t.Run("too many functions", func(t *testing.T) {
s := newStore()
const max = 10
s.functionMaxTypes = max
s.typeIDs = make(map[string]FunctionTypeID)
for i := 0; i < max; i++ {
s.typeIDs[strconv.Itoa(i)] = 0
}
_, err := s.getFunctionTypeID(&FunctionType{})
require.Error(t, err)
})
t.Run("ok", func(t *testing.T) {
tests := []*FunctionType{
{Params: []ValueType{}},
{Params: []ValueType{ValueTypeF32}},
{Results: []ValueType{ValueTypeF64}},
{Params: []ValueType{ValueTypeI32}, Results: []ValueType{ValueTypeI64}},
}
for _, tt := range tests {
tc := tt
t.Run(tc.String(), func(t *testing.T) {
s := newStore()
actual, err := s.getFunctionTypeID(tc)
require.NoError(t, err)
expectedTypeID, ok := s.typeIDs[tc.String()]
require.True(t, ok)
require.Equal(t, expectedTypeID, actual)
})
}
})
}
func TestExecuteConstExpression(t *testing.T) {
t.Run("basic type const expr", func(t *testing.T) {
for _, vt := range []ValueType{ValueTypeI32, ValueTypeI64, ValueTypeF32, ValueTypeF64} {
t.Run(ValueTypeName(vt), func(t *testing.T) {
expr := &ConstantExpression{}
switch vt {
case ValueTypeI32:
expr.Data = []byte{1}
expr.Opcode = OpcodeI32Const
case ValueTypeI64:
expr.Data = []byte{2}
expr.Opcode = OpcodeI64Const
case ValueTypeF32:
expr.Data = u64.LeBytes(api.EncodeF32(math.MaxFloat32))
expr.Opcode = OpcodeF32Const
case ValueTypeF64:
expr.Data = u64.LeBytes(api.EncodeF64(math.MaxFloat64))
expr.Opcode = OpcodeF64Const
}
raw := executeConstExpression(nil, expr)
require.NotNil(t, raw)
switch vt {
case ValueTypeI32:
actual, ok := raw.(int32)
require.True(t, ok)
require.Equal(t, int32(1), actual)
case ValueTypeI64:
actual, ok := raw.(int64)
require.True(t, ok)
require.Equal(t, int64(2), actual)
case ValueTypeF32:
actual, ok := raw.(float32)
require.True(t, ok)
require.Equal(t, float32(math.MaxFloat32), actual)
case ValueTypeF64:
actual, ok := raw.(float64)
require.True(t, ok)
require.Equal(t, float64(math.MaxFloat64), actual)
}
})
}
})
t.Run("reference types", func(t *testing.T) {
tests := []struct {
name string
expr *ConstantExpression
exp interface{}
}{
{
name: "ref.null (externref)",
expr: &ConstantExpression{
Opcode: OpcodeRefNull,
Data: []byte{RefTypeExternref},
},
exp: int64(0),
},
{
name: "ref.null (funcref)",
expr: &ConstantExpression{
Opcode: OpcodeRefNull,
Data: []byte{RefTypeFuncref},
},
exp: int64(0),
},
{
name: "ref.func",
expr: &ConstantExpression{
Opcode: OpcodeRefFunc,
Data: []byte{1},
},
exp: uint32(1),
},
{
name: "ref.func",
expr: &ConstantExpression{
Opcode: OpcodeRefFunc,
Data: []byte{0x5d},
},
exp: uint32(93),
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
val := executeConstExpression(nil, tc.expr)
require.Equal(t, tc.exp, val)
})
}
})
t.Run("global expr", func(t *testing.T) {
tests := []struct {
valueType ValueType
val, valHi uint64
}{
{valueType: ValueTypeI32, val: 10},
{valueType: ValueTypeI64, val: 20},
{valueType: ValueTypeF32, val: uint64(math.Float32bits(634634432.12311))},
{valueType: ValueTypeF64, val: math.Float64bits(1.12312311)},
{valueType: ValueTypeV128, val: 0x1, valHi: 0x2},
{valueType: ValueTypeExternref, val: 0x12345},
{valueType: ValueTypeFuncref, val: 0x54321},
}
for _, tt := range tests {
tc := tt
t.Run(ValueTypeName(tc.valueType), func(t *testing.T) {
// The index specified in Data equals zero.
expr := &ConstantExpression{Data: []byte{0}, Opcode: OpcodeGlobalGet}
globals := []*GlobalInstance{{Val: tc.val, ValHi: tc.valHi, Type: &GlobalType{ValType: tc.valueType}}}
val := executeConstExpression(globals, expr)
require.NotNil(t, val)
switch tc.valueType {
case ValueTypeI32:
actual, ok := val.(int32)
require.True(t, ok)
require.Equal(t, int32(tc.val), actual)
case ValueTypeI64:
actual, ok := val.(int64)
require.True(t, ok)
require.Equal(t, int64(tc.val), actual)
case ValueTypeF32:
actual, ok := val.(float32)
require.True(t, ok)
require.Equal(t, api.DecodeF32(tc.val), actual)
case ValueTypeF64:
actual, ok := val.(float64)
require.True(t, ok)
require.Equal(t, api.DecodeF64(tc.val), actual)
case ValueTypeV128:
vector, ok := val.([2]uint64)
require.True(t, ok)
require.Equal(t, uint64(0x1), vector[0])
require.Equal(t, uint64(0x2), vector[1])
case ValueTypeFuncref, ValueTypeExternref:
actual, ok := val.(int64)
require.True(t, ok)
require.Equal(t, int64(tc.val), actual)
}
})
}
})
t.Run("vector", func(t *testing.T) {
expr := &ConstantExpression{Data: []byte{
1, 0, 0, 0, 0, 0, 0, 0,
2, 0, 0, 0, 0, 0, 0, 0,
}, Opcode: OpcodeVecV128Const}
val := executeConstExpression(nil, expr)
require.NotNil(t, val)
vector, ok := val.([2]uint64)
require.True(t, ok)
require.Equal(t, uint64(0x1), vector[0])
require.Equal(t, uint64(0x2), vector[1])
})
}
func Test_resolveImports(t *testing.T) {
const moduleName = "test"
const name = "target"
t.Run("module not instantiated", func(t *testing.T) {
modules := map[string]*ModuleInstance{}
_, _, _, _, err := resolveImports(&Module{ImportSection: []*Import{{Module: "unknown", Name: "unknown"}}}, modules)
require.EqualError(t, err, "module[unknown] not instantiated")
})
t.Run("export instance not found", func(t *testing.T) {
modules := map[string]*ModuleInstance{
moduleName: {Exports: map[string]ExportInstance{}, Name: moduleName},
}
_, _, _, _, err := resolveImports(&Module{ImportSection: []*Import{{Module: moduleName, Name: "unknown"}}}, modules)
require.EqualError(t, err, "\"unknown\" is not exported in module \"test\"")
})
t.Run("func", func(t *testing.T) {
t.Run("ok", func(t *testing.T) {
externMod := &ModuleInstance{
Functions: []FunctionInstance{
{Definition: &FunctionDefinition{funcType: &FunctionType{Results: []ValueType{ValueTypeF32}}}},
{Definition: &FunctionDefinition{funcType: &FunctionType{Results: []ValueType{ValueTypeI32}}}},
},
Exports: map[string]ExportInstance{
name: {Type: ExternTypeFunc, Index: 0},
"": {Type: ExternTypeFunc, Index: 1},
},
Name: moduleName,
}
modules := map[string]*ModuleInstance{
moduleName: externMod,
}
m := &Module{
TypeSection: []*FunctionType{{Results: []ValueType{ValueTypeF32}}, {Results: []ValueType{ValueTypeI32}}},
ImportSection: []*Import{
{Module: moduleName, Name: name, Type: ExternTypeFunc, DescFunc: 0},
{Module: moduleName, Name: "", Type: ExternTypeFunc, DescFunc: 1},
},
}
functions, _, _, _, err := resolveImports(m, modules)
require.NoError(t, err)
require.True(t, functionsContain(functions, &externMod.Functions[0]), "expected to find %v in %v", &externMod.Functions[0], functions)
require.True(t, functionsContain(functions, &externMod.Functions[1]), "expected to find %v in %v", &externMod.Functions[1], functions)
})
t.Run("type out of range", func(t *testing.T) {
modules := map[string]*ModuleInstance{
moduleName: {Exports: map[string]ExportInstance{name: {}}, Name: moduleName},
}
_, _, _, _, err := resolveImports(&Module{ImportSection: []*Import{{Module: moduleName, Name: name, Type: ExternTypeFunc, DescFunc: 100}}}, modules)
require.EqualError(t, err, "import[0] func[test.target]: function type out of range")
})
t.Run("signature mismatch", func(t *testing.T) {
externMod := &ModuleInstance{
Functions: []FunctionInstance{{Definition: &FunctionDefinition{funcType: &FunctionType{}}}},
Exports: map[string]ExportInstance{
name: {Type: ExternTypeFunc, Index: 0},
},
Name: moduleName,
}
modules := map[string]*ModuleInstance{moduleName: externMod}
m := &Module{
TypeSection: []*FunctionType{{Results: []ValueType{ValueTypeF32}}},
ImportSection: []*Import{{Module: moduleName, Name: name, Type: ExternTypeFunc, DescFunc: 0}},
}
_, _, _, _, err := resolveImports(m, modules)
require.EqualError(t, err, "import[0] func[test.target]: signature mismatch: v_f32 != v_v")
})
})
t.Run("global", func(t *testing.T) {
t.Run("ok", func(t *testing.T) {
g := &GlobalInstance{Type: &GlobalType{ValType: ValueTypeI32}}
modules := map[string]*ModuleInstance{
moduleName: {
Globals: []*GlobalInstance{g},
Exports: map[string]ExportInstance{name: {Type: ExternTypeGlobal, Index: 0}}, Name: moduleName,
},
}
_, globals, _, _, err := resolveImports(&Module{ImportSection: []*Import{{Module: moduleName, Name: name, Type: ExternTypeGlobal, DescGlobal: g.Type}}}, modules)
require.NoError(t, err)
require.True(t, globalsContain(globals, g), "expected to find %v in %v", g, globals)
})
t.Run("mutability mismatch", func(t *testing.T) {
modules := map[string]*ModuleInstance{
moduleName: {
Globals: []*GlobalInstance{{Type: &GlobalType{Mutable: false}}},
Exports: map[string]ExportInstance{name: {
Type: ExternTypeGlobal,
Index: 0,
}},
Name: moduleName,
},
}
_, _, _, _, err := resolveImports(&Module{ImportSection: []*Import{{Module: moduleName, Name: name, Type: ExternTypeGlobal, DescGlobal: &GlobalType{Mutable: true}}}}, modules)
require.EqualError(t, err, "import[0] global[test.target]: mutability mismatch: true != false")
})
t.Run("type mismatch", func(t *testing.T) {
modules := map[string]*ModuleInstance{
moduleName: {
Globals: []*GlobalInstance{{Type: &GlobalType{ValType: ValueTypeI32}}},
Exports: map[string]ExportInstance{name: {
Type: ExternTypeGlobal,
Index: 0,
}},
Name: moduleName,
},
}
_, _, _, _, err := resolveImports(&Module{ImportSection: []*Import{{Module: moduleName, Name: name, Type: ExternTypeGlobal, DescGlobal: &GlobalType{ValType: ValueTypeF64}}}}, modules)
require.EqualError(t, err, "import[0] global[test.target]: value type mismatch: f64 != i32")
})
})
t.Run("memory", func(t *testing.T) {
t.Run("ok", func(t *testing.T) {
max := uint32(10)
memoryInst := &MemoryInstance{Max: max}
modules := map[string]*ModuleInstance{
moduleName: {
Memory: memoryInst,
Exports: map[string]ExportInstance{name: {
Type: ExternTypeMemory,
}},
Name: moduleName,
},
}
_, _, _, memory, err := resolveImports(&Module{ImportSection: []*Import{{Module: moduleName, Name: name, Type: ExternTypeMemory, DescMem: &Memory{Max: max}}}}, modules)
require.NoError(t, err)
require.Equal(t, memory, memoryInst)
})
t.Run("minimum size mismatch", func(t *testing.T) {
importMemoryType := &Memory{Min: 2, Cap: 2}
modules := map[string]*ModuleInstance{
moduleName: {
Memory: &MemoryInstance{Min: importMemoryType.Min - 1, Cap: 2},
Exports: map[string]ExportInstance{name: {
Type: ExternTypeMemory,
}},
Name: moduleName,
},
}
_, _, _, _, err := resolveImports(&Module{ImportSection: []*Import{{Module: moduleName, Name: name, Type: ExternTypeMemory, DescMem: importMemoryType}}}, modules)
require.EqualError(t, err, "import[0] memory[test.target]: minimum size mismatch: 2 > 1")
})
t.Run("maximum size mismatch", func(t *testing.T) {
max := uint32(10)
importMemoryType := &Memory{Max: max}
modules := map[string]*ModuleInstance{
moduleName: {
Memory: &MemoryInstance{Max: MemoryLimitPages},
Exports: map[string]ExportInstance{name: {
Type: ExternTypeMemory,
}},
Name: moduleName,
},
}
_, _, _, _, err := resolveImports(&Module{ImportSection: []*Import{{Module: moduleName, Name: name, Type: ExternTypeMemory, DescMem: importMemoryType}}}, modules)
require.EqualError(t, err, "import[0] memory[test.target]: maximum size mismatch: 10 < 65536")
})
})
}
func TestModuleInstance_validateData(t *testing.T) {
m := &ModuleInstance{Memory: &MemoryInstance{Buffer: make([]byte, 5)}}
tests := []struct {
name string
data []*DataSegment
expErr string
}{
{
name: "ok",
data: []*DataSegment{
{OffsetExpression: &ConstantExpression{Opcode: OpcodeI32Const, Data: const1}, Init: []byte{0}},
{OffsetExpression: &ConstantExpression{Opcode: OpcodeI32Const, Data: leb128.EncodeInt32(2)}, Init: []byte{0}},
},
},
{
name: "out of bounds - single one byte",
data: []*DataSegment{
{OffsetExpression: &ConstantExpression{Opcode: OpcodeI32Const, Data: leb128.EncodeInt32(5)}, Init: []byte{0}},
},
expErr: "data[0]: out of bounds memory access",
},
{
name: "out of bounds - multi bytes",
data: []*DataSegment{
{OffsetExpression: &ConstantExpression{Opcode: OpcodeI32Const, Data: leb128.EncodeInt32(0)}, Init: []byte{0}},
{OffsetExpression: &ConstantExpression{Opcode: OpcodeI32Const, Data: leb128.EncodeInt32(3)}, Init: []byte{0, 1, 2}},
},
expErr: "data[1]: out of bounds memory access",
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
err := m.validateData(tc.data)
if tc.expErr != "" {
require.EqualError(t, err, tc.expErr)
} else {
require.NoError(t, err)
}
})
}
}
func TestModuleInstance_applyData(t *testing.T) {
t.Run("ok", func(t *testing.T) {
m := &ModuleInstance{Memory: &MemoryInstance{Buffer: make([]byte, 10)}}
err := m.applyData([]*DataSegment{
{OffsetExpression: &ConstantExpression{Opcode: OpcodeI32Const, Data: const0}, Init: []byte{0xa, 0xf}},
{OffsetExpression: &ConstantExpression{Opcode: OpcodeI32Const, Data: leb128.EncodeUint32(8)}, Init: []byte{0x1, 0x5}},
})
require.NoError(t, err)
require.Equal(t, []byte{0xa, 0xf, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x5}, m.Memory.Buffer)
require.Equal(t, [][]byte{{0xa, 0xf}, {0x1, 0x5}}, m.DataInstances)
})
t.Run("error", func(t *testing.T) {
m := &ModuleInstance{Memory: &MemoryInstance{Buffer: make([]byte, 5)}}
err := m.applyData([]*DataSegment{
{OffsetExpression: &ConstantExpression{Opcode: OpcodeI32Const, Data: leb128.EncodeUint32(8)}, Init: []byte{}},
})
require.EqualError(t, err, "data[0]: out of bounds memory access")
})
}
func globalsContain(globals []*GlobalInstance, want *GlobalInstance) bool {
for _, f := range globals {
if f == want {
return true
}
}
return false
}
func functionsContain(functions []*FunctionInstance, want *FunctionInstance) bool {
for _, f := range functions {
if f == want {
return true
}
}
return false
}
func TestModuleInstance_applyTableInits(t *testing.T) {
t.Run("extenref", func(t *testing.T) {
tables := []*TableInstance{{Type: RefTypeExternref, References: make([]Reference, 10)}}
for i := range tables[0].References {
tables[0].References[i] = 0xffff // non-null ref.
}
m := &ModuleInstance{}
// This shouldn't panic.
m.applyTableInits(tables, []tableInitEntry{{offset: 100}})
m.applyTableInits(tables, []tableInitEntry{
{offset: 0, nullExternRefCount: 3},
{offset: 100}, // Iteration stops at this point, so the offset:5 below shouldn't be applied.
{offset: 5, nullExternRefCount: 5},
})
require.Equal(t, []Reference{0, 0, 0, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff},
tables[0].References)
m.applyTableInits(tables, []tableInitEntry{
{offset: 5, nullExternRefCount: 5},
})
require.Equal(t, []Reference{0, 0, 0, 0xffff, 0xffff, 0, 0, 0, 0, 0}, tables[0].References)
})
t.Run("funcref", func(t *testing.T) {
e := &mockEngine{}
me, err := e.NewModuleEngine("", nil, nil)
me.(*mockModuleEngine).functionRefs = map[Index]Reference{0: 0xa, 1: 0xaa, 2: 0xaaa, 3: 0xaaaa}
require.NoError(t, err)
m := &ModuleInstance{Engine: me}
tables := []*TableInstance{{Type: RefTypeFuncref, References: make([]Reference, 10)}}
for i := range tables[0].References {
tables[0].References[i] = 0xffff // non-null ref.
}
// This shouldn't panic.
m.applyTableInits(tables, []tableInitEntry{{offset: 100}})
m.applyTableInits(tables, []tableInitEntry{
{offset: 0, functionIndexes: []*Index{uint32Ptr(0), uint32Ptr(1), uint32Ptr(2)}},
{offset: 100}, // Iteration stops at this point, so the offset:5 below shouldn't be applied.
{offset: 5, nullExternRefCount: 5},
})
require.Equal(t, []Reference{0xa, 0xaa, 0xaaa, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff},
tables[0].References)
m.applyTableInits(tables, []tableInitEntry{
{offset: 5, functionIndexes: []*Index{uint32Ptr(0), nil, uint32Ptr(2)}},
})
require.Equal(t, []Reference{0xa, 0xaa, 0xaaa, 0xffff, 0xffff, 0xa, 0xffff, 0xaaa, 0xffff, 0xffff},
tables[0].References)
})
}
// TestPreAllocatedTypeIDs ensures that PreAllocatedTypeIDs has no duplication on the values (FunctionTypeID).
func TestPreAllocatedTypeIDs(t *testing.T) {
exists := make(map[FunctionTypeID]struct{}, len(preAllocatedTypeIDs))
for _, v := range preAllocatedTypeIDs {
_, ok := exists[v]
require.False(t, ok)
exists[v] = struct{}{}
}
}