Skip to content

Commit

Permalink
feat(cli): add typescript support by bundling processor with esbuild (#…
Browse files Browse the repository at this point in the history
…2360)

* feat(cli): add typescript support by bundling processor with esbuild

* fix(cli): call ts handler function

* refactor(cli): only allow typescript processor in non-lambda

* feat(cli): handle error in ts bundling

* refactor(cli): use original processor file name

* test(cli): add basic tests for typescript support

* fix(cli): only extract extname if processor exists

* test(cli): add assert for error log
  • Loading branch information
bernardobridge authored Dec 15, 2023
1 parent db46e67 commit 6103717
Show file tree
Hide file tree
Showing 9 changed files with 211 additions and 4 deletions.
14 changes: 13 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

60 changes: 59 additions & 1 deletion packages/artillery/lib/cmds/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const os = require('os');
const esbuild = require('esbuild-wasm');
const createLauncher = require('../launch-platform');
const createConsoleReporter = require('../../console-reporter');

Expand Down Expand Up @@ -436,6 +437,57 @@ RunCommand.runCommandImplementation = async function (flags, argv, args) {
}
};

function replaceProcessorIfTypescript(script, scriptPath, platform) {
const relativeProcessorPath = script.config.processor;

if (!relativeProcessorPath) {
return script;
}
const extensionType = path.extname(relativeProcessorPath);

if (extensionType != '.ts') {
return script;
}

if (platform == 'aws:lambda') {
throw new Error('Typescript processor is not supported on AWS Lambda');
}

const actualProcessorPath = path.resolve(
path.dirname(scriptPath),
relativeProcessorPath
);
const processorFileName = path.basename(actualProcessorPath, extensionType);

const tmpDir = os.tmpdir();
const newProcessorPath = path.join(
tmpDir,
`${processorFileName}-${Date.now()}.js`
);

try {
esbuild.buildSync({
entryPoints: [actualProcessorPath],
outfile: newProcessorPath,
bundle: true,
platform: 'node',
format: 'cjs',
sourcemap: 'inline',
sourceRoot: '/' //TODO: review this?
});
} catch (error) {
throw new Error(`Failed to compile Typescript processor\n${error.message}`);
}

global.artillery.hasTypescriptProcessor = true;
console.log(
`Bundled Typescript file into JS. New processor path: ${newProcessorPath}`
);

script.config.processor = newProcessorPath;
return script;
}

