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 19, 2019
1 parent dea4201 commit dc8063f
Show file tree
Hide file tree
Showing 10 changed files with 1,102 additions and 11 deletions.
9 changes: 9 additions & 0 deletions e2e/jasmine/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,12 @@ jasmine_node_test(
"@npm//zone.js",
],
)

jasmine_node_test(
name = "coverage_test",
srcs = [
"coverage.spec.js",
"coverage_source.js",
],
coverage = True,
)
10 changes: 10 additions & 0 deletions e2e/jasmine/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 e2e/jasmine/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;
3 changes: 2 additions & 1 deletion packages/jasmine/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
],
"main": "index.js",
"dependencies": {
"jasmine": "~3.3.1"
"jasmine": "~3.3.1",
"v8-coverage": "1.0.8"
},
"bazelWorkspaces": {
"npm_bazel_jasmine": {
Expand Down
11 changes: 10 additions & 1 deletion packages/jasmine/src/jasmine_node_test.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ def jasmine_node_test(
expected_exit_code = 0,
tags = [],
jasmine = Label("@npm//@bazel/jasmine"),
coverage = False,
**kwargs):
"""Runs tests in NodeJS using the Jasmine test runner.
Expand All @@ -42,6 +43,7 @@ def jasmine_node_test(
expected_exit_code: The expected exit code for the test. Defaults to 0.
tags: bazel tags applied to test
jasmine: a label providing the jasmine dependency
coverage: Enables code coverage collection and reporting
**kwargs: remaining arguments are passed to the test rule
"""
devmode_js_sources(
Expand All @@ -52,15 +54,22 @@ def jasmine_node_test(
)

all_data = data + srcs + deps + [jasmine]

all_data += [":%s_devmode_srcs.MF" % name]
all_data += [Label("@bazel_tools//tools/bash/runfiles")]
entry_point = "@bazel/jasmine/src/jasmine_runner.js"

templated_args = ["$(location :%s_devmode_srcs.MF)" % name]
if coverage:
templated_args = templated_args + ["--coverage"]
else:
templated_args = templated_args + ["--nocoverage"]

nodejs_test(
name = name,
data = all_data,
entry_point = entry_point,
templated_args = ["$(location :%s_devmode_srcs.MF)" % name],
templated_args = templated_args,
testonly = 1,
expected_exit_code = expected_exit_code,
tags = tags,
Expand Down
85 changes: 77 additions & 8 deletions packages/jasmine/src/jasmine_runner.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const fs = require('fs');
const path = require('path');

let jasmineCore = null
let JasmineRunner = require('jasmine/lib/jasmine');
Expand Down Expand Up @@ -55,26 +56,51 @@ 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];
const enableCoverage = coverageArg === '--coverage';

// 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];

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

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
.filter(f => !IS_NODE_MODULE.test(f))

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))
// the jasmine_runner.js gets in here as a file to run
.filter(f => !f.endsWith('jasmine_runner.js'))
.map(f => require.resolve(f))
// the reporting lib resolves the relative path to our cwd 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 +113,53 @@ function main(args) {
// so we need to add it back
jrunner.configureDefaultReporter({});


let covExecutor;
let covDir;
if (enableCoverage) {
// lazily pull these deps in for only when we want to collect coverage
const crypto = require('crypto');
const Execute = require('v8-coverage/src/execute');

// make a tmpdir inside our tmpdir for just this run
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) {
const Report = require('v8-coverage/src/report');
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 going to have to write a file at some point for bazel coverage
// so may as well 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);
}


});

if (TOTAL_SHARDS) {
Expand Down
14 changes: 14 additions & 0 deletions packages/jasmine/test/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,17 @@ jasmine_node_test(
tags = ["manual"],
deps = ["//:jasmine_runner"],
)

jasmine_node_test(
name = "coverage_test",
srcs = [
"coverage.spec.js",
"coverage_source.js",
],
coverage = True,
jasmine = "@npm//jasmine",
deps = [
"//:jasmine_runner",
"@npm//v8-coverage",
],
)
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;
Loading

0 comments on commit dc8063f

Please sign in to comment.