github.com/wasilibs/wazerox@v0.0.0-20240124024944-4923be63ab5f/internal/integration_test/engine/hammer_test.go (about)

     1  package adhoc
     2  
     3  import (
     4  	"context"
     5  	"runtime"
     6  	"sync"
     7  	"testing"
     8  
     9  	wazero "github.com/wasilibs/wazerox"
    10  	"github.com/wasilibs/wazerox/api"
    11  	"github.com/wasilibs/wazerox/experimental/opt"
    12  	"github.com/wasilibs/wazerox/internal/platform"
    13  	"github.com/wasilibs/wazerox/internal/testing/hammer"
    14  	"github.com/wasilibs/wazerox/internal/testing/require"
    15  	"github.com/wasilibs/wazerox/sys"
    16  )
    17  
    18  var hammers = map[string]testCase{
    19  	// Tests here are similar to what's described in /RATIONALE.md, but deviate as they involve blocking functions.
    20  	"close importing module while in use": {f: closeImportingModuleWhileInUse},
    21  	"close imported module while in use":  {f: closeImportedModuleWhileInUse},
    22  }
    23  
    24  func TestEngineCompiler_hammer(t *testing.T) {
    25  	if !platform.CompilerSupported() {
    26  		t.Skip()
    27  	}
    28  	runAllTests(t, hammers, wazero.NewRuntimeConfigCompiler(), false)
    29  }
    30  
    31  func TestEngineInterpreter_hammer(t *testing.T) {
    32  	runAllTests(t, hammers, wazero.NewRuntimeConfigInterpreter(), false)
    33  }
    34  
    35  func TestEngineWazevo_hammer(t *testing.T) {
    36  	if runtime.GOARCH != "arm64" {
    37  		t.Skip()
    38  	}
    39  	c := opt.NewRuntimeConfigOptimizingCompiler()
    40  	runAllTests(t, hammers, c, true)
    41  }
    42  
    43  func closeImportingModuleWhileInUse(t *testing.T, r wazero.Runtime) {
    44  	closeModuleWhileInUse(t, r, func(imported, importing api.Module) (api.Module, api.Module) {
    45  		// Close the importing module, despite calls being in-flight.
    46  		require.NoError(t, importing.Close(testCtx))
    47  
    48  		// Prove a module can be redefined even with in-flight calls.
    49  		binary := callReturnImportWasm(t, imported.Name(), importing.Name(), i32)
    50  		importing, err := r.Instantiate(testCtx, binary)
    51  		require.NoError(t, err)
    52  		return imported, importing
    53  	})
    54  }
    55  
    56  func closeImportedModuleWhileInUse(t *testing.T, r wazero.Runtime) {
    57  	closeModuleWhileInUse(t, r, func(imported, importing api.Module) (api.Module, api.Module) {
    58  		// Close the importing and imported module, despite calls being in-flight.
    59  		require.NoError(t, importing.Close(testCtx))
    60  		require.NoError(t, imported.Close(testCtx))
    61  
    62  		// Redefine the imported module, with a function that no longer blocks.
    63  		imported, err := r.NewHostModuleBuilder(imported.Name()).
    64  			NewFunctionBuilder().
    65  			WithFunc(func(ctx context.Context, x uint32) uint32 {
    66  				return x
    67  			}).
    68  			Export("return_input").
    69  			Instantiate(testCtx)
    70  		require.NoError(t, err)
    71  
    72  		// Redefine the importing module, which should link to the redefined host module.
    73  		binary := callReturnImportWasm(t, imported.Name(), importing.Name(), i32)
    74  		importing, err = r.Instantiate(testCtx, binary)
    75  		require.NoError(t, err)
    76  
    77  		return imported, importing
    78  	})
    79  }
    80  
    81  func closeModuleWhileInUse(t *testing.T, r wazero.Runtime, closeFn func(imported, importing api.Module) (api.Module, api.Module)) {
    82  	P := 8               // max count of goroutines
    83  	if testing.Short() { // Adjust down if `-test.short`
    84  		P = 4
    85  	}
    86  
    87  	// To know return path works on a closed module, we need to block calls.
    88  	var calls sync.WaitGroup
    89  	calls.Add(P)
    90  	blockAndReturn := func(ctx context.Context, x uint32) uint32 {
    91  		calls.Wait()
    92  		return x
    93  	}
    94  
    95  	// Create the host module, which exports the blocking function.
    96  	imported, err := r.NewHostModuleBuilder(t.Name() + "-imported").
    97  		NewFunctionBuilder().WithFunc(blockAndReturn).Export("return_input").
    98  		Instantiate(testCtx)
    99  	require.NoError(t, err)
   100  	defer imported.Close(testCtx)
   101  
   102  	// Import that module.
   103  	binary := callReturnImportWasm(t, imported.Name(), t.Name()+"-importing", i32)
   104  	importing, err := r.Instantiate(testCtx, binary)
   105  	require.NoError(t, err)
   106  	defer importing.Close(testCtx)
   107  
   108  	// As this is a blocking function call, only run 1 per goroutine.
   109  	i := importing // pin the module used inside goroutines
   110  	hammer.NewHammer(t, P, 1).Run(func(name string) {
   111  		// In all cases, the importing module is closed, so the error should have that as its module name.
   112  		requireFunctionCallExits(t, i.ExportedFunction("call_return_input"))
   113  	}, func() { // When all functions are in-flight, re-assign the modules.
   114  		imported, importing = closeFn(imported, importing)
   115  		// Unblock all the calls
   116  		calls.Add(-P)
   117  	})
   118  	// As references may have changed, ensure we close both.
   119  	defer imported.Close(testCtx)
   120  	defer importing.Close(testCtx)
   121  	if t.Failed() {
   122  		return // At least one test failed, so return now.
   123  	}
   124  
   125  	// If unloading worked properly, a new function call should route to the newly instantiated module.
   126  	requireFunctionCall(t, importing.ExportedFunction("call_return_input"))
   127  }
   128  
   129  func requireFunctionCall(t *testing.T, fn api.Function) {
   130  	res, err := fn.Call(testCtx, 3)
   131  	require.NoError(t, err)
   132  	require.Equal(t, uint64(3), res[0])
   133  }
   134  
   135  func requireFunctionCallExits(t *testing.T, fn api.Function) {
   136  	_, err := fn.Call(testCtx, 3)
   137  	require.Equal(t, sys.NewExitError(0), err)
   138  }