Skip to content

Commit

Permalink
[internal] add mark and fixture for lockfile generation, take #2 (#15271
Browse files Browse the repository at this point in the history
)

Add a `jvm_lockfile` pytest fixture and related custom mark `pytest.mark.jvm_lockfile` for defining lockfiles for use in tests. This allows avoiding having to replicate lockfile content in code using `TestCoursierWrapper`, which can become unwiedly once a resolve in complicated enough.

Lockfiles are generated by running `./pants internal-generate-test-lockfile-fixtures ::` which collects all tests marked with `pytest.mark.jvm_lockfile`, resolves their requirements, and writes out the resulting lockfiles. A custom Pants goal is used so that the plugin code gets access to the Pants source tree and targets. (An earlier iteration of this PR tried to use a `RuleRunner` in a script under `build-support/bin`, but `RuleRunner` is used for testing in an isolated directory and has no access to the Pants repository sources.)
  • Loading branch information
Tom Dyas authored May 9, 2022
1 parent 6e546bd commit a95d9ce
Show file tree
Hide file tree
Showing 12 changed files with 448 additions and 4 deletions.
14 changes: 13 additions & 1 deletion build-support/bin/_release_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,13 @@
_expected_maintainers = {"EricArellano", "gshuflin", "illicitonion", "wisechengyi"}


# Disable the Pants repository-internal internal_plugins.test_lockfile_fixtures plugin because
# otherwise inclusion of that plugin will fail due to its `pytest` import not being included in the pex.
DISABLED_BACKENDS_CONFIG = {
"PANTS_BACKEND_PACKAGES": '-["internal_plugins.test_lockfile_fixtures"]',
}


class PackageAccessValidator:
@classmethod
def validate_all(cls):
Expand Down Expand Up @@ -298,6 +305,10 @@ def run_venv_pants(args: list[str]) -> str:
],
check=True,
stdout=subprocess.PIPE,
env={
**os.environ,
**DISABLED_BACKENDS_CONFIG,
},
)
.stdout.decode()
.strip()
Expand Down Expand Up @@ -852,8 +863,9 @@ def build_pex(fetch: bool) -> None:
shutil.copyfile(dest, validated_pex_path)
validated_pex_path.chmod(0o777)
Path(tmpdir, "BUILD_ROOT").touch()
# We also need to filter out Pants options like `PANTS_CONFIG_FILES`.
# We also need to filter out Pants options like `PANTS_CONFIG_FILES` and disable certain internal backends.
env = {k: v for k, v in env.items() if not k.startswith("PANTS_")}
env.update(DISABLED_BACKENDS_CONFIG)
subprocess.run([validated_pex_path, "--version"], env=env, check=True, cwd=dest.parent)
green(f"Validated {dest}")

Expand Down
4 changes: 4 additions & 0 deletions pants-plugins/internal_plugins/test_lockfile_fixtures/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Copyright 2022 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).

python_sources()
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Copyright 2022 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).
from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path

import pytest

from pants.jvm.resolve.common import ArtifactRequirement, ArtifactRequirements, Coordinate
from pants.jvm.resolve.coursier_fetch import CoursierResolvedLockfile
from pants.jvm.resolve.lockfile_metadata import LockfileContext
from pants.util.docutil import bin_name


@dataclass(frozen=True)
class JVMLockfileFixtureDefinition:
lockfile_rel_path: Path
coordinates: tuple[Coordinate, ...]

@classmethod
def from_kwargs(cls, kwargs) -> JVMLockfileFixtureDefinition:
lockfile_rel_path = kwargs["path"]
if not lockfile_rel_path:
raise ValueError("`path` must be specified as a relative path to a lockfile")

requirements = kwargs["requirements"] or []
coordinates: list[Coordinate] = []
for requirement in requirements:
if isinstance(requirement, Coordinate):
coordinates.append(requirement)
elif isinstance(requirement, str):
coordinate = Coordinate.from_coord_str(requirement)
coordinates.append(coordinate)
else:
raise ValueError(
f"Unsupported type `{type(requirement)}` for JVM coordinate. Expected `Coordinate` or `str`."
)

return cls(
lockfile_rel_path=Path(lockfile_rel_path),
coordinates=tuple(coordinates),
)


@dataclass(frozen=True)
class JVMLockfileFixture:
lockfile: CoursierResolvedLockfile
serialized_lockfile: str
requirements: ArtifactRequirements


class JvmLockfilePlugin:
def pytest_configure(self, config):
config.addinivalue_line(
"markers",
"jvm_lockfile(path, requirements): mark test to configure a `jvm_lockfile` fixture",
)

@pytest.fixture
def jvm_lockfile(self, request) -> JVMLockfileFixture:
mark = request.node.get_closest_marker("jvm_lockfile")

definition = JVMLockfileFixtureDefinition.from_kwargs(mark.kwargs)

# Load the lockfile.
lockfile_path = request.node.path.parent / definition.lockfile_rel_path
lockfile_contents = lockfile_path.read_bytes()
lockfile = CoursierResolvedLockfile.from_serialized(lockfile_contents)

# Check the lockfile's requirements against the requirements in the lockfile.
# Fail the test if the lockfile needs to be regenerated.
artifact_reqs = ArtifactRequirements(
[ArtifactRequirement(coordinate) for coordinate in definition.coordinates]
)
if not lockfile.metadata:
raise ValueError(
f"Expected JVM lockfile {definition.lockfile_rel_path} to have metadata."
)
if not lockfile.metadata.is_valid_for(artifact_reqs, LockfileContext.TOOL):
raise ValueError(
f"Lockfile fixture {definition.lockfile_rel_path} is not valid. "
"Please re-generate it using: "
f"{bin_name()} internal-generate-test-lockfile-fixtures ::"
)

return JVMLockfileFixture(lockfile, lockfile_contents.decode(), artifact_reqs)
26 changes: 26 additions & 0 deletions pants-plugins/internal_plugins/test_lockfile_fixtures/register.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Copyright 2022 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).
from __future__ import annotations

from internal_plugins.test_lockfile_fixtures.rules import rules as test_lockfile_fixtures_rules
from pants.backend.python.register import rules as python_rules
from pants.backend.python.register import target_types as python_target_types
from pants.core.goals.test import rules as core_test_rules
from pants.core.util_rules import config_files, source_files
from pants.jvm.resolve import coursier_fetch, coursier_setup


def target_types():
return python_target_types()


def rules():
return (
*test_lockfile_fixtures_rules(),
*python_rules(), # python backend
*core_test_rules(),
*config_files.rules(),
*coursier_fetch.rules(),
*coursier_setup.rules(),
*source_files.rules(),
)
Loading

0 comments on commit a95d9ce

Please sign in to comment.