Files
wazero/config_test.go
Crypt Keeper 9414b0bb74 Switches default random source to deterministic (#736)
This changes the default random source to provide deterministic values
similar to how nanotime and walltime do. This also prevents any worries
about if wasm can deplete the host's underlying source of entropy.

Signed-off-by: Adrian Cole <adrian@tetrate.io>
2022-08-06 17:47:50 +08:00

812 lines
21 KiB
Go

package wazero
import (
"context"
"crypto/rand"
"io"
"io/fs"
"math"
"reflect"
"testing"
"testing/fstest"
internalsys "github.com/tetratelabs/wazero/internal/sys"
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: "bulk-memory-operations",
with: func(c RuntimeConfig) RuntimeConfig {
return c.WithFeatureBulkMemoryOperations(true)
},
expected: &runtimeConfig{
enabledFeatures: wasm.FeatureBulkMemoryOperations | wasm.FeatureReferenceTypes,
},
},
{
name: "multi-value",
with: func(c RuntimeConfig) RuntimeConfig {
return c.WithFeatureMultiValue(true)
},
expected: &runtimeConfig{
enabledFeatures: wasm.FeatureMultiValue,
},
},
{
name: "mutable-global",
with: func(c RuntimeConfig) RuntimeConfig {
return c.WithFeatureMutableGlobal(true)
},
expected: &runtimeConfig{
enabledFeatures: wasm.FeatureMutableGlobal,
},
},
{
name: "nontrapping-float-to-int-conversion",
with: func(c RuntimeConfig) RuntimeConfig {
return c.WithFeatureNonTrappingFloatToIntConversion(true)
},
expected: &runtimeConfig{
enabledFeatures: wasm.FeatureNonTrappingFloatToIntConversion,
},
},
{
name: "sign-extension-ops",
with: func(c RuntimeConfig) RuntimeConfig {
return c.WithFeatureSignExtensionOps(true)
},
expected: &runtimeConfig{
enabledFeatures: wasm.FeatureSignExtensionOps,
},
},
{
name: "REC-wasm-core-1-20191205",
with: func(c RuntimeConfig) RuntimeConfig {
return c.WithFeatureSignExtensionOps(true).WithWasmCore1()
},
expected: &runtimeConfig{
enabledFeatures: wasm.Features20191205,
},
},
{
name: "WD-wasm-core-2-20220419",
with: func(c RuntimeConfig) RuntimeConfig {
return c.WithFeatureMutableGlobal(false).WithWasmCore2()
},
expected: &runtimeConfig{
enabledFeatures: wasm.Features20220419,
},
},
{
name: "reference-types",
with: func(c RuntimeConfig) RuntimeConfig {
return c.WithFeatureReferenceTypes(true)
},
expected: &runtimeConfig{
enabledFeatures: wasm.FeatureBulkMemoryOperations | wasm.FeatureReferenceTypes,
},
},
{
name: "simd",
with: func(c RuntimeConfig) RuntimeConfig {
return c.WithFeatureSIMD(true)
},
expected: &runtimeConfig{
enabledFeatures: wasm.FeatureSIMD,
},
},
}
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)
})
}
}
func TestRuntimeConfig_FeatureToggle(t *testing.T) {
tests := []struct {
name string
feature wasm.Features
expectDefault bool
setFeature func(RuntimeConfig, bool) RuntimeConfig
}{
{
name: "bulk-memory-operations",
feature: wasm.FeatureBulkMemoryOperations,
expectDefault: false,
setFeature: func(c RuntimeConfig, v bool) RuntimeConfig {
return c.WithFeatureBulkMemoryOperations(v)
},
},
{
name: "multi-value",
feature: wasm.FeatureMultiValue,
expectDefault: false,
setFeature: func(c RuntimeConfig, v bool) RuntimeConfig {
return c.WithFeatureMultiValue(v)
},
},
{
name: "mutable-global",
feature: wasm.FeatureMutableGlobal,
expectDefault: true,
setFeature: func(c RuntimeConfig, v bool) RuntimeConfig {
return c.WithFeatureMutableGlobal(v)
},
},
{
name: "nontrapping-float-to-int-conversion",
feature: wasm.FeatureNonTrappingFloatToIntConversion,
expectDefault: false,
setFeature: func(c RuntimeConfig, v bool) RuntimeConfig {
return c.WithFeatureNonTrappingFloatToIntConversion(v)
},
},
{
name: "sign-extension-ops",
feature: wasm.FeatureSignExtensionOps,
expectDefault: false,
setFeature: func(c RuntimeConfig, v bool) RuntimeConfig {
return c.WithFeatureSignExtensionOps(v)
},
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
c := NewRuntimeConfig().(*runtimeConfig)
require.Equal(t, tc.expectDefault, c.enabledFeatures.Get(tc.feature))
// Set to false even if it was initially false.
c = tc.setFeature(c, false).(*runtimeConfig)
require.False(t, c.enabledFeatures.Get(tc.feature))
// Set true makes it true
c = tc.setFeature(c, true).(*runtimeConfig)
require.True(t, c.enabledFeatures.Get(tc.feature))
// Set false makes it false again
c = tc.setFeature(c, false).(*runtimeConfig)
require.False(t, c.enabledFeatures.Get(tc.feature))
})
}
}
func TestCompileConfig(t *testing.T) {
mp := func(minPages uint32, maxPages *uint32) (min, capacity, max uint32) {
return 0, 1, 1
}
tests := []struct {
name string
with func(CompileConfig) CompileConfig
expected *compileConfig
}{
{
name: "WithMemorySizer",
with: func(c CompileConfig) CompileConfig {
return c.WithMemorySizer(mp)
},
expected: &compileConfig{memorySizer: mp},
},
{
name: "WithMemorySizer twice",
with: func(c CompileConfig) CompileConfig {
return c.WithMemorySizer(wasm.MemorySizer).WithMemorySizer(mp)
},
expected: &compileConfig{memorySizer: mp},
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
input := &compileConfig{}
rc := tc.with(input).(*compileConfig)
// We cannot compare func, but we can compare reflect.Value
// See https://go.dev/ref/spec#Comparison_operators
require.Equal(t, reflect.ValueOf(tc.expected.memorySizer), reflect.ValueOf(rc.memorySizer))
// The source wasn't modified
require.Equal(t, &compileConfig{}, input)
})
}
}
func TestModuleConfig(t *testing.T) {
tests := []struct {
name string
with func(ModuleConfig) ModuleConfig
expected string
}{
{
name: "WithName",
with: func(c ModuleConfig) ModuleConfig {
return c.WithName("wazero")
},
expected: "wazero",
},
{
name: "WithName empty",
with: func(c ModuleConfig) ModuleConfig {
return c.WithName("")
},
},
{
name: "WithName twice",
with: func(c ModuleConfig) ModuleConfig {
return c.WithName("wazero").WithName("wa0")
},
expected: "wa0",
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
input := NewModuleConfig()
rc := tc.with(input)
require.Equal(t, tc.expected, 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) {
// Always assigns clocks so that pointers are constant.
var wt sys.Walltime = func(context.Context) (int64, int32) {
return 0, 0
}
var nt sys.Nanotime = func(context.Context) int64 {
return 0
}
base := NewModuleConfig()
base.(*moduleConfig).walltime = &wt
base.(*moduleConfig).walltimeResolution = 1
base.(*moduleConfig).nanotime = &nt
base.(*moduleConfig).nanotimeResolution = 1
testFS := testfs.FS{}
testFS2 := testfs.FS{"/": &testfs.File{}}
tests := []struct {
name string
input ModuleConfig
expected *internalsys.Context
}{
{
name: "empty",
input: base,
expected: requireSysContext(t,
math.MaxUint32, // max
nil, // args
nil, // environ
nil, // stdin
nil, // stdout
nil, // stderr
nil, // randSource
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution
nil, // nanosleep
nil, // fs
),
},
{
name: "WithArgs",
input: base.WithArgs("a", "bc"),
expected: requireSysContext(t,
math.MaxUint32, // max
[]string{"a", "bc"}, // args
nil, // environ
nil, // stdin
nil, // stdout
nil, // stderr
nil, // randSource
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution
nil, // nanosleep
nil, // fs
),
},
{
name: "WithArgs empty ok", // Particularly argv[0] can be empty, and we have no rules about others.
input: base.WithArgs("", "bc"),
expected: requireSysContext(t,
math.MaxUint32, // max
[]string{"", "bc"}, // args
nil, // environ
nil, // stdin
nil, // stdout
nil, // stderr
nil, // randSource
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution
nil, // nanosleep
nil, // fs
),
},
{
name: "WithArgs second call overwrites",
input: base.WithArgs("a", "bc").WithArgs("bc", "a"),
expected: requireSysContext(t,
math.MaxUint32, // max
[]string{"bc", "a"}, // args
nil, // environ
nil, // stdin
nil, // stdout
nil, // stderr
nil, // randSource
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution
nil, // nanosleep
nil, // fs
),
},
{
name: "WithEnv",
input: base.WithEnv("a", "b"),
expected: requireSysContext(t,
math.MaxUint32, // max
nil, // args
[]string{"a=b"}, // environ
nil, // stdin
nil, // stdout
nil, // stderr
nil, // randSource
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution
nil, // nanosleep
nil, // fs
),
},
{
name: "WithEnv empty value",
input: base.WithEnv("a", ""),
expected: requireSysContext(t,
math.MaxUint32, // max
nil, // args
[]string{"a="}, // environ
nil, // stdin
nil, // stdout
nil, // stderr
nil, // randSource
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution
nil, // nanosleep
nil, // fs
),
},
{
name: "WithEnv twice",
input: base.WithEnv("a", "b").WithEnv("c", "de"),
expected: requireSysContext(t,
math.MaxUint32, // max
nil, // args
[]string{"a=b", "c=de"}, // environ
nil, // stdin
nil, // stdout
nil, // stderr
nil, // randSource
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution
nil, // nanosleep
nil, // fs
),
},
{
name: "WithEnv overwrites",
input: base.WithEnv("a", "bc").WithEnv("c", "de").WithEnv("a", "de"),
expected: requireSysContext(t,
math.MaxUint32, // max
nil, // args
[]string{"a=de", "c=de"}, // environ
nil, // stdin
nil, // stdout
nil, // stderr
nil, // randSource
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution
nil, // nanosleep
nil, // fs
),
},
{
name: "WithEnv twice",
input: base.WithEnv("a", "b").WithEnv("c", "de"),
expected: requireSysContext(t,
math.MaxUint32, // max
nil, // args
[]string{"a=b", "c=de"}, // environ
nil, // stdin
nil, // stdout
nil, // stderr
nil, // randSource
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution
nil, // nanosleep
nil, // fs
),
},
{
name: "WithFS",
input: base.WithFS(testFS),
expected: requireSysContext(t,
math.MaxUint32, // max
nil, // args
nil, // environ
nil, // stdin
nil, // stdout
nil, // stderr
nil, // randSource
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution
nil, // nanosleep
testFS,
),
},
{
name: "WithFS overwrites",
input: base.WithFS(testFS).WithFS(testFS2),
expected: requireSysContext(t,
math.MaxUint32, // max
nil, // args
nil, // environ
nil, // stdin
nil, // stdout
nil, // stderr
nil, // randSource
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution
nil, // nanosleep
testFS2, // fs
),
},
{
name: "WithRandSource",
input: base.WithRandSource(rand.Reader),
expected: requireSysContext(t,
math.MaxUint32, // max
nil, // args
nil, // environ
nil, // stdin
nil, // stdout
nil, // stderr
rand.Reader, // randSource
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution
nil, // nanosleep
nil, // fs
),
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
sysCtx, err := tc.input.(*moduleConfig).toSysContext()
require.NoError(t, err)
require.Equal(t, tc.expected, sysCtx)
})
}
}
// 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(context.Context) (sec int64, nsec int32) {
return 1, 2
}, 3),
expectedSec: 1,
expectedNsec: 2,
expectedResolution: 3,
},
{
name: "overwrites",
input: NewModuleConfig().
WithWalltime(func(context.Context) (sec int64, nsec int32) {
return 3, 4
}, 5).
WithWalltime(func(context.Context) (sec int64, nsec int32) {
return 1, 2
}, 3),
expectedSec: 1,
expectedNsec: 2,
expectedResolution: 3,
},
{
name: "invalid resolution",
input: NewModuleConfig().
WithWalltime(func(context.Context) (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(testCtx)
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(ctx context.Context) (sec int64, nsec int32) {
require.Equal(t, testCtx, ctx)
return 1, 2
}, 3).(*moduleConfig).toSysContext()
require.NoError(t, err)
sec, nsec := sysCtx.Walltime(testCtx)
// 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(context.Context) int64 {
return 1
}, 2),
expectedNanos: 1,
expectedResolution: 2,
},
{
name: "overwrites",
input: NewModuleConfig().
WithNanotime(func(context.Context) int64 {
return 3
}, 4).
WithNanotime(func(context.Context) int64 {
return 1
}, 2),
expectedNanos: 1,
expectedResolution: 2,
},
{
name: "invalid resolution",
input: NewModuleConfig().
WithNanotime(func(context.Context) 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(testCtx)
require.Equal(t, tc.expectedNanos, nanos)
require.Equal(t, tc.expectedResolution, sysCtx.NanotimeResolution())
} else {
require.EqualError(t, err, tc.expectedErr)
}
})
}
t.Run("context", func(t *testing.T) {
sysCtx, err := NewModuleConfig().
WithNanotime(func(ctx context.Context) int64 {
require.Equal(t, testCtx, ctx)
return 1
}, 2).(*moduleConfig).toSysContext()
require.NoError(t, err)
// If below pass, the context was correct!
require.Equal(t, int64(1), sysCtx.Nanotime(testCtx))
})
}
// 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(ctx context.Context, ns int64) {
require.Equal(t, testCtx, ctx)
}).(*moduleConfig).toSysContext()
require.NoError(t, err)
// If below pass, the context was correct!
sysCtx.Nanosleep(testCtx, 2)
}
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.fs = fstest.MapFS{}
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.fs)
}
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_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)
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))
}
}
// requireSysContext ensures wasm.NewContext doesn't return an error, which makes it usable in test matrices.
func requireSysContext(
t *testing.T,
max uint32,
args, environ []string,
stdin io.Reader,
stdout, stderr io.Writer,
randSource io.Reader,
walltime *sys.Walltime, walltimeResolution sys.ClockResolution,
nanotime *sys.Nanotime, nanotimeResolution sys.ClockResolution,
nanosleep *sys.Nanosleep,
fs fs.FS,
) *internalsys.Context {
sysCtx, err := internalsys.NewContext(
max,
args,
environ,
stdin,
stdout,
stderr,
randSource,
walltime, walltimeResolution,
nanotime, nanotimeResolution,
nanosleep,
fs,
)
require.NoError(t, err)
return sysCtx
}