karma-coverage
Advanced tools
Comparing version 0.4.0 to 0.4.1
@@ -0,1 +1,10 @@ | ||
<a name"0.4.1"></a> | ||
### 0.4.1 (2015-06-09) | ||
#### Features | ||
* **preprocessor:** Add sourcemap support ([de3b738b](https://github.com/karma-runner/karma-coverage/commit/de3b738b), closes [#109](https://github.com/karma-runner/karma-coverage/issues/109)) | ||
* **reporter:** add check coverage thresholds ([bc63b158](https://github.com/karma-runner/karma-coverage/commit/bc63b158), closes [#21](https://github.com/karma-runner/karma-coverage/issues/21)) | ||
<a name"0.4.0"></a> | ||
@@ -16,11 +25,6 @@ ## 0.4.0 (2015-06-09) | ||
* | ||
Karma is no longer a `peerDependency` so it needs to be installed | ||
manually. | ||
* Karma is no longer a `peerDependency` so it needs to be installed | ||
manually. Ref https://github.com/karma-runner/integration-tests/issues/5 ([eebcc989](https://github.com/karma-runner/karma-coverage/commit/eebcc989)) | ||
Ref https://github.com/karma-runner/integration-tests/issues/5 | ||
([eebcc989](https://github.com/karma-runner/karma-coverage/commit/eebcc989)) | ||
<a name"0.3.1"></a> | ||
@@ -27,0 +31,0 @@ ### 0.3.1 (2015-06-09) |
@@ -0,1 +1,7 @@ | ||
// karma-coverage | ||
// ============== | ||
// | ||
// Main entry point for the karma-coverage module. | ||
// Exposes the preprocessor and reporter plugins. | ||
module.exports = { | ||
@@ -2,0 +8,0 @@ 'preprocessor:coverage': ['factory', require('./preprocessor')], |
@@ -1,20 +0,54 @@ | ||
var istanbul = require('istanbul'), | ||
minimatch = require('minimatch'), | ||
globalSourceCache = require('./sourceCache'), | ||
extend = require('util')._extend, | ||
coverageMap = require('./coverageMap') | ||
// Coverage Preprocessor | ||
// ===================== | ||
// | ||
// Depends on the the reporter to generate an actual report | ||
var createCoveragePreprocessor = function (logger, basePath, reporters, coverageReporter) { | ||
// Dependencies | ||
// ------------ | ||
var istanbul = require('istanbul') | ||
var minimatch = require('minimatch') | ||
var SourceMapConsumer = require('source-map').SourceMapConsumer | ||
var SourceMapGenerator = require('source-map').SourceMapGenerator | ||
var globalSourceCache = require('./source-cache') | ||
var extend = require('util')._extend | ||
var coverageMap = require('./coverage-map') | ||
// Regexes | ||
// ------- | ||
var coverageObjRegex = /\{.*"path".*"fnMap".*"statementMap".*"branchMap".*\}/g | ||
// Preprocessor creator function | ||
function createCoveragePreprocessor (logger, helper, basePath, reporters, coverageReporter) { | ||
var _ = helper._ | ||
var log = logger.create('preprocessor.coverage') | ||
var instrumenterOverrides = (coverageReporter && coverageReporter.instrumenter) || {} | ||
var instrumenters = extend({istanbul: istanbul}, (coverageReporter && coverageReporter.instrumenters)) | ||
var sourceCache = globalSourceCache.getByBasePath(basePath) | ||
var includeAllSources = coverageReporter && coverageReporter.includeAllSources === true | ||
var instrumentersOptions = Object.keys(instrumenters).reduce(function getInstumenterOptions (memo, instrumenterName) { | ||
memo[instrumenterName] = (coverageReporter && coverageReporter.instrumenterOptions && coverageReporter.instrumenterOptions[instrumenterName]) || {} | ||
return memo | ||
}, {}) | ||
// Options | ||
// ------- | ||
var instrumenterOverrides = {} | ||
var instrumenters = {istanbul: istanbul} | ||
var includeAllSources = false | ||
if (coverageReporter) { | ||
instrumenterOverrides = coverageReporter.instrumenter | ||
instrumenters = extend({istanbul: istanbul}, coverageReporter.instrumenters) | ||
includeAllSources = coverageReporter.includeAllSources === true | ||
} | ||
var sourceCache = globalSourceCache.get(basePath) | ||
var instrumentersOptions = _.reduce(instrumenters, function getInstumenterOptions (memo, instrument, name) { | ||
memo[name] = {} | ||
if (coverageReporter && coverageReporter.instrumenterOptions) { | ||
memo[name] = coverageReporter.instrumenterOptions[name] | ||
} | ||
return memo | ||
}, {}) | ||
// if coverage reporter is not used, do not preprocess the files | ||
if (reporters.indexOf('coverage') === -1) { | ||
if (!_.includes(reporters, 'coverage')) { | ||
return function (content, _, done) { | ||
@@ -27,12 +61,11 @@ done(content) | ||
function checkInstrumenters () { | ||
var literal | ||
for (var pattern in instrumenterOverrides) { | ||
literal = String(instrumenterOverrides[pattern]) | ||
if (Object.keys(instrumenters).indexOf(literal) < 0) { | ||
return _.reduce(instrumenterOverrides, function (acc, literal, pattern) { | ||
if (!_.contains(_.keys(instrumenters), String(literal))) { | ||
log.error('Unknown instrumenter: %s', literal) | ||
return false | ||
} | ||
} | ||
return true | ||
return acc | ||
}, true) | ||
} | ||
if (!checkInstrumenters()) { | ||
@@ -51,10 +84,28 @@ return function (content, _, done) { | ||
for (var pattern in instrumenterOverrides) { | ||
_.forEach(instrumenterOverrides, function (literal, pattern) { | ||
if (minimatch(file.originalPath, pattern, {dot: true})) { | ||
instrumenterLiteral = String(instrumenterOverrides[pattern]) | ||
instrumenterLiteral = String(literal) | ||
} | ||
}) | ||
var InstrumenterConstructor = instrumenters[instrumenterLiteral].Instrumenter | ||
var constructOptions = instrumentersOptions[instrumenterLiteral] || {} | ||
var codeGenerationOptions = null | ||
if (file.sourceMap) { | ||
log.debug('Enabling source map generation for "%s".', file.originalPath) | ||
codeGenerationOptions = extend({ | ||
format: { | ||
compact: !constructOptions.noCompact | ||
}, | ||
sourceMap: file.sourceMap.file, | ||
sourceMapWithCode: true, | ||
file: file.path | ||
}, constructOptions.codeGenerationOptions || {}) | ||
} | ||
var instrumenter = new instrumenters[instrumenterLiteral].Instrumenter(instrumentersOptions[instrumenterLiteral] || {}) | ||
var options = extend({}, constructOptions) | ||
options = extend(options, {codeGenerationOptions: codeGenerationOptions}) | ||
var instrumenter = new InstrumenterConstructor(options) | ||
instrumenter.instrument(content, jsPath, function (err, instrumentedCode) { | ||
@@ -65,2 +116,11 @@ if (err) { | ||
if (file.sourceMap && instrumenter.lastSourceMap()) { | ||
log.debug('Adding source map to instrumented file for "%s".', file.originalPath) | ||
var generator = SourceMapGenerator.fromSourceMap(new SourceMapConsumer(instrumenter.lastSourceMap().toString())) | ||
generator.applySourceMap(new SourceMapConsumer(file.sourceMap)) | ||
file.sourceMap = JSON.parse(generator.toString()) | ||
instrumentedCode += '\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,' | ||
instrumentedCode += new Buffer(JSON.stringify(file.sourceMap)).toString('base64') + '\n' | ||
} | ||
// remember the actual immediate instrumented JS for given original path | ||
@@ -70,3 +130,2 @@ sourceCache[jsPath] = content | ||
if (includeAllSources) { | ||
var coverageObjRegex = /\{.*"path".*"fnMap".*"statementMap".*"branchMap".*\}/g | ||
var coverageObjMatch = coverageObjRegex.exec(instrumentedCode) | ||
@@ -88,2 +147,3 @@ | ||
'logger', | ||
'helper', | ||
'config.basePath', | ||
@@ -90,0 +150,0 @@ 'config.reporters', |
@@ -0,32 +1,29 @@ | ||
// Coverage Reporter | ||
// Part of this code is based on [1], which is licensed under the New BSD License. | ||
// For more information see the See the accompanying LICENSE-istanbul file for terms. | ||
// | ||
// [1]: https://github.com/gotwarlost/istanbul/blob/master/lib/command/check-coverage.js | ||
// ===================== | ||
// | ||
// Generates the report | ||
// Dependencies | ||
// ------------ | ||
var path = require('path') | ||
var util = require('util') | ||
var istanbul = require('istanbul') | ||
var globalSourceCache = require('./sourceCache') | ||
var coverageMap = require('./coverageMap') | ||
var minimatch = require('minimatch') | ||
var Store = istanbul.Store | ||
var globalSourceCache = require('./source-cache') | ||
var coverageMap = require('./coverage-map') | ||
var SourceCacheStore = require('./source-cache-store') | ||
var SourceCacheStore = function (opts) { | ||
Store.call(this, opts) | ||
opts = opts || {} | ||
this.sourceCache = opts.sourceCache | ||
function isAbsolute (file) { | ||
if (path.isAbsolute) { | ||
return path.isAbsolute(file) | ||
} | ||
return path.resolve(file) === path.normalize(file) | ||
} | ||
SourceCacheStore.TYPE = 'sourceCacheLookup' | ||
util.inherits(SourceCacheStore, Store) | ||
Store.mix(SourceCacheStore, { | ||
keys: function () { | ||
throw new Error('Not implemented') | ||
}, | ||
get: function (key) { | ||
return this.sourceCache[key] | ||
}, | ||
hasKey: function (key) { | ||
return this.sourceCache.hasOwnProperty(key) | ||
}, | ||
set: function (key, contents) { | ||
throw new Error('Not applicable') | ||
} | ||
}) | ||
// TODO(vojta): inject only what required (config.basePath, config.coverageReporter) | ||
@@ -36,6 +33,15 @@ var CoverageReporter = function (rootConfig, helper, logger) { | ||
var log = logger.create('coverage') | ||
// Instance variables | ||
// ------------------ | ||
this.adapters = [] | ||
// Options | ||
// ------- | ||
var config = rootConfig.coverageReporter || {} | ||
var basePath = rootConfig.basePath | ||
var reporters = config.reporters | ||
var sourceCache = globalSourceCache.getByBasePath(basePath) | ||
var sourceCache = globalSourceCache.get(basePath) | ||
var includeAllSources = config.includeAllSources === true | ||
@@ -51,3 +57,2 @@ | ||
this.adapters = [] | ||
var collectors | ||
@@ -69,5 +74,6 @@ var pendingFileWritings = 0 | ||
if (pendingFileWritings <= 0) { | ||
Object.keys(collectors).forEach(function (key) { | ||
collectors[key].dispose() | ||
_.forEach(collectors, function (collector) { | ||
collector.dispose() | ||
}) | ||
fileWritingFinished() | ||
@@ -77,12 +83,114 @@ } | ||
/** | ||
* Generate the output directory from the `coverageReporter.dir` and | ||
* `coverageReporter.subdir` options. | ||
* | ||
* @param {String} browserName - The browser name | ||
* @param {String} dir - The given option | ||
* @param {String|Function} subdir - The given option | ||
* | ||
* @return {String} - The output directory | ||
*/ | ||
function normalize (key) { | ||
// Exclude keys will always be relative, but covObj keys can be absolute or relative | ||
var excludeKey = isAbsolute(key) ? path.relative(basePath, key) : key | ||
// Also normalize for files that start with `./`, etc. | ||
excludeKey = path.normalize(excludeKey) | ||
return excludeKey | ||
} | ||
function removeFiles (covObj, patterns) { | ||
var obj = {} | ||
Object.keys(covObj).forEach(function (key) { | ||
// Do any patterns match the resolved key | ||
var found = patterns.some(function (pattern) { | ||
return minimatch(normalize(key), pattern, {dot: true}) | ||
}) | ||
// if no patterns match, keep the key | ||
if (!found) { | ||
obj[key] = covObj[key] | ||
} | ||
}) | ||
return obj | ||
} | ||
function overrideThresholds (key, overrides) { | ||
var thresholds = {} | ||
// First match wins | ||
Object.keys(overrides).some(function (pattern) { | ||
if (minimatch(normalize(key), pattern, {dot: true})) { | ||
thresholds = overrides[pattern] | ||
return true | ||
} | ||
}) | ||
return thresholds | ||
} | ||
function checkCoverage (browser, collector) { | ||
var defaultThresholds = { | ||
global: { | ||
statements: 0, | ||
branches: 0, | ||
lines: 0, | ||
functions: 0, | ||
excludes: [] | ||
}, | ||
each: { | ||
statements: 0, | ||
branches: 0, | ||
lines: 0, | ||
functions: 0, | ||
excludes: [], | ||
overrides: {} | ||
} | ||
} | ||
var thresholds = helper.merge({}, defaultThresholds, config.check) | ||
var rawCoverage = collector.getFinalCoverage() | ||
var globalResults = istanbul.utils.summarizeCoverage(removeFiles(rawCoverage, thresholds.global.excludes)) | ||
var eachResults = removeFiles(rawCoverage, thresholds.each.excludes) | ||
// Summarize per-file results and mutate original results. | ||
Object.keys(eachResults).forEach(function (key) { | ||
eachResults[key] = istanbul.utils.summarizeFileCoverage(eachResults[key]) | ||
}) | ||
var coverageFailed = false | ||
function check (name, thresholds, actuals) { | ||
[ | ||
'statements', | ||
'branches', | ||
'lines', | ||
'functions' | ||
].forEach(function (key) { | ||
var actual = actuals[key].pct | ||
var actualUncovered = actuals[key].total - actuals[key].covered | ||
var threshold = thresholds[key] | ||
if (threshold < 0) { | ||
if (threshold * -1 < actualUncovered) { | ||
coverageFailed = true | ||
log.error(browser.name + ': Uncovered count for ' + key + ' (' + actualUncovered + | ||
') exceeds ' + name + ' threshold (' + -1 * threshold + ')') | ||
} | ||
} else { | ||
if (actual < threshold) { | ||
coverageFailed = true | ||
log.error(browser.name + ': Coverage for ' + key + ' (' + actual + | ||
'%) does not meet ' + name + ' threshold (' + threshold + '%)') | ||
} | ||
} | ||
}) | ||
} | ||
check('global', thresholds.global, globalResults) | ||
Object.keys(eachResults).forEach(function (key) { | ||
var keyThreshold = helper.merge(thresholds.each, overrideThresholds(key, thresholds.each.overrides)) | ||
check('per-file' + ' (' + key + ') ', keyThreshold, eachResults[key]) | ||
}) | ||
return coverageFailed | ||
} | ||
// Generate the output directory from the `coverageReporter.dir` and | ||
// `coverageReporter.subdir` options. | ||
function generateOutputDir (browserName, dir, subdir) { | ||
@@ -92,3 +200,3 @@ dir = dir || 'coverage' | ||
if (typeof subdir === 'function') { | ||
if (_.isFunction(subdir)) { | ||
subdir = subdir(browserName) | ||
@@ -111,5 +219,6 @@ } | ||
collectors[browser.id] = new istanbul.Collector() | ||
if (includeAllSources) { | ||
collectors[browser.id].add(coverageMap.get()) | ||
} | ||
if (!includeAllSources) return | ||
collectors[browser.id].add(coverageMap.get()) | ||
} | ||
@@ -120,52 +229,68 @@ | ||
if (!collector) { | ||
return | ||
} | ||
if (!collector) return | ||
if (!result || !result.coverage) return | ||
if (result && result.coverage) { | ||
collector.add(result.coverage) | ||
} | ||
collector.add(result.coverage) | ||
} | ||
this.onSpecComplete = function (browser, result) { | ||
if (result.coverage) { | ||
collectors[browser.id].add(result.coverage) | ||
} | ||
if (!result.coverage) return | ||
collectors[browser.id].add(result.coverage) | ||
} | ||
this.onRunComplete = function (browsers) { | ||
this.onRunComplete = function (browsers, results) { | ||
var checkedCoverage = {} | ||
reporters.forEach(function (reporterConfig) { | ||
browsers.forEach(function (browser) { | ||
var collector = collectors[browser.id] | ||
if (collector) { | ||
pendingFileWritings++ | ||
var mainDir = reporterConfig.dir || config.dir | ||
var subDir = reporterConfig.subdir || config.subdir | ||
var simpleOutputDir = generateOutputDir(browser.name, mainDir, subDir) | ||
var resolvedOutputDir = path.resolve(basePath, simpleOutputDir) | ||
if (!collector) { | ||
return | ||
} | ||
var outputDir = helper.normalizeWinPath(resolvedOutputDir) | ||
var options = helper.merge({ | ||
sourceStore: _.isEmpty(sourceCache) ? null : new SourceCacheStore({ | ||
sourceCache: sourceCache | ||
}) | ||
}, config, reporterConfig, { | ||
dir: outputDir | ||
}) | ||
var reporter = istanbul.Report.create(reporterConfig.type || 'html', options) | ||
// If reporting to console, skip directory creation | ||
if (reporterConfig.type && reporterConfig.type.match(/^(text|text-summary)$/) && typeof reporterConfig.file === 'undefined') { | ||
writeReport(reporter, collector) | ||
return | ||
// If config.check is defined, check coverage levels for each browser | ||
if (config.hasOwnProperty('check') && !checkedCoverage[browser.id]) { | ||
checkedCoverage[browser.id] = true | ||
var coverageFailed = checkCoverage(browser, collector) | ||
if (coverageFailed) { | ||
if (results) { | ||
results.exitCode = 1 | ||
} | ||
} | ||
} | ||
helper.mkdirIfNotExists(outputDir, function () { | ||
log.debug('Writing coverage to %s', outputDir) | ||
writeReport(reporter, collector) | ||
disposeCollectors() | ||
}) | ||
pendingFileWritings++ | ||
var mainDir = reporterConfig.dir || config.dir | ||
var subDir = reporterConfig.subdir || config.subdir | ||
var simpleOutputDir = generateOutputDir(browser.name, mainDir, subDir) | ||
var resolvedOutputDir = path.resolve(basePath, simpleOutputDir) | ||
var outputDir = helper.normalizeWinPath(resolvedOutputDir) | ||
var sourceStore = _.isEmpty(sourceCache) ? null : new SourceCacheStore({ | ||
sourceCache: sourceCache | ||
}) | ||
var options = helper.merge({ | ||
sourceStore: sourceStore | ||
}, config, reporterConfig, { | ||
dir: outputDir | ||
}) | ||
var reporter = istanbul.Report.create(reporterConfig.type || 'html', options) | ||
// If reporting to console, skip directory creation | ||
var isConsole = reporterConfig.type && reporterConfig.type.match(/^(text|text-summary)$/) | ||
var hasNoFile = _.isUndefined(reporterConfig.file) | ||
if (isConsole && hasNoFile) { | ||
writeReport(reporter, collector) | ||
return | ||
} | ||
helper.mkdirIfNotExists(outputDir, function () { | ||
log.debug('Writing coverage to %s', outputDir) | ||
writeReport(reporter, collector) | ||
disposeCollectors() | ||
}) | ||
}) | ||
@@ -172,0 +297,0 @@ }) |
{ | ||
"name": "karma-coverage", | ||
"version": "0.4.0", | ||
"version": "0.4.1", | ||
"description": "A Karma plugin. Generate code coverage.", | ||
@@ -22,5 +22,6 @@ "main": "lib/index.js", | ||
"dependencies": { | ||
"istanbul": "^0.3.0", | ||
"istanbul": "^0.3.15", | ||
"dateformat": "^1.0.6", | ||
"minimatch": "^2.0.8" | ||
"minimatch": "^2.0.8", | ||
"source-map": "^0.4.2" | ||
}, | ||
@@ -51,8 +52,7 @@ "license": "MIT", | ||
"Maksim Ryzhikov <rv.maksim@gmail.com>", | ||
"Nick Malaguti <nmalaguti@palantir.com>", | ||
"Allen Bierbaum <abierbaum@gmail.com>", | ||
"Matt Winchester <m_winche@yahoo.com>", | ||
"Wei Kin Huang <weikin.huang04@gmail.com>", | ||
"Douglas Duteil <douglasduteil@gmail.com>", | ||
"Wei Kin Huang <weikin.huang04@gmail.com>", | ||
"Michael Stramel <m.stramel89@gmail.com>", | ||
"Nick Malaguti <nmalaguti@palantir.com>", | ||
"Nick Matantsev <nick.matantsev@gmail.com>", | ||
@@ -79,4 +79,5 @@ "Petar Manev <petar.manev2010@gmail.com>", | ||
"Maciej Rzepiński <maciej.rzepinski@gmail.com>", | ||
"Mark Ethan Trostler <mark@zzo.com>" | ||
"Mark Ethan Trostler <mark@zzo.com>", | ||
"Michael Stramel <m.stramel89@gmail.com>" | ||
] | ||
} |
@@ -193,2 +193,41 @@ # karma-coverage | ||
#### check | ||
**Type:** Object | ||
**Description:** This will be used to configure minimum threshold enforcement for coverage results. If the thresholds are not met, karma will return failure. Thresholds, when specified as a positive number are taken to be the minimum percentage required. When a threshold is specified as a negative number it represents the maximum number of uncovered entities allowed. | ||
For example, `statements: 90` implies minimum statement coverage is 90%. `statements: -10` implies that no more than 10 uncovered statements are allowed. | ||
`global` applies to all files together and `each` on a per-file basis. A list of files or patterns can be excluded from enforcement via the `exclude` property. On a per-file or pattern basis, per-file thresholds can be overridden via the `overrides` property. | ||
```javascript | ||
coverageReporter: { | ||
check: { | ||
global: { | ||
statements: 50, | ||
branches: 50, | ||
functions: 50, | ||
lines: 50, | ||
excludes: [ | ||
'foo/bar/**/*.js' | ||
] | ||
}, | ||
each: { | ||
statements: 50, | ||
branches: 50, | ||
functions: 50, | ||
lines: 50, | ||
excludes: [ | ||
'other/directory/**/*.js' | ||
], | ||
overrides: { | ||
'baz/component/**/*.js': { | ||
statements: 98 | ||
} | ||
} | ||
} | ||
} | ||
} | ||
``` | ||
#### watermarks | ||
@@ -195,0 +234,0 @@ **Type:** Object |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Mixed license
License(Experimental) Package contains multiple licenses.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
37777
12
486
338
0
4
1
+ Addedsource-map@^0.4.2
+ Addedsource-map@0.4.4(transitive)
Updatedistanbul@^0.3.15