From d23251211b81ed36540c968d5c19a6dad422b530 Mon Sep 17 00:00:00 2001 From: Derek Cormier Date: Thu, 1 Dec 2022 16:00:03 -0800 Subject: [PATCH] feat: jest coverage output --- e2e/case6.sh | 17 ++++++++++++ e2e/case7.sh | 17 ++++++++++++ jest/private/BUILD.bazel | 6 +++++ jest/private/jest_config_template.mjs | 24 ++++++++++++++++- jest/private/jest_test.bzl | 38 +++++++++++++++++++++++++-- jest/private/noop.sh | 2 ++ jest/tests/BUILD.bazel | 19 ++++++++++++++ jest/tests/case6.index.js | 5 ++++ jest/tests/case6.jest.config.js | 0 jest/tests/case6.test.js | 5 ++++ jest/tests/case7.index.js | 5 ++++ jest/tests/case7.test.js | 5 ++++ 12 files changed, 140 insertions(+), 3 deletions(-) create mode 100755 e2e/case6.sh create mode 100755 e2e/case7.sh create mode 100755 jest/private/noop.sh create mode 100644 jest/tests/case6.index.js create mode 100644 jest/tests/case6.jest.config.js create mode 100644 jest/tests/case6.test.js create mode 100644 jest/tests/case7.index.js create mode 100644 jest/tests/case7.test.js diff --git a/e2e/case6.sh b/e2e/case6.sh new file mode 100755 index 0000000..0a094a3 --- /dev/null +++ b/e2e/case6.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +set -o errexit -o nounset -o pipefail + +# Case 6: generate a coverage report +bazel coverage //jest/tests:case6 --instrument_test_targets + +COVERAGE_FILE="bazel-testlogs/jest/tests/case6/coverage.dat" + +if [ ! -f "$COVERAGE_FILE" ]; then + echo "Missing coverage file $COVERAGE_FILE" + exit 1 +fi + +if ! grep -q "foobar" "$COVERAGE_FILE"; then + echo "Coverage file does not contain coverage for foobar function" + exit 1 +fi diff --git a/e2e/case7.sh b/e2e/case7.sh new file mode 100755 index 0000000..303cc29 --- /dev/null +++ b/e2e/case7.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +set -o errexit -o nounset -o pipefail + +# Case 7: generate a coverage report, no supplied jest config file +bazel coverage //jest/tests:case7 --instrument_test_targets + +COVERAGE_FILE="bazel-testlogs/jest/tests/case7/coverage.dat" + +if [ ! -f "$COVERAGE_FILE" ]; then + echo "Missing coverage file $COVERAGE_FILE" + exit 1 +fi + +if ! grep -q "foobar" "$COVERAGE_FILE"; then + echo "Coverage file does not contain coverage for foobar function" + exit 1 +fi diff --git a/jest/private/BUILD.bazel b/jest/private/BUILD.bazel index 8604f21..73f64c9 100644 --- a/jest/private/BUILD.bazel +++ b/jest/private/BUILD.bazel @@ -12,3 +12,9 @@ bzl_library( srcs = ["jest_test.bzl"], visibility = ["//jest:__subpackages__"], ) + +sh_binary( + name = "noop", + srcs = ["noop.sh"], + visibility = ["//visibility:public"], +) diff --git a/jest/private/jest_config_template.mjs b/jest/private/jest_config_template.mjs index a943a0e..86e0e07 100644 --- a/jest/private/jest_config_template.mjs +++ b/jest/private/jest_config_template.mjs @@ -1,10 +1,12 @@ // jest.config.js template for jest_test rule -import { existsSync } from "fs"; +import { existsSync, readFileSync } from "fs"; import * as path from "path"; +import { fileURLToPath } from "url"; // isTest indicates if this is a test target or if this is a binary target generating reference output snapshots for the snapshot updater target const isTest = !!process.env.TEST_TARGET; const updateSnapshots = process.env.JEST_TEST__UPDATE_SNAPSHOTS; +const coverageEnabled = !!"{{COVERAGE_ENABLED}}"; const autoConfReporters = !!"{{AUTO_CONF_REPORTERS}}"; const autoConfTestSequencer = !!"{{AUTO_CONF_TEST_SEQUENCER}}"; const userConfigShortPath = "{{USER_CONFIG_SHORT_PATH}}"; @@ -138,6 +140,26 @@ if (updateSnapshots) { config.snapshotResolver = bazelSnapshotResolverPath; } +if (coverageEnabled) { + config.collectCoverage = true; + config.coverageDirectory = path.dirname(process.env.COVERAGE_OUTPUT_FILE); + config.coverageReporters = [ + "text", + ["lcovonly", { file: path.basename(process.env.COVERAGE_OUTPUT_FILE) }], + ]; + + // Glob pattern paths for which files to cover must be relative to this + // jest config file. + const jestConfigDir = path.dirname(fileURLToPath(import.meta.url)); + + // Only generate coverage for files declared in the COVERAGE_MANIFEST + config.collectCoverageFrom = readFileSync(process.env.COVERAGE_MANIFEST) + .toString("utf8") + .split("\n") + .filter((f) => f != "") + .map((f) => path.relative(jestConfigDir, f)); +} + if (process.env.JS_BINARY__LOG_DEBUG) { console.error( "DEBUG: aspect_rules_jest[jest_test]: config:", diff --git a/jest/private/jest_test.bzl b/jest/private/jest_test.bzl index 8927b5d..337125d 100644 --- a/jest/private/jest_test.bzl +++ b/jest/private/jest_test.bzl @@ -45,9 +45,18 @@ _attrs = dicts.add(js_binary_lib.attrs, { allow_single_file = True, default = Label("//jest/private:jest_config_template.mjs"), ), + # Earlier versions of Bazel expect this attribute to be present. + # https://github.com/bazelbuild/bazel/issues/13978 + # We use a no-op because jest itself generates the coverage. + "_lcov_merger": attr.label( + executable = True, + default = Label("//jest/private:noop"), + cfg = "exec", + ), }) def _impl(ctx): + providers = [] generated_config = ctx.actions.declare_file("%s__jest.config.mjs" % ctx.label.name) user_config = copy_file_to_bin_action(ctx, ctx.file.config) if ctx.attr.config else None @@ -60,6 +69,7 @@ def _impl(ctx): "{{BAZEL_SEQUENCER_SHORT_PATH}}": ctx.file.bazel_sequencer.short_path, "{{BAZEL_SNAPSHOT_REPORTER_SHORT_PATH}}": ctx.file.bazel_snapshot_reporter.short_path, "{{BAZEL_SNAPSHOT_RESOLVER_SHORT_PATH}}": ctx.file.bazel_snapshot_resolver.short_path, + "{{COVERAGE_ENABLED}}": "1" if ctx.coverage_instrumented() else "", "{{JUNIT_REPORTER_SHORT_PATH}}": "../{jest_repository}/node_modules/jest-junit/index.js".format(jest_repository = ctx.attr.jest_repository), "{{USER_CONFIG_SHORT_PATH}}": user_config.short_path if user_config else "", "{{USER_CONFIG_PATH}}": user_config.path if user_config else "", @@ -129,12 +139,36 @@ def _impl(ctx): for target in ctx.attr.data ]) - return [ + if ctx.configuration.coverage_enabled: + providers.append( + coverage_common.instrumented_files_info( + ctx, + source_attributes = ["data"], + extensions = [ + "cjs", + "cjx", + "cts", + "ctx", + "js", + "jsx", + "mjs", + "mjx", + "mts", + "mtx", + "ts", + "tsx", + ], + ), + ) + + providers.append( DefaultInfo( executable = launcher.executable, runfiles = runfiles, ), - ] + ) + + return providers lib = struct( attrs = _attrs, diff --git a/jest/private/noop.sh b/jest/private/noop.sh new file mode 100755 index 0000000..ec5b9e5 --- /dev/null +++ b/jest/private/noop.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env sh +exit 0 diff --git a/jest/tests/BUILD.bazel b/jest/tests/BUILD.bazel index 50a8d41..9b588ca 100644 --- a/jest/tests/BUILD.bazel +++ b/jest/tests/BUILD.bazel @@ -39,3 +39,22 @@ jest_test( "case5.test.js", ], ) + +# Case 6: Coverage reporting (see e2e test) +jest_test( + name = "case6", + config = "case6.jest.config.js", + data = [ + "case6.index.js", + "case6.test.js", + ], +) + +# Case 7: Coverage reporting, no supplied jest config file (see e2e test) +jest_test( + name = "case7", + data = [ + "case7.index.js", + "case7.test.js", + ], +) diff --git a/jest/tests/case6.index.js b/jest/tests/case6.index.js new file mode 100644 index 0000000..26f2e75 --- /dev/null +++ b/jest/tests/case6.index.js @@ -0,0 +1,5 @@ +function foobar() { + return "foobar"; +} + +exports.foobar = foobar; diff --git a/jest/tests/case6.jest.config.js b/jest/tests/case6.jest.config.js new file mode 100644 index 0000000..e69de29 diff --git a/jest/tests/case6.test.js b/jest/tests/case6.test.js new file mode 100644 index 0000000..b84c1e0 --- /dev/null +++ b/jest/tests/case6.test.js @@ -0,0 +1,5 @@ +const { foobar } = require("./case6.index.js"); + +test("foobar", () => { + expect(foobar()).toEqual("foobar"); +}); diff --git a/jest/tests/case7.index.js b/jest/tests/case7.index.js new file mode 100644 index 0000000..26f2e75 --- /dev/null +++ b/jest/tests/case7.index.js @@ -0,0 +1,5 @@ +function foobar() { + return "foobar"; +} + +exports.foobar = foobar; diff --git a/jest/tests/case7.test.js b/jest/tests/case7.test.js new file mode 100644 index 0000000..2e8e17d --- /dev/null +++ b/jest/tests/case7.test.js @@ -0,0 +1,5 @@ +const { foobar } = require("./case7.index.js"); + +test("foobar", () => { + expect(foobar()).toEqual("foobar"); +});