Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions addon/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module.exports = {
root: true,
parserOptions: {
ecmaVersion: 6,
sourceType: 'module'
},
extends: 'eslint:recommended',
env: {
browser: true
},
rules: {
}
};
15 changes: 15 additions & 0 deletions addon/helpers/ember-cli-code-coverage-increment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import Ember from 'ember';

export function emberCliCodeCoverageIncrement(params, hash) {
let { path, statement, branch, condition } = hash;

if (statement) {
window.__coverage__[path].s[statement]++;
}

if (branch && condition) {
window.__coverage__[path].b[branch][condition]++;
}
}

export default Ember.Helper.helper(emberCliCodeCoverageIncrement);
8 changes: 8 additions & 0 deletions addon/helpers/ember-cli-code-coverage-register.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import Ember from 'ember';

export function emberCliCodeCoverageRegister([rawData]) {
let coverageData = JSON.parse(rawData);
window.__coverage__[coverageData.path] = coverageData;
}

export default Ember.Helper.helper(emberCliCodeCoverageRegister);
1 change: 1 addition & 0 deletions app/helpers/ember-cli-code-coverage-increment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default, emberCliCodeCoverageIncrement } from 'ember-cli-code-coverage/helpers/ember-cli-code-coverage-increment';
1 change: 1 addition & 0 deletions app/helpers/ember-cli-code-coverage-register.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default, emberCliCodeCoverageRegister } from 'ember-cli-code-coverage/helpers/ember-cli-code-coverage-register';
18 changes: 18 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,23 @@ module.exports = {
fileLookup: null,

// Ember Methods
setupPreprocessorRegistry: function(type, registry) {
if (!this._isCoverageEnabled()) { return; }

const buildTemplateInstrumenter = require('./lib/template-instrumenter');
let TemplateInstrumenter = buildTemplateInstrumenter(
this._parentName(),
this.parent.root,
this.registry.extensionsForType('template'),
this.project.isEmberCLIAddon()
);

registry.add('htmlbars-ast-plugin', {
name: "template-instrumenter",
plugin: TemplateInstrumenter,
baseDir: __dirname
});
},

included: function(appOrAddon) {
this._super.included.apply(this, arguments);
Expand Down Expand Up @@ -204,6 +221,7 @@ module.exports = {
return walkSync(dir, { directories: false, globs }).map(file => {
const postfix = hasEmberCliTypescript ? file : file.replace(EXT_RE, '.js');
const module = prefix + '/' + postfix;

this.fileLookup[module] = path.join(dirname, file);
return module;
});
Expand Down
226 changes: 226 additions & 0 deletions lib/template-instrumenter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
'use strict';

const LINE_ENDINGS = /(?:\r\n?|\n)/;
require('string.prototype.startswith');
const fixPath = require('./coverage-instrumenter').fixPath;

module.exports = function(appName, appRoot, templateExtensions, isAddon) {
return class IstanbulInstrumenter {
constructor(options) {

this.options = options;

let moduleName = options.meta.moduleName;
let relativePath = moduleName && fixPath(moduleName, appName, appRoot, templateExtensions, isAddon);

this.relativePath = relativePath;

this.coverageData = {
path: this.relativePath,
s: { },
b: { },
f: { },
fnMap: { },
statementMap: { },
branchMap: { },
code: [ ]
};

this._currentStatement = 0;
this._currentBranch = 0;

if (options.contents) {
this.coverageData.code = options.contents.split(LINE_ENDINGS);
}
}

shouldInstrument() {
let relativePath = this.relativePath;

return !relativePath || relativePath.startsWith('app') || relativePath.startsWith('addon');
}

currentContainer() {
return this._containerStack[this._containerStack.length - 1];
}

insertHelper(container, node, hash) {
let children = container.body || container.children;
let index = children.indexOf(node);
let b = this.syntax.builders;

hash.pairs.push(
b.pair('path', b.string(this.relativePath))
);

let helper = b.mustache(
b.path('ember-cli-code-coverage-increment'),
null,
hash
);
helper.isCoverageHelper = true;

container._statementsToInsert = container._statementsToInsert || [];
container._statementsToInsert.unshift({
helper,
index
});
}

insertStatementHelper(node) {
let b = this.syntax.builders;

let hash = b.hash([
b.pair('statement', b.string(this._currentStatement))
]);
this.insertHelper(this.currentContainer(), node, hash);
}

insertBranchHelper(container, node, condition) {
let b = this.syntax.builders;

let hash = b.hash([
b.pair('branch', b.string(this._currentBranch)),
b.pair('condition', b.string(condition))
]);

this.insertHelper(container, node, hash);
}

processStatementsToInsert(node) {
if (node._statementsToInsert) {
node._statementsToInsert.forEach((statement) => {
let { helper, index } = statement;

let children = node.children || node.body;
children.splice(index, 0, helper);
});
}
}

handleBlock(node) {
// cannot process blocks without a loc
if (!node.loc) {
return;
}

if (node.isCoverageHelper) { return; }
if (this.currentContainer()._ignoreCoverage) { return; }

this.handleStatement(node);

this._currentBranch++;
this.coverageData.b[this._currentBranch] = [0,0];
this.coverageData.branchMap[this._currentBranch] = {
start: { line: node.loc.start.line, column: node.loc.start.column },
end: { line: node.loc.end.line, column: node.loc.end.column },
};

if (node.type === 'BlockStatement') {
this.insertBranchHelper(node.program, node);
}
}

handleStatement(node) {
if (node.type === 'TextNode' && node.chars.trim() === '') {
return;
}

if (node.isCoverageHelper) { return; }
if (this.currentContainer()._ignoreCoverage) { return; }

// cannot process statements without a loc
if (!node.loc) {
return;
}

if (node.loc.start.line == null) {
return;
}

this._currentStatement++;
this.coverageData.s[this._currentStatement] = 0;
this.coverageData.statementMap[this._currentStatement] = {
start: {
line: node.loc.start.line,
column: node.loc.start.column
},
end: {
line: node.loc && node.loc.end.line,
column: node.loc && node.loc.end.column
},
};

this.insertStatementHelper(node);
}

transform(ast) {
if (!this.shouldInstrument()) {
return;
}

let handleBlock = {
enter: (node) => {
this.handleBlock(node);
this._containerStack.push(node);
},
exit: (node) => {
this._containerStack.pop();
this.processStatementsToInsert(node);
}
};

let handleStatement = (node) => this.handleStatement(node);

let b = this.syntax.builders;

this.syntax.traverse(ast, {
Program: {
enter: (node) => {
if (!this._topLevelProgram) {
this._topLevelProgram = node;
this._containerStack = [node];
} else {
this._containerStack.push(node);
}
},
exit: (node) => {
this.processStatementsToInsert(node);
if (node === this._topLevelProgram) {
let helper = b.mustache(
b.path('ember-cli-code-coverage-register'),
[
b.string(JSON.stringify(this.coverageData))
]
);
helper.isCoverageHelper = true;

node.body.unshift(helper);
} else {
this._containerStack.pop();
}
},
},

ElementNode: handleBlock,
BlockStatement: handleBlock,
MustacheStatement: handleStatement,
TextNode: handleStatement,

AttrNode: {
enter: (node) => {
this._containerStack.push(node);
// cannot properly inject helpers into AttrNode positions
node._ignoreCoverage = true;
},

exit: () => {
this._containerStack.pop();
}
}
});

return ast;
}
};
};
2 changes: 1 addition & 1 deletion test/unit/index-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -516,4 +516,4 @@ describe('index.js', function() {

});

});
});
61 changes: 61 additions & 0 deletions tests/unit/helpers/ember-cli-code-coverage-increment-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { emberCliCodeCoverageIncrement } from 'dummy/helpers/ember-cli-code-coverage-increment';
import { module, test } from 'qunit';

const ORIGINAL_COVERAGE = window.__coverage__;

function registerFile(path) {
window.__coverage__[path] = {
"path": path,
"s": {
"1": 0,
"2": 0,
"3": 0,
"4": 0,
"5": 0,
"6": 0
},
"b": {
"1": [0, 0],
"2": [0, 0]
},
"f": { },
"fnMap": { },
"statementMap": {
// not needed for testing
},
"branchMap": {
// not needed for testing
},
"code": [
// not needed for testing
]
};
}

module('Unit | Helper | ember cli code coverage increment', {
beforeEach() {
window.__coverage__ = {};
},

afterEach() {
window.__coverage__ = ORIGINAL_COVERAGE;
}
});

test('it increments the given statement', function(assert) {
let path = 'app/templates/foo';
registerFile(path);

emberCliCodeCoverageIncrement([], { path, statement: "1" });

assert.equal(window.__coverage__[path].s["1"], 1, 'statement was incremented');
});

test('it increments the given branch', function(assert) {
let path = 'app/templates/foo';
registerFile(path);

emberCliCodeCoverageIncrement([], { path, branch: '1', condition: '0' });

assert.equal(window.__coverage__[path].b[1][0], 1, 'branch was incremented');
});
Loading