Skip to content

Commit

Permalink
Fix coverage base global report creation bug (cherypick of #14547) (#…
Browse files Browse the repository at this point in the history
…14549)

We must create the base global report with the same value of "branch" as the subsequent reports we merge into it.
Otherwise the reports have different formats, and we get a "Can't combine arc data with line data" error.

Fixes #14542.

[ci skip-rust]

[ci skip-build-wheels]
  • Loading branch information
benjyw authored Feb 21, 2022
1 parent 55c9c46 commit 699ef2a
Show file tree
Hide file tree
Showing 2 changed files with 163 additions and 20 deletions.
72 changes: 52 additions & 20 deletions src/python/pants/backend/python/goals/coverage_py.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from enum import Enum
from io import StringIO
from pathlib import PurePath
from typing import cast
from typing import Any, MutableMapping, cast

import toml

Expand Down Expand Up @@ -286,33 +286,42 @@ class InvalidCoverageConfigError(Exception):
pass


def _update_config(fc: FileContent) -> FileContent:
if PurePath(fc.path).suffix == ".toml":
try:
all_config = toml.loads(fc.content.decode())
except toml.TomlDecodeError as exc:
raise InvalidCoverageConfigError(
f"Failed to parse the coverage.py config `{fc.path}` as TOML. Please either fix "
f"the config or update `[coverage-py].config` and/or "
f"`[coverage-py].config_discovery`.\n\nParse error: {repr(exc)}"
)
tool = all_config.setdefault("tool", {})
coverage = tool.setdefault("coverage", {})
run = coverage.setdefault("run", {})
run["relative_files"] = True
if "pytest.pex/*" not in run.get("omit", []):
run["omit"] = [*run.get("omit", []), "pytest.pex/*"]
return FileContent(fc.path, toml.dumps(all_config).encode())
def _parse_toml_config(fc: FileContent) -> MutableMapping[str, Any]:
try:
return toml.loads(fc.content.decode())
except toml.TomlDecodeError as exc:
raise InvalidCoverageConfigError(
f"Failed to parse the coverage.py config `{fc.path}` as TOML. Please either fix "
f"the config or update `[coverage-py].config` and/or "
f"`[coverage-py].config_discovery`.\n\nParse error: {repr(exc)}"
)


def _parse_ini_config(fc: FileContent) -> configparser.ConfigParser:
cp = configparser.ConfigParser()
try:
cp.read_string(fc.content.decode())
return cp
except configparser.Error as exc:
raise InvalidCoverageConfigError(
f"Failed to parse the coverage.py config `{fc.path}` as INI. Please either fix "
f"the config or update `[coverage-py].config` and/or `[coverage-py].config_discovery`."
f"\n\nParse error: {repr(exc)}"
)


def _update_config(fc: FileContent) -> FileContent:
if PurePath(fc.path).suffix == ".toml":
all_config = _parse_toml_config(fc)
tool = all_config.setdefault("tool", {})
coverage = tool.setdefault("coverage", {})
run = coverage.setdefault("run", {})
run["relative_files"] = True
if "pytest.pex/*" not in run.get("omit", []):
run["omit"] = [*run.get("omit", []), "pytest.pex/*"]
return FileContent(fc.path, toml.dumps(all_config).encode())

cp = _parse_ini_config(fc)
run_section = "coverage:run" if fc.path in ("tox.ini", "setup.cfg") else "run"
if not cp.has_section(run_section):
cp.add_section(run_section)
Expand All @@ -326,6 +335,21 @@ def _update_config(fc: FileContent) -> FileContent:
return FileContent(fc.path, stream.getvalue().encode())


def get_branch_value_from_config(fc: FileContent) -> bool:
# Note that coverage's default value for the branch setting is False, which we mirror here.
if PurePath(fc.path).suffix == ".toml":
all_config = _parse_toml_config(fc)
return bool(
all_config.get("tool", {}).get("coverage", {}).get("run", {}).get("branch", False)
)

