github.com/wasilibs/wazerox@v0.0.0-20240124024944-4923be63ab5f/builder_test.go (about) 1 package wazero 2 3 import ( 4 "context" 5 "testing" 6 7 "github.com/wasilibs/wazerox/api" 8 "github.com/wasilibs/wazerox/internal/testing/require" 9 "github.com/wasilibs/wazerox/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("host") 39 }, 40 expected: &wasm.Module{NameSection: &wasm.NameSection{ModuleName: "host"}}, 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("host"). 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 Exports: map[string]*wasm.Export{ 65 "1": {Name: "1", Type: wasm.ExternTypeFunc, Index: 0}, 66 }, 67 NameSection: &wasm.NameSection{ 68 FunctionNames: wasm.NameMap{{Index: 0, Name: "1"}}, 69 ModuleName: "host", 70 }, 71 }, 72 }, 73 { 74 name: "WithFunc WithName WithParameterNames", 75 input: func(r Runtime) HostModuleBuilder { 76 return r.NewHostModuleBuilder("host").NewFunctionBuilder(). 77 WithFunc(uint32_uint32). 78 WithName("get").WithParameterNames("x"). 79 Export("1") 80 }, 81 expected: &wasm.Module{ 82 TypeSection: []wasm.FunctionType{ 83 {Params: []api.ValueType{i32}, Results: []api.ValueType{i32}}, 84 }, 85 FunctionSection: []wasm.Index{0}, 86 CodeSection: []wasm.Code{wasm.MustParseGoReflectFuncCode(uint32_uint32)}, 87 ExportSection: []wasm.Export{ 88 {Name: "1", Type: wasm.ExternTypeFunc, Index: 0}, 89 }, 90 Exports: map[string]*wasm.Export{ 91 "1": {Name: "1", Type: wasm.ExternTypeFunc, Index: 0}, 92 }, 93 NameSection: &wasm.NameSection{ 94 FunctionNames: wasm.NameMap{{Index: 0, Name: "get"}}, 95 LocalNames: []wasm.NameMapAssoc{{Index: 0, NameMap: wasm.NameMap{{Index: 0, Name: "x"}}}}, 96 ModuleName: "host", 97 }, 98 }, 99 }, 100 { 101 name: "WithFunc WithName WithResultNames", 102 input: func(r Runtime) HostModuleBuilder { 103 return r.NewHostModuleBuilder("host").NewFunctionBuilder(). 104 WithFunc(uint32_uint32). 105 WithName("get").WithResultNames("x"). 106 Export("1") 107 }, 108 expected: &wasm.Module{ 109 TypeSection: []wasm.FunctionType{ 110 {Params: []api.ValueType{i32}, Results: []api.ValueType{i32}}, 111 }, 112 FunctionSection: []wasm.Index{0}, 113 CodeSection: []wasm.Code{wasm.MustParseGoReflectFuncCode(uint32_uint32)}, 114 ExportSection: []wasm.Export{ 115 {Name: "1", Type: wasm.ExternTypeFunc, Index: 0}, 116 }, 117 Exports: map[string]*wasm.Export{ 118 "1": {Name: "1", Type: wasm.ExternTypeFunc, Index: 0}, 119 }, 120 NameSection: &wasm.NameSection{ 121 FunctionNames: wasm.NameMap{{Index: 0, Name: "get"}}, 122 ResultNames: []wasm.NameMapAssoc{{Index: 0, NameMap: wasm.NameMap{{Index: 0, Name: "x"}}}}, 123 ModuleName: "host", 124 }, 125 }, 126 }, 127 { 128 name: "WithFunc overwrites existing", 129 input: func(r Runtime) HostModuleBuilder { 130 return r.NewHostModuleBuilder("host"). 131 NewFunctionBuilder().WithFunc(uint32_uint32).Export("1"). 132 NewFunctionBuilder().WithFunc(uint64_uint32).Export("1") 133 }, 134 expected: &wasm.Module{ 135 TypeSection: []wasm.FunctionType{ 136 {Params: []api.ValueType{i64}, Results: []api.ValueType{i32}}, 137 }, 138 FunctionSection: []wasm.Index{0}, 139 CodeSection: []wasm.Code{wasm.MustParseGoReflectFuncCode(uint64_uint32)}, 140 ExportSection: []wasm.Export{ 141 {Name: "1", Type: wasm.ExternTypeFunc, Index: 0}, 142 }, 143 Exports: map[string]*wasm.Export{ 144 "1": {Name: "1", Type: wasm.ExternTypeFunc, Index: 0}, 145 }, 146 NameSection: &wasm.NameSection{ 147 FunctionNames: wasm.NameMap{{Index: 0, Name: "1"}}, 148 ModuleName: "host", 149 }, 150 }, 151 }, 152 { 153 name: "WithFunc twice", 154 input: func(r Runtime) HostModuleBuilder { 155 // Intentionally out of order 156 return r.NewHostModuleBuilder("host"). 157 NewFunctionBuilder().WithFunc(uint64_uint32).Export("2"). 158 NewFunctionBuilder().WithFunc(uint32_uint32).Export("1") 159 }, 160 expected: &wasm.Module{ 161 TypeSection: []wasm.FunctionType{ 162 {Params: []api.ValueType{i64}, Results: []api.ValueType{i32}}, 163 {Params: []api.ValueType{i32}, Results: []api.ValueType{i32}}, 164 }, 165 FunctionSection: []wasm.Index{0, 1}, 166 CodeSection: []wasm.Code{wasm.MustParseGoReflectFuncCode(uint64_uint32), wasm.MustParseGoReflectFuncCode(uint32_uint32)}, 167 ExportSection: []wasm.Export{ 168 {Name: "2", Type: wasm.ExternTypeFunc, Index: 0}, 169 {Name: "1", Type: wasm.ExternTypeFunc, Index: 1}, 170 }, 171 Exports: map[string]*wasm.Export{ 172 "2": {Name: "2", Type: wasm.ExternTypeFunc, Index: 0}, 173 "1": {Name: "1", Type: wasm.ExternTypeFunc, Index: 1}, 174 }, 175 NameSection: &wasm.NameSection{ 176 FunctionNames: wasm.NameMap{{Index: 0, Name: "2"}, {Index: 1, Name: "1"}}, 177 ModuleName: "host", 178 }, 179 }, 180 }, 181 { 182 name: "WithGoFunction", 183 input: func(r Runtime) HostModuleBuilder { 184 return r.NewHostModuleBuilder("host"). 185 NewFunctionBuilder(). 186 WithGoFunction(gofunc1, []api.ValueType{i32}, []api.ValueType{i32}). 187 Export("1") 188 }, 189 expected: &wasm.Module{ 190 TypeSection: []wasm.FunctionType{ 191 {Params: []api.ValueType{i32}, Results: []api.ValueType{i32}}, 192 }, 193 FunctionSection: []wasm.Index{0}, 194 CodeSection: []wasm.Code{ 195 {GoFunc: gofunc1}, 196 }, 197 ExportSection: []wasm.Export{ 198 {Name: "1", Type: wasm.ExternTypeFunc, Index: 0}, 199 }, 200 Exports: map[string]*wasm.Export{ 201 "1": {Name: "1", Type: wasm.ExternTypeFunc, Index: 0}, 202 }, 203 NameSection: &wasm.NameSection{ 204 FunctionNames: wasm.NameMap{{Index: 0, Name: "1"}}, 205 ModuleName: "host", 206 }, 207 }, 208 }, 209 { 210 name: "WithGoFunction WithName WithParameterNames", 211 input: func(r Runtime) HostModuleBuilder { 212 return r.NewHostModuleBuilder("host").NewFunctionBuilder(). 213 WithGoFunction(gofunc1, []api.ValueType{i32}, []api.ValueType{i32}). 214 WithName("get").WithParameterNames("x"). 215 Export("1") 216 }, 217 expected: &wasm.Module{ 218 TypeSection: []wasm.FunctionType{ 219 {Params: []api.ValueType{i32}, Results: []api.ValueType{i32}}, 220 }, 221 FunctionSection: []wasm.Index{0}, 222 CodeSection: []wasm.Code{ 223 {GoFunc: gofunc1}, 224 }, 225 ExportSection: []wasm.Export{ 226 {Name: "1", Type: wasm.ExternTypeFunc, Index: 0}, 227 }, 228 Exports: map[string]*wasm.Export{ 229 "1": {Name: "1", Type: wasm.ExternTypeFunc, Index: 0}, 230 }, 231 NameSection: &wasm.NameSection{ 232 FunctionNames: wasm.NameMap{{Index: 0, Name: "get"}}, 233 LocalNames: []wasm.NameMapAssoc{{Index: 0, NameMap: wasm.NameMap{{Index: 0, Name: "x"}}}}, 234 ModuleName: "host", 235 }, 236 }, 237 }, 238 { 239 name: "WithGoFunction overwrites existing", 240 input: func(r Runtime) HostModuleBuilder { 241 return r.NewHostModuleBuilder("host"). 242 NewFunctionBuilder(). 243 WithGoFunction(gofunc1, []api.ValueType{i32}, []api.ValueType{i32}). 244 Export("1"). 245 NewFunctionBuilder(). 246 WithGoFunction(gofunc2, []api.ValueType{i64}, []api.ValueType{i32}). 247 Export("1") 248 }, 249 expected: &wasm.Module{ 250 TypeSection: []wasm.FunctionType{ 251 {Params: []api.ValueType{i64}, Results: []api.ValueType{i32}}, 252 }, 253 FunctionSection: []wasm.Index{0}, 254 CodeSection: []wasm.Code{ 255 {GoFunc: gofunc2}, 256 }, 257 ExportSection: []wasm.Export{ 258 {Name: "1", Type: wasm.ExternTypeFunc, Index: 0}, 259 }, 260 Exports: map[string]*wasm.Export{ 261 "1": {Name: "1", Type: wasm.ExternTypeFunc, Index: 0}, 262 }, 263 NameSection: &wasm.NameSection{ 264 FunctionNames: wasm.NameMap{{Index: 0, Name: "1"}}, 265 ModuleName: "host", 266 }, 267 }, 268 }, 269 { 270 name: "WithGoFunction twice", 271 input: func(r Runtime) HostModuleBuilder { 272 // Intentionally not in lexicographic order 273 return r.NewHostModuleBuilder("host"). 274 NewFunctionBuilder(). 275 WithGoFunction(gofunc2, []api.ValueType{i64}, []api.ValueType{i32}). 276 Export("2"). 277 NewFunctionBuilder(). 278 WithGoFunction(gofunc1, []api.ValueType{i32}, []api.ValueType{i32}). 279 Export("1") 280 }, 281 expected: &wasm.Module{ 282 TypeSection: []wasm.FunctionType{ 283 {Params: []api.ValueType{i64}, Results: []api.ValueType{i32}}, 284 {Params: []api.ValueType{i32}, Results: []api.ValueType{i32}}, 285 }, 286 FunctionSection: []wasm.Index{0, 1}, 287 CodeSection: []wasm.Code{ 288 {GoFunc: gofunc2}, 289 {GoFunc: gofunc1}, 290 }, 291 ExportSection: []wasm.Export{ 292 {Name: "2", Type: wasm.ExternTypeFunc, Index: 0}, 293 {Name: "1", Type: wasm.ExternTypeFunc, Index: 1}, 294 }, 295 Exports: map[string]*wasm.Export{ 296 "2": {Name: "2", Type: wasm.ExternTypeFunc, Index: 0}, 297 "1": {Name: "1", Type: wasm.ExternTypeFunc, Index: 1}, 298 }, 299 NameSection: &wasm.NameSection{ 300 FunctionNames: wasm.NameMap{{Index: 0, Name: "2"}, {Index: 1, Name: "1"}}, 301 ModuleName: "host", 302 }, 303 }, 304 }, 305 } 306 307 for _, tt := range tests { 308 tc := tt 309 310 t.Run(tc.name, func(t *testing.T) { 311 b := tc.input(NewRuntime(testCtx)).(*hostModuleBuilder) 312 compiled, err := b.Compile(testCtx) 313 require.NoError(t, err) 314 m := compiled.(*compiledModule) 315 316 requireHostModuleEquals(t, tc.expected, m.module) 317 318 require.Equal(t, b.r.store.Engine, m.compiledEngine) 319 320 // TypeIDs must be assigned to compiledModule. 321 expTypeIDs, err := b.r.store.GetFunctionTypeIDs(tc.expected.TypeSection) 322 require.NoError(t, err) 323 require.Equal(t, expTypeIDs, m.typeIDs) 324 325 // Built module must be instantiable by Engine. 326 mod, err := b.r.InstantiateModule(testCtx, m, NewModuleConfig()) 327 require.NoError(t, err) 328 329 // Closing the module shouldn't remove the compiler cache 330 require.NoError(t, mod.Close(testCtx)) 331 require.Equal(t, uint32(1), b.r.store.Engine.CompiledModuleCount()) 332 }) 333 } 334 } 335 336 // TestNewHostModuleBuilder_Compile_Errors only covers a few scenarios to avoid 337 // duplicating tests in internal/wasm/host_test.go 338 func TestNewHostModuleBuilder_Compile_Errors(t *testing.T) { 339 tests := []struct { 340 name string 341 input func(Runtime) HostModuleBuilder 342 expectedErr string 343 }{ 344 { 345 name: "error compiling", // should fail due to invalid param. 346 input: func(rt Runtime) HostModuleBuilder { 347 return rt.NewHostModuleBuilder("host").NewFunctionBuilder(). 348 WithFunc(&wasm.HostFunc{ExportName: "fn", Code: wasm.Code{GoFunc: func(string) {}}}). 349 Export("fn") 350 }, 351 expectedErr: `func[host.fn] param[0] is unsupported: string`, 352 }, 353 } 354 355 for _, tt := range tests { 356 tc := tt 357 358 t.Run(tc.name, func(t *testing.T) { 359 _, e := tc.input(NewRuntime(testCtx)).Compile(testCtx) 360 require.EqualError(t, e, tc.expectedErr) 361 }) 362 } 363 } 364 365 // TestNewHostModuleBuilder_Instantiate ensures Runtime.InstantiateModule is called on success. 366 func TestNewHostModuleBuilder_Instantiate(t *testing.T) { 367 r := NewRuntime(testCtx) 368 m, err := r.NewHostModuleBuilder("env").Instantiate(testCtx) 369 require.NoError(t, err) 370 371 // If this was instantiated, it would be added to the store under the same name 372 require.Equal(t, r.Module("env"), m) 373 374 // Closing the module should remove the compiler cache 375 require.NoError(t, m.Close(testCtx)) 376 require.Zero(t, r.(*runtime).store.Engine.CompiledModuleCount()) 377 } 378 379 // TestNewHostModuleBuilder_Instantiate_Errors ensures errors propagate from Runtime.InstantiateModule 380 func TestNewHostModuleBuilder_Instantiate_Errors(t *testing.T) { 381 r := NewRuntime(testCtx) 382 _, err := r.NewHostModuleBuilder("env").Instantiate(testCtx) 383 require.NoError(t, err) 384 385 _, err = r.NewHostModuleBuilder("env").Instantiate(testCtx) 386 require.EqualError(t, err, "module[env] has already been instantiated") 387 } 388 389 // requireHostModuleEquals is redefined from internal/wasm/host_test.go to avoid an import cycle extracting it. 390 func requireHostModuleEquals(t *testing.T, expected, actual *wasm.Module) { 391 // `require.Equal(t, expected, actual)` fails reflect pointers don't match, so brute compare: 392 for i := range expected.TypeSection { 393 tp := &expected.TypeSection[i] 394 tp.CacheNumInUint64() 395 // When creating the compiled module, we get the type IDs for types, which results in caching type keys. 396 _ = tp.String() 397 } 398 require.Equal(t, expected.TypeSection, actual.TypeSection) 399 require.Equal(t, expected.ImportSection, actual.ImportSection) 400 require.Equal(t, expected.FunctionSection, actual.FunctionSection) 401 require.Equal(t, expected.TableSection, actual.TableSection) 402 require.Equal(t, expected.MemorySection, actual.MemorySection) 403 require.Equal(t, expected.GlobalSection, actual.GlobalSection) 404 require.Equal(t, expected.ExportSection, actual.ExportSection) 405 require.Equal(t, expected.Exports, actual.Exports) 406 require.Equal(t, expected.StartSection, actual.StartSection) 407 require.Equal(t, expected.ElementSection, actual.ElementSection) 408 require.Equal(t, expected.DataSection, actual.DataSection) 409 require.Equal(t, expected.NameSection, actual.NameSection) 410 411 // Special case because reflect.Value can't be compared with Equals 412 // TODO: This is copy/paste with /internal/wasm/host_test.go 413 require.Equal(t, len(expected.CodeSection), len(actual.CodeSection)) 414 for i, c := range expected.CodeSection { 415 actualCode := actual.CodeSection[i] 416 require.Equal(t, c.GoFunc, actualCode.GoFunc) 417 418 // Not wasm 419 require.Nil(t, actualCode.Body) 420 require.Nil(t, actualCode.LocalTypes) 421 } 422 }