276 lines
8.7 KiB
JavaScript
276 lines
8.7 KiB
JavaScript
|
/*
|
||
|
Copyright 2012-2015, Yahoo Inc.
|
||
|
Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
|
||
|
*/
|
||
|
'use strict';
|
||
|
|
||
|
const InsertionText = require('./insertion-text');
|
||
|
const lt = '\u0001';
|
||
|
const gt = '\u0002';
|
||
|
const RE_LT = /</g;
|
||
|
const RE_GT = />/g;
|
||
|
const RE_AMP = /&/g;
|
||
|
// eslint-disable-next-line
|
||
|
var RE_lt = /\u0001/g;
|
||
|
// eslint-disable-next-line
|
||
|
var RE_gt = /\u0002/g;
|
||
|
|
||
|
function title(str) {
|
||
|
return ' title="' + str + '" ';
|
||
|
}
|
||
|
|
||
|
function customEscape(text) {
|
||
|
text = String(text);
|
||
|
return text
|
||
|
.replace(RE_AMP, '&')
|
||
|
.replace(RE_LT, '<')
|
||
|
.replace(RE_GT, '>')
|
||
|
.replace(RE_lt, '<')
|
||
|
.replace(RE_gt, '>');
|
||
|
}
|
||
|
|
||
|
function annotateLines(fileCoverage, structuredText) {
|
||
|
const lineStats = fileCoverage.getLineCoverage();
|
||
|
if (!lineStats) {
|
||
|
return;
|
||
|
}
|
||
|
Object.keys(lineStats).forEach(lineNumber => {
|
||
|
const count = lineStats[lineNumber];
|
||
|
if (structuredText[lineNumber]) {
|
||
|
structuredText[lineNumber].covered = count > 0 ? 'yes' : 'no';
|
||
|
structuredText[lineNumber].hits = count;
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function annotateStatements(fileCoverage, structuredText) {
|
||
|
const statementStats = fileCoverage.s;
|
||
|
const statementMeta = fileCoverage.statementMap;
|
||
|
Object.keys(statementStats).forEach(stName => {
|
||
|
const count = statementStats[stName];
|
||
|
const meta = statementMeta[stName];
|
||
|
const type = count > 0 ? 'yes' : 'no';
|
||
|
const startCol = meta.start.column;
|
||
|
let endCol = meta.end.column + 1;
|
||
|
const startLine = meta.start.line;
|
||
|
const endLine = meta.end.line;
|
||
|
const openSpan =
|
||
|
lt +
|
||
|
'span class="' +
|
||
|
(meta.skip ? 'cstat-skip' : 'cstat-no') +
|
||
|
'"' +
|
||
|
title('statement not covered') +
|
||
|
gt;
|
||
|
const closeSpan = lt + '/span' + gt;
|
||
|
let text;
|
||
|
|
||
|
if (type === 'no' && structuredText[startLine]) {
|
||
|
if (endLine !== startLine) {
|
||
|
endCol = structuredText[startLine].text.originalLength();
|
||
|
}
|
||
|
text = structuredText[startLine].text;
|
||
|
text.wrap(
|
||
|
startCol,
|
||
|
openSpan,
|
||
|
startCol < endCol ? endCol : text.originalLength(),
|
||
|
closeSpan
|
||
|
);
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function annotateFunctions(fileCoverage, structuredText) {
|
||
|
const fnStats = fileCoverage.f;
|
||
|
const fnMeta = fileCoverage.fnMap;
|
||
|
if (!fnStats) {
|
||
|
return;
|
||
|
}
|
||
|
Object.keys(fnStats).forEach(fName => {
|
||
|
const count = fnStats[fName];
|
||
|
const meta = fnMeta[fName];
|
||
|
const type = count > 0 ? 'yes' : 'no';
|
||
|
const startCol = meta.decl.start.column;
|
||
|
let endCol = meta.decl.end.column + 1;
|
||
|
const startLine = meta.decl.start.line;
|
||
|
const endLine = meta.decl.end.line;
|
||
|
const openSpan =
|
||
|
lt +
|
||
|
'span class="' +
|
||
|
(meta.skip ? 'fstat-skip' : 'fstat-no') +
|
||
|
'"' +
|
||
|
title('function not covered') +
|
||
|
gt;
|
||
|
const closeSpan = lt + '/span' + gt;
|
||
|
let text;
|
||
|
|
||
|
if (type === 'no' && structuredText[startLine]) {
|
||
|
if (endLine !== startLine) {
|
||
|
endCol = structuredText[startLine].text.originalLength();
|
||
|
}
|
||
|
text = structuredText[startLine].text;
|
||
|
text.wrap(
|
||
|
startCol,
|
||
|
openSpan,
|
||
|
startCol < endCol ? endCol : text.originalLength(),
|
||
|
closeSpan
|
||
|
);
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function annotateBranches(fileCoverage, structuredText) {
|
||
|
const branchStats = fileCoverage.b;
|
||
|
const branchMeta = fileCoverage.branchMap;
|
||
|
if (!branchStats) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
Object.keys(branchStats).forEach(branchName => {
|
||
|
const branchArray = branchStats[branchName];
|
||
|
const sumCount = branchArray.reduce((p, n) => p + n, 0);
|
||
|
const metaArray = branchMeta[branchName].locations;
|
||
|
let i;
|
||
|
let count;
|
||
|
let meta;
|
||
|
let startCol;
|
||
|
let endCol;
|
||
|
let startLine;
|
||
|
let endLine;
|
||
|
let openSpan;
|
||
|
let closeSpan;
|
||
|
let text;
|
||
|
|
||
|
// only highlight if partial branches are missing or if there is a
|
||
|
// single uncovered branch.
|
||
|
if (sumCount > 0 || (sumCount === 0 && branchArray.length === 1)) {
|
||
|
for (
|
||
|
i = 0;
|
||
|
i < branchArray.length && i < metaArray.length;
|
||
|
i += 1
|
||
|
) {
|
||
|
count = branchArray[i];
|
||
|
meta = metaArray[i];
|
||
|
startCol = meta.start.column;
|
||
|
endCol = meta.end.column + 1;
|
||
|
startLine = meta.start.line;
|
||
|
endLine = meta.end.line;
|
||
|
openSpan =
|
||
|
lt +
|
||
|
'span class="branch-' +
|
||
|
i +
|
||
|
' ' +
|
||
|
(meta.skip ? 'cbranch-skip' : 'cbranch-no') +
|
||
|
'"' +
|
||
|
title('branch not covered') +
|
||
|
gt;
|
||
|
closeSpan = lt + '/span' + gt;
|
||
|
|
||
|
if (count === 0 && structuredText[startLine]) {
|
||
|
//skip branches taken
|
||
|
if (endLine !== startLine) {
|
||
|
endCol = structuredText[
|
||
|
startLine
|
||
|
].text.originalLength();
|
||
|
}
|
||
|
text = structuredText[startLine].text;
|
||
|
if (branchMeta[branchName].type === 'if') {
|
||
|
// 'if' is a special case
|
||
|
// since the else branch might not be visible, being non-existent
|
||
|
text.insertAt(
|
||
|
startCol,
|
||
|
lt +
|
||
|
'span class="' +
|
||
|
(meta.skip
|
||
|
? 'skip-if-branch'
|
||
|
: 'missing-if-branch') +
|
||
|
'"' +
|
||
|
title(
|
||
|
(i === 0 ? 'if' : 'else') +
|
||
|
' path not taken'
|
||
|
) +
|
||
|
gt +
|
||
|
(i === 0 ? 'I' : 'E') +
|
||
|
lt +
|
||
|
'/span' +
|
||
|
gt,
|
||
|
true,
|
||
|
false
|
||
|
);
|
||
|
} else {
|
||
|
text.wrap(
|
||
|
startCol,
|
||
|
openSpan,
|
||
|
startCol < endCol ? endCol : text.originalLength(),
|
||
|
closeSpan
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function annotateSourceCode(fileCoverage, sourceStore) {
|
||
|
let codeArray;
|
||
|
let lineCoverageArray;
|
||
|
try {
|
||
|
const sourceText = sourceStore.getSource(fileCoverage.path);
|
||
|
const code = sourceText.split(/(?:\r?\n)|\r/);
|
||
|
let count = 0;
|
||
|
const structured = code.map(str => {
|
||
|
count += 1;
|
||
|
return {
|
||
|
line: count,
|
||
|
covered: 'neutral',
|
||
|
hits: 0,
|
||
|
text: new InsertionText(str, true)
|
||
|
};
|
||
|
});
|
||
|
structured.unshift({
|
||
|
line: 0,
|
||
|
covered: null,
|
||
|
text: new InsertionText('')
|
||
|
});
|
||
|
annotateLines(fileCoverage, structured);
|
||
|
//note: order is important, since statements typically result in spanning the whole line and doing branches late
|
||
|
//causes mismatched tags
|
||
|
annotateBranches(fileCoverage, structured);
|
||
|
annotateFunctions(fileCoverage, structured);
|
||
|
annotateStatements(fileCoverage, structured);
|
||
|
structured.shift();
|
||
|
|
||
|
codeArray = structured.map(
|
||
|
item => customEscape(item.text.toString()) || ' '
|
||
|
);
|
||
|
|
||
|
lineCoverageArray = structured.map(item => ({
|
||
|
covered: item.covered,
|
||
|
hits: item.hits > 0 ? item.hits + 'x' : ' '
|
||
|
}));
|
||
|
|
||
|
return {
|
||
|
annotatedCode: codeArray,
|
||
|
lineCoverage: lineCoverageArray,
|
||
|
maxLines: structured.length
|
||
|
};
|
||
|
} catch (ex) {
|
||
|
codeArray = [ex.message];
|
||
|
lineCoverageArray = [{ covered: 'no', hits: 0 }];
|
||
|
String(ex.stack || '')
|
||
|
.split(/\r?\n/)
|
||
|
.forEach(line => {
|
||
|
codeArray.push(line);
|
||
|
lineCoverageArray.push({ covered: 'no', hits: 0 });
|
||
|
});
|
||
|
return {
|
||
|
annotatedCode: codeArray,
|
||
|
lineCoverage: lineCoverageArray,
|
||
|
maxLines: codeArray.length
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
|
||
|
module.exports = {
|
||
|
annotateSourceCode
|
||
|
};
|