diff --git a/lib/in-memory-report.js b/lib/in-memory-report.js index f938c6a..0fac999 100644 --- a/lib/in-memory-report.js +++ b/lib/in-memory-report.js @@ -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) } } diff --git a/lib/index.js b/lib/index.js index 2f34cc6..ff9df46 100644 --- a/lib/index.js +++ b/lib/index.js @@ -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')], diff --git a/lib/preprocessor.js b/lib/preprocessor.js index 3bee536..5f51b46 100644 --- a/lib/preprocessor.js +++ b/lib/preprocessor.js @@ -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 // ------- @@ -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) { @@ -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) @@ -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) @@ -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) + } } } diff --git a/lib/report-creator.js b/lib/report-creator.js new file mode 100644 index 0000000..3bb7999 --- /dev/null +++ b/lib/report-creator.js @@ -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 +} diff --git a/lib/reporter.js b/lib/reporter.js index 72980f7..9ab9321 100644 --- a/lib/reporter.js +++ b/lib/reporter.js @@ -11,13 +11,14 @@ // ------------ var path = require('path') -var istanbul = require('istanbul') +var istanbulLibCoverage = require('istanbul-lib-coverage') +var istanbulLibReport = require('istanbul-lib-report') var minimatch = require('minimatch') var _ = require('lodash') -var globalSourceCache = require('./source-cache') -var coverageMap = require('./coverage-map') -var SourceCacheStore = require('./source-cache-store') +var globalSourceMapStore = require('./source-map-store') +var globalCoverageMap = require('./coverage-map') +var reports = require('./report-creator') function isAbsolute (file) { if (path.isAbsolute) { @@ -42,46 +43,18 @@ var CoverageReporter = function (rootConfig, helper, logger, emitter) { var config = rootConfig.coverageReporter || {} var basePath = rootConfig.basePath var reporters = config.reporters - var sourceCache = globalSourceCache.get(basePath) + var sourceMapStore = globalSourceMapStore.get(basePath) var includeAllSources = config.includeAllSources === true if (config.watermarks) { - config.watermarks = helper.merge({}, istanbul.config.defaultConfig().reporting.watermarks, config.watermarks) + config.watermarks = helper.merge({}, istanbulLibReport.getDefaultWatermarks(), config.watermarks) } if (!helper.isDefined(reporters)) { reporters = [config] } - var collectors - var pendingFileWritings = 0 - var fileWritingFinished = function () {} - - function writeReport (reporter, collector) { - try { - if (typeof config._onWriteReport === 'function') { - var newCollector = config._onWriteReport(collector) - if (typeof newCollector === 'object') { - collector = newCollector - } - } - reporter.writeReport(collector, true) - } catch (e) { - log.error(e) - } - - --pendingFileWritings - } - - function disposeCollectors () { - if (pendingFileWritings <= 0) { - _.forEach(collectors, function (collector) { - collector.dispose() - }) - - fileWritingFinished() - } - } + var coverageMaps function normalize (key) { // Exclude keys will always be relative, but covObj keys can be absolute or relative @@ -92,10 +65,10 @@ var CoverageReporter = function (rootConfig, helper, logger, emitter) { return excludeKey } - function removeFiles (covObj, patterns) { - var obj = {} + function getTrackedFiles (coverageMap, patterns) { + var files = [] - Object.keys(covObj).forEach(function (key) { + coverageMap.files().forEach(function (key) { // Do any patterns match the resolved key var found = patterns.some(function (pattern) { return minimatch(normalize(key), pattern, { dot: true }) @@ -103,11 +76,11 @@ var CoverageReporter = function (rootConfig, helper, logger, emitter) { // if no patterns match, keep the key if (!found) { - obj[key] = covObj[key] + files.push(key) } }) - return obj + return files } function overrideThresholds (key, overrides) { @@ -124,7 +97,7 @@ var CoverageReporter = function (rootConfig, helper, logger, emitter) { return thresholds } - function checkCoverage (browser, collector) { + function checkCoverage (browser, coverageMap) { var defaultThresholds = { global: { statements: 0, @@ -145,13 +118,19 @@ var CoverageReporter = function (rootConfig, helper, logger, emitter) { 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 globalTrackedFiles = getTrackedFiles(coverageMap, thresholds.global.excludes) + var eachTrackedFiles = getTrackedFiles(coverageMap, thresholds.each.excludes) + var globalResults = istanbulLibCoverage.createCoverageSummary() + var eachResults = {} + globalTrackedFiles.forEach(function (f) { + var fileCoverage = coverageMap.fileCoverageFor(f) + var summary = fileCoverage.toSummary() + globalResults.merge(summary) + }) + eachTrackedFiles.forEach(function (f) { + var fileCoverage = coverageMap.fileCoverageFor(f) + var summary = fileCoverage.toSummary() + eachResults[f] = summary }) var coverageFailed = false @@ -185,11 +164,11 @@ var CoverageReporter = function (rootConfig, helper, logger, emitter) { }) } - check('global', thresholds.global, globalResults) + check('global', thresholds.global, globalResults.toJSON()) - Object.keys(eachResults).forEach(function (key) { + eachTrackedFiles.forEach(function (key) { var keyThreshold = helper.merge(thresholds.each, overrideThresholds(key, thresholds.each.overrides)) - check('per-file' + ' (' + key + ') ', keyThreshold, eachResults[key]) + check('per-file' + ' (' + key + ') ', keyThreshold, eachResults[key].toJSON()) }) return coverageFailed @@ -209,7 +188,7 @@ var CoverageReporter = function (rootConfig, helper, logger, emitter) { } this.onRunStart = function (browsers) { - collectors = Object.create(null) + coverageMaps = Object.create(null) // TODO(vojta): remove once we don't care about Karma 0.10 if (browsers) { @@ -218,26 +197,30 @@ var CoverageReporter = function (rootConfig, helper, logger, emitter) { } this.onBrowserStart = function (browser) { - collectors[browser.id] = new istanbul.Collector() - - if (!includeAllSources) return + var startingMap = {} + if (includeAllSources) { + startingMap = globalCoverageMap.get() + } - collectors[browser.id].add(coverageMap.get()) + coverageMaps[browser.id] = istanbulLibCoverage.createCoverageMap(startingMap) } this.onBrowserComplete = function (browser, result) { - var collector = collectors[browser.id] + var coverageMap = coverageMaps[browser.id] - if (!collector) return + if (!coverageMap) return if (!result || !result.coverage) return - collector.add(result.coverage) + coverageMap.merge(result.coverage) } this.onSpecComplete = function (browser, result) { + var coverageMap = coverageMaps[browser.id] + + if (!coverageMap) return if (!result.coverage) return - collectors[browser.id].add(result.coverage) + coverageMap.merge(result.coverage) } this.onRunComplete = function (browsers, results) { @@ -245,25 +228,12 @@ var CoverageReporter = function (rootConfig, helper, logger, emitter) { reporters.forEach(function (reporterConfig) { browsers.forEach(function (browser) { - var collector = collectors[browser.id] + var coverageMap = coverageMaps[browser.id] - if (!collector) { + if (!coverageMap) { 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 - } - } - } - - pendingFileWritings++ - var mainDir = reporterConfig.dir || config.dir var subDir = reporterConfig.subdir || config.subdir var browserName = browser.name.replace(':', '') @@ -271,47 +241,51 @@ var CoverageReporter = function (rootConfig, helper, logger, emitter) { 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, { + var options = helper.merge(config, reporterConfig, { dir: outputDir, + subdir: '', browser: browser, emitter: emitter }) - var reporter = istanbul.Report.create(reporterConfig.type || 'html', options) + var remappedCoverageMap = sourceMapStore.transformCoverage(coverageMap).map + + // 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, remappedCoverageMap) + if (coverageFailed) { + if (results) { + results.exitCode = 1 + } + } + } - // If reporting to console or in-memory skip directory creation + var context = istanbulLibReport.createContext(options) + var tree = istanbulLibReport.summarizers.pkg(remappedCoverageMap) + var report = reports.create(reporterConfig.type || 'html', options) + + // // If reporting to console or in-memory skip directory creation var toDisk = !reporterConfig.type || !reporterConfig.type.match(/^(text|text-summary|in-memory)$/) var hasNoFile = _.isUndefined(reporterConfig.file) if (!toDisk && hasNoFile) { - writeReport(reporter, collector) + tree.visit(report, context) return } helper.mkdirIfNotExists(outputDir, function () { log.debug('Writing coverage to %s', outputDir) - writeReport(reporter, collector) - disposeCollectors() + tree.visit(report, context) }) }) }) - - disposeCollectors() } this.onExit = function (done) { - if (pendingFileWritings) { - fileWritingFinished = ( - typeof config._onExit === 'function' - ? (function (done) { return function () { config._onExit(done) } }(done)) - : done - ) + if (typeof config._onExit === 'function') { + config._onExit(done) } else { - (typeof config._onExit === 'function' ? config._onExit(done) : done()) + done() } } } diff --git a/lib/source-cache-store.js b/lib/source-cache-store.js deleted file mode 100644 index 5149b45..0000000 --- a/lib/source-cache-store.js +++ /dev/null @@ -1,40 +0,0 @@ -// Source Cache Store -// ================== -// -// Used by lib/reporter - -// Dependencies -// ------------ - -var util = require('util') -var Store = require('istanbul').Store - -// Constructor -var SourceCacheStore = module.exports = function (opts) { - Store.call(this, opts) - opts = opts || {} - this.sourceCache = opts.sourceCache -} - -// Class Constants -// --------------- -SourceCacheStore.TYPE = 'sourceCacheLookup' - -// Inherits from an Istanbul.Store -util.inherits(SourceCacheStore, Store) - -// Implement needed methods -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') - } -}) diff --git a/lib/source-cache.js b/lib/source-cache.js deleted file mode 100644 index 2de3e81..0000000 --- a/lib/source-cache.js +++ /dev/null @@ -1,16 +0,0 @@ -// Source Cache -// ============ - -var cache = {} - -function get (basePath) { - if (!cache[basePath]) { - cache[basePath] = {} - } - - return cache[basePath] -} - -module.exports = { - get: get -} diff --git a/lib/source-map-store.js b/lib/source-map-store.js new file mode 100644 index 0000000..8b1b1c4 --- /dev/null +++ b/lib/source-map-store.js @@ -0,0 +1,14 @@ +var istanbulLibSourceMaps = require('istanbul-lib-source-maps') + +var cache = {} + +function get (basePath, opts) { + if (!cache[basePath]) { + cache[basePath] = istanbulLibSourceMaps.createSourceMapStore(opts) + } + return cache[basePath] +} + +module.exports = { + get: get +} diff --git a/package.json b/package.json index a095d0e..c403a19 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,11 @@ "dependencies": { "dateformat": "^1.0.6", "istanbul": "^0.4.0", + "istanbul-lib-coverage": "^2.0.5", + "istanbul-lib-instrument": "^3.3.0", + "istanbul-lib-report": "^2.0.8", + "istanbul-lib-source-maps": "^3.0.6", + "istanbul-reports": "^2.2.4", "lodash": "^4.17.11", "minimatch": "^3.0.0", "source-map": "^0.5.1" diff --git a/test/in-memory-report.spec.coffee b/test/in-memory-report.spec.coffee index 01e4a7d..c22feff 100644 --- a/test/in-memory-report.spec.coffee +++ b/test/in-memory-report.spec.coffee @@ -1,19 +1,24 @@ InMemoryReport = require '../lib/in-memory-report' -istanbul = require 'istanbul'; describe 'InMemoryReport', -> emitter = emit: sinon.stub() browser = { name: 'firefox' } - result = { coverage: 'result' } - collector = - getFinalCoverage: sinon.stub().returns result + result = { test: { data: 'result' } } + fc = { + path: 'test' + toJSON: sinon.stub().returns { data: 'result' } + } + node = getFileCoverage: sinon.stub().returns fc it 'should raise an "coverage_complete" event.', -> sut = new InMemoryReport { browser: browser, emitter: emitter} - sut.writeReport collector - expect(collector.getFinalCoverage).to.have.been.called + sut.onStart() + sut.onDetail(node) + sut.onEnd() + expect(node.getFileCoverage).to.have.been.called + expect(fc.toJSON).to.have.been.called expect(emitter.emit).to.have.been.calledWith('coverage_complete', browser, result) it 'should be of type "in-memory"', -> @@ -21,6 +26,3 @@ describe 'InMemoryReport', -> it 'should not fail when created without arguments', -> expect(new InMemoryReport()).to.be.ok - - it 'should inherit from Report', -> - expect(new InMemoryReport()).to.be.an.instanceof(istanbul.Report) \ No newline at end of file diff --git a/test/index.spec.coffee b/test/index.spec.coffee index a68d977..d929ec8 100644 --- a/test/index.spec.coffee +++ b/test/index.spec.coffee @@ -1,7 +1,7 @@ index = require '../lib/index' InMemoryReport = require '../lib/in-memory-report' -istanbul = require 'istanbul' +reportCreator = require '../lib/report-creator' describe 'Index', -> - it 'should register "InMemoryReport" to istanbul', -> - expect(istanbul.Report.create('in-memory', {})).to.be.an.instanceof(InMemoryReport) \ No newline at end of file + it 'should register "InMemoryReport" to Report Creator', -> + expect(reportCreator.create('in-memory', {})).to.be.an.instanceof(InMemoryReport) \ No newline at end of file diff --git a/test/report-creator.spec.coffee b/test/report-creator.spec.coffee new file mode 100644 index 0000000..1ae95d8 --- /dev/null +++ b/test/report-creator.spec.coffee @@ -0,0 +1,45 @@ +istanbulReports = require 'istanbul-reports' + +reportCreator = require '../lib/report-creator' + +describe 'Report Creator', -> + + afterEach -> + reportCreator.reset() + + describe 'register', -> + + it 'should throw when reporter does not include type', -> + reporter = {} + expect(() -> + reportCreator.register(reporter) + ).to.throw + + it 'should complete when report includes a type', -> + reporter = { TYPE: 'test' } + expect(reportCreator.register(reporter)).to.be.equal('test') + + describe 'create', -> + + it 'should return custom reporter if registered', -> + Reporter = sinon.stub() + Reporter.TYPE = 'test' + reportCreator.register(Reporter) + fallbackCreateStub = sinon.stub(istanbulReports, 'create') + reporterOpts = { test: 'options' } + + reporter = reportCreator.create('test', reporterOpts) + + expect(fallbackCreateStub).not.to.be.called + expect(Reporter.calledWithNew()).to.be.true + expect(Reporter).to.be.calledWith(reporterOpts) + + it 'should proxy call to istanbul if custom reporter is not registered', -> + fallbackCreateStub = sinon.stub(istanbulReports, 'create') + fallbackCreateStub.returnsThis() + reporterOpts = { test: 'options' } + + reporter = reportCreator.create('test', reporterOpts) + + expect() + expect(fallbackCreateStub).to.be.calledWith('test', reporterOpts) diff --git a/test/reporter.spec.coffee b/test/reporter.spec.coffee index 9638991..929ff91 100644 --- a/test/reporter.spec.coffee +++ b/test/reporter.spec.coffee @@ -5,7 +5,13 @@ describe 'reporter', -> _ = require 'lodash' events = require 'events' path = require 'path' - istanbul = require 'istanbul' + + istanbulLibCoverage = require 'istanbul-lib-coverage' + istanbulLibReport = require 'istanbul-lib-report' + + globalSourceMapStore = require '../lib/source-map-store' + globalCoverageMap = require '../lib/coverage-map' + reports = require '../lib/report-creator' # TODO(vojta): remove the dependency on karma helper = require '../node_modules/karma/lib/helper' @@ -18,81 +24,89 @@ describe 'reporter', -> loadFile = nodeMocks.loadFile m = null - mockFs = - writeFile: sinon.spy() - - mockStore = sinon.spy() - mockStore.mix = (fn, obj) -> - istanbul.Store.mix fn, obj - - mockAdd = sinon.spy() - mockDispose = sinon.spy() - mockGetFinalCoverage = sinon.stub().returns {} - mockCollector = class Collector - add: mockAdd - dispose: mockDispose - getFinalCoverage: mockGetFinalCoverage - mockWriteReport = sinon.spy() - mockReportCreate = sinon.stub().returns writeReport: mockWriteReport - mockMkdir = sinon.spy() + mkdirIfNotExistsStub = sinon.stub() mockHelper = _: helper._ isDefined: (v) -> helper.isDefined v merge: (v...) -> helper.merge v... - mkdirIfNotExists: mockMkdir + mkdirIfNotExists: mkdirIfNotExistsStub normalizeWinPath: (path) -> helper.normalizeWinPath path - mockCoverageMap = - add: sinon.spy() - get: sinon.spy() - mockDefaultWatermarks = - statements: [50, 80] - branches: [50, 80] - functions: [50, 80] - lines: [50, 80] - - mockSummarizeCoverage = sinon.stub().returns { - lines: {total: 5, covered: 1, skipped: 0, pct: 20}, - statements: {total: 5, covered: 1, skipped: 0, pct: 20}, - functions: {total: 5, covered: 1, skipped: 0, pct: 20}, - branches: {total: 5, covered: 1, skipped: 0, pct: 20} - } + + # Mock Objects + mockCoverageSummary = null + mockFileCoverage = null + mockCoverageMap = null + mockDefaultWatermarks = null + mockPackageSummary = null + mockSourceMapStore = null + mockGlobalCoverageMap = null + + # Stubs + createCoverageSummaryStub = null + createCoverageMapStub = null + createContextStub = null + packageSummaryStub = null + getDefaultWatermarkStub = null + sourceMapStoreGetStub = null + globalCoverageMapGetStub = null + globalCoverageMapAddStub = null + reportCreateStub = null + + mockFs = + writeFile: sinon.spy() mocks = fs: mockFs - istanbul: - Store: mockStore - Collector: mockCollector - Report: create: mockReportCreate - config: defaultConfig: sinon.stub().returns(reporting: watermarks: mockDefaultWatermarks) - utils: - summarizeCoverage: mockSummarizeCoverage - summarizeFileCoverage: mockSummarizeCoverage dateformat: require 'dateformat' - './coverage-map': mockCoverageMap beforeEach -> - m = loadFile __dirname + '/../lib/reporter.js', mocks - - describe 'SourceCacheStore', -> - options = store = null - - beforeEach -> - options = - sourceCache: { './foo': 'TEST_SRC_DATA' } - store = new m.SourceCacheStore options - - it 'should fail on call to keys', -> - expect(-> store.keys()).to.throw() - - it 'should call get and check cache data', -> - expect(store.get('./foo')).to.equal 'TEST_SRC_DATA' - - it 'should call hasKey and check cache data', -> - expect(store.hasKey('./foo')).to.be.true - expect(store.hasKey('./bar')).to.be.false + mockCoverageSummary = + merge: sinon.stub() + toJSON: sinon.stub() + mockFileCoverage = + merge: sinon.stub() + toJSON: sinon.stub() + toSummary: sinon.stub() + mockFileCoverage.toSummary.returns mockCoverageSummary + mockCoverageMap = + fileCoverageFor: sinon.stub() + files: sinon.stub() + merge: sinon.stub() + toJSON: sinon.stub() + mockCoverageMap.fileCoverageFor.returns mockFileCoverage + createCoverageSummaryStub = sinon.stub(istanbulLibCoverage, 'createCoverageSummary') + createCoverageSummaryStub.returns mockCoverageSummary + createCoverageMapStub = sinon.stub(istanbulLibCoverage, 'createCoverageMap') + createCoverageMapStub.returns mockCoverageMap + + mockDefaultWatermarks = + statements: [50, 80] + branches: [50, 80] + functions: [50, 80] + lines: [50, 80] + mockPackageSummary = + visit: sinon.stub() + createContextStub = sinon.stub(istanbulLibReport, 'createContext') + packageSummaryStub = sinon.stub(istanbulLibReport.summarizers, 'pkg') + packageSummaryStub.returns mockPackageSummary + getDefaultWatermarkStub = sinon.stub(istanbulLibReport, 'getDefaultWatermarks') + getDefaultWatermarkStub.returns mockDefaultWatermarks + + mockSourceMapStore = { + transformCoverage: sinon.stub() + } + mockSourceMapStore.transformCoverage.returns { map: mockCoverageMap } + sourceMapStoreGetStub = sinon.stub(globalSourceMapStore, 'get') + sourceMapStoreGetStub.returns mockSourceMapStore + + mockGlobalCoverageMap = {} + globalCoverageMapGetStub = sinon.stub(globalCoverageMap, 'get') + globalCoverageMapGetStub.returns mockGlobalCoverageMap + globalCoverageMapAddStub = sinon.stub(globalCoverageMap, 'add') + + reportCreateStub = sinon.stub(reports, 'create') - it 'should fail on call to set', -> - expect(-> store.set()).to.throw() + m = loadFile __dirname + '/../lib/reporter.js', mocks describe 'CoverageReporter', -> rootConfig = emitter = reporter = null @@ -118,7 +132,7 @@ describe 'reporter', -> browsers.add fakeOpera reporter.onRunStart() browsers.forEach (b) -> reporter.onBrowserStart b - mockMkdir.resetHistory() + mkdirIfNotExistsStub.resetHistory() it 'has no pending file writings', -> done = sinon.spy() @@ -129,22 +143,22 @@ describe 'reporter', -> result = coverage: null reporter.onBrowserComplete fakeChrome, result - expect(mockAdd).not.to.have.been.called + expect(mockCoverageMap.merge).not.to.have.been.called it 'should handle no result', -> reporter.onBrowserComplete fakeChrome, undefined - expect(mockAdd).not.to.have.been.called + expect(mockCoverageMap.merge).not.to.have.been.called it 'should make reports', -> reporter.onRunComplete browsers - expect(mockMkdir).to.have.been.calledTwice + expect(mkdirIfNotExistsStub).to.have.been.calledTwice dir = rootConfig.coverageReporter.dir - expect(mockMkdir.getCall(0).args[0]).to.deep.equal resolve('/base', dir, fakeChrome.name) - expect(mockMkdir.getCall(1).args[0]).to.deep.equal resolve('/base', dir, fakeOpera.name) - mockMkdir.getCall(0).args[1]() - expect(mockReportCreate).to.have.been.called - expect(mockWriteReport).to.have.been.called - createArgs = mockReportCreate.getCall(0).args + expect(mkdirIfNotExistsStub.getCall(0).args[0]).to.deep.equal resolve('/base', dir, fakeChrome.name) + expect(mkdirIfNotExistsStub.getCall(1).args[0]).to.deep.equal resolve('/base', dir, fakeOpera.name) + mkdirIfNotExistsStub.getCall(0).args[1]() + expect(reportCreateStub).to.have.been.called + expect(mockPackageSummary.visit).to.have.been.called + createArgs = reportCreateStub.getCall(0).args expect(createArgs[0]).to.be.equal 'html' expect(createArgs[1].browser).to.be.equal fakeChrome expect(createArgs[1].emitter).to.be.equal emitter @@ -159,14 +173,14 @@ describe 'reporter', -> browsers.forEach (b) -> reporter.onBrowserStart b reporter.onRunComplete browsers - expect(mockMkdir).to.have.been.calledTwice + expect(mkdirIfNotExistsStub).to.have.been.calledTwice dir = customConfig.coverageReporter.dir subdir = customConfig.coverageReporter.subdir - expect(mockMkdir.getCall(0).args[0]).to.deep.equal resolve('/base', dir, subdir) - expect(mockMkdir.getCall(1).args[0]).to.deep.equal resolve('/base', dir, subdir) - mockMkdir.getCall(0).args[1]() - expect(mockReportCreate).to.have.been.called - expect(mockWriteReport).to.have.been.called + expect(mkdirIfNotExistsStub.getCall(0).args[0]).to.deep.equal resolve('/base', dir, subdir) + expect(mkdirIfNotExistsStub.getCall(1).args[0]).to.deep.equal resolve('/base', dir, subdir) + mkdirIfNotExistsStub.getCall(0).args[1]() + expect(reportCreateStub).to.have.been.called + expect(mockPackageSummary.visit).to.have.been.called it 'should support a function for the subdir option', -> customConfig = _.merge {}, rootConfig, @@ -178,13 +192,13 @@ describe 'reporter', -> browsers.forEach (b) -> reporter.onBrowserStart b reporter.onRunComplete browsers - expect(mockMkdir).to.have.been.calledTwice + expect(mkdirIfNotExistsStub).to.have.been.calledTwice dir = customConfig.coverageReporter.dir - expect(mockMkdir.getCall(0).args[0]).to.deep.equal resolve('/base', dir, 'chrome') - expect(mockMkdir.getCall(1).args[0]).to.deep.equal resolve('/base', dir, 'opera') - mockMkdir.getCall(0).args[1]() - expect(mockReportCreate).to.have.been.called - expect(mockWriteReport).to.have.been.called + expect(mkdirIfNotExistsStub.getCall(0).args[0]).to.deep.equal resolve('/base', dir, 'chrome') + expect(mkdirIfNotExistsStub.getCall(1).args[0]).to.deep.equal resolve('/base', dir, 'opera') + mkdirIfNotExistsStub.getCall(0).args[1]() + expect(reportCreateStub).to.have.been.called + expect(mockPackageSummary.visit).to.have.been.called it 'should support a specific dir and subdir per reporter', -> customConfig = _.merge {}, rootConfig, @@ -207,14 +221,14 @@ describe 'reporter', -> browsers.forEach (b) -> reporter.onBrowserStart b reporter.onRunComplete browsers - expect(mockMkdir.callCount).to.equal 4 - expect(mockMkdir.getCall(0).args[0]).to.deep.equal resolve('/base', 'reporter1', 'chrome') - expect(mockMkdir.getCall(1).args[0]).to.deep.equal resolve('/base', 'reporter1', 'opera') - expect(mockMkdir.getCall(2).args[0]).to.deep.equal resolve('/base', 'reporter2', 'CHROME') - expect(mockMkdir.getCall(3).args[0]).to.deep.equal resolve('/base', 'reporter2', 'OPERA') - mockMkdir.getCall(0).args[1]() - expect(mockReportCreate).to.have.been.called - expect(mockWriteReport).to.have.been.called + expect(mkdirIfNotExistsStub.callCount).to.equal 4 + expect(mkdirIfNotExistsStub.getCall(0).args[0]).to.deep.equal resolve('/base', 'reporter1', 'chrome') + expect(mkdirIfNotExistsStub.getCall(1).args[0]).to.deep.equal resolve('/base', 'reporter1', 'opera') + expect(mkdirIfNotExistsStub.getCall(2).args[0]).to.deep.equal resolve('/base', 'reporter2', 'CHROME') + expect(mkdirIfNotExistsStub.getCall(3).args[0]).to.deep.equal resolve('/base', 'reporter2', 'OPERA') + mkdirIfNotExistsStub.getCall(0).args[1]() + expect(reportCreateStub).to.have.been.called + expect(mockPackageSummary.visit).to.have.been.called it 'should fallback to the default dir/subdir if not provided', -> customConfig = _.merge {}, rootConfig, @@ -235,14 +249,14 @@ describe 'reporter', -> browsers.forEach (b) -> reporter.onBrowserStart b reporter.onRunComplete browsers - expect(mockMkdir.callCount).to.equal 4 - expect(mockMkdir.getCall(0).args[0]).to.deep.equal resolve('/base', 'reporter1', 'defaultsubdir') - expect(mockMkdir.getCall(1).args[0]).to.deep.equal resolve('/base', 'reporter1', 'defaultsubdir') - expect(mockMkdir.getCall(2).args[0]).to.deep.equal resolve('/base', 'defaultdir', 'CHROME') - expect(mockMkdir.getCall(3).args[0]).to.deep.equal resolve('/base', 'defaultdir', 'OPERA') - mockMkdir.getCall(0).args[1]() - expect(mockReportCreate).to.have.been.called - expect(mockWriteReport).to.have.been.called + expect(mkdirIfNotExistsStub.callCount).to.equal 4 + expect(mkdirIfNotExistsStub.getCall(0).args[0]).to.deep.equal resolve('/base', 'reporter1', 'defaultsubdir') + expect(mkdirIfNotExistsStub.getCall(1).args[0]).to.deep.equal resolve('/base', 'reporter1', 'defaultsubdir') + expect(mkdirIfNotExistsStub.getCall(2).args[0]).to.deep.equal resolve('/base', 'defaultdir', 'CHROME') + expect(mkdirIfNotExistsStub.getCall(3).args[0]).to.deep.equal resolve('/base', 'defaultdir', 'OPERA') + mkdirIfNotExistsStub.getCall(0).args[1]() + expect(reportCreateStub).to.have.been.called + expect(mockPackageSummary.visit).to.have.been.called it 'should not create directory if reporting text* to console', -> run = -> @@ -256,7 +270,7 @@ describe 'reporter', -> { type: 'text-summary' } ] run() - expect(mockMkdir).not.to.have.been.called + expect(mkdirIfNotExistsStub).not.to.have.been.called it 'should create directory if reporting text* to file', -> run = -> @@ -267,12 +281,12 @@ describe 'reporter', -> rootConfig.coverageReporter.reporters = [{ type: 'text', file: 'file' }] run() - expect(mockMkdir).to.have.been.calledTwice + expect(mkdirIfNotExistsStub).to.have.been.calledTwice - mockMkdir.resetHistory() + mkdirIfNotExistsStub.resetHistory() rootConfig.coverageReporter.reporters = [{ type: 'text-summary', file: 'file' }] run() - expect(mockMkdir).to.have.been.calledTwice + expect(mkdirIfNotExistsStub).to.have.been.calledTwice it 'should support including all sources', -> customConfig = _.merge {}, rootConfig, @@ -280,15 +294,15 @@ describe 'reporter', -> dir: 'defaultdir' includeAllSources: true - mockCoverageMap.get.resetHistory() - mockAdd.resetHistory() + globalCoverageMapGetStub.resetHistory() + createCoverageMapStub.resetHistory() reporter = new m.CoverageReporter customConfig, mockHelper, mockLogger reporter.onRunStart() browsers.forEach (b) -> reporter.onBrowserStart b - expect(mockCoverageMap.get).to.have.been.called - expect(mockAdd).to.have.been.calledWith mockCoverageMap.get.returnValues[0] + expect(globalCoverageMapGetStub).to.have.been.called + expect(createCoverageMapStub).to.have.been.calledWith globalCoverageMapGetStub.returnValues[0] it 'should not retrieve the coverageMap if we aren\'t including all sources', -> customConfig = _.merge {}, rootConfig, @@ -296,26 +310,26 @@ describe 'reporter', -> dir: 'defaultdir' includeAllSources: false - mockCoverageMap.get.resetHistory() + globalCoverageMapGetStub.resetHistory() reporter = new m.CoverageReporter customConfig, mockHelper, mockLogger reporter.onRunStart() browsers.forEach (b) -> reporter.onBrowserStart b - expect(mockCoverageMap.get).not.to.have.been.called + expect(globalCoverageMapGetStub).not.to.have.been.called it 'should default to not including all sources', -> customConfig = _.merge {}, rootConfig, coverageReporter: dir: 'defaultdir' - mockCoverageMap.get.resetHistory() + globalCoverageMapGetStub.resetHistory() reporter = new m.CoverageReporter customConfig, mockHelper, mockLogger reporter.onRunStart() browsers.forEach (b) -> reporter.onBrowserStart b - expect(mockCoverageMap.get).not.to.have.been.called + expect(globalCoverageMapGetStub).not.to.have.been.called it 'should pass watermarks to istanbul', -> watermarks = @@ -333,15 +347,17 @@ describe 'reporter', -> ] watermarks: watermarks - mockReportCreate.reset() + reportCreateStub.resetHistory() + createContextStub.resetHistory() reporter = new m.CoverageReporter customConfig, mockHelper, mockLogger reporter.onRunStart() browsers.forEach (b) -> reporter.onBrowserStart b reporter.onRunComplete browsers - expect(mockReportCreate).to.have.been.called - options = mockReportCreate.getCall(0) + expect(createContextStub).to.have.been.called + expect(reportCreateStub).to.have.been.called + options = reportCreateStub.getCall(0) expect(options.args[1].watermarks).to.deep.equal(watermarks) it 'should merge with istanbul default watermarks', -> @@ -358,42 +374,22 @@ describe 'reporter', -> ] watermarks: watermarks - mockReportCreate.reset() + reportCreateStub.resetHistory() + createContextStub.resetHistory() reporter = new m.CoverageReporter customConfig, mockHelper, mockLogger reporter.onRunStart() browsers.forEach (b) -> reporter.onBrowserStart b reporter.onRunComplete browsers - expect(mockReportCreate).to.have.been.called - options = mockReportCreate.getCall(0) + expect(createContextStub).to.have.been.called + expect(reportCreateStub).to.have.been.called + options = reportCreateStub.getCall(0) expect(options.args[1].watermarks.statements).to.deep.equal(watermarks.statements) expect(options.args[1].watermarks.branches).to.deep.equal(mockDefaultWatermarks.branches) expect(options.args[1].watermarks.functions).to.deep.equal(mockDefaultWatermarks.functions) expect(options.args[1].watermarks.lines).to.deep.equal(watermarks.lines) - it 'should not write reports after disposing the collector', -> - run = -> - reporter = new m.CoverageReporter rootConfig, mockHelper, mockLogger - reporter.onRunStart() - browsers.forEach (b) -> reporter.onBrowserStart b - reporter.onRunComplete browsers - - rootConfig.coverageReporter.reporters = [ - { type: 'text' } - { type: 'html' } - ] - - mockDispose.resetHistory() - mockWriteReport.resetHistory() - mockMkdir.resetHistory() - - run() - - mockMkdir.getCall(0).args[1]() - - expect(mockDispose).not.to.have.been.calledBefore mockWriteReport - it 'should log errors on low coverage and fail the build', -> customConfig = _.merge {}, rootConfig, coverageReporter: @@ -401,9 +397,12 @@ describe 'reporter', -> each: statements: 50 - mockGetFinalCoverage.returns - './foo/bar.js': {} - './foo/baz.js': {} + mockCoverageMap.files.returns ['./foo/bar.js', './foo/baz.js'] + mockCoverageSummary.toJSON.returns + lines: {total: 5, covered: 1, skipped: 0, pct: 20}, + statements: {total: 5, covered: 1, skipped: 0, pct: 20}, + functions: {total: 5, covered: 1, skipped: 0, pct: 20}, + branches: {total: 5, covered: 1, skipped: 0, pct: 20} spy1 = sinon.spy() @@ -431,9 +430,12 @@ describe 'reporter', -> each: statements: 10 - mockGetFinalCoverage.returns - './foo/bar.js': {} - './foo/baz.js': {} + mockCoverageMap.files.returns ['./foo/bar.js', './foo/baz.js'] + mockCoverageSummary.toJSON.returns + lines: {total: 5, covered: 1, skipped: 0, pct: 20}, + statements: {total: 5, covered: 1, skipped: 0, pct: 20}, + functions: {total: 5, covered: 1, skipped: 0, pct: 20}, + branches: {total: 5, covered: 1, skipped: 0, pct: 20} spy1 = sinon.spy() diff --git a/test/source-map-store.spec.coffee b/test/source-map-store.spec.coffee new file mode 100644 index 0000000..191c03b --- /dev/null +++ b/test/source-map-store.spec.coffee @@ -0,0 +1,22 @@ +istanbulLibSourceMaps = require 'istanbul-lib-source-maps' + +globalSourceMapStore = require '../lib/source-map-store' + +describe 'Source Map Store', -> + + it 'should create a source map store for path if it did not exist previously', -> + createSourceMapStoreStub = sinon.stub(istanbulLibSourceMaps, 'createSourceMapStore') + createSourceMapStoreStub.returns({}) + + globalSourceMapStore.get('__test', { opts: 'test' }) + expect(createSourceMapStoreStub).to.be.calledWith({ opts: 'test' }) + + it 'should not create a source map store for path if it previously was called', -> + createSourceMapStoreStub = sinon.stub(istanbulLibSourceMaps, 'createSourceMapStore') + createSourceMapStoreStub.returns({}) + + globalSourceMapStore.get('__test2', { opts: 'test2' }) + globalSourceMapStore.get('__test2', { opts: 'test3' }) + + expect(createSourceMapStoreStub.callCount).to.be.equal(1) + expect(createSourceMapStoreStub).to.be.calledWith({ opts: 'test2' })