2024-08-23 06:59:19 -07:00
import fs from 'fs' ;
2024-09-10 07:41:33 -07:00
import assert from 'node:assert/strict' ;
2024-09-18 00:19:33 -07:00
import path from 'path' ;
2024-08-23 06:59:19 -07:00
import { $ , which , tmpfile } from 'zx' ;
2024-09-18 00:19:33 -07:00
2024-09-12 06:06:43 -07:00
import { buildTestReport , type K6Tag } from '@/test-execution/test-report' ;
2024-09-18 00:19:33 -07:00
import type { Scenario } from '@/types/scenario' ;
2024-09-10 07:41:33 -07:00
export type { K6Tag } ;
2024-09-09 23:25:41 -07:00
2024-08-23 06:59:19 -07:00
export type K6ExecutorOpts = {
k6ExecutablePath : string ;
2024-09-09 23:25:41 -07:00
/** How many concurrent requests to make */
vus : number ;
/** Test duration, e.g. 1m or 30s */
duration : string ;
2024-08-23 06:59:19 -07:00
k6ApiToken? : string ;
n8nApiBaseUrl : string ;
2024-09-09 23:25:41 -07:00
tags? : K6Tag [ ] ;
2024-09-10 07:41:33 -07:00
resultsWebhook ? : {
url : string ;
authHeader : string ;
} ;
2024-08-23 06:59:19 -07:00
} ;
2024-08-27 07:51:43 -07:00
export type K6RunOpts = {
/** Name of the scenario run. Used e.g. when the run is reported to k6 cloud */
scenarioRunName : string ;
} ;
2024-08-23 06:59:19 -07:00
/ * *
* Flag for the k6 CLI .
* @example [ '--duration' , '1m' ]
* @example [ '--quiet' ]
* /
2024-09-09 23:25:41 -07:00
type K6CliFlag = [ string | number ] | [ string , string | number ] ;
2024-08-23 06:59:19 -07:00
2024-08-22 01:33:11 -07:00
/ * *
* Executes test scenarios using k6
* /
export class K6Executor {
2024-08-23 06:59:19 -07:00
/ * *
* This script is dynamically injected into the k6 test script to generate
* a summary report of the test execution .
* /
private readonly handleSummaryScript = `
import { textSummary } from 'https://jslib.k6.io/k6-summary/0.0.2/index.js' ;
export function handleSummary ( data ) {
return {
stdout : textSummary ( data ) ,
'{{scenarioName}}.summary.json' : JSON . stringify ( data ) ,
} ;
}
` ;
constructor ( private readonly opts : K6ExecutorOpts ) { }
2024-08-22 01:33:11 -07:00
2024-08-27 07:51:43 -07:00
async executeTestScenario ( scenario : Scenario , { scenarioRunName } : K6RunOpts ) {
const augmentedTestScriptPath = this . augmentSummaryScript ( scenario , scenarioRunName ) ;
2024-08-23 06:59:19 -07:00
const runDirPath = path . dirname ( augmentedTestScriptPath ) ;
2024-09-09 23:25:41 -07:00
const flags : K6CliFlag [ ] = [
[ '--quiet' ] ,
[ '--duration' , this . opts . duration ] ,
[ '--vus' , this . opts . vus ] ,
] ;
2024-08-23 06:59:19 -07:00
2024-09-10 07:41:33 -07:00
if ( ! this . opts . resultsWebhook && this . opts . k6ApiToken ) {
2024-08-23 06:59:19 -07:00
flags . push ( [ '--out' , 'cloud' ] ) ;
}
const flattedFlags = flags . flat ( 2 ) ;
2024-08-22 01:33:11 -07:00
2024-08-23 00:35:34 -07:00
const k6ExecutablePath = await this . resolveK6ExecutablePath ( ) ;
2024-09-10 07:41:33 -07:00
await $ ( {
2024-08-23 06:59:19 -07:00
cwd : runDirPath ,
2024-08-22 01:33:11 -07:00
env : {
2024-08-23 06:59:19 -07:00
API_BASE_URL : this.opts.n8nApiBaseUrl ,
K6_CLOUD_TOKEN : this.opts.k6ApiToken ,
2024-08-22 01:33:11 -07:00
} ,
2024-09-10 07:41:33 -07:00
stdio : 'inherit' ,
2024-08-23 06:59:19 -07:00
} ) ` ${ k6ExecutablePath } run ${ flattedFlags } ${ augmentedTestScriptPath } ` ;
2024-08-22 01:33:11 -07:00
2024-09-10 07:41:33 -07:00
console . log ( '\n' ) ;
if ( this . opts . resultsWebhook ) {
const endOfTestSummary = this . loadEndOfTestSummary ( runDirPath , scenarioRunName ) ;
const testReport = buildTestReport ( scenario , endOfTestSummary , [
. . . ( this . opts . tags ? ? [ ] ) ,
{ name : 'Vus' , value : this.opts.vus.toString ( ) } ,
{ name : 'Duration' , value : this.opts.duration.toString ( ) } ,
] ) ;
await this . sendTestReport ( testReport ) ;
2024-08-22 01:33:11 -07:00
}
2024-09-10 07:41:33 -07:00
}
async sendTestReport ( testReport : unknown ) {
assert ( this . opts . resultsWebhook ) ;
2024-08-23 06:59:19 -07:00
2024-09-10 07:41:33 -07:00
const response = await fetch ( this . opts . resultsWebhook . url , {
method : 'POST' ,
body : JSON.stringify ( testReport ) ,
headers : {
Authorization : this.opts.resultsWebhook.authHeader ,
'Content-Type' : 'application/json' ,
} ,
} ) ;
if ( ! response . ok ) {
console . warn ( ` Failed to send test summary: ${ response . status } ${ await response . text ( ) } ` ) ;
}
2024-08-23 06:59:19 -07:00
}
/ * *
* Augments the test script with a summary script
*
* @returns Absolute path to the augmented test script
* /
2024-08-27 07:51:43 -07:00
private augmentSummaryScript ( scenario : Scenario , scenarioRunName : string ) {
2024-08-23 06:59:19 -07:00
const fullTestScriptPath = path . join ( scenario . scenarioDirPath , scenario . scriptPath ) ;
const testScript = fs . readFileSync ( fullTestScriptPath , 'utf8' ) ;
2024-08-27 07:51:43 -07:00
const summaryScript = this . handleSummaryScript . replace ( '{{scenarioName}}' , scenarioRunName ) ;
2024-08-23 06:59:19 -07:00
const augmentedTestScript = ` ${ testScript } \ n \ n ${ summaryScript } ` ;
2024-09-03 06:57:49 -07:00
const tempFilePath = tmpfile ( ` ${ scenarioRunName } .js ` , augmentedTestScript ) ;
2024-08-23 06:59:19 -07:00
return tempFilePath ;
}
2024-08-27 07:51:43 -07:00
private loadEndOfTestSummary ( dir : string , scenarioRunName : string ) : K6EndOfTestSummary {
const summaryReportPath = path . join ( dir , ` ${ scenarioRunName } .summary.json ` ) ;
2024-08-23 06:59:19 -07:00
const summaryReport = fs . readFileSync ( summaryReportPath , 'utf8' ) ;
try {
2024-08-27 07:51:43 -07:00
return JSON . parse ( summaryReport ) as K6EndOfTestSummary ;
2024-08-23 06:59:19 -07:00
} catch ( error ) {
throw new Error ( ` Failed to parse the summary report at ${ summaryReportPath } ` ) ;
}
2024-08-22 01:33:11 -07:00
}
2024-08-23 00:35:34 -07:00
/ * *
* @returns Resolved path to the k6 executable
* /
private async resolveK6ExecutablePath ( ) : Promise < string > {
2024-08-23 06:59:19 -07:00
const k6ExecutablePath = await which ( this . opts . k6ExecutablePath , { nothrow : true } ) ;
2024-08-23 00:35:34 -07:00
if ( ! k6ExecutablePath ) {
throw new Error (
'Could not find k6 executable based on your `PATH`. Please ensure k6 is available in your system and add it to your `PATH` or specify the path to the k6 executable using the `K6_PATH` environment variable.' ,
) ;
}
return k6ExecutablePath ;
}
2024-08-22 01:33:11 -07:00
}