Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
128 changes: 117 additions & 11 deletions cuprum/unittests/test_maturin_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,16 @@
import shutil
import subprocess # noqa: S404 - tests assert trusted maturin command handling.
import typing as typ
import zipfile

import pytest

from tests.helpers.docs import repo_root
from tests.helpers.maturin import (
_AARCH64_CONTAINER_PIN_RE,
_AARCH64_CONTAINER_USAGE_RE,
MaturinBuildError,
build_native_wheel_artifact,
read_expected_maturin_version,
read_manylinux_aarch64_container_ref,
read_maturin_pins,
toolchain_available,
wheel_build_snapshot,
workflow_uses_manylinux_aarch64_container_ref,
)

if typ.TYPE_CHECKING:
Expand All @@ -31,22 +26,98 @@
from syrupy.assertion import SnapshotAssertion


_MATURIN_PIN_RE = re.compile(r"maturin==(\d+\.\d+\.\d+)")
_WORKFLOW_PIN_RE = re.compile(r'MATURIN_VERSION:\s*"(\d+\.\d+\.\d+)"')
_ACTION_PIN_RE = re.compile(r'default:\s*"(\d+\.\d+\.\d+)"')
_AARCH64_CONTAINER_PIN_RE = re.compile(
r"^\s*MANYLINUX_AARCH64_CONTAINER:\s*([^\s#]+)\s+#\s*\S.*$",
re.MULTILINE,
)
_AARCH64_CONTAINER_USAGE_RE = re.compile(
r"^\s*container:\s*\$\{\{\s*env\.MANYLINUX_AARCH64_CONTAINER\s*\}\}\s*$",
re.MULTILINE,
)
_MANYLINUX_CONTAINER_SHA256_RE = re.compile(
r"^ghcr\.io/rust-cross/manylinux_2_28-cross@sha256:[0-9a-f]{64}$"
)


def _require_pin_match(
match: re.Match[str] | None,
location: str,
*,
subject: str = "maturin version pin",
) -> str:
"""Return the captured version from a pin match or raise with the location."""
if match is None:
msg = f"Could not locate {subject} in {location}"
raise AssertionError(msg)
return match.group(1)


def _read_text(root: pth.Path, relative: str) -> str:
"""Read a repository-relative UTF-8 text file."""
return (root / relative).read_text(encoding="utf-8")


def _read_expected_maturin_version(root: pth.Path) -> str:
"""Read the maturin version pinned as the dev dependency in pyproject.toml."""
return _require_pin_match(
_MATURIN_PIN_RE.search(_read_text(root, "pyproject.toml")),
"pyproject.toml",
)


def _read_maturin_pins(root: pth.Path) -> dict[str, str]:
"""Read the maturin version pins from every synchronized location."""
return {
"pyproject.toml": _require_pin_match(
_MATURIN_PIN_RE.search(_read_text(root, "pyproject.toml")),
"pyproject.toml",
),
"build-wheels.yml": _require_pin_match(
_WORKFLOW_PIN_RE.search(
_read_text(root, ".github/workflows/build-wheels.yml"),
),
".github/workflows/build-wheels.yml",
),
"build-wheels/action.yml": _require_pin_match(
_ACTION_PIN_RE.search(
_read_text(root, ".github/actions/build-wheels/action.yml"),
),
".github/actions/build-wheels/action.yml",
),
}


def _read_manylinux_aarch64_container_ref(root: pth.Path) -> str:
"""Read the pinned manylinux aarch64 container reference from the workflow."""
return _require_pin_match(
_AARCH64_CONTAINER_PIN_RE.search(
_read_text(root, ".github/workflows/build-wheels.yml"),
),
".github/workflows/build-wheels.yml",
subject="MANYLINUX_AARCH64_CONTAINER pin",
)


def _workflow_uses_manylinux_aarch64_container_ref(root: pth.Path) -> bool:
"""Report whether the workflow references the pinned manylinux container."""
workflow = _read_text(root, ".github/workflows/build-wheels.yml")
return _AARCH64_CONTAINER_USAGE_RE.search(workflow) is not None


def test_maturin_pins_are_synchronized() -> None:
"""Maturin version pins stay aligned across CI and dev dependencies."""
pins = read_maturin_pins(repo_root())
pins = _read_maturin_pins(repo_root())
assert len(set(pins.values())) == 1, f"Expected one maturin pin, found {pins!r}"


