Skip to content

Commit

Permalink
Merge 3154997 into d9b5fd0
Browse files Browse the repository at this point in the history
  • Loading branch information
mstoykov authored Jun 27, 2024
2 parents d9b5fd0 + 3154997 commit b895801
Show file tree
Hide file tree
Showing 45 changed files with 3,560 additions and 1,083 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 @@ -2105,10 +2105,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 @@ -2117,8 +2117,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 @@ -2137,11 +2137,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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ require (
github.com/go-sourcemap/sourcemap v2.1.4+incompatible
github.com/golang/protobuf v1.5.4
github.com/gorilla/websocket v1.5.1
github.com/grafana/sobek v0.0.0-20240611084759-1a77bdaf1a4a
github.com/grafana/sobek v0.0.0-20240624162318-afbc13aa071b
github.com/grafana/xk6-browser v1.6.0
github.com/grafana/xk6-dashboard v0.7.4
github.com/grafana/xk6-output-prometheus-remote v0.4.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/grafana/sobek v0.0.0-20240611084759-1a77bdaf1a4a h1:RE9WtCdu6yQ5kfHA5ophYdf2FSchssXt85USln9eks0=
github.com/grafana/sobek v0.0.0-20240611084759-1a77bdaf1a4a/go.mod h1:tUEHKWaMrxFGrMgjeAH85OEceCGQiSl6a/6Wckj/Vf4=
github.com/grafana/sobek v0.0.0-20240624162318-afbc13aa071b h1:CX/3ThBRroXW/4sLbyfXtHPBdzQFFESeWlkHO6/JvWw=
github.com/grafana/sobek v0.0.0-20240624162318-afbc13aa071b/go.mod h1:tUEHKWaMrxFGrMgjeAH85OEceCGQiSl6a/6Wckj/Vf4=
github.com/grafana/xk6-browser v1.6.0 h1:x8ZfBwiUJRRKNEw+Asr5ae9o2gFvYU1Ll/4dDMNIPZ8=
github.com/grafana/xk6-browser v1.6.0/go.mod h1:xLaGGhTMHIRsMvkVWFYh9RPy87kG2n4L4Or6DeI8U+o=
github.com/grafana/xk6-dashboard v0.7.4 h1:0ZRPTAXW+6A3Xqq/a/OaIZhxUt1SOMwUFff0IPwBHrs=
Expand Down
156 changes: 90 additions & 66 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 *sobek.Object
moduleVUImpl *moduleVUImpl
mainModule sobek.ModuleRecord
mainModuleInstance sobek.ModuleInstance
moduleVUImpl *moduleVUImpl
}

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

func (bi *BundleInstance) getExported(name string) sobek.Value {
return bi.mainModuleExports.Get(name)
re, ambigiuous := bi.mainModule.ResolveExport(name)
if ambigiuous || re == nil {
return nil
}
return bi.mainModuleInstance.GetBindingValue(re.BindingName)
}

// NewBundle creates a new bundle from a source file and a filesystem.
Expand Down Expand Up @@ -94,7 +99,7 @@ func newBundle(
}

c := bundle.newCompiler(piState.Logger)
bundle.ModuleResolver = modules.NewModuleResolver(getJSModules(), generateFileLoad(bundle), c)
bundle.ModuleResolver = modules.NewModuleResolver(getJSModules(), generateFileLoad(bundle), c, bundle.pwd)

// 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 @@ -108,13 +113,13 @@ 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
}
bundle.ModuleResolver.Lock()

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

// populateExports validates and extracts exported objects
func (b *Bundle) populateExports(updateOptions bool, exports *sobek.Object) error {
for _, k := range exports.Keys() {
v := exports.Get(k)
if _, ok := sobek.AssertFunction(v); ok && k != consts.Options {
b.callableExports[k] = struct{}{}
continue
}
switch k {
case consts.Options:
if !updateOptions {
func (b *Bundle) populateExports(updateOptions bool, bi *BundleInstance) error {
var err error
ch := make(chan struct{})
bi.mainModule.GetExportedNames(func(names []string) {
defer close(ch)
for _, k := range names {
v := bi.getExported(k)
if _, ok := sobek.AssertFunction(v); ok && k != consts.Options {
b.callableExports[k] = struct{}{}
continue
}
data, err := json.Marshal(v.Export())
if err != nil {
return fmt.Errorf("error parsing script options: %w", err)
}
dec := json.NewDecoder(bytes.NewReader(data))
dec.DisallowUnknownFields()
if err := dec.Decode(&b.Options); err != nil {
if uerr := json.Unmarshal(data, &b.Options); uerr != nil {
return uerr
switch k {
case consts.Options:
if !updateOptions {
continue
}
var data []byte
data, err = json.Marshal(v.Export())
if err != nil {
err = fmt.Errorf("error parsing script options: %w", err)
return
}
b.preInitState.Logger.WithError(err).Warn("There were unknown fields in the options exported in the script")
dec := json.NewDecoder(bytes.NewReader(data))
dec.DisallowUnknownFields()
if err = dec.Decode(&b.Options); err != nil {
if uerr := json.Unmarshal(data, &b.Options); uerr != nil {
err = uerr
return
}
b.preInitState.Logger.WithError(err).Warn("There were unknown fields in the options exported in the script")
err = nil
}
case consts.SetupFn:
err = errors.New("exported 'setup' must be a function")
return
case consts.TeardownFn:
err = errors.New("exported 'teardown' must be a function")
return
}
case consts.SetupFn:
return errors.New("exported 'setup' must be a function")
case consts.TeardownFn:
return errors.New("exported 'teardown' must be a function")
}
})
<-ch
if err != nil {
return err
}

if len(b.callableExports) == 0 {
Expand All @@ -222,40 +242,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 *sobek.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 @@ -268,7 +282,7 @@ func (b *Bundle) newCompiler(logger logrus.FieldLogger) *compiler.Compiler {
return c
}

func (b *Bundle) instantiate(vuImpl *moduleVUImpl, vuID uint64) (*sobek.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 @@ -301,10 +315,25 @@ func (b *Bundle) instantiate(vuImpl *moduleVUImpl, vuID uint64) (*sobek.Object,
close(initDone)
}()

var exportsV sobek.Value
err = vuImpl.eventLoop.Start(func() error {
bi := &BundleInstance{
Runtime: vuImpl.runtime,
env: b.preInitState.RuntimeOptions.Env,
moduleVUImpl: vuImpl,
}
callback := func() error { // this exists so that sobek catches uncatchable panics such as Interrupt
var err error
exportsV, err = modSys.RunSourceData(b.sourceData)
bi.mainModule, err = modSys.RunSourceData(b.sourceData)
if err != nil {
return err
}
bi.mainModuleInstance = rt.GetModuleInstance(bi.mainModule)
return nil
}

call, _ := sobek.AssertFunction(vuImpl.runtime.ToValue(callback))

err = vuImpl.eventLoop.Start(func() error {
_, err := call(nil)
return err
})

Expand All @@ -317,14 +346,6 @@ func (b *Bundle) instantiate(vuImpl *moduleVUImpl, vuID uint64) (*sobek.Object,
}
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 @@ -334,7 +355,7 @@ func (b *Bundle) instantiate(vuImpl *moduleVUImpl, vuID uint64) (*sobek.Object,

rt.SetRandSource(common.NewRandSource())

return exports, nil
return bi, nil
}

func (b *Bundle) setupJSRuntime(rt *sobek.Runtime, vuID int64, logger logrus.FieldLogger) error {
Expand Down Expand Up @@ -370,14 +391,14 @@ func (b *Bundle) setupJSRuntime(rt *sobek.Runtime, vuID int64, logger logrus.Fie
// this exists only to make the check in the init context.
type requireImpl struct {
inInitContext func() bool
internal *modules.LegacyRequireImpl
modSys *modules.ModuleSystem
}

func (r *requireImpl) require(specifier string) (*sobek.Object, error) {
if !r.inInitContext() {
return nil, fmt.Errorf(cantBeUsedOutsideInitContextMsg, "require")
}
return r.internal.Require(specifier)
return r.modSys.Require(specifier)
}

func (b *Bundle) setInitGlobals(rt *sobek.Runtime, vu *moduleVUImpl, modSys *modules.ModuleSystem) {
Expand All @@ -389,7 +410,7 @@ func (b *Bundle) setInitGlobals(rt *sobek.Runtime, vu *moduleVUImpl, modSys *mod

impl := requireImpl{
inInitContext: func() bool { return vu.state == nil },
internal: modules.NewLegacyRequireImpl(vu, modSys, *b.pwd),
modSys: modSys,
}

mustSet("require", impl.require)
Expand All @@ -404,8 +425,11 @@ func (b *Bundle) setInitGlobals(rt *sobek.Runtime, vu *moduleVUImpl, modSys *mod
return nil, errors.New("open() can't be used with an empty filename")
}
// This uses the pwd from the requireImpl
pwd := impl.internal.CurrentlyRequiredModule()
return openImpl(rt, b.filesystems["file"], &pwd, filename, args...)
pwd, err := modSys.CurrentlyRequiredModule()
if err != nil {
return nil, err
}
return openImpl(rt, b.filesystems["file"], pwd, filename, args...)
})
}

Expand Down
Loading

0 comments on commit b895801

Please sign in to comment.