Skip to content

Commit

Permalink
js: Use the new ESM support in goja through k6
Browse files Browse the repository at this point in the history
This is still very hackish and tries to preserve every observable
behavior possible.

This also specifically skips adding support for:
1. dynamic import
2. top level await
3. anything around the internal go modules

Which will be added later. Also let us concentrate on making this change
as not breaking as possible.
  • Loading branch information
mstoykov committed Feb 29, 2024
1 parent 63b46bb commit 27973ce
Show file tree
Hide file tree
Showing 19 changed files with 715 additions and 746 deletions.
22 changes: 11 additions & 11 deletions cmd/tests/cmd_run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -944,8 +944,8 @@ func TestAbortedByScriptSetupErrorWithDependency(t *testing.T) {
if runtime.GOOS == "windows" {
rootPath += "c:/"
}
assert.Contains(t, stdout, `level=error msg="Error: baz\n\tat baz (`+rootPath+`test/bar.js:6:9(3))\n\tat `+
rootPath+`test/bar.js:3:3(3)\n\tat setup (`+rootPath+`test/test.js:5:3(9))\n" hint="script exception"`)
assert.Contains(t, stdout, `level=error msg="Error: baz\n\tat baz (`+rootPath+`test/bar.js:6:10(3))\n\tat default (`+
rootPath+`test/bar.js:3:7(3))\n\tat setup (`+rootPath+`test/test.js:5:7(8))\n" hint="script exception"`)
assert.Contains(t, stdout, `level=debug msg="Sending test finished" output=cloud ref=123 run_status=7 tainted=false`)
assert.Contains(t, stdout, "bogus summary")
}
Expand Down Expand Up @@ -2106,10 +2106,10 @@ func TestEventSystemError(t *testing.T) {
"got event Init with data '<nil>'",
"got event TestStart with data '<nil>'",
"got event IterStart with data '{Iteration:0 VUID:1 ScenarioName:default Error:<nil>}'",
"got event IterEnd with data '{Iteration:0 VUID:1 ScenarioName:default Error:test aborted: oops! at file:///-:11:16(6)}'",
"got event IterEnd with data '{Iteration:0 VUID:1 ScenarioName:default Error:test aborted: oops! at default (file:///-:11:16(5))}'",
"got event TestEnd with data '<nil>'",
"got event Exit with data '&{Error:test aborted: oops! at file:///-:11:16(6)}'",
"test aborted: oops! at file:///-:11:16(6)",
"got event Exit with data '&{Error:test aborted: oops! at default (file:///-:11:16(5))}'",
"test aborted: oops! at default (file:///-:11:16(5))",
},
expExitCode: exitcodes.ScriptAborted,
},
Expand All @@ -2118,8 +2118,8 @@ func TestEventSystemError(t *testing.T) {
script: "undefinedVar",
expLog: []string{
"got event Exit with data '&{Error:could not initialize '-': could not load JS test " +
"'file:///-': ReferenceError: undefinedVar is not defined\n\tat file:///-:2:0(12)\n}'",
"ReferenceError: undefinedVar is not defined\n\tat file:///-:2:0(12)\n",
"'file:///-': ReferenceError: undefinedVar is not defined\n\tat file:///-:2:1(8)\n}'",
"ReferenceError: undefinedVar is not defined\n\tat file:///-:2:1(8)\n",
},
expExitCode: exitcodes.ScriptException,
},
Expand All @@ -2138,11 +2138,11 @@ func TestEventSystemError(t *testing.T) {
"got event Init with data '<nil>'",
"got event TestStart with data '<nil>'",
"got event IterStart with data '{Iteration:0 VUID:1 ScenarioName:default Error:<nil>}'",
"got event IterEnd with data '{Iteration:0 VUID:1 ScenarioName:default Error:Error: oops!\n\tat file:///-:9:11(3)\n}'",
"Error: oops!\n\tat file:///-:9:11(3)\n",
"got event IterEnd with data '{Iteration:0 VUID:1 ScenarioName:default Error:Error: oops!\n\tat default (file:///-:9:12(3))\n}'",
"Error: oops!\n\tat default (file:///-:9:12(3))\n",
"got event IterStart with data '{Iteration:1 VUID:1 ScenarioName:default Error:<nil>}'",
"got event IterEnd with data '{Iteration:1 VUID:1 ScenarioName:default Error:Error: oops!\n\tat file:///-:9:11(3)\n}'",
"Error: oops!\n\tat file:///-:9:11(3)\n",
"got event IterEnd with data '{Iteration:1 VUID:1 ScenarioName:default Error:Error: oops!\n\tat default (file:///-:9:12(3))\n}'",
"Error: oops!\n\tat default (file:///-:9:12(3))\n",
"got event TestEnd with data '<nil>'",
"got event Exit with data '&{Error:<nil>}'",
},
Expand Down
2 changes: 1 addition & 1 deletion cmd/tests/eventloop_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ func TestEventLoopDoesntCrossIterations(t *testing.T) {
eventLoopTest(t, script, func(logLines []string) {
require.Equal(t, []string{
"setTimeout 1 was stopped because the VU iteration was interrupted",
"just error\n\tat file:///-:13:4(15)\n", "1",
"just error\n\tat default (file:///-:13:5(14))\n", "1",
}, logLines)
})
}
Expand Down
80 changes: 38 additions & 42 deletions js/bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,9 @@ type BundleInstance struct {
// TODO: maybe just have a reference to the Bundle? or save and pass rtOpts?
env map[string]string

mainModuleExports *goja.Object
moduleVUImpl *moduleVUImpl
mainModule goja.ModuleRecord
mainModuleInstance goja.ModuleInstance
moduleVUImpl *moduleVUImpl
}

