From 244a61e8592f80eee698c514a6a0539fa1473c9b Mon Sep 17 00:00:00 2001 From: hveldstra Date: Tue, 20 Aug 2024 14:38:14 +0100 Subject: [PATCH 1/2] refactor: extract prepareTestExecutionPlan() into reusable util --- packages/artillery/lib/cmds/run.js | 101 +---------- .../lib/util/prepare-test-execution-plan.js | 160 ++++++++++++++++++ 2 files changed, 162 insertions(+), 99 deletions(-) create mode 100644 packages/artillery/lib/util/prepare-test-execution-plan.js diff --git a/packages/artillery/lib/cmds/run.js b/packages/artillery/lib/cmds/run.js index b585b74bc9..67fcf48f00 100644 --- a/packages/artillery/lib/cmds/run.js +++ b/packages/artillery/lib/cmds/run.js @@ -1,17 +1,6 @@ const { Command, Flags, Args } = require('@oclif/core'); const { CommonRunFlags } = require('../cli/common-flags'); -const { - readScript, - parseScript, - addOverrides, - addVariables, - addDefaultPlugins, - resolveConfigTemplates, - checkConfig, - resolveConfigPath -} = require('../../util'); - const p = require('util').promisify; const csv = require('csv-parse'); const debug = require('debug')('commands:run'); @@ -29,12 +18,13 @@ const moment = require('moment'); const { SSMS } = require('@artilleryio/int-core').ssms; const telemetry = require('../telemetry').init(); -const validateScript = require('../util/validate-script'); + const { Plugin: CloudPlugin } = require('../platform/cloud/cloud'); const parseTagString = require('../util/parse-tag-string'); const generateId = require('../util/generate-id'); +const prepareTestExecutionPlan = require('../util/prepare-test-execution-plan'); class RunCommand extends Command { static aliases = ['run']; @@ -502,93 +492,6 @@ function replaceProcessorIfTypescript(script, scriptPath) { return script; } -async function prepareTestExecutionPlan(inputFiles, flags, args) { - const scriptPath = inputFiles[0]; - let script1 = {}; - - for (const fn of inputFiles) { - const data = await readScript(fn); - const parsedData = await parseScript(data); - script1 = _.merge(script1, parsedData); - } - - script1 = await checkConfig(script1, scriptPath, flags); - - const script2 = await resolveConfigPath(script1, flags, scriptPath); - - const script3 = await addOverrides(script2, flags); - const script4 = await addVariables(script3, flags); - // The resolveConfigTemplates function expects the config and script path to be passed explicitly because it is used in Fargate as well where the two arguments will not be available on the script - const script5 = await resolveConfigTemplates( - script4, - flags, - script4._configPath, - script4._scriptPath - ); - - if (!script5.config.target) { - throw new Error('No target specified and no environment chosen'); - } - - const validationError = validateScript(script5); - - if (validationError) { - console.log(`Scenario validation error: ${validationError}`); - - process.exit(1); - } - - const script6 = await readPayload(script5); - - if (typeof script6.config.phases === 'undefined' || flags.solo) { - script6.config.phases = [ - { - duration: 1, - arrivalCount: 1 - } - ]; - } - - script6.config.statsInterval = script6.config.statsInterval || 30; - - const script7 = addDefaultPlugins(script5); - const script8 = replaceProcessorIfTypescript(script7, scriptPath); - - return script8; -} - -async function readPayload(script) { - if (!script.config.payload) { - return script; - } - - for (const payloadSpec of script.config.payload) { - const data = fs.readFileSync(payloadSpec.path, 'utf-8'); - - const csvOpts = Object.assign( - { - skip_empty_lines: - typeof payloadSpec.skipEmptyLines === 'undefined' - ? true - : payloadSpec.skipEmptyLines, - cast: typeof payloadSpec.cast === 'undefined' ? true : payloadSpec.cast, - from_line: payloadSpec.skipHeader === true ? 2 : 1, - delimiter: payloadSpec.delimiter || ',' - }, - payloadSpec.options - ); - - try { - const parsedData = await p(csv)(data, csvOpts); - payloadSpec.data = parsedData; - } catch (err) { - throw err; - } - } - - return script; -} - async function sendTelemetry(script, flags, extraProps) { if (process.env.WORKER_ID) { debug('Telemetry: Running in cloud worker, skipping test run event'); diff --git a/packages/artillery/lib/util/prepare-test-execution-plan.js b/packages/artillery/lib/util/prepare-test-execution-plan.js new file mode 100644 index 0000000000..63fd19582c --- /dev/null +++ b/packages/artillery/lib/util/prepare-test-execution-plan.js @@ -0,0 +1,160 @@ +const csv = require('csv-parse'); +const fs = require('node:fs'); +const path = require('node:path'); +const p = require('util').promisify; + +const { + readScript, + parseScript, + addOverrides, + addVariables, + addDefaultPlugins, + resolveConfigTemplates, + checkConfig, + resolveConfigPath +} = require('../../util'); + +const validateScript = require('./validate-script'); + +const _ = require('lodash'); + +async function prepareTestExecutionPlan(inputFiles, flags, _args) { + const scriptPath = inputFiles[0]; + let script1 = {}; + + for (const fn of inputFiles) { + const data = await readScript(fn); + const parsedData = await parseScript(data); + script1 = _.merge(script1, parsedData); + } + + script1 = await checkConfig(script1, scriptPath, flags); + + const script2 = await resolveConfigPath(script1, flags, scriptPath); + + const script3 = await addOverrides(script2, flags); + const script4 = await addVariables(script3, flags); + // The resolveConfigTemplates function expects the config and script path to be passed explicitly because it is used in Fargate as well where the two arguments will not be available on the script + const script5 = await resolveConfigTemplates( + script4, + flags, + script4._configPath, + script4._scriptPath + ); + + if (!script5.config.target) { + throw new Error('No target specified and no environment chosen'); + } + + const validationError = validateScript(script5); + + if (validationError) { + console.log(`Scenario validation error: ${validationError}`); + + process.exit(1); + } + + const script6 = await readPayload(script5); + + if (typeof script6.config.phases === 'undefined' || flags.solo) { + script6.config.phases = [ + { + duration: 1, + arrivalCount: 1 + } + ]; + } + + script6.config.statsInterval = script6.config.statsInterval || 30; + + const script7 = addDefaultPlugins(script5); + const script8 = replaceProcessorIfTypescript(script7, scriptPath); + + return script8; +} + +async function readPayload(script) { + if (!script.config.payload) { + return script; + } + + for (const payloadSpec of script.config.payload) { + const data = fs.readFileSync(payloadSpec.path, 'utf-8'); + + const csvOpts = Object.assign( + { + skip_empty_lines: + typeof payloadSpec.skipEmptyLines === 'undefined' + ? true + : payloadSpec.skipEmptyLines, + cast: typeof payloadSpec.cast === 'undefined' ? true : payloadSpec.cast, + from_line: payloadSpec.skipHeader === true ? 2 : 1, + delimiter: payloadSpec.delimiter || ',' + }, + payloadSpec.options + ); + + try { + const parsedData = await p(csv)(data, csvOpts); + payloadSpec.data = parsedData; + } catch (err) { + throw err; + } + } + + return script; +} + +function replaceProcessorIfTypescript(script, scriptPath) { + const relativeProcessorPath = script.config.processor; + const userExternalPackages = script.config.bundling?.external || []; + + if (!relativeProcessorPath) { + return script; + } + const extensionType = path.extname(relativeProcessorPath); + + if (extensionType != '.ts') { + return script; + } + + const actualProcessorPath = path.resolve( + path.dirname(scriptPath), + relativeProcessorPath + ); + const processorFileName = path.basename(actualProcessorPath, extensionType); + + const processorDir = path.dirname(actualProcessorPath); + const newProcessorPath = path.join( + processorDir, + `dist/${processorFileName}.js` + ); + + //TODO: move require to top of file when Lambda bundle size issue is solved + //must be conditionally required for now as this package is removed in Lambda for now to avoid bigger package sizes + const esbuild = require('esbuild-wasm'); + + try { + esbuild.buildSync({ + entryPoints: [actualProcessorPath], + outfile: newProcessorPath, + bundle: true, + platform: 'node', + format: 'cjs', + sourcemap: 'inline', + external: ['@playwright/test', ...userExternalPackages] + }); + } catch (error) { + throw new Error(`Failed to compile Typescript processor\n${error.message}`); + } + + global.artillery.hasTypescriptProcessor = newProcessorPath; + console.log( + `Bundled Typescript file into JS. New processor path: ${newProcessorPath}` + ); + + script.config.processor = newProcessorPath; + return script; +} + +module.exports = prepareTestExecutionPlan; From 4cdadc9996cfeef07e165644b6dc63ce00859641 Mon Sep 17 00:00:00 2001 From: hveldstra Date: Tue, 20 Aug 2024 14:26:22 +0100 Subject: [PATCH 2/2] feat: send extra metadata to Artillery Cloud --- packages/artillery/lib/cmds/run.js | 10 +++++++++- .../platform/aws-ecs/legacy/run-cluster.js | 19 ++++++++++++++++--- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/packages/artillery/lib/cmds/run.js b/packages/artillery/lib/cmds/run.js index 67fcf48f00..49c12ed837 100644 --- a/packages/artillery/lib/cmds/run.js +++ b/packages/artillery/lib/cmds/run.js @@ -339,7 +339,15 @@ RunCommand.runCommandImplementation = async function (flags, argv, args) { artilleryVersion: { core: global.artillery.version }, - targetUrl: script.config.target + // Properties from the runnable script object: + testConfig: { + target: script.config.target, + phases: script.config.phases, + plugins: script.config.plugins, + environment: script._environment, + scriptPath: script._scriptPath, + configPath: script._configPath + } } }); diff --git a/packages/artillery/lib/platform/aws-ecs/legacy/run-cluster.js b/packages/artillery/lib/platform/aws-ecs/legacy/run-cluster.js index 75849a7b84..707b8d6e7b 100644 --- a/packages/artillery/lib/platform/aws-ecs/legacy/run-cluster.js +++ b/packages/artillery/lib/platform/aws-ecs/legacy/run-cluster.js @@ -46,8 +46,6 @@ const dotenv = require('dotenv'); const util = require('./util'); -const generateId = require('../../../util/generate-id'); - const setDefaultAWSCredentials = require('../../aws/aws-set-default-credentials'); module.exports = runCluster; @@ -76,6 +74,7 @@ const { let IS_FARGATE = false; const TEST_RUN_STATUS = require('./test-run-status'); +const prepareTestExecutionPlan = require('../../../util/prepare-test-execution-plan'); function setupConsoleReporter(quiet) { const reporterOpts = { @@ -218,6 +217,11 @@ async function tryRunCluster(scriptPath, options, artilleryReporter) { } let context = {}; + const inputFiles = [].concat(scriptPath, options.config || []); + const runnableScript = await prepareTestExecutionPlan(inputFiles, options); + + context.runnableScript = runnableScript; + let absoluteScriptPath; if (typeof scriptPath !== 'undefined') { absoluteScriptPath = path.resolve(process.cwd(), scriptPath); @@ -1622,7 +1626,16 @@ async function launchLeadTask(context) { }), artilleryVersion: JSON.stringify({ core: global.artillery.version - }) + }), + // Properties from the runnable script object: + testConfig: { + target: context.runnableScript.config.target, + phases: context.runnableScript.config.phases, + plugins: context.runnableScript.config.plugins, + environment: context.runnableScript._environment, + scriptPath: context.runnableScript._scriptPath, + configPath: context.runnableScript._configPath, + } }; artillery.globalEvents.emit('metadata', metadata);