UNPKG

flow-coverage-report

Version:

Generate an HTML report of the flow coverage data

659 lines (524 loc) 27.4 kB
'use strict'; 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