func (bi *BundleInstance) getCallableExport(name string) goja.Callable {
Expand All @@ -59,7 +60,7 @@ func (bi *BundleInstance) getCallableExport(name string) goja.Callable {
}

func (bi *BundleInstance) getExported(name string) goja.Value {
return bi.mainModuleExports.Get(name)
return bi.mainModuleInstance.GetBindingValue(name)
}

// NewBundle creates a new bundle from a source file and a filesystem.
Expand Down Expand Up @@ -89,7 +90,7 @@ func newBundle(
preInitState: piState,
}
c := bundle.newCompiler(piState.Logger)
bundle.ModuleResolver = modules.NewModuleResolver(getJSModules(), generateFileLoad(bundle), c)
bundle.ModuleResolver = modules.NewModuleResolver(getJSModules(), generateFileLoad(bundle), c, src.URL.JoinPath(".."))

// Instantiate the bundle into a new VM using a bound init context. This uses a context with a
// runtime, but no state, to allow module-provided types to function within the init context.
Expand All @@ -103,12 +104,12 @@ func newBundle(
},
}
vuImpl.eventLoop = eventloop.New(vuImpl)
exports, err := bundle.instantiate(vuImpl, 0)
bi, err := bundle.instantiate(vuImpl, 0)
if err != nil {
return nil, err
}

err = bundle.populateExports(updateOptions, exports)
err = bundle.populateExports(updateOptions, bi)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -165,9 +166,9 @@ func (b *Bundle) makeArchive() *lib.Archive {
}

