Skip to content

Commit

Permalink
feat(builtin): enable coverage on nodejs_test
Browse files Browse the repository at this point in the history
  • Loading branch information
Fabian Wiles committed Sep 21, 2019
1 parent cefc2ae commit 632c3ff
Show file tree
Hide file tree
Showing 38 changed files with 9,052 additions and 335 deletions.
1 change: 1 addition & 0 deletions internal/node/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ exports_files([
"node_repositories.bzl", # Exported to be consumed for generating skydoc.
"node_launcher.sh",
"node_loader.js",
"process_coverage.js",
"BUILD.nodejs_host_os_alias.tpl",
])

Expand Down
8 changes: 8 additions & 0 deletions internal/node/node.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,14 @@ def _nodejs_binary_impl(ctx):
if k in ctx.var.keys():
env_vars += "export %s=\"%s\"\n" % (k, ctx.var[k])

# indicates that this was run with `bazel coverage`
# and that we should collect and store coverage
# TODO: store coverage in the location bazel tells us
if ctx.configuration.coverage_enabled:
# TODO: not sure if $RANDOM is the best var here
# but we need a new dir between runs
env_vars += "export NODE_V8_COVERAGE=$TEST_TMPDIR/$RANDOM\n"

expected_exit_code = 0
if hasattr(ctx.attr, "expected_exit_code"):
expected_exit_code = ctx.attr.expected_exit_code
Expand Down
105 changes: 105 additions & 0 deletions internal/node/process_coverage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@

const path = require('path')
const fs = require('fs');

const sep = path.sep;

const FILE_PROTOCOL = 'file://';
const IS_TEST_FILE = /[^a-zA-Z0-9](spec|test)\.js$/i;
// TODO: needs proper test
const IS_EXTERNAL = new RegExp(`${sep}external${sep}`);
const IS_THIRD_PARTY = new RegExp(`${sep}build_bazel_rules_nodejs${sep}third_party${sep}`);
const IS_LINKER_SCRIPT =
new RegExp(`${sep}build_bazel_rules_nodejs${sep}internal${sep}linker${sep}index.js`);
const NODE_LOADER_SCRIPT = process.argv[2];

// Run Bazel with --define=VERBOSE_LOGS=1 to enable this logging
const VERBOSE_LOGS = !!process.env['VERBOSE_LOGS'];

function log_verbose(...m) {
if (VERBOSE_LOGS) console.error('[process_coverage.js]', ...m);
}

function panic(m) {
throw new Error(`Internal error! Please run again with
--define=VERBOSE_LOG=1
and file an issue: https://github.com/bazelbuild/rules_nodejs/issues/new?template=bug_report.md
Include as much of the build output as you can without disclosing anything confidential.
Error:
${m}
`);
}

function shouldCoverFile(filePath) {
// some coverage collected is from internal nodejs modules
// these modules don't prefix with a file protocol
// so we can filter them out here
if (!filePath.startsWith(FILE_PROTOCOL)) {
return false;
}
const trimmedPath = filePath.replace(FILE_PROTOCOL, '');

// filter out all in third_party
// such as /third_party/github.com/source-map-support
if (IS_THIRD_PARTY.test(trimmedPath)) {
return false;
}
// fitler out all "external" - this should include node_modules
if (IS_EXTERNAL.test(trimmedPath)) {
return false;
}
// don't show coverage for the spec files themselves
if (IS_TEST_FILE.test(trimmedPath)) {
return false;
}
// filter out scripts added by rules_nodejs
if (IS_LINKER_SCRIPT.test(trimmedPath)) {
return false;
}

// filter out the node_launcher.sh script
if (trimmedPath.includes(NODE_LOADER_SCRIPT)) {
return false;
}

return true;
}

function main() {
const v8_coverage_package = path.resolve(process.cwd(), '../build_bazel_rules_nodejs/third_party/npm/node_modules/v8-coverage');
const Report = require(v8_coverage_package);

const covDir = process.env.NODE_V8_COVERAGE;
const reportReadDir = path.join(covDir, 'tmp')

if(!fs.existsSync(reportReadDir)) {
fs.mkdirSync(reportReadDir)
}

// TODO: allow report type to be configurable
const report = new Report(covDir, ['text-summary']);

const rawV8CoverageFiles = fs.readdirSync(covDir);
// Only store the files we actually want coverage for
// This also converts them to instanbul format
// when we hit report.store()
for (const filePath of rawV8CoverageFiles) {
const fullPath = path.join(covDir, filePath);
if (fs.statSync(fullPath).isFile()) {
const file = fs.readFileSync(fullPath);
const v8Cov = JSON.parse(file);

const filteredResult = v8Cov.result.filter(s => {
return shouldCoverFile(s.url);
});
log_verbose('code covered files', filteredResult.map(a => a.url))

// when we store them like this it also converts them to istanbul format
report.store(filteredResult)
}
}

report.generateReport();
}

