diff --git a/packages/firecamp-cli/package.json b/packages/firecamp-cli/package.json index bcb064017..b75e15548 100644 --- a/packages/firecamp-cli/package.json +++ b/packages/firecamp-cli/package.json @@ -23,7 +23,6 @@ "@oclif/core": "^2.8.5", "@oclif/plugin-help": "^5.2.9", "@oclif/plugin-plugins": "^2.4.7", - "boxen": "^7.1.0", "cli-table3": "^0.6.3", "figlet": "^1.6.0", "figures": "^5.0.0", @@ -31,6 +30,8 @@ "kleur": "^4.1.5", "load-json-file": "^7.0.1", "ora": "^6.3.1", + "pretty-bytes": "^6.1.0", + "pretty-ms": "^8.0.0", "react-fast-compare": "^3.2.2" }, "devDependencies": { diff --git a/packages/firecamp-cli/src/commands/collection/run.ts b/packages/firecamp-cli/src/commands/collection/run.ts new file mode 100644 index 000000000..9d7cfe785 --- /dev/null +++ b/packages/firecamp-cli/src/commands/collection/run.ts @@ -0,0 +1,104 @@ +import { Args, Command, Flags } from '@oclif/core' +import { loadJsonFile } from 'load-json-file'; +import c from 'kleur'; +import figlet from 'figlet' +//@ts-ignore https://github.com/egoist/tsup/issues/760 +import Runner, { ERunnerEvents, IRunnerOptions } from '@firecamp/collection-runner' +import _RestExecutor from '@firecamp/rest-executor'; +//@ts-ignore //TODO: rest-executor is commonjs lib while runner is esm. we'll move all lib in esm in future +const RestExecutor = _RestExecutor.default +import CliReporter from '../../reporters/cli.js' +/** + * Run command example + * ./bin/dev run ../../test/data/FirecampRestEchoServer.firecamp_collection.json + */ +export default class Run extends Command { + static description = 'Run Firecamp Collection' + + static examples = [ + '<%= config.bin %> <%= command.id %> ./echo.firecamp_collection.json', + ] + + static flags = { + 'environment': Flags.string({ char: 'e', description: 'Provide a path to a Firecamp Environment file' }), + 'globals': Flags.string({ char: 'g', description: 'Provide a path to a Firecamp Globals file' }), + 'iteration-count': Flags.string({ char: 'n', description: 'Set the number of iterations for the collection run' }), + // 'iteration-data': Flags.string({ char: 'd', description: 'Provide the data file to be used for iterations. (should be JSON or CSV) file to use for iterations JSON or CSV' }), + 'delay-request': Flags.integer({ description: 'Set the extent of delay between requests in milliseconds (default: 0)' }), + // timeout: Flags.integer({ description: 'Set a timeout for collection run in milliseconds (default: 0)' }), + // 'timeout-request': Flags.integer({ description: 'Set a timeout for requests in milliseconds (default: 0)' }), + } + + static args = { + path: Args.string({ description: 'provide the collection path' }), + } + + public async run(): Promise { + const { args, flags } = await this.parse(Run) + const { path } = args + if (!path) { + this.logError('The collection path is missing') + return + } + const { + environment, + globals, + "iteration-count": iterationCount, + "iteration-data": iterationData, + timeout, + "delay-request": delayRequest, + "timeout-request": timeoutRequest, + } = flags; + + // const _filepath = new URL(`../../../${path}`, import.meta.url).pathname + loadJsonFile(path) + .then(async (collection) => { + + let envObj = { values: [] }; + let globalObj = { values: [] }; + if (environment) { + try { + envObj = await loadJsonFile(environment) + } catch (e: any) { + this.logError('could not load environment', e, 1) + } + } + + if (globals) { + try { + globalObj = await loadJsonFile(globals) + } catch (e: any) { + this.logError('could not load globals', e); + } + } + // this.logJson(collection); + const options: IRunnerOptions = { + getExecutor: () => new RestExecutor(), + environment: envObj.values || [], + globals: globalObj.values || [] + } + if (iterationCount) options.iterationCount = +iterationCount; + if (iterationData) options.iterationData = iterationData; + if (timeout) options.timeout = +timeout; + if (delayRequest) options.delayRequest = +delayRequest; + if (timeoutRequest) options.timeoutRequest = +timeoutRequest; + + this.log(c.gray(figlet.textSync("Firecamp"))); + + const runner = new Runner(collection, options); + const emitter = runner.run() + new CliReporter(emitter); + }) + .catch(e => { + // console.error(e) + if (e.code == 'ENOENT') this.logError(`Error: could not load collection file`, e, 1); + else this.logError('The collection file is not valid', null, 1); + }) + } + + logError(title: string, e?: any, exitCode?: number) { + if (title) this.logToStderr(`${c.red('Error')}: ${title}`); + if (e?.message) this.logToStderr(` `, e.message); + if (Number.isInteger(exitCode)) process.exit(exitCode); + } +} diff --git a/packages/firecamp-cli/src/commands/run.ts b/packages/firecamp-cli/src/commands/run.ts deleted file mode 100644 index 7525ccc4a..000000000 --- a/packages/firecamp-cli/src/commands/run.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { Args, Command, Flags } from '@oclif/core' -import { loadJsonFile } from 'load-json-file'; -import figlet from 'figlet' -//@ts-ignore https://github.com/egoist/tsup/issues/760 -import Runner, { ERunnerEvents } from '@firecamp/collection-runner' -import _RestExecutor from '@firecamp/rest-executor'; -//@ts-ignore //TODO: rest-executor is commonjs lib while runner is esm. we'll move all lib in esm in future -const RestExecutor = _RestExecutor.default -import Reporter from './../helper/reporter.js' -/** - * Run command example - * ./bin/dev run ../../test/data/FirecampRestEchoServer.firecamp_collection.json - */ -export default class Run extends Command { - static description = 'Run Firecamp Collection' - - static examples = [ - '<%= config.bin %> <%= command.id %>', - ] - - static flags = { - // flag with a value (-n, --name=VALUE) - // name: Flags.string({ char: 'n', description: 'name to print' }), - // flag with no value (-f, --force) - // force: Flags.boolean({ char: 'f' }), - } - - static args = { - file: Args.string({ description: 'firecamp collection path' }), - } - - public async run(): Promise { - const { args } = await this.parse(Run) - const { file } = args - if (!file) { - this.logToStderr('error: The collection path is missing') - return - } - this.log(figlet.textSync("Firecamp")) - - - // tasks.run() - // return - - const _filepath = new URL(`../../${file}`, import.meta.url).pathname - loadJsonFile(_filepath) - .then(collection => { - // this.logJson(collection); - const runner = new Runner(collection, { - executeRequest: (request: any) => { - const executor = new RestExecutor(); - return executor.send(request, { collectionVariables: [], environment: [], globals: [] }); - } - }) - const reporter = new Reporter(); - - return runner.run() - .on(ERunnerEvents.Start, () => { }) - .on(ERunnerEvents.BeforeRequest, (request: any) => { - // console.log(request) - return reporter.startRequest(request) - }) - .on(ERunnerEvents.Request, async (result: any) => { - - reporter.done(result) - }) - // .on(ERunnerEvents.Done, console.log) - }) - .then(testResults => { - // console.log(testResults) - // this.logJson(testResults) - }) - .catch(e => { - console.error(e) - if (e.code == 'ENOENT') this.logToStderr(`error: file not exist at ${_filepath}`) - else this.logToStderr('error: The collection file is not valid') - }) - } -} diff --git a/packages/firecamp-cli/src/commands/table.ts b/packages/firecamp-cli/src/commands/table.ts deleted file mode 100644 index 76880867c..000000000 --- a/packages/firecamp-cli/src/commands/table.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Command, ux } from '@oclif/core' -import c from 'kleur'; -import Table from 'cli-table3'; -import boxen from 'boxen'; - -export default class Users extends Command { - static flags = {} - - - async run() { - const { flags } = await this.parse(Users) - // const { data: users } = await axios.get('https://jsonplaceholder.typicode.com/users') - - console.log(boxen('unicorn', { padding: 1 })); - - - const _header = (h: string) => c.magenta(h); - var table = new Table({ - head: [ - _header('method'), - _header('api'), - _header('status'), - _header('pass'), - _header('fail') - ], - // colWidths: [100, 400, 200, 150, 150 ], - wordWrap: true, - chars: { - 'top': '═', 'top-mid': '╤', 'top-left': '╔', 'top-right': '╗' - , 'bottom': '═', 'bottom-mid': '╧', 'bottom-left': '╚', 'bottom-right': '╝' - , 'left': '║', 'left-mid': '╟', 'mid': '─', 'mid-mid': '┼' - , 'right': '║', 'right-mid': '╢', 'middle': '│' - } - }); - - table.push( - [c.green('GET'), c.blue('https://localhost:300/api/post'), '200', 3, 2], - [c.blue('POST'), c.blue('https://localhost:300/api/post'), '200', 3, 1], - [c.red('DELETE'), c.blue('https://localhost:300/api/post'), '200', 3, 2] - ); - - this.log(table.toString()); - - // console.log(c.blue('Hello') + ' World' + c.red('!')); - - // boxen('unicorn', {padding: 1}) - - } -} \ No newline at end of file diff --git a/packages/firecamp-cli/src/helper/reporter.ts b/packages/firecamp-cli/src/helper/reporter.ts deleted file mode 100644 index de2bbc128..000000000 --- a/packages/firecamp-cli/src/helper/reporter.ts +++ /dev/null @@ -1,79 +0,0 @@ -import ora, { Ora } from 'ora'; -import c from 'kleur'; -import figures from 'figures'; - -export default class Reporter { - - private request: any; - private spinner: Ora = ora(); - init() { - this.spinner = ora() - this.newLine(); - } - - startRequest(request: any) { - // console.log(request) - this.init() - this.request = request - this.spinner.start(this._title()) - } - - done(result: any) { - - if (result.response?.testResult) { - const { testResult, testResult: { tests } } = result.response; - this.spinner.stopAndPersist({ - symbol: testResult.failed == 0 ? c.green(figures.tick) : c.red(figures.cross) - }); - tests.map((t: any) => { - if (t.isPassed) this.logTest(t) - else this.logTest(t, true) - }) - // console.log(testResult) - - this.logResponseMeta(result.response.response) - - } - } - - newLine(n: number = 1) { - if (n > 10) n = 5 - if (n < 0) n = 1; - Array.from({ length: n }).forEach(() => console.log('')) - } - - _title() { - const { method = '', name = '', url, path } = this.request - const title = `${name} ${c.dim().cyan(`(${path})`)}`; - return title; - - // title with url - return `${title} - ${c.dim(method.toUpperCase() + ' ' + url)}` - } - - logTest(test: any, failed = false) { - const { name, error } = test - const log = !failed - ? `${c.green(' ' + figures.tick)} ${c.dim(name)}` - : `${c.red(' ' + figures.cross)} ${c.red().dim(name)}` - console.log(log) - - if (error) - console.log(` `, c.italic().dim(error.message)) - // throw new Error(error) - } - - logResponseMeta(response: any) { - const { code, status, responseTime, responseSize } = response - - console.log(c.dim(' ----------------')) - console.log( - ` `, - c.dim(`${code} ${status}`), - c.dim(`${responseTime}ms`), - c.dim(`${responseSize}B`) - ) - } - -} diff --git a/packages/firecamp-cli/src/reporters/cli.ts b/packages/firecamp-cli/src/reporters/cli.ts new file mode 100644 index 000000000..c2bdda012 --- /dev/null +++ b/packages/firecamp-cli/src/reporters/cli.ts @@ -0,0 +1,148 @@ +import ora, { Ora } from 'ora'; +import c from 'kleur'; +import figures from 'figures'; +import Table from 'cli-table3'; +import prettyMs from 'pretty-ms'; +import prettyBytes from 'pretty-bytes'; +import { ERunnerEvents } from '@firecamp/collection-runner'; + +export default class Reporter { + + private request: any; + private response: any; + private spinner: Ora; + constructor(emitter: any) { + this.spinner = ora(); + emitter + .on(ERunnerEvents.Start, (col: any) => { + this.log(figures.squareCenter, c.bold(col.name)) + this.newLine(); + }) + .on(ERunnerEvents.BeforeIteration, (it: any) => { + if (it.total > 1) { + this.log(c.gray().dim().underline(`Iteration ${it.current}/${it.total}`)); + this.newLine(); + } + }) + .on(ERunnerEvents.Iteration, () => { + this.newLine(); + }) + .on(ERunnerEvents.BeforeFolder, (folder: any) => { + this.log(figures.triangleRight, c.bold(folder.name)); + }) + .on(ERunnerEvents.Folder, (folder: any) => { + this.newLine(); + }) + .on(ERunnerEvents.BeforeRequest, (request: any) => { + this.onBeforeRequest(request) + }) + .on(ERunnerEvents.Request, (result: any) => { + this.onRequest(result) + }) + .on(ERunnerEvents.Done, (result: any) => { + this.onDone(result) + }); + } + initSpinner() { + this.spinner = ora() + // this.newLine(); + } + + onBeforeRequest(request: any) { + // console.log(request) + this.initSpinner() + this.request = request + this.response = null; + this.spinner.start(this._title()) + } + + onRequest(result: any) { + this.response = result?.response?.response + if (result.response?.testResult) { + const { testResult, testResult: { tests } } = result.response; + this.spinner.stopAndPersist({ + symbol: '↳', //testResult.failed == 0 ? c.green(figures.tick) : c.red(figures.cross), + text: this._title() + }); + tests.map((t: any) => { + if (t.isPassed) this.logTest(t) + else this.logTest(t, true) + }) + // console.log(testResult) + } + this.newLine(); + } + + onDone(result: any) { + this.logResult(result) + } + + newLine(n: number = 1) { + if (n > 10) n = 5 + if (n < 0) n = 1; + Array.from({ length: n }).forEach(() => this.log('')) + } + + _title() { + const { method = '', name = '', url, path } = this.request + let title = `${name} ${c.dim().cyan(`(${path})`)}`; + + // title with url + title = `${title} + ${c.gray(method.toUpperCase() + ' ' + url)}`; + + if (this.response) + title += ' ' + this._responseMeta() + return title; + } + + _responseMeta() { + const { code, status, responseTime, responseSize } = this.response + const line = `[${code + ' ' + status}, ${prettyBytes(responseSize)}, ${prettyMs(responseTime)}]`; + return c.dim(line) + } + + logTest(test: any, failed = false) { + const { name, error } = test + const log = !failed + ? `${c.green(' ' + figures.tick)} ${c.gray(name)}` + : `${c.red(' ' + figures.cross)} ${c.gray(name)}` + this.log(log) + + if (error) + this.log(` `, c.italic().gray().dim(error.message)) + // throw new Error(error) + } + + logResult(summary: any) { + const { stats: { iterations, requests, tests }, timings, transfers } = summary; + const _header = (h: string) => c.magenta(h); + var table = new Table(); + table.push( + ['', { hAlign: 'right', content: _header('executed') }, { hAlign: 'right', content: _header('failed') }], + [ + { hAlign: 'right', content: c[iterations.failed ? 'red' : 'gray']('iterations') }, + { hAlign: 'right', content: iterations.total }, + { hAlign: 'right', content: c[iterations.failed ? 'red' : 'gray'](iterations.failed) }], + [ + { hAlign: 'right', content: c[requests.failed ? 'red' : 'gray']('requests') }, + { hAlign: 'right', content: requests.total }, + { hAlign: 'right', content: c[requests.failed ? 'red' : 'gray'](requests.failed) } + ], + [ + { hAlign: 'right', content: c[tests.failed ? 'red' : 'gray']('tests') }, + { hAlign: 'right', content: tests.total }, + { hAlign: 'right', content: c[tests.failed ? 'red' : 'gray'](tests.failed) } + ], + [{ colSpan: 3, content: `total run duration: ${prettyMs(timings.runDuration)}` }], + [{ colSpan: 3, content: `total data received: ${prettyBytes(transfers.responseTotal)} (approx)` }], + [{ colSpan: 3, content: `average response time: ${prettyMs(timings.responseAvg)} [min: ${prettyMs(timings.responseMin)}, max: ${prettyMs(timings.responseMax)}]` }], + ); + this.log(table.toString()); + } + + log(...l: any[]) { + console.log(...l) + } + +} diff --git a/packages/firecamp-cli/test/data/echo.env.json b/packages/firecamp-cli/test/data/echo.env.json new file mode 100644 index 000000000..754be506a --- /dev/null +++ b/packages/firecamp-cli/test/data/echo.env.json @@ -0,0 +1,10 @@ +{ + "id": "678efsdf6X3r4i3", + "name": "RestEchoServer", + "values": [ + { + "key": "host", + "value": "http://localhost:3000" + } + ] +} \ No newline at end of file diff --git a/packages/firecamp-cli/test/data/FirecampRestEchoServer.firecamp_collection.json b/packages/firecamp-cli/test/data/restEchoServer.firecamp_collection.json similarity index 99% rename from packages/firecamp-cli/test/data/FirecampRestEchoServer.firecamp_collection.json rename to packages/firecamp-cli/test/data/restEchoServer.firecamp_collection.json index 7d954552c..a455eccb1 100644 --- a/packages/firecamp-cli/test/data/FirecampRestEchoServer.firecamp_collection.json +++ b/packages/firecamp-cli/test/data/restEchoServer.firecamp_collection.json @@ -123,7 +123,7 @@ "requests": [ { "url": { - "raw": "http://localhost:3000/get?foo1=bar1&foo2=bar2", + "raw": "{{host}}/get?foo1=bar1&foo2=bar2", "queryParams": [ { "id": "RRMgDmGWwOa9ZwpijZiWj", @@ -190,7 +190,7 @@ }, { "url": { - "raw": "http://localhost:3000/post", + "raw": "{{host}}/post", "queryParams": [], "pathParams": [] }, @@ -253,7 +253,7 @@ }, { "url": { - "raw": "http://localhost:3000/post", + "raw": "{{host}}/post", "queryParams": [], "pathParams": [] }, diff --git a/packages/firecamp-collection-runner/dist/chunk-3OSFNS7Z.cjs b/packages/firecamp-collection-runner/dist/chunk-3OSFNS7Z.cjs new file mode 100644 index 000000000..1f3dc6f7b --- /dev/null +++ b/packages/firecamp-collection-runner/dist/chunk-3OSFNS7Z.cjs @@ -0,0 +1,10 @@ +"use strict";Object.defineProperty(exports, "__esModule", {value: true});var __defProp = Object.defineProperty; +var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; +var __publicField = (obj, key, value) => { + __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); + return value; +}; + + + +exports.__publicField = __publicField; diff --git a/packages/firecamp-collection-runner/dist/chunk-XXPGZHWZ.js b/packages/firecamp-collection-runner/dist/chunk-XXPGZHWZ.js new file mode 100644 index 000000000..782cce266 --- /dev/null +++ b/packages/firecamp-collection-runner/dist/chunk-XXPGZHWZ.js @@ -0,0 +1,10 @@ +var __defProp = Object.defineProperty; +var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; +var __publicField = (obj, key, value) => { + __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); + return value; +}; + +export { + __publicField +}; diff --git a/packages/firecamp-collection-runner/dist/index.cjs b/packages/firecamp-collection-runner/dist/index.cjs index b999723c6..f6833930d 100644 --- a/packages/firecamp-collection-runner/dist/index.cjs +++ b/packages/firecamp-collection-runner/dist/index.cjs @@ -1,10 +1,8 @@ -"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }var __defProp = Object.defineProperty; -var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; -var __publicField = (obj, key, value) => { - __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); - return value; -}; +"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _createStarExport(obj) { Object.keys(obj) .filter((key) => key !== "default" && key !== "__esModule") .forEach((key) => { if (exports.hasOwnProperty(key)) { return; } Object.defineProperty(exports, key, {enumerable: true, configurable: true, get: () => obj[key]}); }); } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } + +var _chunk3OSFNS7Zcjs = require('./chunk-3OSFNS7Z.cjs'); var _eventemitter3 = require('eventemitter3'); var _eventemitter32 = _interopRequireDefault(_eventemitter3); +var _typesjs = require('./types.js'); _createStarExport(_typesjs); const delay = async (ts) => { return new Promise((rs) => { setTimeout(() => { @@ -12,29 +10,56 @@ const delay = async (ts) => { }, ts); }); }; -var ERunnerEvents = /* @__PURE__ */ ((ERunnerEvents2) => { - ERunnerEvents2["Start"] = "start"; - ERunnerEvents2["BeforeRequest"] = "beforeRequest"; - ERunnerEvents2["Request"] = "request"; - ERunnerEvents2["Done"] = "done"; - return ERunnerEvents2; -})(ERunnerEvents || {}); class Runner { constructor(collection, options) { - __publicField(this, "collection"); - __publicField(this, "options"); - __publicField(this, "requestOrdersForExecution"); - __publicField(this, "executedRequestQueue"); - __publicField(this, "currentRequestInExecution"); - __publicField(this, "testResults", []); - __publicField(this, "emitter"); - __publicField(this, "i", 0); + _chunk3OSFNS7Zcjs.__publicField.call(void 0, this, "collection"); + _chunk3OSFNS7Zcjs.__publicField.call(void 0, this, "options"); + _chunk3OSFNS7Zcjs.__publicField.call(void 0, this, "folderRunSequence"); + _chunk3OSFNS7Zcjs.__publicField.call(void 0, this, "testResults", []); + _chunk3OSFNS7Zcjs.__publicField.call(void 0, this, "emitter"); + _chunk3OSFNS7Zcjs.__publicField.call(void 0, this, "runStatistics", { + stats: { + iterations: { failed: 0, total: 0 }, + requests: { failed: 0, total: 0 }, + tests: { failed: 0, total: 0 } + }, + timings: { + started: 0, + runDuration: 0, + responseMin: 0, + responseMax: 0, + responseAvg: 0, + responseTotal: 0 + }, + transfers: { + responseTotal: 0 + } + }); this.collection = collection; this.options = options; - this.requestOrdersForExecution = /* @__PURE__ */ new Set(); - this.executedRequestQueue = /* @__PURE__ */ new Set(); - this.currentRequestInExecution = ""; + this.folderRunSequence = /* @__PURE__ */ new Set(); this.emitter = new (0, _eventemitter32.default)(); + this.validate(); + this.assignDefaultOptions(); + this.prepareFolderRunSequence(); + } + assignDefaultOptions() { + if (!this.options.hasOwnProperty("iterationCount")) + this.options.iterationCount = 1; + if (!this.options.hasOwnProperty("delayRequest")) + this.options.delayRequest = 0; + if (!this.options.hasOwnProperty("timeout")) + this.options.timeout = 0; + if (!this.options.hasOwnProperty("timeoutRequest")) + this.options.timeoutRequest = 0; + if (typeof this.options.iterationCount != "number") + throw new Error("--iteration-count is invalid", { cause: "invalidOption" }); + if (typeof this.options.delayRequest != "number") + throw new Error("--delay-request is invalid", { cause: "invalidOption" }); + if (typeof this.options.timeout != "number") + throw new Error("--timeout is invalid", { cause: "invalidOption" }); + if (typeof this.options.timeoutRequest != "number") + throw new Error("--timeout-request is invalid", { cause: "invalidOption" }); } /** * validate that the collection format is valid @@ -55,95 +80,157 @@ class Runner { throw new Error("The collection's request items format is invalid"); return true; } - /** - * prepare an Set of request execution order - */ - prepareRequestExecutionOrder() { + prepareFolderRunSequence() { const { collection, folders } = this.collection; - const { __meta: { fOrders: rootFolderIds = [], rOrders: rootRequestIds = [] } } = collection; - const extractRequestIdsFromFolder = (fId, requestIds = []) => { - const folder = folders.find((f) => f.__ref.id == fId); - if (!folder) - return requestIds; - if (_optionalChain([folder, 'access', _2 => _2.__meta, 'access', _3 => _3.fOrders, 'optionalAccess', _4 => _4.length])) { - const rIds = folder.__meta.fOrders.map((fId2) => extractRequestIdsFromFolder(fId2, requestIds)); - requestIds = [...requestIds, ...rIds]; - } - if (_optionalChain([folder, 'access', _5 => _5.__meta, 'access', _6 => _6.rOrders, 'optionalAccess', _7 => _7.length])) { - requestIds = [...requestIds, ...folder.__meta.rOrders]; - } - return requestIds; - }; - if (Array.isArray(rootFolderIds)) { - rootFolderIds.map((fId) => { - const requestIds = extractRequestIdsFromFolder(fId); - requestIds.forEach(this.requestOrdersForExecution.add, this.requestOrdersForExecution); - }); - } - if (Array.isArray(rootRequestIds)) { - rootRequestIds.forEach(this.requestOrdersForExecution.add, this.requestOrdersForExecution); + const folderMap = new Map(folders.map((folder) => [folder.__ref.id, folder])); + const traverseFolders = (order) => order.flatMap( + (folderId) => folderMap.has(folderId) ? [folderId, ...traverseFolders([folderMap.get(folderId).__ref.folderId])] : [] + ); + const ids = traverseFolders(collection.__meta.fOrders); + ids.forEach(this.folderRunSequence.add, this.folderRunSequence); + } + updateResponseStatistics(response) { + const { + testResult, + response: { code, status, responseSize, responseTime } + } = response; + if (Number.isInteger(testResult.total)) + this.runStatistics.stats.tests.total += testResult.total; + if (Number.isInteger(testResult.failed)) + this.runStatistics.stats.tests.failed += testResult.failed; + if (Number.isInteger(responseSize)) + this.runStatistics.transfers.responseTotal += responseSize; + if (Number.isInteger(responseTime)) { + const { + stats: { requests }, + timings: { responseMin, responseMax } + } = this.runStatistics; + if (responseMin == 0) + this.runStatistics.timings.responseMin = responseTime; + else if (responseTime < responseMin) + this.runStatistics.timings.responseMin = responseTime; + if (responseMax == 0) + this.runStatistics.timings.responseMax = responseTime; + else if (responseTime > responseMax) + this.runStatistics.timings.responseMax = responseTime; + this.runStatistics.timings.responseTotal += responseTime; + this.runStatistics.timings.responseAvg = this.runStatistics.timings.responseTotal / requests.total; } } - async executeRequest(requestId) { + async runRequest(requestId) { const { folders, requests } = this.collection; const request = requests.find((r) => r.__ref.id == requestId); - this.emitter.emit("beforeRequest" /* BeforeRequest */, { + this.emitter.emit(_typesjs.ERunnerEvents.BeforeRequest, { name: request.__meta.name, url: request.url.raw, method: request.method.toUpperCase(), path: fetchRequestPath(folders, request), id: request.__ref.id }); - await delay(500); - const response = await this.options.executeRequest(request); - this.emitter.emit("request" /* Request */, { + await delay(this.options.delayRequest); + const { globals, environment } = this.options; + const executor = this.options.getExecutor(); + const response = await executor.send(request, { collectionVariables: [], environment, globals }); + ; + this.updateResponseStatistics(response); + this.emitter.emit(_typesjs.ERunnerEvents.Request, { id: request.__ref.id, response }); + this.runStatistics.stats.requests.total += 1; return { request, response }; } - async start() { + async runFolder(folderId) { + const folder = this.collection.folders.find((f) => f.__ref.id == folderId); + const requestIds = folder.__meta.rOrders || []; + if (!requestIds.length) + return; + this.emitter.emit(_typesjs.ERunnerEvents.BeforeFolder, { + name: folder.name, + id: folder.__ref.id + }); try { - const { value: requestId, done } = this.requestOrdersForExecution.values().next(); - this.i = this.i + 1; - if (!done) { - this.currentRequestInExecution = requestId; - const res = await this.executeRequest(requestId); + for (let i = 0; i < requestIds.length; i++) { + const res = await this.runRequest(requestIds[i]); this.testResults.push(res); - this.executedRequestQueue.add(requestId); - this.requestOrdersForExecution.delete(requestId); - await this.start(); } - } catch (error) { - console.error(`Error while running the collection:`, error); + } catch (e) { + console.error(`Error while running the collection:`, e); } + this.emitter.emit(_typesjs.ERunnerEvents.Folder, { + id: folder.__ref.id + }); } - exposeOnlyOn() { - return { - on: (evt, fn) => { - this.emitter.on(evt, fn); - return this.exposeOnlyOn(); + async runRootRequests() { + const { collection } = this.collection; + const requestIds = collection.__meta.rOrders || []; + if (!requestIds.length) + return; + this.emitter.emit(_typesjs.ERunnerEvents.BeforeFolder, { + name: "./", + id: collection.__ref.id + }); + try { + for (let i = 0; i < requestIds.length; i++) { + const res = await this.runRequest(requestIds[i]); + this.testResults.push(res); } - }; + } catch (e) { + console.error(`Error while running the collection:`, e); + } + this.emitter.emit(_typesjs.ERunnerEvents.Folder, { + id: collection.__ref.id + }); } - run() { + async runIteration() { try { - this.validate(); + const folderSet = this.folderRunSequence.values(); + const next = async () => { + const { value: folderId, done } = folderSet.next(); + if (!done) { + await this.runFolder(folderId); + await next(); + } + }; + await next(); + await this.runRootRequests(); } catch (e) { - throw e; + console.error(`Error while running the collection:`, e); } - this.prepareRequestExecutionOrder(); + } + run() { setTimeout(async () => { const { collection } = this.collection; - this.emitter.emit("start" /* Start */, { + this.runStatistics.timings.started = (/* @__PURE__ */ new Date()).valueOf(); + this.emitter.emit(_typesjs.ERunnerEvents.Start, { name: collection.name, id: collection.__ref.id }); - await this.start(); - this.emitter.emit("done" /* Done */); + for (let i = 0; i < this.options.iterationCount; i++) { + this.emitter.emit(_typesjs.ERunnerEvents.BeforeIteration, { + current: i + 1, + total: this.options.iterationCount + }); + await this.runIteration(); + this.emitter.emit(_typesjs.ERunnerEvents.Iteration, { + current: i + 1, + total: this.options.iterationCount + }); + this.runStatistics.stats.iterations.total += 1; + } + this.runStatistics.timings.runDuration = (/* @__PURE__ */ new Date()).valueOf() - this.runStatistics.timings.started; + this.emitter.emit(_typesjs.ERunnerEvents.Done, this.runStatistics); }); return this.exposeOnlyOn(); } + exposeOnlyOn() { + return { + on: (evt, fn) => { + this.emitter.on(evt, fn); + return this.exposeOnlyOn(); + } + }; + } } const fetchRequestPath = (folders, request) => { const requestPath = []; @@ -159,4 +246,4 @@ const fetchRequestPath = (folders, request) => { -exports.ERunnerEvents = ERunnerEvents; exports.default = Runner; +exports.default = Runner; diff --git a/packages/firecamp-collection-runner/dist/index.d.ts b/packages/firecamp-collection-runner/dist/index.d.ts index 152027ecf..34de1dbae 100644 --- a/packages/firecamp-collection-runner/dist/index.d.ts +++ b/packages/firecamp-collection-runner/dist/index.d.ts @@ -1,18 +1,16 @@ -declare enum ERunnerEvents { - Start = "start", - BeforeRequest = "beforeRequest", - Request = "request", - Done = "done" -} +import { IRunnerOptions } from './types.js'; +export { ERunnerEvents, IRunStatistics } from './types.js'; +import '@firecamp/types'; + declare class Runner { private collection; private options; - private requestOrdersForExecution; - private executedRequestQueue; - private currentRequestInExecution; + private folderRunSequence; private testResults; private emitter; - constructor(collection: any, options: any); + private runStatistics; + constructor(collection: any, options: IRunnerOptions); + private assignDefaultOptions; /** * validate that the collection format is valid * TODO: late we need to add the zod or json schema here for strong validation @@ -21,17 +19,16 @@ declare class Runner { * @returns boolean */ private validate; - /** - * prepare an Set of request execution order - */ - private prepareRequestExecutionOrder; - private executeRequest; - i: number; - private start; - private exposeOnlyOn; + private prepareFolderRunSequence; + private updateResponseStatistics; + private runRequest; + private runFolder; + private runRootRequests; + private runIteration; run(): { on: (evt: string, fn: (...a: any[]) => void) => any; }; + private exposeOnlyOn; } -export { ERunnerEvents, Runner as default }; +export { IRunnerOptions, Runner as default }; diff --git a/packages/firecamp-collection-runner/dist/index.js b/packages/firecamp-collection-runner/dist/index.js index 5ed7cee5f..cf052d1be 100644 --- a/packages/firecamp-collection-runner/dist/index.js +++ b/packages/firecamp-collection-runner/dist/index.js @@ -1,10 +1,8 @@ -var __defProp = Object.defineProperty; -var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; -var __publicField = (obj, key, value) => { - __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); - return value; -}; +import { + __publicField +} from "./chunk-XXPGZHWZ.js"; import EventEmitter from "eventemitter3"; +import { ERunnerEvents } from "./types.js"; const delay = async (ts) => { return new Promise((rs) => { setTimeout(() => { @@ -12,29 +10,56 @@ const delay = async (ts) => { }, ts); }); }; -var ERunnerEvents = /* @__PURE__ */ ((ERunnerEvents2) => { - ERunnerEvents2["Start"] = "start"; - ERunnerEvents2["BeforeRequest"] = "beforeRequest"; - ERunnerEvents2["Request"] = "request"; - ERunnerEvents2["Done"] = "done"; - return ERunnerEvents2; -})(ERunnerEvents || {}); class Runner { constructor(collection, options) { __publicField(this, "collection"); __publicField(this, "options"); - __publicField(this, "requestOrdersForExecution"); - __publicField(this, "executedRequestQueue"); - __publicField(this, "currentRequestInExecution"); + __publicField(this, "folderRunSequence"); __publicField(this, "testResults", []); __publicField(this, "emitter"); - __publicField(this, "i", 0); + __publicField(this, "runStatistics", { + stats: { + iterations: { failed: 0, total: 0 }, + requests: { failed: 0, total: 0 }, + tests: { failed: 0, total: 0 } + }, + timings: { + started: 0, + runDuration: 0, + responseMin: 0, + responseMax: 0, + responseAvg: 0, + responseTotal: 0 + }, + transfers: { + responseTotal: 0 + } + }); this.collection = collection; this.options = options; - this.requestOrdersForExecution = /* @__PURE__ */ new Set(); - this.executedRequestQueue = /* @__PURE__ */ new Set(); - this.currentRequestInExecution = ""; + this.folderRunSequence = /* @__PURE__ */ new Set(); this.emitter = new EventEmitter(); + this.validate(); + this.assignDefaultOptions(); + this.prepareFolderRunSequence(); + } + assignDefaultOptions() { + if (!this.options.hasOwnProperty("iterationCount")) + this.options.iterationCount = 1; + if (!this.options.hasOwnProperty("delayRequest")) + this.options.delayRequest = 0; + if (!this.options.hasOwnProperty("timeout")) + this.options.timeout = 0; + if (!this.options.hasOwnProperty("timeoutRequest")) + this.options.timeoutRequest = 0; + if (typeof this.options.iterationCount != "number") + throw new Error("--iteration-count is invalid", { cause: "invalidOption" }); + if (typeof this.options.delayRequest != "number") + throw new Error("--delay-request is invalid", { cause: "invalidOption" }); + if (typeof this.options.timeout != "number") + throw new Error("--timeout is invalid", { cause: "invalidOption" }); + if (typeof this.options.timeoutRequest != "number") + throw new Error("--timeout-request is invalid", { cause: "invalidOption" }); } /** * validate that the collection format is valid @@ -55,95 +80,157 @@ class Runner { throw new Error("The collection's request items format is invalid"); return true; } - /** - * prepare an Set of request execution order - */ - prepareRequestExecutionOrder() { + prepareFolderRunSequence() { const { collection, folders } = this.collection; - const { __meta: { fOrders: rootFolderIds = [], rOrders: rootRequestIds = [] } } = collection; - const extractRequestIdsFromFolder = (fId, requestIds = []) => { - const folder = folders.find((f) => f.__ref.id == fId); - if (!folder) - return requestIds; - if (folder.__meta.fOrders?.length) { - const rIds = folder.__meta.fOrders.map((fId2) => extractRequestIdsFromFolder(fId2, requestIds)); - requestIds = [...requestIds, ...rIds]; - } - if (folder.__meta.rOrders?.length) { - requestIds = [...requestIds, ...folder.__meta.rOrders]; - } - return requestIds; - }; - if (Array.isArray(rootFolderIds)) { - rootFolderIds.map((fId) => { - const requestIds = extractRequestIdsFromFolder(fId); - requestIds.forEach(this.requestOrdersForExecution.add, this.requestOrdersForExecution); - }); - } - if (Array.isArray(rootRequestIds)) { - rootRequestIds.forEach(this.requestOrdersForExecution.add, this.requestOrdersForExecution); + const folderMap = new Map(folders.map((folder) => [folder.__ref.id, folder])); + const traverseFolders = (order) => order.flatMap( + (folderId) => folderMap.has(folderId) ? [folderId, ...traverseFolders([folderMap.get(folderId).__ref.folderId])] : [] + ); + const ids = traverseFolders(collection.__meta.fOrders); + ids.forEach(this.folderRunSequence.add, this.folderRunSequence); + } + updateResponseStatistics(response) { + const { + testResult, + response: { code, status, responseSize, responseTime } + } = response; + if (Number.isInteger(testResult.total)) + this.runStatistics.stats.tests.total += testResult.total; + if (Number.isInteger(testResult.failed)) + this.runStatistics.stats.tests.failed += testResult.failed; + if (Number.isInteger(responseSize)) + this.runStatistics.transfers.responseTotal += responseSize; + if (Number.isInteger(responseTime)) { + const { + stats: { requests }, + timings: { responseMin, responseMax } + } = this.runStatistics; + if (responseMin == 0) + this.runStatistics.timings.responseMin = responseTime; + else if (responseTime < responseMin) + this.runStatistics.timings.responseMin = responseTime; + if (responseMax == 0) + this.runStatistics.timings.responseMax = responseTime; + else if (responseTime > responseMax) + this.runStatistics.timings.responseMax = responseTime; + this.runStatistics.timings.responseTotal += responseTime; + this.runStatistics.timings.responseAvg = this.runStatistics.timings.responseTotal / requests.total; } } - async executeRequest(requestId) { + async runRequest(requestId) { const { folders, requests } = this.collection; const request = requests.find((r) => r.__ref.id == requestId); - this.emitter.emit("beforeRequest" /* BeforeRequest */, { + this.emitter.emit(ERunnerEvents.BeforeRequest, { name: request.__meta.name, url: request.url.raw, method: request.method.toUpperCase(), path: fetchRequestPath(folders, request), id: request.__ref.id }); - await delay(500); - const response = await this.options.executeRequest(request); - this.emitter.emit("request" /* Request */, { + await delay(this.options.delayRequest); + const { globals, environment } = this.options; + const executor = this.options.getExecutor(); + const response = await executor.send(request, { collectionVariables: [], environment, globals }); + ; + this.updateResponseStatistics(response); + this.emitter.emit(ERunnerEvents.Request, { id: request.__ref.id, response }); + this.runStatistics.stats.requests.total += 1; return { request, response }; } - async start() { + async runFolder(folderId) { + const folder = this.collection.folders.find((f) => f.__ref.id == folderId); + const requestIds = folder.__meta.rOrders || []; + if (!requestIds.length) + return; + this.emitter.emit(ERunnerEvents.BeforeFolder, { + name: folder.name, + id: folder.__ref.id + }); try { - const { value: requestId, done } = this.requestOrdersForExecution.values().next(); - this.i = this.i + 1; - if (!done) { - this.currentRequestInExecution = requestId; - const res = await this.executeRequest(requestId); + for (let i = 0; i < requestIds.length; i++) { + const res = await this.runRequest(requestIds[i]); this.testResults.push(res); - this.executedRequestQueue.add(requestId); - this.requestOrdersForExecution.delete(requestId); - await this.start(); } - } catch (error) { - console.error(`Error while running the collection:`, error); + } catch (e) { + console.error(`Error while running the collection:`, e); } + this.emitter.emit(ERunnerEvents.Folder, { + id: folder.__ref.id + }); } - exposeOnlyOn() { - return { - on: (evt, fn) => { - this.emitter.on(evt, fn); - return this.exposeOnlyOn(); + async runRootRequests() { + const { collection } = this.collection; + const requestIds = collection.__meta.rOrders || []; + if (!requestIds.length) + return; + this.emitter.emit(ERunnerEvents.BeforeFolder, { + name: "./", + id: collection.__ref.id + }); + try { + for (let i = 0; i < requestIds.length; i++) { + const res = await this.runRequest(requestIds[i]); + this.testResults.push(res); } - }; + } catch (e) { + console.error(`Error while running the collection:`, e); + } + this.emitter.emit(ERunnerEvents.Folder, { + id: collection.__ref.id + }); } - run() { + async runIteration() { try { - this.validate(); + const folderSet = this.folderRunSequence.values(); + const next = async () => { + const { value: folderId, done } = folderSet.next(); + if (!done) { + await this.runFolder(folderId); + await next(); + } + }; + await next(); + await this.runRootRequests(); } catch (e) { - throw e; + console.error(`Error while running the collection:`, e); } - this.prepareRequestExecutionOrder(); + } + run() { setTimeout(async () => { const { collection } = this.collection; - this.emitter.emit("start" /* Start */, { + this.runStatistics.timings.started = (/* @__PURE__ */ new Date()).valueOf(); + this.emitter.emit(ERunnerEvents.Start, { name: collection.name, id: collection.__ref.id }); - await this.start(); - this.emitter.emit("done" /* Done */); + for (let i = 0; i < this.options.iterationCount; i++) { + this.emitter.emit(ERunnerEvents.BeforeIteration, { + current: i + 1, + total: this.options.iterationCount + }); + await this.runIteration(); + this.emitter.emit(ERunnerEvents.Iteration, { + current: i + 1, + total: this.options.iterationCount + }); + this.runStatistics.stats.iterations.total += 1; + } + this.runStatistics.timings.runDuration = (/* @__PURE__ */ new Date()).valueOf() - this.runStatistics.timings.started; + this.emitter.emit(ERunnerEvents.Done, this.runStatistics); }); return this.exposeOnlyOn(); } + exposeOnlyOn() { + return { + on: (evt, fn) => { + this.emitter.on(evt, fn); + return this.exposeOnlyOn(); + } + }; + } } const fetchRequestPath = (folders, request) => { const requestPath = []; @@ -156,7 +243,7 @@ const fetchRequestPath = (folders, request) => { } return `./${requestPath.join("/")}`; }; +export * from "./types.js"; export { - ERunnerEvents, Runner as default }; diff --git a/packages/firecamp-collection-runner/dist/types.cjs b/packages/firecamp-collection-runner/dist/types.cjs new file mode 100644 index 000000000..e46c40e1d --- /dev/null +++ b/packages/firecamp-collection-runner/dist/types.cjs @@ -0,0 +1,15 @@ +"use strict";Object.defineProperty(exports, "__esModule", {value: true});require('./chunk-3OSFNS7Z.cjs'); +var ERunnerEvents = /* @__PURE__ */ ((ERunnerEvents2) => { + ERunnerEvents2["Start"] = "start"; + ERunnerEvents2["BeforeRequest"] = "beforeRequest"; + ERunnerEvents2["Request"] = "request"; + ERunnerEvents2["BeforeFolder"] = "beforeFolder"; + ERunnerEvents2["Folder"] = "folder"; + ERunnerEvents2["BeforeIteration"] = "beforeIteration"; + ERunnerEvents2["Iteration"] = "iteration"; + ERunnerEvents2["Done"] = "done"; + return ERunnerEvents2; +})(ERunnerEvents || {}); + + +exports.ERunnerEvents = ERunnerEvents; diff --git a/packages/firecamp-collection-runner/dist/types.d.ts b/packages/firecamp-collection-runner/dist/types.d.ts new file mode 100644 index 000000000..cb947a9db --- /dev/null +++ b/packages/firecamp-collection-runner/dist/types.d.ts @@ -0,0 +1,54 @@ +import { TId, TVariable } from '@firecamp/types'; + +declare enum ERunnerEvents { + Start = "start", + BeforeRequest = "beforeRequest", + Request = "request", + BeforeFolder = "beforeFolder", + Folder = "folder", + BeforeIteration = "beforeIteration", + Iteration = "iteration", + Done = "done" +} +interface IRunnerOptions { + getExecutor: () => any; + environment?: TId | TVariable[]; + globals?: TId | TVariable[]; + iterationCount?: number; + iterationData?: string; + delayRequest?: number; + timeout?: number; + timeoutRequest?: number; +} +type TStats = { + total: number; + failed: number; +}; +interface IRunStatistics { + /** stats contains the meta information of every components of run, every component will have 'total' and 'failed' count refs */ + stats: { + iterations: TStats; + requests: TStats; + tests: TStats; + }; + timings: { + /** run start time */ + started: number; + /** total time of the run */ + runDuration: number; + /** average response time of the run */ + responseAvg: number; + /** minimum response time of the run */ + responseMin: number; + /** maximum response time of the run */ + responseMax: number; + /** total response time of all requests */ + responseTotal: number; + }; + transfers: { + /** total response data received in run */ + responseTotal: 0; + }; +} + +export { ERunnerEvents, IRunStatistics, IRunnerOptions }; diff --git a/packages/firecamp-collection-runner/dist/types.js b/packages/firecamp-collection-runner/dist/types.js new file mode 100644 index 000000000..186fd9f4d --- /dev/null +++ b/packages/firecamp-collection-runner/dist/types.js @@ -0,0 +1,15 @@ +import "./chunk-XXPGZHWZ.js"; +var ERunnerEvents = /* @__PURE__ */ ((ERunnerEvents2) => { + ERunnerEvents2["Start"] = "start"; + ERunnerEvents2["BeforeRequest"] = "beforeRequest"; + ERunnerEvents2["Request"] = "request"; + ERunnerEvents2["BeforeFolder"] = "beforeFolder"; + ERunnerEvents2["Folder"] = "folder"; + ERunnerEvents2["BeforeIteration"] = "beforeIteration"; + ERunnerEvents2["Iteration"] = "iteration"; + ERunnerEvents2["Done"] = "done"; + return ERunnerEvents2; +})(ERunnerEvents || {}); +export { + ERunnerEvents +}; diff --git a/packages/firecamp-collection-runner/src/index.ts b/packages/firecamp-collection-runner/src/index.ts index 6865f021a..64da7ae42 100644 --- a/packages/firecamp-collection-runner/src/index.ts +++ b/packages/firecamp-collection-runner/src/index.ts @@ -1,5 +1,6 @@ import EventEmitter from 'eventemitter3'; import { TId } from "@firecamp/types"; +import { ERunnerEvents, IRunStatistics, IRunnerOptions } from './types.js'; const delay = async (ts: number): Promise => { return new Promise((rs) => { @@ -9,29 +10,58 @@ const delay = async (ts: number): Promise => { }) } -export enum ERunnerEvents { - Start = 'start', - BeforeRequest = 'beforeRequest', - Request = 'request', - Done = 'done' -} - export default class Runner { private collection: any; - private options: any; - private requestOrdersForExecution: Set; - private executedRequestQueue: Set; - private currentRequestInExecution: TId; + private options: IRunnerOptions; + private folderRunSequence: Set; private testResults: any = []; private emitter: EventEmitter; - constructor(collection, options) { + private runStatistics: IRunStatistics = { + stats: { + iterations: { failed: 0, total: 0 }, + requests: { failed: 0, total: 0 }, + tests: { failed: 0, total: 0 }, + }, + timings: { + started: 0, + runDuration: 0, + responseMin: 0, + responseMax: 0, + responseAvg: 0, + responseTotal: 0, + }, + transfers: { + responseTotal: 0 + } + } + + constructor(collection, options: IRunnerOptions) { this.collection = collection; this.options = options; - this.requestOrdersForExecution = new Set(); - this.executedRequestQueue = new Set(); - this.currentRequestInExecution = ''; + this.folderRunSequence = new Set(); this.emitter = new EventEmitter(); + this.validate(); + this.assignDefaultOptions(); + this.prepareFolderRunSequence(); + } + + private assignDefaultOptions() { + if (!this.options.hasOwnProperty('iterationCount')) this.options.iterationCount = 1; + if (!this.options.hasOwnProperty('delayRequest')) this.options.delayRequest = 0; + if (!this.options.hasOwnProperty('timeout')) this.options.timeout = 0; + if (!this.options.hasOwnProperty('timeoutRequest')) this.options.timeoutRequest = 0; + + + if (typeof this.options.iterationCount != 'number') + throw new Error('--iteration-count is invalid', { cause: 'invalidOption' }) + if (typeof this.options.delayRequest != 'number') + throw new Error('--delay-request is invalid', { cause: 'invalidOption' }) + if (typeof this.options.timeout != 'number') + throw new Error('--timeout is invalid', { cause: 'invalidOption' }) + if (typeof this.options.timeoutRequest != 'number') + throw new Error('--timeout-request is invalid', { cause: 'invalidOption' }) + } /** @@ -50,39 +80,50 @@ export default class Runner { return true; } - /** - * prepare an Set of request execution order - */ - private prepareRequestExecutionOrder() { + private prepareFolderRunSequence() { const { collection, folders } = this.collection - const { __meta: { fOrders: rootFolderIds = [], rOrders: rootRequestIds = [] } } = collection - - const extractRequestIdsFromFolder = (fId: TId, requestIds: TId[] = []) => { - const folder = folders.find(f => f.__ref.id == fId); - if (!folder) return requestIds; - if (folder.__meta.fOrders?.length) { - const rIds = folder.__meta.fOrders.map(fId => extractRequestIdsFromFolder(fId, requestIds)) - requestIds = [...requestIds, ...rIds] - } - if (folder.__meta.rOrders?.length) { - requestIds = [...requestIds, ...folder.__meta.rOrders] - } - return requestIds; - } + const folderMap = new Map(folders.map(folder => [folder.__ref.id, folder])); + const traverseFolders = (order) => + order.flatMap(folderId => + folderMap.has(folderId) + //@ts-ignore + ? [folderId, ...traverseFolders([folderMap.get(folderId).__ref.folderId])] + : [] + ); + const ids = traverseFolders(collection.__meta.fOrders); + ids.forEach(this.folderRunSequence.add, this.folderRunSequence); + } - if (Array.isArray(rootFolderIds)) { - rootFolderIds.map(fId => { - const requestIds = extractRequestIdsFromFolder(fId) - // console.log(requestIds, fId) - requestIds.forEach(this.requestOrdersForExecution.add, this.requestOrdersForExecution); - }); - } - if (Array.isArray(rootRequestIds)) { - rootRequestIds.forEach(this.requestOrdersForExecution.add, this.requestOrdersForExecution); + private updateResponseStatistics(response: any) { + const { + testResult, + response: { code, status, responseSize, responseTime } + } = response; + + if (Number.isInteger(testResult.total)) this.runStatistics.stats.tests.total += testResult.total; + // if (Number.isInteger(passed)) this.runStatistics.stats.tests.pass += passed; + if (Number.isInteger(testResult.failed)) this.runStatistics.stats.tests.failed += testResult.failed; + if (Number.isInteger(responseSize)) this.runStatistics.transfers.responseTotal += responseSize + + if (Number.isInteger(responseTime)) { + const { + stats: { requests }, + timings: { responseMin, responseMax } + } = this.runStatistics + if (responseMin == 0) this.runStatistics.timings.responseMin = responseTime; + else if (responseTime < responseMin) this.runStatistics.timings.responseMin = responseTime; + + if (responseMax == 0) this.runStatistics.timings.responseMax = responseTime; + else if (responseTime > responseMax) this.runStatistics.timings.responseMax = responseTime; + + this.runStatistics.timings.responseTotal += responseTime; + this.runStatistics.timings.responseAvg = ( + this.runStatistics.timings.responseTotal / requests.total + ) } } - private async executeRequest(requestId: TId) { + private async runRequest(requestId: TId) { const { folders, requests } = this.collection; const request = requests.find(r => r.__ref.id == requestId); @@ -95,75 +136,140 @@ export default class Runner { id: request.__ref.id }); - await delay(500); - const response = await this.options.executeRequest(request); + await delay(this.options.delayRequest); + const { globals, environment } = this.options; + const executor = this.options.getExecutor() + const response = await executor.send(request, { collectionVariables: [], environment, globals });; + + this.updateResponseStatistics(response); /** emit 'request' event on request execution completion */ this.emitter.emit(ERunnerEvents.Request, { id: request.__ref.id, response }); - + this.runStatistics.stats.requests.total += 1 return { request, response }; } - i = 0; - private async start() { + private async runFolder(folderId: TId) { + const folder = this.collection.folders.find(f => f.__ref.id == folderId); + const requestIds = folder.__meta.rOrders || []; + if (!requestIds.length) return; + + /** emit 'beforeFolder' event just before folder execution start */ + this.emitter.emit(ERunnerEvents.BeforeFolder, { + name: folder.name, + id: folder.__ref.id + }); try { - const { value: requestId, done } = this.requestOrdersForExecution.values().next(); - // if (this.i > 0) return - this.i = this.i + 1 - if (!done) { - this.currentRequestInExecution = requestId; - const res = await this.executeRequest(requestId); + for (let i = 0; i < requestIds.length; i++) { + const res = await this.runRequest(requestIds[i]); this.testResults.push(res); - this.executedRequestQueue.add(requestId); - this.requestOrdersForExecution.delete(requestId); - await this.start(); } + } + catch (e) { + console.error(`Error while running the collection:`, e); + // await this.runIteration(); // Retry fetching info for the remaining IDs even if an error occurred + } + + /** emit 'folder' event on folder run completion */ + this.emitter.emit(ERunnerEvents.Folder, { + id: folder.__ref.id + }); + } + private async runRootRequests() { + + const { collection } = this.collection; + const requestIds = collection.__meta.rOrders || []; + if (!requestIds.length) return; + this.emitter.emit(ERunnerEvents.BeforeFolder, { + name: './', + id: collection.__ref.id + }); + + try { + for (let i = 0; i < requestIds.length; i++) { + const res = await this.runRequest(requestIds[i]); + this.testResults.push(res); + } } - catch (error) { - console.error(`Error while running the collection:`, error); - // await this.start(); // Retry fetching info for the remaining IDs even if an error occurred + catch (e) { + console.error(`Error while running the collection:`, e); + // await this.runIteration(); // Retry fetching info for the remaining IDs even if an error occurred } + this.emitter.emit(ERunnerEvents.Folder, { + id: collection.__ref.id + }); } - private exposeOnlyOn() { - return { - on: (evt: string, fn: (...a) => void) => { - this.emitter.on(evt, fn) - return this.exposeOnlyOn() + private async runIteration() { + + try { + const folderSet = this.folderRunSequence.values(); + const next = async () => { + const { value: folderId, done } = folderSet.next(); + if (!done) { + await this.runFolder(folderId); + await next(); + } } + await next(); + await this.runRootRequests(); + } + catch (e) { + console.error(`Error while running the collection:`, e); } } run() { - try { this.validate() } catch (e) { throw e } - this.prepareRequestExecutionOrder(); - setTimeout(async () => { const { collection } = this.collection; - + this.runStatistics.timings.started = new Date().valueOf(); /** emit 'start' event on runner start */ this.emitter.emit(ERunnerEvents.Start, { name: collection.name, id: collection.__ref.id }); - await this.start(); + for (let i = 0; i < this.options.iterationCount; i++) { + /** emit 'beforeIteration' event just before iteration start */ + this.emitter.emit(ERunnerEvents.BeforeIteration, { + current: i + 1, + total: this.options.iterationCount + }); + + await this.runIteration(); + + /** emit 'iteration' event just after the iteration complete */ + this.emitter.emit(ERunnerEvents.Iteration, { + current: i + 1, + total: this.options.iterationCount + }); + this.runStatistics.stats.iterations.total += 1; + } /** emit 'done' event once runner iterations are completed */ - this.emitter.emit(ERunnerEvents.Done); + this.runStatistics.timings.runDuration = new Date().valueOf() - this.runStatistics.timings.started; + this.emitter.emit(ERunnerEvents.Done, this.runStatistics); }); - // return this.testResults; return this.exposeOnlyOn() } + + private exposeOnlyOn() { + return { + on: (evt: string, fn: (...a) => void) => { + this.emitter.on(evt, fn) + return this.exposeOnlyOn() + } + } + } } @@ -177,4 +283,6 @@ const fetchRequestPath = (folders, request) => { currentFolder = folders.find(folder => folder.__ref.id === parentFolderId); } return `./${requestPath.join('/')}`; -} \ No newline at end of file +} + +export * from './types.js' \ No newline at end of file diff --git a/packages/firecamp-collection-runner/src/types.ts b/packages/firecamp-collection-runner/src/types.ts new file mode 100644 index 000000000..9691322bd --- /dev/null +++ b/packages/firecamp-collection-runner/src/types.ts @@ -0,0 +1,87 @@ +import { TId, TVariable } from "@firecamp/types"; + +export enum ERunnerEvents { + Start = 'start', + BeforeRequest = 'beforeRequest', + Request = 'request', + BeforeFolder = 'beforeFolder', + Folder = 'folder', + BeforeIteration = 'beforeIteration', + Iteration = 'iteration', + Done = 'done' +} + +export interface IRunnerOptions { + getExecutor: () => any; + environment?: TId | TVariable[]; + globals?: TId | TVariable[]; + iterationCount?: number; + iterationData?: string; + delayRequest?: number; + timeout?: number; + timeoutRequest?: number; +} + +type TStats = { + total: number, + failed: number +} +export interface IRunStatistics { + /** stats contains the meta information of every components of run, every component will have 'total' and 'failed' count refs */ + stats: { + iterations: TStats, + requests: TStats, + tests: TStats + }, + timings: { + + /** run start time */ + started: number; + + /** total time of the run */ + runDuration: number, + + /** average response time of the run */ + responseAvg: number, + + /** minimum response time of the run */ + responseMin: number, + + /** maximum response time of the run */ + responseMax: number, + + /** total response time of all requests */ + responseTotal: number; + + /** standard deviation of response time of the run */ + // responseSd: number, + + /** average DNS lookup time of the run */ + // dnsAverage: number, + + /** minimum DNS lookup time of the run */ + // dnsMin: number, + + /** maximum DNS lookup time of the run */ + // dnsMax: number, + + /** standard deviation of DNS lookup time of the run */ + // dnsSd: number, + + /** average first byte time of the run */ + // firstByteAverage: number, + + /** minimum first byte time of the run */ + // firstByteMin: number, + + /** maximum first byte time of the run */ + // firstByteMax: number, + + /** standard deviation of first byte time of the run */ + // firstByteSd: 0 + }, + transfers: { + /** total response data received in run */ + responseTotal: 0 + }, +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9e64ff61d..5bb533794 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,9 +23,6 @@ importers: '@oclif/plugin-plugins': specifier: ^2.4.7 version: 2.4.7(@types/node@16.18.35)(typescript@4.9.5) - boxen: - specifier: ^7.1.0 - version: 7.1.0 cli-table3: specifier: ^0.6.3 version: 0.6.3 @@ -47,6 +44,12 @@ importers: ora: specifier: ^6.3.1 version: 6.3.1 + pretty-bytes: + specifier: ^6.1.0 + version: 6.1.0 + pretty-ms: + specifier: ^8.0.0 + version: 8.0.0 react-fast-compare: specifier: ^3.2.2 version: 3.2.2 @@ -3605,12 +3608,6 @@ packages: require-from-string: 2.0.2 uri-js: 4.4.1 - /ansi-align@3.0.1: - resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} - dependencies: - string-width: 4.2.3 - dev: false - /ansi-colors@4.1.1: resolution: {integrity: sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==} engines: {node: '>=6'} @@ -3674,6 +3671,7 @@ packages: /ansi-styles@6.2.1: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} engines: {node: '>=12'} + dev: true /ansicolors@0.3.2: resolution: {integrity: sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg==} @@ -4257,11 +4255,6 @@ packages: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} - /camelcase@7.0.1: - resolution: {integrity: sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==} - engines: {node: '>=14.16'} - dev: false - /caniuse-lite@1.0.30001498: resolution: {integrity: sha512-LFInN2zAwx3ANrGCDZ5AKKJroHqNKyjXitdV5zRIVIaQlXKj3GmxUKagoKsjqUfckpAObPCEWnk5EeMlyMWcgw==} dev: true @@ -4433,11 +4426,6 @@ packages: engines: {node: '>=0.10.0'} dev: true - /cli-boxes@3.0.0: - resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} - engines: {node: '>=10'} - dev: false - /cli-cursor@3.1.0: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} engines: {node: '>=8'} @@ -5361,6 +5349,7 @@ packages: /eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + dev: true /ee-first@1.1.1: resolution: {integrity: sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=} @@ -5386,6 +5375,7 @@ packages: /emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + dev: true /encodeurl@1.0.2: resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} @@ -10014,6 +10004,11 @@ packages: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} + /parse-ms@3.0.0: + resolution: {integrity: sha512-Tpb8Z7r7XbbtBTrM9UhpkzzaMrqA2VXMT3YChzYltwV3P3pM6t8wl7TvpMnSTosz1aQAdVib7kdoys7vYOPerw==} + engines: {node: '>=12'} + dev: false + /password-prompt@1.1.2: resolution: {integrity: sha512-bpuBhROdrhuN3E7G/koAju0WjVw9/uQOG5Co5mokNj0MiOSBVZS1JTwM4zl55hu0WFmIEFvO9cU9sJQiBIYeIA==} dependencies: @@ -10168,6 +10163,11 @@ packages: engines: {node: '>=6'} dev: true + /pretty-bytes@6.1.0: + resolution: {integrity: sha512-Rk753HI8f4uivXi4ZCIYdhmG1V+WKzvRMg/X+M42a6t7D07RcmopXJMDNk6N++7Bl75URRGsb40ruvg7Hcp2wQ==} + engines: {node: ^14.13.1 || >=16.0.0} + dev: false + /pretty-format@28.1.3: resolution: {integrity: sha512-8gFb/To0OmxHR9+ZTb14Df2vNxdGCX8g1xWGUTqUw5TiZvcQf5sHKObd5UcPyLLyowNwDAMTF3XWOG1B6mxl1Q==} engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} @@ -10187,6 +10187,13 @@ packages: react-is: 18.2.0 dev: true + /pretty-ms@8.0.0: + resolution: {integrity: sha512-ASJqOugUF1bbzI35STMBUpZqdfYKlJugy6JBziGi2EE+AL5JPJGSzvpeVXojxrr0ViUYoToUjb5kjSEGf7Y83Q==} + engines: {node: '>=14.16'} + dependencies: + parse-ms: 3.0.0 + dev: false + /proc-log@1.0.0: resolution: {integrity: sha512-aCk8AO51s+4JyuYGg3Q/a6gnrlDO09NpVWePtjp7xwphcoQ04x5WAfCyugcsbLooWcMJ87CLkD4+604IckEdhg==} dev: true @@ -11246,6 +11253,7 @@ packages: eastasianwidth: 0.2.0 emoji-regex: 9.2.2 strip-ansi: 7.1.0 + dev: true /string.prototype.trim@1.2.7: resolution: {integrity: sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==} @@ -12005,7 +12013,7 @@ packages: resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} engines: {node: '>=8'} dev: true - + /type-fest@2.19.0: resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} engines: {node: '>=12.20'} @@ -12491,7 +12499,7 @@ packages: engines: {node: '>=8'} dependencies: string-width: 4.2.3 - + /widest-line@4.0.1: resolution: {integrity: sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==} engines: {node: '>=12'} @@ -12505,7 +12513,7 @@ packages: dependencies: execa: 4.1.0 dev: true - + /word-wrap@1.2.3: resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==} engines: {node: '>=0.10.0'} @@ -12554,6 +12562,7 @@ packages: ansi-styles: 6.2.1 string-width: 5.1.2 strip-ansi: 7.1.0 + dev: true /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}