diff --git a/pkg/execution/execution.go b/pkg/execution/execution.go index 1e36408..2df44af 100644 --- a/pkg/execution/execution.go +++ b/pkg/execution/execution.go @@ -21,57 +21,76 @@ package execution import ( - "context" "errors" "time" "github.com/dop251/goja" "go.k6.io/k6/js/common" + "go.k6.io/k6/js/modules" "go.k6.io/k6/lib" ) -// Execution is a JS module to return information about the execution in progress. -type Execution struct{} +type ( + // RootModule is the global module instance that will create module + // instances for each VU. + RootModule struct{} -// New returns a pointer to a new Execution. -func New() *Execution { - return &Execution{} -} - -// GetVUStats returns information about the currently executing VU. -func (e *Execution) GetVUStats(ctx context.Context) (goja.Value, error) { - vuState := lib.GetState(ctx) - if vuState == nil { - return nil, errors.New("getting VU information in the init context is not supported") + // ModuleInstance represents an instance of the execution module. + ModuleInstance struct { + modules.InstanceCore + obj *goja.Object } +) - rt := common.GetRuntime(ctx) - if rt == nil { - return nil, errors.New("goja runtime is nil in context") - } +var ( + _ modules.IsModuleV2 = &RootModule{} + _ modules.Instance = &ModuleInstance{} +) - stats := map[string]interface{}{ - "id": vuState.VUID, - "idGlobal": vuState.VUIDGlobal, - "iteration": vuState.Iteration, - "iterationScenario": func() goja.Value { - return rt.ToValue(vuState.GetScenarioVUIter()) - }, - } +// New returns a pointer to a new RootModule instance. +func New() *RootModule { + return &RootModule{} +} - obj, err := newLazyJSObject(rt, stats) - if err != nil { - return nil, err +// NewModuleInstance implements the modules.IsModuleV2 interface to return +// a new instance for each VU. +func (*RootModule) NewModuleInstance(m modules.InstanceCore) modules.Instance { + mi := &ModuleInstance{InstanceCore: m} + rt := m.GetRuntime() + o := rt.NewObject() + defProp := func(name string, newInfo func() (*goja.Object, error)) { + err := o.DefineAccessorProperty(name, rt.ToValue(func() goja.Value { + obj, err := newInfo() + if err != nil { + common.Throw(rt, err) + } + return obj + }), nil, goja.FLAG_FALSE, goja.FLAG_TRUE) + if err != nil { + common.Throw(rt, err) + } } + defProp("scenario", mi.newScenarioInfo) + defProp("test", mi.newTestInfo) + defProp("vu", mi.newVUInfo) + + mi.obj = o - return obj, nil + return mi } -// GetScenarioStats returns information about the currently executing scenario. -func (e *Execution) GetScenarioStats(ctx context.Context) (goja.Value, error) { - ss := lib.GetScenarioState(ctx) +// GetExports returns the exports of the execution module. +func (mi *ModuleInstance) GetExports() modules.Exports { + return modules.Exports{Default: mi.obj} +} + +// newScenarioInfo returns a goja.Object with property accessors to retrieve +// information about the scenario the current VU is running in. +func (mi *ModuleInstance) newScenarioInfo() (*goja.Object, error) { + ctx := mi.GetContext() vuState := lib.GetState(ctx) + ss := lib.GetScenarioState(ctx) if ss == nil || vuState == nil { return nil, errors.New("getting scenario information in the init context is not supported") } @@ -81,35 +100,40 @@ func (e *Execution) GetScenarioStats(ctx context.Context) (goja.Value, error) { return nil, errors.New("goja runtime is nil in context") } - var iterGlobal interface{} - if vuState.GetScenarioGlobalVUIter != nil { - iterGlobal = vuState.GetScenarioGlobalVUIter() - } else { - iterGlobal = goja.Null() - } - - stats := map[string]interface{}{ - "name": ss.Name, - "executor": ss.Executor, - "startTime": float64(ss.StartTime.UnixNano()) / 1e9, - "progress": func() goja.Value { + si := map[string]func() interface{}{ + "name": func() interface{} { + ctx := mi.GetContext() + ss := lib.GetScenarioState(ctx) + return ss.Name + }, + "executor": func() interface{} { + ctx := mi.GetContext() + ss := lib.GetScenarioState(ctx) + return ss.Executor + }, + "startTime": func() interface{} { return float64(ss.StartTime.UnixNano()) / 1e9 }, + "progress": func() interface{} { p, _ := ss.ProgressFn() - return rt.ToValue(p) + return p + }, + "iteration": func() interface{} { + return vuState.GetScenarioLocalVUIter() + }, + "iterationGlobal": func() interface{} { + if vuState.GetScenarioGlobalVUIter != nil { + return vuState.GetScenarioGlobalVUIter() + } + return goja.Null() }, - "iteration": vuState.GetScenarioLocalVUIter(), - "iterationGlobal": iterGlobal, - } - - obj, err := newLazyJSObject(rt, stats) - if err != nil { - return nil, err } - return obj, nil + return newInfoObj(rt, si) } -// GetTestInstanceStats returns test information for the current k6 instance. -func (e *Execution) GetTestInstanceStats(ctx context.Context) (goja.Value, error) { +// newTestInfo returns a goja.Object with property accessors to retrieve +// information about the overall test run (local instance). +func (mi *ModuleInstance) newTestInfo() (*goja.Object, error) { + ctx := mi.GetContext() es := lib.GetExecutionState(ctx) if es == nil { return nil, errors.New("getting test information in the init context is not supported") @@ -120,48 +144,62 @@ func (e *Execution) GetTestInstanceStats(ctx context.Context) (goja.Value, error return nil, errors.New("goja runtime is nil in context") } - stats := map[string]interface{}{ - "duration": func() goja.Value { - return rt.ToValue(float64(es.GetCurrentTestRunDuration()) / float64(time.Millisecond)) + ti := map[string]func() interface{}{ + "duration": func() interface{} { + return float64(es.GetCurrentTestRunDuration()) / float64(time.Millisecond) }, - "iterationsCompleted": func() goja.Value { - return rt.ToValue(es.GetFullIterationCount()) + "iterationsCompleted": func() interface{} { + return es.GetFullIterationCount() }, - "iterationsInterrupted": func() goja.Value { - return rt.ToValue(es.GetPartialIterationCount()) + "iterationsInterrupted": func() interface{} { + return es.GetPartialIterationCount() }, - "vusActive": func() goja.Value { - return rt.ToValue(es.GetCurrentlyActiveVUsCount()) + "vusActive": func() interface{} { + return es.GetCurrentlyActiveVUsCount() }, - "vusMax": func() goja.Value { - return rt.ToValue(es.GetInitializedVUsCount()) + "vusMax": func() interface{} { + return es.GetInitializedVUsCount() }, } - obj, err := newLazyJSObject(rt, stats) - if err != nil { - return nil, err + return newInfoObj(rt, ti) +} + +// newVUInfo returns a goja.Object with property accessors to retrieve +// information about the currently executing VU. +func (mi *ModuleInstance) newVUInfo() (*goja.Object, error) { + ctx := mi.GetContext() + vuState := lib.GetState(ctx) + if vuState == nil { + return nil, errors.New("getting VU information in the init context is not supported") + } + + rt := common.GetRuntime(ctx) + if rt == nil { + return nil, errors.New("goja runtime is nil in context") } - return obj, nil + vi := map[string]func() interface{}{ + "id": func() interface{} { return vuState.VUID }, + "idGlobal": func() interface{} { return vuState.VUIDGlobal }, + "iteration": func() interface{} { return vuState.Iteration }, + "iterationScenario": func() interface{} { + return vuState.GetScenarioVUIter() + }, + } + + return newInfoObj(rt, vi) } -func newLazyJSObject(rt *goja.Runtime, data map[string]interface{}) (goja.Value, error) { - obj := rt.NewObject() +func newInfoObj(rt *goja.Runtime, props map[string]func() interface{}) (*goja.Object, error) { + o := rt.NewObject() - for k, v := range data { - if val, ok := v.(func() goja.Value); ok { - if err := obj.DefineAccessorProperty(k, rt.ToValue(val), - nil, goja.FLAG_FALSE, goja.FLAG_TRUE); err != nil { - return nil, err - } - } else { - if err := obj.DefineDataProperty(k, rt.ToValue(v), - goja.FLAG_FALSE, goja.FLAG_FALSE, goja.FLAG_TRUE); err != nil { - return nil, err - } + for p, get := range props { + err := o.DefineAccessorProperty(p, rt.ToValue(get), nil, goja.FLAG_FALSE, goja.FLAG_TRUE) + if err != nil { + return nil, err } } - return obj, nil + return o, nil } diff --git a/pkg/execution/execution_test.go b/pkg/execution/execution_test.go index e8d66a3..fe315d0 100644 --- a/pkg/execution/execution_test.go +++ b/pkg/execution/execution_test.go @@ -27,7 +27,7 @@ func TestMain(m *testing.M) { os.Exit(m.Run()) } -func TestExecutionStatsVUSharing(t *testing.T) { +func TestExecutionInfoVUSharing(t *testing.T) { t.Parallel() script := []byte(` import exec from 'k6/x/execution'; @@ -58,14 +58,14 @@ func TestExecutionStatsVUSharing(t *testing.T) { }; export function cvus() { - const stats = Object.assign({scenario: 'cvus'}, exec.getVUStats()); - console.log(JSON.stringify(stats)); + const info = Object.assign({scenario: 'cvus'}, exec.vu); + console.log(JSON.stringify(info)); sleep(0.2); }; export function carr() { - const stats = Object.assign({scenario: 'carr'}, exec.getVUStats()); - console.log(JSON.stringify(stats)); + const info = Object.assign({scenario: 'carr'}, exec.vu); + console.log(JSON.stringify(info)); }; `) @@ -136,7 +136,7 @@ func TestExecutionStatsVUSharing(t *testing.T) { } } -func TestExecutionStatsScenarioIter(t *testing.T) { +func TestExecutionInfoScenarioIter(t *testing.T) { t.Parallel() script := []byte(` import exec from 'k6/x/execution'; @@ -166,13 +166,13 @@ func TestExecutionStatsScenarioIter(t *testing.T) { }; export function pvu() { - const stats = Object.assign({VUID: __VU}, exec.getScenarioStats()); - console.log(JSON.stringify(stats)); + const info = Object.assign({VUID: __VU}, exec.scenario); + console.log(JSON.stringify(info)); } export function carr() { - const stats = Object.assign({VUID: __VU}, exec.getScenarioStats()); - console.log(JSON.stringify(stats)); + const info = Object.assign({VUID: __VU}, exec.scenario); + console.log(JSON.stringify(info)); }; `) @@ -248,9 +248,8 @@ func TestSharedIterationsStable(t *testing.T) { }, }; export default function () { - const stats = exec.getScenarioStats(); sleep(1); - console.log(JSON.stringify(Object.assign({VUID: __VU}, stats))); + console.log(JSON.stringify(Object.assign({VUID: __VU}, exec.scenario))); } `) @@ -305,7 +304,7 @@ func TestSharedIterationsStable(t *testing.T) { } } -func TestExecutionStats(t *testing.T) { +func TestExecutionInfo(t *testing.T) { t.Parallel() testCases := []struct { @@ -315,48 +314,47 @@ func TestExecutionStats(t *testing.T) { var exec = require('k6/x/execution'); exports.default = function() { - var vuStats = exec.getVUStats(); - if (vuStats.id !== 1) throw new Error('unexpected VU ID: '+vuStats.id); - if (vuStats.idGlobal !== 10) throw new Error('unexpected global VU ID: '+vuStats.idGlobal); - if (vuStats.iteration !== 0) throw new Error('unexpected VU iteration: '+vuStats.iteration); - if (vuStats.iterationScenario !== 0) throw new Error('unexpected scenario iteration: '+vuStats.iterationScenario); + if (exec.vu.id !== 1) throw new Error('unexpected VU ID: '+exec.vu.id); + if (exec.vu.idGlobal !== 10) throw new Error('unexpected global VU ID: '+exec.vu.idGlobal); + if (exec.vu.iteration !== 0) throw new Error('unexpected VU iteration: '+exec.vu.iteration); + if (exec.vu.iterationScenario !== 0) throw new Error('unexpected scenario iteration: '+exec.vu.iterationScenario); }`}, {name: "vu_err", script: ` var exec = require('k6/x/execution'); - exec.getVUStats(); + exec.vu; `, expErr: "getting VU information in the init context is not supported"}, {name: "scenario_ok", script: ` var exec = require('k6/x/execution'); var sleep = require('k6').sleep; exports.default = function() { - var ss = exec.getScenarioStats(); + var si = exec.scenario; sleep(0.1); - if (ss.name !== 'default') throw new Error('unexpected scenario name: '+ss.name); - if (ss.executor !== 'test-exec') throw new Error('unexpected executor: '+ss.executor); - if (ss.startTime > new Date().getTime()) throw new Error('unexpected startTime: '+ss.startTime); - if (ss.progress !== 0.1) throw new Error('unexpected progress: '+ss.progress); - if (ss.iteration !== 3) throw new Error('unexpected scenario local iteration: '+ss.iteration); - if (ss.iterationGlobal !== 4) throw new Error('unexpected scenario local iteration: '+ss.iterationGlobal); + if (si.name !== 'default') throw new Error('unexpected scenario name: '+si.name); + if (si.executor !== 'test-exec') throw new Error('unexpected executor: '+si.executor); + if (si.startTime > new Date().getTime()) throw new Error('unexpected startTime: '+si.startTime); + if (si.progress !== 0.1) throw new Error('unexpected progress: '+si.progress); + if (si.iteration !== 3) throw new Error('unexpected scenario local iteration: '+si.iteration); + if (si.iterationGlobal !== 4) throw new Error('unexpected scenario local iteration: '+si.iterationGlobal); }`}, {name: "scenario_err", script: ` var exec = require('k6/x/execution'); - exec.getScenarioStats(); + exec.scenario; `, expErr: "getting scenario information in the init context is not supported"}, {name: "test_ok", script: ` var exec = require('k6/x/execution'); exports.default = function() { - var ts = exec.getTestInstanceStats(); - if (ts.duration !== 0) throw new Error('unexpected test duration: '+ts.duration); - if (ts.vusActive !== 1) throw new Error('unexpected vusActive: '+ts.vusActive); - if (ts.vusMax !== 0) throw new Error('unexpected vusMax: '+ts.vusMax); - if (ts.iterationsCompleted !== 0) throw new Error('unexpected iterationsCompleted: '+ts.iterationsCompleted); - if (ts.iterationsInterrupted !== 0) throw new Error('unexpected iterationsInterrupted: '+ts.iterationsInterrupted); + var ti = exec.test; + if (ti.duration !== 0) throw new Error('unexpected test duration: '+ti.duration); + if (ti.vusActive !== 1) throw new Error('unexpected vusActive: '+ti.vusActive); + if (ti.vusMax !== 0) throw new Error('unexpected vusMax: '+ti.vusMax); + if (ti.iterationsCompleted !== 0) throw new Error('unexpected iterationsCompleted: '+ti.iterationsCompleted); + if (ti.iterationsInterrupted !== 0) throw new Error('unexpected iterationsInterrupted: '+ti.iterationsInterrupted); }`}, {name: "test_err", script: ` var exec = require('k6/x/execution'); - exec.getTestInstanceStats(); + exec.test; `, expErr: "getting test information in the init context is not supported"}, }