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

     1  package wazero
     2  
     3  import (
     4  	"context"
     5  	"crypto/rand"
     6  	"io"
     7  	"io/fs"
     8  	"math"
     9  	"testing"
    10  	"testing/fstest"
    11  
    12  	"wa-lang.org/wazero/api"
    13  	internalsys "wa-lang.org/wazero/internal/sys"
    14  	testfs "wa-lang.org/wazero/internal/testing/fs"
    15  	"wa-lang.org/wazero/internal/testing/require"
    16  	"wa-lang.org/wazero/internal/wasm"
    17  	"wa-lang.org/wazero/sys"
    18  )
    19  
    20  func TestRuntimeConfig(t *testing.T) {
    21  	tests := []struct {
    22  		name     string
    23  		with     func(RuntimeConfig) RuntimeConfig
    24  		expected RuntimeConfig
    25  	}{
    26  		{
    27  			name: "features",
    28  			with: func(c RuntimeConfig) RuntimeConfig {
    29  				return c.WithCoreFeatures(api.CoreFeaturesV1)
    30  			},
    31  			expected: &runtimeConfig{
    32  				enabledFeatures: api.CoreFeaturesV1,
    33  			},
    34  		},
    35  		{
    36  			name: "memoryLimitPages",
    37  			with: func(c RuntimeConfig) RuntimeConfig {
    38  				return c.WithMemoryLimitPages(10)
    39  			},
    40  			expected: &runtimeConfig{
    41  				memoryLimitPages: 10,
    42  			},
    43  		},
    44  		{
    45  			name: "memoryCapacityFromMax",
    46  			with: func(c RuntimeConfig) RuntimeConfig {
    47  				return c.WithMemoryCapacityFromMax(true)
    48  			},
    49  			expected: &runtimeConfig{
    50  				memoryCapacityFromMax: true,
    51  			},
    52  		},
    53  	}
    54  
    55  	for _, tt := range tests {
    56  		tc := tt
    57  
    58  		t.Run(tc.name, func(t *testing.T) {
    59  			input := &runtimeConfig{}
    60  			rc := tc.with(input)
    61  			require.Equal(t, tc.expected, rc)
    62  			// The source wasn't modified
    63  			require.Equal(t, &runtimeConfig{}, input)
    64  		})
    65  	}
    66  
    67  	t.Run("memoryLimitPages invalid panics", func(t *testing.T) {
    68  		err := require.CapturePanic(func() {
    69  			input := &runtimeConfig{}
    70  			input.WithMemoryLimitPages(wasm.MemoryLimitPages + 1)
    71  		})
    72  		require.EqualError(t, err, "memoryLimitPages invalid: 65537 > 65536")
    73  	})
    74  }
    75  
    76  func TestModuleConfig(t *testing.T) {
    77  	tests := []struct {
    78  		name     string
    79  		with     func(ModuleConfig) ModuleConfig
    80  		expected string
    81  	}{
    82  		{
    83  			name: "WithName",
    84  			with: func(c ModuleConfig) ModuleConfig {
    85  				return c.WithName("wazero")
    86  			},
    87  			expected: "wazero",
    88  		},
    89  		{
    90  			name: "WithName empty",
    91  			with: func(c ModuleConfig) ModuleConfig {
    92  				return c.WithName("")
    93  			},
    94  		},
    95  		{
    96  			name: "WithName twice",
    97  			with: func(c ModuleConfig) ModuleConfig {
    98  				return c.WithName("wazero").WithName("wa0")
    99  			},
   100  			expected: "wa0",
   101  		},
   102  	}
   103  	for _, tt := range tests {
   104  		tc := tt
   105  
   106  		t.Run(tc.name, func(t *testing.T) {
   107  			input := NewModuleConfig()
   108  			rc := tc.with(input)
   109  			require.Equal(t, tc.expected, rc.(*moduleConfig).name)
   110  			// The source wasn't modified
   111  			require.Equal(t, NewModuleConfig(), input)
   112  		})
   113  	}
   114  }
   115  
   116  // TestModuleConfig_toSysContext only tests the cases that change the inputs to
   117  // sys.NewContext.
   118  func TestModuleConfig_toSysContext(t *testing.T) {
   119  	// Always assigns clocks so that pointers are constant.
   120  	var wt sys.Walltime = func(context.Context) (int64, int32) {
   121  		return 0, 0
   122  	}
   123  	var nt sys.Nanotime = func(context.Context) int64 {
   124  		return 0
   125  	}
   126  	base := NewModuleConfig()
   127  	base.(*moduleConfig).walltime = &wt
   128  	base.(*moduleConfig).walltimeResolution = 1
   129  	base.(*moduleConfig).nanotime = &nt
   130  	base.(*moduleConfig).nanotimeResolution = 1
   131  
   132  	testFS := testfs.FS{}
   133  	testFS2 := testfs.FS{"/": &testfs.File{}}
   134  
   135  	tests := []struct {
   136  		name     string
   137  		input    ModuleConfig
   138  		expected *internalsys.Context
   139  	}{
   140  		{
   141  			name:  "empty",
   142  			input: base,
   143  			expected: requireSysContext(t,
   144  				math.MaxUint32, // max
   145  				nil,            // args
   146  				nil,            // environ
   147  				nil,            // stdin
   148  				nil,            // stdout
   149  				nil,            // stderr
   150  				nil,            // randSource
   151  				&wt, 1,         // walltime, walltimeResolution
   152  				&nt, 1, // nanotime, nanotimeResolution
   153  				nil, // nanosleep
   154  				nil, // fs
   155  			),
   156  		},
   157  		{
   158  			name:  "WithArgs",
   159  			input: base.WithArgs("a", "bc"),
   160  			expected: requireSysContext(t,
   161  				math.MaxUint32,      // max
   162  				[]string{"a", "bc"}, // args
   163  				nil,                 // environ
   164  				nil,                 // stdin
   165  				nil,                 // stdout
   166  				nil,                 // stderr
   167  				nil,                 // randSource
   168  				&wt, 1,              // walltime, walltimeResolution
   169  				&nt, 1, // nanotime, nanotimeResolution
   170  				nil, // nanosleep
   171  				nil, // fs
   172  			),
   173  		},
   174  		{
   175  			name:  "WithArgs empty ok", // Particularly argv[0] can be empty, and we have no rules about others.
   176  			input: base.WithArgs("", "bc"),
   177  			expected: requireSysContext(t,
   178  				math.MaxUint32,     // max
   179  				[]string{"", "bc"}, // args
   180  				nil,                // environ
   181  				nil,                // stdin
   182  				nil,                // stdout
   183  				nil,                // stderr
   184  				nil,                // randSource
   185  				&wt, 1,             // walltime, walltimeResolution
   186  				&nt, 1, // nanotime, nanotimeResolution
   187  				nil, // nanosleep
   188  				nil, // fs
   189  			),
   190  		},
   191  		{
   192  			name:  "WithArgs second call overwrites",
   193  			input: base.WithArgs("a", "bc").WithArgs("bc", "a"),
   194  			expected: requireSysContext(t,
   195  				math.MaxUint32,      // max
   196  				[]string{"bc", "a"}, // args
   197  				nil,                 // environ
   198  				nil,                 // stdin
   199  				nil,                 // stdout
   200  				nil,                 // stderr
   201  				nil,                 // randSource
   202  				&wt, 1,              // walltime, walltimeResolution
   203  				&nt, 1, // nanotime, nanotimeResolution
   204  				nil, // nanosleep
   205  				nil, // fs
   206  			),
   207  		},
   208  		{
   209  			name:  "WithEnv",
   210  			input: base.WithEnv("a", "b"),
   211  			expected: requireSysContext(t,
   212  				math.MaxUint32,  // max
   213  				nil,             // args
   214  				[]string{"a=b"}, // environ
   215  				nil,             // stdin
   216  				nil,             // stdout
   217  				nil,             // stderr
   218  				nil,             // randSource
   219  				&wt, 1,          // walltime, walltimeResolution
   220  				&nt, 1, // nanotime, nanotimeResolution
   221  				nil, // nanosleep
   222  				nil, // fs
   223  			),
   224  		},
   225  		{
   226  			name:  "WithEnv empty value",
   227  			input: base.WithEnv("a", ""),
   228  			expected: requireSysContext(t,
   229  				math.MaxUint32, // max
   230  				nil,            // args
   231  				[]string{"a="}, // environ
   232  				nil,            // stdin
   233  				nil,            // stdout
   234  				nil,            // stderr
   235  				nil,            // randSource
   236  				&wt, 1,         // walltime, walltimeResolution
   237  				&nt, 1, // nanotime, nanotimeResolution
   238  				nil, // nanosleep
   239  				nil, // fs
   240  			),
   241  		},
   242  		{
   243  			name:  "WithEnv twice",
   244  			input: base.WithEnv("a", "b").WithEnv("c", "de"),
   245  			expected: requireSysContext(t,
   246  				math.MaxUint32,          // max
   247  				nil,                     // args
   248  				[]string{"a=b", "c=de"}, // environ
   249  				nil,                     // stdin
   250  				nil,                     // stdout
   251  				nil,                     // stderr
   252  				nil,                     // randSource
   253  				&wt, 1,                  // walltime, walltimeResolution
   254  				&nt, 1, // nanotime, nanotimeResolution
   255  				nil, // nanosleep
   256  				nil, // fs
   257  			),
   258  		},
   259  		{
   260  			name:  "WithEnv overwrites",
   261  			input: base.WithEnv("a", "bc").WithEnv("c", "de").WithEnv("a", "de"),
   262  			expected: requireSysContext(t,
   263  				math.MaxUint32,           // max
   264  				nil,                      // args
   265  				[]string{"a=de", "c=de"}, // environ
   266  				nil,                      // stdin
   267  				nil,                      // stdout
   268  				nil,                      // stderr
   269  				nil,                      // randSource
   270  				&wt, 1,                   // walltime, walltimeResolution
   271  				&nt, 1, // nanotime, nanotimeResolution
   272  				nil, // nanosleep
   273  				nil, // fs
   274  			),
   275  		},
   276  		{
   277  			name:  "WithEnv twice",
   278  			input: base.WithEnv("a", "b").WithEnv("c", "de"),
   279  			expected: requireSysContext(t,
   280  				math.MaxUint32,          // max
   281  				nil,                     // args
   282  				[]string{"a=b", "c=de"}, // environ
   283  				nil,                     // stdin
   284  				nil,                     // stdout
   285  				nil,                     // stderr
   286  				nil,                     // randSource
   287  				&wt, 1,                  // walltime, walltimeResolution
   288  				&nt, 1, // nanotime, nanotimeResolution
   289  				nil, // nanosleep
   290  				nil, // fs
   291  			),
   292  		},
   293  		{
   294  			name:  "WithFS",
   295  			input: base.WithFS(testFS),
   296  			expected: requireSysContext(t,
   297  				math.MaxUint32, // max
   298  				nil,            // args
   299  				nil,            // environ
   300  				nil,            // stdin
   301  				nil,            // stdout
   302  				nil,            // stderr
   303  				nil,            // randSource
   304  				&wt, 1,         // walltime, walltimeResolution
   305  				&nt, 1, // nanotime, nanotimeResolution
   306  				nil, // nanosleep
   307  				testFS,
   308  			),
   309  		},
   310  		{
   311  			name:  "WithFS overwrites",
   312  			input: base.WithFS(testFS).WithFS(testFS2),
   313  			expected: requireSysContext(t,
   314  				math.MaxUint32, // max
   315  				nil,            // args
   316  				nil,            // environ
   317  				nil,            // stdin
   318  				nil,            // stdout
   319  				nil,            // stderr
   320  				nil,            // randSource
   321  				&wt, 1,         // walltime, walltimeResolution
   322  				&nt, 1, // nanotime, nanotimeResolution
   323  				nil,     // nanosleep
   324  				testFS2, // fs
   325  			),
   326  		},
   327  		{
   328  			name:  "WithRandSource",
   329  			input: base.WithRandSource(rand.Reader),
   330  			expected: requireSysContext(t,
   331  				math.MaxUint32, // max
   332  				nil,            // args
   333  				nil,            // environ
   334  				nil,            // stdin
   335  				nil,            // stdout
   336  				nil,            // stderr
   337  				rand.Reader,    // randSource
   338  				&wt, 1,         // walltime, walltimeResolution
   339  				&nt, 1, // nanotime, nanotimeResolution
   340  				nil, // nanosleep
   341  				nil, // fs
   342  			),
   343  		},
   344  	}
   345  
   346  	for _, tt := range tests {
   347  		tc := tt
   348  
   349  		t.Run(tc.name, func(t *testing.T) {
   350  			sysCtx, err := tc.input.(*moduleConfig).toSysContext()
   351  			require.NoError(t, err)
   352  			require.Equal(t, tc.expected, sysCtx)
   353  		})
   354  	}
   355  }
   356  
   357  // TestModuleConfig_toSysContext_WithWalltime has to test differently because we can't
   358  // compare function pointers when functions are passed by value.
   359  func TestModuleConfig_toSysContext_WithWalltime(t *testing.T) {
   360  	tests := []struct {
   361  		name               string
   362  		input              ModuleConfig
   363  		expectedSec        int64
   364  		expectedNsec       int32
   365  		expectedResolution sys.ClockResolution
   366  		expectedErr        string
   367  	}{
   368  		{
   369  			name: "ok",
   370  			input: NewModuleConfig().
   371  				WithWalltime(func(context.Context) (sec int64, nsec int32) {
   372  					return 1, 2
   373  				}, 3),
   374  			expectedSec:        1,
   375  			expectedNsec:       2,
   376  			expectedResolution: 3,
   377  		},
   378  		{
   379  			name: "overwrites",
   380  			input: NewModuleConfig().
   381  				WithWalltime(func(context.Context) (sec int64, nsec int32) {
   382  					return 3, 4
   383  				}, 5).
   384  				WithWalltime(func(context.Context) (sec int64, nsec int32) {
   385  					return 1, 2
   386  				}, 3),
   387  			expectedSec:        1,
   388  			expectedNsec:       2,
   389  			expectedResolution: 3,
   390  		},
   391  		{
   392  			name: "invalid resolution",
   393  			input: NewModuleConfig().
   394  				WithWalltime(func(context.Context) (sec int64, nsec int32) {
   395  					return 1, 2
   396  				}, 0),
   397  			expectedErr: "invalid Walltime resolution: 0",
   398  		},
   399  	}
   400  
   401  	for _, tt := range tests {
   402  		tc := tt
   403  
   404  		t.Run(tc.name, func(t *testing.T) {
   405  			sysCtx, err := tc.input.(*moduleConfig).toSysContext()
   406  			if tc.expectedErr == "" {
   407  				require.Nil(t, err)
   408  				sec, nsec := sysCtx.Walltime(testCtx)
   409  				require.Equal(t, tc.expectedSec, sec)
   410  				require.Equal(t, tc.expectedNsec, nsec)
   411  				require.Equal(t, tc.expectedResolution, sysCtx.WalltimeResolution())
   412  			} else {
   413  				require.EqualError(t, err, tc.expectedErr)
   414  			}
   415  		})
   416  	}
   417  
   418  	t.Run("context", func(t *testing.T) {
   419  		sysCtx, err := NewModuleConfig().
   420  			WithWalltime(func(ctx context.Context) (sec int64, nsec int32) {
   421  				require.Equal(t, testCtx, ctx)
   422  				return 1, 2
   423  			}, 3).(*moduleConfig).toSysContext()
   424  		require.NoError(t, err)
   425  		sec, nsec := sysCtx.Walltime(testCtx)
   426  		// If below pass, the context was correct!
   427  		require.Equal(t, int64(1), sec)
   428  		require.Equal(t, int32(2), nsec)
   429  	})
   430  }
   431  
   432  // TestModuleConfig_toSysContext_WithNanotime has to test differently because we can't
   433  // compare function pointers when functions are passed by value.
   434  func TestModuleConfig_toSysContext_WithNanotime(t *testing.T) {
   435  	tests := []struct {
   436  		name               string
   437  		input              ModuleConfig
   438  		expectedNanos      int64
   439  		expectedResolution sys.ClockResolution
   440  		expectedErr        string
   441  	}{
   442  		{
   443  			name: "ok",
   444  			input: NewModuleConfig().
   445  				WithNanotime(func(context.Context) int64 {
   446  					return 1
   447  				}, 2),
   448  			expectedNanos:      1,
   449  			expectedResolution: 2,
   450  		},
   451  		{
   452  			name: "overwrites",
   453  			input: NewModuleConfig().
   454  				WithNanotime(func(context.Context) int64 {
   455  					return 3
   456  				}, 4).
   457  				WithNanotime(func(context.Context) int64 {
   458  					return 1
   459  				}, 2),
   460  			expectedNanos:      1,
   461  			expectedResolution: 2,
   462  		},
   463  		{
   464  			name: "invalid resolution",
   465  			input: NewModuleConfig().
   466  				WithNanotime(func(context.Context) int64 {
   467  					return 1
   468  				}, 0),
   469  			expectedErr: "invalid Nanotime resolution: 0",
   470  		},
   471  	}
   472  
   473  	for _, tt := range tests {
   474  		tc := tt
   475  
   476  		t.Run(tc.name, func(t *testing.T) {
   477  			sysCtx, err := tc.input.(*moduleConfig).toSysContext()
   478  			if tc.expectedErr == "" {
   479  				require.Nil(t, err)
   480  				nanos := sysCtx.Nanotime(testCtx)
   481  				require.Equal(t, tc.expectedNanos, nanos)
   482  				require.Equal(t, tc.expectedResolution, sysCtx.NanotimeResolution())
   483  			} else {
   484  				require.EqualError(t, err, tc.expectedErr)
   485  			}
   486  		})
   487  	}
   488  
   489  	t.Run("context", func(t *testing.T) {
   490  		sysCtx, err := NewModuleConfig().
   491  			WithNanotime(func(ctx context.Context) int64 {
   492  				require.Equal(t, testCtx, ctx)
   493  				return 1
   494  			}, 2).(*moduleConfig).toSysContext()
   495  		require.NoError(t, err)
   496  		// If below pass, the context was correct!
   497  		require.Equal(t, int64(1), sysCtx.Nanotime(testCtx))
   498  	})
   499  }
   500  
   501  // TestModuleConfig_toSysContext_WithNanosleep has to test differently because
   502  // we can't compare function pointers when functions are passed by value.
   503  func TestModuleConfig_toSysContext_WithNanosleep(t *testing.T) {
   504  	sysCtx, err := NewModuleConfig().
   505  		WithNanosleep(func(ctx context.Context, ns int64) {
   506  			require.Equal(t, testCtx, ctx)
   507  		}).(*moduleConfig).toSysContext()
   508  	require.NoError(t, err)
   509  	// If below pass, the context was correct!
   510  	sysCtx.Nanosleep(testCtx, 2)
   511  }
   512  
   513  func TestModuleConfig_toSysContext_Errors(t *testing.T) {
   514  	tests := []struct {
   515  		name        string
   516  		input       ModuleConfig
   517  		expectedErr string
   518  	}{
   519  		{
   520  			name:        "WithArgs arg contains NUL",
   521  			input:       NewModuleConfig().WithArgs("", string([]byte{'a', 0})),
   522  			expectedErr: "args invalid: contains NUL character",
   523  		},
   524  		{
   525  			name:        "WithEnv key contains NUL",
   526  			input:       NewModuleConfig().WithEnv(string([]byte{'a', 0}), "a"),
   527  			expectedErr: "environ invalid: contains NUL character",
   528  		},
   529  		{
   530  			name:        "WithEnv value contains NUL",
   531  			input:       NewModuleConfig().WithEnv("a", string([]byte{'a', 0})),
   532  			expectedErr: "environ invalid: contains NUL character",
   533  		},
   534  		{
   535  			name:        "WithEnv key contains equals",
   536  			input:       NewModuleConfig().WithEnv("a=", "a"),
   537  			expectedErr: "environ invalid: key contains '=' character",
   538  		},
   539  		{
   540  			name:        "WithEnv empty key",
   541  			input:       NewModuleConfig().WithEnv("", "a"),
   542  			expectedErr: "environ invalid: empty key",
   543  		},
   544  	}
   545  	for _, tt := range tests {
   546  		tc := tt
   547  
   548  		t.Run(tc.name, func(t *testing.T) {
   549  			_, err := tc.input.(*moduleConfig).toSysContext()
   550  			require.EqualError(t, err, tc.expectedErr)
   551  		})
   552  	}
   553  }
   554  
   555  func TestModuleConfig_clone(t *testing.T) {
   556  	mc := NewModuleConfig().(*moduleConfig)
   557  	cloned := mc.clone()
   558  
   559  	// Make post-clone changes
   560  	mc.fs = fstest.MapFS{}
   561  	mc.environKeys["2"] = 2
   562  
   563  	cloned.environKeys["1"] = 1
   564  
   565  	// Ensure the maps are not shared
   566  	require.Equal(t, map[string]int{"2": 2}, mc.environKeys)
   567  	require.Equal(t, map[string]int{"1": 1}, cloned.environKeys)
   568  
   569  	// Ensure the fs is not shared
   570  	require.Nil(t, cloned.fs)
   571  }
   572  
   573  func Test_compiledModule_Name(t *testing.T) {
   574  	tests := []struct {
   575  		name     string
   576  		input    *compiledModule
   577  		expected string
   578  	}{
   579  		{
   580  			name:  "no name section",
   581  			input: &compiledModule{module: &wasm.Module{}},
   582  		},
   583  		{
   584  			name:  "empty name",
   585  			input: &compiledModule{module: &wasm.Module{NameSection: &wasm.NameSection{}}},
   586  		},
   587  		{
   588  			name:     "name",
   589  			input:    &compiledModule{module: &wasm.Module{NameSection: &wasm.NameSection{ModuleName: "foo"}}},
   590  			expected: "foo",
   591  		},
   592  	}
   593  
   594  	for _, tt := range tests {
   595  		tc := tt
   596  
   597  		t.Run(tc.name, func(t *testing.T) {
   598  			require.Equal(t, tc.expected, tc.input.Name())
   599  		})
   600  	}
   601  }
   602  
   603  func Test_compiledModule_Close(t *testing.T) {
   604  	for _, ctx := range []context.Context{nil, testCtx} { // Ensure it doesn't crash on nil!
   605  		e := &mockEngine{name: "1", cachedModules: map[*wasm.Module]struct{}{}}
   606  
   607  		var cs []*compiledModule
   608  		for i := 0; i < 10; i++ {
   609  			m := &wasm.Module{}
   610  			err := e.CompileModule(ctx, m, nil)
   611  			require.NoError(t, err)
   612  			cs = append(cs, &compiledModule{module: m, compiledEngine: e})
   613  		}
   614  
   615  		// Before Close.
   616  		require.Equal(t, 10, len(e.cachedModules))
   617  
   618  		for _, c := range cs {
   619  			require.NoError(t, c.Close(ctx))
   620  		}
   621  
   622  		// After Close.
   623  		require.Zero(t, len(e.cachedModules))
   624  	}
   625  }
   626  
   627  // requireSysContext ensures wasm.NewContext doesn't return an error, which makes it usable in test matrices.
   628  func requireSysContext(
   629  	t *testing.T,
   630  	max uint32,
   631  	args, environ []string,
   632  	stdin io.Reader,
   633  	stdout, stderr io.Writer,
   634  	randSource io.Reader,
   635  	walltime *sys.Walltime, walltimeResolution sys.ClockResolution,
   636  	nanotime *sys.Nanotime, nanotimeResolution sys.ClockResolution,
   637  	nanosleep *sys.Nanosleep,
   638  	fs fs.FS,
   639  ) *internalsys.Context {
   640  	sysCtx, err := internalsys.NewContext(
   641  		max,
   642  		args,
   643  		environ,
   644  		stdin,
   645  		stdout,
   646  		stderr,
   647  		randSource,
   648  		walltime, walltimeResolution,
   649  		nanotime, nanotimeResolution,
   650  		nanosleep,
   651  		fs,
   652  	)
   653  	require.NoError(t, err)
   654  	return sysCtx
   655  }