Skip to content

Commit

Permalink
chore(deps): Use latest istanbul lib packages (#377)
Browse files Browse the repository at this point in the history
BREAKING CHANGE:

This set of changes may impact some use cases.

* chore: Add Updated Istanbul Dependencies

The istanbul package is deprecated in favor several split packages that
control different aspects of how istanbul works. This commit adds the
recommended packages that will be used in future commits as
karma-coverage's usage of istanbul is updated to the latest api.

* refactor(reporter): Follow new report API

This commit refactors the in memory report implementation to use the new
istanbul report API.

Report creation is removed from newer versions of the istanbul API, so
this commit adds a set of utility functions to wrap around the new API
and provide similar functionality as the old API. The top level export
uses the new utility function to register the in-memory report.

* refactor(preprocessor): Switch to istanbul-lib-instrument

This commit updates the preprocessor to use istanbul-lib-instrument
instead of the deprecated istanbul package. The biggest change in this
refactor is using a callable function instead of a constructor when
creating instrumenters

The old istanbul package exposed the Instrumenter directly, allowing the
preprocessor to create an instance of it. istanbul-lib-instrument,
however, exposes a callable function that creates an Instrumenter.
This commit updates the preprocessor to follow this new pattern of using
a callable function. In order to ensure backwards compatibility, a
utility function is added to wrap constructors with a callable function
for creation automatically.

This change allows the following configuration for creating instrumenters:
1. An object that contains an Instrumenter constructor
2. An Instrumenter constructor itself
3. A callable function that returns an Instrumenter instance.

This commit also uses the istanbul-lib-source-maps package to handle
storing source maps. A global source map store registers source maps so
they can be used later on in the reporter.

* refactor(reporter): Switch to istanbul-lib-coverage

This commit updates the reporter by using the istanbul-lib-coverage
package api for handling coverage checking/management and the
istanbul-lib-report package api for handling reporting.

The new apis remove the need for collectors and remove the need to
handle disposing collectors.

* refactor: Remove unused source cache utilities

This commit removes the source-cache-store and source-cache files as
they are no longer being used. The source-map-store and
istanbul-lib-source-maps are used instead, so these files are no longer
needed.

* feat(util): Add Reset Functionality

This commit updates the report creator utility to allow resetting the
custom reporter map.

* fix(preprocessor): Track Coverage Maps Properly

This commit updates the preprocessor to properly access file coverage
when storing it in the global coverage map (when includeAllSources is
true). The previous method did not work because the returned
instrumented code from the default istanbul instrumenter returns the
coverage map in a POJO object instead of JSON notation. This breaks the
coverage regex used to match and parse the coverage map.

The istanbul instrumenter offers the ability to receive the coverage map
for the last instrumented file through a separate function, so that is
tested for and used if it is supported. The original method is used as a
fallback for backwards compatibility.

This commit also addresses changes from the v0 instanbul instrumenter
options. The changes are additive only to maintain backwards compatibility
for other instrumenters.

* fix(reporter): Access Data Properly to Check Coverage

This commit fixes errors with accessing data properly during the
checkCoverage method. A previous commit updated the implementation to
use istanbul-lib-coverage, but this involved an api change to access the
raw coverage data (which checkCoverage uses).

This commit also fixes the checking coverage for each file by using a
map to store file coverage summaries instead of merging summaries like
the global results. Per file coverage now works as expected.

* test: Update Unit Tests to use new Istanbul API

This commit updates the mocking done in unit tests to properly mock the
new istanbul API. Additionally, new unit test suites are added for the
utility methods report-creator and source-map-store.
  • Loading branch information
pr1sm authored and johnjbarton committed Jun 19, 2019
1 parent b8f82a0 commit f40d6af
Show file tree
Hide file tree
Showing 14 changed files with 440 additions and 339 deletions.
17 changes: 11 additions & 6 deletions lib/in-memory-report.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
var Report = require('istanbul').Report
var util = require('util')

function InMemoryReport (opt) {
this.opt = opt
}

util.inherits(InMemoryReport, Report)
InMemoryReport.prototype.onStart = function (root, context) {
this.data = {}
}

InMemoryReport.prototype.onDetail = function (node) {
const fc = node.getFileCoverage()
const key = fc.path
this.data[key] = fc.toJSON()
}

InMemoryReport.prototype.writeReport = function (collector, sync) {
InMemoryReport.prototype.onEnd = function () {
if (!this.opt.emitter || !this.opt.emitter.emit) {
console.error('Could not raise "coverage_complete" event, missing emitter because it was not supplied during initialization of the reporter')
} else {
this.opt.emitter.emit('coverage_complete', this.opt.browser, collector.getFinalCoverage())
this.opt.emitter.emit('coverage_complete', this.opt.browser, this.data)
}
}

Expand Down
2 changes: 1 addition & 1 deletion lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// Exposes the preprocessor and reporter plugins.

// Registering one additional (karma specific) reporter: in-memory
require('istanbul').Report.register(require('./in-memory-report'))
require('./report-creator').register(require('./in-memory-report'))

module.exports = {
'preprocessor:coverage': ['factory', require('./preprocessor')],
Expand Down
89 changes: 68 additions & 21 deletions lib/preprocessor.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
// Dependencies
// ------------

var istanbul = require('istanbul')
var { createInstrumenter } = require('istanbul-lib-instrument')
var minimatch = require('minimatch')
var path = require('path')
var _ = require('lodash')
var SourceMapConsumer = require('source-map').SourceMapConsumer
var SourceMapGenerator = require('source-map').SourceMapGenerator
var globalSourceCache = require('./source-cache')
var coverageMap = require('./coverage-map')
var globalSourceMapStore = require('./source-map-store')
var globalCoverageMap = require('./coverage-map')

// Regexes
// -------
Expand All @@ -27,21 +27,60 @@ function createCoveragePreprocessor (logger, helper, basePath, reporters, covera
// Options
// -------

function isConstructor (Func) {
try {
// eslint-disable-next-line
new Func()
} catch (err) {
// error message should be of the form: "TypeError: func is not a constructor"
// test for this type of message to ensure we failed due to the function not being
// constructable
if (/TypeError.*constructor/.test(err.message)) {
return false
}
}
return true
}

function getCreatorFunction (Obj) {
if (Obj.Instrumenter) {
return function (opts) {
return new Obj.Instrumenter(opts)
}
}
if (!_.isFunction(Obj)) {
// Object doesn't have old instrumenter variable and isn't a
// constructor, so we can't use it to create an instrumenter
return null
}
if (isConstructor(Obj)) {
return function (opts) {
return new Obj(opts)
}
}
return Obj
}

var instrumenterOverrides = {}
var instrumenters = { istanbul: istanbul }
var instrumenters = { istanbul: createInstrumenter }
var includeAllSources = false
var useJSExtensionForCoffeeScript = false

if (coverageReporter) {
instrumenterOverrides = coverageReporter.instrumenter
instrumenters = Object.assign({}, { istanbul: istanbul }, coverageReporter.instrumenters)
_.forEach(coverageReporter.instrumenters, function (instrumenter, literal) {
var creatorFunction = getCreatorFunction(instrumenter)
if (creatorFunction) {
instrumenters[literal] = creatorFunction
}
})
includeAllSources = coverageReporter.includeAllSources === true
useJSExtensionForCoffeeScript = coverageReporter.useJSExtensionForCoffeeScript === true
}

var sourceCache = globalSourceCache.get(basePath)
var sourceMapStore = globalSourceMapStore.get(basePath)

var instrumentersOptions = _.reduce(instrumenters, function getInstumenterOptions (memo, instrument, name) {
var instrumentersOptions = _.reduce(instrumenters, function getInstrumenterOptions (memo, instrument, name) {
memo[name] = {}

if (coverageReporter && coverageReporter.instrumenterOptions) {
Expand Down Expand Up @@ -88,9 +127,11 @@ function createCoveragePreprocessor (logger, helper, basePath, reporters, covera
}
})

var InstrumenterConstructor = instrumenters[instrumenterLiteral].Instrumenter
var instrumenterCreator = instrumenters[instrumenterLiteral]
var constructOptions = instrumentersOptions[instrumenterLiteral] || {}
var options = Object.assign({}, constructOptions)
var codeGenerationOptions = null
options.autoWrap = options.autoWrap || !options.noAutoWrap

if (file.sourceMap) {
log.debug('Enabling source map generation for "%s".', file.originalPath)
Expand All @@ -102,12 +143,12 @@ function createCoveragePreprocessor (logger, helper, basePath, reporters, covera
sourceMapWithCode: true,
file: file.path
}, constructOptions.codeGenerationOptions || {})
options.produceSourceMap = true
}

var options = Object.assign({}, constructOptions)
options = Object.assign({}, options, { codeGenerationOptions: codeGenerationOptions })

var instrumenter = new InstrumenterConstructor(options)
var instrumenter = instrumenterCreator(options)
instrumenter.instrument(content, jsPath, function (err, instrumentedCode) {
if (err) {
log.error('%s\n at %s', err.message, file.originalPath)
Expand All @@ -122,19 +163,25 @@ function createCoveragePreprocessor (logger, helper, basePath, reporters, covera
instrumentedCode += Buffer.from(JSON.stringify(file.sourceMap)).toString('base64') + '\n'
}

// remember the actual immediate instrumented JS for given original path
sourceCache[jsPath] = content
// Register the sourceMap for transformation during reporting
sourceMapStore.registerMap(jsPath, file.sourceMap)

if (includeAllSources) {
// reset stateful regex
coverageObjRegex.lastIndex = 0

var coverageObjMatch = coverageObjRegex.exec(instrumentedCode)

if (coverageObjMatch !== null) {
var coverageObj = JSON.parse(coverageObjMatch[0])

coverageMap.add(coverageObj)
var coverageObj
// Check if the file coverage object is exposed from the instrumenter directly
if (instrumenter.lastFileCoverage) {
coverageObj = instrumenter.lastFileCoverage()
globalCoverageMap.add(coverageObj)
} else {
// Attempt to match and parse coverage object from instrumented code

// reset stateful regex
coverageObjRegex.lastIndex = 0
var coverageObjMatch = coverageObjRegex.exec(instrumentedCode)
if (coverageObjMatch !== null) {
coverageObj = JSON.parse(coverageObjMatch[0])
globalCoverageMap.add(coverageObj)
}
}
}

Expand Down
41 changes: 41 additions & 0 deletions lib/report-creator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Report Creator
// ==============
//
// Wrapper of Istanbul's report creator to allow registering
// custom reporters

// Dependencies
// ------------
var istanbulReports = require('istanbul-reports')

var customReporterMap = {}

function register (reporter) {
var registeredType = reporter.TYPE
if (!registeredType) {
throw new Error('Registering a custom reporter requires a type!')
}

customReporterMap[registeredType] = reporter
return registeredType
}

function create (type, opts) {
var Reporter = customReporterMap[type]
if (Reporter) {
return new Reporter(opts)
}

// fallback to istanbul's report creator if reporter isn't found
return istanbulReports.create(type, opts)
}

function reset () {
customReporterMap = {}
}

module.exports = {
create: create,
register: register,
reset: reset
}
Loading

0 comments on commit f40d6af

Please sign in to comment.