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 }