wa-lang.org/wazero@v1.0.2/imports/assemblyscript/assemblyscript_test.go (about)

     1  package assemblyscript
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	_ "embed"
     7  	"encoding/hex"
     8  	"errors"
     9  	"io"
    10  	"strings"
    11  	"testing"
    12  	"testing/iotest"
    13  	"unicode/utf16"
    14  
    15  	"wa-lang.org/wazero"
    16  	"wa-lang.org/wazero/api"
    17  	. "wa-lang.org/wazero/experimental"
    18  	"wa-lang.org/wazero/experimental/logging"
    19  	"wa-lang.org/wazero/internal/testing/proxy"
    20  	"wa-lang.org/wazero/internal/testing/require"
    21  	"wa-lang.org/wazero/internal/u64"
    22  	"wa-lang.org/wazero/internal/wasm"
    23  	"wa-lang.org/wazero/sys"
    24  )
    25  
    26  // testCtx is an arbitrary, non-default context. Non-nil also prevents linter errors.
    27  var testCtx = context.WithValue(context.Background(), struct{}{}, "arbitrary")
    28  
    29  func TestAbort(t *testing.T) {
    30  	tests := []struct {
    31  		name     string
    32  		exporter FunctionExporter
    33  		expected string
    34  	}{
    35  		{
    36  			name:     "enabled",
    37  			exporter: NewFunctionExporter(),
    38  			expected: "message at filename:1:2\n",
    39  		},
    40  		{
    41  			name:     "disabled",
    42  			exporter: NewFunctionExporter().WithAbortMessageDisabled(),
    43  			expected: "",
    44  		},
    45  	}
    46  
    47  	for _, tt := range tests {
    48  		tc := tt
    49  
    50  		t.Run(tc.name, func(t *testing.T) {
    51  			var stderr bytes.Buffer
    52  			mod, r, log := requireProxyModule(t, tc.exporter, wazero.NewModuleConfig().WithStderr(&stderr))
    53  			defer r.Close(testCtx)
    54  
    55  			messageOff, filenameOff := writeAbortMessageAndFileName(t, mod.Memory(), encodeUTF16("message"), encodeUTF16("filename"))
    56  
    57  			_, err := mod.ExportedFunction(functionAbort).
    58  				Call(testCtx, uint64(messageOff), uint64(filenameOff), uint64(1), uint64(2))
    59  			require.Error(t, err)
    60  			sysErr, ok := err.(*sys.ExitError)
    61  			require.True(t, ok, err)
    62  			require.Equal(t, uint32(255), sysErr.ExitCode())
    63  			require.Equal(t, `
    64  --> proxy.abort(message=4,fileName=22,lineNumber=1,columnNumber=2)
    65  	==> env.~lib/builtins/abort(message=4,fileName=22,lineNumber=1,columnNumber=2)
    66  `, "\n"+log.String())
    67  
    68  			require.Equal(t, tc.expected, stderr.String())
    69  		})
    70  	}
    71  }
    72  
    73  func TestAbort_Error(t *testing.T) {
    74  	var stderr bytes.Buffer
    75  	mod, r, log := requireProxyModule(t, NewFunctionExporter(), wazero.NewModuleConfig().WithStderr(&stderr))
    76  	defer r.Close(testCtx)
    77  
    78  	tests := []struct {
    79  		name          string
    80  		messageUTF16  []byte
    81  		fileNameUTF16 []byte
    82  		expectedLog   string
    83  	}{
    84  		{
    85  			name:          "bad message",
    86  			messageUTF16:  encodeUTF16("message")[:5],
    87  			fileNameUTF16: encodeUTF16("filename"),
    88  			expectedLog: `
    89  --> proxy.abort(message=4,fileName=13,lineNumber=1,columnNumber=2)
    90  	==> env.~lib/builtins/abort(message=4,fileName=13,lineNumber=1,columnNumber=2)
    91  `,
    92  		},
    93  		{
    94  			name:          "bad filename",
    95  			messageUTF16:  encodeUTF16("message"),
    96  			fileNameUTF16: encodeUTF16("filename")[:5],
    97  			expectedLog: `
    98  --> proxy.abort(message=4,fileName=22,lineNumber=1,columnNumber=2)
    99  	==> env.~lib/builtins/abort(message=4,fileName=22,lineNumber=1,columnNumber=2)
   100  `,
   101  		},
   102  	}
   103  
   104  	for _, tt := range tests {
   105  		tc := tt
   106  
   107  		t.Run(tc.name, func(t *testing.T) {
   108  			defer log.Reset()
   109  			defer stderr.Reset()
   110  
   111  			messageOff, filenameOff := writeAbortMessageAndFileName(t, mod.Memory(), tc.messageUTF16, tc.fileNameUTF16)
   112  
   113  			_, err := mod.ExportedFunction(functionAbort).
   114  				Call(testCtx, uint64(messageOff), uint64(filenameOff), uint64(1), uint64(2))
   115  			require.Error(t, err)
   116  			sysErr, ok := err.(*sys.ExitError)
   117  			require.True(t, ok, err)
   118  			require.Equal(t, uint32(255), sysErr.ExitCode())
   119  			require.Equal(t, tc.expectedLog, "\n"+log.String())
   120  
   121  			require.Equal(t, "", stderr.String()) // nothing output if strings fail to read.
   122  		})
   123  	}
   124  }
   125  
   126  func TestSeed(t *testing.T) {
   127  	mod, r, log := requireProxyModule(t, NewFunctionExporter(), wazero.NewModuleConfig())
   128  	defer r.Close(testCtx)
   129  
   130  	ret, err := mod.ExportedFunction(functionSeed).Call(testCtx)
   131  	require.NoError(t, err)
   132  	require.Equal(t, `
   133  --> proxy.seed()
   134  	==> env.~lib/builtins/seed()
   135  	<== (4.958153677776298e-175)
   136  <-- (4.958153677776298e-175)
   137  `, "\n"+log.String())
   138  
   139  	require.Equal(t, "538c7f96b164bf1b", hex.EncodeToString(u64.LeBytes(ret[0])))
   140  }
   141  
   142  func TestSeed_error(t *testing.T) {
   143  	tests := []struct {
   144  		name        string
   145  		source      io.Reader
   146  		expectedErr string
   147  	}{
   148  		{
   149  			name:   "not 8 bytes",
   150  			source: bytes.NewReader([]byte{0, 1}),
   151  			expectedErr: `error reading random seed: unexpected EOF (recovered by wazero)
   152  wasm stack trace:
   153  	env.~lib/builtins/seed() f64
   154  	proxy.seed() f64`,
   155  		},
   156  		{
   157  			name:   "error reading",
   158  			source: iotest.ErrReader(errors.New("ice cream")),
   159  			expectedErr: `error reading random seed: ice cream (recovered by wazero)
   160  wasm stack trace:
   161  	env.~lib/builtins/seed() f64
   162  	proxy.seed() f64`,
   163  		},
   164  	}
   165  
   166  	for _, tt := range tests {
   167  		tc := tt
   168  
   169  		t.Run(tc.name, func(t *testing.T) {
   170  			mod, r, log := requireProxyModule(t, NewFunctionExporter(), wazero.NewModuleConfig().WithRandSource(tc.source))
   171  			defer r.Close(testCtx)
   172  
   173  			_, err := mod.ExportedFunction(functionSeed).Call(testCtx)
   174  			require.EqualError(t, err, tc.expectedErr)
   175  			require.Equal(t, `
   176  --> proxy.seed()
   177  	==> env.~lib/builtins/seed()
   178  `, "\n"+log.String())
   179  		})
   180  	}
   181  }
   182  
   183  // TestFunctionExporter_Trace ensures the trace output is according to configuration.
   184  func TestFunctionExporter_Trace(t *testing.T) {
   185  	noArgs := []uint64{4, 0, 0, 0, 0, 0, 0}
   186  	noArgsLog := `
   187  --> proxy.trace(message=4,nArgs=0,arg0=0,arg1=0,arg2=0,arg3=0,arg4=0)
   188  	==> env.~lib/builtins/trace(message=4,nArgs=0,arg0=0,arg1=0,arg2=0,arg3=0,arg4=0)
   189  	<== ()
   190  <-- ()
   191  `
   192  
   193  	tests := []struct {
   194  		name                  string
   195  		exporter              FunctionExporter
   196  		params                []uint64
   197  		message               []byte
   198  		outErr                bool
   199  		expected, expectedLog string
   200  	}{
   201  		{
   202  			name:     "disabled",
   203  			exporter: NewFunctionExporter(),
   204  			params:   noArgs,
   205  			expected: "",
   206  			// expect no host call since it is disabled. ==> is host and --> is wasm.
   207  			expectedLog: strings.ReplaceAll(noArgsLog, "==", "--"),
   208  		},
   209  		{
   210  			name:        "ToStderr",
   211  			exporter:    NewFunctionExporter().WithTraceToStderr(),
   212  			params:      noArgs,
   213  			expected:    "trace: hello\n",
   214  			expectedLog: noArgsLog,
   215  		},
   216  		{
   217  			name:        "ToStdout - no args",
   218  			exporter:    NewFunctionExporter().WithTraceToStdout(),
   219  			params:      noArgs,
   220  			expected:    "trace: hello\n",
   221  			expectedLog: noArgsLog,
   222  		},
   223  		{
   224  			name:     "ToStdout - one arg",
   225  			exporter: NewFunctionExporter().WithTraceToStdout(),
   226  			params:   []uint64{4, 1, api.EncodeF64(1), 0, 0, 0, 0},
   227  			expected: "trace: hello 1\n",
   228  			expectedLog: `
   229  --> proxy.trace(message=4,nArgs=1,arg0=1,arg1=0,arg2=0,arg3=0,arg4=0)
   230  	==> env.~lib/builtins/trace(message=4,nArgs=1,arg0=1,arg1=0,arg2=0,arg3=0,arg4=0)
   231  	<== ()
   232  <-- ()
   233  `,
   234  		},
   235  		{
   236  			name:     "ToStdout - two args",
   237  			exporter: NewFunctionExporter().WithTraceToStdout(),
   238  			params:   []uint64{4, 2, api.EncodeF64(1), api.EncodeF64(2), 0, 0, 0},
   239  			expected: "trace: hello 1,2\n",
   240  			expectedLog: `
   241  --> proxy.trace(message=4,nArgs=2,arg0=1,arg1=2,arg2=0,arg3=0,arg4=0)
   242  	==> env.~lib/builtins/trace(message=4,nArgs=2,arg0=1,arg1=2,arg2=0,arg3=0,arg4=0)
   243  	<== ()
   244  <-- ()
   245  `,
   246  		},
   247  		{
   248  			name:     "ToStdout - five args",
   249  			exporter: NewFunctionExporter().WithTraceToStdout(),
   250  			params: []uint64{
   251  				4,
   252  				5,
   253  				api.EncodeF64(1),
   254  				api.EncodeF64(2),
   255  				api.EncodeF64(3.3),
   256  				api.EncodeF64(4.4),
   257  				api.EncodeF64(5),
   258  			},
   259  			expected: "trace: hello 1,2,3.3,4.4,5\n",
   260  			expectedLog: `
   261  --> proxy.trace(message=4,nArgs=5,arg0=1,arg1=2,arg2=3.3,arg3=4.4,arg4=5)
   262  	==> env.~lib/builtins/trace(message=4,nArgs=5,arg0=1,arg1=2,arg2=3.3,arg3=4.4,arg4=5)
   263  	<== ()
   264  <-- ()
   265  `,
   266  		},
   267  		{
   268  			name:        "not 8 bytes",
   269  			exporter:    NewFunctionExporter().WithTraceToStderr(),
   270  			message:     encodeUTF16("hello")[:5],
   271  			params:      noArgs,
   272  			expectedLog: noArgsLog,
   273  		},
   274  		{
   275  			name:        "error writing",
   276  			exporter:    NewFunctionExporter().WithTraceToStderr(),
   277  			outErr:      true,
   278  			params:      noArgs,
   279  			expectedLog: noArgsLog,
   280  		},
   281  	}
   282  
   283  	for _, tt := range tests {
   284  		tc := tt
   285  
   286  		t.Run(tc.name, func(t *testing.T) {
   287  			var out bytes.Buffer
   288  
   289  			config := wazero.NewModuleConfig()
   290  			if strings.Contains("ToStderr", tc.name) {
   291  				config = config.WithStderr(&out)
   292  			} else {
   293  				config = config.WithStdout(&out)
   294  			}
   295  			if tc.outErr {
   296  				config = config.WithStderr(&errWriter{err: errors.New("ice cream")})
   297  			}
   298  
   299  			mod, r, log := requireProxyModule(t, tc.exporter, config)
   300  			defer r.Close(testCtx)
   301  
   302  			message := tc.message
   303  			if message == nil {
   304  				message = encodeUTF16("hello")
   305  			}
   306  			ok := mod.Memory().WriteUint32Le(testCtx, 0, uint32(len(message)))
   307  			require.True(t, ok)
   308  			ok = mod.Memory().Write(testCtx, uint32(4), message)
   309  			require.True(t, ok)
   310  
   311  			_, err := mod.ExportedFunction(functionTrace).Call(testCtx, tc.params...)
   312  			require.NoError(t, err)
   313  			require.Equal(t, tc.expected, out.String())
   314  			require.Equal(t, tc.expectedLog, "\n"+log.String())
   315  		})
   316  	}
   317  }
   318  
   319  func Test_readAssemblyScriptString(t *testing.T) {
   320  	tests := []struct {
   321  		name       string
   322  		memory     func(context.Context, api.Memory)
   323  		offset     int
   324  		expected   string
   325  		expectedOk bool
   326  	}{
   327  		{
   328  			name: "success",
   329  			memory: func(testCtx context.Context, memory api.Memory) {
   330  				memory.WriteUint32Le(testCtx, 0, 10)
   331  				b := encodeUTF16("hello")
   332  				memory.Write(testCtx, 4, b)
   333  			},
   334  			offset:     4,
   335  			expected:   "hello",
   336  			expectedOk: true,
   337  		},
   338  		{
   339  			name: "can't read size",
   340  			memory: func(testCtx context.Context, memory api.Memory) {
   341  				b := encodeUTF16("hello")
   342  				memory.Write(testCtx, 0, b)
   343  			},
   344  			offset:     0, // will attempt to read size from offset -4
   345  			expectedOk: false,
   346  		},
   347  		{
   348  			name: "odd size",
   349  			memory: func(testCtx context.Context, memory api.Memory) {
   350  				memory.WriteUint32Le(testCtx, 0, 9)
   351  				b := encodeUTF16("hello")
   352  				memory.Write(testCtx, 4, b)
   353  			},
   354  			offset:     4,
   355  			expectedOk: false,
   356  		},
   357  		{
   358  			name: "can't read string",
   359  			memory: func(testCtx context.Context, memory api.Memory) {
   360  				memory.WriteUint32Le(testCtx, 0, 10_000_000) // set size to too large value
   361  				b := encodeUTF16("hello")
   362  				memory.Write(testCtx, 4, b)
   363  			},
   364  			offset:     4,
   365  			expectedOk: false,
   366  		},
   367  	}
   368  
   369  	for _, tt := range tests {
   370  		tc := tt
   371  
   372  		t.Run(tc.name, func(t *testing.T) {
   373  			mem := wasm.NewMemoryInstance(&wasm.Memory{Min: 1, Cap: 1, Max: 1})
   374  			tc.memory(testCtx, mem)
   375  
   376  			s, ok := readAssemblyScriptString(testCtx, mem, uint32(tc.offset))
   377  			require.Equal(t, tc.expectedOk, ok)
   378  			require.Equal(t, tc.expected, s)
   379  		})
   380  	}
   381  }
   382  
   383  func writeAbortMessageAndFileName(t *testing.T, mem api.Memory, messageUTF16, fileNameUTF16 []byte) (uint32, uint32) {
   384  	off := uint32(0)
   385  	ok := mem.WriteUint32Le(testCtx, off, uint32(len(messageUTF16)))
   386  	require.True(t, ok)
   387  	off += 4
   388  	messageOff := off
   389  	ok = mem.Write(testCtx, off, messageUTF16)
   390  	require.True(t, ok)
   391  	off += uint32(len(messageUTF16))
   392  	ok = mem.WriteUint32Le(testCtx, off, uint32(len(fileNameUTF16)))
   393  	require.True(t, ok)
   394  	off += 4
   395  	filenameOff := off
   396  	ok = mem.Write(testCtx, off, fileNameUTF16)
   397  	require.True(t, ok)
   398  	return messageOff, filenameOff
   399  }
   400  
   401  func encodeUTF16(s string) []byte {
   402  	runes := utf16.Encode([]rune(s))
   403  	b := make([]byte, len(runes)*2)
   404  	for i, r := range runes {
   405  		b[i*2] = byte(r)
   406  		b[i*2+1] = byte(r >> 8)
   407  	}
   408  	return b
   409  }
   410  
   411  type errWriter struct {
   412  	err error
   413  }
   414  
   415  func (w *errWriter) Write([]byte) (int, error) {
   416  	return 0, w.err
   417  }
   418  
   419  func requireProxyModule(t *testing.T, fns FunctionExporter, config wazero.ModuleConfig) (api.Module, api.Closer, *bytes.Buffer) {
   420  	var log bytes.Buffer
   421  
   422  	// Set context to one that has an experimental listener
   423  	ctx := context.WithValue(testCtx, FunctionListenerFactoryKey{}, logging.NewLoggingListenerFactory(&log))
   424  
   425  	r := wazero.NewRuntime(ctx)
   426  
   427  	builder := r.NewHostModuleBuilder("env")
   428  	fns.ExportFunctions(builder)
   429  
   430  	envCompiled, err := builder.Compile(ctx)
   431  	require.NoError(t, err)
   432  
   433  	_, err = r.InstantiateModule(ctx, envCompiled, config)
   434  	require.NoError(t, err)
   435  
   436  	proxyBin := proxy.GetProxyModuleBinary("env", envCompiled)
   437  
   438  	proxyCompiled, err := r.CompileModule(ctx, proxyBin)
   439  	require.NoError(t, err)
   440  
   441  	mod, err := r.InstantiateModule(ctx, proxyCompiled, config)
   442  	require.NoError(t, err)
   443  	return mod, r, &log
   444  }