cp = _parse_ini_config(fc)
run_section = "coverage:run" if fc.path in ("tox.ini", "setup.cfg") else "run"
if not cp.has_section(run_section):
return False
return cp.getboolean(run_section, "branch", fallback=False)


@rule
async def create_or_update_coverage_config(coverage: CoverageSubsystem) -> CoverageConfig:
config_files = await Get(ConfigFiles, ConfigFilesRequest, coverage.config_request)
Expand Down Expand Up @@ -374,6 +398,7 @@ class MergedCoverageData:
async def merge_coverage_data(
data_collection: PytestCoverageDataCollection,
coverage_setup: CoverageSetup,
coverage_config: CoverageConfig,
coverage: CoverageSubsystem,
source_roots: AllSourceRoots,
) -> MergedCoverageData:
Expand All @@ -393,8 +418,13 @@ async def merge_coverage_data(
addresses.append(data.address)

if coverage.global_report:
# It's important to set the `branch` value in the empty base report to the value it will
# have when running on real inputs, so that the reports are of the same type, and can be
# merged successfully. Otherwise we may get "Can't combine arc data with line data" errors.
# See https://github.com/pantsbuild/pants/issues/14542 .
config_contents = await Get(DigestContents, Digest, coverage_config.digest)
branch = get_branch_value_from_config(config_contents[0]) if config_contents else False
global_coverage_base_dir = PurePath("__global_coverage__")

global_coverage_config_path = global_coverage_base_dir / "pyproject.toml"
global_coverage_config_content = toml.dumps(
{
Expand All @@ -403,6 +433,7 @@ async def merge_coverage_data(
"run": {
"relative_files": True,
"source": list(source_root.path for source_root in source_roots),
"branch": branch,
}
}
}
Expand Down Expand Up @@ -460,7 +491,8 @@ async def merge_coverage_data(
ProcessResult,
VenvPexProcess(
coverage_setup.pex,
argv=("combine", *sorted(coverage_data_file_paths)),
# We tell combine to keep the original input files, to aid debugging in the sandbox.
argv=("combine", "--keep", *sorted(coverage_data_file_paths)),
input_digest=input_digest,
output_files=(".coverage",),
description=f"Merge {len(coverage_data_file_paths)} Pytest coverage reports.",
Expand Down
111 changes: 111 additions & 0 deletions src/python/pants/backend/python/goals/coverage_py_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from pants.backend.python.goals.coverage_py import (
CoverageSubsystem,
create_or_update_coverage_config,
get_branch_value_from_config,
)
from pants.core.util_rules.config_files import ConfigFiles, ConfigFilesRequest
from pants.engine.fs import (
Expand Down Expand Up @@ -224,3 +225,113 @@ def test_update_run_section() -> None:
""" # noqa: W291
)
)


def branch(path: str, content: str) -> bool:
fc = FileContent(path, content.encode())
return get_branch_value_from_config(fc)


def test_get_branch_value_from_config() -> None:
assert (
branch(
"pyproject.toml",
dedent(
"""\
[tool.coverage.run]
relative_files = false
foo = "bar"
"""
),
)
is False
)

assert (
branch(
"pyproject.toml",
dedent(
"""\
[tool.coverage.run]
relative_files = false
branch = true
foo = "bar"
"""
),
)
is True
)

assert (
branch(
"pyproject.toml",
dedent(
"""\
[tool.coverage]
[tool.coverage.run]
branch = true
"""
),
)
is True
)

assert (
branch(
".coveragerc",
dedent(
"""\
[run]
relative_files: False
branch: False
foo: bar
"""
),
)
is False
)

assert (
branch(
".coveragerc",
dedent(
"""\
[run]
relative_files: False
branch: True
foo: bar
"""
),
)
is True
)

assert (
branch(
"setup.cfg",
dedent(
"""\
[coverage:run]
relative_files: False
branch: False
foo: bar
"""
),
)
is False
)

assert (
branch(
"setup.cfg",
dedent(
"""\
[coverage:run]
relative_files: False
branch: True
foo: bar
"""
),
)
is True
)

0 comments on commit 699ef2a

Please sign in to comment.