khutzpa
Version:
Node powered, cross-platform, drop-in replacement for Chutzpah.exe
210 lines (178 loc) • 8.99 kB
JavaScript
const karma = require("karma");
const Server = karma.Server;
const fs = require("fs");
const nodePath = require("node:path");
const karmaConfigTools = require("./karmaConfigTools");
const utils = require("../helpers/utils");
const urlOpener = require("./urlOpener");
// https://stackoverflow.com/a/52338335/1028230
function copyFolderSync(from, to) {
if (!fs.existsSync(to)) {
fs.mkdirSync(to);
}
fs.readdirSync(from).forEach((element) => {
if (fs.lstatSync(nodePath.join(from, element)).isFile()) {
fs.copyFileSync(nodePath.join(from, element), nodePath.join(to, element));
} else {
copyFolderSync(nodePath.join(from, element), nodePath.join(to, element));
}
});
}
function copyCoverageFiles(coverageDir, newIndexLoc) {
var newCoverageDir = nodePath.dirname(newIndexLoc);
copyFolderSync(coverageDir, newCoverageDir);
var oldIndexLoc = nodePath.join(coverageDir, "index.html");
fs.copyFileSync(oldIndexLoc, newIndexLoc);
}
function startKarmaCoverageRun(overrides, outFile, codeCoverageSuccessPercentage) {
overrides = Object.assign({}, karmaConfigTools.overridesForCoverage, overrides);
var karmaConfig = karmaConfigTools.createKarmaConfig(
overrides,
codeCoverageSuccessPercentage
);
utils.logit(karmaConfig);
return new Promise(function (resolve, reject) {
karma.config
.parseConfig(
null,
karmaConfig,
// In most cases, parseOptions.throwErrors = true should also be set.
// This disables process exiting and allows errors to result in rejected promises.
// http://karma-runner.github.io/6.3/dev/public-api.html
{ promiseConfig: true, throwErrors: true }
)
.then(
(parsedKarmaConfig) => {
// fwiw
// http://karma-runner.github.io/6.4/dev/public-api.html
// I'm not sure why it names the callback function.
const server = new Server(parsedKarmaConfig, function doneCallback(
exitCode
) {
utils.logit("Karma has exited with " + exitCode);
utils.logit(arguments);
var coverageDirName = "coverage";
// TODO: This is a dangerous default once we support other browsers.
var coverageSubDir = "Chrome";
if (parsedKarmaConfig && parsedKarmaConfig.coverageReporter) {
// covereporter can have an array of reporters instead of one.
// https://github.com/karma-runner/karma-coverage/blob/HEAD/docs/configuration.md#reporters
var htmlReporterInfo = parsedKarmaConfig.coverageReporter;
if (Array.isArray(htmlReporterInfo.reporters)) {
htmlReporterInfo =
htmlReporterInfo.reporters.find(
(x) => x.type === "html"
) || htmlReporterInfo;
}
coverageDirName = htmlReporterInfo.dir || coverageDir;
coverageSubDir = htmlReporterInfo.subdir || coverageSubDir;
}
var coverageDir = nodePath.join(
karmaConfig.basePath,
coverageDirName
);
fs.readdir(coverageDir, function (err, list) {
var latestCoverageDir = "";
var latestTime = 0;
utils.logit(list, err);
list.forEach((file) => {
// TODO: Keep in mind that subdir can be configured by a function.
// https://github.com/karma-runner/karma-coverage/blob/master/docs/configuration.md#subdir
// NOTE: coverageSubDir value can be anywhere in file... which allows the default "Chrome" to match
// the wacky default folder name construction like "Chrome 118.0.0.0 (Mac OS 10.15.7)"
// -- TODO: ^^^^ as long as we only use Chrome.
if (file.indexOf(coverageSubDir) > -1) {
var fullPath = nodePath.join(coverageDir, file);
var statsObj = fs.statSync(fullPath);
if (statsObj.isDirectory()) {
utils.logit(`
path: ${fullPath}
last accessed: ${statsObj.atimeMs},
last changed: ${statsObj.ctimeMs},
last modified: ${statsObj.mtimeMs},
`);
}
if (statsObj.ctimeMs > latestTime) {
latestTime = statsObj.ctimeMs;
latestCoverageDir = fullPath;
}
}
});
if (latestCoverageDir) {
utils.logit(latestCoverageDir);
if (outFile) {
copyCoverageFiles(latestCoverageDir, outFile);
// We're going to let you open the file yourself if you used coveragehtml
// mostly because that's what Chutzpah Runner does.
} else {
var coverageFilePath = nodePath.join(
latestCoverageDir,
"index.html"
);
urlOpener.openUrl(coverageFilePath);
}
resolve(exitCode);
} else {
reject(new Error("No coverage directory found!"));
}
});
});
server.start();
},
(rejectReason) => {
/* respond to the rejection reason error */
console.error("Error", rejectReason);
}
);
});
}
function runKarmaCoverage(configInfo, outFile) {
// The config object gives back a collection of all refs (ie, required
// files that aren't tests) in allRefFilePaths and the tests in specFiles.
// karma's files property wants everything... I think...
// So first let's put the two together.
var allFiles = configInfo.allRefFilePaths.concat(configInfo.specFiles);
// Now we need an object literal with each file to cover to tell karma
// to use the coverage preprocessor to, um, preprocess those files.
// We could use wildcard patterns, but this is much more straightforward
// (if not efficient -- though I suspect this means karma is doing less
// lifting and it doesn't matter).
var preprocessObj = {};
configInfo.coverageFiles.forEach((fileToCover) => {
preprocessObj[fileToCover] = ["coverage"];
});
var overrides = {
port: 9876,
basePath: configInfo.jsonFileParent,
// files: [{ pattern: "**/*.js", nocache: true }], // NOTE: nocache true breaks the coverage reporter.
// string-only is equivalent to...
// {pattern: theStringPath, watched: true, served: true, included: true}
files: allFiles,
// everything but the tests.
// "**/!(*test).js": ["coverage"],
preprocessors: preprocessObj,
};
var codeCoverageSuccessPercentage = parseInt(
configInfo.codeCoverageSuccessPercentage,
10
);
utils.logit(
"config overrides for karma (and codeCoverageSuccessPercentage, if exists):",
overrides,
codeCoverageSuccessPercentage
);
return startKarmaCoverageRun(overrides, outFile, codeCoverageSuccessPercentage);
}
if (require.main === module) {
// First two are always "Node" and the path to what was called.
// Trash those.
const myArgs = process.argv.slice(2);
utils.alwaysLog("myArgs: ", myArgs);
var basePath = myArgs[0];
startKarmaCoverageRun(basePath).then(function () {
utils.alwaysLog("done");
});
}
module.exports = {
runKarmaCoverage,
};