Files
wazero/builder_test.go
2023-03-15 13:45:52 +09:00

389 lines
13 KiB
Go

package wazero
import (
"context"
"testing"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/internal/testing/require"
"github.com/tetratelabs/wazero/internal/wasm"
)
// TestNewHostModuleBuilder_Compile only covers a few scenarios to avoid duplicating tests in internal/wasm/host_test.go
func TestNewHostModuleBuilder_Compile(t *testing.T) {
i32, i64 := api.ValueTypeI32, api.ValueTypeI64
uint32_uint32 := func(context.Context, uint32) uint32 {
return 0
}
uint64_uint32 := func(context.Context, uint64) uint32 {
return 0
}
gofunc1 := api.GoFunc(func(ctx context.Context, stack []uint64) {
stack[0] = 0
})
gofunc2 := api.GoFunc(func(ctx context.Context, stack []uint64) {
stack[0] = 0
})
tests := []struct {
name string
input func(Runtime) HostModuleBuilder
expected *wasm.Module
}{
{
name: "empty",
input: func(r Runtime) HostModuleBuilder {
return r.NewHostModuleBuilder("")
},
expected: &wasm.Module{},
},
{
name: "only name",
input: func(r Runtime) HostModuleBuilder {
return r.NewHostModuleBuilder("env")
},
expected: &wasm.Module{NameSection: &wasm.NameSection{ModuleName: "env"}},
},
{
name: "WithFunc",
input: func(r Runtime) HostModuleBuilder {
return r.NewHostModuleBuilder("").
NewFunctionBuilder().WithFunc(uint32_uint32).Export("1")
},
expected: &wasm.Module{
TypeSection: []wasm.FunctionType{
{Params: []api.ValueType{i32}, Results: []api.ValueType{i32}},
},
FunctionSection: []wasm.Index{0},
CodeSection: []*wasm.Code{wasm.MustParseGoReflectFuncCode(uint32_uint32)},
ExportSection: []wasm.Export{
{Name: "1", Type: wasm.ExternTypeFunc, Index: 0},
},
NameSection: &wasm.NameSection{
FunctionNames: wasm.NameMap{{Index: 0, Name: "1"}},
},
},
},
{
name: "WithFunc WithName WithParameterNames",
input: func(r Runtime) HostModuleBuilder {
return r.NewHostModuleBuilder("").NewFunctionBuilder().
WithFunc(uint32_uint32).
WithName("get").WithParameterNames("x").
Export("1")
},
expected: &wasm.Module{
TypeSection: []wasm.FunctionType{
{Params: []api.ValueType{i32}, Results: []api.ValueType{i32}},
},
FunctionSection: []wasm.Index{0},
CodeSection: []*wasm.Code{wasm.MustParseGoReflectFuncCode(uint32_uint32)},
ExportSection: []wasm.Export{
{Name: "1", Type: wasm.ExternTypeFunc, Index: 0},
},
NameSection: &wasm.NameSection{
FunctionNames: wasm.NameMap{{Index: 0, Name: "get"}},
LocalNames: []*wasm.NameMapAssoc{{Index: 0, NameMap: wasm.NameMap{{Index: 0, Name: "x"}}}},
},
},
},
{
name: "WithFunc WithName WithResultNames",
input: func(r Runtime) HostModuleBuilder {
return r.NewHostModuleBuilder("").NewFunctionBuilder().
WithFunc(uint32_uint32).
WithName("get").WithResultNames("x").
Export("1")
},
expected: &wasm.Module{
TypeSection: []wasm.FunctionType{
{Params: []api.ValueType{i32}, Results: []api.ValueType{i32}},
},
FunctionSection: []wasm.Index{0},
CodeSection: []*wasm.Code{wasm.MustParseGoReflectFuncCode(uint32_uint32)},
ExportSection: []wasm.Export{
{Name: "1", Type: wasm.ExternTypeFunc, Index: 0},
},
NameSection: &wasm.NameSection{
FunctionNames: wasm.NameMap{{Index: 0, Name: "get"}},
ResultNames: []*wasm.NameMapAssoc{{Index: 0, NameMap: wasm.NameMap{{Index: 0, Name: "x"}}}},
},
},
},
{
name: "WithFunc overwrites existing",
input: func(r Runtime) HostModuleBuilder {
return r.NewHostModuleBuilder("").
NewFunctionBuilder().WithFunc(uint32_uint32).Export("1").
NewFunctionBuilder().WithFunc(uint64_uint32).Export("1")
},
expected: &wasm.Module{
TypeSection: []wasm.FunctionType{
{Params: []api.ValueType{i64}, Results: []api.ValueType{i32}},
},
FunctionSection: []wasm.Index{0},
CodeSection: []*wasm.Code{wasm.MustParseGoReflectFuncCode(uint64_uint32)},
ExportSection: []wasm.Export{
{Name: "1", Type: wasm.ExternTypeFunc, Index: 0},
},
NameSection: &wasm.NameSection{
FunctionNames: wasm.NameMap{{Index: 0, Name: "1"}},
},
},
},
{
name: "WithFunc twice",
input: func(r Runtime) HostModuleBuilder {
// Intentionally out of order
return r.NewHostModuleBuilder("").
NewFunctionBuilder().WithFunc(uint64_uint32).Export("2").
NewFunctionBuilder().WithFunc(uint32_uint32).Export("1")
},
expected: &wasm.Module{
TypeSection: []wasm.FunctionType{
{Params: []api.ValueType{i32}, Results: []api.ValueType{i32}},
{Params: []api.ValueType{i64}, Results: []api.ValueType{i32}},
},
FunctionSection: []wasm.Index{0, 1},
CodeSection: []*wasm.Code{wasm.MustParseGoReflectFuncCode(uint32_uint32), wasm.MustParseGoReflectFuncCode(uint64_uint32)},
ExportSection: []wasm.Export{
{Name: "1", Type: wasm.ExternTypeFunc, Index: 0},
{Name: "2", Type: wasm.ExternTypeFunc, Index: 1},
},
NameSection: &wasm.NameSection{
FunctionNames: wasm.NameMap{{Index: 0, Name: "1"}, {Index: 1, Name: "2"}},
},
},
},
{
name: "WithGoFunction",
input: func(r Runtime) HostModuleBuilder {
return r.NewHostModuleBuilder("").
NewFunctionBuilder().
WithGoFunction(gofunc1, []api.ValueType{i32}, []api.ValueType{i32}).
Export("1")
},
expected: &wasm.Module{
TypeSection: []wasm.FunctionType{
{Params: []api.ValueType{i32}, Results: []api.ValueType{i32}},
},
FunctionSection: []wasm.Index{0},
CodeSection: []*wasm.Code{
{GoFunc: gofunc1},
},
ExportSection: []wasm.Export{
{Name: "1", Type: wasm.ExternTypeFunc, Index: 0},
},
NameSection: &wasm.NameSection{
FunctionNames: wasm.NameMap{{Index: 0, Name: "1"}},
},
},
},
{
name: "WithGoFunction WithName WithParameterNames",
input: func(r Runtime) HostModuleBuilder {
return r.NewHostModuleBuilder("").NewFunctionBuilder().
WithGoFunction(gofunc1, []api.ValueType{i32}, []api.ValueType{i32}).
WithName("get").WithParameterNames("x").
Export("1")
},
expected: &wasm.Module{
TypeSection: []wasm.FunctionType{
{Params: []api.ValueType{i32}, Results: []api.ValueType{i32}},
},
FunctionSection: []wasm.Index{0},
CodeSection: []*wasm.Code{
{GoFunc: gofunc1},
},
ExportSection: []wasm.Export{
{Name: "1", Type: wasm.ExternTypeFunc, Index: 0},
},
NameSection: &wasm.NameSection{
FunctionNames: wasm.NameMap{{Index: 0, Name: "get"}},
LocalNames: []*wasm.NameMapAssoc{{Index: 0, NameMap: wasm.NameMap{{Index: 0, Name: "x"}}}},
},
},
},
{
name: "WithGoFunction overwrites existing",
input: func(r Runtime) HostModuleBuilder {
return r.NewHostModuleBuilder("").
NewFunctionBuilder().
WithGoFunction(gofunc1, []api.ValueType{i32}, []api.ValueType{i32}).
Export("1").
NewFunctionBuilder().
WithGoFunction(gofunc2, []api.ValueType{i64}, []api.ValueType{i32}).
Export("1")
},
expected: &wasm.Module{
TypeSection: []wasm.FunctionType{
{Params: []api.ValueType{i64}, Results: []api.ValueType{i32}},
},
FunctionSection: []wasm.Index{0},
CodeSection: []*wasm.Code{
{GoFunc: gofunc2},
},
ExportSection: []wasm.Export{
{Name: "1", Type: wasm.ExternTypeFunc, Index: 0},
},
NameSection: &wasm.NameSection{
FunctionNames: wasm.NameMap{{Index: 0, Name: "1"}},
},
},
},
{
name: "WithGoFunction twice",
input: func(r Runtime) HostModuleBuilder {
// Intentionally out of order
return r.NewHostModuleBuilder("").
NewFunctionBuilder().
WithGoFunction(gofunc2, []api.ValueType{i64}, []api.ValueType{i32}).
Export("2").
NewFunctionBuilder().
WithGoFunction(gofunc1, []api.ValueType{i32}, []api.ValueType{i32}).
Export("1")
},
expected: &wasm.Module{
TypeSection: []wasm.FunctionType{
{Params: []api.ValueType{i32}, Results: []api.ValueType{i32}},
{Params: []api.ValueType{i64}, Results: []api.ValueType{i32}},
},
FunctionSection: []wasm.Index{0, 1},
CodeSection: []*wasm.Code{
{GoFunc: gofunc1},
{GoFunc: gofunc2},
},
ExportSection: []wasm.Export{
{Name: "1", Type: wasm.ExternTypeFunc, Index: 0},
{Name: "2", Type: wasm.ExternTypeFunc, Index: 1},
},
NameSection: &wasm.NameSection{
FunctionNames: wasm.NameMap{{Index: 0, Name: "1"}, {Index: 1, Name: "2"}},
},
},
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
b := tc.input(NewRuntime(testCtx)).(*hostModuleBuilder)
compiled, err := b.Compile(testCtx)
require.NoError(t, err)
m := compiled.(*compiledModule)
requireHostModuleEquals(t, tc.expected, m.module)
require.Equal(t, b.r.store.Engine, m.compiledEngine)
// TypeIDs must be assigned to compiledModule.
expTypeIDs, err := b.r.store.GetFunctionTypeIDs(tc.expected.TypeSection)
require.NoError(t, err)
require.Equal(t, expTypeIDs, m.typeIDs)
// Built module must be instantiable by Engine.
mod, err := b.r.InstantiateModule(testCtx, m, NewModuleConfig())
require.NoError(t, err)
// Closing the module shouldn't remove the compiler cache
require.NoError(t, mod.Close(testCtx))
require.Equal(t, uint32(1), b.r.store.Engine.CompiledModuleCount())
})
}
}
// TestNewHostModuleBuilder_Compile_Errors only covers a few scenarios to avoid
// duplicating tests in internal/wasm/host_test.go
func TestNewHostModuleBuilder_Compile_Errors(t *testing.T) {
tests := []struct {
name string
input func(Runtime) HostModuleBuilder
expectedErr string
}{
{
name: "error compiling", // should fail due to missing result.
input: func(rt Runtime) HostModuleBuilder {
return rt.NewHostModuleBuilder("").NewFunctionBuilder().
WithFunc(&wasm.HostFunc{
ExportNames: []string{"fn"},
ResultTypes: []wasm.ValueType{wasm.ValueTypeI32},
Code: &wasm.Code{Body: []byte{wasm.OpcodeEnd}},
}).Export("fn")
},
expectedErr: `invalid function[0] export["fn"]: not enough results
have ()
want (i32)`,
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
_, e := tc.input(NewRuntime(testCtx)).Compile(testCtx)
require.EqualError(t, e, tc.expectedErr)
})
}
}
// TestNewHostModuleBuilder_Instantiate ensures Runtime.InstantiateModule is called on success.
func TestNewHostModuleBuilder_Instantiate(t *testing.T) {
r := NewRuntime(testCtx)
m, err := r.NewHostModuleBuilder("env").Instantiate(testCtx)
require.NoError(t, err)
// If this was instantiated, it would be added to the store under the same name
require.Equal(t, r.Module("env"), m)
// Closing the module should remove the compiler cache
require.NoError(t, m.Close(testCtx))
require.Zero(t, r.(*runtime).store.Engine.CompiledModuleCount())
}
// TestNewHostModuleBuilder_Instantiate_Errors ensures errors propagate from Runtime.InstantiateModule
func TestNewHostModuleBuilder_Instantiate_Errors(t *testing.T) {
r := NewRuntime(testCtx)
_, err := r.NewHostModuleBuilder("env").Instantiate(testCtx)
require.NoError(t, err)
_, err = r.NewHostModuleBuilder("env").Instantiate(testCtx)
require.EqualError(t, err, "module[env] has already been instantiated")
}
// requireHostModuleEquals is redefined from internal/wasm/host_test.go to avoid an import cycle extracting it.
func requireHostModuleEquals(t *testing.T, expected, actual *wasm.Module) {
// `require.Equal(t, expected, actual)` fails reflect pointers don't match, so brute compare:
for i := range expected.TypeSection {
tp := &expected.TypeSection[i]
tp.CacheNumInUint64()
// When creating the compiled module, we get the type IDs for types, which results in caching type keys.
_ = tp.String()
}
require.Equal(t, expected.TypeSection, actual.TypeSection)
require.Equal(t, expected.ImportSection, actual.ImportSection)
require.Equal(t, expected.FunctionSection, actual.FunctionSection)
require.Equal(t, expected.TableSection, actual.TableSection)
require.Equal(t, expected.MemorySection, actual.MemorySection)
require.Equal(t, expected.GlobalSection, actual.GlobalSection)
require.Equal(t, expected.ExportSection, actual.ExportSection)
require.Equal(t, expected.StartSection, actual.StartSection)
require.Equal(t, expected.ElementSection, actual.ElementSection)
require.Equal(t, expected.DataSection, actual.DataSection)
require.Equal(t, expected.NameSection, actual.NameSection)
// Special case because reflect.Value can't be compared with Equals
// TODO: This is copy/paste with /internal/wasm/host_test.go
require.Equal(t, len(expected.CodeSection), len(actual.CodeSection))
for i, c := range expected.CodeSection {
actualCode := actual.CodeSection[i]
require.Equal(t, c.GoFunc, actualCode.GoFunc)
// Not wasm
require.Nil(t, actualCode.Body)
require.Nil(t, actualCode.LocalTypes)
}
}