flow-coverage-report
Version:
Generate an HTML report of the flow coverage data
659 lines (524 loc) • 27.4 kB
JavaScript
;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.escapeFileName = escapeFileName;
exports.roundNumber = roundNumber;
exports.getCoveredPercent = getCoveredPercent;
exports.isFlowAnnotation = isFlowAnnotation;
exports.checkFlowStatus = checkFlowStatus;
exports.collectFlowCoverageForFile = collectFlowCoverageForFile;
exports.summarizeAnnotations = summarizeAnnotations;
exports.collectFlowCoverage = collectFlowCoverage;
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
var _minimatch = _interopRequireDefault(require("minimatch"));
var _temp = _interopRequireDefault(require("temp"));
var _flowAnnotationCheck = require("flow-annotation-check");
var _promisified = require("./promisified");
function _createForOfIteratorHelper(o, allowArrayLike) { var it; if (typeof Symbol === "undefined" || o[Symbol.iterator] == null) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = o[Symbol.iterator](); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; }
function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; }
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { (0, _defineProperty2["default"])(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }
// Load the Array.prototype.find polyfill if needed (e.g. nodejs 0.12).
/* istanbul ignore if */
if (!Array.prototype.find) {
require('array.prototype.find').shim();
} // Escape special characters in file names.
function escapeFileName(fileName) {
return fileName.replace(/(["\s'$`\\])/g, '\\$1');
}
function roundNumber(n) {
var numberDecimals = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
if (numberDecimals > 0) {
var fact = Math.pow(10, Math.floor(numberDecimals));
return Math.round(n * fact) / fact;
}
return Math.floor(n);
}
/* eslint-disable camelcase */
function getCoveredPercent(_ref) {
var covered_count = _ref.covered_count,
uncovered_count = _ref.uncovered_count;
var numberDecimals = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
var total = covered_count + uncovered_count;
if (total === 0) {
return 100;
}
return roundNumber(covered_count / total * 100, numberDecimals);
}
/* eslint-enable camelcase */
function isFlowAnnotation(annotation) {
var strict = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
var validFlowAnnotation = new Set(['flow', 'flow weak', 'flow strict', 'flow strict-local']);
if (strict) {
validFlowAnnotation["delete"]('flow weak');
}
return validFlowAnnotation.has(annotation);
} // Definitions and flow types related to checkFlowStatus.
function checkFlowStatus(_x, _x2, _x3) {
return _checkFlowStatus.apply(this, arguments);
} // Definitions and flow types related to collectFlowCoverageForFile.
function _checkFlowStatus() {
_checkFlowStatus = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee(flowCommandPath, projectDir, temporaryDirPath) {
var temporaryFilePath, result, statusData, unexpectedException;
return _regenerator["default"].wrap(function _callee$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
if (process.env.VERBOSE && process.env.VERBOSE === 'DUMP_JSON') {
temporaryFilePath = _temp["default"].path(temporaryDirPath ? {
suffix: '.json',
dir: temporaryDirPath
} : {
suffix: '.json'
});
}
_context.next = 3;
return (0, _promisified.exec)("".concat(flowCommandPath, " status --json"), {
cwd: projectDir,
maxBuffer: Infinity
}, {
dontReject: true
});
case 3:
result = _context.sent;
if (!(result.err && result.err.code !== 2)) {
_context.next = 7;
break;
}
if (process.env.VERBOSE) {
console.error('Flow status error', result.err, result.stderr, result.stdout);
}
throw result.err;
case 7:
if (!temporaryFilePath) {
_context.next = 11;
break;
}
_context.next = 10;
return (0, _promisified.writeFile)(temporaryFilePath, result.stdout || '');
case 10:
console.log('Flow status result saved to', temporaryFilePath);
case 11:
_context.prev = 11;
statusData = JSON.parse(String(result.stdout));
_context.next = 20;
break;
case 15:
_context.prev = 15;
_context.t0 = _context["catch"](11);
unexpectedException = _context.t0; // Verify the integrity of the format of the JSON status result.
if (!unexpectedException) {
_context.next = 20;
break;
}
throw new Error("Parsing error on Flow status JSON result: ".concat(_context.t0));
case 20:
if (!(statusData && statusData.flowVersion)) {
_context.next = 22;
break;
}
return _context.abrupt("return", statusData);
case 22:
throw new Error('Invalid Flow status JSON format');
case 23:
case "end":
return _context.stop();
}
}
}, _callee, null, [[11, 15]]);
}));
return _checkFlowStatus.apply(this, arguments);
}
function collectFlowCoverageForFile(_x4, _x5, _x6) {
return _collectFlowCoverageForFile.apply(this, arguments);
} // Definition and flow types related to collectForCoverage.
function _collectFlowCoverageForFile() {
_collectFlowCoverageForFile = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee2(options, filename, temporaryDirPath) {
var flowCommandPath, flowCommandTimeout, projectDir, strictCoverage, temporaryFilePath, emptyCoverageData, result, parsedData, flowCoverageParsingError;
return _regenerator["default"].wrap(function _callee2$(_context2) {
while (1) {
switch (_context2.prev = _context2.next) {
case 0:
flowCommandPath = options.flowCommandPath, flowCommandTimeout = options.flowCommandTimeout, projectDir = options.projectDir, strictCoverage = options.strictCoverage;
if (process.env.VERBOSE && process.env.VERBOSE === 'DUMP_JSON') {
temporaryFilePath = _temp["default"].path(temporaryDirPath ? {
suffix: '.json',
dir: temporaryDirPath
} : {
suffix: '.json'
});
}
emptyCoverageData = {
filename: filename,
annotation: 'no flow',
isFlow: false,
expressions: {
/* eslint-disable camelcase */
covered_count: 0,
uncovered_count: 0,
uncovered_locs: []
/* eslint-enable */
}
};
if (process.env.VERBOSE) {
console.log("Collecting coverage data from ".concat(filename, " (timeouts in ").concat(flowCommandTimeout, ")..."));
}
_context2.next = 6;
return (0, _promisified.exec)("".concat(flowCommandPath, " coverage --json ").concat(escapeFileName(filename)), // NOTE: set a default timeouts and maxButter to Infinity to prevent,
// misconfigured projects and source files that should raises errors
// or hangs the flow daemon to prevent the coverage reporter to complete
// the data collection. (See https://github.com/rpl/flow-coverage-report/pull/4
// and https://github.com/rpl/flow-coverage-report/pull/5 for rationale,
// thanks to to @mynameiswhm and @ryan953 for their help on hunting down this issue)
{
cwd: projectDir,
timeout: flowCommandTimeout,
maxBuffer: Infinity
}, {
dontReject: true
});
case 6:
result = _context2.sent;
if (!result.err) {
_context2.next = 14;
break;
}
console.error('ERROR Collecting coverage data from', filename, result.err, result.stderr);
if (!process.env.VERBOSE) {
_context2.next = 13;
break;
}
if (!temporaryFilePath) {
_context2.next = 13;
break;
}
_context2.next = 13;
return (0, _promisified.writeFile)(temporaryFilePath, result.stdout || '');
case 13:
return _context2.abrupt("return", _objectSpread(_objectSpread({}, emptyCoverageData), {}, {
percent: Number.NaN,
isError: true,
flowCoverageError: undefined,
flowCoverageException: result.err && result.err.message,
flowCoverageStderr: result.stderr,
flowCoverageParsingError: undefined
}));
case 14:
if (!process.env.VERBOSE) {
_context2.next = 20;
break;
}
console.log("Collecting coverage data from ".concat(filename, " completed."));
if (!temporaryFilePath) {
_context2.next = 20;
break;
}
_context2.next = 19;
return (0, _promisified.writeFile)(temporaryFilePath, result.stdout || '');
case 19:
console.log("Saved json dump of collected coverage data from ".concat(filename, " to ").concat(temporaryFilePath, "."));
case 20:
if (result.stdout) {
try {
parsedData = JSON.parse(String(result.stdout));
} catch (error) {
flowCoverageParsingError = error.message;
}
}
if (result.stderr) {
try {
parsedData = JSON.parse(String(result.stderr));
delete result.stderr; // Because our old Flow version doesn't like the optional catch:
// eslint-disable-next-line unicorn/prefer-optional-catch-binding
} catch (_) {}
}
if (!(parsedData && !parsedData.error)) {
_context2.next = 30;
break;
}
parsedData.filename = filename;
_context2.next = 26;
return (0, _flowAnnotationCheck.genCheckFlowStatus)(flowCommandPath, filename);
case 26:
parsedData.annotation = _context2.sent;
parsedData.isFlow = isFlowAnnotation(parsedData.annotation, Boolean(strictCoverage)); // In strictCoverage mode all files that are not strictly flow
// (e.g. non annotated and flow weak files) are considered
// as completely uncovered.
if (strictCoverage && !parsedData.isFlow) {
/* eslint-disable camelcase */
parsedData.expressions.uncovered_count += parsedData.expressions.covered_count;
parsedData.expressions.covered_count = 0;
/* eslint-enable */
}
return _context2.abrupt("return", parsedData);
case 30:
return _context2.abrupt("return", _objectSpread(_objectSpread({}, emptyCoverageData), {}, {
percent: Number.NaN,
isError: true,
isFlow: Boolean(parsedData && parsedData.isFlow),
flowCoverageError: parsedData && parsedData.error,
flowCoverageException: undefined,
flowCoverageParsingError: flowCoverageParsingError,
flowCoverageStderr: result.stderr
}));
case 31:
case "end":
return _context2.stop();
}
}
}, _callee2);
}));
return _collectFlowCoverageForFile.apply(this, arguments);
}
function summarizeAnnotations(coverageSummaryData) {
var flowFiles = 0;
var flowWeakFiles = 0;
var noFlowFiles = 0;
var filenames = Object.keys(coverageSummaryData.files);
filenames.forEach(function (filename) {
var annotation = coverageSummaryData.files[filename].annotation;
switch (annotation) {
case 'flow weak':
flowWeakFiles += 1;
break;
case 'no flow':
noFlowFiles += 1;
break;
default:
if (typeof annotation === 'string' && isFlowAnnotation(annotation, true)) {
flowFiles += 1;
return;
}
throw new Error("Unexpected missing flow annotation on ".concat(filename));
}
});
return {
passed: flowWeakFiles + noFlowFiles === 0,
flowFiles: flowFiles,
flowWeakFiles: flowWeakFiles,
noFlowFiles: noFlowFiles,
totalFiles: filenames.length
};
}
function collectFlowCoverage(_x7, _x8) {
return _collectFlowCoverage.apply(this, arguments);
}
function _collectFlowCoverage() {
_collectFlowCoverage = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee5(options, temporaryDirPath) {
var flowCommandPath, projectDir, globIncludePatterns, _options$globExcludeP, globExcludePatterns, threshold, percentDecimals, _options$concurrentFi, concurrentFiles, strictCoverage, excludeNonFlow, flowStatus, now, coverageGeneratedAt, annotationSummary, coverageSummaryData, cleanupUncoveredLoc, waitForCollectedDataFromFiles, drainQueue, _drainQueue, collectCoverageAndGenerateReportForGlob, _collectCoverageAndGenerateReportForGlob;
return _regenerator["default"].wrap(function _callee5$(_context6) {
while (1) {
switch (_context6.prev = _context6.next) {
case 0:
_collectCoverageAndGenerateReportForGlob = function _collectCoverageAndGe2() {
_collectCoverageAndGenerateReportForGlob = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee4(globIncludePattern) {
var files, _iterator, _step, _loop, _ret;
return _regenerator["default"].wrap(function _callee4$(_context5) {
while (1) {
switch (_context5.prev = _context5.next) {
case 0:
_context5.next = 2;
return (0, _promisified.glob)(globIncludePattern, {
cwd: projectDir,
root: projectDir
});
case 2:
files = _context5.sent;
_iterator = _createForOfIteratorHelper(files);
_context5.prev = 4;
_loop = /*#__PURE__*/_regenerator["default"].mark(function _loop() {
var filename, annotation;
return _regenerator["default"].wrap(function _loop$(_context4) {
while (1) {
switch (_context4.prev = _context4.next) {
case 0:
filename = _step.value;
if (!(globExcludePatterns && globExcludePatterns.find(function (pattern) {
return (0, _minimatch["default"])(filename, pattern);
}) !== undefined)) {
_context4.next = 4;
break;
}
if (process.env.VERBOSE) {
console.log("Skip ".concat(filename, ", matched excluded pattern."));
}
return _context4.abrupt("return", "continue");
case 4:
if (!excludeNonFlow) {
_context4.next = 11;
break;
}
_context4.next = 7;
return (0, _flowAnnotationCheck.genCheckFlowStatus)(flowCommandPath, filename);
case 7:
annotation = _context4.sent;
if (!(annotation === 'no flow')) {
_context4.next = 11;
break;
}
if (process.env.VERBOSE) {
console.log("Skip ".concat(filename, ", matched 'no flow' in excludeNonFlow mode."));
}
return _context4.abrupt("return", "continue");
case 11:
if (process.env.VERBOSE) {
console.log("Queue ".concat(filename, " flow coverage data collection"));
} // eslint-disable-next-line promise/prefer-await-to-then
waitForCollectedDataFromFiles.push(collectFlowCoverageForFile(options, filename, temporaryDirPath).then(function (data) {
/* eslint-disable camelcase */
coverageSummaryData.covered_count += data.expressions.covered_count;
coverageSummaryData.uncovered_count += data.expressions.uncovered_count;
data.percent = getCoveredPercent(data.expressions, percentDecimals);
if (!data.filename) {
throw new Error('Unxepected missing filename from collected coverage data');
}
coverageSummaryData.files[data.filename] = data;
data.expressions.uncovered_locs = data.expressions.uncovered_locs.map(cleanupUncoveredLoc);
/* eslint-enable camelcase */
})); // If we have collected at least `concurrentFiles` number of files,
// wait the queue to be drained.
if (!(waitForCollectedDataFromFiles.length >= concurrentFiles)) {
_context4.next = 16;
break;
}
_context4.next = 16;
return drainQueue();
case 16:
case "end":
return _context4.stop();
}
}
}, _loop);
});
_iterator.s();
case 7:
if ((_step = _iterator.n()).done) {
_context5.next = 14;
break;
}
return _context5.delegateYield(_loop(), "t0", 9);
case 9:
_ret = _context5.t0;
if (!(_ret === "continue")) {
_context5.next = 12;
break;
}
return _context5.abrupt("continue", 12);
case 12:
_context5.next = 7;
break;
case 14:
_context5.next = 19;
break;
case 16:
_context5.prev = 16;
_context5.t1 = _context5["catch"](4);
_iterator.e(_context5.t1);
case 19:
_context5.prev = 19;
_iterator.f();
return _context5.finish(19);
case 22:
if (!(waitForCollectedDataFromFiles.length > 0)) {
_context5.next = 25;
break;
}
_context5.next = 25;
return drainQueue();
case 25:
return _context5.abrupt("return", files);
case 26:
case "end":
return _context5.stop();
}
}
}, _callee4, null, [[4, 16, 19, 22]]);
}));
return _collectCoverageAndGenerateReportForGlob.apply(this, arguments);
};
collectCoverageAndGenerateReportForGlob = function _collectCoverageAndGe(_x9) {
return _collectCoverageAndGenerateReportForGlob.apply(this, arguments);
};
_drainQueue = function _drainQueue3() {
_drainQueue = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee3() {
return _regenerator["default"].wrap(function _callee3$(_context3) {
while (1) {
switch (_context3.prev = _context3.next) {
case 0:
if (process.env.VERBOSE) {
console.log("Wait for ".concat(waitForCollectedDataFromFiles.length, " queued files."));
} // Wait the queued files.
_context3.next = 3;
return Promise.all(waitForCollectedDataFromFiles);
case 3:
// Empty the collected Data From files queue.
waitForCollectedDataFromFiles = [];
case 4:
case "end":
return _context3.stop();
}
}
}, _callee3);
}));
return _drainQueue.apply(this, arguments);
};
drainQueue = function _drainQueue2() {
return _drainQueue.apply(this, arguments);
};
cleanupUncoveredLoc = function _cleanupUncoveredLoc(loc) {
delete loc.start.source;
delete loc.end.source;
return loc;
};
flowCommandPath = options.flowCommandPath, projectDir = options.projectDir, globIncludePatterns = options.globIncludePatterns, _options$globExcludeP = options.globExcludePatterns, globExcludePatterns = _options$globExcludeP === void 0 ? [] : _options$globExcludeP, threshold = options.threshold, percentDecimals = options.percentDecimals, _options$concurrentFi = options.concurrentFiles, concurrentFiles = _options$concurrentFi === void 0 ? 1 : _options$concurrentFi, strictCoverage = options.strictCoverage, excludeNonFlow = options.excludeNonFlow;
_context6.next = 8;
return checkFlowStatus(flowCommandPath, projectDir, temporaryDirPath);
case 8:
flowStatus = _context6.sent;
now = new Date();
coverageGeneratedAt = now.toDateString() + ' ' + now.toTimeString();
annotationSummary = {
passed: false,
flowFiles: 0,
flowWeakFiles: 0,
noFlowFiles: 0,
totalFiles: 0
};
coverageSummaryData = {
threshold: threshold,
covered_count: 0,
uncovered_count: 0,
// eslint-disable-line camelcase
percent: 0,
generatedAt: coverageGeneratedAt,
flowStatus: flowStatus,
flowAnnotations: annotationSummary,
files: {},
globIncludePatterns: globIncludePatterns,
globExcludePatterns: globExcludePatterns,
concurrentFiles: concurrentFiles,
strictCoverage: Boolean(strictCoverage),
excludeNonFlow: excludeNonFlow
}; // Remove the source attribute from all ucovered_locs entry.
waitForCollectedDataFromFiles = [];
_context6.next = 16;
return Promise.all(globIncludePatterns.map(collectCoverageAndGenerateReportForGlob));
case 16:
coverageSummaryData.percent = getCoveredPercent(coverageSummaryData, percentDecimals);
coverageSummaryData.flowAnnotations = summarizeAnnotations(coverageSummaryData);
return _context6.abrupt("return", coverageSummaryData);
case 19:
case "end":
return _context6.stop();
}
}
}, _callee5);
}));
return _collectFlowCoverage.apply(this, arguments);
}
//# sourceMappingURL=flow.js.map