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

Use the remapped code coverage summary for code coverage threshold check #499

Merged
merged 15 commits into from
Nov 15, 2018
Merged
Show file tree
Hide file tree
Changes from 9 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
84 changes: 66 additions & 18 deletions config/karma/shared.karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,15 @@ const logger = require('@blackbaud/skyux-logger');
* @param {*} config
*/
function getCoverageThreshold(skyPagesConfig) {

function getProperty(threshold) {
return {
global: {
statements: threshold,
branches: threshold,
functions: threshold,
lines: threshold
}
};
}

switch (skyPagesConfig.skyux.codeCoverageThreshold) {
case 'none':
return getProperty(0);
return 0;

case 'standard':
return getProperty(80);
return 80;

case 'strict':
return getProperty(100);
return 100;
}
}

Expand All @@ -47,6 +35,8 @@ function getConfig(config) {
let testWebpackConfig = require('../webpack/test.webpack.config');
let remapIstanbul = require('remap-istanbul');

const utils = require('istanbul').utils;

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

Expand All @@ -58,6 +48,8 @@ function getConfig(config) {
preprocessors[specBundle] = ['coverage', 'webpack', 'sourcemap'];
preprocessors[specStyles] = ['webpack'];

let remapCoverageSummary;

config.set({
basePath: '',
frameworks: ['jasmine'],
Expand All @@ -77,15 +69,71 @@ function getConfig(config) {
webpack: testWebpackConfig.getWebpackConfig(skyPagesConfig, argv),
coverageReporter: {
dir: path.join(process.cwd(), 'coverage'),
check: getCoverageThreshold(skyPagesConfig),
reporters: [
{ type: 'json' },
{ type: 'html' },
{ type: 'text-summary' },
{ type: 'lcov' }
{ type: 'lcov' },
{ type: 'in-memory' }
],
_onWriteReport: function (collector) {
return remapIstanbul.remap(collector.getFinalCoverage());
const newCollector = remapIstanbul.remap(collector.getFinalCoverage());

if (!remapCoverageSummary) {

Choose a reason for hiding this comment

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

Any concern on how this might affect multiple browsers? My concern is that the first result will be returned for all subsequent calls, even though they may be different browsers with different coverage.

// Cache the remapped coverage summary so we can evaluate it in the _onExit() hook.
let summaries = [];

newCollector.files().forEach((file) => {
summaries.push(
utils.summarizeFileCoverage(
newCollector.fileCoverageFor(file)
)
);
});

remapCoverageSummary = utils.mergeSummaryObjects.apply(null, summaries);
}

return newCollector;
},

_onExit: function (done) {
const threshold = getCoverageThreshold(skyPagesConfig);

if (threshold) {
// The karma-coverage library does not use the coverage summary from the remapped source
// code, so its built-in code coverage check uses numbers that don't match what's reported
// to the user. This will use the coverage summary generated from the remapped
// source code.
var keys = [
'statements',
'branches',
'lines',
'functions'
];

const threshold = getCoverageThreshold(skyPagesConfig);

Choose a reason for hiding this comment

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

I don’t believe this line is needed twice.


let coverageFailed;

keys.forEach(function (key) {

Choose a reason for hiding this comment

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

Any reason to not use fat arrow here?

var actual = remapCoverageSummary[key].pct;

if (actual < threshold) {
coverageFailed = true;
logger.error(
`Coverage for ${key} (${actual}%) does not meet global threshold (${threshold}%)`
);
}
});

if (coverageFailed) {
logger.info(`Karma has exited with 1.`);
process.exit(1);
}
}

done();
}
},
webpackServer: {
Expand Down
185 changes: 146 additions & 39 deletions test/config-karma-shared.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,33 +59,6 @@ describe('config karma shared', () => {
});
});

function checkCodeCoverage(configValue, threshold) {

mock('../config/sky-pages/sky-pages.config.js', {
getSkyPagesConfig: () => ({
skyux: {
codeCoverageThreshold: configValue
}
})
});

mock(testConfigFilename, {
getWebpackConfig: () => {}
});

mock.reRequire('../config/karma/shared.karma.conf')({
set: (config) => {
expect(config.coverageReporter.check).toEqual({
global: {
statements: threshold,
branches: threshold,
functions: threshold,
lines: threshold
}
});
}
});
}

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

it('should handle codeCoverageThreshold set to "none"', () => {
checkCodeCoverage('none', 0);
});

it('should handle codeCoverageThreshold set to "standard"', () => {
checkCodeCoverage('standard', 80);
});

it('should handle codeCoverageThreshold set to "strict"', () => {
checkCodeCoverage('strict', 100);
});

it('should pass the logColor flag to the config', () => {
mock('@blackbaud/skyux-logger', { logColor: false });
mock.reRequire('../config/karma/shared.karma.conf')({
Expand Down Expand Up @@ -153,4 +114,150 @@ describe('config karma shared', () => {
});
});

describe('code coverage', () => {
let errorSpy;
let exitSpy;
let infoSpy;

const coverageProps = [
'statements',
'branches',
'lines',
'functions'
];

beforeEach(() => {
mock(testConfigFilename, {
getWebpackConfig: () => {}
});

errorSpy = jasmine.createSpy('error');
infoSpy = jasmine.createSpy('info');

exitSpy = spyOn(process, 'exit');

mock('@blackbaud/skyux-logger', {
error: errorSpy,
info: infoSpy
});

mock('remap-istanbul', {
remap: () => {
return {
fileCoverageFor: () => { },
files: () => [
'test.js'
]
};
}
});
});

function createMergeSummaryObjectSpy(testPct) {
return jasmine.createSpy('mergeSummaryObjects').and.callFake(() => {
const summary = {};

coverageProps.forEach((key) => {
summary[key] = {
pct: testPct
};
});

return summary;
});
}

function mockIstanbul(mergeSummaryObjects) {
mock('istanbul', {
utils: {
summarizeFileCoverage: () => {},
mergeSummaryObjects
}
});
}

function mockConfig(codeCoverageThreshold) {
mock('../config/sky-pages/sky-pages.config.js', {
getSkyPagesConfig: () => ({
skyux: {
codeCoverageThreshold
}
})
});
}

function resetSpies() {
errorSpy.calls.reset();
infoSpy.calls.reset();
exitSpy.calls.reset();
}

function checkCodeCoverage(thresholdName, threshold, testPct, shouldPass) {
const mergeSummaryObjectsSpy = createMergeSummaryObjectSpy(testPct);

mockIstanbul(mergeSummaryObjectsSpy);
mockConfig(thresholdName);

resetSpies();

mock.reRequire('../config/karma/shared.karma.conf')({
set: (config) => {
const fakeCollector = {
getFinalCoverage: () => {
return {
files: () => []
};
}
};

// Simulate multiple reporters and verify that the merged coverage summary
// is only created once.
config.coverageReporter._onWriteReport(fakeCollector);
config.coverageReporter._onWriteReport(fakeCollector);

expect(mergeSummaryObjectsSpy).toHaveBeenCalledTimes(1);

// Verify the tests pass or fail based on the coverage percentage.
const doneSpy = jasmine.createSpy('done');

config.coverageReporter._onExit(doneSpy);

if (shouldPass) {
expect(exitSpy).not.toHaveBeenCalled();
expect(errorSpy).not.toHaveBeenCalled();
expect(infoSpy).not.toHaveBeenCalledWith('Karma has exited with 1.');
} else {
expect(exitSpy).toHaveBeenCalledWith(1);

coverageProps.forEach((key) => {
expect(errorSpy).toHaveBeenCalledWith(
`Coverage for ${key} (${testPct}%) does not meet global threshold (${threshold}%)`
);
});

expect(infoSpy).toHaveBeenCalledWith('Karma has exited with 1.');
}

expect(doneSpy).toHaveBeenCalled();
}
});
}

it('should handle codeCoverageThreshold set to "none"', () => {
checkCodeCoverage('none', 0, 0, true);
checkCodeCoverage('none', 0, 1, true);
});

it('should handle codeCoverageThreshold set to "standard"', () => {
checkCodeCoverage('standard', 80, 79, false);
checkCodeCoverage('standard', 80, 80, true);
checkCodeCoverage('standard', 80, 81, true);
});

it('should handle codeCoverageThreshold set to "strict"', () => {
checkCodeCoverage('strict', 100, 99, false);
checkCodeCoverage('strict', 100, 100, true);
});
});

});