2024-08-23 06:59:19 -07:00
import fs from 'fs' ;
import path from 'path' ;
import { $ , which , tmpfile } from 'zx' ;
2024-08-23 03:19:12 -07:00
import type { Scenario } from '@/types/scenario' ;
2024-08-22 01:33:11 -07:00
2024-08-23 06:59:19 -07:00
export type K6ExecutorOpts = {
k6ExecutablePath : string ;
k6ApiToken? : string ;
n8nApiBaseUrl : string ;
} ;
/ * *
* Flag for the k6 CLI .
* @example [ '--duration' , '1m' ]
* @example [ '--quiet' ]
* /
type K6CliFlag = [ string ] | [ string , string ] ;
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
async executeTestScenario ( scenario : Scenario ) {
2024-08-23 06:59:19 -07:00
const augmentedTestScriptPath = this . augmentSummaryScript ( scenario ) ;
const runDirPath = path . dirname ( augmentedTestScriptPath ) ;
const flags : K6CliFlag [ ] = [ [ '--quiet' ] , [ '--duration' , '1m' ] , [ '--vus' , '5' ] ] ;
if ( this . opts . k6ApiToken ) {
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-08-22 01:33:11 -07:00
const processPromise = $ ( {
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-08-23 06:59:19 -07:00
} ) ` ${ k6ExecutablePath } run ${ flattedFlags } ${ augmentedTestScriptPath } ` ;
2024-08-22 01:33:11 -07:00
for await ( const chunk of processPromise . stdout ) {
2024-08-23 03:19:12 -07:00
console . log ( ( chunk as Buffer ) . toString ( ) ) ;
2024-08-22 01:33:11 -07:00
}
2024-08-23 06:59:19 -07:00
this . loadEndOfTestSummary ( runDirPath , scenario . name ) ;
}
/ * *
* Augments the test script with a summary script
*
* @returns Absolute path to the augmented test script
* /
private augmentSummaryScript ( scenario : Scenario ) {
const fullTestScriptPath = path . join ( scenario . scenarioDirPath , scenario . scriptPath ) ;
const testScript = fs . readFileSync ( fullTestScriptPath , 'utf8' ) ;
const summaryScript = this . handleSummaryScript . replace ( '{{scenarioName}}' , scenario . name ) ;
const augmentedTestScript = ` ${ testScript } \ n \ n ${ summaryScript } ` ;
const tempFilePath = tmpfile ( ` ${ scenario . name } .ts ` , augmentedTestScript ) ;
return tempFilePath ;
}
private loadEndOfTestSummary ( dir : string , scenarioName : string ) : K6EndOfTestSummary {
const summaryReportPath = path . join ( dir , ` ${ scenarioName } .summary.json ` ) ;
const summaryReport = fs . readFileSync ( summaryReportPath , 'utf8' ) ;
try {
return JSON . parse ( summaryReport ) ;
} 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
}