Files
wazero/wasm_test.go
Crypt Keeper 50d9fa58a1 Runtime.NewModule -> InstantiateModule and adds ModuleBuilder (#349)
This reverts `Runtime.NewModule` back to `InstantiateModule` as it calls
more attention to the registration aspect of it, and also makes a chain
of `NewXX` more clear. This is particularly helpful as this change
introduces `ModuleBuilder` which is created by `NewModuleBuilder`.

`ModuleBuilder` is a way to define a WebAssembly 1.0 (20191205) in Go.
The first iteration allows setting the module name and exported
functions. The next PR will add globals.

Ex. Below defines and instantiates a module named "env" with one function:

```go
hello := func() {
	fmt.Fprintln(stdout, "hello!")
}
_, err := r.NewModuleBuilder("env").ExportFunction("hello", hello).InstantiateModule()
```

If the same module may be instantiated multiple times, it is more efficient to separate steps. Ex.

```go
env, err := r.NewModuleBuilder("env").ExportFunction("get_random_string", getRandomString).Build()

_, err := r.InstantiateModule(env.WithName("env.1"))
_, err := r.InstantiateModule(env.WithName("env.2"))
```

Note: Builder methods do not return errors, to allow chaining. Any validation errors are deferred until Build.
Note: Insertion order is not retained. Anything defined by this builder is sorted lexicographically on Build.

Signed-off-by: Adrian Cole <adrian@tetrate.io>
2022-03-09 10:39:13 +08:00

330 lines
8.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.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.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.DecodeModule([]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.DecodeModule(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("").ExportFunction("start", start).Instantiate()
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.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,
))
}