Files
wazero/internal/integration_test/engine/hammer_test.go
Crypt Keeper 45ff2fe12f Propagates context to all api interface methods that aren't constant (#502)
This prepares for exposing operations like Memory.Grow while keeping the
ability to trace what did that, by adding a `context.Context` initial
parameter. This adds this to all API methods that mutate or return
mutated data.

Before, we made a change to trace functions and general lifecycle
commands, but we missed this part. Ex. We track functions, but can't
track what closed the module, changed memory or a mutable constant.
Changing to do this now is not only more consistent, but helps us
optimize at least the interpreter to help users identify otherwise
opaque code that can cause harm. This is critical before we add more
functions that can cause harm, such as Memory.Grow.

Signed-off-by: Adrian Cole <adrian@tetrate.io>
2022-04-25 08:13:18 +08:00

122 lines
4.3 KiB
Go

package adhoc
import (
"sync"
"testing"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/internal/testing/hammer"
"github.com/tetratelabs/wazero/internal/testing/require"
"github.com/tetratelabs/wazero/sys"
)
var hammers = map[string]func(t *testing.T, r wazero.Runtime){
// Tests here are similar to what's described in /RATIONALE.md, but deviate as they involve blocking functions.
"close importing module while in use": closeImportingModuleWhileInUse,
"close imported module while in use": closeImportedModuleWhileInUse,
}
func TestEngineJIT_hammer(t *testing.T) {
if !wazero.JITSupported {
t.Skip()
}
runAllTests(t, hammers, wazero.NewRuntimeConfigJIT())
}
func TestEngineInterpreter_hammer(t *testing.T) {
runAllTests(t, hammers, wazero.NewRuntimeConfigInterpreter())
}
func closeImportingModuleWhileInUse(t *testing.T, r wazero.Runtime) {
closeModuleWhileInUse(t, r, func(imported, importing api.Module) (api.Module, api.Module) {
// Close the importing module, despite calls being in-flight.
require.NoError(t, importing.Close(testCtx))
// Prove a module can be redefined even with in-flight calls.
source := callReturnImportSource(imported.Name(), importing.Name())
importing, err := r.InstantiateModuleFromCode(testCtx, source)
require.NoError(t, err)
return imported, importing
})
}
func closeImportedModuleWhileInUse(t *testing.T, r wazero.Runtime) {
closeModuleWhileInUse(t, r, func(imported, importing api.Module) (api.Module, api.Module) {
// Close the importing and imported module, despite calls being in-flight.
require.NoError(t, importing.Close(testCtx))
require.NoError(t, imported.Close(testCtx))
// Redefine the imported module, with a function that no longer blocks.
imported, err := r.NewModuleBuilder(imported.Name()).ExportFunction("return_input", func(x uint32) uint32 {
return x
}).Instantiate(testCtx)
require.NoError(t, err)
// Redefine the importing module, which should link to the redefined host module.
source := callReturnImportSource(imported.Name(), importing.Name())
importing, err = r.InstantiateModuleFromCode(testCtx, source)
require.NoError(t, err)
return imported, importing
})
}
func closeModuleWhileInUse(t *testing.T, r wazero.Runtime, closeFn func(imported, importing api.Module) (api.Module, api.Module)) {
P := 8 // max count of goroutines
if testing.Short() { // Adjust down if `-test.short`
P = 4
}
// To know return path works on a closed module, we need to block calls.
var calls sync.WaitGroup
calls.Add(P)
blockAndReturn := func(x uint32) uint32 {
calls.Wait()
return x
}
// Create the host module, which exports the blocking function.
imported, err := r.NewModuleBuilder(t.Name()+"-imported").
ExportFunction("return_input", blockAndReturn).Instantiate(testCtx)
require.NoError(t, err)
defer imported.Close(testCtx)
// Import that module.
source := callReturnImportSource(imported.Name(), t.Name()+"-importing")
importing, err := r.InstantiateModuleFromCode(testCtx, source)
require.NoError(t, err)
defer importing.Close(testCtx)
// As this is a blocking function call, only run 1 per goroutine.
i := importing // pin the module used inside goroutines
hammer.NewHammer(t, P, 1).Run(func(name string) {
// In all cases, the importing module is closed, so the error should have that as its module name.
requireFunctionCallExits(t, i.Name(), i.ExportedFunction("call_return_import"))
}, func() { // When all functions are in-flight, re-assign the modules.
imported, importing = closeFn(imported, importing)
// Unblock all the calls
calls.Add(-P)
})
// As references may have changed, ensure we close both.
defer imported.Close(testCtx)
defer importing.Close(testCtx)
if t.Failed() {
return // At least one test failed, so return now.
}
// If unloading worked properly, a new function call should route to the newly instantiated module.
requireFunctionCall(t, importing.ExportedFunction("call_return_import"))
}
func requireFunctionCall(t *testing.T, fn api.Function) {
res, err := fn.Call(testCtx, 3)
require.NoError(t, err)
require.Equal(t, uint64(3), res[0])
}
func requireFunctionCallExits(t *testing.T, moduleName string, fn api.Function) {
_, err := fn.Call(testCtx, 3)
require.Equal(t, sys.NewExitError(moduleName, 0), err)
}