Skip to content

Commit

Permalink
feat(builtin): enable coverage on nodejs_test
Browse files Browse the repository at this point in the history
BREAKING CHANGE: jasmine_node_test not longer has the `coverage`
attribute
  • Loading branch information
Fabian Wiles committed Apr 16, 2020
1 parent ad2eba1 commit ddf1efb
Show file tree
Hide file tree
Showing 23 changed files with 708 additions and 220 deletions.
1 change: 1 addition & 0 deletions BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ pkg_npm(
"//internal/bazel_integration_test:package_contents",
"//internal/common:package_contents",
"//internal/copy_repository:package_contents",
"//internal/coverage:package_contents",
"//internal/golden_file_test:package_contents",
"//internal/js_library:package_contents",
"//internal/linker:package_contents",
Expand Down
3 changes: 3 additions & 0 deletions common.bazelrc
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ common --experimental_allow_incremental_repository_updates
build --incompatible_strict_action_env
run --incompatible_strict_action_env

# when running `bazel coverage` ensure that the test targets are instrumented
coverage --instrument_test_targets

# Load any settings specific to the current user.
# .bazelrc.user should appear in .gitignore so that settings are not shared with team members
# This needs to be last statement in this
Expand Down
9 changes: 0 additions & 9 deletions e2e/jasmine/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,3 @@ jasmine_node_test(
"@npm//zone.js",
],
)

jasmine_node_test(
name = "coverage_test",
srcs = [
"coverage.spec.js",
"coverage_source.js",
],
coverage = True,
)
10 changes: 0 additions & 10 deletions e2e/jasmine/coverage.spec.js

This file was deleted.

36 changes: 36 additions & 0 deletions internal/coverage/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
load("@build_bazel_rules_nodejs//:index.bzl", "nodejs_binary")

# BEGIN-INTERNAL
load("@npm_bazel_typescript//:index.from_src.bzl", "checked_in_ts_library")

checked_in_ts_library(
name = "lcov_merger_js_lib",
srcs = ["lcov_merger.ts"],
checked_in_js = "lcov_merger.js",
visibility = ["//visibility:public"],
deps = ["@npm//@types/node"],
)
# END-INTERNAL

nodejs_binary(
name = "lcov_merger_js",
entry_point = "lcov_merger.js",
visibility = ["//visibility:public"],
)

sh_binary(
name = "lcov_merger_sh",
srcs = ["lcov_merger.sh"],
visibility = ["//visibility:public"],
)