main();
10 changes: 10 additions & 0 deletions internal/node/test/coverage/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
load("//:defs.bzl", "nodejs_test")

nodejs_test(
name = "coverage",
data = [
"produces-coverage.js",
"produces-coverage.spec.js",
],
entry_point = ":produces-coverage.spec.js",
)
3 changes: 3 additions & 0 deletions internal/node/test/coverage/produces-coverage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
exports.greaterThan5 = function(input) {
return input > 5;
}
13 changes: 13 additions & 0 deletions internal/node/test/coverage/produces-coverage.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const lib = require('./produces-coverage');
const assert = require('assert');

if (!process.env['NODE_V8_COVERAGE']) {
console.log(
`expected process.env['NODE_V8_COVERAGE'] to be defined - run this with: bazel coverage`);
}

function test() {
assert.equal(lib.greaterThan5(6), true);
}

test();
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@
"typeorm": "0.2.18",
"typescript": "3.1.6",
"unidiff": "1.0.1",
"v8-coverage": "1.0.9",
"webpack": "~4.29.3",
"zone.js": "0.8.29"
},
Expand Down
2 changes: 1 addition & 1 deletion packages/jasmine/src/index.from_src.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ load(":index.bzl", _jasmine_node_test = "jasmine_node_test")

def jasmine_node_test(
deps = [],
jasmine_deps = ["@npm//jasmine", "@npm//jasmine-core", "@npm//v8-coverage"],
jasmine_deps = ["@npm//jasmine", "@npm//jasmine-core"],
**kwargs):
_jasmine_node_test(
# When there is no @npm//@bazel/jasmine package we use @npm_bazel_jasmine instead.
Expand Down
5 changes: 0 additions & 5 deletions packages/jasmine/src/jasmine_node_test.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,6 @@ def jasmine_node_test(
templated_args = kwargs.pop("templated_args", [])
templated_args.append("$(location :%s_devmode_srcs.MF)" % name)

if coverage:
templated_args.append("--coverage")
else:
templated_args.append("--nocoverage")

if config_file:
# Calculate a label relative to the user's BUILD file
pkg = Label("%s//%s:__pkg__" % (native.repository_name(), native.package_name()))
Expand Down
64 changes: 2 additions & 62 deletions packages/jasmine/src/jasmine_runner.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
const fs = require('fs');
const path = require('path');
const bazelJasmine = require('@bazel/jasmine');

const JasmineRunner = bazelJasmine.jasmine;
Expand Down Expand Up @@ -60,22 +59,16 @@ function readArg() {
}

function main(args) {
if (args.length < 3) {
if (args.length < 2) {
throw new Error('expected argument missing');
}


// first args is always the path to the manifest
const manifest = require.resolve(readArg());
// second is always a flag to enable coverage or not
const coverageArg = readArg();
const enableCoverage = coverageArg === '--coverage';
// config file is the next arg
const configFile = readArg();

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

const jrunner = new JasmineRunner({jasmineCore: jasmineCore});
if (configFile !== '--noconfig') {
jrunner.loadConfigFile(require.resolve(configFile));
Expand All @@ -86,17 +79,6 @@ function main(args) {
// 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
Expand All @@ -116,53 +98,11 @@ 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;

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);
}


process.exit(exitCode);
});

if (TOTAL_SHARDS) {
Expand Down
16 changes: 16 additions & 0 deletions scripts/vendor_npm.sh
Original file line number Diff line number Diff line change
Expand Up @@ -281,5 +281,21 @@ $(cat ${THIRD_PARTY_DIR}/yarn.lock | awk '{print "# "$0}')
)
done

##############################################################################
# c8
##############################################################################
(
prep v8-coverage
ncc src/report.js
echo """filegroup(
name = \"package_contents\",
srcs = glob([\"**/*.js\"]) + [
\"BUILD.bazel\",
],
)
""" >> ${DST_DIR}/BUILD.bazel
build_file_footer
)

rm -rf node_modules
)
Loading

0 comments on commit 632c3ff

Please sign in to comment.