Skip to content

Commit

Permalink
feat(@bazel/jasmine): add coverage reporting
Browse files Browse the repository at this point in the history
Adds Coverage collection support via the V8 Coverage API's
This feature is disabled by default but can be enabled with `coverage = True`
There is a performance overhead (~20%) by enabling this feature
When enabled the only reported is text-summary which outputs the coverage
summary as stdout, but we plan to intergrate with bazel coverage for better reporting
accross a whole repository
  • Loading branch information
Fabian Wiles committed Mar 14, 2019
1 parent 8e28710 commit abe7672
Show file tree
Hide file tree
Showing 7 changed files with 1,072 additions and 11 deletions.
3 changes: 2 additions & 1 deletion packages/jasmine/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"url": "https://github.com/bazelbuild/rules_nodejs/issues"
},
"dependencies": {
"jasmine": "~3.3.1"
"jasmine": "~3.3.1",
"v8-coverage": "1.0.8"
},
"bazelWorkspaces": {
"npm_bazel_jasmine": {
Expand Down
11 changes: 11 additions & 0 deletions packages/jasmine/src/jasmine_node_test.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ def jasmine_node_test(
expected_exit_code = 0,
tags = [],
jasmine = "@npm//@bazel/jasmine",
args = [],
coverage = False,
**kwargs):
"""Runs tests in NodeJS using the Jasmine test runner.
Expand All @@ -42,6 +44,8 @@ def jasmine_node_test(
jasmine: a label providing the jasmine dependency
expected_exit_code: The expected exit code for the test. Defaults to 0.
tags: bazel tags applied to test
args: Any arguments to be passed the test process
coverage: Enables code coverage collection and reporting
**kwargs: remaining arguments are passed to the test rule
"""
devmode_js_sources(
Expand All @@ -51,7 +55,13 @@ def jasmine_node_test(
tags = tags,
)

# append on any user args to the end
args = ["--coverage=%s" % coverage] + args

all_data = data + srcs + deps + [jasmine]

# code coverage reporting dependancy
all_data += ["@npm//v8-coverage"]
all_data += [Label("//:src/jasmine_runner.js")]
all_data += [":%s_devmode_srcs.MF" % name]
all_data += [Label("@bazel_tools//tools/bash/runfiles")]
Expand All @@ -65,5 +75,6 @@ def jasmine_node_test(
testonly = 1,
expected_exit_code = expected_exit_code,
tags = tags,
args = args,
**kwargs
)
87 changes: 78 additions & 9 deletions packages/jasmine/src/jasmine_runner.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');

let jasmineCore = null
let JasmineRunner = require('jasmine/lib/jasmine');
const Execute = require('v8-coverage/src/execute');
const Report = require('v8-coverage/src/report');


if (global.jasmine) {
// global.jasmine has been initialized which means a bootstrap script
// has already required `jasmine-core` and called jasmineCore.boot()
Expand Down Expand Up @@ -55,26 +61,52 @@ if (TOTAL_SHARDS) {
// the maximum (See: https://nodejs.org/api/errors.html#errors_error_stacktracelimit)
Error.stackTraceLimit = Infinity;

const IS_TEST_FILE = /[^a-zA-Z0-9](spec|test)\.js$/i;
const IS_NODE_MODULE = /\/node_modules\//

function main(args) {
if (!args.length) {
throw new Error('Spec file manifest expected argument missing');
}
// first args is always the path to the manifest
const manifest = require.resolve(args[0]);
// second is always a flag to enable coverage or not
const coverageArg = args[1];
let enableCoverage = false;
if (coverageArg.startsWith('--coverage')) {
// Python has capital booleans
enableCoverage = coverageArg.split('=')[1] === 'True';
}
// Remove the manifest, some tested code may process the argv.
process.argv.splice(2, 1)[0];
// Also remove the --coverage flag
process.argv.splice(2, 2)[0];

const jrunner = new JasmineRunner({jasmineCore: jasmineCore});
fs.readFileSync(manifest, UTF8)
.split('\n')
.filter(l => l.length > 0)
const allFiles = fs.readFileSync(manifest, UTF8)
.split('\n')
.filter(l => l.length > 0)
// Filter out files from node_modules that match test.js or spec.js
.filter(f => !IS_NODE_MODULE.test(f))

// the relative directory the coverage reporter uses to find the files
const cwd = process.cwd()

const sourceFiles = allFiles
// Filter out all .spec and .test files so we only report
// coverage against the source files
.filter(f => !IS_TEST_FILE.test(f))
.map(f => require.resolve(f))
// the reporting lib resolves the relative path instead of using the absolute one
// so match it here
.map(f => path.relative(cwd, f))

allFiles
// Filter here so that only files ending in `spec.js` and `test.js`
// are added to jasmine as spec files. This is important as other
// deps such as "@npm//typescript" if executed may cause the test to
// fail or have unexpected side-effects. "@npm//typescript" would
// try to execute tsc, print its help, and process.exit(1)
.filter(f => /[^a-zA-Z0-9](spec|test)\.js$/i.test(f))
// Filter out files from node_modules that match test.js or spec.js
.filter(f => !/\/node_modules\//.test(f))
.filter(f => IS_TEST_FILE.test(f))
.forEach(f => jrunner.addSpecFile(f));

var noSpecsFound = true;
Expand All @@ -87,10 +119,47 @@ function main(args) {
// so we need to add it back
jrunner.configureDefaultReporter({});


let covExecutor;
let covDir;
if (enableCoverage) {
covDir = path.join(process.env['TEST_TMPDIR'], String(crypto.randomBytes(4).readUInt32LE(0)));
covExecutor = new Execute({include: sourceFiles, exclude: []});
covExecutor.startProfiler();
}

jrunner.onComplete((passed) => {
let exitCode = passed ? 0 : BAZEL_EXIT_TESTS_FAILED;
if (noSpecsFound) exitCode = BAZEL_EXIT_NO_TESTS_FOUND;
process.exit(exitCode);

if (enableCoverage) {
covExecutor.stopProfiler((err, data) => {
if (err) {
console.error(err);
process.exit(1);
}
const sourceCoverge = covExecutor.filterResult(data.result);
// we could do this all in memory if we wanted
// just take a look at v8-coverage/src/report.js and reimplement some of those methods
// but we're goign to have to write a file at some point for bazel coverage
// so mayaswell support it now
// the lib expects these paths to exist for some reason
fs.mkdirSync(covDir);
fs.mkdirSync(path.join(covDir, 'tmp'));
// only do a text summary for now
// once we know what format bazel coverage wants we can output
// lcov or some other format
const report = new Report(covDir, ['text-summary']);
report.store(sourceCoverge);
report.generateReport();

process.exit(exitCode);
});
} else {
process.exit(exitCode);
}


});

// Re-do logic from jrunner.execute() here so that
Expand Down Expand Up @@ -138,5 +207,5 @@ function getAllSpecs(jasmineEnv) {
}

if (require.main === module) {
process.exitCode = main(process.argv.slice(2));
process.exitCode = main(process.argv.slice(2));
}
10 changes: 10 additions & 0 deletions packages/jasmine/test/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,13 @@ jasmine_node_test(
jasmine = "@npm//jasmine",
shard_count = 3,
)

jasmine_node_test(
name = "coverage_test",
srcs = [
"coverage.spec.js",
"coverage_source.js",
],
coverage = True,
jasmine = "@npm//jasmine",
)
10 changes: 10 additions & 0 deletions packages/jasmine/test/coverage.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const { isString } = require('./coverage_source');

describe('coverage function', () => {
it('should cover one branch', () => {
expect(isString(2)).toBe(false);
});
it('should cover the other branch', () => {
expect(isString('some string')).toBe(true);
});
});
9 changes: 9 additions & 0 deletions packages/jasmine/test/coverage_source.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
function isString(input) {
if(typeof input === 'string') {
return true;
} else {
return false;
}
}

exports.isString = isString;

0 comments on commit abe7672

Please sign in to comment.