Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Coverage thresholds can be set up for individual files #4185

Merged
merged 3 commits into from
Aug 24, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"micromatch": "^2.3.11",
"mkdirp": "^0.5.1",
"mocha": "^3.4.2",
"mock-fs": "^4.4.1",
"prettier": "^1.5.2",
"progress": "^1.1.8",
"react": "^15.4.2",
Expand Down
1 change: 1 addition & 0 deletions packages/jest-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"dependencies": {
"ansi-escapes": "^2.0.0",
"chalk": "^2.0.1",
"glob": "^7.1.2",
"graceful-fs": "^4.1.11",
"is-ci": "^1.0.10",
"istanbul-api": "^1.1.1",
Expand Down
132 changes: 115 additions & 17 deletions packages/jest-cli/src/reporters/__tests__/coverage_reporter.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
'use strict';

jest
.mock('fs')
.mock('istanbul-lib-coverage')
.mock('istanbul-lib-source-maps')
.mock('istanbul-api');
Expand All @@ -18,6 +17,9 @@ let libSourceMaps;
let CoverageReporter;
let istanbulApi;

import path from 'path';
import mock from 'mock-fs';

beforeEach(() => {
istanbulApi = require('istanbul-api');
istanbulApi.createReporter = jest.fn(() => ({
Expand All @@ -28,6 +30,23 @@ beforeEach(() => {
CoverageReporter = require('../coverage_reporter');
libCoverage = require('istanbul-lib-coverage');
libSourceMaps = require('istanbul-lib-source-maps');

const fileTree = {};
fileTree[process.cwd() + '/path-test-files'] = {
'full_path_file.js': '',
'glob-path': {
'file1.js': '',
'file2.js': '',
},
'non_covered_file.js': '',
'relative_path_file.js': '',
};

mock(fileTree);
});

afterEach(() => {
mock.restore();
});

describe('onRunComplete', () => {
Expand All @@ -50,18 +69,37 @@ 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));

return {
getCoverageSummary() {
return {
toJSON() {
return {
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: 0, pct: 50, skipped: 0, total: 0},
};
},
};
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},
};
return {
toSummary() {
return covSummary;
},
};
} else {
return undefined;
}
},
files() {
return files;
},
};
});
Expand All @@ -75,7 +113,7 @@ describe('onRunComplete', () => {
});
});

it('getLastError() returns an error when threshold is not met', () => {
it('getLastError() returns an error when threshold is not met for global', () => {
const testReporter = new CoverageReporter(
{
collectCoverage: true,
Expand All @@ -93,17 +131,77 @@ describe('onRunComplete', () => {
return testReporter
.onRunComplete(new Set(), {}, mockAggResults)
.then(() => {
expect(testReporter.getLastError()).toBeTruthy();
expect(testReporter.getLastError().message.split('\n')).toHaveLength(1);
});
});

it('getLastError() returns an error when threshold is not met for file', () => {
const covThreshold = {};
[
'global',
path.resolve(`${process.cwd()}/path-test-files/full_path_file.js`),
'./path-test-files/relative_path_file.js',
'path-test-files/glob-*/*.js',
].forEach(path => {
covThreshold[path] = {
statements: 100,
};
});

const testReporter = new CoverageReporter(
{
collectCoverage: true,
coverageThreshold: covThreshold,
},
{
maxWorkers: 2,
},
);
testReporter.log = jest.fn();
return testReporter
.onRunComplete(new Set(), {}, mockAggResults)
.then(() => {
expect(testReporter.getLastError().message.split('\n')).toHaveLength(5);
});
});

it('getLastError() returns `undefined` when threshold is met', () => {
const covThreshold = {};
[
'global',
path.resolve(`${process.cwd()}/path-test-files/full_path_file.js`),
'./path-test-files/relative_path_file.js',
'path-test-files/glob-*/*.js',
].forEach(path => {
covThreshold[path] = {
statements: 50,
};
});

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();
});
});

it('getLastError() returns an error when threshold is for non-covered file', () => {
const testReporter = new CoverageReporter(
{
collectCoverage: true,
coverageThreshold: {
global: {
statements: 50,
'path-test-files/non_covered_file.js': {
statements: 100,
},
},
},
Expand All @@ -115,7 +213,7 @@ describe('onRunComplete', () => {
return testReporter
.onRunComplete(new Set(), {}, mockAggResults)
.then(() => {
expect(testReporter.getLastError()).toBeUndefined();
expect(testReporter.getLastError().message.split('\n')).toHaveLength(1);
});
});
});
70 changes: 64 additions & 6 deletions packages/jest-cli/src/reporters/coverage_reporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
AggregatedResult,
CoverageMap,
FileCoverage,
CoverageSummary,
SerializableError,
TestResult,
} from 'types/TestResult';
Expand All @@ -29,6 +30,8 @@ import pify from 'pify';
import workerFarm from 'worker-farm';
import BaseReporter from './base_reporter';
import CoverageWorker from './coverage_worker';
import path from 'path';
import glob from 'glob';

const FAIL_COLOR = chalk.bold.red;
const RUNNING_TEST_COLOR = chalk.bold.dim;
Expand Down Expand Up @@ -204,8 +207,6 @@ class CoverageReporter extends BaseReporter {

_checkThreshold(globalConfig: GlobalConfig, map: CoverageMap) {
if (globalConfig.coverageThreshold) {
const results = map.getCoverageSummary().toJSON();

function check(name, thresholds, actuals) {
return [
'statements',
Expand Down Expand Up @@ -235,10 +236,67 @@ class CoverageReporter extends BaseReporter {
return errors;
}, []);
}
const errors = check(
'global',
globalConfig.coverageThreshold.global,
results,

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 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.`,
];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wonder if we could push these logic branches into check function?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like check function being unaware of such details and just sticking to comparing numbers. I can add move this to check as a two distinct conditions if actuals or thresholds do not contain given key respectively. What do you think?

}
}
})
.filter(errorArray => {
return (
errorArray !== undefined &&
errorArray !== null &&
errorArray.length > 0
);
}),
);

if (errors.length > 0) {
Expand Down
5 changes: 3 additions & 2 deletions types/TestResult.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,15 @@ type FileCoverageTotal = {|
total: number,
covered: number,
skipped: number,
pct?: number,
pct: number,
|};

type CoverageSummary = {|
export type CoverageSummary = {|
lines: FileCoverageTotal,
statements: FileCoverageTotal,
branches: FileCoverageTotal,
functions: FileCoverageTotal,
merge: (other: CoverageSummary) => void,
|};

export type FileCoverage = {|
Expand Down
4 changes: 4 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4380,6 +4380,10 @@ mocha@^3.4.2:
mkdirp "0.5.1"
supports-color "3.1.2"

mock-fs@^4.4.1:
version "4.4.1"
resolved "https://registry.yarnpkg.com/mock-fs/-/mock-fs-4.4.1.tgz#f285fa025b42a4031faf75b66f632b21e7056683"

modify-values@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.0.tgz#e2b6cdeb9ce19f99317a53722f3dbf5df5eaaab2"
Expand Down