Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scenario's stage number #2199

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions js/modules/k6/execution/execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
74 changes: 74 additions & 0 deletions js/modules/k6/execution/execution_test.go
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*
*/

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())
}
11 changes: 11 additions & 0 deletions lib/executor/ramping_arrival_rate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
12 changes: 12 additions & 0 deletions lib/executor/ramping_vus.go
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down Expand Up @@ -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{
Comment on lines +577 to +579
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: you can just make with the appropriate len(instead of cap) and do stages[i] = ... instead of append

Index: uint(i),
Name: s.Name.String,
Duration: time.Duration(s.Duration.Duration),
})
}
return stages
}(),
})

vuHandles := make([]*vuHandle, maxVUs)
Expand Down
42 changes: 39 additions & 3 deletions lib/executors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Comment on lines +134 to +137
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is going to throw an exception on executors with no stages? I am not certain this is the best idea instead of returning undefined for example, but 🤷 works as well.


// 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
}
}
Comment on lines +139 to +149
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really think that trying to calculate the exact stage on each call is a good idea. I really don't think that will be what most users will care about and if they do there is an easy way to do it (as you do here).

But I would expect it will be really surprising to people accessing exec.scenario.stage.number in two different parts of their iteration and it ... changing.

I do expect that later one will definitely get them to scratch their head quite a bit and try to figure out how did they write their code in such a way that a key they just added with exec.scenario.stage.number is now not there 🤔


// 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
Expand Down
118 changes: 118 additions & 0 deletions lib/executors_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}