filegroup(
name = "package_contents",
srcs = glob([
"*.sh",
"*.js",
]) + [
"BUILD.bazel",
],
visibility = ["//:__pkg__"],
)
117 changes: 117 additions & 0 deletions internal/coverage/lcov_merger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/* THIS FILE GENERATED FROM .ts; see BUILD.bazel */ /* clang-format off *//**
* @license
* Copyright 2017 The Bazel Authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
(function (factory) {
if (typeof module === "object" && typeof module.exports === "object") {
var v = factory(require, exports);
if (v !== undefined) module.exports = v;
}
else if (typeof define === "function" && define.amd) {
define("build_bazel_rules_nodejs/internal/coverage/lcov_merger", ["require", "exports", "crypto", "fs", "path"], factory);
}
})(function (require, exports) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const crypto = require("crypto");
const fs = require("fs");
const path = require("path");
function getArg(argv, key) {
return argv.find(a => a === key).split('=')[1];
}
/**
* This is designed to collect the coverage of one target, since in nodejs
* and using NODE_V8_COVERAGE it may produce more than one coverage file, however bazel expects
* there to be only one lcov file. So this collects up the v8 coverage json's merges them and
* converts them to lcov for bazel to pick up later.
* any tool reporting coverage not just jasmine
*/
function main() {
return __awaiter(this, void 0, void 0, function* () {
// see here for what args are passed in
// https://github.com/bazelbuild/bazel/blob/master/tools/test/collect_coverage.sh#L175-L181
const argv = process.argv;
const coverageDir = getArg(argv, 'coverage_dir');
const outputFile = getArg(argv, 'output_file');
const sourceFileManifest = getArg(argv, 'source_file_manifest');
const tmpdir = process.env.TEST_TMPDIR;
if (!sourceFileManifest || !tmpdir || !outputFile) {
throw new Error();
}
const instrumentedSourceFiles = fs.readFileSync(sourceFileManifest).toString('utf8').split('\n');
// c8 will name the output report file lcov.info
// so we give it a dir that it can write to
// later on we'll move and rename it into output_file as bazel expects
const c8OutputDir = path.join(tmpdir, crypto.randomBytes(4).toString('hex'));
fs.mkdirSync(c8OutputDir);
const includes = instrumentedSourceFiles
// the manifest may include files such as .bash so we want to reduce that down to the set
// we can run coverage on in JS
.filter(f => ['.js', '.jsx', 'cjs', '.ts', '.tsx', '.mjs'].includes(path.extname(f)))
.map(f => {
// at runtime we only run .js or .mjs
// meaning that the coverage written by v8 will only include urls to .js or .mjs
// so the source files need to be mapped from their input to output extensions
// TODO: how do we know what source files produce .mjs or cjs?
const p = path.parse(f);
let targetExt;
switch (p.ext) {
case '.mjs':
targetExt = '.mjs';
default:
targetExt = '.js';
}
return path.format(Object.assign(Object.assign({}, p), { base: undefined, ext: targetExt }));
});
// only require in c8 when we're actually going to do some coverage
const c8 = require('c8');
// see https://github.com/bcoe/c8/blob/master/lib/report.js
// for more info on this function
// TODO: enable the --all functionality
yield new c8
.Report({
include: includes,
// the test-exclude lib will include everything if our includes array is empty
// so instead when it's empty exclude everything
// but when it does have a value, we only want to use those includes, so don't exclude
// anything
exclude: includes.length === 0 ? ['**'] : [],
reportsDirectory: c8OutputDir,
// tempDirectory as actually the dir that c8 will read from for the v8 json files
tempDirectory: coverageDir,
resolve: '',
// TODO: maybe add an attribute to allow more reporters
// or maybe an env var?
reporter: ['lcovonly']
})
.run();
// moves the report into the files bazel expects
// lcovonly names this file lcov.info
fs.copyFileSync(path.join(c8OutputDir, 'lcov.info'), outputFile);
});
}
if (require.main === module) {
main();
}
});
24 changes: 24 additions & 0 deletions internal/coverage/lcov_merger.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/usr/bin/env bash

# @license
# Copyright 2017 The Bazel Authors. All rights reserved.

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.

# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# This is a workaround for bazelbuild/bazel#6293. Since Bazel 0.18.0, Bazel
# expects tests to have an "$lcov_merger' or "_lcov_merger" attribute that
# points to an executable. If this is missing, the test driver fails.

# Copied from https://github.com/bazelbuild/rules_go/blob/64c97b54ea2918fc7f1a59d68cd27d1fdb0bd663/go/tools/builders/lcov_merger.sh

exit 0
105 changes: 105 additions & 0 deletions internal/coverage/lcov_merger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/**
* @license
* Copyright 2017 The Bazel Authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import * as crypto from 'crypto';
import * as fs from 'fs';
import * as path from 'path';

function getArg(argv: string[], key: string): string {
return argv.find(a => a === key)!.split('=')[1];
}

/**
* This is designed to collect the coverage of one target, since in nodejs
* and using NODE_V8_COVERAGE it may produce more than one coverage file, however bazel expects
* there to be only one lcov file. So this collects up the v8 coverage json's merges them and
* converts them to lcov for bazel to pick up later.
* any tool reporting coverage not just jasmine
*/
async function main() {
// see here for what args are passed in
// https://github.com/bazelbuild/bazel/blob/master/tools/test/collect_coverage.sh#L175-L181
const argv = process.argv;
const coverageDir = getArg(argv, 'coverage_dir');
const outputFile = getArg(argv, 'output_file');
const sourceFileManifest = getArg(argv, 'source_file_manifest');
const tmpdir = process.env.TEST_TMPDIR;

if (!sourceFileManifest || !tmpdir || !outputFile) {
throw new Error();
}

const instrumentedSourceFiles = fs.readFileSync(sourceFileManifest).toString('utf8').split('\n');

// c8 will name the output report file lcov.info
// so we give it a dir that it can write to
// later on we'll move and rename it into output_file as bazel expects
const c8OutputDir = path.join(tmpdir!, crypto.randomBytes(4).toString('hex'));
fs.mkdirSync(c8OutputDir);

const includes =
instrumentedSourceFiles
// the manifest may include files such as .bash so we want to reduce that down to the set
// we can run coverage on in JS
.filter(f => ['.js', '.jsx', 'cjs', '.ts', '.tsx', '.mjs'].includes(path.extname(f)))
.map(f => {
// at runtime we only run .js or .mjs
// meaning that the coverage written by v8 will only include urls to .js or .mjs
// so the source files need to be mapped from their input to output extensions
// TODO: how do we know what source files produce .mjs or cjs?
const p = path.parse(f);
let targetExt;
switch (p.ext) {
case '.mjs':
targetExt = '.mjs';
default:
targetExt = '.js';
}

return path.format({...p, base: undefined, ext: targetExt});
});

// only require in c8 when we're actually going to do some coverage
const c8 = require('c8');
// see https://github.com/bcoe/c8/blob/master/lib/report.js
// for more info on this function
// TODO: enable the --all functionality
await new c8
.Report({
include: includes,
// the test-exclude lib will include everything if our includes array is empty
// so instead when it's empty exclude everything
// but when it does have a value, we only want to use those includes, so don't exclude
// anything
exclude: includes.length === 0 ? ['**'] : [],
reportsDirectory: c8OutputDir,
// tempDirectory as actually the dir that c8 will read from for the v8 json files
tempDirectory: coverageDir,
resolve: '',
// TODO: maybe add an attribute to allow more reporters
// or maybe an env var?
reporter: ['lcovonly']
})
.run();
// moves the report into the files bazel expects
// lcovonly names this file lcov.info
fs.copyFileSync(path.join(c8OutputDir, 'lcov.info'), outputFile);
}

