diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f37dede68c7..68c45859de0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * `[expect]` toMatchObject throws TypeError when a source property is null ([#6313](https://github.com/facebook/jest/pull/6313)) * `[jest-cli]` Normalize slashes in paths in CLI output on Windows ((#6310)[https://github.com/facebook/jest/pull/6310]) +* `[jest-cli]` Improve the message when running coverage while there are no files matching global threshold ([#6334](https://github.com/facebook/jest/pull/6334)) ## 23.0.1 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 ca5bcb6be0f8..9cb7528aec3c 100644 --- a/packages/jest-cli/src/reporters/__tests__/coverage_reporter.test.js +++ b/packages/jest-cli/src/reporters/__tests__/coverage_reporter.test.js @@ -16,6 +16,7 @@ let libSourceMaps; let CoverageReporter; let istanbulApi; +import {CoverageSummary} from 'istanbul-lib-coverage/lib/file'; import path from 'path'; import mock from 'mock-fs'; @@ -32,6 +33,9 @@ beforeEach(() => { const fileTree = {}; fileTree[process.cwd() + '/path-test-files'] = { + '000pc_coverage_file.js': '', + '050pc_coverage_file.js': '', + '100pc_coverage_file.js': '', 'full_path_file.js': '', 'glob-path': { 'file1.js': '', @@ -68,29 +72,46 @@ describe('onRunComplete', () => { }; libCoverage.createCoverageMap = jest.fn(() => { - const files = [ - './path-test-files/covered_file_without_threshold.js', - './path-test-files/full_path_file.js', - './path-test-files/relative_path_file.js', - './path-test-files/glob-path/file1.js', - './path-test-files/glob-path/file2.js', - ].map(p => path.resolve(p)); + const covSummary = { + branches: {covered: 0, pct: 0, skipped: 0, total: 0}, + functions: {covered: 0, pct: 0, skipped: 0, total: 0}, + lines: {covered: 0, pct: 0, skipped: 0, total: 0}, + statements: {covered: 5, pct: 50, skipped: 0, total: 10}, + }; + const fileCoverage = [ + ['./path-test-files/covered_file_without_threshold.js'], + ['./path-test-files/full_path_file.js'], + ['./path-test-files/relative_path_file.js'], + ['./path-test-files/glob-path/file1.js'], + ['./path-test-files/glob-path/file2.js'], + [ + './path-test-files/000pc_coverage_file.js', + {statements: {covered: 0, pct: 0, total: 10}}, + ], + [ + './path-test-files/050pc_coverage_file.js', + {statements: {covered: 5, pct: 50, total: 10}}, + ], + [ + './path-test-files/100pc_coverage_file.js', + {statements: {covered: 10, pct: 100, total: 10}}, + ], + ].reduce((c, f) => { + const file = path.resolve(f[0]); + const override = f[1]; + c[file] = new CoverageSummary({ + ...covSummary, + ...override, + }); + return c; + }, {}); return { fileCoverageFor(path) { - if (files.indexOf(path) !== -1) { - const covSummary = { - branches: {covered: 0, pct: 0, skipped: 0, total: 0}, - functions: {covered: 0, pct: 0, skipped: 0, total: 0}, - lines: {covered: 0, pct: 0, skipped: 0, total: 0}, - merge(other) { - return covSummary; - }, - statements: {covered: 0, pct: 50, skipped: 0, total: 0}, - }; + if (fileCoverage[path]) { return { toSummary() { - return covSummary; + return fileCoverage[path]; }, }; } else { @@ -98,7 +119,7 @@ describe('onRunComplete', () => { } }, files() { - return files; + return Object.keys(fileCoverage); }, }; }); @@ -281,4 +302,125 @@ describe('onRunComplete', () => { expect(testReporter.getLastError().message.split('\n')).toHaveLength(1); }); }); + + test(`getLastError() returns 'undefined' when global threshold group + is empty because PATH and GLOB threshold groups have matched all the + files in the coverage data.`, () => { + const testReporter = new CoverageReporter( + { + collectCoverage: true, + coverageThreshold: { + './path-test-files/': { + statements: 50, + }, + global: { + statements: 100, + }, + }, + }, + { + maxWorkers: 2, + }, + ); + testReporter.log = jest.fn(); + return testReporter + .onRunComplete(new Set(), {}, mockAggResults) + .then(() => { + expect(testReporter.getLastError()).toBeUndefined(); + }); + }); + + test(`getLastError() returns 'undefined' when file and directory path + threshold groups overlap`, () => { + const covThreshold = {}; + [ + './path-test-files/', + './path-test-files/covered_file_without_threshold.js', + './path-test-files/full_path_file.js', + './path-test-files/relative_path_file.js', + './path-test-files/glob-path/file1.js', + './path-test-files/glob-path/file2.js', + './path-test-files/*.js', + ].forEach(path => { + covThreshold[path] = { + statements: 0, + }; + }); + + const testReporter = new CoverageReporter( + { + collectCoverage: true, + coverageThreshold: covThreshold, + }, + { + maxWorkers: 2, + }, + ); + testReporter.log = jest.fn(); + return testReporter + .onRunComplete(new Set(), {}, mockAggResults) + .then(() => { + expect(testReporter.getLastError()).toBeUndefined(); + }); + }); + + test(`that if globs or paths are specified alongside global, coverage + data for matching paths will be subtracted from overall coverage + and thresholds will be applied independently`, () => { + const testReporter = new CoverageReporter( + { + collectCoverage: true, + coverageThreshold: { + './path-test-files/100pc_coverage_file.js': { + statements: 100, + }, + global: { + statements: 50, + }, + }, + }, + { + maxWorkers: 2, + }, + ); + testReporter.log = jest.fn(); + // 100% coverage file is removed from overall coverage so + // coverage drops to < 50% + return testReporter + .onRunComplete(new Set(), {}, mockAggResults) + .then(() => { + expect(testReporter.getLastError().message.split('\n')).toHaveLength(1); + }); + }); + + test(`that files are matched by all matching threshold groups`, () => { + const testReporter = new CoverageReporter( + { + collectCoverage: true, + coverageThreshold: { + './path-test-files/': { + statements: 50, + }, + './path-test-files/050pc_coverage_file.js': { + statements: 50, + }, + './path-test-files/100pc_coverage_*.js': { + statements: 100, + }, + './path-test-files/100pc_coverage_file.js': { + statements: 100, + }, + }, + }, + { + maxWorkers: 2, + }, + ); + testReporter.log = jest.fn(); + return testReporter + .onRunComplete(new Set(), {}, mockAggResults) + .then(() => { + expect(testReporter.getLastError()).toBeUndefined(); + }); + }); }); diff --git a/packages/jest-cli/src/reporters/coverage_reporter.js b/packages/jest-cli/src/reporters/coverage_reporter.js index 4fa08a4f0a9d..d1bcc4efa410 100644 --- a/packages/jest-cli/src/reporters/coverage_reporter.js +++ b/packages/jest-cli/src/reporters/coverage_reporter.js @@ -248,51 +248,60 @@ export default class CoverageReporter extends BaseReporter { }; 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); + const coveredFilesSortedIntoThresholdGroup = coveredFiles.reduce( + (files, file) => { + const pathOrGlobMatches = thresholdGroups + .map(thresholdGroup => { + const absoluteThresholdGroup = path.resolve(thresholdGroup); - // The threshold group might be a path: + // The threshold group might be a path: - if (file.indexOf(absoluteThresholdGroup) === 0) { - groupTypeByThresholdGroup[thresholdGroup] = - THRESHOLD_GROUP_TYPES.PATH; - return [file, thresholdGroup]; - } + 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: + // 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) - .map(filePath => path.resolve(filePath)); - } + // 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) + .map(filePath => path.resolve(filePath)); + } - if (filesByGlob[absoluteThresholdGroup].indexOf(file) > -1) { - groupTypeByThresholdGroup[thresholdGroup] = - THRESHOLD_GROUP_TYPES.GLOB; - return [file, thresholdGroup]; + if (filesByGlob[absoluteThresholdGroup].indexOf(file) > -1) { + groupTypeByThresholdGroup[thresholdGroup] = + THRESHOLD_GROUP_TYPES.GLOB; + return [file, thresholdGroup]; + } + + return; + }) + .filter(a => a !== undefined); + + if (pathOrGlobMatches.length > 0) { + return files.concat(pathOrGlobMatches); } - } - // 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]; - } + // 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 files.concat([[file, THRESHOLD_GROUP_TYPES.GLOBAL]]); + } - // A covered file that doesn't have a threshold: - return [file, undefined]; - }); + // A covered file that doesn't have a threshold: + return files.concat([[file, undefined]]); + }, + [], + ); const getFilesInThresholdGroup = thresholdGroup => coveredFilesSortedIntoThresholdGroup @@ -364,9 +373,15 @@ export default class CoverageReporter extends BaseReporter { ); break; default: - errors = errors.concat( - `Jest: Coverage data for ${thresholdGroup} was not found.`, - ); + // If the file specified by path is not found, error is returned. + if (thresholdGroup !== THRESHOLD_GROUP_TYPES.GLOBAL) { + errors = errors.concat( + `Jest: Coverage data for ${thresholdGroup} was not found.`, + ); + } + // Sometimes all files in the coverage data are matched by + // PATH and GLOB threshold groups in which case, don't error when + // the global threshold group doesn't match any files. } });