Skip to content

Commit

Permalink
Improve the message when running coverage while there are no tests
Browse files Browse the repository at this point in the history
fixes #6141
Update the coverage reporting so that it still conforms to the
documentation but doesn't throw an error when there are no files matching
"global" threshold group (maybe because they are already matched with a path or glob).
Also make sure that a file is matched against all matching path and glob threshold groups instead of just one.
  • Loading branch information
rhys williams committed May 29, 2018
1 parent a43fd6c commit 12e2f38
Show file tree
Hide file tree
Showing 3 changed files with 215 additions and 56 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
180 changes: 161 additions & 19 deletions packages/jest-cli/src/reporters/__tests__/coverage_reporter.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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': '',
Expand Down Expand Up @@ -68,37 +72,54 @@ 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 {
return undefined;
}
},
files() {
return files;
return Object.keys(fileCoverage);
},
};
});
Expand Down Expand Up @@ -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();
});
});
});
90 changes: 53 additions & 37 deletions packages/jest-cli/src/reporters/coverage_reporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -248,51 +248,61 @@ 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.reduce(
(agg, 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 agg.concat([[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 agg.concat([[file, thresholdGroup]]);
}

return agg;
},
[],
);

if (filesByGlob[absoluteThresholdGroup].indexOf(file) > -1) {
groupTypeByThresholdGroup[thresholdGroup] =
THRESHOLD_GROUP_TYPES.GLOB;
return [file, thresholdGroup];
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
Expand Down Expand Up @@ -364,9 +374,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.
}
});

Expand Down

0 comments on commit 12e2f38

Please sign in to comment.