Skip to content
This repository was archived by the owner on Dec 8, 2022. It is now read-only.

Commit 76f7239

Browse files
Use the remapped code coverage summary for code coverage threshold check (#499)
* Use the remapped code coverage summary for code coverage threshold check * Fixed lint rule * Unit tests * Removed redundant const * Validate that done() called * Be explicit about whether test should pass coverage threshold check * Fixed lint error * Report coverage inside _onWriteReport() since _onExit() doesn't get called during `watch` * Minor refactoring * Fixed linting error. * Use function expression for consistency * Changed `let` to`const` where appropriate
1 parent d7841b3 commit 76f7239

File tree

2 files changed

+257
-67
lines changed

2 files changed

+257
-67
lines changed

config/karma/shared.karma.conf.js

+92-22
Original file line numberDiff line numberDiff line change
@@ -8,27 +8,15 @@ const logger = require('@blackbaud/skyux-logger');
88
* @param {*} config
99
*/
1010
function getCoverageThreshold(skyPagesConfig) {
11-
12-
function getProperty(threshold) {
13-
return {
14-
global: {
15-
statements: threshold,
16-
branches: threshold,
17-
functions: threshold,
18-
lines: threshold
19-
}
20-
};
21-
}
22-
2311
switch (skyPagesConfig.skyux.codeCoverageThreshold) {
2412
case 'none':
25-
return getProperty(0);
13+
return 0;
2614

2715
case 'standard':
28-
return getProperty(80);
16+
return 80;
2917

3018
case 'strict':
31-
return getProperty(100);
19+
return 100;
3220
}
3321
}
3422

@@ -44,20 +32,26 @@ function getConfig(config) {
4432
const path = require('path');
4533
const srcPath = path.join(process.cwd(), 'src');
4634

47-
let testWebpackConfig = require('../webpack/test.webpack.config');
48-
let remapIstanbul = require('remap-istanbul');
35+
const testWebpackConfig = require('../webpack/test.webpack.config');
36+
const remapIstanbul = require('remap-istanbul');
37+
38+
const utils = require('istanbul').utils;
4939

5040
// See minimist documentation regarding `argv._` https://github.com/substack/minimist
51-
let skyPagesConfig = require('../sky-pages/sky-pages.config').getSkyPagesConfig(argv._[0]);
41+
const skyPagesConfig = require('../sky-pages/sky-pages.config').getSkyPagesConfig(argv._[0]);
5242

5343
// Using __dirname so this file can be extended from other configuration file locations
5444
const specBundle = `${__dirname}/../../utils/spec-bundle.js`;
5545
const specStyles = `${__dirname}/../../utils/spec-styles.js`;
56-
let preprocessors = {};
46+
47+
const preprocessors = {};
5748

5849
preprocessors[specBundle] = ['coverage', 'webpack', 'sourcemap'];
5950
preprocessors[specStyles] = ['webpack'];
6051

52+
let onWriteReportIndex = -1;
53+
let coverageFailed;
54+
6155
config.set({
6256
basePath: '',
6357
frameworks: ['jasmine'],
@@ -77,15 +71,91 @@ function getConfig(config) {
7771
webpack: testWebpackConfig.getWebpackConfig(skyPagesConfig, argv),
7872
coverageReporter: {
7973
dir: path.join(process.cwd(), 'coverage'),
80-
check: getCoverageThreshold(skyPagesConfig),
8174
reporters: [
8275
{ type: 'json' },
8376
{ type: 'html' },
8477
{ type: 'text-summary' },
85-
{ type: 'lcov' }
78+
{ type: 'lcov' },
79+
{ type: 'in-memory' }
8680
],
8781
_onWriteReport: function (collector) {
88-
return remapIstanbul.remap(collector.getFinalCoverage());
82+
onWriteReportIndex++;
83+
84+
const newCollector = remapIstanbul.remap(collector.getFinalCoverage());
85+
86+
const threshold = getCoverageThreshold(skyPagesConfig);
87+
88+
// The karma-coverage library does not use the coverage summary from the remapped source
89+
// code, so its built-in code coverage check uses numbers that don't match what's reported
90+
// to the user. This will use the coverage summary generated from the remapped
91+
// source code.
92+
if (threshold) {
93+
// When calling the _onWriteReport() method, karma-coverage loops through each reporter,
94+
// then for each reporter loops through each browser. Since karma-coverage doesn't
95+
// supply this method with any information about the reporter or browser for which this
96+
// method is being called, we must calculate it by looking at how many times the method
97+
// has been called.
98+
const browserIndex = Math.floor(onWriteReportIndex / this.reporters.length);
99+
100+
if (onWriteReportIndex % this.reporters.length === 0) {
101+
// The karma-coverage library has moved to the next browser and has started the first
102+
// reporter for that browser, so evaluate the code coverage now.
103+
const browserName = config.browsers[browserIndex];
104+
105+
const summaries = [];
106+
107+
newCollector.files().forEach((file) => {
108+
summaries.push(
109+
utils.summarizeFileCoverage(
110+
newCollector.fileCoverageFor(file)
111+
)
112+
);
113+
});
114+
115+
const remapCoverageSummary =
116+
utils.mergeSummaryObjects.apply(
117+
null,
118+
summaries
119+
);
120+
121+
const keys = [
122+
'statements',
123+
'branches',
124+
'lines',
125+
'functions'
126+
];
127+
128+
keys.forEach((key) => {
129+
let actual = remapCoverageSummary[key].pct;
130+
131+
if (actual < threshold) {
132+
coverageFailed = true;
133+
logger.error(
134+
`Coverage in ${browserName} for ${key} (${actual}%) does not meet ` +
135+
`global threshold (${threshold}%)`
136+
);
137+
}
138+
});
139+
}
140+
}
141+
142+
// It's possible the user is running the watch command, so the field we're
143+
// using to track the number of calls to _onWriteReport() needs to be reset
144+
// after each run.
145+
if (onWriteReportIndex === (this.reporters.length * config.browsers.length - 1)) {
146+
onWriteReportIndex = -1;
147+
}
148+
149+
return newCollector;
150+
},
151+
152+
_onExit: function (done) {
153+
if (coverageFailed) {
154+
logger.info('Karma has exited with 1.');
155+
process.exit(1);
156+
}
157+
158+
done();
89159
}
90160
},
91161
webpackServer: {

test/config-karma-shared.spec.js

+165-45
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,8 @@ describe('config karma shared', () => {
2222
});
2323

2424
mock.reRequire('../config/karma/shared.karma.conf')({
25-
set: (config) => {
26-
const collector = {
27-
getFinalCoverage: () => ({})
28-
};
25+
set: () => {
2926
expect(called).toEqual(true);
30-
expect(typeof config.coverageReporter._onWriteReport).toEqual('function');
31-
expect(config.coverageReporter._onWriteReport(collector)).toBeDefined();
3227
done();
3328
}
3429
});
@@ -59,33 +54,6 @@ describe('config karma shared', () => {
5954
});
6055
});
6156

62-
function checkCodeCoverage(configValue, threshold) {
63-
64-
mock('../config/sky-pages/sky-pages.config.js', {
65-
getSkyPagesConfig: () => ({
66-
skyux: {
67-
codeCoverageThreshold: configValue
68-
}
69-
})
70-
});
71-
72-
mock(testConfigFilename, {
73-
getWebpackConfig: () => {}
74-
});
75-
76-
mock.reRequire('../config/karma/shared.karma.conf')({
77-
set: (config) => {
78-
expect(config.coverageReporter.check).toEqual({
79-
global: {
80-
statements: threshold,
81-
branches: threshold,
82-
functions: threshold,
83-
lines: threshold
84-
}
85-
});
86-
}
87-
});
88-
}
8957

9058
it('should not add the check property when codeCoverageThreshold is not defined', () => {
9159
mock('../config/sky-pages/sky-pages.config.js', {
@@ -105,18 +73,6 @@ describe('config karma shared', () => {
10573
});
10674
});
10775

108-
it('should handle codeCoverageThreshold set to "none"', () => {
109-
checkCodeCoverage('none', 0);
110-
});
111-
112-
it('should handle codeCoverageThreshold set to "standard"', () => {
113-
checkCodeCoverage('standard', 80);
114-
});
115-
116-
it('should handle codeCoverageThreshold set to "strict"', () => {
117-
checkCodeCoverage('strict', 100);
118-
});
119-
12076
it('should pass the logColor flag to the config', () => {
12177
mock('@blackbaud/skyux-logger', { logColor: false });
12278
mock.reRequire('../config/karma/shared.karma.conf')({
@@ -153,4 +109,168 @@ describe('config karma shared', () => {
153109
});
154110
});
155111

112+
describe('code coverage', () => {
113+
let errorSpy;
114+
let exitSpy;
115+
let infoSpy;
116+
117+
const coverageProps = [
118+
'statements',
119+
'branches',
120+
'lines',
121+
'functions'
122+
];
123+
124+
beforeEach(() => {
125+
mock(testConfigFilename, {
126+
getWebpackConfig: () => {}
127+
});
128+
129+
errorSpy = jasmine.createSpy('error');
130+
infoSpy = jasmine.createSpy('info');
131+
132+
exitSpy = spyOn(process, 'exit');
133+
134+
mock('@blackbaud/skyux-logger', {
135+
error: errorSpy,
136+
info: infoSpy
137+
});
138+
139+
mock('remap-istanbul', {
140+
remap: () => {
141+
return {
142+
fileCoverageFor: () => { },
143+
files: () => [
144+
'test.js'
145+
]
146+
};
147+
}
148+
});
149+
});
150+
151+
function createMergeSummaryObjectSpy(testPct) {
152+
return jasmine.createSpy('mergeSummaryObjects').and.callFake(() => {
153+
const summary = {};
154+
155+
coverageProps.forEach((key) => {
156+
summary[key] = {
157+
pct: testPct
158+
};
159+
});
160+
161+
return summary;
162+
});
163+
}
164+
165+
function mockIstanbul(mergeSummaryObjects) {
166+
mock('istanbul', {
167+
utils: {
168+
summarizeFileCoverage: () => {},
169+
mergeSummaryObjects
170+
}
171+
});
172+
}
173+
174+
function mockConfig(codeCoverageThreshold) {
175+
mock('../config/sky-pages/sky-pages.config.js', {
176+
getSkyPagesConfig: () => ({
177+
skyux: {
178+
codeCoverageThreshold
179+
}
180+
})
181+
});
182+
}
183+
184+
function resetSpies() {
185+
errorSpy.calls.reset();
186+
infoSpy.calls.reset();
187+
exitSpy.calls.reset();
188+
}
189+
190+
function checkCodeCoverage(thresholdName, threshold, testPct, shouldPass) {
191+
const mergeSummaryObjectsSpy = createMergeSummaryObjectSpy(testPct);
192+
193+
mockIstanbul(mergeSummaryObjectsSpy);
194+
mockConfig(thresholdName);
195+
196+
resetSpies();
197+
198+
const browsers = ['Chrome', 'Firefox'];
199+
const reporters = [
200+
{ type: 'json' },
201+
{ type: 'html' }
202+
];
203+
204+
mock.reRequire('../config/karma/shared.karma.conf')({
205+
browsers: browsers,
206+
set: (config) => {
207+
config.coverageReporter.reporters = reporters;
208+
209+
const fakeCollector = {
210+
getFinalCoverage: () => {
211+
return {
212+
files: () => []
213+
};
214+
}
215+
};
216+
217+
// Simulate multiple reporters/browsers the same way that karma-coverage does.
218+
reporters.forEach(() => {
219+
browsers.forEach(() => {
220+
config.coverageReporter._onWriteReport(fakeCollector);
221+
});
222+
});
223+
224+
// Code coverage should be evaluated once per browser unless the threshold is 0,
225+
// in which case it should not be called at all.
226+
expect(mergeSummaryObjectsSpy).toHaveBeenCalledTimes(
227+
threshold === 0 ? 0 : browsers.length
228+
);
229+
230+
// Verify the tests pass or fail based on the coverage percentage.
231+
const doneSpy = jasmine.createSpy('done');
232+
233+
config.coverageReporter._onExit(doneSpy);
234+
235+
if (shouldPass) {
236+
expect(exitSpy).not.toHaveBeenCalled();
237+
expect(errorSpy).not.toHaveBeenCalled();
238+
expect(infoSpy).not.toHaveBeenCalledWith('Karma has exited with 1.');
239+
} else {
240+
expect(exitSpy).toHaveBeenCalledWith(1);
241+
242+
browsers.forEach((browserName) => {
243+
coverageProps.forEach((key) => {
244+
expect(errorSpy).toHaveBeenCalledWith(
245+
`Coverage in ${browserName} for ${key} (${testPct}%) does not meet ` +
246+
`global threshold (${threshold}%)`
247+
);
248+
});
249+
})
250+
251+
expect(infoSpy).toHaveBeenCalledWith('Karma has exited with 1.');
252+
}
253+
254+
expect(doneSpy).toHaveBeenCalled();
255+
}
256+
});
257+
}
258+
259+
it('should handle codeCoverageThreshold set to "none"', () => {
260+
checkCodeCoverage('none', 0, 0, true);
261+
checkCodeCoverage('none', 0, 1, true);
262+
});
263+
264+
it('should handle codeCoverageThreshold set to "standard"', () => {
265+
checkCodeCoverage('standard', 80, 79, false);
266+
checkCodeCoverage('standard', 80, 80, true);
267+
checkCodeCoverage('standard', 80, 81, true);
268+
});
269+
270+
it('should handle codeCoverageThreshold set to "strict"', () => {
271+
checkCodeCoverage('strict', 100, 99, false);
272+
checkCodeCoverage('strict', 100, 100, true);
273+
});
274+
});
275+
156276
});

0 commit comments

Comments
 (0)