package wazero import ( "bytes" "context" _ "embed" "testing" "time" "github.com/tetratelabs/wazero/api" experimentalsys "github.com/tetratelabs/wazero/experimental/sys" "github.com/tetratelabs/wazero/internal/fstest" "github.com/tetratelabs/wazero/internal/platform" internalsys "github.com/tetratelabs/wazero/internal/sys" "github.com/tetratelabs/wazero/internal/sysfs" testfs "github.com/tetratelabs/wazero/internal/testing/fs" "github.com/tetratelabs/wazero/internal/testing/require" "github.com/tetratelabs/wazero/internal/wasm" "github.com/tetratelabs/wazero/sys" ) func TestRuntimeConfig(t *testing.T) { tests := []struct { name string with func(RuntimeConfig) RuntimeConfig expected RuntimeConfig }{ { name: "features", with: func(c RuntimeConfig) RuntimeConfig { return c.WithCoreFeatures(api.CoreFeaturesV1) }, expected: &runtimeConfig{ enabledFeatures: api.CoreFeaturesV1, }, }, { name: "memoryLimitPages", with: func(c RuntimeConfig) RuntimeConfig { return c.WithMemoryLimitPages(10) }, expected: &runtimeConfig{ memoryLimitPages: 10, }, }, { name: "memoryCapacityFromMax", with: func(c RuntimeConfig) RuntimeConfig { return c.WithMemoryCapacityFromMax(true) }, expected: &runtimeConfig{ memoryCapacityFromMax: true, }, }, { name: "WithDebugInfoEnabled", with: func(c RuntimeConfig) RuntimeConfig { return c.WithDebugInfoEnabled(false) }, expected: &runtimeConfig{ dwarfDisabled: true, // dwarf is a more technical name and ok here. }, }, { name: "WithCustomSections", with: func(c RuntimeConfig) RuntimeConfig { return c.WithCustomSections(true) }, expected: &runtimeConfig{ storeCustomSections: true, }, }, { name: "WithCloseOnContextDone", with: func(c RuntimeConfig) RuntimeConfig { return c.WithCloseOnContextDone(true) }, expected: &runtimeConfig{ensureTermination: true}, }, } for _, tt := range tests { tc := tt t.Run(tc.name, func(t *testing.T) { input := &runtimeConfig{} rc := tc.with(input) require.Equal(t, tc.expected, rc) // The source wasn't modified require.Equal(t, &runtimeConfig{}, input) }) } t.Run("memoryLimitPages invalid panics", func(t *testing.T) { err := require.CapturePanic(func() { input := &runtimeConfig{} input.WithMemoryLimitPages(wasm.MemoryLimitPages + 1) }) require.EqualError(t, err, "memoryLimitPages invalid: 65537 > 65536") }) } func TestModuleConfig(t *testing.T) { tests := []struct { name string with func(ModuleConfig) ModuleConfig expectNameSet bool expectedName string }{ { name: "WithName default", with: func(c ModuleConfig) ModuleConfig { return c }, expectNameSet: false, expectedName: "", }, { name: "WithName", with: func(c ModuleConfig) ModuleConfig { return c.WithName("wazero") }, expectNameSet: true, expectedName: "wazero", }, { name: "WithName empty", with: func(c ModuleConfig) ModuleConfig { return c.WithName("") }, expectNameSet: true, expectedName: "", }, { name: "WithName twice", with: func(c ModuleConfig) ModuleConfig { return c.WithName("wazero").WithName("wa0") }, expectNameSet: true, expectedName: "wa0", }, { name: "WithName can clear", with: func(c ModuleConfig) ModuleConfig { return c.WithName("wazero").WithName("") }, expectNameSet: true, expectedName: "", }, } for _, tt := range tests { tc := tt t.Run(tc.name, func(t *testing.T) { input := NewModuleConfig() rc := tc.with(input) require.Equal(t, tc.expectNameSet, rc.(*moduleConfig).nameSet) require.Equal(t, tc.expectedName, rc.(*moduleConfig).name) // The source wasn't modified require.Equal(t, NewModuleConfig(), input) }) } } // TestModuleConfig_toSysContext only tests the cases that change the inputs to // sys.NewContext. func TestModuleConfig_toSysContext(t *testing.T) { base := NewModuleConfig() tests := []struct { name string input func() (mc ModuleConfig, verify func(t *testing.T, sys *internalsys.Context)) }{ { name: "empty", input: func() (ModuleConfig, func(t *testing.T, sys *internalsys.Context)) { return base, func(t *testing.T, sys *internalsys.Context) { require.NotNil(t, sys) } }, }, { name: "WithNanotime", input: func() (ModuleConfig, func(t *testing.T, sys *internalsys.Context)) { config := base.WithNanotime(func() int64 { return 1234567 }, 54321) return config, func(t *testing.T, sys *internalsys.Context) { require.Equal(t, 1234567, int(sys.Nanotime())) require.Equal(t, 54321, int(sys.NanotimeResolution())) } }, }, { name: "WithSysNanotime", input: func() (ModuleConfig, func(t *testing.T, sys *internalsys.Context)) { config := base.WithSysNanotime() return config, func(t *testing.T, sys *internalsys.Context) { require.Equal(t, int(1), int(sys.NanotimeResolution())) } }, }, { name: "WithWalltime", input: func() (ModuleConfig, func(t *testing.T, sys *internalsys.Context)) { config := base.WithWalltime(func() (sec int64, nsec int32) { return 5, 10 }, 54321) return config, func(t *testing.T, sys *internalsys.Context) { actualSec, actualNano := sys.Walltime() require.Equal(t, 5, int(actualSec)) require.Equal(t, 10, int(actualNano)) require.Equal(t, 54321, int(sys.WalltimeResolution())) } }, }, { name: "WithSysWalltime", input: func() (ModuleConfig, func(t *testing.T, sys *internalsys.Context)) { config := base.WithSysWalltime() return config, func(t *testing.T, sys *internalsys.Context) { require.Equal(t, int(time.Microsecond.Nanoseconds()), int(sys.WalltimeResolution())) } }, }, { name: "WithArgs empty", input: func() (ModuleConfig, func(t *testing.T, sys *internalsys.Context)) { config := base.WithArgs() return config, func(t *testing.T, sys *internalsys.Context) { args := sys.Args() require.Equal(t, 0, len(args)) } }, }, { name: "WithArgs", input: func() (ModuleConfig, func(t *testing.T, sys *internalsys.Context)) { config := base.WithArgs("a", "bc") return config, func(t *testing.T, sys *internalsys.Context) { args := sys.Args() require.Equal(t, 2, len(args)) require.Equal(t, "a", string(args[0])) require.Equal(t, "bc", string(args[1])) } }, }, { name: "WithArgs empty ok", // Particularly argv[0] can be empty, and we have no rules about others. input: func() (ModuleConfig, func(t *testing.T, sys *internalsys.Context)) { config := base.WithArgs("", "bc") return config, func(t *testing.T, sys *internalsys.Context) { args := sys.Args() require.Equal(t, 2, len(args)) require.Equal(t, "", string(args[0])) require.Equal(t, "bc", string(args[1])) } }, }, { name: "WithArgs second call overwrites", input: func() (ModuleConfig, func(t *testing.T, sys *internalsys.Context)) { config := base.WithArgs("a", "bc").WithArgs("bc", "a") return config, func(t *testing.T, sys *internalsys.Context) { args := sys.Args() require.Equal(t, 2, len(args)) require.Equal(t, "bc", string(args[0])) require.Equal(t, "a", string(args[1])) } }, }, { name: "WithEnv", input: func() (ModuleConfig, func(t *testing.T, sys *internalsys.Context)) { config := base.WithEnv("a", "b") return config, func(t *testing.T, sys *internalsys.Context) { envs := sys.Environ() require.Equal(t, 1, len(envs)) require.Equal(t, "a=b", string(envs[0])) } }, }, { name: "WithEnv empty value", input: func() (ModuleConfig, func(t *testing.T, sys *internalsys.Context)) { config := base.WithEnv("a", "") return config, func(t *testing.T, sys *internalsys.Context) { envs := sys.Environ() require.Equal(t, 1, len(envs)) require.Equal(t, "a=", string(envs[0])) } }, }, { name: "WithEnv twice", input: func() (ModuleConfig, func(t *testing.T, sys *internalsys.Context)) { config := base.WithEnv("a", "b").WithEnv("c", "de") return config, func(t *testing.T, sys *internalsys.Context) { envs := sys.Environ() require.Equal(t, 2, len(envs)) require.Equal(t, "a=b", string(envs[0])) require.Equal(t, "c=de", string(envs[1])) } }, }, { name: "WithEnv overwrites", input: func() (ModuleConfig, func(t *testing.T, sys *internalsys.Context)) { config := base.WithEnv("a", "bc").WithEnv("c", "de").WithEnv("a", "ff") return config, func(t *testing.T, sys *internalsys.Context) { envs := sys.Environ() require.Equal(t, 2, len(envs)) require.Equal(t, "a=ff", string(envs[0])) require.Equal(t, "c=de", string(envs[1])) } }, }, { name: "WithEnv twice", input: func() (ModuleConfig, func(t *testing.T, sys *internalsys.Context)) { config := base.WithEnv("a", "b").WithEnv("c", "de") return config, func(t *testing.T, sys *internalsys.Context) { envs := sys.Environ() require.Equal(t, 2, len(envs)) require.Equal(t, "a=b", string(envs[0])) require.Equal(t, "c=de", string(envs[1])) } }, }, { name: "WithFS", input: func() (ModuleConfig, func(t *testing.T, sys *internalsys.Context)) { testFS := &testfs.FS{} config := base.WithFS(testFS) return config, func(t *testing.T, sys *internalsys.Context) { rootfs := sys.FS().RootFS() require.Equal(t, &sysfs.AdaptFS{FS: testFS}, rootfs) } }, }, { name: "WithFS overwrites", input: func() (ModuleConfig, func(t *testing.T, sys *internalsys.Context)) { testFS, testFS2 := &testfs.FS{}, &testfs.FS{} config := base.WithFS(testFS).WithFS(testFS2) return config, func(t *testing.T, sys *internalsys.Context) { rootfs := sys.FS().RootFS() require.Equal(t, &sysfs.AdaptFS{FS: testFS2}, rootfs) } }, }, { name: "WithFS nil", input: func() (ModuleConfig, func(t *testing.T, sys *internalsys.Context)) { config := base.WithFS(nil) return config, func(t *testing.T, sys *internalsys.Context) { rootfs := sys.FS().RootFS() require.Equal(t, experimentalsys.UnimplementedFS{}, rootfs) } }, }, { name: "WithRandSource", input: func() (ModuleConfig, func(t *testing.T, sys *internalsys.Context)) { r := bytes.NewReader([]byte{1, 2, 3, 4}) config := base.WithRandSource(r) return config, func(t *testing.T, sys *internalsys.Context) { actual := sys.RandSource() require.Equal(t, r, actual) } }, }, { name: "WithRandSource nil", input: func() (ModuleConfig, func(t *testing.T, sys *internalsys.Context)) { config := base.WithRandSource(nil) return config, func(t *testing.T, sys *internalsys.Context) { actual := sys.RandSource() require.Equal(t, platform.NewFakeRandSource(), actual) } }, }, } for _, tt := range tests { tc := tt t.Run(tc.name, func(t *testing.T) { config, verify := tc.input() actual, err := config.(*moduleConfig).toSysContext() require.NoError(t, err) verify(t, actual) }) } } // TestModuleConfig_toSysContext_WithWalltime has to test differently because we can't // compare function pointers when functions are passed by value. func TestModuleConfig_toSysContext_WithWalltime(t *testing.T) { tests := []struct { name string input ModuleConfig expectedSec int64 expectedNsec int32 expectedResolution sys.ClockResolution expectedErr string }{ { name: "ok", input: NewModuleConfig(). WithWalltime(func() (sec int64, nsec int32) { return 1, 2 }, 3), expectedSec: 1, expectedNsec: 2, expectedResolution: 3, }, { name: "overwrites", input: NewModuleConfig(). WithWalltime(func() (sec int64, nsec int32) { return 3, 4 }, 5). WithWalltime(func() (sec int64, nsec int32) { return 1, 2 }, 3), expectedSec: 1, expectedNsec: 2, expectedResolution: 3, }, { name: "invalid resolution", input: NewModuleConfig(). WithWalltime(func() (sec int64, nsec int32) { return 1, 2 }, 0), expectedErr: "invalid Walltime resolution: 0", }, } for _, tt := range tests { tc := tt t.Run(tc.name, func(t *testing.T) { sysCtx, err := tc.input.(*moduleConfig).toSysContext() if tc.expectedErr == "" { require.Nil(t, err) sec, nsec := sysCtx.Walltime() require.Equal(t, tc.expectedSec, sec) require.Equal(t, tc.expectedNsec, nsec) require.Equal(t, tc.expectedResolution, sysCtx.WalltimeResolution()) } else { require.EqualError(t, err, tc.expectedErr) } }) } t.Run("context", func(t *testing.T) { sysCtx, err := NewModuleConfig(). WithWalltime(func() (sec int64, nsec int32) { return 1, 2 }, 3).(*moduleConfig).toSysContext() require.NoError(t, err) sec, nsec := sysCtx.Walltime() // If below pass, the context was correct! require.Equal(t, int64(1), sec) require.Equal(t, int32(2), nsec) }) } // TestModuleConfig_toSysContext_WithNanotime has to test differently because we can't // compare function pointers when functions are passed by value. func TestModuleConfig_toSysContext_WithNanotime(t *testing.T) { tests := []struct { name string input ModuleConfig expectedNanos int64 expectedResolution sys.ClockResolution expectedErr string }{ { name: "ok", input: NewModuleConfig(). WithNanotime(func() int64 { return 1 }, 2), expectedNanos: 1, expectedResolution: 2, }, { name: "overwrites", input: NewModuleConfig(). WithNanotime(func() int64 { return 3 }, 4). WithNanotime(func() int64 { return 1 }, 2), expectedNanos: 1, expectedResolution: 2, }, { name: "invalid resolution", input: NewModuleConfig(). WithNanotime(func() int64 { return 1 }, 0), expectedErr: "invalid Nanotime resolution: 0", }, } for _, tt := range tests { tc := tt t.Run(tc.name, func(t *testing.T) { sysCtx, err := tc.input.(*moduleConfig).toSysContext() if tc.expectedErr == "" { require.Nil(t, err) nanos := sysCtx.Nanotime() require.Equal(t, tc.expectedNanos, nanos) require.Equal(t, tc.expectedResolution, sysCtx.NanotimeResolution()) } else { require.EqualError(t, err, tc.expectedErr) } }) } } // TestModuleConfig_toSysContext_WithNanosleep has to test differently because // we can't compare function pointers when functions are passed by value. func TestModuleConfig_toSysContext_WithNanosleep(t *testing.T) { sysCtx, err := NewModuleConfig(). WithNanosleep(func(ns int64) { require.Equal(t, int64(2), ns) }).(*moduleConfig).toSysContext() require.NoError(t, err) sysCtx.Nanosleep(2) } // TestModuleConfig_toSysContext_WithOsyield has to test differently because // we can't compare function pointers when functions are passed by value. func TestModuleConfig_toSysContext_WithOsyield(t *testing.T) { var yielded bool sysCtx, err := NewModuleConfig(). WithOsyield(func() { yielded = true }).(*moduleConfig).toSysContext() require.NoError(t, err) sysCtx.Osyield() require.True(t, yielded) } func TestModuleConfig_toSysContext_Errors(t *testing.T) { tests := []struct { name string input ModuleConfig expectedErr string }{ { name: "WithArgs arg contains NUL", input: NewModuleConfig().WithArgs("", string([]byte{'a', 0})), expectedErr: "args invalid: contains NUL character", }, { name: "WithEnv key contains NUL", input: NewModuleConfig().WithEnv(string([]byte{'a', 0}), "a"), expectedErr: "environ invalid: contains NUL character", }, { name: "WithEnv value contains NUL", input: NewModuleConfig().WithEnv("a", string([]byte{'a', 0})), expectedErr: "environ invalid: contains NUL character", }, { name: "WithEnv key contains equals", input: NewModuleConfig().WithEnv("a=", "a"), expectedErr: "environ invalid: key contains '=' character", }, { name: "WithEnv empty key", input: NewModuleConfig().WithEnv("", "a"), expectedErr: "environ invalid: empty key", }, } for _, tt := range tests { tc := tt t.Run(tc.name, func(t *testing.T) { _, err := tc.input.(*moduleConfig).toSysContext() require.EqualError(t, err, tc.expectedErr) }) } } func TestModuleConfig_clone(t *testing.T) { mc := NewModuleConfig().(*moduleConfig) cloned := mc.clone() // Make post-clone changes mc.fsConfig = NewFSConfig().WithFSMount(fstest.FS, "/") mc.environKeys["2"] = 2 cloned.environKeys["1"] = 1 // Ensure the maps are not shared require.Equal(t, map[string]int{"2": 2}, mc.environKeys) require.Equal(t, map[string]int{"1": 1}, cloned.environKeys) // Ensure the fs is not shared require.Nil(t, cloned.fsConfig) } func Test_compiledModule_Name(t *testing.T) { tests := []struct { name string input *compiledModule expected string }{ { name: "no name section", input: &compiledModule{module: &wasm.Module{}}, }, { name: "empty name", input: &compiledModule{module: &wasm.Module{NameSection: &wasm.NameSection{}}}, }, { name: "name", input: &compiledModule{module: &wasm.Module{NameSection: &wasm.NameSection{ModuleName: "foo"}}}, expected: "foo", }, } for _, tt := range tests { tc := tt t.Run(tc.name, func(t *testing.T) { require.Equal(t, tc.expected, tc.input.Name()) }) } } func Test_compiledModule_CustomSections(t *testing.T) { tests := []struct { name string input *compiledModule expected []string }{ { name: "no custom section", input: &compiledModule{module: &wasm.Module{}}, expected: []string{}, }, { name: "name", input: &compiledModule{module: &wasm.Module{ CustomSections: []*wasm.CustomSection{ {Name: "custom1"}, {Name: "custom2"}, {Name: "customDup"}, {Name: "customDup"}, }, }}, expected: []string{ "custom1", "custom2", "customDup", "customDup", }, }, } for _, tt := range tests { tc := tt t.Run(tc.name, func(t *testing.T) { customSections := tc.input.CustomSections() require.Equal(t, len(tc.expected), len(customSections)) for i := 0; i < len(tc.expected); i++ { require.Equal(t, tc.expected[i], customSections[i].Name()) } }) } } func Test_compiledModule_Close(t *testing.T) { for _, ctx := range []context.Context{nil, testCtx} { // Ensure it doesn't crash on nil! e := &mockEngine{name: "1", cachedModules: map[*wasm.Module]struct{}{}} var cs []*compiledModule for i := 0; i < 10; i++ { m := &wasm.Module{} err := e.CompileModule(ctx, m, nil, false) require.NoError(t, err) cs = append(cs, &compiledModule{module: m, compiledEngine: e}) } // Before Close. require.Equal(t, 10, len(e.cachedModules)) for _, c := range cs { require.NoError(t, c.Close(ctx)) } // After Close. require.Zero(t, len(e.cachedModules)) } } func TestNewRuntimeConfig(t *testing.T) { c, ok := NewRuntimeConfig().(*runtimeConfig) require.True(t, ok) // Should be cloned from the source. require.NotEqual(t, engineLessConfig, c) // Ensures if the correct engine is selected. if platform.CompilerSupported() { require.Equal(t, engineKindCompiler, c.engineKind) } else { require.Equal(t, engineKindInterpreter, c.engineKind) } }