Skip to content

Commit

Permalink
Add combined coverage threshold for directories
Browse files Browse the repository at this point in the history
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
  • Loading branch information
ttmarek committed Nov 14, 2017
1 parent 757f163 commit ab1d9fa
Show file tree
Hide file tree
Showing 2 changed files with 198 additions and 60 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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,
Expand All @@ -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);
});
});
});
184 changes: 128 additions & 56 deletions packages/jest-cli/src/reporters/coverage_reporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}%`,
);
}
}
Expand All @@ -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) {
Expand Down

0 comments on commit ab1d9fa

Please sign in to comment.