// populateExports validates and extracts exported objects
func (b *Bundle) populateExports(updateOptions bool, exports *goja.Object) error {
for _, k := range exports.Keys() {
v := exports.Get(k)
func (b *Bundle) populateExports(updateOptions bool, bi *BundleInstance) error {
for _, k := range bi.mainModule.GetExportedNames() {
v := bi.getExported(k)
if _, ok := goja.AssertFunction(v); ok && k != consts.Options {
b.callableExports[k] = struct{}{}
continue
Expand Down Expand Up @@ -216,40 +217,34 @@ func (b *Bundle) Instantiate(ctx context.Context, vuID uint64) (*BundleInstance,
},
}
vuImpl.eventLoop = eventloop.New(vuImpl)
exports, err := b.instantiate(vuImpl, vuID)
bi, err := b.instantiate(vuImpl, vuID)
if err != nil {
return nil, err
}

bi := &BundleInstance{
Runtime: vuImpl.runtime,
env: b.preInitState.RuntimeOptions.Env,
moduleVUImpl: vuImpl,
mainModuleExports: exports,
if err = bi.manipulateOptions(b.Options); err != nil {
return nil, err
}

return bi, nil
}

func (bi *BundleInstance) manipulateOptions(options lib.Options) error {
// Grab any exported functions that could be executed. These were
// already pre-validated in cmd.validateScenarioConfig(), just get them here.
jsOptions := exports.Get(consts.Options)
jsOptions := bi.getExported(consts.Options)
var jsOptionsObj *goja.Object
if common.IsNullish(jsOptions) {
jsOptionsObj = vuImpl.runtime.NewObject()
err := exports.Set(consts.Options, jsOptionsObj)
if err != nil {
return nil, fmt.Errorf("couldn't set exported options with merged values: %w", err)
}
} else {
jsOptionsObj = jsOptions.ToObject(vuImpl.runtime)
return nil
}

jsOptionsObj = jsOptions.ToObject(bi.Runtime)
var instErr error
b.Options.ForEachSpecified("json", func(key string, val interface{}) {
options.ForEachSpecified("json", func(key string, val interface{}) {
if err := jsOptionsObj.Set(key, val); err != nil {
instErr = err
}
})

return bi, instErr
return instErr
}

func (b *Bundle) newCompiler(logger logrus.FieldLogger) *compiler.Compiler {
Expand All @@ -262,7 +257,7 @@ func (b *Bundle) newCompiler(logger logrus.FieldLogger) *compiler.Compiler {
return c
}

func (b *Bundle) instantiate(vuImpl *moduleVUImpl, vuID uint64) (*goja.Object, error) {
func (b *Bundle) instantiate(vuImpl *moduleVUImpl, vuID uint64) (*BundleInstance, error) {
rt := vuImpl.runtime
err := b.setupJSRuntime(rt, int64(vuID), b.preInitState.Logger)
if err != nil {
Expand Down Expand Up @@ -294,14 +289,23 @@ func (b *Bundle) instantiate(vuImpl *moduleVUImpl, vuID uint64) (*goja.Object, e
}
close(initDone)
}()

var exportsV goja.Value
bi := &BundleInstance{
Runtime: vuImpl.runtime,
env: b.preInitState.RuntimeOptions.Env,
moduleVUImpl: vuImpl,
}
err = common.RunWithPanicCatching(b.preInitState.Logger, rt, func() error {
return vuImpl.eventLoop.Start(func() error {
//nolint:govet // here we shadow err on purpose
var err error
exportsV, err = modSys.RunSourceData(b.sourceData)
return err

bi.mainModule, err = modSys.RunSourceData(b.sourceData)
if err != nil {
return err
}
// TODO move this here
bi.mainModuleInstance = rt.GetModuleInstance(bi.mainModule)
return nil
})
})

Expand All @@ -314,14 +318,6 @@ func (b *Bundle) instantiate(vuImpl *moduleVUImpl, vuID uint64) (*goja.Object, e
}
return nil, err
}
if common.IsNullish(exportsV) {
return nil, errors.New("exports must not be set to null or undefined")
}
exports := exportsV.ToObject(vuImpl.runtime)

if exports == nil {
return nil, errors.New("exports must be an object")
}

// If we've already initialized the original VU init context, forbid
// any subsequent VUs to open new files
Expand All @@ -331,7 +327,7 @@ func (b *Bundle) instantiate(vuImpl *moduleVUImpl, vuID uint64) (*goja.Object, e

rt.SetRandSource(common.NewRandSource())

return exports, nil
return bi, nil
}

func (b *Bundle) setupJSRuntime(rt *goja.Runtime, vuID int64, logger logrus.FieldLogger) error {
Expand Down Expand Up @@ -402,7 +398,7 @@ func (b *Bundle) setInitGlobals(rt *goja.Runtime, vu *moduleVUImpl, modSys *modu
}
// This uses the pwd from the requireImpl
pwd := impl.internal.CurrentlyRequiredModule()
return openImpl(rt, b.filesystems["file"], &pwd, filename, args...)
return openImpl(rt, b.filesystems["file"], pwd, filename, args...)
})

return func() {
Expand Down
4 changes: 3 additions & 1 deletion js/bundle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ func TestNewBundle(t *testing.T) {
t.Parallel()
_, err := getSimpleBundle(t, "/script.js", "\x00")
require.NotNil(t, err)
require.Contains(t, err.Error(), "SyntaxError: file:///script.js: Unexpected character '\x00' (1:0)\n> 1 | \x00\n")
require.Contains(t, err.Error(), "file:///script.js: Line 1:1 Unexpected token ILLEGAL (and 1 more errors)")
})
t.Run("Error", func(t *testing.T) {
t.Parallel()
Expand Down Expand Up @@ -532,12 +532,14 @@ func TestNewBundleFromArchive(t *testing.T) {
checkArchive(t, arc, baseCompatModeRtOpts, "")
})

/* TODO remove completely - this no longer makes sense
t.Run("es6_archive_with_wrong_compat_mode", func(t *testing.T) {
t.Parallel()
arc, err := getArchive(t, es6Code, baseCompatModeRtOpts)
require.Error(t, err)
require.Nil(t, arc)
})
*/

t.Run("messed_up_archive", func(t *testing.T) {
t.Parallel()
Expand Down
Loading

0 comments on commit 27973ce

Please sign in to comment.