Skip to content

Commit

Permalink
feat: Refactor istanbul-lib-report so report can choose summarizer (#408
Browse files Browse the repository at this point in the history
)

BREAKING CHANGE: Existing istanbul-lib-report API's have been changed
  • Loading branch information
coreyfarrell committed Jun 17, 2019
1 parent 3354bfb commit 0f328fd
Show file tree
Hide file tree
Showing 24 changed files with 821 additions and 752 deletions.
28 changes: 6 additions & 22 deletions packages/istanbul-lib-report/index.js
Expand Up @@ -8,9 +8,9 @@
* @module Exports
*/

const summarizer = require('./lib/summarizer');
const Context = require('./lib/context');
const watermarks = require('./lib/watermarks');
const ReportBase = require('./lib/report-base');

module.exports = {
/**
Expand All @@ -21,6 +21,7 @@ module.exports = {
createContext(opts) {
return new Context(opts);
},

/**
* returns the default watermarks that would be used when not
* overridden
Expand All @@ -30,27 +31,10 @@ module.exports = {
*/
getDefaultWatermarks() {
return watermarks.getDefault();
}
};
/**
* standard summary functions
*/
module.exports.summarizers = {
/**
* a summarizer that creates a flat tree with one root node and bunch of
* files directly under it
*/
flat: summarizer.createFlatSummary,
/**
* a summarizer that creates a hierarchical tree where the coverage summaries
* of each directly reflect the summaries of all subdirectories and files in it
*/
nested: summarizer.createNestedSummary,
},

/**
* a summarizer that creates a tree in which directories are not nested.
* Every subdirectory is a child of the root node and only reflects the
* coverage numbers for the files in it (i.e. excludes subdirectories).
* This is the default summarizer.
* Base class for all reports
*/
pkg: summarizer.createPackageSummary
ReportBase
};
10 changes: 9 additions & 1 deletion packages/istanbul-lib-report/lib/context.js
Expand Up @@ -8,6 +8,7 @@ const FileWriter = require('./file-writer');
const XMLWriter = require('./xml-writer');
const tree = require('./tree');
const watermarks = require('./watermarks');
const SummarizerFactory = require('./summarizer-factory');

