Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

loadmill

Package Overview
Dependencies
Maintainers
3
Versions
100
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

loadmill - npm Package Compare versions

Comparing version 3.2.5 to 4.0.0

2

lib/index.d.ts

@@ -5,5 +5,3 @@ import './polyfills';

run(config: Loadmill.Configuration, paramsOrCallback?: Loadmill.ParamsOrCallback, callback?: Loadmill.Callback): Promise<string>;
runFolder(folderPath: string, paramsOrCallback?: Loadmill.ParamsOrCallback, callback?: Loadmill.Callback): Promise<Array<Loadmill.TestResult>>;
wait(testDefOrId: string | Loadmill.TestDef, callback?: Loadmill.Callback): Promise<Loadmill.TestResult>;
runTestSuite(suite: Loadmill.TestSuiteDef, paramsOrCallback?: Loadmill.ParamsOrCallback, callback?: Loadmill.Callback): Promise<Loadmill.TestDef>;
runTestPlan(testPlan: Loadmill.TestPlanDef, params: Loadmill.Params): Promise<Loadmill.TestDef | undefined>;

@@ -10,0 +8,0 @@ junitReport(testResult: Loadmill.TestResult, path?: string | undefined): Promise<void>;

@@ -8,44 +8,10 @@ "use strict";

var reporter_1 = require("./reporter");
var TEST_PLAN_POLL_INTERVAL_IN_MS = 10 * 1000; // 10 seconds
function Loadmill(options) {
var _a = options, token = _a.token, _b = _a._testingServerHost, _testingServerHost = _b === void 0 ? utils_1.TESTING_HOST : _b;
var testingServer = "https://" + _testingServerHost;
var testSuitesAPI = testingServer + "/api/test-suites";
var testPlansAPI = testingServer + "/api/test-plans";
function _runFolderSync(listOfFiles, execFunc) {
var funcArgs = [];
for (var _i = 2; _i < arguments.length; _i++) {
funcArgs[_i - 2] = arguments[_i];
}
return tslib_1.__awaiter(this, void 0, void 0, function () {
var results, _a, listOfFiles_1, file, res, testResult;
return tslib_1.__generator(this, function (_b) {
switch (_b.label) {
case 0:
results = [];
_a = 0, listOfFiles_1 = listOfFiles;
_b.label = 1;
case 1:
if (!(_a < listOfFiles_1.length)) return [3 /*break*/, 5];
file = listOfFiles_1[_a];
return [4 /*yield*/, execFunc.apply(void 0, tslib_1.__spreadArray([file], funcArgs))];
case 2:
res = _b.sent();
return [4 /*yield*/, _wait(res)];
case 3:
testResult = _b.sent();
results.push(testResult);
if (!testResult.passed)
return [3 /*break*/, 5];
_b.label = 4;
case 4:
_a++;
return [3 /*break*/, 1];
case 5: return [2 /*return*/, results];
}
});
});
}
function _wait(testDefOrId, callback) {
return tslib_1.__awaiter(this, void 0, void 0, function () {
var resolve, reject, testDef, apiUrl, webUrl, intervalId;
var resolve, reject, testDef, apiUrl, webUrl, retries, intervalId;
var _this = this;

@@ -59,2 +25,3 @@ return tslib_1.__generator(this, function (_a) {

webUrl = getTestWebUrl(testDef, testingServer);
retries = 1;
intervalId = setInterval(function () { return tslib_1.__awaiter(_this, void 0, void 0, function () {

@@ -79,9 +46,4 @@ var body, bodyWithFlows, testResult, err_1;

case 3:
testResult = tslib_1.__assign(tslib_1.__assign({}, testDef), { url: webUrl, description: body && body.description, passed: isTestPassed(body, testDef.type), startTime: body.startTime, endTime: body.endTime, status: body.status });
if (testDef.type === Loadmill.TYPES.SUITE) {
testResult.flowRuns = reductFlowRunsData(body.testSuiteFlowRuns);
}
else if (testDef.type === Loadmill.TYPES.TEST_PLAN) {
testResult.testSuitesRuns = reductTestSuitesRuns(body.testSuitesRuns, testingServer);
}
testResult = toTestResult(testDef, webUrl, body);
redactData(testResult, body, testingServer);
if (callback) {

@@ -97,3 +59,9 @@ callback(null, testResult);

err_1 = _a.sent();
clearInterval(intervalId);
if (retries < 3) {
retries++;
return [2 /*return*/];
}
else {
clearInterval(intervalId);
}
if (callback) {

@@ -109,3 +77,3 @@ callback(err_1, null);

});
}); }, 10 * 1000);
}); }, TEST_PLAN_POLL_INTERVAL_IN_MS);
return [2 /*return*/, callback ? null : new Promise(function (_resolve, _reject) {

@@ -118,35 +86,2 @@ resolve = _resolve;

}
function _runTestSuite(suite, paramsOrCallback, callback) {
var _a;
return tslib_1.__awaiter(this, void 0, void 0, function () {
var params, overrideParameters, suiteId, additionalDescription, labels, failGracefully, pool;
var _this = this;
return tslib_1.__generator(this, function (_b) {
params = paramsOrCallback && typeof paramsOrCallback !== 'function' ? paramsOrCallback : {};
overrideParameters = toParams(params, (_a = suite.options) === null || _a === void 0 ? void 0 : _a.parametersFile);
suiteId = suite.id;
additionalDescription = suite.options && suite.options.additionalDescription;
labels = suite.options && suite.options.labels && utils_1.filterLabels(suite.options.labels);
failGracefully = suite.options && suite.options.failGracefully;
pool = suite.options && suite.options.pool;
return [2 /*return*/, wrap(function () { return tslib_1.__awaiter(_this, void 0, void 0, function () {
var _a, testSuiteRunId, err;
return tslib_1.__generator(this, function (_b) {
switch (_b.label) {
case 0: return [4 /*yield*/, superagent.post(testSuitesAPI + "/" + suiteId + "/run" + (failGracefully ? '?failGracefully=true' : ''))
.send({ overrideParameters: overrideParameters, additionalDescription: additionalDescription, labels: labels, pool: pool })
.auth(token, '')];
case 1:
_a = (_b.sent()).body, testSuiteRunId = _a.testSuiteRunId, err = _a.err;
if (err || !testSuiteRunId) {
console.error(err ? JSON.stringify(err) : "The server encountered an error while handling the request");
return [2 /*return*/];
}
return [2 /*return*/, { id: testSuiteRunId, type: Loadmill.TYPES.SUITE }];
}
});
}); }, callback || paramsOrCallback)];
});
});
}
function _runTestPlan(testPlan, params) {

@@ -219,24 +154,5 @@ var _a;

},
runFolder: function (folderPath, paramsOrCallback, callback) {
return tslib_1.__awaiter(this, void 0, void 0, function () {
var listOfFiles;
return tslib_1.__generator(this, function (_a) {
listOfFiles = utils_1.getJSONFilesInFolderRecursively(folderPath);
if (listOfFiles.length === 0) {
console.log("No Loadmill test files were found at " + folderPath + " - exiting...");
}
return [2 /*return*/, _runFolderSync(listOfFiles, this.run, paramsOrCallback, callback)];
});
});
},
wait: function (testDefOrId, callback) {
return _wait(testDefOrId, callback);
},
runTestSuite: function (suite, paramsOrCallback, callback) {
return tslib_1.__awaiter(this, void 0, void 0, function () {
return tslib_1.__generator(this, function (_a) {
return [2 /*return*/, _runTestSuite(suite, paramsOrCallback, callback)];
});
});
},
runTestPlan: function (testPlan, params) {

@@ -274,2 +190,8 @@ return tslib_1.__awaiter(this, void 0, void 0, function () {

};
function toTestResult(testDef, webUrl, body) {
return tslib_1.__assign(tslib_1.__assign({}, testDef), { url: webUrl, description: body && body.description, passed: isTestPassed(body, testDef.type), startTime: body.startTime, endTime: body.endTime, status: body.status });
}
function redactData(testResult, body, testingServer) {
testResult.testSuitesRuns = reductTestSuitesRuns(body.testSuitesRuns, testingServer);
}
function isTestInFinalState(body, runType) {

@@ -310,11 +232,2 @@ if (runType === Loadmill.TYPES.TEST_PLAN) {

}
function reductFlowRunsData(flowRuns) {
if (flowRuns) {
return flowRuns.map(function (f) { return ({
id: f.id,
description: f.description,
status: f.status
}); });
}
}
function reductTestSuitesRuns(suitesRuns, testingServer) {

@@ -321,0 +234,0 @@ if (suitesRuns) {

169

lib/loadmill.js

@@ -9,4 +9,4 @@ "use strict";

program
.usage("<testSuiteId | load-config-file-or-folder> -t <token> [options] [parameter=value...]")
.description("Run a test suite (default option), test plan or a load test on loadmill.com.\n " +
.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 " +

@@ -16,4 +16,3 @@ "Learn more at https://www.npmjs.com/package/loadmill#cli")

.option("-l, --load-test", "Launch a load test.")
.option("--test-plan", "Launch a test plan.")
.option("-s, --test-suite", "Launch a test suite (default option). If set then a test suite id must be provided instead of config file.")
.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")

@@ -44,3 +43,3 @@ .option("--additional-description <description>", "Add an additional description at the end of the current suite's description - available only for test suites.")

return tslib_1.__awaiter(this, void 0, void 0, function () {
var wait, bail, quiet, token, verbose, colors, report, junitReport, junitReportPath, mochawesomeReport, mochawesomeReportPath, parallel, loadTest, testPlan, additionalDescription, labels, labelsExpression, pool, branch, retryFailedFlows, parametersFile, _a, input, rawParams, logger, parameters, testSuite, loadmill, testFailed, testStopped, res, suiteLabels, running, e_1, extInfo, planLabels, running, e_2, extInfo, fileOrFolder, listOfFiles, _i, listOfFiles_1, file, res_1, id;
var wait, bail, quiet, token, verbose, colors, report, junitReport, junitReportPath, mochawesomeReport, mochawesomeReportPath, parallel, loadTest, testPlan, additionalDescription, labels, labelsExpression, pool, branch, retryFailedFlows, parametersFile, _a, input, rawParams, logger, parameters, loadmill, testFailed, testStopped, res, planLabels, running, e_1, extInfo, configFile, res_1, id;
return tslib_1.__generator(this, function (_b) {

@@ -55,3 +54,2 @@ switch (_b.label) {

parameters = toParams(rawParams, parametersFile);
testSuite = !loadTest && !testPlan;
if (verbose) {

@@ -76,3 +74,2 @@ // verbose trumps quiet:

testPlan: testPlan,
testSuite: testSuite,
additionalDescription: additionalDescription,

@@ -102,6 +99,6 @@ labels: labels,

};
if (!testSuite) return [3 /*break*/, 13];
suiteLabels = utils_1.convertStrToArr(labels);
if (!utils_1.isUUID(input)) { //if test suite flag is on then the input should be uuid
validationFailed("Test suite run flag is on but no valid test suite id was provided.");
if (!(testPlan || !loadTest)) return [3 /*break*/, 13];
planLabels = utils_1.convertStrToArr(labels);
if (!utils_1.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.");
}

@@ -111,7 +108,13 @@ _b.label = 1;

_b.trys.push([1, 11, , 12]);
logger.verbose("Executing suite with id " + input);
return [4 /*yield*/, loadmill.runTestSuite({
logger.verbose("Executing test plan with id " + input);
return [4 /*yield*/, loadmill.runTestPlan({
id: input,
options: {
additionalDescription: additionalDescription, labels: suiteLabels, pool: pool
additionalDescription: additionalDescription,
labels: planLabels,
labelsExpression: labelsExpression,
pool: pool,
parallel: parallel,
branch: branch,
maxFlakyFlowRetries: retryFailedFlows
}

@@ -123,10 +126,14 @@ }, parameters)];

if (!wait) return [3 /*break*/, 8];
logger.verbose("Waiting for test suite run with id", running.id);
logger.verbose("Waiting for test plan run with id", running.id);
return [4 /*yield*/, loadmill.wait(running)];
case 3:
res = _b.sent();
if (report && res.flowRuns) {
utils_1.printFlowRunsReport(res.description, res.flowRuns, logger, colors);
if (!quiet) {
logger.log(res ? utils_1.getObjectAsString(res, colors) : running.id);
}
if (!(res && junitReport)) return [3 /*break*/, 5];
if (report && res.testSuitesRuns) {
utils_1.printTestSuitesRunsReport(res.description, res.testSuitesRuns, logger, colors);
}
if (!res) return [3 /*break*/, 7];
if (!junitReport) return [3 /*break*/, 5];
return [4 /*yield*/, reporter_1.junitReport(res, token, junitReportPath)];

@@ -137,3 +144,3 @@ case 4:

case 5:
if (!(res && mochawesomeReport)) return [3 /*break*/, 7];
if (!mochawesomeReport) return [3 /*break*/, 7];
return [4 /*yield*/, reporter_1.mochawesomeReport(res, token, mochawesomeReportPath)];

@@ -144,13 +151,12 @@ case 6:

case 7:
if (res && res.status === 'STOPPED') {
testStopped("Test plan with id " + (res.id || input) + " has stopped");
}
if (res && res.passed != null && !res.passed) {
testFailed("Test suite " + (res.id || input) + " has failed");
testFailed("Test plan with id " + (res.id || input) + " has failed");
}
_b.label = 8;
case 8:
if (!quiet) {
logger.log(res ? utils_1.getObjectAsString(res, colors) : running.id);
}
return [3 /*break*/, 10];
case 8: return [3 /*break*/, 10];
case 9:
testFailed("Couldn't run test suite with id " + input);
testFailed("Couldn't run test plan with id " + input);
_b.label = 10;

@@ -164,101 +170,21 @@ case 10: return [3 /*break*/, 12];

extInfo = e_1.response && e_1.response.res && e_1.response.res.text;
testFailed("Couldn't run test suite with id " + input + ". " + (extInfo ? extInfo : ''));
testFailed("Couldn't run test plan with id " + input + " " + (extInfo ? extInfo : ''));
return [3 /*break*/, 12];
case 12: return [3 /*break*/, 32];
case 12: return [3 /*break*/, 17];
case 13:
if (!testPlan) return [3 /*break*/, 26];
planLabels = utils_1.convertStrToArr(labels);
if (!utils_1.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.");
configFile = input;
if (!configFile) {
validationFailed("No configuration file were provided.");
}
_b.label = 14;
logger.verbose("Launching " + configFile + " as load test");
return [4 /*yield*/, loadmill.run(configFile, parameters)];
case 14:
_b.trys.push([14, 24, , 25]);
logger.verbose("Executing test plan with id " + input);
return [4 /*yield*/, loadmill.runTestPlan({
id: input,
options: {
additionalDescription: additionalDescription,
labels: planLabels,
labelsExpression: labelsExpression,
pool: pool,
parallel: parallel,
branch: branch,
maxFlakyFlowRetries: retryFailedFlows
}
}, parameters)];
case 15:
running = _b.sent();
if (!(running && running.id)) return [3 /*break*/, 22];
if (!wait) return [3 /*break*/, 21];
logger.verbose("Waiting for test plan run with id", running.id);
return [4 /*yield*/, loadmill.wait(running)];
case 16:
res = _b.sent();
if (!quiet) {
logger.log(res ? utils_1.getObjectAsString(res, colors) : running.id);
}
if (report && res.testSuitesRuns) {
utils_1.printTestSuitesRunsReport(res.description, res.testSuitesRuns, logger, colors);
}
if (!res) return [3 /*break*/, 20];
if (!junitReport) return [3 /*break*/, 18];
return [4 /*yield*/, reporter_1.junitReport(res, token, junitReportPath)];
case 17:
_b.sent();
_b.label = 18;
case 18:
if (!mochawesomeReport) return [3 /*break*/, 20];
return [4 /*yield*/, reporter_1.mochawesomeReport(res, token, mochawesomeReportPath)];
case 19:
_b.sent();
_b.label = 20;
case 20:
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");
}
_b.label = 21;
case 21: return [3 /*break*/, 23];
case 22:
testFailed("Couldn't run test plan with id " + input);
_b.label = 23;
case 23: return [3 /*break*/, 25];
case 24:
e_2 = _b.sent();
if (verbose) {
logger.error(e_2);
}
extInfo = e_2.response && e_2.response.res && e_2.response.res.text;
testFailed("Couldn't run test plan with id " + input + " " + (extInfo ? extInfo : ''));
return [3 /*break*/, 25];
case 25: return [3 /*break*/, 32];
case 26:
fileOrFolder = input;
if (!fileOrFolder) {
validationFailed("No configuration file or folder were provided.");
}
listOfFiles = utils_1.getJSONFilesInFolderRecursively(fileOrFolder);
if (listOfFiles.length === 0) {
logger.log("No Loadmill test files were found at " + fileOrFolder + " - exiting...");
}
_i = 0, listOfFiles_1 = listOfFiles;
_b.label = 27;
case 27:
if (!(_i < listOfFiles_1.length)) return [3 /*break*/, 32];
file = listOfFiles_1[_i];
res_1 = void 0;
logger.verbose("Launching " + file + " as load test");
return [4 /*yield*/, loadmill.run(file, parameters)];
case 28:
id = _b.sent();
if (!(wait && loadTest)) return [3 /*break*/, 30];
if (!(wait && loadTest)) return [3 /*break*/, 16];
logger.verbose("Waiting for test:", res_1 ? res_1.id : id);
return [4 /*yield*/, loadmill.wait(res_1 || id)];
case 29:
case 15:
res_1 = _b.sent();
_b.label = 30;
case 30:
_b.label = 16;
case 16:
if (!quiet) {

@@ -268,3 +194,3 @@ logger.log(JSON.stringify(res_1, null, 4) || id);

if (res_1 && res_1.passed != null && !res_1.passed) {
logger.error("\u274C Test " + file + " failed.");
logger.error("\u274C Test " + configFile + " failed.");
if (bail) {

@@ -274,7 +200,4 @@ process.exit(1);

}
_b.label = 31;
case 31:
_i++;
return [3 /*break*/, 27];
case 32: return [2 /*return*/];
_b.label = 17;
case 17: return [2 /*return*/];
}

@@ -281,0 +204,0 @@ });

"use strict";
exports.__esModule = true;
exports.TESTING_HOST = exports.getLogger = exports.Logger = exports.sleep = exports.readRawParams = exports.toLoadmillParams = exports.isUUID = exports.isString = exports.isEmptyObj = exports.getJSONFilesInFolderRecursively = exports.filterLabels = exports.convertArrToLabelQueryParams = exports.convertStrToArr = exports.printTestSuitesRunsReport = exports.printFlowRunsReport = exports.getObjectAsString = void 0;
exports.TESTING_HOST = exports.getLogger = exports.Logger = exports.sleep = exports.readRawParams = exports.toLoadmillParams = exports.isUUID = exports.isString = exports.isEmptyObj = exports.filterLabels = exports.convertArrToLabelQueryParams = exports.convertStrToArr = exports.printTestSuitesRunsReport = exports.printFlowRunsReport = exports.getObjectAsString = void 0;
var tslib_1 = require("tslib");
var fs = require("fs");
var path = require("path");
var isEmpty = require("lodash/isEmpty");

@@ -58,18 +57,2 @@ var isAString = require("lodash/isString");

exports.filterLabels = filterLabels;
var getJSONFilesInFolderRecursively = function (fileOrFolder, filelist) {
if (filelist === void 0) { filelist = []; }
var isFile = fs.statSync(fileOrFolder).isFile();
if (isFile && endsWith(fileOrFolder, '.json')) {
filelist.push(fileOrFolder);
}
else if (!isFile) {
fs.readdirSync(fileOrFolder)
.map(function (file) {
return exports.getJSONFilesInFolderRecursively(path.join(fileOrFolder, file), filelist);
});
}
return filelist;
};
exports.getJSONFilesInFolderRecursively = getJSONFilesInFolderRecursively;
var endsWith = function (str, suffix) { return str.indexOf(suffix, str.length - suffix.length) !== -1; };
var isEmptyObj = function (obj) { return isEmpty(obj); };

@@ -76,0 +59,0 @@ exports.isEmptyObj = isEmptyObj;

{
"stats": {
"suites": 2,
"tests": 2,
"suites": 1,
"tests": 1,
"passes": 1,
"failures": 1,
"start": "2021-08-01T13:22:15.261Z",
"end": "2021-08-01T13:22:36.176Z",
"failures": 0,
"start": "2023-05-17T09:28:29.751Z",
"end": "2023-05-17T09:28:50.430Z",
"pending": 0,
"testsRegistered": 2,
"testsRegistered": 1,
"pendingPercent": 0,
"passPercent": 50,
"passPercent": 100,
"other": 0,

@@ -17,3 +17,3 @@ "hasOther": false,

"hasSkipped": false,
"duration": 13641
"duration": 16984
},

@@ -25,9 +25,9 @@ "results": [

{
"title": "yigal node junit - Yigal plan",
"title": "long suite - long plan",
"tests": [
{
"title": "Flow 1 good",
"fullTitle": "Flow 1 good",
"title": "New Flow",
"fullTitle": "New Flow",
"timedOut": false,
"duration": 2164,
"duration": 5446,
"state": "passed",

@@ -39,55 +39,41 @@ "pass": true,

"pending": false,
"code": "https://app.loadmill.com/app/api-tests/test-suite-runs/08e9591f-a21b-4144-ad2b-e9d14a27ef16/flows/b9f24e01-8225-45cd-a655-df47c988c311",
"code": "https://1910-207-232-22-138.ngrok-free.app/app/api-tests/test-suite-runs/b5adc8c7-0f80-4f25-b008-8b913d8cc004/flows/de1d8280-4d6f-4b74-8e32-0e8d3e818562",
"err": {},
"uuid": "b9f24e01-8225-45cd-a655-df47c988c311"
"uuid": "de1d8280-4d6f-4b74-8e32-0e8d3e818562"
},
{
"title": "Flow 1 bad",
"fullTitle": "Flow 1 bad",
"title": "New Flow (copy)",
"fullTitle": "New Flow (copy)",
"timedOut": false,
"duration": 1792,
"state": "failed",
"pass": false,
"fail": true,
"duration": 5279,
"state": "passed",
"pass": true,
"fail": false,
"isHook": false,
"skipped": false,
"pending": false,
"code": "https://app.loadmill.com/app/api-tests/test-suite-runs/08e9591f-a21b-4144-ad2b-e9d14a27ef16/flows/aeb60043-e175-4917-a764-83601428b528",
"err": {
"showDiff": true,
"actual": "",
"negate": false,
"_message": "",
"generatedMessage": false,
"diff": "Request #2 - GET http://httpbin.org/status/401 => HTTP status 401 - Unauthorized \n "
},
"uuid": "aeb60043-e175-4917-a764-83601428b528"
"code": "https://1910-207-232-22-138.ngrok-free.app/app/api-tests/test-suite-runs/b5adc8c7-0f80-4f25-b008-8b913d8cc004/flows/444da308-a5f5-467f-92b3-94c6589dcdb7",
"err": {},
"uuid": "444da308-a5f5-467f-92b3-94c6589dcdb7"
},
{
"title": "Flow 1 good but assertions",
"fullTitle": "Flow 1 good but assertions",
"title": "New Flow (copy) (copy)",
"fullTitle": "New Flow (copy) (copy)",
"timedOut": false,
"duration": 2400,
"state": "failed",
"pass": false,
"fail": true,
"duration": 5284,
"state": "passed",
"pass": true,
"fail": false,
"isHook": false,
"skipped": false,
"pending": false,
"code": "https://app.loadmill.com/app/api-tests/test-suite-runs/08e9591f-a21b-4144-ad2b-e9d14a27ef16/flows/785ae469-9cff-41e2-ac95-fb6dbd8dcb4d",
"err": {
"showDiff": true,
"actual": "",
"negate": false,
"_message": "",
"generatedMessage": false,
"diff": "Request #2 - Flow desc Rivi - GET http://httpbin.org/status/200 => \n \n \n+ \"Expected: __status Equals 300 \n- \"Actual: 200 \n \n \n+ \"Expected: __launchedBy Equals Rivi \n- \"Actual: Yigal Dviri \n \n \n+ \"Expected: __status Matches 534534 \n- \"Actual: 200 \n \n \n+ \"Expected: __status Doesn't contain 200 \n- \"Actual: 200 \n \n \n+ \"Expected: __responseEndTime Doesn't exist \n- \"Actual: 1627824141763 \n \n \n+ \"Expected: __status Less than 100 \n- \"Actual: 200 \n \n \n+ \"Expected: rivi Exists \n- \"Actual: null \n \n \n+ \"Expected: __status Greater than 300 \n- \"Actual: 200 "
},
"uuid": "785ae469-9cff-41e2-ac95-fb6dbd8dcb4d"
"code": "https://1910-207-232-22-138.ngrok-free.app/app/api-tests/test-suite-runs/b5adc8c7-0f80-4f25-b008-8b913d8cc004/flows/f9fae9f7-3af8-4088-a6bb-575ded2c3a48",
"err": {},
"uuid": "f9fae9f7-3af8-4088-a6bb-575ded2c3a48"
},
{
"title": "timout",
"fullTitle": "timout",
"title": "this one should fail",
"fullTitle": "this one should fail",
"timedOut": false,
"duration": 4784,
"duration": 654,
"state": "failed",

@@ -99,3 +85,3 @@ "pass": false,

"pending": false,
"code": "https://app.loadmill.com/app/api-tests/test-suite-runs/08e9591f-a21b-4144-ad2b-e9d14a27ef16/flows/2670ae3e-ebf6-4b2a-b959-62f5edc9acb6",
"code": "https://1910-207-232-22-138.ngrok-free.app/app/api-tests/test-suite-runs/b5adc8c7-0f80-4f25-b008-8b913d8cc004/flows/dea2de7a-572b-44bd-b6f0-d1b935e2d0cb",
"err": {

@@ -107,40 +93,17 @@ "showDiff": true,

"generatedMessage": false,
"diff": "Request #2 - GET http://httpbin.org/delay/5 => Timeout of 2000ms exceeded \n "
"diff": "Request #1 - GET https://bin.blockchain-socks.xyz/status/412 => HTTP status 412 - Precondition Failed "
},
"uuid": "2670ae3e-ebf6-4b2a-b959-62f5edc9acb6"
},
{
"title": "Loops",
"fullTitle": "Loops",
"timedOut": false,
"duration": 1401,
"state": "failed",
"pass": false,
"fail": true,
"isHook": false,
"skipped": false,
"pending": false,
"code": "https://app.loadmill.com/app/api-tests/test-suite-runs/08e9591f-a21b-4144-ad2b-e9d14a27ef16/flows/4ce96ac5-8dfd-48d3-9455-48b32c975381",
"err": {
"showDiff": true,
"actual": "",
"negate": false,
"_message": "",
"generatedMessage": false,
"diff": "Request #1 - Flow desc Rivi - GET http://httpbin.org/status/200 => Request failed to meet loop condition for 2 iterations \n "
},
"uuid": "4ce96ac5-8dfd-48d3-9455-48b32c975381"
"uuid": "dea2de7a-572b-44bd-b6f0-d1b935e2d0cb"
}
],
"duration": 12906,
"duration": 16984,
"suites": [],
"uuid": "08e9591f-a21b-4144-ad2b-e9d14a27ef16",
"uuid": "b5adc8c7-0f80-4f25-b008-8b913d8cc004",
"passes": [
"b9f24e01-8225-45cd-a655-df47c988c311"
"de1d8280-4d6f-4b74-8e32-0e8d3e818562",
"444da308-a5f5-467f-92b3-94c6589dcdb7",
"f9fae9f7-3af8-4088-a6bb-575ded2c3a48"
],
"failures": [
"aeb60043-e175-4917-a764-83601428b528",
"785ae469-9cff-41e2-ac95-fb6dbd8dcb4d",
"2670ae3e-ebf6-4b2a-b959-62f5edc9acb6",
"4ce96ac5-8dfd-48d3-9455-48b32c975381"
"dea2de7a-572b-44bd-b6f0-d1b935e2d0cb"
],

@@ -155,37 +118,2 @@ "root": false,

"pending": []
},
{
"title": "yigal test prod ipfy - Yigal plan",
"tests": [
{
"title": "ipify yay",
"fullTitle": "ipify yay",
"timedOut": false,
"duration": 462,
"state": "passed",
"pass": true,
"fail": false,
"isHook": false,
"skipped": false,
"pending": false,
"code": "https://app.loadmill.com/app/api-tests/test-suite-runs/fc868bae-b167-4495-bcbe-ac58811f6755/flows/d26f109a-3a34-4199-9930-5b7c933e7285",
"err": {},
"uuid": "d26f109a-3a34-4199-9930-5b7c933e7285"
}
],
"duration": 735,
"suites": [],
"uuid": "fc868bae-b167-4495-bcbe-ac58811f6755",
"passes": [
"d26f109a-3a34-4199-9930-5b7c933e7285"
],
"failures": [],
"root": false,
"_timeout": 0,
"file": "",
"fullFile": "",
"beforeHooks": [],
"afterHooks": [],
"skipped": [],
"pending": []
}

@@ -197,3 +125,3 @@ ],

"_timeout": 0,
"uuid": "08e9591f-a21b-4144-ad2b-e9d14a27ef16",
"uuid": "b5adc8c7-0f80-4f25-b008-8b913d8cc004",
"beforeHooks": [],

@@ -206,3 +134,3 @@ "afterHooks": [],

"skipped": [],
"duration": 13641,
"duration": 16984,
"rootEmpty": true

@@ -209,0 +137,0 @@ }

{
"name": "loadmill",
"version": "3.2.5",
"version": "4.0.0",
"description": "A node.js module for running load tests and functional tests on loadmill.com",

@@ -5,0 +5,0 @@ "keywords": [

@@ -33,76 +33,2 @@ # Loadmill

### Test Suites
You may launch an existing test suite by supplying the suite id - this is usually useful for testing your API for regressions after every new deployment.
Test suites are launched and not awaiting the results.
```js
const loadmill = require('loadmill')({token: process.env.LOADMILL_API_TOKEN});
/**
* @returns { id: 'uuid', type: 'test-suite' }
*/
const result = await loadmill.runTestSuite({id: "test-suite-uuid"});
```
You can also extend the suite object with `options` object - containing:
* additionalDescription - added at the end of the test suite description.
* labels - will execute only flows attached to these labales.
Also, you may add a second argument if you wish to override suite parameters
```js
const result = await loadmill.runTestSuite(
{
id: "test-suite-uuid",
options: { //optional
additionalDescription: "description to add", // will be added to the end of the test suite description.
labels: ["label1", "label2"], //run flows that are assigned to specific label/s
pool: "some-pool-name" // Execute tests from a dedicated agent's pool (when using private agent)
}
},
{
"parameterKey": "overrided value"
}
);
```
You can run the test suite and create a junit-like report in the end:
```js
/**
* @returns {id: string, type: 'load' | 'test-suite', passed: boolean, url: string}
*/
loadmill.runTestSuite({id: "test-suite-uuid"})
.then(loadmill.wait)
.then(loadmill.junitReport);
// promise with async/await
const id = await loadmill.runTestSuite({id: "test-suite-uuid"});
const result = await loadmill.wait(id);
loadmill.junitReport(result); // may add a second arg of path to save the report to.
```
You can run the test suite and create a mochawesome report in the end:
```js
/**
* @returns {id: string, type: 'load' | 'test-suite', passed: boolean, url: string}
*/
loadmill.runTestSuite({id: "test-suite-uuid"})
.then(loadmill.wait)
.then(loadmill.mochawesomeReport);
// promise with async/await
const id = await loadmill.runTestSuite(
{
id: "test-suite-uuid", // required
options: { //optional
additionalDescription: "description to add", // added at the end of the test suite description.
labels: ["label1", "label2"], // run flows that are assigned to specific label/s
pool: "some-pool-name" // Execute tests from a dedicated agent's pool (when using private agent)
}
},
{ "parameterKey": "overrided value" } //optional
);
const result = await loadmill.wait(id);
loadmill.mochawesomeReport(result); // may add a second arg of path to save the report to.
```
### Test Plans

@@ -153,3 +79,3 @@

/**
* @returns {id: string, type: 'load' | 'test-suite', passed: boolean, url: string}
* @returns {id: string, type: 'load' | 'test-plan', passed: boolean, url: string}
*/

@@ -179,17 +105,17 @@ loadmill.run("./load-tests/long_test.json")

```
loadmill <load-config-file-or-folder | test-suite-id> -t <token> [options] [parameter=value...]
loadmill <test-plan-id || load-test-config-file> -t <token> [options] [parameter=value...]
```
### Test suites
### Test Plan
You may launch a test suite by setting the `-s` or `--test-suite` option:
You may launch a test plan by setting the --test-plan option:
```
loadmill test-suite-id --test-suite -t DW2rTlkNmE6A3ax5LVTSDxv2Jfw4virjQpmbOaLG
loadmill <test-plan-id> --test-plan -w -v -t <token> --report --colors --labels "label1,label2"
```
The test suite will be launched and its unique identifier will be printed to the standard output. You may alternatively
set the `-w` or `--wait` option in order to wait for the test-suite to finish, in which case only the result JSON will be
set the `-w` or `--wait` option in order to wait for the test-plan to finish, in which case only the result JSON will be
printed out at the end
You can add an additional description at the end of the current suite's description with the `--additional-description <description>` option.
You can add an additional description at the end of the current plan's description with the `--additional-description <description>` option.

@@ -199,13 +125,5 @@ You can tell loadmill to run flows that are assigned to a specific label with the `--labels <labels>` option. Multiple labels can be provided by seperated them with "," (e.g. 'label1,label2').

```
loadmill <test-suite-id> --test-suite -t <token> --labels "label1,label2"
loadmill <test-plan-id> --test-plan -t <token> --labels "label1,label2" --additional-description "build 1986"
```
### Test Plan
You may launch a test plan by setting the --test-plan option:
```
loadmill <test-plan-id> --test-plan -w -v -t <token> --report --colors --labels "label1,label2"
```
### Load Tests

@@ -245,6 +163,5 @@

- `-l, --load-test` Launch a load test.
- `--test-plan` Launch a test plan.
- `-s, --test-suite` Launch a test suite. If set then a test suite id must be provided instead of config file.
- `--test-plan` Launch a test plan (default).
- `-p, --parallel` Set the concurrency of a running test suites in a test plan. Max concurrency is 10.
- `--additional-description <description>` Add an additional description at the end of the current suite's / test-plan's description.
- `--additional-description <description>` Add an additional description at the end of the current test-plan's description.
- `--labels <labels>`, Run flows that are assigned to a specific label. Multiple labels can be provided by seperated them with "," (e.g. 'label1,label2').

@@ -251,0 +168,0 @@ - `--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')

@@ -5,10 +5,11 @@ import './polyfills'

import {
getJSONFilesInFolderRecursively,
filterLabels,
TESTING_HOST,
toLoadmillParams,
readRawParams
readRawParams,
} from './utils';
import { junitReport as createJunitReport, mochawesomeReport as createMochawesomeReport } from './reporter';
const TEST_PLAN_POLL_INTERVAL_IN_MS = 10 * 1000 // 10 seconds
export = Loadmill;

@@ -24,22 +25,4 @@

const testingServer = "https://" + _testingServerHost;
const testSuitesAPI = `${testingServer}/api/test-suites`;
const testPlansAPI = `${testingServer}/api/test-plans`;
async function _runFolderSync(
listOfFiles: string[],
execFunc: (...args) => Promise<any>,
...funcArgs) {
const results: Loadmill.TestResult[] = [];
for (let file of listOfFiles) {
let res = await execFunc(file, ...funcArgs);
const testResult = await _wait(res);
results.push(testResult);
if (!testResult.passed) break;
}
return results;
}
async function _wait(testDefOrId: string | Loadmill.TestDef, callback?: Loadmill.Callback): Promise<Loadmill.TestResult> {

@@ -56,2 +39,3 @@ let resolve, reject;

let retries = 1;
const intervalId = setInterval(async () => {

@@ -70,18 +54,5 @@ try {

const testResult: Loadmill.TestResult = {
...testDef,
url: webUrl,
description: body && body.description,
passed: isTestPassed(body, testDef.type),
startTime: body.startTime,
endTime: body.endTime,
status: body.status
};
const testResult: Loadmill.TestResult = toTestResult(testDef, webUrl, body);
if (testDef.type === Loadmill.TYPES.SUITE) {
testResult.flowRuns = reductFlowRunsData(body.testSuiteFlowRuns);
}
else if (testDef.type === Loadmill.TYPES.TEST_PLAN) {
testResult.testSuitesRuns = reductTestSuitesRuns(body.testSuitesRuns, testingServer)
}
redactData(testResult, body, testingServer);

@@ -97,4 +68,10 @@ if (callback) {

catch (err) {
clearInterval(intervalId);
if (retries < 3) {
retries++;
return;
} else {
clearInterval(intervalId);
}
if (callback) {

@@ -107,4 +84,3 @@ callback(err, null);

}
},
10 * 1000);
}, TEST_PLAN_POLL_INTERVAL_IN_MS);

@@ -117,38 +93,2 @@ return callback ? null! as Promise<any> : new Promise((_resolve, _reject) => {

async function _runTestSuite(
suite: Loadmill.TestSuiteDef,
paramsOrCallback: Loadmill.ParamsOrCallback,
callback?: Loadmill.Callback) {
const params = paramsOrCallback && typeof paramsOrCallback !== 'function' ? paramsOrCallback : {};
const overrideParameters = toParams(params, suite.options?.parametersFile);
const suiteId = suite.id;
const additionalDescription = suite.options && suite.options.additionalDescription;
const labels = suite.options && suite.options.labels && filterLabels(suite.options.labels);
const failGracefully = suite.options && suite.options.failGracefully;
const pool = suite.options && suite.options.pool;
return wrap(
async () => {
const {
body: {
testSuiteRunId,
err
}
} = await superagent.post(`${testSuitesAPI}/${suiteId}/run${failGracefully ? '?failGracefully=true' : ''}`)
.send({ overrideParameters, additionalDescription, labels, pool })
.auth(token, '');
if (err || !testSuiteRunId) {
console.error(err ? JSON.stringify(err) : "The server encountered an error while handling the request");
return;
}
return { id: testSuiteRunId, type: Loadmill.TYPES.SUITE };
},
callback || paramsOrCallback
);
}
async function _runTestPlan(

@@ -214,15 +154,2 @@ testPlan: Loadmill.TestPlanDef,

async runFolder(
folderPath: string,
paramsOrCallback?: Loadmill.ParamsOrCallback,
callback?: Loadmill.Callback): Promise<Array<Loadmill.TestResult>> {
const listOfFiles = getJSONFilesInFolderRecursively(folderPath);
if (listOfFiles.length === 0) {
console.log(`No Loadmill test files were found at ${folderPath} - exiting...`);
}
return _runFolderSync(listOfFiles, this.run, paramsOrCallback, callback);
},
wait(testDefOrId: string | Loadmill.TestDef, callback?: Loadmill.Callback): Promise<Loadmill.TestResult> {

@@ -232,10 +159,2 @@ return _wait(testDefOrId, callback);

async runTestSuite(
suite: Loadmill.TestSuiteDef,
paramsOrCallback?: Loadmill.ParamsOrCallback,
callback?: Loadmill.Callback): Promise<Loadmill.TestDef> {
return _runTestSuite(suite, paramsOrCallback, callback);
},
async runTestPlan(

@@ -269,2 +188,19 @@ testPlan: Loadmill.TestPlanDef,

}
function toTestResult(testDef: Loadmill.TestDef, webUrl: string, body: any): Loadmill.TestResult {
return {
...testDef,
url: webUrl,
description: body && body.description,
passed: isTestPassed(body, testDef.type),
startTime: body.startTime,
endTime: body.endTime,
status: body.status
};
}
function redactData(testResult: Loadmill.TestResult, body: any, testingServer: string) {
testResult.testSuitesRuns = reductTestSuitesRuns(body.testSuitesRuns, testingServer);
}
function isTestInFinalState(body, runType) {

@@ -307,12 +243,2 @@ if (runType === Loadmill.TYPES.TEST_PLAN) {

function reductFlowRunsData(flowRuns) {
if (flowRuns) {
return flowRuns.map(f => ({
id: f.id,
description: f.description,
status: f.status
}));
}
}
function reductTestSuitesRuns(suitesRuns, testingServer) {

@@ -319,0 +245,0 @@ if (suitesRuns) {

import * as Loadmill from './index';
import * as program from 'commander';
import {
getJSONFilesInFolderRecursively,
getLogger,

@@ -9,3 +8,2 @@ isUUID,

convertStrToArr,
printFlowRunsReport,
printTestSuitesRunsReport,

@@ -18,5 +16,5 @@ toLoadmillParams,

program
.usage("<testSuiteId | load-config-file-or-folder> -t <token> [options] [parameter=value...]")
.usage("<testPlanId | load-config-file> -t <token> [options] [parameter=value...]")
.description(
"Run a test suite (default option), test plan or a load test on loadmill.com.\n " +
"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 " +

@@ -27,4 +25,3 @@ "Learn more at https://www.npmjs.com/package/loadmill#cli"

.option("-l, --load-test", "Launch a load test.")
.option("--test-plan", "Launch a test plan.")
.option("-s, --test-suite", "Launch a test suite (default option). If set then a test suite id must be provided instead of config file.")
.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")

@@ -91,3 +88,2 @@ .option("--additional-description <description>", "Add an additional description at the end of the current suite's description - available only for test suites.")

const testSuite = !loadTest && !testPlan;
if (verbose) {

@@ -113,3 +109,2 @@ // verbose trumps quiet:

testPlan,
testSuite,
additionalDescription,

@@ -146,62 +141,3 @@ labels,

let res: Loadmill.TestResult | undefined;
if (testSuite) {
const suiteLabels = convertStrToArr(labels)
if (!isUUID(input)) { //if test suite flag is on then the input should be uuid
validationFailed("Test suite run flag is on but no valid test suite id was provided.");
}
try {
logger.verbose(`Executing suite with id ${input}`);
let running = await loadmill.runTestSuite(
{
id: input,
options: {
additionalDescription, labels: suiteLabels, pool
}
},
parameters);
if (running && running.id) {
if (wait) {
logger.verbose("Waiting for test suite run with id", running.id);
res = await loadmill.wait(running);
if (report && res.flowRuns) {
printFlowRunsReport(res.description, res.flowRuns, logger, colors);
}
if (res && junitReport) {
await createJunitReport(res, token, junitReportPath);
}
if (res && mochawesomeReport) {
await createMochawesomeReport(res, token, mochawesomeReportPath);
}
if (res && res.passed != null && !res.passed) {
testFailed(`Test suite ${res.id || input} has failed`);
}
}
if (!quiet) {
logger.log(res ? getObjectAsString(res, colors) : running.id);
}
} else {
testFailed(`Couldn't run test suite 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 suite with id ${input}. ${extInfo ? extInfo : ''}`);
}
}
else if (testPlan) {
if (testPlan || !loadTest) {
const planLabels = convertStrToArr(labels)

@@ -275,36 +211,28 @@

else { // if test suite flag is off then the input should be fileOrFolder
else { // if test plan flag is off then the input should be a conf file
const fileOrFolder = input;
if (!fileOrFolder) {
validationFailed("No configuration file or folder were provided.");
const configFile = input;
if (!configFile) {
validationFailed("No configuration file were provided.");
}
let res;
const listOfFiles = getJSONFilesInFolderRecursively(fileOrFolder);
if (listOfFiles.length === 0) {
logger.log(`No Loadmill test files were found at ${fileOrFolder} - exiting...`);
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);
}
for (let file of listOfFiles) {
let res;
if (!quiet) {
logger.log(JSON.stringify(res, null, 4) || id);
}
logger.verbose(`Launching ${file} as load test`);
const id = await loadmill.run(file, parameters);
if (res && res.passed != null && !res.passed) {
logger.error(`❌ Test ${configFile} failed.`);
if (wait && loadTest) {
logger.verbose("Waiting for test:", res ? res.id : id);
res = await loadmill.wait(res || id);
if (bail) {
process.exit(1);
}
if (!quiet) {
logger.log(JSON.stringify(res, null, 4) || id);
}
if (res && res.passed != null && !res.passed) {
logger.error(`❌ Test ${file} failed.`);
if (bail) {
process.exit(1);
}
}
}

@@ -311,0 +239,0 @@ }

import * as fs from "fs";
import * as path from "path";
import isEmpty = require('lodash/isEmpty');

@@ -58,20 +57,2 @@ import isAString = require('lodash/isString');

export const getJSONFilesInFolderRecursively = (fileOrFolder: string, filelist: string[] = []): string[] => {
let isFile = fs.statSync(fileOrFolder).isFile();
if (isFile && endsWith(fileOrFolder, '.json')) {
filelist.push(fileOrFolder);
} else if (!isFile) {
fs.readdirSync(fileOrFolder)
.map(file =>
getJSONFilesInFolderRecursively(path.join(fileOrFolder, file), filelist));
}
return filelist;
};
const endsWith = (str, suffix) => str.indexOf(suffix, str.length - suffix.length) !== -1;
export const isEmptyObj = (obj) => isEmpty(obj);

@@ -78,0 +59,0 @@ export const isString = (obj) => isAString(obj);

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc