diff --git a/README.md b/README.md index 5554f99..d156027 100644 --- a/README.md +++ b/README.md @@ -64,10 +64,18 @@ module.exports = function (config) { // enforce percentage thresholds // anything under these percentages will cause karma to fail with an exit code of 1 if not running in watch mode thresholds: { - statements: 100, - lines: 100, - branches: 100, - functions: 100 + global: { // thresholds for all files + statements: 100, + lines: 100, + branches: 100, + functions: 100 + }, + each: { // thresholds per file + statements: 100, + lines: 100, + branches: 100, + functions: 100 + } } } diff --git a/src/reporter.js b/src/reporter.js index 39feba6..e55299f 100644 --- a/src/reporter.js +++ b/src/reporter.js @@ -1,10 +1,23 @@ 'use strict'; const istanbul = require('istanbul-api'); -const fixWebpackSourcePaths = require('./util').fixWebpackSourcePaths; +const util = require('./util'); const BROWSER_PLACEHOLDER = '%browser%'; +function checkThresholds(thresholds, summary) { + const failedTypes = []; + + Object.keys(thresholds).forEach(key => { + const coverage = summary[key].pct; + if (coverage < thresholds[key]) { + failedTypes.push(key); + } + }); + + return failedTypes; +} + function CoverageIstanbulReporter(baseReporterDecorator, logger, config) { baseReporterDecorator(this); @@ -48,7 +61,7 @@ function CoverageIstanbulReporter(baseReporterDecorator, logger, config) { Object.keys(coverage).forEach(filename => { const fileCoverage = coverage[filename]; if (fileCoverage.inputSourceMap && coverageIstanbulReporter.fixWebpackSourcePaths) { - fileCoverage.inputSourceMap = fixWebpackSourcePaths(fileCoverage.inputSourceMap); + fileCoverage.inputSourceMap = util.fixWebpackSourcePaths(fileCoverage.inputSourceMap); } if ( coverageIstanbulReporter.skipFilesWithNoCoverage && @@ -68,21 +81,57 @@ function CoverageIstanbulReporter(baseReporterDecorator, logger, config) { reporter.write(remappedCoverageMap); - const thresholds = coverageIstanbulReporter.thresholds; - if (thresholds) { - // Adapted from https://github.com/istanbuljs/nyc/blob/98ebdff573be91e1098bb7259776a9082a5c1ce1/index.js#L463-L478 - let thresholdCheckFailed = false; - const summary = remappedCoverageMap.getCoverageSummary(); - Object.keys(thresholds).forEach(key => { - const coverage = summary[key].pct; - if (coverage < thresholds[key]) { - thresholdCheckFailed = true; - log.error(`Coverage for ${key} (${coverage}%) does not meet global threshold (${thresholds[key]}%)`); + const thresholds = { + global: { + statements: 0, + lines: 0, + branches: 0, + functions: 0 + }, + each: { + statements: 0, + lines: 0, + branches: 0, + functions: 0 + } + }; + + const userThresholds = coverageIstanbulReporter.thresholds; + + if (userThresholds) { + if (userThresholds.global || userThresholds.each) { + Object.assign(thresholds.global, userThresholds.global); + Object.assign(thresholds.each, userThresholds.each); + } else { + Object.assign(thresholds.global, userThresholds); + } + } + + let thresholdCheckFailed = false; + + // Adapted from https://github.com/istanbuljs/nyc/blob/98ebdff573be91e1098bb7259776a9082a5c1ce1/index.js#L463-L478 + const globalSummary = remappedCoverageMap.getCoverageSummary(); + const failedGlobalTypes = checkThresholds(thresholds.global, globalSummary); + failedGlobalTypes.forEach(type => { + thresholdCheckFailed = true; + log.error(`Coverage for ${type} (${globalSummary[type].pct}%) does not meet global threshold (${thresholds.global[type]}%)`); + }); + + remappedCoverageMap.files().forEach(file => { + const fileSummary = remappedCoverageMap.fileCoverageFor(file).toSummary().data; + const failedFileTypes = checkThresholds(thresholds.each, fileSummary); + + failedFileTypes.forEach(type => { + thresholdCheckFailed = true; + if (coverageIstanbulReporter.fixWebpackSourcePaths) { + file = util.fixWebpackFilePath(file); } + log.error(`Coverage for ${type} (${fileSummary[type].pct}%) in file ${file} does not meet per file threshold (${thresholds.each[type]}%)`); }); - if (thresholdCheckFailed && results) { - results.exitCode = 1; - } + }); + + if (thresholdCheckFailed && results) { + results.exitCode = 1; } }); }; diff --git a/src/util.js b/src/util.js index 3b0e950..8a1b8e5 100644 --- a/src/util.js +++ b/src/util.js @@ -1,18 +1,26 @@ -function fixWebpackSourcePaths(sourceMap) { +function fixWebpackFilePath(filePath) { const isWin = process.platform.startsWith('win'); + if (filePath.indexOf('!') !== -1) { + filePath = filePath.split('!').pop(); + } + + if (filePath.indexOf('?') !== -1) { + filePath = filePath.split('?')[0]; + } + + // Workaround for https://github.com/mattlewis92/karma-coverage-istanbul-reporter/issues/9 + if (isWin) { + filePath = filePath.replace(/\\/g, '/'); + } + + return filePath; +} + +function fixWebpackSourcePaths(sourceMap) { return Object.assign({}, sourceMap, { sources: sourceMap.sources.map(source => { - if (source.indexOf('!') !== -1) { - source = source.split('!').pop(); - } - if (source.indexOf('?') !== -1) { - source = source.split('?')[0]; - } - // Workaround for https://github.com/mattlewis92/karma-coverage-istanbul-reporter/issues/9 - if (isWin) { - source = source.replace(/\\/g, '/'); - } + source = fixWebpackFilePath(source); if (sourceMap.sourceRoot && source.startsWith(sourceMap.sourceRoot)) { source = source.replace(sourceMap.sourceRoot, ''); } @@ -22,3 +30,4 @@ function fixWebpackSourcePaths(sourceMap) { } module.exports.fixWebpackSourcePaths = fixWebpackSourcePaths; +module.exports.fixWebpackFilePath = fixWebpackFilePath; diff --git a/test/reporter.spec.js b/test/reporter.spec.js index 3bdd694..2496316 100644 --- a/test/reporter.spec.js +++ b/test/reporter.spec.js @@ -187,5 +187,48 @@ describe('karma-coverage-istanbul-reporter', () => { setTimeout(checkOutput, fileReadTimeout); // Hacky workaround to make sure the output file has been written }); }); + + it('should enforce per file thresholds', done => { + const server = createServer({ + coverageIstanbulReporter: { + reports: ['json-summary'], + dir: path.join(__dirname, 'fixtures', 'outputs'), + fixWebpackSourcePaths: true, + thresholds: { + global: { + statements: 50, + lines: 50, + branches: 50, + functions: 50 + }, + each: { + statements: 80, + lines: 80, + branches: 80, + functions: 60 + } + } + } + }); + server.start(); + + function checkOutput() { + const output = fs.readFileSync(OUTPUT_LOG_FILE).toString(); + expect(output).not.to.contain('[ERROR] reporter.coverage-istanbul - Coverage for statements (81.82%) does not meet global threshold (50%)'); + expect(output).not.to.contain('[ERROR] reporter.coverage-istanbul - Coverage for lines (81.82%) does not meet global threshold (50%)'); + expect(output).not.to.contain('[ERROR] reporter.coverage-istanbul - Coverage for functions (60%) does not meet global threshold (50%)'); + expect(Boolean(output.match(/\[ERROR\] reporter\.coverage-istanbul - Coverage for statements \(85\.71%\) in file \/.+test\/fixtures\/typescript\/src\/example\.ts does not meet per file threshold \(80%\)/))).to.equal(false); + expect(Boolean(output.match(/\[ERROR\] reporter\.coverage-istanbul - Coverage for lines \(85\.71%\) in file \/.+test\/fixtures\/typescript\/src\/example\.ts does not meet per file threshold \(80%\)/))).to.equal(false); + expect(Boolean(output.match(/\[ERROR\] reporter\.coverage-istanbul - Coverage for functions \(66\.67%\) in file \/.+test\/fixtures\/typescript\/src\/example\.ts does not meet per file threshold \(60%\)/))).to.equal(false); + expect(Boolean(output.match(/\[ERROR\] reporter\.coverage-istanbul - Coverage for statements \(75%\) in file \/.+test\/fixtures\/typescript\/src\/another-file\.ts does not meet per file threshold \(80%\)/))).not.to.equal(false); + expect(Boolean(output.match(/\[ERROR\] reporter\.coverage-istanbul - Coverage for lines \(75%\) in file \/.+test\/fixtures\/typescript\/src\/another-file\.ts does not meet per file threshold \(80%\)/))).not.to.equal(false); + expect(Boolean(output.match(/\[ERROR\] reporter\.coverage-istanbul - Coverage for functions \(50%\) in file \/.+test\/fixtures\/typescript\/src\/another-file\.ts does not meet per file threshold \(60%\)/))).not.to.equal(false); + done(); + } + + server.on('run_complete', () => { + setTimeout(checkOutput, fileReadTimeout); // Hacky workaround to make sure the output file has been written + }); + }); }); });