11// SPDX-License-Identifier: Apache-2.0
22
3+ import { randomIntBetween } from 'https://jslib.k6.io/k6-utils/1.4.0/index.js' ;
34import { check , sleep } from 'k6' ;
45import { Gauge } from 'k6/metrics' ;
5- import { randomIntBetween } from 'https://jslib.k6.io/k6-utils/1.4.0/index.js' ;
66
77import { setDefaultValuesForEnvParameters } from './parameters.js' ;
8+ import { calculateRateAllocation } from './traffic-weights.js' ;
89
910setDefaultValuesForEnvParameters ( ) ;
1011
@@ -178,20 +179,24 @@ function defaultMetrics() {
178179}
179180
180181function getTestType ( ) {
181- return __ENV . TEST_TYPE !== undefined && __ENV . TEST_TYPE === 'load' ? 'load' : 'performance' ;
182+ if ( __ENV . TEST_TYPE === 'stress' ) return 'stress' ;
183+ if ( __ENV . TEST_TYPE === 'load' ) return 'load' ;
184+ return 'performance' ;
182185}
183186
184187function markdownReport ( data , isFirstColumnUrl , scenarios ) {
188+ const testType = getTestType ( ) ;
185189 const firstColumnName = isFirstColumnUrl ? 'URL' : 'Scenario' ;
186- const header = `| ${ firstColumnName } | VUS | Reqs | Pass % | RPS (1/s) | Pass RPS (1/s) | Avg. Req Duration (ms) | Median (ms) | Min (ms) | Max (ms) | P(90) (ms) | P(95) (ms) | Comment |
190+ const secondColumnName = getTestType ( ) === 'stress' ? 'Expected RPS' : 'VUS' ;
191+ const header = `| ${ firstColumnName } | ${ secondColumnName } | Reqs | Pass % | RPS (1/s) | Pass RPS (1/s) | Avg. Req Duration (ms) | Median (ms) | Min (ms) | Max (ms) | P(90) (ms) | P(95) (ms) | Comment |
187192|----------|-----|------|--------|-----|----------|-------------------|-------|-----|-----|-------|-------|---------|` ;
188193
189194 // collect the metrics
190195 const { metrics } = data ;
191196
192197 const isDebugMode = __ENV [ 'DEBUG_MODE' ] === 'true' ;
193198 if ( isDebugMode ) {
194- console . log ( " Raw metrics:" ) ;
199+ console . log ( ' Raw metrics:' ) ;
195200 console . log ( JSON . stringify ( metrics , null , 2 ) ) ;
196201 }
197202
@@ -228,8 +233,12 @@ function markdownReport(data, isFirstColumnUrl, scenarios) {
228233 markdown += `JSON-RPC-RELAY URL: ${ __ENV [ 'RELAY_BASE_URL' ] } \n\n` ;
229234 markdown += `Timestamp: ${ new Date ( Date . now ( ) ) . toISOString ( ) } \n\n` ;
230235 markdown += `Duration: ${ __ENV [ 'DEFAULT_DURATION' ] } \n\n` ;
231- markdown += `Test Type: ${ getTestType ( ) } \n\n` ;
232- markdown += `Virtual Users (VUs): ${ __ENV [ 'DEFAULT_VUS' ] } \n\n` ;
236+ markdown += `Test Type: ${ testType } \n\n` ;
237+ if ( testType === 'stress' ) {
238+ markdown += `Target Total RPS: ${ __ENV [ 'STRESS_TEST_TARGET_TOTAL_RPS' ] || 'N/A' } \n\n` ;
239+ } else {
240+ markdown += `Virtual Users (VUs): ${ __ENV [ 'DEFAULT_VUS' ] } \n\n` ;
241+ }
233242
234243 markdown += `${ header } \n` ;
235244 for ( const scenario of Object . keys ( scenarioMetrics ) . sort ( ) ) {
@@ -248,7 +257,16 @@ function markdownReport(data, isFirstColumnUrl, scenarios) {
248257 const httpMaxDuration = scenarioMetric [ 'http_req_duration' ] . values [ 'max' ] . toFixed ( 2 ) ;
249258
250259 const firstColumn = isFirstColumnUrl ? scenarioUrls [ scenario ] : scenario ;
251- markdown += `| ${ firstColumn } | ${ __ENV . DEFAULT_VUS } | ${ httpReqs } | ${ passPercentage } | ${ rps } | ${ passRps } | ${ httpReqDuration } | ${ httpMedDuration } | ${ httpMinDuration } | ${ httpMaxDuration } | ${ httpP90Duration } | ${ httpP95Duration } | |\n` ;
260+
261+ if ( testType === 'stress' ) {
262+ // Show expected RPS for stress test
263+ const expectedRPS = ( globalThis . rateAllocation && globalThis . rateAllocation [ scenario ] ) || 'N/A' ;
264+ markdown += `| ${ firstColumn } | ${ expectedRPS } | ${ httpReqs } | ${ passPercentage } | ${ rps } | ${ passRps } | ${ httpReqDuration } | ${ httpMedDuration } | ${ httpMinDuration } | ${ httpMaxDuration } | ${ httpP90Duration } | ${ httpP95Duration } | |\n` ;
265+ } else {
266+ // Show VUs for load/performance test
267+ const actualVUs = ( globalThis . vuAllocation && globalThis . vuAllocation [ scenario ] ) || __ENV . DEFAULT_VUS ;
268+ markdown += `| ${ firstColumn } | ${ actualVUs } | ${ httpReqs } | ${ passPercentage } | ${ rps } | ${ passRps } | ${ httpReqDuration } | ${ httpMedDuration } | ${ httpMinDuration } | ${ httpMaxDuration } | ${ httpP90Duration } | ${ httpP95Duration } | |\n` ;
269+ }
252270 } catch ( err ) {
253271 console . error ( `Unable to render report for scenario ${ scenario } ` ) ;
254272 }
@@ -265,13 +283,12 @@ function TestScenarioBuilder() {
265283 this . _testDuration = undefined ;
266284 this . _maxDuration = undefined ;
267285
268- this . build = function ( ) {
269- const that = this ;
286+ this . build = ( ) => {
270287 return {
271- options : getOptionsWithScenario ( that . _name , that . _tags , that . _maxDuration , that . _testDuration ) ,
272- run : function ( testParameters , iteration = 0 , vuIndex = 0 , iterationByVu = 0 ) {
273- const response = that . _request ( testParameters , iteration , vuIndex , iterationByVu ) ;
274- check ( response , that . _checks ) ;
288+ options : getOptionsWithScenario ( this . _name , this . _tags , this . _maxDuration , this . _testDuration ) ,
289+ run : ( testParameters , iteration = 0 , vuIndex = 0 , iterationByVu = 0 ) => {
290+ const response = this . _request ( testParameters , iteration , vuIndex , iterationByVu ) ;
291+ check ( response , this . _checks ) ;
275292 // if Load test, then we need to sleep for random time between 1 and 5 seconds
276293 if ( getTestType ( ) === 'load' ) {
277294 sleep ( randomIntBetween ( 1 , 5 ) ) ;
@@ -313,4 +330,94 @@ function TestScenarioBuilder() {
313330 return this ;
314331}
315332
316- export { getSequentialTestScenarios , markdownReport , TestScenarioBuilder } ;
333+ /**
334+ * Get stress test scenario options for a specific endpoint
335+ *
336+ * Uses the "constant-arrival-rate" executor, which generates a fixed number of iterations (requests) per time unit.
337+ * The executor will dynamically ramp up or lower the number of virtual users as needed to maintain the target request rate.
338+ * This makes it ideal for stress and throughput testing where a steady RPS is required.
339+ *
340+ * See: https://grafana.com/docs/k6/latest/using-k6/scenarios/executors/constant-arrival-rate
341+ *
342+ * @param {string } endpoint - The endpoint name
343+ * @param {number } targetTotalRPS - Target total RPS to distribute
344+ * @param {string } duration - Test duration
345+ * @returns {Object } K6 scenario configuration
346+ */
347+ export function getStressScenarioOptions ( endpoint , targetTotalRPS = 100 , duration = '60s' ) {
348+ const rateAllocation = calculateRateAllocation ( targetTotalRPS ) ;
349+ const targetRate = rateAllocation [ endpoint ] ;
350+ const VU_BUFFER_MULTIPLIER = parseFloat ( __ENV . VU_BUFFER_MULTIPLIER ) || 3 ; // default buffer multiplier
351+
352+ return {
353+ executor : 'constant-arrival-rate' ,
354+ rate : Math . max ( 1 , Math . round ( targetRate ) ) , // requests per second (must be integer, minimum 1)
355+ duration : duration ,
356+ preAllocatedVUs : Math . max ( 1 , Math . ceil ( targetRate * VU_BUFFER_MULTIPLIER ) ) ,
357+ maxVUs : Math . max ( 1 , Math . ceil ( 3 * targetRate * VU_BUFFER_MULTIPLIER ) ) ,
358+ startTime : '0s' , // All scenarios start simultaneously for stress testing
359+ gracefulStop : __ENV . DEFAULT_GRACEFUL_STOP || '5s' ,
360+ exec : 'run' ,
361+ } ;
362+ }
363+
364+ /**
365+ * Create concurrent stress test scenarios with realistic traffic distribution
366+ * @param {Object } tests - Test modules object
367+ * @returns {Object } Stress test configuration
368+ */
369+ function getStressTestScenarios ( tests ) {
370+ tests = getFilteredTests ( tests ) ;
371+
372+ // Use new STRESS_TEST_TARGET_TOTAL_RPS environment variable
373+ const targetTotalRPS = parseInt ( __ENV . STRESS_TEST_TARGET_TOTAL_RPS ) || 100 ;
374+ const testDuration = __ENV . DEFAULT_DURATION || '60s' ;
375+
376+ const funcs = { } ;
377+ const scenarios = { } ;
378+ const thresholds = { } ;
379+
380+ // Create concurrent scenarios for all endpoints
381+ for ( const testName of Object . keys ( tests ) . sort ( ) ) {
382+ const testModule = tests [ testName ] ;
383+ const testScenarios = testModule . options . scenarios ;
384+ const testThresholds = testModule . options . thresholds ;
385+
386+ for ( const [ scenarioName ] of Object . entries ( testScenarios ) ) {
387+ // Get stress scenario options with realistic RPS allocation
388+ const stressOptions = getStressScenarioOptions ( scenarioName , targetTotalRPS , testDuration ) ;
389+
390+ funcs [ scenarioName ] = testModule [ stressOptions . exec ] ;
391+ scenarios [ scenarioName ] = stressOptions ;
392+
393+ // Set up thresholds
394+ const tag = `scenario:${ scenarioName } ` ;
395+ for ( const [ name , threshold ] of Object . entries ( testThresholds ) ) {
396+ if ( name === 'http_req_duration' ) {
397+ thresholds [ `${ name } {${ tag } ,expected_response:true}` ] = threshold ;
398+ } else {
399+ thresholds [ `${ name } {${ tag } }` ] = threshold ;
400+ }
401+ }
402+ thresholds [ `http_reqs{${ tag } }` ] = [ 'count>0' ] ;
403+ thresholds [ `${ SCENARIO_DURATION_METRIC_NAME } {${ tag } }` ] = [ 'value>0' ] ;
404+ }
405+ }
406+
407+ const testOptions = Object . assign ( { } , getOptions ( ) , { scenarios, thresholds } ) ;
408+
409+ return {
410+ funcs,
411+ options : testOptions ,
412+ scenarioDurationGauge : new Gauge ( SCENARIO_DURATION_METRIC_NAME ) ,
413+ } ;
414+ }
415+
416+ export {
417+ getSequentialTestScenarios ,
418+ getStressTestScenarios ,
419+ markdownReport ,
420+ TestScenarioBuilder ,
421+ getFilteredTests ,
422+ getOptions ,
423+ } ;
0 commit comments