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

Utils for tagging stages #6

Merged
merged 1 commit into from
Apr 29, 2022
Merged
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
149 changes: 149 additions & 0 deletions src/stages.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import exec from 'k6/execution'

// parseDuration parses the provided string as an Integer number
// in millisecond precision
function parseDuration(str) {
codebien marked this conversation as resolved.
Show resolved Hide resolved
if (str == null || str.length < 1) {
throw new Error('str is empty')
}

// the sum in millisecond of the parsed duration
let d = 0;

// current contains the partial seen number
// it's reset when a time unit is found
let current = '';


// it tracks the seen time units
// and it denies eventual duplicated
let seen = {}

for (let i = 0; i < str.length; i++) {
// append the current char if it's a number or a decimal separator
if (isNumber(str[i]) || str[i] == '.') {
current += str[i]
}

// return if the next char is not a time unit
if (str[i+1] == null || isNumber(str[i+1]) || str[i+1] == '.') {
continue
}

let v = parseFloat(current, 10)
let next = str[i+1]

switch (next) {
case 'd':
d += v*24*60*60*1000
break;
case 'h':
d += v*60*60*1000
break;
case 'm':
if (i + 2 < str.length && str[i+2] == 's') {
// millisecond is the maximum precision
// truncate eventual decimal
d += Math.trunc(v)
i++
next = 'ms'
} else {
d += v*60*1000
}
break;
case 's':
d += v*1000
break;
default:
codebien marked this conversation as resolved.
Show resolved Hide resolved
throw new Error(`${next} is an unsupported time unit`)
}
if (seen[next]) {
throw new Error(`${next} time unit is provided multiple times`)
}
seen[next] = true
i++
current = ''
}
// flush in case no time unit has been provided
// for the latest group
if (current.length > 0) {
d += parseFloat(current, 10)
}
return d
}

// isNumber return true if the c character is a number
function isNumber(c) {
return c >= '0' && c <= '9'
}

// getCurrentStageIndex returns the computed index of the running stage.
function getCurrentStageIndex() {
let scenario = exec.test.options.scenarios[exec.scenario.name]
if (scenario == null) {
throw new Error(`the exec.test.options object doesn't contain the current scenario ${exec.scenario.name}`)
}
if (scenario.stages == null) {
throw new Error(`only ramping-vus or ramping-arravial-rate supports stages, it is not possible to get a stage index on other executors.`)
}

if (scenario.stages.length < 1) {
throw new Error(`the current scenario ${scenario.name} doesn't contain any stage`)
}

let sum = 0;
let elapsed = new Date() - exec.scenario.startTime
for (let i = 0; i < scenario.stages.length; i++) {
sum += parseDuration(scenario.stages[i].duration)
if (elapsed < sum) {
return i
}
}

return scenario.stages.length-1
}

// tagWithCurrentStageIndex adds a tag with a `stage` key
// and the index of the current running stage as value.
function tagWithCurrentStageIndex() {
exec.vu.tags['stage'] = getCurrentStageIndex()
}

// tagWithCurrentStageProfile adds a tag with a `stage` key
// and the profile (ramp-up, steady or ramp-down) computed
// from the current running stage.
function tagWithCurrentStageProfile() {
//ramp-up when previous.target < current.target
//ramp-down when previous.target > current.target
//steady when prevuious.target = current.target

let getStageProfile = function() {
let currentIndex = getCurrentStageIndex()
if (currentIndex < 1) {
return 'ramp-up'
}

let stages = exec.test.options.scenarios[exec.scenario.name].stages
let current = stages[currentIndex]
let previous = stages[currentIndex-1]

if (current.target > previous.target) {
return 'ramp-up'
}

if (previous.target == current.target) {
return 'steady'
}

return 'ramp-down'
}

exec.vu.tags['stage_profile'] = getStageProfile()
}

export {
parseDuration,
getCurrentStageIndex,
tagWithCurrentStageIndex,
tagWithCurrentStageProfile,
}
247 changes: 247 additions & 0 deletions tests/stages.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
import { describe, expect } from 'https://jslib.k6.io/k6chaijs/4.3.4.0/index.js'
import exec from 'k6/execution'
import { sleep } from 'k6'

import {
parseDuration,
getCurrentStageIndex,
tagWithCurrentStageIndex,
tagWithCurrentStageProfile,
} from '../src/stages.js'


export let options = {
scenarios: {
'parseDuration': {
executor: 'shared-iterations',
iterations: 1,
exec: 'testParseDuration'
},
'getCurrentStageIndex': {
executor: 'ramping-vus',
stages: [
{ duration: "1s10ms", target: 1 },
{ duration: "1s", target: 1 },
],
exec: 'testGetCurrentStageIndex'
},
'tagWithCurrentStageIndex': {
executor: 'ramping-vus',
stages: [
{ duration: "1s", target: 1 },
{ duration: "1s", target: 1 },
],
exec: 'testTagWithCurrentStageIndex'
},
'tagWithRampUpWhenOnlyOne': {
executor: 'ramping-vus',
stages: [
{ duration: "0.1s", target: 1 }
],
exec: 'testTagWithRampUpProfileWhenOnlyOne'
},
'tagWithRampUp': {
executor: 'ramping-vus',
stages: [
{ duration: "0.1s", target: 1 },
{ duration: "0.1s", target: 2 }
],
exec: 'testTagWithRampUp'
},
'tagWithSteady': {
executor: 'ramping-vus',
stages: [
{ duration: "0.1s", target: 1 },
{ duration: "0.1s", target: 1 }
],
exec: 'testTagWithSteady'
},
'tagWithRampDown': {
executor: 'ramping-vus',
stages: [
{ duration: "0.1s", target: 2 },
{ duration: "0.1s", target: 1 }
],
exec: 'testTagWithRampDown'
},
'unsupported-executor': {
executor: 'shared-iterations',
iterations: 1,
exec: 'testUnsupportedExecutor'
}
}
}