function defaultSourceLookup(path) {
try {
Expand Down Expand Up @@ -41,10 +42,13 @@ function normalizeWatermarks(specified = {}) {
*/
class Context {
constructor(opts) {
opts = opts || {};
this.dir = opts.dir || 'coverage';
this.watermarks = normalizeWatermarks(opts.watermarks);
this.sourceFinder = opts.sourceFinder || defaultSourceLookup;
this._summarizerFactory = new SummarizerFactory(
opts.coverageMap,
opts.defaultSummarizer
);
this.data = {};
}

Expand Down Expand Up @@ -109,6 +113,10 @@ class Context {
getVisitor(partialVisitor) {
return new tree.Visitor(partialVisitor);
}

getTree(name = 'defaultSummarizer') {
return this._summarizerFactory[name];
}
}

Object.defineProperty(Context.prototype, 'writer', {
Expand Down
16 changes: 16 additions & 0 deletions packages/istanbul-lib-report/lib/report-base.js
@@ -0,0 +1,16 @@
'use strict';

// TODO: switch to class private field when targetting node.js 12
const _summarizer = Symbol('ReportBase.#summarizer');

class ReportBase {
constructor(summarizer) {
this[_summarizer] = summarizer;
}

execute(context) {
context.getTree(this[_summarizer]).visit(this, context);
}
}

module.exports = ReportBase;
Expand Up @@ -139,25 +139,6 @@ function findCommonParent(paths) {
);
}

function toInitialList(coverageMap) {
const list = coverageMap.files().map(filePath => ({
filePath,
path: new Path(filePath),
fileCoverage: coverageMap.fileCoverageFor(filePath)
}));

const commonParent = findCommonParent(list.map(o => o.path.parent()));
if (commonParent.length > 0) {
list.forEach(o => {
o.path.splice(0, commonParent.length);
});
}
return {
list,
commonParent
};
}

function findOrCreateParent(parentPath, nodeMap, created = () => {}) {
let parent = nodeMap[parentPath.toString()];

Expand Down Expand Up @@ -196,16 +177,6 @@ function addAllPaths(topPaths, nodeMap, path, node) {
parent.addChild(node);
}

function toNested(coverageMap) {
const nodeMap = Object.create(null);
const topPaths = [];
toInitialList(coverageMap).list.forEach(o => {
const node = new ReportNode(o.path, o.fileCoverage);
addAllPaths(topPaths, nodeMap, o.path, node);
});
return topPaths;
}

function foldIntoOneDir(node, parent) {
const { children } = node;
if (children.length === 1 && !children[0].fileCoverage) {
Expand All @@ -216,17 +187,7 @@ function foldIntoOneDir(node, parent) {
return node;
}

function createNestedSummary(coverageMap) {
const topNodes = toNested(coverageMap).map(node => foldIntoOneDir(node));

if (topNodes.length === 1) {
return new ReportTree(topNodes[0]);
}

return new ReportTree(ReportNode.createRoot(topNodes));
}

function packageSummaryPrefix(dirParents, { commonParent }) {
function pkgSummaryPrefix(dirParents, commonParent) {
if (!dirParents.some(dp => dp.path.length === 0)) {
return;
}
Expand All @@ -238,30 +199,86 @@ function packageSummaryPrefix(dirParents, { commonParent }) {
return commonParent.name();
}

function createPackageSummary(coverageMap) {
const flattened = toInitialList(coverageMap);
const dirParents = toDirParents(flattened.list);
class SummarizerFactory {
constructor(coverageMap, defaultSummarizer = 'pkg') {
this._coverageMap = coverageMap;
this._defaultSummarizer = defaultSummarizer;
this._initialList = coverageMap.files().map(filePath => ({
filePath,
path: new Path(filePath),
fileCoverage: coverageMap.fileCoverageFor(filePath)
}));
this._commonParent = findCommonParent(
this._initialList.map(o => o.path.parent())
);
if (this._commonParent.length > 0) {
this._initialList.forEach(o => {
o.path.splice(0, this._commonParent.length);
});
}
}

if (dirParents.length === 1) {
return new ReportTree(dirParents[0]);
get defaultSummarizer() {
return this[this._defaultSummarizer];
}

return new ReportTree(
ReportNode.createRoot(dirParents),
packageSummaryPrefix(dirParents, flattened)
);
}
get flat() {
if (!this._flat) {
this._flat = new ReportTree(
ReportNode.createRoot(
this._initialList.map(
node => new ReportNode(node.path, node.fileCoverage)
)
)
);
}

function createFlatSummary(coverageMap) {
const list = toInitialList(coverageMap).list.map(
node => new ReportNode(node.path, node.fileCoverage)
);
return this._flat;
}

_createPkg() {
const dirParents = toDirParents(this._initialList);
if (dirParents.length === 1) {
return new ReportTree(dirParents[0]);
}

return new ReportTree(
ReportNode.createRoot(dirParents),
pkgSummaryPrefix(dirParents, this._commonParent)
);
}

get pkg() {
if (!this._pkg) {
this._pkg = this._createPkg();
}

return this._pkg;
}

return new ReportTree(ReportNode.createRoot(list));
_createNested() {
const nodeMap = Object.create(null);
const topPaths = [];
this._initialList.forEach(o => {
const node = new ReportNode(o.path, o.fileCoverage);
addAllPaths(topPaths, nodeMap, o.path, node);
});

const topNodes = topPaths.map(node => foldIntoOneDir(node));
if (topNodes.length === 1) {
return new ReportTree(topNodes[0]);
}

return new ReportTree(ReportNode.createRoot(topNodes));
}

get nested() {
if (!this._nested) {
this._nested = this._createNested();
}

return this._nested;
}
}

module.exports = {
createNestedSummary,
createPackageSummary,
createFlatSummary
};
module.exports = SummarizerFactory;
24 changes: 16 additions & 8 deletions packages/istanbul-lib-report/test/context.test.js
Expand Up @@ -3,54 +3,62 @@

const assert = require('chai').assert;
const Context = require('../lib/context');
const coverageMap = require('./helpers/coverage-map');

const optsEmptyCoverage = {
coverageMap: coverageMap.empty
};

describe('context', () => {
it('provides a writer when not specified', () => {
const ctx = new Context();
const ctx = new Context(optsEmptyCoverage);
const w = ctx.writer;

assert.ok(w);
assert.ok(w === ctx.writer);
assert.ok(w === ctx.getWriter());
});
it('returns an XML writer', () => {
const ctx = new Context();
const ctx = new Context(optsEmptyCoverage);
const w = ctx.writer;
const cw = w.writeFile(null);
assert.ok(ctx.getXMLWriter(cw));
});
it('returns source text by default', () => {
const ctx = new Context();
const ctx = new Context(optsEmptyCoverage);
const file = __filename;
assert.ok(ctx.getSource(file));
});
it('throws when source file not found', () => {
const ctx = new Context();
const ctx = new Context(optsEmptyCoverage);
const file = __filename;
assert.throws(ctx.getSource.bind(ctx, file + '.xxx'));
});
it('provides the correct classes for default watermarks', () => {
const ctx = new Context();
const ctx = new Context(optsEmptyCoverage);
assert.equal(ctx.classForPercent('statements', 49), 'low');
assert.equal(ctx.classForPercent('branches', 50), 'medium');
assert.equal(ctx.classForPercent('functions', 80), 'high');
assert.equal(ctx.classForPercent('lines', 85), 'high');
assert.equal(ctx.classForPercent('xlines', 85), 'unknown');
});
it('allows watermark overrides', () => {
const w = {
const watermarks = {
statements: {},
branches: [10],
lines: [90, 95]
};
const ctx = new Context({ watermarks: w });
const ctx = new Context({
watermarks,
coverageMap: coverageMap.empty
});
assert.equal(ctx.classForPercent('statements', 49), 'low');
assert.equal(ctx.classForPercent('branches', 50), 'medium');
assert.equal(ctx.classForPercent('functions', 80), 'high');
assert.equal(ctx.classForPercent('lines', 85), 'low');
});
it('returns a visitor', () => {
const ctx = new Context();
const ctx = new Context(optsEmptyCoverage);
const visitor = ctx.getVisitor({});
assert.ok(typeof visitor.onStart === 'function');
});
Expand Down

0 comments on commit 0f328fd

Please sign in to comment.