From dc9f3b7b653c21689392b598b4b961798fee3778 Mon Sep 17 00:00:00 2001 From: Ivan <2103732+codebien@users.noreply.github.com> Date: Tue, 12 Oct 2021 16:13:05 +0200 Subject: [PATCH 1/2] js/k6/exec: Added scenario.stage object The stage object shares information about the current scenario's stage. --- js/modules/k6/execution/execution.go | 15 +++++ js/modules/k6/execution/execution_test.go | 74 +++++++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 js/modules/k6/execution/execution_test.go diff --git a/js/modules/k6/execution/execution.go b/js/modules/k6/execution/execution.go index 56e944499ca..3bf70bf95ae 100644 --- a/js/modules/k6/execution/execution.go +++ b/js/modules/k6/execution/execution.go @@ -130,6 +130,21 @@ func (mi *ModuleInstance) newScenarioInfo() (*goja.Object, error) { "iterationInTest": func() interface{} { return vuState.GetScenarioGlobalVUIter() }, + "stage": func() interface{} { + stage, err := getScenarioState().CurrentStage() + if err != nil { + common.Throw(rt, err) + } + si := map[string]func() interface{}{ + "number": func() interface{} { return stage.Index }, + "name": func() interface{} { return stage.Name }, + } + obj, err := newInfoObj(rt, si) + if err != nil { + common.Throw(rt, err) + } + return obj + }, } return newInfoObj(rt, si) diff --git a/js/modules/k6/execution/execution_test.go b/js/modules/k6/execution/execution_test.go new file mode 100644 index 00000000000..f539189acc8 --- /dev/null +++ b/js/modules/k6/execution/execution_test.go @@ -0,0 +1,74 @@ +/* + * + * k6 - a next-generation load testing tool + * Copyright (C) 2021 Load Impact + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +package execution + +import ( + "context" + "testing" + "time" + + "github.com/dop251/goja" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.k6.io/k6/js/common" + "go.k6.io/k6/js/modulestest" + "go.k6.io/k6/lib" +) + +func TestScenarioStage(t *testing.T) { + t.Parallel() + + rt := goja.New() + ctx := common.WithRuntime(context.Background(), rt) + ctx = lib.WithScenarioState(ctx, &lib.ScenarioState{ + Stages: []lib.ScenarioStage{ + { + Index: 0, + Name: "ramp up", + Duration: 10 * time.Second, + }, + { + Index: 1, + Name: "ramp down", + Duration: 10 * time.Second, + }, + }, + StartTime: time.Now().Add(-11 * time.Second), + }) + m, ok := New().NewModuleInstance( + &modulestest.InstanceCore{ + Runtime: rt, + InitEnv: &common.InitEnvironment{}, + State: &lib.State{}, + Ctx: ctx, + }, + ).(*ModuleInstance) + require.True(t, ok) + require.NoError(t, rt.Set("exec", m.GetExports().Default)) + + num, err := rt.RunString(`exec.scenario.stage.number`) + require.NoError(t, err) + assert.Equal(t, int64(1), num.ToInteger()) + + stage, err := rt.RunString(`exec.scenario.stage.name`) + require.NoError(t, err) + assert.Equal(t, "ramp down", stage.String()) +} From e9ed2c5c77d1925c52f9ed958f8eb7b3e10aed08 Mon Sep 17 00:00:00 2001 From: Ivan <2103732+codebien@users.noreply.github.com> Date: Tue, 12 Oct 2021 16:13:38 +0200 Subject: [PATCH 2/2] lib: Track the Stages in scenario state Added a StagesDurations for tracking Scenario's Stages. Populate it from Ramping VUs and Ramping arrival rate executors. --- lib/executor/ramping_arrival_rate.go | 11 +++ lib/executor/ramping_vus.go | 12 +++ lib/executors.go | 42 +++++++++- lib/executors_test.go | 118 +++++++++++++++++++++++++++ 4 files changed, 180 insertions(+), 3 deletions(-) create mode 100644 lib/executors_test.go diff --git a/lib/executor/ramping_arrival_rate.go b/lib/executor/ramping_arrival_rate.go index a2736205ec9..46b678a5d6d 100644 --- a/lib/executor/ramping_arrival_rate.go +++ b/lib/executor/ramping_arrival_rate.go @@ -398,6 +398,17 @@ func (varr RampingArrivalRate) Run( Executor: varr.config.Type, StartTime: startTime, ProgressFn: progressFn, + Stages: func() []lib.ScenarioStage { + stages := make([]lib.ScenarioStage, 0, len(varr.config.Stages)) + for i, s := range varr.config.Stages { + stages = append(stages, lib.ScenarioStage{ + Index: uint(i), + Name: s.Name.String, + Duration: time.Duration(s.Duration.Duration), + }) + } + return stages + }(), }) returnVU := func(u lib.InitializedVU) { diff --git a/lib/executor/ramping_vus.go b/lib/executor/ramping_vus.go index 8c037cf8941..a6551b429a7 100644 --- a/lib/executor/ramping_vus.go +++ b/lib/executor/ramping_vus.go @@ -52,6 +52,7 @@ func init() { // Stage contains type Stage struct { + Name null.String `json:"name"` Duration types.NullDuration `json:"duration"` Target null.Int `json:"target"` // TODO: maybe rename this to endVUs? something else? // TODO: add a progression function? @@ -572,6 +573,17 @@ func (vlv RampingVUs) Run( Executor: vlv.config.Type, StartTime: startTime, ProgressFn: progressFn, + Stages: func() []lib.ScenarioStage { + stages := make([]lib.ScenarioStage, 0, len(vlv.config.Stages)) + for i, s := range vlv.config.Stages { + stages = append(stages, lib.ScenarioStage{ + Index: uint(i), + Name: s.Name.String, + Duration: time.Duration(s.Duration.Duration), + }) + } + return stages + }(), }) vuHandles := make([]*vuHandle, maxVUs) diff --git a/lib/executors.go b/lib/executors.go index 1929c72e71f..c51df42873a 100644 --- a/lib/executors.go +++ b/lib/executors.go @@ -113,9 +113,45 @@ type ExecutorConfig interface { // ScenarioState holds runtime scenario information returned by the k6/execution // JS module. type ScenarioState struct { - Name, Executor string - StartTime time.Time - ProgressFn func() (float64, []string) + Name string + Executor string + StartTime time.Time + ProgressFn func() (float64, []string) + Stages []ScenarioStage +} + +// ScenarioStage represents a Scenario's Stage. +// where Index tracks the original slice's position of the Stage. +type ScenarioStage struct { + Index uint + Name string + Duration time.Duration +} + +// CurrentStage returns the detected Stage that is currently running +// based on the StartTime of the Scenario. +func (s *ScenarioState) CurrentStage() (*ScenarioStage, error) { + if len(s.Stages) < 1 { + // TODO: improve this error message + return nil, fmt.Errorf("can't get the current Stage because any Stage has been defined") + } + + // sum represents the stages passed + sum := int64(0) + elapsed := time.Since(s.StartTime) + for _, stage := range s.Stages { + sum += int64(stage.Duration) + // when elapsed is smaller than sum + // then the current stage has been found + if int64(elapsed) < sum { + return &stage, nil + } + } + + // it happen when: + // * the total duration is equal to the latest stage's upper limit + // * the latest stage is taking more than the expected defined duration + return &s.Stages[len(s.Stages)-1], nil } // InitVUFunc is just a shorthand so we don't have to type the function diff --git a/lib/executors_test.go b/lib/executors_test.go new file mode 100644 index 00000000000..f8f6bbd05f9 --- /dev/null +++ b/lib/executors_test.go @@ -0,0 +1,118 @@ +package lib + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestScenarioStateCurrentStage(t *testing.T) { + t.Parallel() + + t.Run("Success", func(t *testing.T) { + t.Parallel() + // this just asserts that the fields are populated as expected + s1 := ScenarioStage{ + Index: 1, + Name: "stage1", + Duration: time.Second, + } + + state := ScenarioState{ + Stages: []ScenarioStage{ + { + Index: 0, + Duration: 2 * time.Second, + }, + s1, + }, + } + stage, err := state.CurrentStage() + require.NoError(t, err) + assert.Equal(t, &s1, stage) + }) + + t.Run("SuccessEdgeCases", func(t *testing.T) { + t.Parallel() + tests := []struct { + name string + stages []time.Duration + elapsed time.Duration // it fakes the elapsed scenario time + expIndex uint + }{ + { + name: "ZeroTime", + stages: []time.Duration{5 * time.Second, 20 * time.Second}, + elapsed: 0, + expIndex: 0, + }, + { + name: "FirstStage", + stages: []time.Duration{5 * time.Second, 20 * time.Second}, + elapsed: 4 * time.Second, + expIndex: 0, + }, + { + name: "MiddleStage", + stages: []time.Duration{5 * time.Second, 20 * time.Second, 10 * time.Second}, + elapsed: 10 * time.Second, + expIndex: 1, + }, + { + name: "StageUpperLimit", + stages: []time.Duration{5 * time.Second, 20 * time.Second, 10 * time.Second}, + elapsed: 25 * time.Second, + expIndex: 2, + }, + { + name: "OverLatestStage", + stages: []time.Duration{5 * time.Second, 20 * time.Second}, + elapsed: 30 * time.Second, + expIndex: 1, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + stages := func() []ScenarioStage { + stages := make([]ScenarioStage, 0, len(tc.stages)) + for i, duration := range tc.stages { + stage := ScenarioStage{ + Index: uint(i), + Duration: duration, + } + if uint(i) == tc.expIndex { + stage.Name = tc.name + } + stages = append(stages, stage) + } + return stages + } + + state := ScenarioState{ + Stages: stages(), + StartTime: time.Now().Add(-tc.elapsed), + } + + stage, err := state.CurrentStage() + require.NoError(t, err) + assert.Equal(t, tc.expIndex, stage.Index) + assert.Equal(t, tc.name, stage.Name) + }) + } + }) + + t.Run("ErrorOnEmpty", func(t *testing.T) { + t.Parallel() + state := ScenarioState{} + stage, err := state.CurrentStage() + require.NotNil(t, err) + assert.Contains(t, err.Error(), "any Stage") + assert.Nil(t, stage) + }) +}