const path = require('node:path');
const {readFileSync} = require('node:fs');
const mri = require('mri');
const NanoTime = require('./NanoTime.js');
module.exports = RunOptions;
/**
* @private
*/
const MILLI2NANO = BigInt(1000000); // eslint-disable-line no-magic-numbers
/**
* @private
*/
const NUMBER_OF_IGNORED_CLI_ARGS = 2;
/**
* @example
* const RunOptions = require('pete/lib/RunOptions');
* var options = new RunOptions();
*
* @alias module:pete/lib/RunOptions
* @class
*/
function RunOptions () {
/**
* ID of test.
*
* @see {module:pete/lib/createId}
* @type {string}
*/
this.id = '';
/**
* Name of test.
*
* @type {string}
*/
this.name = '';
/**
* Run type to use for the test.
* To use custom runner, pass full path to JS file that exports factory function.
*
* @type {string}
* @default 'auto'
* @see {@link module:pete/lib/runs/auto.TYPES} for supported type names.
*/
this.runType = 'auto';
/**
* Limit total time (in milliseconds) that target function can take when run multiple times.
*
* @type {number}
* @default Infinity
*/
this.limitCPUTime = Infinity;
/**
* Limit total time (in milliseconds) that test run can take.
*
* @type {number}
* @default 3000
*/
this.limitRealTime = 3000; // eslint-disable-line no-magic-numbers
/**
* Limit how many times target function should be called when tested.
*
* @type {number}
* @default Infinity
*/
this.limitSamples = Infinity;
/**
* Same as `limitSamples`, only for "warmup" phase of test run.
* Set to 0 to skip "warmup" phase.
*
* @type {number}
* @default 3000
*/
this.warmupSamples = 3000; // eslint-disable-line no-magic-numbers
/**
* Set to `true` to stop all tests when there is an error.
*
* @type {boolean}
* @default true
*/
this.exitOnError = true;
/**
* Array of arguments to pass to target function.
* In case of `callback` type function, test run will provide one additional argument,
* and pass it as the last one.
*
* @example
* options.args = ['foo', 2]; // eslint-disable-line no-magic-numbers
*
* @type {any[]}
* @default []
*/
this.args = [];
/**
* ID of the only test that should be run in current process.
* Empty string means "no filtering" - all tests will be run.
*
* @type {string|string[]}
* @default ''
*/
this.runOnlyIf = '';
/**
* List of percentiles that should be reported back.
*
* @type {number[]}
* @default [50, 75, 90, 95, 97.5, 99, 99.95, 99.99]
*/
this.percentiles = [50, 75, 90, 95, 97.5, 99, 99.95, 99.99]; // eslint-disable-line no-magic-numbers
/**
* List of reporters that should receive report data.
*
* @example
* options.reporters = ['tap?out=./report/test.tap', 'mico'];
*
* @type {string[]}
* @default []
*/
this.reporters = [];
}
/**
* Parse command line arguments and apply them as options.
*
* @return {module:pete/lib/RunOptions} `this`
*/
RunOptions.prototype.setFromArgv = function setFromArgv () {
const argv = mri(process.argv.slice(NUMBER_OF_IGNORED_CLI_ARGS), {
boolean: 'exitOnError',
// WARNING: Do not specify `default`, to not overwrite stuff from opts!
alias : {
name : ['n'],
runType : ['t', 'type'], // 'auto', 'sync', 'generator', 'callback', 'async', 'fork', 'skip'
limitCPUTime : ['c', 'maxCpuTime'],
limitRealTime: ['r', 'maxRealTime'],
limitSamples : ['s', 'maxSamples'],
warmupSamples: ['w', 'maxWarmup'],
percentiles : ['p', 'percentile'],
reporters : ['o', 'output', 'report'],
exitOnError : ['b', 'bail'],
runOnlyIf : ['j', 'just', 'only'],
args : ['a', 'arg']
}
});
return this.setFromOptions(argv);
};
/**
* Pick properties from target options object if their name matches supported option.
*
* @param {object} options
* @return {module:pete/lib/RunOptions} `this`
*/
RunOptions.prototype.setFromOptions = function setFromOptions (options = {}) {
Object.keys(this).forEach(key => {
this[key] = Reflect.has(options, key) ? options[key] : this[key];
});
return this.sanitize();
};
/**
* Make sure that option values are of correct type, converting them when needed.
*
* return {module:pete/lib/RunOptions} `this`
*/
RunOptions.prototype.sanitize = function sanitize () {
if (typeof this.percentiles === 'string') {
this.percentiles = this.percentiles.split(',').map(Number);
}
else if (typeof this.percentiles === 'number') {
this.percentiles = [this.percentiles];
}
if (typeof this.reporters === 'string') {
this.reporters = [this.reporters];
}
if (Array.isArray(this.name)) {
this.name = this.name.pop();
console.error(`WARNING: only single \`name\` value is supported. Using last one: ${this.name}`);
}
if (Array.isArray(this.runType)) {
this.runType = this.runType.pop();
console.error(`WARNING: only single \`runType\` value is supported. Using last one: ${this.runType}`);
}
if (Array.isArray(this.limitCPUTime)) {
this.limitCPUTime = this.limitCPUTime.pop();
console.error(`WARNING: only single \`limitCPUTime\` value is supported. Using last one: ${this.limitCPUTime}`);
}
if (Array.isArray(this.limitRealTime)) {
this.limitRealTime = this.limitRealTime.pop();
console.error(`WARNING: only single \`limitRealTime\` value is supported. Using last one: ${this.limitRealTime}`);
}
if (Array.isArray(this.limitSamples)) {
this.limitSamples = this.limitSamples.pop();
console.error(`WARNING: only single \`limitSamples\` value is supported. Using last one: ${this.limitSamples}`);
}
if (Array.isArray(this.warmupSamples)) {
this.warmupSamples = this.warmupSamples.pop();
console.error(`WARNING: only single \`warmupSamples\` value is supported. Using last one: ${this.warmupSamples}`);
}
this.name = this.name || '';
this.runType = this.runType || 'auto';
this.limitCPUTime = toNanoseconds(this.limitCPUTime) || Infinity;
this.limitRealTime = toNanoseconds(this.limitRealTime) || Infinity;
this.limitSamples = Number(this.limitSamples) || Infinity;
this.warmupSamples = Number(this.warmupSamples) || Infinity;
return this;
};
/**
* Return `null` if options are valid and test can run.
*
* @return {Error|null}
*/
RunOptions.prototype.areInvalid = function areInvalid () {
var err;
/*
* Used mainly internally, when running tests through fork function,
* but it is ok if user finds correct ID and uses it manually :).
*/
if (this.runOnlyIf) {
if (typeof this.runOnlyIf === 'string' && this.runOnlyIf !== this.id) {
err = new Error('`id` does not match `runOnlyIf`');
err.code = 'ID_MISMATCH';
return err;
}
if (Array.isArray(this.runOnlyIf) && this.runOnlyIf.indexOf(this.id) < 0) {
err = new Error('`id` does not match any of `runOnlyIf`');
err.code = 'ID_MISMATCH';
return err;
}
}
const ONE = BigInt(1);
if (this.limitCPUTime < ONE && this.limitRealTime < ONE) {
err = new Error('One of `limitCPUTime` or `limitRealTime` has to be set to non-zero value');
err.code = 'VALUE_MISSING';
return err;
}
if (this.warmupSamples === Infinity) {
err = new Error('`warmupSamples` cannot be set to Infinity, or warmup phase would never end');
err.code = 'VALUE_INVALID';
return err;
}
if (this.limitCPUTime === Infinity && this.limitRealTime === Infinity && this.limitSamples === Infinity) {
err = new Error('Limits cannot be all set to Infinity at the same time, or test would never end');
err.code = 'VALUE_INVALID';
return err;
}
return null;
};
/**
* Return a "help text", suitable to output in CLI environment.
*
* @return {string}
*/
RunOptions.prototype.helpText = function helpText () {
const name = path.basename(require.main.filename);
const readmePath = path.join(path.dirname(__dirname), 'README.md');
const readme = readFileSync(readmePath, 'utf8');
const readmeCLI = readme.match(/[#]+ Usage \(CLI\)\n(?<usage>[\w\W]+?)\n+(?:[#]+|$)/);
const usage = (readmeCLI && readmeCLI.groups.usage)
|| `Read ${readmePath} for more information.`;
return `Usage: ${name} [-c X] [-r X] [-s X] [-w X] [-p X] [-o X] [-b] file1.js ... fileN.js
${usage}`;
};
/**
* Convert number to BigInt, unless it is Infinity or already a BigInt.
*
* @private
* @param {number|bigint|Infinity} milliseconds
* @return {bigint|Infinity}
*/
function toNanoseconds (milliseconds) {
if (milliseconds === Infinity || typeof milliseconds === 'bigint') {
return milliseconds;
}
else if (milliseconds instanceof NanoTime) {
return milliseconds.value;
}
try {
return BigInt(milliseconds) * MILLI2NANO;
}
catch (e) {
if (!(e instanceof SyntaxError)) {
console.error(e);
}
return NaN;
}
}