async function prepareTestExecutionPlan(inputFiles, flags, args) {
let script1 = {};

Expand Down Expand Up @@ -500,7 +552,13 @@ async function prepareTestExecutionPlan(inputFiles, flags, args) {
script5.config.statsInterval = script5.config.statsInterval || 30;

const script6 = addDefaultPlugins(script5);
return script6;
const script7 = replaceProcessorIfTypescript(
script6,
inputFiles[0],
flags.platform
);

return script7;
}

async function readPayload(script) {
Expand Down
3 changes: 2 additions & 1 deletion packages/artillery/lib/platform/aws-lambda/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,8 @@ class PlatformLambda {
'detective',
'is-builtin-module',
'try-require',
'walk-sync'
'walk-sync',
'esbuild-wasm'
],
{
cwd: a9cwd
Expand Down
20 changes: 19 additions & 1 deletion packages/artillery/lib/platform/local/artillery-worker-local.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@ const STATES = require('../worker-states');

const awaitOnEE = require('../../util/await-on-ee');

const returnWorkerEnv = (needsSourcemap) => {
let env = { ...process.env };

if (needsSourcemap) {
env['NODE_OPTIONS'] = process.env.NODE_OPTIONS
? `${process.env.NODE_OPTIONS} --enable-source-maps`
: '--enable-source-maps';
}

return env;
};

class ArtilleryWorker {
constructor(opts) {
this.opts = opts;
Expand All @@ -20,13 +32,19 @@ class ArtilleryWorker {
async init(_opts) {
this.state = STATES.initializing;

this.worker = new Worker(path.join(__dirname, 'worker.js'));
const workerEnv = returnWorkerEnv(global.artillery.hasTypescriptProcessor);

this.worker = new Worker(path.join(__dirname, 'worker.js'), {
env: workerEnv
});
this.workerId = this.worker.threadId;
this.worker.on('error', this.onError.bind(this));
// TODO:
this.worker.on('exit', (exitCode) => {
this.events.emit('exit', exitCode);
});

//eslint-disable-next-line handle-callback-err
this.worker.on('messageerror', (err) => {});

// TODO: Expose performance metrics via getHeapSnapshot() and performance object.
Expand Down
1 change: 1 addition & 0 deletions packages/artillery/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@
"dependency-tree": "^10.0.9",
"detective": "^5.1.0",
"dotenv": "^16.0.1",
"esbuild-wasm": "^0.19.8",
"eventemitter3": "^4.0.4",
"fs-extra": "^10.1.0",
"ip": "^1.1.8",
Expand Down
71 changes: 71 additions & 0 deletions packages/artillery/test/cli/run-typescript.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
const tap = require('tap');
const { execute, returnTmpPath } = require('../cli/_helpers.js');
const { createHash } = require('crypto');
const fs = require('fs');

let reportFilePath;
tap.beforeEach(async (t) => {
reportFilePath = returnTmpPath(
`report-${createHash('md5')
.update(t.name)
.digest('hex')}-${Date.now()}.json`
);
});

// tap.test('Can run a Typescript processor', async (t) => {
// const [exitCode, output] = await execute([
// 'run',
// '-o',
// `${reportFilePath}`,
// 'test/scripts/scenarios-typescript/lodash.yml'
// ]);

// t.equal(exitCode, 0, 'CLI should exit with code 0');
// t.ok(
// output.stdout.includes('Got context using lodash: true'),
// 'Should be able to use lodash in a scenario to get context'
// );
// const json = JSON.parse(fs.readFileSync(reportFilePath, 'utf8'));

// t.equal(
// json.aggregate.counters['http.codes.200'],
// 2,
// 'Should have made 2 requests'
// );
// t.equal(
// json.aggregate.counters['hey_from_ts'],
// 2,
// 'Should have emitted 2 custom metrics from ts processor'
// );
// });

tap.test(
'Failure from a Typescript processor has a resolvable stack trace via source maps',
async (t) => {
const [exitCode, output] = await execute([
'run',
'-o',
`${reportFilePath}`,
'test/scripts/scenarios-typescript/error.yml'
]);

t.equal(exitCode, 11, 'CLI should exit with code 11');
t.ok(
output.stdout.includes('error_from_ts_processor'),
'Should have logged error from ts processor'
);

// Search for the path
const pathRegex = /\((.*?):\d+:\d+\)/;
const match = output.stdout.match(pathRegex);

// Extract the path if found
const extractedPath = match ? match[1] : null;

t.ok(
extractedPath.includes('.ts'),
'Should be using source maps to resolve the path to a .ts file'
);
t.ok(fs.existsSync(extractedPath), 'Error path should exist');
}
);
15 changes: 15 additions & 0 deletions packages/artillery/test/scripts/scenarios-typescript/error.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
config:
target: "http://asciiart.artillery.io:8080"
phases:
- duration: 2
arrivalRate: 1
name: "Phase 1"
processor: "./processor.ts"
variables:
isTypescript: true

scenarios:
- flow:
- function: processorWithError
- get:
url: "/"
15 changes: 15 additions & 0 deletions packages/artillery/test/scripts/scenarios-typescript/lodash.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
config:
target: "http://asciiart.artillery.io:8080"
phases:
- duration: 2
arrivalRate: 1
name: "Phase 1"
processor: "./processor.ts"
variables:
isTypescript: true

scenarios:
- flow:
- function: myTest
- get:
url: "/"
16 changes: 16 additions & 0 deletions packages/artillery/test/scripts/scenarios-typescript/processor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import _ from 'lodash';

export const myTest = async (context, ee, next) => {
const isTypescript = _.get(context, 'vars.isTypescript');

console.log(`Got context using lodash: ${JSON.stringify(isTypescript)}`);

ee.emit('counter', 'hey_from_ts', 1);

next();
};

export const processorWithError = async (context, ee, next) => {
throw new Error('error_from_ts_processor');
next();
};

0 comments on commit 6103717

Please sign in to comment.