diff --git a/go.mod b/go.mod index 3266b25..acaf823 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,6 @@ require ( github.com/sirupsen/logrus v1.8.1 github.com/spf13/afero v1.1.2 github.com/stretchr/testify v1.7.0 - go.k6.io/k6 v0.32.1-0.20210622082042-2039c5691bbe + go.k6.io/k6 v0.32.1-0.20210624122905-f24bd9a86806 gopkg.in/guregu/null.v3 v3.3.0 ) diff --git a/go.sum b/go.sum index d52b342..0d5cefc 100644 --- a/go.sum +++ b/go.sum @@ -329,8 +329,8 @@ github.com/zyedidia/highlight v0.0.0-20170330143449-201131ce5cf5/go.mod h1:c1r+O go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.k6.io/k6 v0.31.2-0.20210510132435-c2958278a362/go.mod h1:WmqQqrJOnATU2o+vmmfPejQquY7DDsfUflLWUpu+LX0= go.k6.io/k6 v0.31.2-0.20210511090412-61f464b99a2d/go.mod h1:5BTMcTH7K+IEoBUPBRM15M9c97nBqeKzQfop868FMiw= -go.k6.io/k6 v0.32.1-0.20210622082042-2039c5691bbe h1:k3IcFb/gUSo/XQPLdaaw7YJXBrPY/Y+AHEh3HtfXNy0= -go.k6.io/k6 v0.32.1-0.20210622082042-2039c5691bbe/go.mod h1:SNG6/ZknLfIqNGbYUfORZT6sNdUuBJ15ntzt8jZloc0= +go.k6.io/k6 v0.32.1-0.20210624122905-f24bd9a86806 h1:8DkI2JZ289voqprAvvDYPDTO49qpXsi4Pxeac+9zVH8= +go.k6.io/k6 v0.32.1-0.20210624122905-f24bd9a86806/go.mod h1:SNG6/ZknLfIqNGbYUfORZT6sNdUuBJ15ntzt8jZloc0= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= diff --git a/pkg/execution/execution.go b/pkg/execution/execution.go index 1e36408..0d5f6c6 100644 --- a/pkg/execution/execution.go +++ b/pkg/execution/execution.go @@ -23,6 +23,7 @@ package execution import ( "context" "errors" + "sort" "time" "github.com/dop251/goja" @@ -31,47 +32,101 @@ import ( "go.k6.io/k6/lib" ) -// Execution is a JS module to return information about the execution in progress. -type Execution struct{} +type ( + // RootExecution is the global module instance that will create module + // instances for each VU. + RootExecution struct{} + // Execution is a JS module that returns information about the currently + // executing test run. + Execution struct{ *goja.Proxy } +) -// New returns a pointer to a new Execution. -func New() *Execution { +// NewModuleInstancePerVU fulfills the k6 modules.HasModuleInstancePerVU +// interface so that each VU will get a separate copy of the module. +func (*RootExecution) NewModuleInstancePerVU() interface{} { 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") +// New returns a pointer to a new RootExecution instance. +func New() *RootExecution { + return &RootExecution{} +} + +// WithContext fulfills the k6 modules.HasWithContext interface to allow +// retrieving the VU, scenario and test state from the context used by each VU. +// It initializes a goja.Proxy object for the per-VU module instance, which in +// turn retrieves goja.DynamicObject instances for each property (scenario, vu, +// test). +func (e *Execution) WithContext(getCtx func() context.Context) { + keys := []string{"scenario", "vu", "test"} + + pcfg := goja.ProxyTrapConfig{ + OwnKeys: func(target *goja.Object) *goja.Object { + ctx := getCtx() + rt := common.GetRuntime(ctx) + return rt.ToValue(keys).ToObject(rt) + }, + Has: func(target *goja.Object, prop string) (available bool) { + return sort.SearchStrings(keys, prop) != -1 + }, + Get: func(target *goja.Object, prop string, r goja.Value) goja.Value { + return dynObjValue(getCtx, target, prop) + }, + GetOwnPropertyDescriptor: func(target *goja.Object, prop string) (desc goja.PropertyDescriptor) { + desc.Enumerable, desc.Configurable = goja.FLAG_TRUE, goja.FLAG_TRUE + desc.Value = dynObjValue(getCtx, target, prop) + return desc + }, } + ctx := getCtx() rt := common.GetRuntime(ctx) - if rt == nil { - return nil, errors.New("goja runtime is nil in context") + proxy := rt.NewProxy(rt.NewObject(), &pcfg) + e.Proxy = &proxy +} + +// dynObjValue returns a goja.Value for a specific prop on target. +func dynObjValue(getCtx func() context.Context, target *goja.Object, prop string) goja.Value { + v := target.Get(prop) + if v != nil { + return v } - stats := map[string]interface{}{ - "id": vuState.VUID, - "idGlobal": vuState.VUIDGlobal, - "iteration": vuState.Iteration, - "iterationScenario": func() goja.Value { - return rt.ToValue(vuState.GetScenarioVUIter()) - }, + ctx := getCtx() + rt := common.GetRuntime(ctx) + var ( + dobj *execInfo + err error + ) + switch prop { + case "scenario": + dobj, err = newScenarioInfo(getCtx) + case "test": + dobj, err = newTestInfo(getCtx) + case "vu": + dobj, err = newVUInfo(getCtx) } - obj, err := newLazyJSObject(rt, stats) if err != nil { - return nil, err + // TODO: Something less drastic? + common.Throw(rt, err) } - return obj, nil + if dobj != nil { + v = rt.NewDynamicObject(dobj) + } + if err := target.Set(prop, v); err != nil { + common.Throw(rt, err) + } + return v } -// GetScenarioStats returns information about the currently executing scenario. -func (e *Execution) GetScenarioStats(ctx context.Context) (goja.Value, error) { - ss := lib.GetScenarioState(ctx) +// newScenarioInfo returns a goja.DynamicObject implementation to retrieve +// information about the scenario the current VU is running in. +func newScenarioInfo(getCtx func() context.Context) (*execInfo, error) { + ctx := getCtx() 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 +136,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 := getCtx() + ss := lib.GetScenarioState(ctx) + return ss.Name + }, + "executor": func() interface{} { + ctx := getCtx() + 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 newExecInfo(rt, si), nil } -// GetTestInstanceStats returns test information for the current k6 instance. -func (e *Execution) GetTestInstanceStats(ctx context.Context) (goja.Value, error) { +// newTestInfo returns a goja.DynamicObject implementation to retrieve +// information about the overall test run (local instance). +func newTestInfo(getCtx func() context.Context) (*execInfo, error) { + ctx := getCtx() es := lib.GetExecutionState(ctx) if es == nil { return nil, errors.New("getting test information in the init context is not supported") @@ -120,48 +180,85 @@ 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 newExecInfo(rt, ti), nil +} + +// newVUInfo returns a goja.DynamicObject implementation to retrieve +// information about the currently executing VU. +func newVUInfo(getCtx func() context.Context) (*execInfo, error) { + ctx := getCtx() + 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 newExecInfo(rt, vi), nil } -func newLazyJSObject(rt *goja.Runtime, data map[string]interface{}) (goja.Value, error) { - obj := rt.NewObject() +// execInfo is a goja.DynamicObject implementation to lazily return data only +// on property access. +type execInfo struct { + rt *goja.Runtime + obj map[string]func() interface{} + keys []string +} - 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 - } - } +var _ goja.DynamicObject = &execInfo{} + +func newExecInfo(rt *goja.Runtime, obj map[string]func() interface{}) *execInfo { + keys := make([]string, 0, len(obj)) + for k := range obj { + keys = append(keys, k) } + return &execInfo{obj: obj, keys: keys, rt: rt} +} - return obj, nil +func (ei *execInfo) Get(key string) goja.Value { + if fn, ok := ei.obj[key]; ok { + return ei.rt.ToValue(fn()) + } + return goja.Undefined() } + +func (ei *execInfo) Set(key string, val goja.Value) bool { return false } + +func (ei *execInfo) Has(key string) bool { + _, has := ei.obj[key] + return has +} + +func (ei *execInfo) Delete(key string) bool { return false } + +func (ei *execInfo) Keys() []string { return ei.keys } 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"}, }