khutzpa
Version:
Node powered, cross-platform, drop-in replacement for Chutzpah.exe
297 lines (250 loc) • 11.2 kB
JavaScript
const prompt = require("prompt-sync")({ sigint: true });
const fs = require("fs");
const portscanner = require("portscanner");
const chutzpahConfigReader = require("./services/chutzpahReader");
const specRunner = require("./services/runJasmineSpecs");
const coverageRunner = require("./services/coverage");
const server = require("./services/expressServer");
const wrappedKarma = require("./services/wrappedKarma");
const utils = require("./helpers/utils");
const packageInfo = require("./package.json");
const chutzpahWalk = require("./services/chutzpahWalk");
const { findTheRoot } = require("./helpers/findTheRoot");
const urlOpener = require("./services/urlOpener");
function printUsage() {
console.warn(`
=================================================
khutzpa v${packageInfo.version} usage:
=================================================
khutzpa /path/to/root/directory /{command}
A path *must* be included.
Currently supported commands include:
/openInBrowser
/coverage
/runAllSuites
/runOne
/findAllSuites
/walkAllRunOne
/version
/usage
`);
// If you're showing usage you may have had a bogus command.
// Chances are, if you care about return values, you don't want
// usage shown.
process.exit(846); // "bad"
}
function findPortNotInUseAsync() {
return new Promise((resolve, reject) => {
portscanner.findAPortNotInUse(3000, 3100, "127.0.0.1", function (error, port) {
if (error) {
reject(error);
} else {
resolve(port);
}
});
});
}
// Note that for some actionTypes we'll do a walk to find all the configs
// first, but the default, eg, is to take the startingFilePath and look for
// the [single] closest Chutzpah.json file with no QA in this method for
// that selection.
// Note that this method DOES call chutzpahConfigReader.getConfigInfo to
// get contents for each config [whether they're from a walk or are the closest
// to the file given].
function runCommandAsync(startingFilePath, actionType, args) {
var fnAction = () => {
console.error("no action given: " + actionType);
};
var chutzpahConfigLocs = [startingFilePath];
// most of the time, we'll be running a set of async operations on
// the chutzpah contents of each applicable chutzpah config; thus the default.
// But see below where sometimes we return a static value directly.
var runEachPromise = true;
var staticPayload = {};
switch (actionType) {
case actionTypes.OPEN_IN_BROWSER:
utils.logit("open in browser");
fnAction = function (khutzpaConfigInfo) {
var allFiles = khutzpaConfigInfo.allRefFilePaths.concat(
khutzpaConfigInfo.specFiles
);
var root = findTheRoot(
allFiles.filter((x) => !x.toLowerCase().startsWith("http"))
);
return findPortNotInUseAsync().then(
(expressPort) => {
return specRunner
.createSpecHtml(khutzpaConfigInfo, false, root)
.then(
(results) => {
var serverApp = server.startRunner(
root,
results.runnerHtml
);
serverApp.listen(expressPort, function () {
utils.logit(
`Example app listening on port ${expressPort}!`
);
});
// Yes, strangely random has to be true to use a specific seed value
// (making the order, um, random in a specific way? Using a specific
// "random" seed? It's weird).
var parsedSeed = parseInt(khutzpaConfigInfo.seed, 10);
var querystring = isNaN(parsedSeed)
? `random=${!!khutzpaConfigInfo.random}`
: `random=true&seed=${parsedSeed}`;
var runnerUrl = `http://localhost:${expressPort}/runner?${querystring}`;
urlOpener.openUrl(runnerUrl);
// this prevents the process.exit call.
// TODO: This is an ugly hack. Do better.
return undefined;
},
function (err) {
console.error(err);
}
);
},
(error) => {
console.error("Unable to find an open port", error);
throw "Unable to find an open port";
}
);
};
break;
case actionTypes.WITH_COVERAGE:
utils.debugLog("coverage");
var outFile;
var indexOfPath = args.indexOf("/coveragehtml");
if (indexOfPath !== -1) {
outFile = args[indexOfPath + 1];
}
fnAction = function (configInfo) {
return coverageRunner.runKarmaCoverage(configInfo, outFile);
};
break;
case actionTypes.RUN_ALL_CHUTZPAHS:
utils.debugLog("run all the chutzpahs");
chutzpahConfigLocs = chutzpahWalk.walk(startingFilePath);
fnAction = wrappedKarma.runWrappedKarma;
break;
case actionTypes.FIND_ALL_CHUTZPAHS:
utils.debugLog("FIND all the chutzpahs but don't run them");
staticPayload = chutzpahWalk.walk(startingFilePath);
runEachPromise = false;
break;
case actionTypes.DEFAULT_RUN_ONE_IN_KARMA:
// note that this (and other walk-less actions) uses
// chutzpahConfigLocs = [startingFilePath];
// instead of the results of a walk.
utils.debugLog("Run one in karma");
fnAction = wrappedKarma.runWrappedKarma;
break;
case actionTypes.WALK_ALL_RUN_ONE:
utils.debugLog("walk all run one");
chutzpahConfigLocs = chutzpahWalk.walk(startingFilePath);
if (chutzpahConfigLocs.length) {
var jsonLocsWithIndex = chutzpahConfigLocs.map((x, i) => `${i} -- ${x}`);
console.log(jsonLocsWithIndex.join("\n"));
const whichOne = prompt("Which do you want to run?");
var whichIndex = parseInt(whichOne, 10);
if (
(whichIndex || 0 === whichIndex) &&
whichIndex > -1 &&
whichIndex < chutzpahConfigLocs.length
) {
utils.debugLog("Running config index: " + whichIndex);
chutzpahConfigLocs = [chutzpahConfigLocs[whichIndex]];
fnAction = wrappedKarma.runWrappedKarma;
} else {
console.warn("Invalid index selected.");
process.exit();
}
} else {
console.warn("No Chutzpah files found.");
process.exit();
}
break;
default:
fnAction = printUsage;
}
utils.debugLog("fnAction is set");
if (runEachPromise) {
return Promise.all(
chutzpahConfigLocs.map(function (chutzpahSearchStart) {
return chutzpahConfigReader.getConfigInfo(chutzpahSearchStart).then(
function (configContents) {
return fnAction(configContents, chutzpahSearchStart);
},
function (err) {
console.error(err, chutzpahSearchStart);
return err;
}
);
})
);
}
return Promise.resolve(staticPayload);
}
// eo cmdCallHandler
// Okay, I know, I know. enums are a code smell. mvp v1.
// https://lostechies.com/jimmybogard/2008/08/12/enumeration-classes/
var actionTypes = {
OPEN_IN_BROWSER: 1,
WITH_COVERAGE: 2,
RUN_ALL_CHUTZPAHS: 3,
FIND_ALL_CHUTZPAHS: 4,
WALK_ALL_RUN_ONE: 5,
PRINT_USAGE: 6,
DEFAULT_RUN_ONE_IN_KARMA: 7,
};
if (require.main === module) {
try {
// First two arguments for a node process are always "Node"
// and the path to this app. Trash those.
const myArgs = process.argv.slice(2);
utils.logit("myArgs: ", myArgs);
if (myArgs.indexOf("/version") > -1) {
return console.log(packageInfo.version);
}
var filePath = myArgs.shift();
if (!filePath || !fs.existsSync(filePath)) {
printUsage();
} else {
var command =
myArgs.indexOf("/openInBrowser") > -1
? actionTypes.OPEN_IN_BROWSER
: myArgs.indexOf("/coverage") > -1
? actionTypes.WITH_COVERAGE
: myArgs.indexOf("/findAllSuites") > -1
? actionTypes.FIND_ALL_CHUTZPAHS
: myArgs.indexOf("/runAllSuites") > -1
? actionTypes.RUN_ALL_CHUTZPAHS
: myArgs.indexOf("/walkAllRunOne") > -1
? actionTypes.WALK_ALL_RUN_ONE
: myArgs.indexOf("/version") > -1
? actionTypes.PRINT_USAGE
: actionTypes.DEFAULT_RUN_ONE_IN_KARMA;
utils.logit(command);
runCommandAsync(filePath, command, myArgs).then(function (resultsIfAny) {
utils.debugLog("done");
if (
Array.isArray(resultsIfAny) &&
!resultsIfAny.every((x) => x === undefined)
) {
console.log("\n\n::RESULTS::");
console.log(resultsIfAny);
console.log("::eoRESULTS::\n\n");
const firstError = resultsIfAny.find((x) => x && x !== 0);
// On Windows, to see the returned code (https://stackoverflow.com/a/334893/1028230):
// cmd.exe: echo %ERRORLEVEL%
// pwsh.exe: echo $LastExitCode
process.exit(firstError || 0);
}
});
}
} catch (e) {
console.error("An error occurred:", e);
}
}
module.exports = runCommandAsync;