Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

rules/python: Add a coverage_tool attribute to py_runtime. #15590

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
72a0f9e
rules/python: Rework built-in coveragepy support
TLATER Jan 31, 2022
80a6cb9
Add a coverage_tool attribute to py_runtime target.
adam-azarchs May 27, 2022
55f1aed
Merge commit '72a0f9ed13e4b1f561aee4268e4919b84ac04395' into azarchs/…
adam-azarchs Aug 11, 2022
de88f81
Fix up filenames in pycoverage-generated lcov output.
adam-azarchs May 31, 2022
437934e
Properly add runfiles for coverage target.
adam-azarchs Jun 7, 2022
37b4286
Add integration test for python coverage.
adam-azarchs Jun 7, 2022
b472c21
Update documentation.
adam-azarchs Jun 7, 2022
f2c9cd2
Add requires-network to bazel_coverage_hermetic_py_test.
adam-azarchs Jun 7, 2022
57d88fb
Deduplicate cov_tool path entry.
adam-azarchs Aug 11, 2022
e9d5582
Fix bad merge conflict resolution.
adam-azarchs Aug 11, 2022
9bb352d
Use a mock coverage tool in unit test.
adam-azarchs Aug 19, 2022
4d628d3
Merge remote-tracking branch 'origin/master' into azarchs/py_runtime-…
adam-azarchs Aug 19, 2022
1746437
Don't use parse_intermixed_args().
adam-azarchs Aug 19, 2022
fed8618
Make mock_coverage.py executable.
adam-azarchs Aug 19, 2022
baed9a2
Remove platform constraints from test python_toolchain.
adam-azarchs Aug 19, 2022
337f928
Address comments from code review.
adam-azarchs Aug 31, 2022
fc21d6a
Merge remote-tracking branch 'origin/master' into azarchs/py_runtime-…
adam-azarchs Aug 31, 2022
7c9b34f
Ignore failures unlinking the rcfile.
adam-azarchs Sep 1, 2022
4550f70
Fix docstring style.
adam-azarchs Sep 1, 2022
ec50345
Detail the requrements for the coverage_tool.
adam-azarchs Sep 5, 2022
3b892de
Factor out VERBOSE_COVERAGE handling in stub.
adam-azarchs Sep 6, 2022
1e8ee96
Merge remote-tracking branch 'origin/master' into azarchs/py_runtime-…
adam-azarchs Sep 7, 2022
4242f74
Merge remote-tracking branch 'origin/master' into azarchs/py_runtime-…
adam-azarchs Sep 19, 2022
b95db8e
Merge branch 'bazelbuild:master' into azarchs/py_runtime-coverage
adam-azarchs Sep 22, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is probably an optimization to be made somewhere in here to only have the coverage dependency when coverage is enabled. The isCodeCoverage() check in the Java code prevents it from being included at runtime, but it still causes Bazel to do various target and config resolution steps.

Coverage being enabled can be detected with a config setting, e.g.

config_setting(
    name = "coverage_enabled",
    values = {"collect_code_coverage": "true"},
)

py_library(
    name = "coverage_if_enabled",
    deps = select({
        ":coverage_enabled": [
            ":coverage",
        ],
        "//conditions:default": [],
    }),
)

I'm not sure how that fits into toolchain registration; can selects be used within WORKSPACE?

In any case, I'm find with figuring this out in a separate PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Declaration of the toolchain is in a BUILD file, not the WORKSPACE. However, I am not sure either about how select gets evaluated in toolchain resolution. As I commented elsewhere I think eventually this bit of the doc should be just baked into python_register_toolchains (in rules_python) so that most users don't have to care about it.

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()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (ruleContext.getConfiguration().isCodeCoverageEnabled()) {
// 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, bazelConfig);

// 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,32 @@ 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, BazelPythonConfiguration bazelConfig) {
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