Use electabul to instrument and report coverage

This commit is contained in:
Kevin Sawicki 2016-08-03 14:12:05 -07:00
parent 2c0de93f06
commit ad07a20d9a
6 changed files with 30 additions and 183 deletions

View file

@ -3,6 +3,7 @@
"version": "1.3.2",
"devDependencies": {
"asar": "^0.11.0",
"electabul": "~0.0.2",
"request": "*",
"standard": "^7.1.2",
"standard-markdown": "^1.1.1"
@ -24,8 +25,8 @@
"scripts": {
"bootstrap": "python ./script/bootstrap.py",
"build": "python ./script/build.py -c D",
"coverage": "npm run instrument-code-coverage && npm run test -- --use-instrumented-asar",
"instrument-code-coverage": "node ./spec/coverage/instrument.js",
"coverage": "npm run instrument-code-coverage && npm test -- --use-instrumented-asar",
"instrument-code-coverage": "electabul instrument --input-path ./lib --output-path ./out/coverage/electron.asar",
"lint": "npm run lint-js && npm run lint-cpp && npm run lint-docs",
"lint-js": "standard && cd spec && standard",
"lint-cpp": "python ./script/cpplint.py",

View file

@ -997,6 +997,7 @@ describe('browser-window module', function () {
describe('dev tool extensions', function () {
describe('BrowserWindow.addDevToolsExtension', function () {
let showPanelIntevalId
this.timeout(10000)
beforeEach(function () {
@ -1008,7 +1009,7 @@ describe('browser-window module', function () {
assert.equal(BrowserWindow.getDevToolsExtensions().hasOwnProperty('foo'), true)
w.webContents.on('devtools-opened', function () {
var showPanelIntevalId = setInterval(function () {
showPanelIntevalId = setInterval(function () {
if (w && w.devToolsWebContents) {
var showLastPanel = function () {
var lastPanelId = WebInspector.inspectorView._tabbedPane._tabs.peekLast().id
@ -1024,6 +1025,10 @@ describe('browser-window module', function () {
w.loadURL('about:blank')
})
afterEach(function () {
clearInterval(showPanelIntevalId)
})
it('throws errors for missing manifest.json files', function () {
assert.throws(function () {
BrowserWindow.addDevToolsExtension(path.join(__dirname, 'does-not-exist'))

View file

@ -1,34 +0,0 @@
// Generate an instrumented .asar file for all the files in lib/ and save it
// to out/coverage/electron-instrumented.asar
var asar = require('asar')
var fs = require('fs')
var glob = require('glob')
var Instrumenter = require('istanbul').Instrumenter
var mkdirp = require('mkdirp')
var path = require('path')
var rimraf = require('rimraf')
var instrumenter = new Instrumenter()
var outputPath = path.join(__dirname, '..', '..', 'out', 'coverage')
var libPath = path.join(__dirname, '..', '..', 'lib')
rimraf.sync(path.join(outputPath, 'lib'))
glob.sync('**/*.js', {cwd: libPath}).forEach(function (relativePath) {
var rawPath = path.join(libPath, relativePath)
var raw = fs.readFileSync(rawPath, 'utf8')
var generatedPath = path.join(outputPath, 'lib', relativePath)
var generated = instrumenter.instrumentSync(raw, rawPath)
mkdirp.sync(path.dirname(generatedPath))
fs.writeFileSync(generatedPath, generated)
})
var asarPath = path.join(outputPath, 'electron.asar')
asar.createPackageWithOptions(path.join(outputPath, 'lib'), asarPath, {}, function (error) {
if (error) {
console.error(error.stack || error)
process.exit(1)
}
})

View file

@ -1,140 +0,0 @@
const asar = require('asar')
const fs = require('fs')
const glob = require('glob')
const mkdirp = require('mkdirp')
const path = require('path')
const rimraf = require('rimraf')
const {Collector, Instrumenter, Reporter} = require('istanbul')
const outputPath = path.join(__dirname, '..', '..', 'out', 'coverage')
const libPath = path.join(__dirname, '..', '..', 'lib')
// Add unrequired files to the coverage report so all files are present there
const addUnrequiredFiles = (coverage) => {
const instrumenter = new Instrumenter()
const libPath = path.join(__dirname, '..', '..', 'lib')
glob.sync('**/*.js', {cwd: libPath}).map(function (relativePath) {
return path.join(libPath, relativePath)
}).filter(function (filePath) {
return coverage[filePath] == null
}).forEach(function (filePath) {
instrumenter.instrumentSync(fs.readFileSync(filePath, 'utf8'), filePath)
// When instrumenting the code, istanbul will give each FunctionDeclaration
// a value of 1 in coverState.s,presumably to compensate for function
// hoisting. We need to reset this, as the function was not hoisted, as it
// was never loaded.
Object.keys(instrumenter.coverState.s).forEach(function (key) {
instrumenter.coverState.s[key] = 0
});
coverage[filePath] = instrumenter.coverState
})
}
// Add coverage data to collector for all opened browser windows
const addBrowserWindowsData = (collector) => {
const dataPath = path.join(outputPath, 'data')
glob.sync('*.json', {cwd: dataPath}).map(function (relativePath) {
return path.join(dataPath, relativePath)
}).forEach(function (filePath) {
collector.add(JSON.parse(fs.readFileSync(filePath)));
})
}
// Generate a code coverage report in out/coverage/lcov-report
exports.generateReport = () => {
const coverage = window.__coverage__
if (coverage == null) return
addUnrequiredFiles(coverage)
const collector = new Collector()
collector.add(coverage)
const {ipcRenderer} = require('electron')
collector.add(ipcRenderer.sendSync('get-coverage'))
addBrowserWindowsData(collector)
const reporter = new Reporter(null, outputPath)
reporter.addAll(['text', 'lcov'])
reporter.write(collector, true, function () {})
}
// Save coverage data from the browser window with the given pid
const saveCoverageData = (webContents, coverage, pid) => {
if (coverage && pid) {
const dataPath = path.join(outputPath, 'data', `${pid || webContents.getId()}-${webContents.getType()}-${Date.now()}.json`)
mkdirp.sync(path.dirname(dataPath))
fs.writeFileSync(dataPath, JSON.stringify(coverage))
}
}
const getCoverageFromWebContents = (webContents, callback) => {
webContents.executeJavaScript('[window.__coverage__, window.process && window.process.pid]', (results) => {
const coverage = results[0]
const pid = results[1]
callback(coverage, pid)
})
}
const saveWebContentsCoverage = (webContents, callback) => {
getCoverageFromWebContents(webContents, (coverage, pid) => {
saveCoverageData(webContents, coverage, pid)
callback()
})
}
// Save coverage data when a BrowserWindow is closed manually
const patchBrowserWindow = () => {
const {BrowserWindow} = require('electron')
const {destroy} = BrowserWindow.prototype
BrowserWindow.prototype.destroy = function () {
if (this.isDestroyed() || !this.getURL()) {
return destroy.call(this)
}
saveWebContentsCoverage(this.webContents, () => {
if (this.devToolsWebContents) {
saveWebContentsCoverage(this.devToolsWebContents, () => {
destroy.call(this)
})
} else {
destroy.call(this)
}
})
}
}
// Save coverage data when beforeunload fires on the webContent's window object
const saveCoverageOnBeforeUnload = () => {
const {app, ipcMain} = require('electron')
ipcMain.on('save-coverage', function (event, coverage, pid) {
saveCoverageData(event.sender, coverage, pid)
})
ipcMain.on('report-coverage', function (event, message) {
saveCoverageData(event.sender, message.coverage, `${message.pid}-extension`)
})
app.on('web-contents-created', function (event, webContents) {
webContents.executeJavaScript(`
window.addEventListener('beforeunload', function () {
require('electron').ipcRenderer.send('save-coverage', window.__coverage__, window.process && window.process.pid)
})
`)
})
}
exports.setupCoverage = () => {
const coverage = global.__coverage__
if (coverage == null) return
rimraf.sync(path.join(outputPath, 'data'))
patchBrowserWindow()
saveCoverageOnBeforeUnload()
}

View file

@ -13,11 +13,12 @@
// Disable use of deprecated functions.
process.throwDeprecation = true;
// Check if we are running in CI.
var path = require('path');
var electron = require ('electron');
var remote = electron.remote;
var ipcRenderer = electron.ipcRenderer;
// Check if we are running in CI.
var isCi = remote.getGlobal('isCi')
if (!isCi) {
@ -47,6 +48,8 @@
});
}
var Coverage = require('electabul').Coverage;
var Mocha = require('mocha');
var mocha = new Mocha();
@ -81,9 +84,17 @@
walker.on('end', function() {
var runner = mocha.run(function() {
Mocha.utils.highlightTags('code');
var coverage = new Coverage({
libPath: path.join(__dirname, '..', '..', 'lib'),
outputPath: path.join(__dirname, '..', '..', 'out', 'coverage'),
formats: ['text', 'lcov']
});
coverage.addCoverage(ipcRenderer.sendSync('get-main-process-coverage'))
coverage.generateReport()
if (isCi)
ipcRenderer.send('process.exit', runner.failures);
require('../coverage/reporter').generateReport()
});
});
})();

View file

@ -8,6 +8,7 @@ const dialog = electron.dialog
const BrowserWindow = electron.BrowserWindow
const protocol = electron.protocol
const Coverage = require('electabul').Coverage
const fs = require('fs')
const path = require('path')
const url = require('url')
@ -63,8 +64,13 @@ ipcMain.on('echo', function (event, msg) {
event.returnValue = msg
})
ipcMain.on('get-coverage', function(event) {
event.returnValue = global.__coverage__
const coverage = new Coverage({
outputPath: path.join(__dirname, '..', '..', 'out', 'coverage')
})
coverage.setup()
ipcMain.on('get-main-process-coverage', function (event) {
event.returnValue = global.__coverage__ || null
})
global.isCi = !!argv.ci
@ -84,8 +90,6 @@ app.on('window-all-closed', function () {
app.quit()
})
require('../coverage/reporter').setupCoverage()
app.on('ready', function () {
// Test if using protocol module would crash.
electron.protocol.registerStringProtocol('test-if-crashes', function () {})