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