def test_installed_maturin_matches_expected_pin() -> None:
"""The active maturin CLI matches the pinned development dependency."""
if shutil.which("maturin") is None:
pytest.skip("maturin is not installed.")
expected = read_expected_maturin_version(repo_root())
expected = _read_expected_maturin_version(repo_root())
installed = im.version("maturin")
assert installed == expected, (
f"Expected maturin {expected}, but {installed} is installed"
Expand All @@ -55,15 +126,15 @@ def test_installed_maturin_matches_expected_pin() -> None:

def test_manylinux_aarch64_container_is_pinned_to_sha256() -> None:
"""Aarch64 manylinux container pin must be immutable."""
container_ref = read_manylinux_aarch64_container_ref(repo_root())
container_ref = _read_manylinux_aarch64_container_ref(repo_root())
assert _MANYLINUX_CONTAINER_SHA256_RE.fullmatch(container_ref), (
f"Expected SHA-256 pinned container ref, found {container_ref!r}"
)


def test_manylinux_aarch64_container_is_referenced_by_build_step() -> None:
"""The build job should consume the pinned aarch64 container variable."""
assert workflow_uses_manylinux_aarch64_container_ref(repo_root()), (
assert _workflow_uses_manylinux_aarch64_container_ref(repo_root()), (
"Expected build-wheels.yml to reference env.MANYLINUX_AARCH64_CONTAINER"
)

Expand Down Expand Up @@ -195,7 +266,7 @@ def test_maturin_wheel_build_snapshot(
) -> None:
"""Native wheel metadata and layout match the expected maturin output."""
root = repo_root()
expected = read_expected_maturin_version(root)
expected = _read_expected_maturin_version(root)
if not toolchain_available():
pytest.skip("Rust toolchain unavailable.")

Expand All @@ -207,3 +278,38 @@ def test_maturin_wheel_build_snapshot(
assert snapshot_payload == snapshot, (
"Built wheel metadata, file list, and build settings changed."
)


@pytest.mark.parametrize(
("members", "expected_message"),
[
pytest.param(
{"cuprum-0.1.0.dist-info/METADATA": "Name: cuprum\n"},
"wheel is missing .dist-info/WHEEL metadata",
id="missing_wheel",
),
pytest.param(
{"cuprum-0.1.0.dist-info/WHEEL": "Root-Is-Purelib: false\n"},
"wheel is missing .dist-info/METADATA metadata",
id="missing_metadata",
),
],
)
def test_wheel_build_snapshot_reports_missing_dist_info(
tmp_path: pth.Path,
members: dict[str, str],
expected_message: str,
) -> None:
"""A wheel missing either dist-info member fails with AssertionError.

``wheel_build_snapshot`` documents ``AssertionError`` for absent metadata,
so a missing ``METADATA`` must not surface as the ``KeyError`` that
``ZipFile.read`` would otherwise raise.
"""
whl_path = tmp_path / "cuprum-0.1.0-py3-none-any.whl"
with zipfile.ZipFile(whl_path, "w") as archive:
for name, payload in members.items():
archive.writestr(name, payload)

with pytest.raises(AssertionError, match=re.escape(expected_message)):
wheel_build_snapshot(whl_path)
24 changes: 22 additions & 2 deletions docs/developers-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -1400,8 +1400,28 @@ change alters the architecture of the lint gate, update

## Maturin pin synchronization and native wheel tests

The `tests/helpers/maturin.py` module provides shared helpers for tests that
validate the maturin version pin contract and native wheel build output.
These checks live in `cuprum/unittests/test_maturin_build.py`, split across two
boundaries. The **pin-synchronization** checks are inlined there as private
helpers (`_read_maturin_pins`, `_read_expected_maturin_version`,
`_read_manylinux_aarch64_container_ref`, and their regexes): they read
repository files, have a single consumer, and gain nothing from indirection.
The **native wheel build and snapshot** machinery stays in
`tests/helpers/maturin.py`, because it wraps `subprocess` and `zipfile`
introspection that does not inline cleanly.
Comment on lines +1403 to +1410

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the regex ownership description below.

Replace “shared regex” in Lines 1446-1450 with wording that identifies the
private test-local regexes; Lines 1403-1406 now explicitly document that the
helpers and regexes are inlined in test_maturin_build.py.

Triage: [type:docstyle]

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/developers-guide.md` around lines 1403 - 1410, Update the regex
ownership wording in the documentation around the “shared regex” reference so it
identifies the private test-local regexes in test_maturin_build.py, consistent
with the documented helpers and regexes being inlined there. Do not describe
them as shared.

Source: Coding guidelines


That module's scope is deliberately narrow: build a wheel
(`build_native_wheel_artifact`), report toolchain availability
(`toolchain_available`), and normalize a built wheel into a stable snapshot
(`wheel_build_snapshot`). The build runs `python -m maturin` under the current
interpreter, so it uses whichever maturin that environment provides rather than
selecting a version itself. Two separate mechanisms enforce alignment with the
declared pin: `test_installed_maturin_matches_expected_pin` compares the
installed CLI against the `pyproject.toml` pin, and the snapshot test asserts
the built wheel's `Generator` matches that same pin, so a wheel built by an
Comment on lines +1417 to +1420

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Describe the installed-version check accurately.

Replace “compares the installed CLI” with the current-interpreter package
metadata check, or invoke maturin --version. The test gates on a CLI being on
PATH but obtains the version through importlib.metadata.version("maturin").

Triage: [type:docstyle]

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/developers-guide.md` around lines 1417 - 1420, Update the documentation
around test_installed_maturin_matches_expected_pin to describe its actual check:
it verifies the installed maturin package version from current-interpreter
metadata via importlib.metadata.version("maturin"), while only gating on the CLI
being available on PATH; alternatively, change the implementation to obtain the
version with maturin --version.

unexpected maturin fails the suite. Its re-use policy is to stay that way — do not
re-externalize further helpers into it until a second concrete consumer exists
and the shared interface can be designed against real requirements rather than
anticipated ones.

**Pin synchronization** (`test_maturin_pins_are_synchronized`) Asserts that the
maturin version declared in `pyproject.toml`,
Expand Down
Loading
Loading