From 6140c6cc4439dd10038ca38f66ed343dbe4d79c2 Mon Sep 17 00:00:00 2001 From: Benjamin Date: Fri, 24 Nov 2017 19:37:12 -0800 Subject: [PATCH] feat: initial implementation --- .gitignore | 5 + LICENSE.txt | 14 +++ README.md | 3 + index.js | 5 + lib/branch.js | 27 +++++ lib/function.js | 28 ++++++ lib/line.js | 20 ++++ lib/script.js | 114 +++++++++++++++++++++ package.json | 31 ++++++ test/fixtures/functions.js | 153 +++++++++++++++++++++++++++++ test/fixtures/scripts/branches.js | 9 ++ test/fixtures/scripts/functions.js | 48 +++++++++ test/script.js | 33 +++++++ test/utils/run-fixture.js | 78 +++++++++++++++ 14 files changed, 568 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 index.js create mode 100644 lib/branch.js create mode 100644 lib/function.js create mode 100644 lib/line.js create mode 100644 lib/script.js create mode 100644 package.json create mode 100644 test/fixtures/functions.js create mode 100644 test/fixtures/scripts/branches.js create mode 100644 test/fixtures/scripts/functions.js create mode 100644 test/script.js create mode 100644 test/utils/run-fixture.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..2ab33cdd --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.DS_Store +coverage +.nyc_output +node_modules +package-lock.json diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 00000000..629264e9 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,14 @@ +Copyright (c) 2017, Contributors + +Permission to use, copy, modify, and/or distribute this software +for any purpose with or without fee is hereby granted, provided +that the above copyright notice and this permission notice +appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE +LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 00000000..cbbe9f44 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# v8-to-istanbul + +converts from v8 coverage format to [istanbul's coverage format](https://github.com/gotwarlost/istanbul/blob/master/coverage.json.md). diff --git a/index.js b/index.js new file mode 100644 index 00000000..71575c1a --- /dev/null +++ b/index.js @@ -0,0 +1,5 @@ +const Script = require('./lib/script') + +module.exports = function (path) { + return new Script(path) +} diff --git a/lib/branch.js b/lib/branch.js new file mode 100644 index 00000000..99c40f03 --- /dev/null +++ b/lib/branch.js @@ -0,0 +1,27 @@ +module.exports = class CovBranch { + constructor (startLine, startCol, endLine, endCol, count) { + this.startLine = startLine + this.startCol = startCol + this.endLine = endLine + this.endCol = endCol + this.count = count + } + toIstanbul () { + const location = { + start: { + line: this.startLine.line, + column: this.startCol - this.startLine.startCol + }, + end: { + line: this.endLine.line, + column: this.endCol - this.endLine.startCol + } + } + return { + type: 'branch', + line: this.line, + loc: location, + locations: [Object.assign({}, location)] + } + } +} diff --git a/lib/function.js b/lib/function.js new file mode 100644 index 00000000..40226fb1 --- /dev/null +++ b/lib/function.js @@ -0,0 +1,28 @@ +module.exports = class CovFunction { + constructor (name, startLine, startCol, endLine, endCol, count) { + this.name = name + this.startLine = startLine + this.startCol = startCol + this.endLine = endLine + this.endCol = endCol + this.count = count + } + toIstanbul () { + const loc = { + start: { + line: this.startLine.line, + column: this.startCol - this.startLine.startCol + }, + end: { + line: this.endLine.line, + column: this.endCol - this.endLine.startCol + } + } + return { + name: this.name, + decl: loc, + loc: loc, + line: this.startLine.line + } + } +} diff --git a/lib/line.js b/lib/line.js new file mode 100644 index 00000000..d4e3fb5f --- /dev/null +++ b/lib/line.js @@ -0,0 +1,20 @@ +module.exports = class CovLine { + constructor (line, startCol, endCol) { + this.line = line + this.startCol = startCol + this.endCol = endCol + this.count = 0 + } + toIstanbul () { + return { + start: { + line: this.line, + column: 0 + }, + end: { + line: this.line, + column: this.endCol - this.startCol + } + } + } +} diff --git a/lib/script.js b/lib/script.js new file mode 100644 index 00000000..5f252116 --- /dev/null +++ b/lib/script.js @@ -0,0 +1,114 @@ +const fs = require('fs') +const CovBranch = require('./branch') +const CovLine = require('./line') +const CovFunction = require('./function') + +// Node.js injects a header when executing a script. +const header = '(function (exports, require, module, __filename, __dirname) { ' + +module.exports = class CovScript { + constructor (scriptPath) { + const source = fs.readFileSync(scriptPath, 'utf8') + this.path = scriptPath + this.lines = [] + this.branches = [] + this.functions = [] + this.eof = -1 + this._buildLines(source, this.lines) + } + _buildLines (source, lines) { + let position = 0 + source.split('\n').forEach((lineStr, i) => { + this.eof = position + lineStr.length + lines.push(new CovLine(i + 1, position, this.eof)) + position += lineStr.length + 1 // also add the \n. + }) + } + applyCoverage (blocks) { + blocks.forEach(block => { + block.ranges.forEach(range => { + const startCol = Math.max(0, range.startOffset - header.length) + const endCol = Math.min(this.eof, range.endOffset - header.length) + const lines = this.lines.filter(line => { + return startCol <= line.endCol && endCol >= line.startCol + }) + + if (block.isBlockCoverage && lines.length) { + // record branches. + this.branches.push(new CovBranch( + lines[0], + startCol, + lines[lines.length - 1], + endCol, + range.count + )) + } else if (block.functionName && lines.length) { + // record functions. + this.functions.push(new CovFunction( + block.functionName, + lines[0], + startCol, + lines[lines.length - 1], + endCol, + range.count + )) + } + + // record the lines (we record these as statements, such that we're + // compatible with Istanbul 2.0). + lines.forEach(line => { + // make sure branch spans entire line; don't record 'goodbye' + // branch in `const foo = true ? 'hello' : 'goodbye'` as a + // 0 for line coverage. + if (startCol <= line.startCol && endCol >= line.endCol) { + line.count = range.count + } + }) + }) + }) + } + toIstanbul () { + const istanbulInner = Object.assign( + {path: this.path}, + this._statementsToIstanbul(), + this._branchesToIstanbul(), + this._functionsToIstanbul() + ) + const istanbulOuter = {} + istanbulOuter[this.path] = istanbulInner + return istanbulOuter + } + _statementsToIstanbul () { + const statements = { + statementMap: {}, + s: {} + } + this.lines.forEach((line, index) => { + statements.statementMap[`${index}`] = line.toIstanbul() + statements.s[`${index}`] = line.count + }) + return statements + } + _branchesToIstanbul () { + const branches = { + branchMap: {}, + b: {} + } + this.branches.forEach((branch, index) => { + branches.branchMap[`${index}`] = branch.toIstanbul() + branches.b[`${index}`] = [branch.count] + }) + return branches + } + _functionsToIstanbul () { + const functions = { + fnMap: {}, + f: {} + } + this.functions.forEach((fn, index) => { + functions.fnMap[`${index}`] = fn.toIstanbul() + functions.f[`${index}`] = fn.count + }) + return functions + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..dc620f8a --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "v8-to-istanbul", + "version": "1.0.0", + "description": "convert from v8 coverage format to istanbul's format", + "main": "index.js", + "scripts": { + "test": "nyc mocha test/*.js", + "posttest": "standard" + }, + "repository": { + "url": "git@github.com:bcoe/v8-to-istanbul.git" + }, + "keywords": [ + "istanbul", + "v8", + "coverage" + ], + "standard": { + "ignore": [ + "**/test/fixtures" + ] + }, + "author": "Ben Coe ", + "license": "ISC", + "devDependencies": { + "chai": "^4.1.2", + "mocha": "^4.0.1", + "nyc": "^11.3.0", + "standard": "^10.0.3" + } +} diff --git a/test/fixtures/functions.js b/test/fixtures/functions.js new file mode 100644 index 00000000..febbd2bf --- /dev/null +++ b/test/fixtures/functions.js @@ -0,0 +1,153 @@ +module.exports = { + describe: 'functions', + coverageV8: { + "scriptId": "71", + "url": "/Users/benjamincoe/bcoe/v8-to-istanbul/test/fixtures/scripts/functions.js", + "functions": [ + { + "functionName": "", + "ranges": [ + { + "startOffset": 0, + "endOffset": 790, + "count": 1 + } + ], + "isBlockCoverage": true + }, + { + "functionName": "", + "ranges": [ + { + "startOffset": 1, + "endOffset": 788, + "count": 1 + } + ], + "isBlockCoverage": true + }, + { + "functionName": "a", + "ranges": [ + { + "startOffset": 93, + "endOffset": 162, + "count": 0 + } + ], + "isBlockCoverage": false + }, + { + "functionName": "b", + "ranges": [ + { + "startOffset": 198, + "endOffset": 315, + "count": 1 + }, + { + "startOffset": 242, + "endOffset": 251, + "count": 0 + }, + { + "startOffset": 292, + "endOffset": 297, + "count": 0 + }, + { + "startOffset": 311, + "endOffset": 314, + "count": 0 + } + ], + "isBlockCoverage": true + }, + { + "functionName": "c", + "ranges": [ + { + "startOffset": 374, + "endOffset": 423, + "count": 2 + } + ], + "isBlockCoverage": true + }, + { + "functionName": "Foo", + "ranges": [ + { + "startOffset": 510, + "endOffset": 537, + "count": 0 + } + ], + "isBlockCoverage": false + }, + { + "functionName": "hello", + "ranges": [ + { + "startOffset": 546, + "endOffset": 580, + "count": 0 + } + ], + "isBlockCoverage": false + }, + { + "functionName": "Bar", + "ranges": [ + { + "startOffset": 657, + "endOffset": 688, + "count": 1 + } + ], + "isBlockCoverage": true + }, + { + "functionName": "hello", + "ranges": [ + { + "startOffset": 699, + "endOffset": 750, + "count": 1 + } + ], + "isBlockCoverage": true + } + ] + }, + assertions: [ + { + describe: 'function that is not executed', + lines: [ + { + start: 2, + end: 6, + count: 0 + } + ] + }, + { + describe: 'function that is called once', + lines: [ + { + startLine: 9, + endLine: 14, + count: 1 + } + ], + branches: [ + { + startLine: 10, + startCol: 15, + endLine: 10, + endCol: 20 + } + ] + }, + ] +} diff --git a/test/fixtures/scripts/branches.js b/test/fixtures/scripts/branches.js new file mode 100644 index 00000000..1210a53e --- /dev/null +++ b/test/fixtures/scripts/branches.js @@ -0,0 +1,9 @@ +const a = 99 && + 33 || 13 +if (false) { + const a = 99 +} + else { + console.info('hello') + } +const b = a ? 'hello' : 'goodbye' diff --git a/test/fixtures/scripts/functions.js b/test/fixtures/scripts/functions.js new file mode 100644 index 00000000..7b3c071f --- /dev/null +++ b/test/fixtures/scripts/functions.js @@ -0,0 +1,48 @@ +// function that's not called. +function a() { + if (x == 42) { + if (x == 43) b(); else c(); + } +} + +// function that's called once. + function b () { + const i = a ? 'hello' : 'goodbye' + const ii = a && b + const iii = a || 33 + return ii + } + +b() + +// function that's called multiple times. +const c = () => { + const i = 22 + const ii = i && + 99 +} + +c(); c() + +// class that never has member functions called. +class Foo { + constructor () { + this.test = 99 + } + hello () { + console.info('hello') + } +} + +// class that has member functions called. + class Bar { + constructor () { + this.test = 99 + } + hello () { + console.info(`Hello ${this.test}`) + } + } + +const d = new Bar() +d.hello() diff --git a/test/script.js b/test/script.js new file mode 100644 index 00000000..799dd68c --- /dev/null +++ b/test/script.js @@ -0,0 +1,33 @@ +/* global describe, it */ + +const {readdirSync, lstatSync} = require('fs') +const path = require('path') +const runFixture = require('./utils/run-fixture') +const Script = require('../lib/script') + +require('chai').should() + +describe('Script', () => { + describe('constructor', () => { + it('creates line instance for each line in script', () => { + const script = new Script( + require.resolve('./fixtures/scripts/functions.js') + ) + script.lines.length.should.equal(49) + }) + }) + + // execute JavaScript files in fixtures directory; these + // files contain the raw v8 output along with a set of + // assertions. the original scripts can be found in the + // fixtures/scripts folder. + const fixtureRoot = path.resolve(__dirname, './fixtures') + readdirSync(fixtureRoot).forEach(file => { + const fixturePath = path.resolve(fixtureRoot, file) + const stats = lstatSync(fixturePath) + if (stats.isFile()) { + const fixture = require(fixturePath) + runFixture(fixture) + } + }) +}) diff --git a/test/utils/run-fixture.js b/test/utils/run-fixture.js new file mode 100644 index 00000000..e41df980 --- /dev/null +++ b/test/utils/run-fixture.js @@ -0,0 +1,78 @@ +/* global describe, it */ + +const toIstanbul = require('../../') + +require('chai').should() + +module.exports = (fixture) => { + describe(fixture.describe, () => { + fixture.assertions.forEach(assertion => { + describe(assertion.describe, () => { + const script = toIstanbul(fixture.coverageV8.url) + script.applyCoverage(fixture.coverageV8.functions) + let coverageIstanbul = script.toIstanbul() + + // run with DEBUG=true to output coverage information to + // terminal; this is useful when writing new tests. + if (process.env.DEBUG === 'true') { + console.info('------------------') + console.info(JSON.stringify(coverageIstanbul, null, 2)) + console.info('------------------') + } + + // the top level object is keyed on filename, grab the inner + // object which is easier to assert against. + coverageIstanbul = coverageIstanbul[Object.keys(coverageIstanbul)[0]] + + it('has appropriate line coverage', () => { + ;(assertion.lines || []).forEach(expectedLine => { + assertLine(coverageIstanbul, expectedLine) + }) + }) + + it('has appropriate branch coverage', () => { + ;(assertion.branches || []).forEach(expectedBranch => { + assertBranch(coverageIstanbul, expectedBranch) + }) + }) + }) + }) + }) +} + +function assertLine (coverageIstanbul, expectedLine) { + let started = false + let start = expectedLine.startLine + const keys = Object.keys(coverageIstanbul.statementMap) + for (var i = 0, key; (key = keys[i]) !== undefined; i++) { + if (start > expectedLine.endLine) break + const observedLine = coverageIstanbul.statementMap[key] + if (observedLine.start.line === expectedLine.startLine) { + started = true + } + if (started) { + start.should.equal(observedLine.start.line) + coverageIstanbul.s[key].should.equal(expectedLine.count) + start += 1 + } + } +} + +function assertBranch (coverageIstanbul, expectedBranch) { + // let started = false + // let start = expectedBranch.startLine + /* + Object.keys(coverageIstanbul.statementMap).forEach(key => { + if (start > expectedLine.endLine) return + const observedLine = coverageIstanbul.statementMap[key] + if (observedLine.start.line === expectedLine.startLine) { + started = true + } + if (started) { + start.should.equal(observedLine.start.line) + coverageIstanbul.s[key].should.equal(expectedLine.count) + start += 1 + } + }) + */ +}