if (require.main === module) {
main();
}
3 changes: 3 additions & 0 deletions internal/node/launcher.sh
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ fi
export RUNFILES
# --- end RUNFILES initialization ---

# TODO: debug - remove this
set -x

TEMPLATED_env_vars

# Note: for debugging it is useful to see what files are actually present
Expand Down
20 changes: 20 additions & 0 deletions internal/node/node.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,14 @@ fi
elif k in ctx.configuration.default_shell_env.keys():
env_vars += "export %s=\"%s\"\n" % (k, ctx.configuration.default_shell_env[k])

# indicates that this was run with `bazel coverage`
if ctx.configuration.coverage_enabled:
# indicates that we have files to instrumented somewhere in the deps
if (ctx.coverage_instrumented() or any([ctx.coverage_instrumented(dep) for dep in ctx.attr.data])):
# we export NODE_V8_COVERAGE here to tell V8 to collect coverage
# then when the nodejs process exists it'll write it to COVERAGE_DIR
env_vars += "export NODE_V8_COVERAGE=$COVERAGE_DIR\n"

expected_exit_code = 0
if hasattr(ctx.attr, "expected_exit_code"):
expected_exit_code = ctx.attr.expected_exit_code
Expand Down Expand Up @@ -315,6 +323,11 @@ fi
deps = depset([ctx.file.entry_point], transitive = [node_modules, sources]),
pkgs = ctx.attr.data,
),
# indicates that the this binary should be instrumented by coverage
# see https://docs.bazel.build/versions/master/skylark/lib/coverage_common.html
# since this will be called from a nodejs_test, where the entrypoint is going to be the test file
# we shouldn't add the entrypoint as a attribute to collect here
coverage_common.instrumented_files_info(ctx, dependency_attributes = ["data"], extensions = ["js", "ts"]),
]

_NODEJS_EXECUTABLE_ATTRS = {
Expand Down Expand Up @@ -628,6 +641,13 @@ nodejs_test = rule(
doc = "The expected exit code for the test. Defaults to 0.",
default = 0,
),
# See the content of lcov_merger_sh for the reason we need this
"_lcov_merger": attr.label(
executable = True,
default = Label("@build_bazel_rules_nodejs//internal/coverage:lcov_merger_sh"),
# default = Label("@build_bazel_rules_nodejs//internal/coverage:lcov_merger_js"),
cfg = "target",
),
}),
doc = """
Identical to `nodejs_binary`, except this can be used with `bazel test` as well.
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,8 @@
"typeorm": "0.2.18",
"typescript": "^3.7.5",
"unidiff": "1.0.1",
"v8-coverage": "1.0.9",
"yarn": "1.22.0",
"yarn": "1.21.1",
"c8": "7.1.0",
"zone.js": "0.8.29"
},
"// resolutions/**/jest-serializer": "need to resolve to a version that doesn't have a /// <reference path='../v8.d.ts' /> in build/index.d.ts which exposes a bug in ts_library",
Expand Down
Loading

0 comments on commit ddf1efb

Please sign in to comment.