Skip to content

Commit f238bec

Browse files
author
rhys williams
committed
Improve the message when running coverage while there are no tests
fixes jestjs#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.
1 parent 2307643 commit f238bec

File tree

3 files changed

+215
-56
lines changed

3 files changed

+215
-56
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
* `[expect]` toMatchObject throws TypeError when a source property is null ([#6313](https://github.com/facebook/jest/pull/6313))
1010
* `[jest-cli]` Normalize slashes in paths in CLI output on Windows ([#6310](https://github.com/facebook/jest/pull/6310))
11+
* `[jest-cli]` Improve the message when running coverage while there are no files matching global threshold ([#6334](https://github.com/facebook/jest/pull/6334))
1112

1213
### Chore & Maintenance
1314

packages/jest-cli/src/reporters/__tests__/coverage_reporter.test.js

+161-19
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ let libSourceMaps;
1616
let CoverageReporter;
1717
let istanbulApi;
1818

19+
import {CoverageSummary} from 'istanbul-lib-coverage/lib/file';
1920
import path from 'path';
2021
import mock from 'mock-fs';
2122

@@ -32,6 +33,9 @@ beforeEach(() => {
3233

3334
const fileTree = {};
3435
fileTree[process.cwd() + '/path-test-files'] = {
36+
'000pc_coverage_file.js': '',
37+
'050pc_coverage_file.js': '',
38+
'100pc_coverage_file.js': '',
3539
'full_path_file.js': '',
3640
'glob-path': {
3741
'file1.js': '',
@@ -68,37 +72,54 @@ describe('onRunComplete', () => {
6872
};
6973

7074
libCoverage.createCoverageMap = jest.fn(() => {
71-
const files = [
72-
'./path-test-files/covered_file_without_threshold.js',
73-
'./path-test-files/full_path_file.js',
74-
'./path-test-files/relative_path_file.js',
75-
'./path-test-files/glob-path/file1.js',
76-
'./path-test-files/glob-path/file2.js',
77-
].map(p => path.resolve(p));
75+
const covSummary = {
76+
branches: {covered: 0, pct: 0, skipped: 0, total: 0},
77+
functions: {covered: 0, pct: 0, skipped: 0, total: 0},
78+
lines: {covered: 0, pct: 0, skipped: 0, total: 0},
79+
statements: {covered: 5, pct: 50, skipped: 0, total: 10},
80+
};
81+
const fileCoverage = [
82+
['./path-test-files/covered_file_without_threshold.js'],
83+
['./path-test-files/full_path_file.js'],
84+
['./path-test-files/relative_path_file.js'],
85+
['./path-test-files/glob-path/file1.js'],
86+
['./path-test-files/glob-path/file2.js'],
87+
[
88+
'./path-test-files/000pc_coverage_file.js',
89+
{statements: {covered: 0, pct: 0, total: 10}},
90+
],
91+
[
92+
'./path-test-files/050pc_coverage_file.js',
93+
{statements: {covered: 5, pct: 50, total: 10}},
94+
],
95+
[
96+
'./path-test-files/100pc_coverage_file.js',
97+
{statements: {covered: 10, pct: 100, total: 10}},
98+
],
99+
].reduce((c, f) => {
100+
const file = path.resolve(f[0]);
101+
const override = f[1];
102+
c[file] = new CoverageSummary({
103+
...covSummary,
104+
...override,
105+
});
106+
return c;
107+
}, {});
78108

79109
return {
80110
fileCoverageFor(path) {
81-
if (files.indexOf(path) !== -1) {
82-
const covSummary = {
83-
branches: {covered: 0, pct: 0, skipped: 0, total: 0},
84-
functions: {covered: 0, pct: 0, skipped: 0, total: 0},
85-
lines: {covered: 0, pct: 0, skipped: 0, total: 0},
86-
merge(other) {
87-
return covSummary;
88-
},
89-
statements: {covered: 0, pct: 50, skipped: 0, total: 0},
90-
};
111+
if (fileCoverage[path]) {
91112
return {
92113
toSummary() {
93-
return covSummary;
114+
return fileCoverage[path];
94115
},
95116
};
96117
} else {
97118
return undefined;
98119
}
99120
},
100121
files() {
101-
return files;
122+
return Object.keys(fileCoverage);
102123
},
103124
};
104125
});
@@ -281,4 +302,125 @@ describe('onRunComplete', () => {
281302
expect(testReporter.getLastError().message.split('\n')).toHaveLength(1);
282303
});
283304
});
305+
306+
test(`getLastError() returns 'undefined' when global threshold group
307+
is empty because PATH and GLOB threshold groups have matched all the
308+
files in the coverage data.`, () => {
309+
const testReporter = new CoverageReporter(
310+
{
311+
collectCoverage: true,
312+
coverageThreshold: {
313+
'./path-test-files/': {
314+
statements: 50,
315+
},
316+
global: {
317+
statements: 100,
318+
},
319+
},
320+
},
321+
{
322+
maxWorkers: 2,
323+
},
324+
);
325+
testReporter.log = jest.fn();
326+
return testReporter
327+
.onRunComplete(new Set(), {}, mockAggResults)
328+
.then(() => {
329+
expect(testReporter.getLastError()).toBeUndefined();
330+
});
331+
});
332+
333+
test(`getLastError() returns 'undefined' when file and directory path
334+
threshold groups overlap`, () => {
335+
const covThreshold = {};
336+
[
337+
'./path-test-files/',
338+
'./path-test-files/covered_file_without_threshold.js',
339+
'./path-test-files/full_path_file.js',
340+
'./path-test-files/relative_path_file.js',
341+
'./path-test-files/glob-path/file1.js',
342+
'./path-test-files/glob-path/file2.js',
343+
'./path-test-files/*.js',
344+
].forEach(path => {
345+
covThreshold[path] = {
346+
statements: 0,
347+
};
348+
});
349+
350+
const testReporter = new CoverageReporter(
351+
{
352+
collectCoverage: true,
353+
coverageThreshold: covThreshold,
354+
},
355+
{
356+
maxWorkers: 2,
357+
},
358+
);
359+
testReporter.log = jest.fn();
360+
return testReporter
361+
.onRunComplete(new Set(), {}, mockAggResults)
362+
.then(() => {
363+
expect(testReporter.getLastError()).toBeUndefined();
364+
});
365+
});
366+
367+
test(`that if globs or paths are specified alongside global, coverage
368+
data for matching paths will be subtracted from overall coverage
369+
and thresholds will be applied independently`, () => {
370+
const testReporter = new CoverageReporter(
371+
{
372+
collectCoverage: true,
373+
coverageThreshold: {
374+
'./path-test-files/100pc_coverage_file.js': {
375+
statements: 100,
376+
},
377+
global: {
378+
statements: 50,
379+
},
380+
},
381+
},
382+
{
383+
maxWorkers: 2,
384+
},
385+
);
386+
testReporter.log = jest.fn();
387+
// 100% coverage file is removed from overall coverage so
388+
// coverage drops to < 50%
389+
return testReporter
390+
.onRunComplete(new Set(), {}, mockAggResults)
391+
.then(() => {
392+
expect(testReporter.getLastError().message.split('\n')).toHaveLength(1);
393+
});
394+
});
395+
396+
test(`that files are matched by all matching threshold groups`, () => {
397+
const testReporter = new CoverageReporter(
398+
{
399+
collectCoverage: true,
400+
coverageThreshold: {
401+
'./path-test-files/': {
402+
statements: 50,
403+
},
404+
'./path-test-files/050pc_coverage_file.js': {
405+
statements: 50,
406+
},
407+
'./path-test-files/100pc_coverage_*.js': {
408+
statements: 100,
409+
},
410+
'./path-test-files/100pc_coverage_file.js': {
411+
statements: 100,
412+
},
413+
},
414+
},
415+
{
416+
maxWorkers: 2,
417+
},
418+
);
419+
testReporter.log = jest.fn();
420+
return testReporter
421+
.onRunComplete(new Set(), {}, mockAggResults)
422+
.then(() => {
423+
expect(testReporter.getLastError()).toBeUndefined();
424+
});
425+
});
284426
});

packages/jest-cli/src/reporters/coverage_reporter.js

+53-37
Original file line numberDiff line numberDiff line change
@@ -248,51 +248,61 @@ export default class CoverageReporter extends BaseReporter {
248248
};
249249
const coveredFiles = map.files();
250250
const thresholdGroups = Object.keys(globalConfig.coverageThreshold);
251-
const numThresholdGroups = thresholdGroups.length;
252251
const groupTypeByThresholdGroup = {};
253252
const filesByGlob = {};
254253

255-
const coveredFilesSortedIntoThresholdGroup = coveredFiles.map(file => {
256-
for (let i = 0; i < numThresholdGroups; i++) {
257-
const thresholdGroup = thresholdGroups[i];
258-
const absoluteThresholdGroup = path.resolve(thresholdGroup);
254+
const coveredFilesSortedIntoThresholdGroup = coveredFiles.reduce(
255+
(files, file) => {
256+
const pathOrGlobMatches = thresholdGroups.reduce(
257+
(agg, thresholdGroup) => {
258+
const absoluteThresholdGroup = path.resolve(thresholdGroup);
259259

260-
// The threshold group might be a path:
260+
// The threshold group might be a path:
261261

262-
if (file.indexOf(absoluteThresholdGroup) === 0) {
263-
groupTypeByThresholdGroup[thresholdGroup] =
264-
THRESHOLD_GROUP_TYPES.PATH;
265-
return [file, thresholdGroup];
266-
}
262+
if (file.indexOf(absoluteThresholdGroup) === 0) {
263+
groupTypeByThresholdGroup[thresholdGroup] =
264+
THRESHOLD_GROUP_TYPES.PATH;
265+
return agg.concat([[file, thresholdGroup]]);
266+
}
267267

268-
// If the threshold group is not a path it might be a glob:
268+
// If the threshold group is not a path it might be a glob:
269269

270-
// Note: glob.sync is slow. By memoizing the files matching each glob
271-
// (rather than recalculating it for each covered file) we save a tonne
272-
// of execution time.
273-
if (filesByGlob[absoluteThresholdGroup] === undefined) {
274-
filesByGlob[absoluteThresholdGroup] = glob
275-
.sync(absoluteThresholdGroup)
276-
.map(filePath => path.resolve(filePath));
277-
}
270+
// Note: glob.sync is slow. By memoizing the files matching each glob
271+
// (rather than recalculating it for each covered file) we save a tonne
272+
// of execution time.
273+
if (filesByGlob[absoluteThresholdGroup] === undefined) {
274+
filesByGlob[absoluteThresholdGroup] = glob
275+
.sync(absoluteThresholdGroup)
276+
.map(filePath => path.resolve(filePath));
277+
}
278+
279+
if (filesByGlob[absoluteThresholdGroup].indexOf(file) > -1) {
280+
groupTypeByThresholdGroup[thresholdGroup] =
281+
THRESHOLD_GROUP_TYPES.GLOB;
282+
return agg.concat([[file, thresholdGroup]]);
283+
}
284+
285+
return agg;
286+
},
287+
[],
288+
);
278289

279-
if (filesByGlob[absoluteThresholdGroup].indexOf(file) > -1) {
280-
groupTypeByThresholdGroup[thresholdGroup] =
281-
THRESHOLD_GROUP_TYPES.GLOB;
282-
return [file, thresholdGroup];
290+
if (pathOrGlobMatches.length > 0) {
291+
return files.concat(pathOrGlobMatches);
283292
}
284-
}
285293

286-
// Neither a glob or a path? Toss it in global if there's a global threshold:
287-
if (thresholdGroups.indexOf(THRESHOLD_GROUP_TYPES.GLOBAL) > -1) {
288-
groupTypeByThresholdGroup[THRESHOLD_GROUP_TYPES.GLOBAL] =
289-
THRESHOLD_GROUP_TYPES.GLOBAL;
290-
return [file, THRESHOLD_GROUP_TYPES.GLOBAL];
291-
}
294+
// Neither a glob or a path? Toss it in global if there's a global threshold:
295+
if (thresholdGroups.indexOf(THRESHOLD_GROUP_TYPES.GLOBAL) > -1) {
296+
groupTypeByThresholdGroup[THRESHOLD_GROUP_TYPES.GLOBAL] =
297+
THRESHOLD_GROUP_TYPES.GLOBAL;
298+
return files.concat([[file, THRESHOLD_GROUP_TYPES.GLOBAL]]);
299+
}
292300

293-
// A covered file that doesn't have a threshold:
294-
return [file, undefined];
295-
});
301+
// A covered file that doesn't have a threshold:
302+
return files.concat([[file, undefined]]);
303+
},
304+
[],
305+
);
296306

297307
const getFilesInThresholdGroup = thresholdGroup =>
298308
coveredFilesSortedIntoThresholdGroup
@@ -364,9 +374,15 @@ export default class CoverageReporter extends BaseReporter {
364374
);
365375
break;
366376
default:
367-
errors = errors.concat(
368-
`Jest: Coverage data for ${thresholdGroup} was not found.`,
369-
);
377+
// If the file specified by path is not found, error is returned.
378+
if (thresholdGroup !== THRESHOLD_GROUP_TYPES.GLOBAL) {
379+
errors = errors.concat(
380+
`Jest: Coverage data for ${thresholdGroup} was not found.`,
381+
);
382+
}
383+
// Sometimes all files in the coverage data are matched by
384+
// PATH and GLOB threshold groups in which case, don't error when
385+
// the global threshold group doesn't match any files.
370386
}
371387
});
372388

0 commit comments

Comments
 (0)