wa-lang.org/wazero@v1.0.2/internal/integration_test/engine/hammer_test.go (about)

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