From ab1d9fac92522ef674ac945ee870dd1209499111 Mon Sep 17 00:00:00 2001 From: ttmarek Date: Mon, 13 Nov 2017 13:54:26 -0500 Subject: [PATCH] Add combined coverage threshold for directories Add unit test for passing directory coverage Add test for when there is no coverage data available Fix type errors and make code more familiar Run prettier on changed files --- .../__tests__/coverage_reporter.test.js | 74 ++++++- .../src/reporters/coverage_reporter.js | 184 ++++++++++++------ 2 files changed, 198 insertions(+), 60 deletions(-) diff --git a/packages/jest-cli/src/reporters/__tests__/coverage_reporter.test.js b/packages/jest-cli/src/reporters/__tests__/coverage_reporter.test.js index 0962ab57d7cd..ca5bcb6be0f8 100644 --- a/packages/jest-cli/src/reporters/__tests__/coverage_reporter.test.js +++ b/packages/jest-cli/src/reporters/__tests__/coverage_reporter.test.js @@ -112,7 +112,7 @@ describe('onRunComplete', () => { }); }); - it('getLastError() returns an error when threshold is not met for global', () => { + test('getLastError() returns an error when threshold is not met for global', () => { const testReporter = new CoverageReporter( { collectCoverage: true, @@ -134,7 +134,7 @@ describe('onRunComplete', () => { }); }); - it('getLastError() returns an error when threshold is not met for file', () => { + test('getLastError() returns an error when threshold is not met for file', () => { const covThreshold = {}; [ 'global', @@ -164,7 +164,7 @@ describe('onRunComplete', () => { }); }); - it('getLastError() returns `undefined` when threshold is met', () => { + test('getLastError() returns `undefined` when threshold is met', () => { const covThreshold = {}; [ 'global', @@ -194,7 +194,7 @@ describe('onRunComplete', () => { }); }); - it('getLastError() returns an error when threshold is for non-covered file', () => { + test('getLastError() returns an error when threshold is not met for non-covered file', () => { const testReporter = new CoverageReporter( { collectCoverage: true, @@ -215,4 +215,70 @@ describe('onRunComplete', () => { expect(testReporter.getLastError().message.split('\n')).toHaveLength(1); }); }); + + test('getLastError() returns an error when threshold is not met for directory', () => { + const testReporter = new CoverageReporter( + { + collectCoverage: true, + coverageThreshold: { + './path-test-files/glob-path/': { + statements: 100, + }, + }, + }, + { + maxWorkers: 2, + }, + ); + testReporter.log = jest.fn(); + return testReporter + .onRunComplete(new Set(), {}, mockAggResults) + .then(() => { + expect(testReporter.getLastError().message.split('\n')).toHaveLength(1); + }); + }); + + test('getLastError() returns `undefined` when threshold is met for directory', () => { + const testReporter = new CoverageReporter( + { + collectCoverage: true, + coverageThreshold: { + './path-test-files/glob-path/': { + statements: 40, + }, + }, + }, + { + maxWorkers: 2, + }, + ); + testReporter.log = jest.fn(); + return testReporter + .onRunComplete(new Set(), {}, mockAggResults) + .then(() => { + expect(testReporter.getLastError()).toBeUndefined(); + }); + }); + + test('getLastError() returns an error when there is no coverage data for a threshold', () => { + const testReporter = new CoverageReporter( + { + collectCoverage: true, + coverageThreshold: { + './path/doesnt/exist': { + statements: 40, + }, + }, + }, + { + maxWorkers: 2, + }, + ); + testReporter.log = jest.fn(); + return testReporter + .onRunComplete(new Set(), {}, mockAggResults) + .then(() => { + expect(testReporter.getLastError().message.split('\n')).toHaveLength(1); + }); + }); }); diff --git a/packages/jest-cli/src/reporters/coverage_reporter.js b/packages/jest-cli/src/reporters/coverage_reporter.js index b7fd95cf111e..b6e17b626433 100644 --- a/packages/jest-cli/src/reporters/coverage_reporter.js +++ b/packages/jest-cli/src/reporters/coverage_reporter.js @@ -234,8 +234,9 @@ export default class CoverageReporter extends BaseReporter { } } else if (actual < threshold) { errors.push( - `Jest: Coverage for ${key} (${actual}` + - `%) does not meet ${name} threshold (${threshold}%)`, + `Jest: "${name}" coverage threshold for ${key} (${ + threshold + }%) not met: ` + `${actual}%`, ); } } @@ -245,66 +246,137 @@ export default class CoverageReporter extends BaseReporter { ); } - const expandedThresholds = {}; - Object.keys(globalConfig.coverageThreshold).forEach(filePathOrGlob => { - if (filePathOrGlob !== 'global') { - const pathArray = glob.sync(filePathOrGlob); - pathArray.forEach(filePath => { - expandedThresholds[path.resolve(filePath)] = - globalConfig.coverageThreshold[filePathOrGlob]; - }); - } else { - expandedThresholds.global = globalConfig.coverageThreshold.global; + const THRESHOLD_GROUP_TYPES = { + GLOB: 'glob', + GLOBAL: 'global', + PATH: 'path', + }; + const coveredFiles = map.files(); + const thresholdGroups = Object.keys(globalConfig.coverageThreshold); + const numThresholdGroups = thresholdGroups.length; + const groupTypeByThresholdGroup = {}; + const filesByGlob = {}; + + const coveredFilesSortedIntoThresholdGroup = coveredFiles.map(file => { + for (let i = 0; i < numThresholdGroups; i++) { + const thresholdGroup = thresholdGroups[i]; + const absoluteThresholdGroup = path.resolve(thresholdGroup); + + // The threshold group might be a path: + + if (file.indexOf(absoluteThresholdGroup) === 0) { + groupTypeByThresholdGroup[thresholdGroup] = + THRESHOLD_GROUP_TYPES.PATH; + return [file, thresholdGroup]; + } + + // If the threshold group is not a path it might be a glob: + + // Note: glob.sync is slow. By memoizing the files matching each glob + // (rather than recalculating it for each covered file) we save a tonne + // of execution time. + if (filesByGlob[absoluteThresholdGroup] === undefined) { + filesByGlob[absoluteThresholdGroup] = glob.sync( + absoluteThresholdGroup, + ); + } + + if (filesByGlob[absoluteThresholdGroup].indexOf(file) > -1) { + groupTypeByThresholdGroup[thresholdGroup] = + THRESHOLD_GROUP_TYPES.GLOB; + return [file, thresholdGroup]; + } + } + + // Neither a glob or a path? Toss it in global if there's a global threshold: + if (thresholdGroups.indexOf(THRESHOLD_GROUP_TYPES.GLOBAL) > -1) { + groupTypeByThresholdGroup[THRESHOLD_GROUP_TYPES.GLOBAL] = + THRESHOLD_GROUP_TYPES.GLOBAL; + return [file, THRESHOLD_GROUP_TYPES.GLOBAL]; } + + // A covered file that doesn't have a threshold: + return [file, undefined]; }); - const filteredCoverageSummary = map - .files() - .filter( - filePath => Object.keys(expandedThresholds).indexOf(filePath) === -1, - ) - .map(filePath => map.fileCoverageFor(filePath)) - .reduce((summary: ?CoverageSummary, fileCov: FileCoverage) => { - return summary === undefined || summary === null - ? (summary = fileCov.toSummary()) - : summary.merge(fileCov.toSummary()); - }, undefined); - - const errors = [].concat.apply( - [], - Object.keys(expandedThresholds) - .map(thresholdKey => { - if (thresholdKey === 'global') { - if (filteredCoverageSummary !== undefined) { - return check( - 'global', - expandedThresholds.global, - filteredCoverageSummary, - ); - } else { - return []; - } - } else { - if (map.files().indexOf(thresholdKey) !== -1) { - return check( - thresholdKey, - expandedThresholds[thresholdKey], - map.fileCoverageFor(thresholdKey).toSummary(), - ); - } else { - return [ - `Jest: Coverage data for ${thresholdKey} was not found.`, - ]; + const getFilesInThresholdGroup = thresholdGroup => + coveredFilesSortedIntoThresholdGroup + .filter(fileAndGroup => fileAndGroup[1] === thresholdGroup) + .map(fileAndGroup => fileAndGroup[0]); + + function combineCoverage(filePaths) { + return filePaths + .map(filePath => map.fileCoverageFor(filePath)) + .reduce( + ( + combinedCoverage: ?CoverageSummary, + nextFileCoverage: FileCoverage, + ) => { + if (combinedCoverage === undefined || combinedCoverage === null) { + return nextFileCoverage.toSummary(); } + return combinedCoverage.merge(nextFileCoverage.toSummary()); + }, + undefined, + ); + } + + let errors = []; + + thresholdGroups.forEach(thresholdGroup => { + switch (groupTypeByThresholdGroup[thresholdGroup]) { + case THRESHOLD_GROUP_TYPES.GLOBAL: { + const coverage = combineCoverage( + getFilesInThresholdGroup(THRESHOLD_GROUP_TYPES.GLOBAL), + ); + if (coverage) { + errors = errors.concat( + check( + thresholdGroup, + globalConfig.coverageThreshold[thresholdGroup], + coverage, + ), + ); + } + break; + } + case THRESHOLD_GROUP_TYPES.PATH: { + const coverage = combineCoverage( + getFilesInThresholdGroup(thresholdGroup), + ); + if (coverage) { + errors = errors.concat( + check( + thresholdGroup, + globalConfig.coverageThreshold[thresholdGroup], + coverage, + ), + ); } - }) - .filter(errorArray => { - return ( - errorArray !== undefined && - errorArray !== null && - errorArray.length > 0 + break; + } + case THRESHOLD_GROUP_TYPES.GLOB: + getFilesInThresholdGroup(thresholdGroup).forEach( + fileMatchingGlob => { + errors = errors.concat( + check( + fileMatchingGlob, + globalConfig.coverageThreshold[thresholdGroup], + map.fileCoverageFor(fileMatchingGlob).toSummary(), + ), + ); + }, + ); + break; + default: + errors = errors.concat( + `Jest: Coverage data for ${thresholdGroup} was not found.`, ); - }), + } + }); + + errors = errors.filter( + err => err !== undefined && err !== null && err.length > 0, ); if (errors.length > 0) {