wa-lang.org/wazero@v1.0.2/builder_test.go (about)

     1  package wazero
     2  
     3  import (
     4  	"context"
     5  	"testing"
     6  
     7  	"wa-lang.org/wazero/api"
     8  	"wa-lang.org/wazero/internal/testing/require"
     9  	"wa-lang.org/wazero/internal/wasm"
    10  )
    11  
    12  // TestNewHostModuleBuilder_Compile only covers a few scenarios to avoid duplicating tests in internal/wasm/host_test.go
    13  func TestNewHostModuleBuilder_Compile(t *testing.T) {
    14  	i32, i64 := api.ValueTypeI32, api.ValueTypeI64
    15  
    16  	uint32_uint32 := func(context.Context, uint32) uint32 {
    17  		return 0
    18  	}
    19  	uint64_uint32 := func(context.Context, uint64) uint32 {
    20  		return 0
    21  	}
    22  
    23  	gofunc1 := api.GoFunc(func(ctx context.Context, stack []uint64) {
    24  		stack[0] = 0
    25  	})
    26  	gofunc2 := api.GoFunc(func(ctx context.Context, stack []uint64) {
    27  		stack[0] = 0
    28  	})
    29  
    30  	tests := []struct {
    31  		name     string
    32  		input    func(Runtime) HostModuleBuilder
    33  		expected *wasm.Module
    34  	}{
    35  		{
    36  			name: "empty",
    37  			input: func(r Runtime) HostModuleBuilder {
    38  				return r.NewHostModuleBuilder("")
    39  			},
    40  			expected: &wasm.Module{},
    41  		},
    42  		{
    43  			name: "only name",
    44  			input: func(r Runtime) HostModuleBuilder {
    45  				return r.NewHostModuleBuilder("env")
    46  			},
    47  			expected: &wasm.Module{NameSection: &wasm.NameSection{ModuleName: "env"}},
    48  		},
    49  		{
    50  			name: "WithFunc",
    51  			input: func(r Runtime) HostModuleBuilder {
    52  				return r.NewHostModuleBuilder("").
    53  					NewFunctionBuilder().WithFunc(uint32_uint32).Export("1")
    54  			},
    55  			expected: &wasm.Module{
    56  				TypeSection: []*wasm.FunctionType{
    57  					{Params: []api.ValueType{i32}, Results: []api.ValueType{i32}},
    58  				},
    59  				FunctionSection: []wasm.Index{0},
    60  				CodeSection:     []*wasm.Code{wasm.MustParseGoReflectFuncCode(uint32_uint32)},
    61  				ExportSection: []*wasm.Export{
    62  					{Name: "1", Type: wasm.ExternTypeFunc, Index: 0},
    63  				},
    64  				NameSection: &wasm.NameSection{
    65  					FunctionNames: wasm.NameMap{{Index: 0, Name: "1"}},
    66  				},
    67  			},
    68  		},
    69  		{
    70  			name: "WithFunc WithName WithParameterNames",
    71  			input: func(r Runtime) HostModuleBuilder {
    72  				return r.NewHostModuleBuilder("").NewFunctionBuilder().
    73  					WithFunc(uint32_uint32).
    74  					WithName("get").WithParameterNames("x").
    75  					Export("1")
    76  			},
    77  			expected: &wasm.Module{
    78  				TypeSection: []*wasm.FunctionType{
    79  					{Params: []api.ValueType{i32}, Results: []api.ValueType{i32}},
    80  				},
    81  				FunctionSection: []wasm.Index{0},
    82  				CodeSection:     []*wasm.Code{wasm.MustParseGoReflectFuncCode(uint32_uint32)},
    83  				ExportSection: []*wasm.Export{
    84  					{Name: "1", Type: wasm.ExternTypeFunc, Index: 0},
    85  				},
    86  				NameSection: &wasm.NameSection{
    87  					FunctionNames: wasm.NameMap{{Index: 0, Name: "get"}},
    88  					LocalNames:    []*wasm.NameMapAssoc{{Index: 0, NameMap: wasm.NameMap{{Index: 0, Name: "x"}}}},
    89  				},
    90  			},
    91  		},
    92  		{
    93  			name: "WithFunc overwrites existing",
    94  			input: func(r Runtime) HostModuleBuilder {
    95  				return r.NewHostModuleBuilder("").
    96  					NewFunctionBuilder().WithFunc(uint32_uint32).Export("1").
    97  					NewFunctionBuilder().WithFunc(uint64_uint32).Export("1")
    98  			},
    99  			expected: &wasm.Module{
   100  				TypeSection: []*wasm.FunctionType{
   101  					{Params: []api.ValueType{i64}, Results: []api.ValueType{i32}},
   102  				},
   103  				FunctionSection: []wasm.Index{0},
   104  				CodeSection:     []*wasm.Code{wasm.MustParseGoReflectFuncCode(uint64_uint32)},
   105  				ExportSection: []*wasm.Export{
   106  					{Name: "1", Type: wasm.ExternTypeFunc, Index: 0},
   107  				},
   108  				NameSection: &wasm.NameSection{
   109  					FunctionNames: wasm.NameMap{{Index: 0, Name: "1"}},
   110  				},
   111  			},
   112  		},
   113  		{
   114  			name: "WithFunc twice",
   115  			input: func(r Runtime) HostModuleBuilder {
   116  				// Intentionally out of order
   117  				return r.NewHostModuleBuilder("").
   118  					NewFunctionBuilder().WithFunc(uint64_uint32).Export("2").
   119  					NewFunctionBuilder().WithFunc(uint32_uint32).Export("1")
   120  			},
   121  			expected: &wasm.Module{
   122  				TypeSection: []*wasm.FunctionType{
   123  					{Params: []api.ValueType{i32}, Results: []api.ValueType{i32}},
   124  					{Params: []api.ValueType{i64}, Results: []api.ValueType{i32}},
   125  				},
   126  				FunctionSection: []wasm.Index{0, 1},
   127  				CodeSection:     []*wasm.Code{wasm.MustParseGoReflectFuncCode(uint32_uint32), wasm.MustParseGoReflectFuncCode(uint64_uint32)},
   128  				ExportSection: []*wasm.Export{
   129  					{Name: "1", Type: wasm.ExternTypeFunc, Index: 0},
   130  					{Name: "2", Type: wasm.ExternTypeFunc, Index: 1},
   131  				},
   132  				NameSection: &wasm.NameSection{
   133  					FunctionNames: wasm.NameMap{{Index: 0, Name: "1"}, {Index: 1, Name: "2"}},
   134  				},
   135  			},
   136  		},
   137  		{
   138  			name: "WithGoFunction",
   139  			input: func(r Runtime) HostModuleBuilder {
   140  				return r.NewHostModuleBuilder("").
   141  					NewFunctionBuilder().
   142  					WithGoFunction(gofunc1, []api.ValueType{i32}, []api.ValueType{i32}).
   143  					Export("1")
   144  			},
   145  			expected: &wasm.Module{
   146  				TypeSection: []*wasm.FunctionType{
   147  					{Params: []api.ValueType{i32}, Results: []api.ValueType{i32}},
   148  				},
   149  				FunctionSection: []wasm.Index{0},
   150  				CodeSection: []*wasm.Code{
   151  					{IsHostFunction: true, GoFunc: gofunc1},
   152  				},
   153  				ExportSection: []*wasm.Export{
   154  					{Name: "1", Type: wasm.ExternTypeFunc, Index: 0},
   155  				},
   156  				NameSection: &wasm.NameSection{
   157  					FunctionNames: wasm.NameMap{{Index: 0, Name: "1"}},
   158  				},
   159  			},
   160  		},
   161  		{
   162  			name: "WithGoFunction WithName WithParameterNames",
   163  			input: func(r Runtime) HostModuleBuilder {
   164  				return r.NewHostModuleBuilder("").NewFunctionBuilder().
   165  					WithGoFunction(gofunc1, []api.ValueType{i32}, []api.ValueType{i32}).
   166  					WithName("get").WithParameterNames("x").
   167  					Export("1")
   168  			},
   169  			expected: &wasm.Module{
   170  				TypeSection: []*wasm.FunctionType{
   171  					{Params: []api.ValueType{i32}, Results: []api.ValueType{i32}},
   172  				},
   173  				FunctionSection: []wasm.Index{0},
   174  				CodeSection: []*wasm.Code{
   175  					{IsHostFunction: true, GoFunc: gofunc1},
   176  				},
   177  				ExportSection: []*wasm.Export{
   178  					{Name: "1", Type: wasm.ExternTypeFunc, Index: 0},
   179  				},
   180  				NameSection: &wasm.NameSection{
   181  					FunctionNames: wasm.NameMap{{Index: 0, Name: "get"}},
   182  					LocalNames:    []*wasm.NameMapAssoc{{Index: 0, NameMap: wasm.NameMap{{Index: 0, Name: "x"}}}},
   183  				},
   184  			},
   185  		},
   186  		{
   187  			name: "WithGoFunction overwrites existing",
   188  			input: func(r Runtime) HostModuleBuilder {
   189  				return r.NewHostModuleBuilder("").
   190  					NewFunctionBuilder().
   191  					WithGoFunction(gofunc1, []api.ValueType{i32}, []api.ValueType{i32}).
   192  					Export("1").
   193  					NewFunctionBuilder().
   194  					WithGoFunction(gofunc2, []api.ValueType{i64}, []api.ValueType{i32}).
   195  					Export("1")
   196  			},
   197  			expected: &wasm.Module{
   198  				TypeSection: []*wasm.FunctionType{
   199  					{Params: []api.ValueType{i64}, Results: []api.ValueType{i32}},
   200  				},
   201  				FunctionSection: []wasm.Index{0},
   202  				CodeSection: []*wasm.Code{
   203  					{IsHostFunction: true, GoFunc: gofunc2},
   204  				},
   205  				ExportSection: []*wasm.Export{
   206  					{Name: "1", Type: wasm.ExternTypeFunc, Index: 0},
   207  				},
   208  				NameSection: &wasm.NameSection{
   209  					FunctionNames: wasm.NameMap{{Index: 0, Name: "1"}},
   210  				},
   211  			},
   212  		},
   213  		{
   214  			name: "WithGoFunction twice",
   215  			input: func(r Runtime) HostModuleBuilder {
   216  				// Intentionally out of order
   217  				return r.NewHostModuleBuilder("").
   218  					NewFunctionBuilder().
   219  					WithGoFunction(gofunc2, []api.ValueType{i64}, []api.ValueType{i32}).
   220  					Export("2").
   221  					NewFunctionBuilder().
   222  					WithGoFunction(gofunc1, []api.ValueType{i32}, []api.ValueType{i32}).
   223  					Export("1")
   224  			},
   225  			expected: &wasm.Module{
   226  				TypeSection: []*wasm.FunctionType{
   227  					{Params: []api.ValueType{i32}, Results: []api.ValueType{i32}},
   228  					{Params: []api.ValueType{i64}, Results: []api.ValueType{i32}},
   229  				},
   230  				FunctionSection: []wasm.Index{0, 1},
   231  				CodeSection: []*wasm.Code{
   232  					{IsHostFunction: true, GoFunc: gofunc1},
   233  					{IsHostFunction: true, GoFunc: gofunc2},
   234  				},
   235  				ExportSection: []*wasm.Export{
   236  					{Name: "1", Type: wasm.ExternTypeFunc, Index: 0},
   237  					{Name: "2", Type: wasm.ExternTypeFunc, Index: 1},
   238  				},
   239  				NameSection: &wasm.NameSection{
   240  					FunctionNames: wasm.NameMap{{Index: 0, Name: "1"}, {Index: 1, Name: "2"}},
   241  				},
   242  			},
   243  		},
   244  	}
   245  
   246  	for _, tt := range tests {
   247  		tc := tt
   248  
   249  		t.Run(tc.name, func(t *testing.T) {
   250  			b := tc.input(NewRuntime(testCtx)).(*hostModuleBuilder)
   251  			compiled, err := b.Compile(testCtx)
   252  			require.NoError(t, err)
   253  			m := compiled.(*compiledModule)
   254  
   255  			requireHostModuleEquals(t, tc.expected, m.module)
   256  
   257  			require.Equal(t, b.r.store.Engine, m.compiledEngine)
   258  
   259  			// Built module must be instantiable by Engine.
   260  			mod, err := b.r.InstantiateModule(testCtx, m, NewModuleConfig())
   261  			require.NoError(t, err)
   262  
   263  			// Closing the module shouldn't remove the compiler cache
   264  			require.NoError(t, mod.Close(testCtx))
   265  			require.Equal(t, uint32(1), b.r.store.Engine.CompiledModuleCount())
   266  		})
   267  	}
   268  }
   269  
   270  // TestNewHostModuleBuilder_Compile_Errors only covers a few scenarios to avoid
   271  // duplicating tests in internal/wasm/host_test.go
   272  func TestNewHostModuleBuilder_Compile_Errors(t *testing.T) {
   273  	tests := []struct {
   274  		name        string
   275  		input       func(Runtime) HostModuleBuilder
   276  		expectedErr string
   277  	}{
   278  		{
   279  			name: "error compiling", // should fail due to missing result.
   280  			input: func(rt Runtime) HostModuleBuilder {
   281  				return rt.NewHostModuleBuilder("").NewFunctionBuilder().
   282  					WithFunc(&wasm.HostFunc{
   283  						ExportNames: []string{"fn"},
   284  						ResultTypes: []wasm.ValueType{wasm.ValueTypeI32},
   285  						Code:        &wasm.Code{IsHostFunction: true, Body: []byte{wasm.OpcodeEnd}},
   286  					}).Export("fn")
   287  			},
   288  			expectedErr: `invalid function[0] export["fn"]: not enough results
   289  	have ()
   290  	want (i32)`,
   291  		},
   292  	}
   293  
   294  	for _, tt := range tests {
   295  		tc := tt
   296  
   297  		t.Run(tc.name, func(t *testing.T) {
   298  			_, e := tc.input(NewRuntime(testCtx)).Compile(testCtx)
   299  			require.EqualError(t, e, tc.expectedErr)
   300  		})
   301  	}
   302  }
   303  
   304  // TestNewHostModuleBuilder_Instantiate ensures Runtime.InstantiateModule is called on success.
   305  func TestNewHostModuleBuilder_Instantiate(t *testing.T) {
   306  	r := NewRuntime(testCtx)
   307  	m, err := r.NewHostModuleBuilder("env").Instantiate(testCtx, r)
   308  	require.NoError(t, err)
   309  
   310  	// If this was instantiated, it would be added to the store under the same name
   311  	require.Equal(t, r.(*runtime).ns.Module("env"), m)
   312  
   313  	// Closing the module should remove the compiler cache
   314  	require.NoError(t, m.Close(testCtx))
   315  	require.Zero(t, r.(*runtime).store.Engine.CompiledModuleCount())
   316  }
   317  
   318  // TestNewHostModuleBuilder_Instantiate_Errors ensures errors propagate from Runtime.InstantiateModule
   319  func TestNewHostModuleBuilder_Instantiate_Errors(t *testing.T) {
   320  	r := NewRuntime(testCtx)
   321  	_, err := r.NewHostModuleBuilder("env").Instantiate(testCtx, r)
   322  	require.NoError(t, err)
   323  
   324  	_, err = r.NewHostModuleBuilder("env").Instantiate(testCtx, r)
   325  	require.EqualError(t, err, "module[env] has already been instantiated")
   326  }
   327  
   328  // requireHostModuleEquals is redefined from internal/wasm/host_test.go to avoid an import cycle extracting it.
   329  func requireHostModuleEquals(t *testing.T, expected, actual *wasm.Module) {
   330  	// `require.Equal(t, expected, actual)` fails reflect pointers don't match, so brute compare:
   331  	for _, tp := range expected.TypeSection {
   332  		tp.CacheNumInUint64()
   333  	}
   334  	require.Equal(t, expected.TypeSection, actual.TypeSection)
   335  	require.Equal(t, expected.ImportSection, actual.ImportSection)
   336  	require.Equal(t, expected.FunctionSection, actual.FunctionSection)
   337  	require.Equal(t, expected.TableSection, actual.TableSection)
   338  	require.Equal(t, expected.MemorySection, actual.MemorySection)
   339  	require.Equal(t, expected.GlobalSection, actual.GlobalSection)
   340  	require.Equal(t, expected.ExportSection, actual.ExportSection)
   341  	require.Equal(t, expected.StartSection, actual.StartSection)
   342  	require.Equal(t, expected.ElementSection, actual.ElementSection)
   343  	require.Equal(t, expected.DataSection, actual.DataSection)
   344  	require.Equal(t, expected.NameSection, actual.NameSection)
   345  
   346  	// Special case because reflect.Value can't be compared with Equals
   347  	// TODO: This is copy/paste with /internal/wasm/host_test.go
   348  	require.Equal(t, len(expected.CodeSection), len(actual.CodeSection))
   349  	for i, c := range expected.CodeSection {
   350  		actualCode := actual.CodeSection[i]
   351  		require.True(t, actualCode.IsHostFunction)
   352  		require.Equal(t, c.GoFunc, actualCode.GoFunc)
   353  
   354  		// Not wasm
   355  		require.Nil(t, actualCode.Body)
   356  		require.Nil(t, actualCode.LocalTypes)
   357  	}
   358  }