export function testGetCurrentStageIndex() {
describe('getCurrentStageIndex', () => {
describe('returns the first stage when is in t0', () => {
expect(getCurrentStageIndex()).to.be.equal(0)
})

sleep(1.2)

describe('returns the next stage if the iteration exceedes the expected duration', () => {
expect(getCurrentStageIndex()).to.be.equal(1)
})

sleep(1)

describe('returns the last stage when the iteration exceedes the total expected duration', () => {
expect(getCurrentStageIndex()).to.be.equal(1)
})
})
}

export function testTagWithCurrentStageIndex() {
describe('tagWithCurrentStageIndex', () => {
describe('tags with the current stage', () => {
tagWithCurrentStageIndex()
expect(exec.vu.tags['stage']).to.be.equal(`${exec.scenario.iterationInTest}`)
})

// it forces to get only two iterations
sleep(1)
})
}

export function testTagWithRampUpProfileWhenOnlyOne() {
describe('tagWithCurrentStageProfile', () => {
describe(`tags with ramp-up profile when only one stage`, () => {
tagWithCurrentStageProfile()
expect(exec.vu.tags['stage_profile']).to.be.equal('ramp-up')
})
})
}

export function testTagWithRampUp() {
describe('tagWithCurrentStageProfile', () => {
describe(`tags with ramp-up profile when the current stage has a target greater than previous`, () => {
if (exec.vu.iterationInScenario !== 1) {
return
}
tagWithCurrentStageProfile()
expect(exec.vu.tags['stage_profile']).to.be.equal('ramp-up')
})
sleep(0.1)
})
}

export function testTagWithSteady() {
describe('tagWithCurrentStageProfile', () => {
describe(`tags with steady profile when the current stage has the same target of the previous`, () => {
if (exec.vu.iterationInScenario !== 1) {
return
}
tagWithCurrentStageProfile()
expect(exec.vu.tags['stage_profile']).to.be.equal('steady')
})
sleep(0.1)
})
}

export function testTagWithRampDown() {
describe('tagWithCurrentStageProfile', () => {
describe(`tags with ramp-down profile when the current stage has a target less than previous`, () => {
if (exec.vu.iterationInScenario !== 1) {
return
}
tagWithCurrentStageProfile()
expect(exec.vu.tags['stage_profile']).to.be.equal('ramp-down')
})
sleep(0.1)
})
}

export function testUnsupportedExecutor() {
describe('throw Error if the executor is not supported', () => {
describe('getCurrentStageIndex', () => {
expect(getCurrentStageIndex).to.throw()
})
describe('tagWithCurrentStageIndex', () => {
expect(tagWithCurrentStageIndex).to.throw()
})
describe('tagWithCurrentStageProfile', () => {
expect(tagWithCurrentStageProfile).to.throw()
})
})
}

export function testParseDuration() {
codebien marked this conversation as resolved.
Show resolved Hide resolved
describe('parseDuration', () => {
let testcase = '1d5h31m20s9ms'
describe(testcase, () => {
expect(parseDuration(testcase)).to.be.equal(((86400+18000+1860+20)*1000)+9)
})

testcase = '5.2h'
describe(testcase, () => {
expect(parseDuration(testcase)).to.be.equal(18720*1000)
})

testcase = '1d5.2h31m20s9ms'
describe(testcase, () => {
expect(parseDuration(testcase)).to.be.equal(((86400+18720+1860+20)*1000)+9)
})

// ms is the maximum precision so this case is truncated
testcase = '9.3ms'
describe(testcase, () => {
expect(parseDuration(testcase)).to.be.equal(9)
})

testcase = '1531209'
describe(testcase, () => {
expect(parseDuration(testcase)).to.be.equal(1531209)
})

testcase = '9.h1.s'
describe(testcase, () => {
expect(parseDuration(testcase)).to.be.equal(32401*1000)
})

testcase = '9.h1.s'
describe(testcase, () => {
expect(parseDuration(testcase)).to.be.equal(32401*1000)
})

testcase = '1s12'
describe(testcase, () => {
expect(parseDuration(testcase)).to.be.equal(1012)
})

testcase = '1h-1s'
describe(testcase, () => {
expect(parseDuration(testcase)).to.be.equal(3601*1000)
})

testcase = '-1h 1s'
describe(testcase, () => {
expect(parseDuration(testcase)).to.be.equal(3601*1000)
})

testcase = '0'
describe(testcase, () => {
expect(parseDuration(testcase)).to.be.equal(0)
})

testcase = '0s'
describe(testcase, () => {
expect(parseDuration(testcase)).to.be.equal(0)
})

testcase = '1s 1s 1s'
describe(testcase, () => {
expect(() => { return parseDuration(testcase) }).to.throw()
})

testcase = '1w'
describe(testcase, () => {
expect(() => { return parseDuration(testcase) }).to.throw()
})

testcase = ''
describe(testcase, () => {
expect(() => { return parseDuration(testcase) }).to.throw()
})
})
}