loadmill
Version:
A node.js module for running load tests and functional tests on loadmill.com
279 lines (240 loc) • 10.3 kB
text/typescript
import * as Loadmill from './index';
import * as program from 'commander';
import {
getLogger,
isUUID,
getObjectAsString,
convertStrToArr,
printTestSuitesRunsReport,
toLoadmillParams,
readRawParams,
printOnlyFailedFlowRunsReport,
} from './utils';
import { junitReport as createJunitReport, mochawesomeReport as createMochawesomeReport } from './reporter';
program
.usage("<testPlanId | load-config-file> -t <token> [options] [parameter=value...]")
.description(
"Run a test plan (default option) or a load test on loadmill.com.\n " +
"You may set parameter values by passing space-separated 'name=value' pairs, e.g. 'host=www.myapp.com port=80' or supply a file using --parameters-file.\n\n " +
"Learn more at https://www.npmjs.com/package/loadmill#cli"
)
.option("-t, --token <token>", "Loadmill API Token. You must provide a token in order to run tests.")
.option("-l, --load-test", "Launch a load test.")
.option("--test-plan", "Launch a test plan (default option).")
.option("-p, --parallel <parallel>", "Set the concurrency of a running test suites in a test plan")
.option("--additional-description <description>", "Add an additional description at the end of the current suite's description - available only for test suites.")
.option("--labels <labels>", "Run flows that are assigned to a specific label (when running a test suite).. Multiple labels can be provided by seperated them with ',' (e.g. 'label1,label2').")
.option("--labels-expression <labelsExpression>", "Run a test plan's suites with flows that match the labels expression. An expression may contain the characters ( ) & | ! (e.g. '(label1 | label2) & !label3')")
.option("--pool <pool>", "Execute tests from a dedicated agent's pool (when using private agent)")
.option("--tags <tags>", "Tag a test plan run with a comma separated list of tags (e.g. 'tag1,tag2')")
.option("-w, --wait", "Wait for the test to finish.")
.option("-n, --no-bail", "Return exit code 0 even if test fails.")
.option("-q, --quiet", "Do not print out anything (except errors).")
.option("-v, --verbose", "Print out extra information for debugging.")
.option("-r, --report", "Print out Test Suite Flow Runs report when the plan has ended.")
.option("--errors-report", "Print out Test Suite Flow Runs errors report when the plan has ended.")
.option("-j, --junit-report", "Create Test Suite (junit style) report when the suite has ended.")
.option("--junit-report-path <junitReportPath>", "Save junit styled report to a path (defaults to current location).")
.option("-m, --mochawesome-report", "Create Test Suite (mochawesome style) report when the suite has ended.")
.option("--mochawesome-report-path <mochawesomeReportPath>", "Save JSON mochawesome styled report to a path (defaults to current location).")
.option("--colors", "Print test results in color")
.option("-b, --branch <branch>", "Run the test plan's suites from a GitHub branch. The latest version of the selected Git branch will be used as the test configuration for the chosen Test Plan")
.option("--retry-failed-flows <numberOfRetries>", "Configure the test plan to re-run failed flows in case your tested system is unstable. Tests that pass after a retry will be considered successful.")
.option("--parameters-file <parametersFile>", "Supply a file with parameters to override. File format should be 'name=value' divided by new line.")
.option("--inlineParameterOverride", "Override parameters strategy: by default, overrided parameters are appended to the end of the parameters list. Using this flag will replace the parameters inline.")
.option("--apiCatalogService <apiCatalogService>", "Use the provided service when mapping the APIs in the catalog. Service will be created if not exist")
.option("--turbo-parallel", "Run the test plan in turbo mode")
.parse(process.argv);
start()
.catch(err => {
console.error(err);
process.exit(2);
});
async function start() {
let {
wait,
bail,
quiet,
token,
verbose,
colors,
report,
errorsReport,
junitReport,
junitReportPath,
mochawesomeReport,
mochawesomeReportPath,
parallel,
loadTest,
testPlan,
additionalDescription,
labels,
labelsExpression,
pool,
tags,
branch,
retryFailedFlows,
parametersFile,
inlineParameterOverride,
apiCatalogService,
turboParallel,
args: [input, ...rawParams]
} = program;
const logger = getLogger({ verbose, colors });
if (!token) {
validationFailed("No API token provided.");
}
const parameters = toParams(rawParams, parametersFile);
if (verbose) {
// verbose trumps quiet:
quiet = false;
logger.log("Inputs:", {
wait,
bail,
quiet,
token,
verbose,
colors,
report,
errorsReport,
junitReport,
junitReportPath,
mochawesomeReport,
mochawesomeReportPath,
parallel,
input,
loadTest,
testPlan,
additionalDescription,
labels,
labelsExpression,
pool,
tags,
branch,
retryFailedFlows,
inlineParameterOverride,
apiCatalogService,
turboParallel,
parameters,
});
}
const loadmill = Loadmill({ token });
const testFailed = (msg: string) => {
logger.log("");
logger.error(`❌ ${msg}.`);
if (bail) {
process.exit(1);
}
}
const testStopped = (msg: string) => {
logger.log("");
logger.error(`✋ ${msg}.`);
if (bail) {
process.exit(1);
}
}
let res: Loadmill.TestResult | undefined;
if (testPlan || !loadTest) {
if (!isUUID(input)) { //if test plan flag is on then the input should be uuid
validationFailed("Test plan run flag is on but no valid test plan id was provided.");
}
const planLabels = convertStrToArr(labels);
const planTags = convertStrToArr(tags);
try {
logger.verbose(`Executing test plan with id ${input}`);
let running = await loadmill.runTestPlan(
{
id: input,
options: {
additionalDescription,
labels: planLabels,
labelsExpression,
pool,
tags : planTags,
parallel,
branch,
maxFlakyFlowRetries: retryFailedFlows,
inlineParameterOverride,
apiCatalogService,
turboParallel,
}
},
parameters);
if (running && running.id) {
if (wait) {
logger.verbose("Waiting for test plan run with id", running.id);
res = await loadmill.wait(running);
if (!quiet) {
logger.log(res ? getObjectAsString(res, colors) : running.id);
}
if (report && res.testSuitesRuns) {
printTestSuitesRunsReport(res.description, res.testSuitesRuns, logger, colors);
}
if (errorsReport && res.testSuitesRuns) {
printOnlyFailedFlowRunsReport(res.testSuitesRuns, logger, colors);
}
if (res) {
if (junitReport) {
await createJunitReport(res, token, junitReportPath);
}
if (mochawesomeReport) {
await createMochawesomeReport(res, token, mochawesomeReportPath);
}
}
if (res && res.status === 'STOPPED') {
testStopped(`Test plan with id ${res.id || input} has stopped`);
}
if (res && res.passed != null && !res.passed) {
testFailed(`Test plan with id ${res.id || input} has failed`);
}
}
} else {
testFailed(`Couldn't run test plan with id ${input}`);
}
} catch (e) {
if (verbose) {
logger.error(e);
}
const extInfo = e.response && e.response.res && e.response.res.text;
testFailed(`Couldn't run test plan with id ${input} ${extInfo ? extInfo : ''}`);
}
}
else { // if test plan flag is off then the input should be a conf file
const configFile = input;
if (!configFile) {
validationFailed("No configuration file were provided.");
}
let res;
logger.verbose(`Launching ${configFile} as load test`);
const id = await loadmill.run(configFile, parameters);
if (wait && loadTest) {
logger.verbose("Waiting for test:", res ? res.id : id);
res = await loadmill.wait(res || id);
}
if (!quiet) {
logger.log(JSON.stringify(res, null, 4) || id);
}
if (res && res.passed != null && !res.passed) {
logger.error(`❌ Test ${configFile} failed.`);
if (bail) {
process.exit(1);
}
}
}
}
function validationFailed(...args) {
console.log('');
console.error(...args);
console.log('');
program.outputHelp();
process.exit(3);
}
function toParams(rawParams: string[], filePath?: string): Loadmill.Params {
try {
const paramsArray = filePath ? [...readRawParams(filePath), ...rawParams] : rawParams;
return toLoadmillParams(paramsArray);
} catch (err) {
validationFailed(err.message);
return {};
}
}