github.com/wasilibs/wazerox@v0.0.0-20240124024944-4923be63ab5f/imports/wasi_snapshot_preview1/wasi_test.go (about)

     1  package wasi_snapshot_preview1_test
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	_ "embed"
     7  	"testing"
     8  	"time"
     9  
    10  	wazero "github.com/wasilibs/wazerox"
    11  	"github.com/wasilibs/wazerox/api"
    12  	"github.com/wasilibs/wazerox/experimental"
    13  	"github.com/wasilibs/wazerox/experimental/logging"
    14  	"github.com/wasilibs/wazerox/imports/wasi_snapshot_preview1"
    15  	"github.com/wasilibs/wazerox/internal/testing/proxy"
    16  	"github.com/wasilibs/wazerox/internal/testing/require"
    17  	"github.com/wasilibs/wazerox/internal/wasip1"
    18  	"github.com/wasilibs/wazerox/sys"
    19  )
    20  
    21  // testCtx is an arbitrary, non-default context. Non-nil also prevents linter errors.
    22  var testCtx = context.WithValue(context.Background(), struct{}{}, "arbitrary")
    23  
    24  const testMemoryPageSize = 1
    25  
    26  // exitOnStartUnstableWasm was generated by the following:
    27  //
    28  //	cd testdata; wat2wasm --debug-names exit_on_start_unstable.wat
    29  //
    30  //go:embed testdata/exit_on_start_unstable.wasm
    31  var exitOnStartUnstableWasm []byte
    32  
    33  func TestNewFunctionExporter(t *testing.T) {
    34  	t.Run("export as wasi_unstable", func(t *testing.T) {
    35  		r := wazero.NewRuntime(testCtx)
    36  		defer r.Close(testCtx)
    37  
    38  		// Instantiate the current WASI functions under the wasi_unstable
    39  		// instead of wasi_snapshot_preview1.
    40  		wasiBuilder := r.NewHostModuleBuilder("wasi_unstable")
    41  		wasi_snapshot_preview1.NewFunctionExporter().ExportFunctions(wasiBuilder)
    42  		_, err := wasiBuilder.Instantiate(testCtx)
    43  		require.NoError(t, err)
    44  
    45  		// Instantiate our test binary, but using the old import names.
    46  		_, err = r.Instantiate(testCtx, exitOnStartUnstableWasm)
    47  
    48  		// Ensure the test binary worked. It should return exit code 2.
    49  		require.Equal(t, uint32(2), err.(*sys.ExitError).ExitCode())
    50  	})
    51  
    52  	t.Run("override function", func(t *testing.T) {
    53  		r := wazero.NewRuntime(testCtx)
    54  		defer r.Close(testCtx)
    55  
    56  		// Export the default WASI functions
    57  		wasiBuilder := r.NewHostModuleBuilder(wasi_snapshot_preview1.ModuleName)
    58  		wasi_snapshot_preview1.NewFunctionExporter().ExportFunctions(wasiBuilder)
    59  
    60  		// Override proc_exit to prove the point that you can add or replace
    61  		// functions like this.
    62  		wasiBuilder.NewFunctionBuilder().
    63  			WithFunc(func(ctx context.Context, mod api.Module, exitCode uint32) {
    64  				require.Equal(t, uint32(2), exitCode)
    65  				// ignore the code instead!
    66  				mod.Close(ctx)
    67  			}).Export("proc_exit")
    68  
    69  		_, err := wasiBuilder.Instantiate(testCtx)
    70  		require.NoError(t, err)
    71  
    72  		// Instantiate our test binary which will use our modified WASI.
    73  		_, err = r.Instantiate(testCtx, exitOnStartWasm)
    74  
    75  		// Ensure the modified function was used!
    76  		require.Nil(t, err)
    77  	})
    78  }
    79  
    80  // maskMemory sets the first memory in the store to '?' * size, so tests can see what's written.
    81  func maskMemory(t *testing.T, mod api.Module, size int) {
    82  	for i := uint32(0); i < uint32(size); i++ {
    83  		require.True(t, mod.Memory().WriteByte(i, '?'))
    84  	}
    85  }
    86  
    87  func requireProxyModule(t *testing.T, config wazero.ModuleConfig) (api.Module, api.Closer, *bytes.Buffer) {
    88  	return requireProxyModuleWithContext(testCtx, t, config)
    89  }
    90  
    91  func requireProxyModuleWithContext(ctx context.Context, t *testing.T, config wazero.ModuleConfig) (api.Module, api.Closer, *bytes.Buffer) {
    92  	var log bytes.Buffer
    93  
    94  	// Set context to one that has an experimental listener
    95  	ctx = context.WithValue(ctx, experimental.FunctionListenerFactoryKey{},
    96  		proxy.NewLoggingListenerFactory(&log, logging.LogScopeAll))
    97  
    98  	r := wazero.NewRuntime(ctx)
    99  
   100  	wasiModuleCompiled, err := wasi_snapshot_preview1.NewBuilder(r).Compile(ctx)
   101  	require.NoError(t, err)
   102  
   103  	_, err = r.InstantiateModule(ctx, wasiModuleCompiled, config)
   104  	require.NoError(t, err)
   105  
   106  	proxyBin := proxy.NewModuleBinary(wasi_snapshot_preview1.ModuleName, wasiModuleCompiled)
   107  
   108  	proxyCompiled, err := r.CompileModule(ctx, proxyBin)
   109  	require.NoError(t, err)
   110  
   111  	mod, err := r.InstantiateModule(ctx, proxyCompiled, config)
   112  	require.NoError(t, err)
   113  
   114  	return mod, r, &log
   115  }
   116  
   117  // requireErrnoNosys ensures a call of the given function returns errno. The log
   118  // message returned can verify the output is wasm `-->` or a host `==>`
   119  // function.
   120  func requireErrnoNosys(t *testing.T, funcName string, params ...uint64) string {
   121  	var log bytes.Buffer
   122  
   123  	// Set context to one that has an experimental listener
   124  	ctx := context.WithValue(testCtx, experimental.FunctionListenerFactoryKey{},
   125  		proxy.NewLoggingListenerFactory(&log, logging.LogScopeAll))
   126  
   127  	r := wazero.NewRuntime(ctx)
   128  	defer r.Close(ctx)
   129  
   130  	// Instantiate the wasi module.
   131  	wasiModuleCompiled, err := wasi_snapshot_preview1.NewBuilder(r).Compile(ctx)
   132  	require.NoError(t, err)
   133  
   134  	_, err = r.InstantiateModule(ctx, wasiModuleCompiled, wazero.NewModuleConfig())
   135  	require.NoError(t, err)
   136  
   137  	proxyBin := proxy.NewModuleBinary(wasi_snapshot_preview1.ModuleName, wasiModuleCompiled)
   138  
   139  	proxyCompiled, err := r.CompileModule(ctx, proxyBin)
   140  	require.NoError(t, err)
   141  
   142  	mod, err := r.InstantiateModule(ctx, proxyCompiled, wazero.NewModuleConfig())
   143  	require.NoError(t, err)
   144  
   145  	requireErrnoResult(t, wasip1.ErrnoNosys, mod, funcName, params...)
   146  	return "\n" + log.String()
   147  }
   148  
   149  func requireErrnoResult(t *testing.T, expectedErrno wasip1.Errno, mod api.Closer, funcName string, params ...uint64) {
   150  	results, err := mod.(api.Module).ExportedFunction(funcName).Call(testCtx, params...)
   151  	require.NoError(t, err)
   152  	errno := wasip1.Errno(results[0])
   153  	require.Equal(t, expectedErrno, errno, "want %s but have %s", wasip1.ErrnoName(expectedErrno), wasip1.ErrnoName(errno))
   154  }
   155  
   156  func newBlockingReader(t *testing.T) blockingReader {
   157  	timeout, cancelFunc := context.WithTimeout(testCtx, 5*time.Second)
   158  	t.Cleanup(cancelFunc)
   159  	return blockingReader{ctx: timeout}
   160  }
   161  
   162  // blockingReader is an io.Reader that never terminates its read
   163  // unless the embedded context is Done()
   164  type blockingReader struct {
   165  	ctx context.Context
   166  }
   167  
   168  // Read implements io.Reader
   169  func (b blockingReader) Read(buf []byte) (n int, err error) {
   170  	<-b.ctx.Done()
   171  	return 0, nil
   172  }