jasmine
Version:
CLI for Jasmine, a simple JavaScript testing framework for browsers and Node
479 lines (410 loc) • 13.4 kB
JavaScript
const path = require('path');
const fs = require('fs');
const os = require('os');
const unWindows = require('./unWindows');
exports = module.exports = Command;
const subCommands = {
init: {
description: 'initialize jasmine',
action: initJasmine
},
examples: {
description: 'install examples',
action: installExamples
},
help: {
description: 'show help',
action: help,
alias: '-h'
},
version: {
description: 'show jasmine and jasmine-core versions',
action: version,
alias: '-v'
},
enumerate: {
description: 'enumerate suites and specs',
action: enumerate
}
};
function Command(projectBaseDir, examplesDir, deps) {
const {print, platform, terminalColumns, Jasmine, ParallelRunner} = deps;
const isWindows = platform() === 'win32';
this.projectBaseDir = isWindows ? unWindows(projectBaseDir) : projectBaseDir;
this.specDir = `${this.projectBaseDir}/spec`;
const command = this;
this.run = async function(args) {
setEnvironmentVariables(args);
let commandToRun;
Object.keys(subCommands).forEach(function(cmd) {
const commandObject = subCommands[cmd];
if (args.indexOf(cmd) >= 0) {
commandToRun = commandObject;
} else if (commandObject.alias && args.indexOf(commandObject.alias) >= 0) {
commandToRun = commandObject;
}
});
if (commandToRun) {
await commandToRun.action({
Jasmine,
projectBaseDir,
specDir: command.specDir,
examplesDir: examplesDir,
print,
terminalColumns
});
} else {
const options = parseOptions(args, isWindows);
if (options.usageErrors.length > 0) {
process.exitCode = 1;
for (const e of options.usageErrors) {
print(e);
}
print('');
help({print, terminalColumns});
} else {
await runJasmine(Jasmine, ParallelRunner, projectBaseDir, options);
}
}
};
}
function isFileArg(arg) {
return arg.indexOf('--') !== 0 && !isEnvironmentVariable(arg);
}
function parseOptions(argv, isWindows) {
let files = [];
let helpers = [];
let requires = [];
let unknownOptions = [];
let usageErrors = [];
let color = undefined;
let reporter;
let configPath;
let filterRegex;
let filterPath;
let failFast;
let random;
let seed;
let numWorkers = 1;
let verbose = false;
for (const arg of argv) {
if (arg === '--no-color') {
color = false;
} else if (arg === '--color') {
color = true;
} else if (arg.match("^--filter=")) {
filterRegex = arg.match("^--filter=(.*)")[1];
} else if (arg.match("^--filter-path=(.*)")) {
const json = arg.match("^--filter-path=(.*)")[1];
try {
filterPath = JSON.parse(json);
} catch {
usageErrors.push('Invalid filter path: ' + json);
}
if (!(filterPath instanceof Array)) {
usageErrors.push('Invalid filter path: ' + json);
}
} else if (arg.match("^--helper=")) {
helpers.push(arg.match("^--helper=(.*)")[1]);
} else if (arg.match("^--require=")) {
requires.push(arg.match("^--require=(.*)")[1]);
} else if (arg === '--fail-fast') {
failFast = true;
} else if (arg.match("^--random=")) {
random = arg.match("^--random=(.*)")[1] === 'true';
} else if (arg.match("^--seed=")) {
seed = arg.match("^--seed=(.*)")[1];
} else if (arg.match("^--config=")) {
configPath = arg.match("^--config=(.*)")[1];
} else if (arg.match("^--reporter=")) {
reporter = arg.match("^--reporter=(.*)")[1];
} else if (arg.match("^--parallel=(.*)")) {
const w = arg.match("^--parallel=(.*)")[1];
if (w === 'auto') {
// A reasonable default in most situations
numWorkers = os.cpus().length -1;
} else {
numWorkers = parseFloat(w);
if (isNaN(numWorkers) || numWorkers < 2 || numWorkers !== Math.floor(numWorkers)) {
usageErrors.push('Argument to --parallel= must be an integer greater than 1');
}
}
} else if (arg === '--verbose') {
verbose = true;
} else if (arg === '--') {
break;
} else if (isFileArg(arg)) {
files.push(isWindows ? unWindows(arg) : arg);
} else if (!isEnvironmentVariable(arg)) {
unknownOptions.push(arg);
}
}
if (numWorkers > 1 && filterPath) {
usageErrors.push('--filter-path is not supported in parallel mode.');
}
if (unknownOptions.length > 0) {
usageErrors.push('Unknown options: ' + unknownOptions.join(', '));
}
return {
color,
configPath,
filterRegex,
filterPath,
failFast,
helpers,
requires,
reporter,
files,
random,
seed,
numWorkers,
verbose,
usageErrors
};
}
async function runJasmine(Jasmine, ParallelRunner, projectBaseDir, options) {
let runner;
if (options.numWorkers > 1) {
runner = new ParallelRunner({
projectBaseDir,
numWorkers: options.numWorkers
});
} else {
runner = new Jasmine({
projectBaseDir
});
}
runner.verbose(options.verbose);
await runner.loadConfigFile(options.configPath || process.env.JASMINE_CONFIG_PATH);
if (options.failFast !== undefined) {
runner.configureEnv({
stopSpecOnExpectationFailure: options.failFast,
stopOnSpecFailure: options.failFast
});
}
if (options.seed !== undefined) {
runner.seed(options.seed);
}
if (options.random !== undefined) {
runner.randomizeTests(options.random);
}
if (options.helpers !== undefined && options.helpers.length) {
runner.addMatchingHelperFiles(options.helpers);
}
if (options.requires !== undefined && options.requires.length) {
runner.addRequires(options.requires);
}
if (options.reporter !== undefined) {
await registerReporter(options.reporter, runner);
}
runner.showColors(options.color);
let filter;
if (options.filterRegex) {
filter = options.filterRegex;
} else if (options.filterPath) {
filter = {path: options.filterPath};
}
try {
await runner.execute(options.files, filter);
} catch (error) {
console.error(error);
process.exit(1);
}
}
async function registerReporter(reporterModuleName, runner) {
let Reporter;
try {
Reporter = await runner.loader.load(resolveReporter(reporterModuleName));
} catch (e) {
throw new Error('Failed to load reporter module '+ reporterModuleName +
'\nUnderlying error: ' + e.stack + '\n(end underlying error)');
}
let reporter;
try {
reporter = new Reporter();
} catch (e) {
throw new Error('Failed to instantiate reporter from '+ reporterModuleName +
'\nUnderlying error: ' + e.stack + '\n(end underlying error)');
}
runner.clearReporters();
runner.addReporter(reporter);
}
function resolveReporter(nameOrPath) {
if (nameOrPath.startsWith('./') || nameOrPath.startsWith('../')) {
return path.resolve(nameOrPath);
} else {
return nameOrPath;
}
}
function initJasmine(options) {
const print = options.print;
const destDir = path.join(options.specDir, 'support/');
makeDirStructure(destDir);
const destPath = path.join(destDir, 'jasmine.mjs');
if (fs.existsSync(destPath)) {
print('spec/support/jasmine.mjs already exists in your project.');
} else {
const contents = fs.readFileSync(
path.join(__dirname, '../lib/examples/jasmine.mjs'), 'utf-8');
fs.writeFileSync(destPath, contents);
}
}
function installExamples(options) {
const specDir = options.specDir;
const projectBaseDir = options.projectBaseDir;
const examplesDir = options.examplesDir;
makeDirStructure(path.join(specDir, 'support'));
makeDirStructure(path.join(specDir, 'jasmine_examples'));
makeDirStructure(path.join(specDir, 'helpers', 'jasmine_examples'));
makeDirStructure(path.join(projectBaseDir, 'lib', 'jasmine_examples'));
copyFiles(
path.join(examplesDir, 'spec', 'helpers', 'jasmine_examples'),
path.join(specDir, 'helpers', 'jasmine_examples'),
new RegExp(/[Hh]elper\.js/)
);
copyFiles(
path.join(examplesDir, 'lib', 'jasmine_examples'),
path.join(projectBaseDir, 'lib', 'jasmine_examples'),
new RegExp(/\.js/)
);
copyFiles(
path.join(examplesDir, 'spec', 'jasmine_examples'),
path.join(specDir, 'jasmine_examples'),
new RegExp(/[Ss]pec.js/)
);
}
function help(deps) {
const print = deps.print;
let terminalColumns = deps.terminalColumns || 80;
print(wrap(terminalColumns, 'Usage: jasmine [command] [options] [files] [--]'));
print('');
print('Commands:');
Object.keys(subCommands).forEach(function(cmd) {
let commandNameText = cmd;
if(subCommands[cmd].alias) {
commandNameText = commandNameText + ',' + subCommands[cmd].alias;
}
print(wrapWithIndent(terminalColumns, lPad(commandNameText, 10) + ' ', subCommands[cmd].description));
});
print('');
print(wrap(terminalColumns, 'If no command is given, Jasmine specs will be run.'));
print('');
print('');
print('Options:');
const options = [
{ syntax: '--parallel=N', help: 'Run in parallel with N workers' },
{ syntax: '--parallel=auto', help: 'Run in parallel with an automatically chosen number of workers' },
{ syntax: '--no-color', help: 'turn off color in spec output' },
{ syntax: '--color', help: 'force turn on color in spec output' },
{ syntax: '--filter=', help: 'filter specs to run only those that match the given regular expression' },
{ syntax: '--filter-path=', help: 'run only the spec or suite that matches ' +
'the given path, e.g. ' +
'--filter-path=\'["parent suite name","child suite name","spec name"]\'' },
{ syntax: '--helper=', help: 'load helper files that match the given string' },
{ syntax: '--require=', help: 'load module that matches the given string' },
{ syntax: '--fail-fast', help: 'stop Jasmine execution on spec failure' },
{ syntax: '--config=', help: 'path to the Jasmine configuration file' },
{ syntax: '--reporter=', help: 'path to reporter to use instead of the default Jasmine reporter' },
{ syntax: '--verbose', help: 'print information that may be useful for debugging configuration' },
{ syntax: '--', help: 'marker to signal the end of options meant for Jasmine' },
];
for (const o of options) {
print(wrapWithIndent(terminalColumns, lPad(o.syntax, 18) + ' ', o.help));
}
print('');
print(wrap(terminalColumns,
'The given arguments take precedence over options in your jasmine.json.'));
print(wrap(terminalColumns,
'The path to your optional jasmine.json can also be configured by setting the JASMINE_CONFIG_PATH environment variable.'));
}
function wrapWithIndent(cols, prefix, suffix) {
const lines = wrap2(cols - prefix.length, suffix);
const indent = lPad('', prefix.length);
return prefix + lines.join('\n' + indent);
}
function wrap(cols, input) {
return wrap2(cols, input).join('\n');
}
function wrap2(cols, input) {
let lines = [];
let start = 0;
while (start < input.length) {
const splitAt = indexOfLastSpaceInRange(start, start + cols, input);
if (splitAt === -1 || input.length - start <= cols) {
lines.push(input.substring(start));
break;
} else {
lines.push(input.substring(start, splitAt));
start = splitAt + 1;
}
}
return lines;
}
function indexOfLastSpaceInRange(start, end, s) {
for (let i = end; i >= start; i--) {
if (s[i] === ' ') {
return i;
}
}
return -1;
}
function version(options) {
const print = options.print;
print('jasmine v' + require('../package.json').version);
const jasmine = new options.Jasmine();
print('jasmine-core v' + jasmine.coreVersion());
}
async function enumerate(options) {
const runner = new options.Jasmine({projectBaseDir: options.projectBaseDir});
await runner.loadConfigFile(options.configPath || process.env.JASMINE_CONFIG_PATH);
if (options.helpers !== undefined && options.helpers.length) {
runner.addMatchingHelperFiles(options.helpers);
}
if (options.requires !== undefined && options.requires.length) {
runner.addRequires(options.requires);
}
const suites = await runner.enumerate();
options.print(JSON.stringify(suites, null, ' '));
}
function lPad(str, length) {
if (str.length >= length) {
return str;
} else {
return lPad(' ' + str, length);
}
}
function copyFiles(srcDir, destDir, pattern) {
const srcDirFiles = fs.readdirSync(srcDir);
srcDirFiles.forEach(function(file) {
if (file.search(pattern) !== -1) {
fs.writeFileSync(path.join(destDir, file), fs.readFileSync(path.join(srcDir, file)));
}
});
}
function makeDirStructure(absolutePath) {
const splitPath = absolutePath.split(path.sep);
splitPath.forEach(function(dir, index) {
if(index > 1) {
const fullPath = path.join(splitPath.slice(0, index).join('/'), dir);
if (!fs.existsSync(fullPath)) {
fs.mkdirSync(fullPath);
}
}
});
}
function isEnvironmentVariable(arg) {
if (arg.match(/^--/)) {
return false;
}
return arg.match(/(.*)=(.*)/);
}
function setEnvironmentVariables(args) {
args.forEach(function (arg) {
const regExpMatch = isEnvironmentVariable(arg);
if(regExpMatch) {
const key = regExpMatch[1];
const value = regExpMatch[2];
process.env[key] = value;
}
});
}