Skip to content

Commit

Permalink
rules/python: Add a coverage_tool attribute to py_runtime.
Browse files Browse the repository at this point in the history
This allows users to specify a target providing the coveragepy tool (and its dependencies).  This is essential for hermetic python builds, where an absolute path will not really work.  It's also superior to other potential methods using environment variables because the runfiles dependency on the coverage tool and its files is only incurred when building with coverage enabled.

This also builds on the work @TLATER began with #14677 to integrate with `coveragepy`'s `lcov` support, with an additional step of at least attempting to convert the absolute paths which `coveragepy` uses in the lcov output into the relative paths which the rest of bazel can actually consume.

This is my first time touching Java code professionally, so I'll admit to mostly cargo-culting those parts, and would welcome any feedback on how to improve things there.  I also would have no objections to someone else taking over this PR to get it over the finish line.  I've tested this out with our own team's internal monorepo, and have successfully generated a full combined coverage report for most of our python and go code.  There's still a bunch of things which don't quite work, in particular when it comes to compiled extension modules or executables run from within python tests, but those will need to be addressed separately, and this is already a giant step forward for our team.

Closes #14436.

Closes #15590.

PiperOrigin-RevId: 476314433
Change-Id: I4be4d10e0af741f4ba1a7b5367c6f7a338a3c43d
  • Loading branch information
adam-azarchs authored and copybara-github committed Sep 23, 2022
1 parent 4c56431 commit 9d01630
Show file tree
Hide file tree
Showing 10 changed files with 680 additions and 50 deletions.
55 changes: 54 additions & 1 deletion site/en/configure/coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,61 @@ py_test(
],
)
```
<!-- TODO: Allow specifying a target for `PYTHON_COVERAGE`, instead of having to use `$(location)` -->

If you are using a hermetic Python toolchain, instead of adding the coverage
dependency to every `py_test` target you can instead add the coverage tool to
the toolchain configuration.

Because the [pip_install][pip_install_rule] rule depends on the Python
toolchain, it cannot be used to fetch the `coverage` module.
Instead, add in your `WORKSPACE` e.g.

```starlark
http_archive(
name = "coverage_linux_x86_64"",
build_file_content = """
py_library(
name = "coverage",
srcs = ["coverage/__main__.py"],
data = glob(["coverage/*", "coverage/**/*.py"]),
visibility = ["//visibility:public"],
)
""",
sha256 = "84631e81dd053e8a0d4967cedab6db94345f1c36107c71698f746cb2636c63e3",
type = "zip",
urls = [
"https://files.pythonhosted.org/packages/74/0d/0f3c522312fd27c32e1abe2fb5c323b583a5c108daf2c26d6e8dfdd5a105/coverage-6.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
],
)
```

Then configure your python toolchain as e.g.

```starlark
py_runtime(
name = "py3_runtime_linux_x86_64",
coverage_tool = "@coverage_linux_x86_64//:coverage",
files = ["@python3_9_x86_64-unknown-linux-gnu//:files"],
interpreter = "@python3_9_x86_64-unknown-linux-gnu//:bin/python3",
python_version = "PY3",
)

py_runtime_pair(
name = "python_runtimes_linux_x86_64",
py2_runtime = None,
py3_runtime = ":py3_runtime_linux_x86_64",
)

toolchain(
name = "python_toolchain_linux_x86_64",
exec_compatible_with = [
"@platforms//os:linux",
"@platforms//cpu:x86_64",
],
toolchain = ":python_runtimes_linux_x86_64",
toolchain_type = "@bazel_tools//tools/python:toolchain_type",
)
```

[lcov]: https://github.com/linux-test-project/lcov
[rules_python]: https://github.com/bazelbuild/rules_python
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,20 @@ public boolean prohibitHyphensInPackagePaths() {
public void collectRunfilesForBinary(
RuleContext ruleContext, Runfiles.Builder builder, PyCommon common, CcInfo ccInfo) {
addRuntime(ruleContext, common, builder);
// select() and build configuration should ideally remove coverage as
// as dependency, but guard against including it at runtime just in case.
if (ruleContext.getConfiguration().isCodeCoverageEnabled()) {
addCoverageSupport(ruleContext, common, builder);
}
}

@Override
public void collectDefaultRunfilesForBinary(
RuleContext ruleContext, PyCommon common, Runfiles.Builder builder) {
addRuntime(ruleContext, common, builder);
if (ruleContext.getConfiguration().isCodeCoverageEnabled()) {
addCoverageSupport(ruleContext, common, builder);
}
}

@Override
Expand Down Expand Up @@ -154,6 +162,9 @@ private static void createStubFile(
// first-stage.
String pythonBinary = getPythonBinary(ruleContext, common, bazelConfig);

// The python code coverage tool to use, if any.
String coverageTool = getCoverageTool(ruleContext, common);

// Version information for host config diagnostic warning.
PythonVersion attrVersion = PyCommon.readPythonVersionFromAttribute(ruleContext.attributes());
boolean attrVersionSpecifiedExplicitly = attrVersion != null;
Expand All @@ -172,6 +183,7 @@ private static void createStubFile(
Substitution.of(
"%main%", common.determineMainExecutableSource(/*withWorkspaceName=*/ true)),
Substitution.of("%python_binary%", pythonBinary),
Substitution.of("%coverage_tool%", coverageTool == null ? "" : coverageTool),
Substitution.of("%imports%", Joiner.on(":").join(common.getImports().toList())),
Substitution.of("%workspace_name%", ruleContext.getWorkspaceName()),
Substitution.of("%is_zipfile%", boolToLiteral(isForZipFile)),
Expand Down Expand Up @@ -461,6 +473,31 @@ private static String getPythonBinary(
return pythonBinary;
}

private static void addCoverageSupport(
RuleContext ruleContext, PyCommon common, Runfiles.Builder builder) {
PyRuntimeInfo provider = getRuntime(ruleContext, common);
if (provider != null && provider.getCoverageTool() != null) {
builder.addArtifact(provider.getCoverageTool());
builder.addTransitiveArtifacts(provider.getCoverageToolFiles());
}
}

@Nullable
private static String getCoverageTool(RuleContext ruleContext, PyCommon common) {
if (!ruleContext.getConfiguration().isCodeCoverageEnabled()) {
return null;
}
String coverageTool = null;
PyRuntimeInfo provider = getRuntime(ruleContext, common);
if (provider != null && provider.getCoverageTool() != null) {
PathFragment workspaceName =
PathFragment.create(ruleContext.getRule().getPackage().getWorkspaceName());
coverageTool =
workspaceName.getRelative(provider.getCoverageTool().getRunfilesPath()).getPathString();
}
return coverageTool;
}

private static String getStubShebang(RuleContext ruleContext, PyCommon common) {
PyRuntimeInfo provider = getRuntime(ruleContext, common);
if (provider != null) {
Expand Down
Loading

0 comments on commit 9d01630

Please sign in to comment.