This extracts `hammer.Hammer` as a utility for re-use, notably adding a feature that ensures all tests run concurrently. Before, tests start in a loop that could be delayed due to goroutine sheduling. Signed-off-by: Adrian Cole <adrian@tetrate.io>
226 lines
6.6 KiB
Go
226 lines
6.6 KiB
Go
package adhoc
|
|
|
|
import (
|
|
"errors"
|
|
"runtime"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/tetratelabs/wazero"
|
|
"github.com/tetratelabs/wazero/wasm"
|
|
)
|
|
|
|
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())
|
|
}
|
|
|
|
type blocker struct {
|
|
running chan bool
|
|
// unblocked uses a WaitGroup as it allows releasing all goroutines at the same time.
|
|
unblocked sync.WaitGroup
|
|
// closed should panic if fn is called when the value is 1.
|
|
//
|
|
// Note: Exclusively reading and updating this with atomics guarantees cross-goroutine observations.
|
|
// See /RATIONALE.md
|
|
closed uint32
|
|
}
|
|
|
|
func (b *blocker) close() {
|
|
atomic.StoreUint32(&b.closed, 1)
|
|
}
|
|
|
|
// fn sends into the running channel, then blocks until it can receive from unblocked one.
|
|
func (b *blocker) fn(input uint32) uint32 {
|
|
if atomic.LoadUint32(&b.closed) == 1 {
|
|
panic(errors.New("closed"))
|
|
}
|
|
b.running <- true // Signal the goroutine is running
|
|
b.unblocked.Wait() // Await until unblocked
|
|
return input
|
|
}
|
|
|
|
var blockAfterAddSource = []byte(`(module
|
|
(import "host" "block" (func $block (param i32) (result i32)))
|
|
(func $block_after_add (param i32) (param i32) (result i32)
|
|
local.get 0
|
|
local.get 1
|
|
i32.add
|
|
call $block
|
|
)
|
|
(export "block_after_add" (func $block_after_add))
|
|
)`)
|
|
|
|
func closeImportingModuleWhileInUse(t *testing.T, r wazero.Runtime) {
|
|
args := []uint64{1, 123}
|
|
exp := args[0] + args[1]
|
|
|
|
P := 8 // P+1 == max count of goroutines/in-flight function calls
|
|
if testing.Short() { // Adjust down if `-test.short`
|
|
P = 4
|
|
}
|
|
defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(P / 2)) // Ensure goroutines have to switch cores.
|
|
|
|
running := make(chan bool)
|
|
b := &blocker{running: running}
|
|
b.unblocked.Add(P)
|
|
|
|
imported, err := r.NewModuleBuilder("host").ExportFunction("block", b.fn).Instantiate()
|
|
require.NoError(t, err)
|
|
defer imported.Close()
|
|
|
|
importing, err := r.InstantiateModuleFromSource(blockAfterAddSource)
|
|
require.NoError(t, err)
|
|
defer importing.Close()
|
|
|
|
// Add channel that tracks P goroutines.
|
|
done := make(chan int)
|
|
for p := 0; p < P; p++ {
|
|
go func() { // Launch goroutine 'p'
|
|
defer completeGoroutine(t, done)
|
|
|
|
// As this is a blocking function, we can only run 1 function per goroutine.
|
|
requireFunctionCall(t, importing.ExportedFunction("block_after_add"), args, exp)
|
|
}()
|
|
}
|
|
|
|
// Wait until all functions are in-flight.
|
|
for i := 0; i < P; i++ {
|
|
<-running
|
|
}
|
|
|
|
// Close the module that exported blockAfterAdd (noting calls to this are in-flight).
|
|
require.NoError(t, importing.Close())
|
|
|
|
// Prove a module can be redefined even with in-flight calls.
|
|
importing, err = r.InstantiateModuleFromSource(blockAfterAddSource)
|
|
require.NoError(t, err)
|
|
defer importing.Close()
|
|
|
|
// If unloading worked properly, a new function call should route to the newly instantiated module.
|
|
b.unblocked.Add(1)
|
|
go func() {
|
|
defer completeGoroutine(t, done)
|
|
|
|
requireFunctionCall(t, importing.ExportedFunction("block_after_add"), args, exp)
|
|
}()
|
|
<-running // Wait for the above function to be in-flight
|
|
P++
|
|
|
|
// Unblock the functions to ensure they don't err on the return path of a closed module.
|
|
b.unblocked.Add(-P)
|
|
|
|
// Wait for all goroutines to complete
|
|
for i := 0; i < P; i++ {
|
|
<-done
|
|
}
|
|
}
|
|
|
|
func closeImportedModuleWhileInUse(t *testing.T, r wazero.Runtime) {
|
|
args := []uint64{1, 123}
|
|
exp := args[0] + args[1]
|
|
|
|
P := 8 // P+1 == max count of goroutines/in-flight function calls
|
|
if testing.Short() { // Adjust down if `-test.short`
|
|
P = 4
|
|
}
|
|
defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(P / 2)) // Ensure goroutines have to switch cores.
|
|
|
|
running := make(chan bool)
|
|
b := &blocker{running: running}
|
|
b.unblocked.Add(P)
|
|
|
|
imported, err := r.NewModuleBuilder("host").ExportFunction("block", b.fn).Instantiate()
|
|
require.NoError(t, err)
|
|
defer imported.Close()
|
|
|
|
importing, err := r.InstantiateModuleFromSource(blockAfterAddSource)
|
|
require.NoError(t, err)
|
|
defer importing.Close()
|
|
|
|
// Add channel that tracks P goroutines.
|
|
done := make(chan int)
|
|
for p := 0; p < P; p++ {
|
|
go func() { // Launch goroutine 'p'
|
|
defer completeGoroutine(t, done)
|
|
|
|
// As this is a blocking function, we can only run 1 function per goroutine.
|
|
requireFunctionCall(t, importing.ExportedFunction("block_after_add"), args, exp)
|
|
}()
|
|
}
|
|
|
|
// Wait until all functions are in-flight.
|
|
for i := 0; i < P; i++ {
|
|
<-running
|
|
}
|
|
|
|
// Close the module that exported the host function (noting calls to this are in-flight).
|
|
require.NoError(t, imported.Close())
|
|
// Close the underlying host function, which causes future calls to it to fail.
|
|
b.close()
|
|
require.Panics(t, func() {
|
|
b.fn(1) // validate it would fail if accidentally called
|
|
})
|
|
// Close the importing module
|
|
require.NoError(t, importing.Close())
|
|
|
|
// Prove a host module can be redefined even with in-flight calls.
|
|
b1 := &blocker{running: running} // New instance, so not yet closed!
|
|
imported, err = r.NewModuleBuilder("host").ExportFunction("block", b1.fn).Instantiate()
|
|
require.NoError(t, err)
|
|
defer imported.Close()
|
|
|
|
// Redefine the importing module, which should link to the redefined host module.
|
|
importing, err = r.InstantiateModuleFromSource(blockAfterAddSource)
|
|
require.NoError(t, err)
|
|
defer importing.Close()
|
|
|
|
// If unloading worked properly, a new function call should route to the newly instantiated module.
|
|
b1.unblocked.Add(1)
|
|
go func() {
|
|
defer completeGoroutine(t, done)
|
|
|
|
requireFunctionCall(t, importing.ExportedFunction("block_after_add"), args, exp)
|
|
}()
|
|
<-running // Wait for the above function to be in-flight
|
|
|
|
// Unblock the functions to ensure they don't err on the return path of a closed module.
|
|
b.unblocked.Add(-P)
|
|
b1.unblocked.Add(-1)
|
|
|
|
// Wait for all goroutines to complete
|
|
for i := 0; i < P+1; i++ {
|
|
<-done
|
|
}
|
|
}
|
|
|
|
func requireFunctionCall(t *testing.T, fn wasm.Function, args []uint64, exp uint64) {
|
|
res, err := fn.Call(nil, args...)
|
|
// We don't expect an error because there's currently no functionality to detect or fail on a closed module.
|
|
require.NoError(t, err)
|
|
require.Equal(t, exp, res[0])
|
|
}
|
|
|
|
func completeGoroutine(t *testing.T, c chan int) {
|
|
// Ensure each require.XX failure is visible on hammer test fail.
|
|
if recovered := recover(); recovered != nil {
|
|
t.Error(recovered.(string))
|
|
}
|
|
c <- 1
|
|
}
|