Skip to content

Commit

Permalink
align test JVM lockfiles better with pytest
Browse files Browse the repository at this point in the history
[ci skip-rust]

[ci skip-build-wheels]
  • Loading branch information
Tom Dyas committed Jun 11, 2022
1 parent 960f12f commit 4cba77f
Show file tree
Hide file tree
Showing 13 changed files with 362 additions and 273 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,30 @@
import textwrap
from dataclasses import dataclass
from pathlib import Path
from typing import Iterable

import pytest
from _pytest.fixtures import FixtureRequest

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
from pants.util.meta import frozen_after_init


@dataclass(frozen=True)
@frozen_after_init
@dataclass(unsafe_hash=True)
class JVMLockfileFixtureDefinition:
lockfile_rel_path: Path
coordinates: tuple[Coordinate, ...]
requirements: 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")
def __init__(
self, lockfile_rel_path: Path | str, requirements: Iterable[Coordinate | str]
) -> None:
self.lockfile_rel_path = (
lockfile_rel_path if isinstance(lockfile_rel_path, Path) else Path(lockfile_rel_path)
)

requirements = kwargs["requirements"] or []
coordinates: list[Coordinate] = []
for requirement in requirements:
if isinstance(requirement, Coordinate):
Expand All @@ -34,15 +37,44 @@ def from_kwargs(cls, kwargs) -> JVMLockfileFixtureDefinition:
coordinate = Coordinate.from_coord_str(requirement)
coordinates.append(coordinate)
else:
raise ValueError(
raise TypeError(
f"Unsupported type `{type(requirement)}` for JVM coordinate. Expected `Coordinate` or `str`."
)
self.requirements = tuple(coordinates)

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

requirements = kwargs["requirements"] or []
return cls(
lockfile_rel_path=Path(lockfile_rel_path),
coordinates=tuple(coordinates),
requirements=requirements,
)

def load(self, request: FixtureRequest) -> JVMLockfileFixture:
lockfile_path = request.node.path.parent / self.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 self.requirements]
)
if not lockfile.metadata:
raise ValueError(f"Expected JVM lockfile {self.lockfile_rel_path} to have metadata.")
if not lockfile.metadata.is_valid_for(artifact_reqs, LockfileContext.TOOL):
raise ValueError(
f"Lockfile fixture {self.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)


@dataclass(frozen=True)
class JVMLockfileFixture:
Expand All @@ -69,40 +101,3 @@ def requirements_as_jvm_artifact_targets(
"""
)
return targets


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)
66 changes: 57 additions & 9 deletions pants-plugins/internal_plugins/test_lockfile_fixtures/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,62 @@
from pants.util.logging import LogLevel

COLLECTION_SCRIPT = r"""\
import inspect
from pathlib import Path
import json
import sys
import pytest
from _pytest.compat import get_real_func
from _pytest import fixtures
def get_func_path(func):
real_func = get_real_func(func)
return inspect.getfile(real_func)
def get_fixturedef(fixture_request, name):
fixturedef = fixture_request._fixture_defs.get(name)
if fixturedef:
return fixturedef
try:
return fixture_request._getnextfixturedef(name)
except fixtures.FixtureLookupError:
return None
def process_fixtures(item):
lockfile_definitions = []
fixture_request = fixtures.FixtureRequest(item, _ispytest=True)
for fixture_name in fixture_request.fixturenames:
fixture_def = get_fixturedef(fixture_request, fixture_name)
if not fixture_def:
continue
func = fixture_def.func
annotations = getattr(func, "__annotations__")
if not annotations or annotations.get("return") != "JVMLockfileFixtureDefinition":
continue
# Note: We just invoke the fixture_def function assuming it takes no arguments. The other two
# ways of invoking for the fixture value cause errors. I have left them here commented-out as an example
# of what failed:
# lockfile_definition = fixture_request.getfixturevalue(fixture_name)
# lockfile_definition = fixture_def.execute(request=request)
lockfile_definition = func()
if lockfile_definition.__class__.__name__ != "JVMLockfileFixtureDefinition":
continue
cwd = Path.cwd()
func_path = Path(get_func_path(func)).relative_to(cwd)
lockfile_definitions.append({
"lockfile_rel_path": str(lockfile_definition.lockfile_rel_path),
"requirements": [c.to_coord_str() for c in lockfile_definition.requirements],
"test_file_path": str(func_path),
})
return lockfile_definitions
class CollectionPlugin:
def __init__(self):
Expand All @@ -53,16 +104,13 @@ def pytest_collection_modifyitems(self, items):
collection_plugin = CollectionPlugin()
pytest.main(["--collect-only", *sys.argv[1:]], plugins=[collection_plugin])
pytest.main(["--setup-only", *sys.argv[1:]], plugins=[collection_plugin])
output = []
cwd = Path.cwd()
for item in collection_plugin.collected:
for mark in item.iter_markers("jvm_lockfile"):
path = Path(item.path).relative_to(cwd)
output.append({
"kwargs": mark.kwargs,
"test_file_path": str(path),
})
output.extend(process_fixtures(item))
with open("tests.json", "w") as f:
f.write(json.dumps(output))
Expand Down Expand Up @@ -198,7 +246,7 @@ async def collect_fixture_configs(
configs = []
for item in raw_config_data:
config = JVMLockfileFixtureConfig(
definition=JVMLockfileFixtureDefinition.from_kwargs(item["kwargs"]),
definition=JVMLockfileFixtureDefinition.from_json_dict(item),
test_file_path=item["test_file_path"],
)
configs.append(config)
Expand All @@ -212,7 +260,7 @@ async def gather_lockfile_fixtures() -> RenderedJVMLockfileFixtures:
rendered_fixtures = []
for config in configs:
artifact_reqs = ArtifactRequirements(
[ArtifactRequirement(coordinate) for coordinate in config.definition.coordinates]
[ArtifactRequirement(coordinate) for coordinate in config.definition.requirements]
)
lockfile = await Get(CoursierResolvedLockfile, ArtifactRequirements, artifact_reqs)
serialized_lockfile = JVMLockfileMetadata.new(artifact_reqs).add_header_to_lockfile(
Expand Down
2 changes: 0 additions & 2 deletions src/python/pants/backend/kotlin/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,3 @@
# Licensed under the Apache License, Version 2.0 (see LICENSE).

python_sources()

python_test_utils(name="test_utils")
Loading

0 comments on commit 4cba77f

Please sign in to comment.