Files
cibuildwheel/unit_test/audit_test.py
T
79244d366c feet: general approach to auditing wheels with abi3audit default (#2805)
* WIP - initial punt at audit command

* Add `abi3audit` as a dependency

* Add helper functions to check stable ABI wheels

* Run `abi3audit` for macOS and Windows wheels

* Copy out of container for repairing?

* Add some notes that `cibuildwheel` runs `abi3audit`

* Add basic unit tests

* Add a basic C extension with `Py_LIMITED_API`

* Add a test project that violates Stable ABI

* Fix linux test

* Skip abi3 wheel tests for Pyodide

* Patch the correct subprocess module

* wrap cleanup of abi3audit dir

* Write the docs for the new options

* Move to above testing in docs

* Implement audit-requires and audit-command

* Some cleanups after self-review

* Add default value

* fix type errors

* the key is `audit-command`, not `audit`

* Add a variety of tests for audit requires options

* Add `test_audit_requires` similar to `test_test_requires`

* Add some configurability-related audit tests

* Fix parsing error with options docs leaving out commands

* Better way to extract version (maybe helps Pyodide?)

* Fix a case of unbound `use_uv`

* Standardise: rename to `abi3_wheel`

* Fix audit command run message

* Simplify custom audit command a bit

* Remove unnecessary skip for Pyodide

* Pyodide should have no default audit command

* More accurate skip messages for Pyodide skips

* Wheels are audited after they are repaired

* Regenerate constraints to include `abi3audit`

* Fix typos

* Some attempts for Windows fixes

* Check `pyvenv.cfg` instead of directory existence

* Add validation for lack of wheel placeholders

* Try yet another Windows `uv` fix

* Regenerate diagram and re-trigger Azure CI

* Add missing `import sys` for abi3 C extension tests

* Remove audit-command at the global level

* Clarify `abi3audit` pinning a little bit

* Regen constraints

* Discard changes to cibuildwheel/resources/constraints-pyodide312.txt

* Discard changes to cibuildwheel/resources/constraints-pyodide313.txt

* try opt-in uv again

* fix issue on windows on Python 3.13 related to nested venvs

On win / python 3.13, virtualenv creates a venv where the 'home'
points back to the venv that sys.executable was running in, rather
than the root install. that seemingly leads to problems with package
resolution, where pip.exe couldn't find the pip python package.
this appears to fix it!

* Update constraints

* chore: revert python-discovery bump

Assisted-by: OpenCode:glm-5.1
Signed-off-by: Henry Schreiner <henryfs@princeton.edu>

* fix: restore workaround for graalpy

Assisted-by: OpenCode:glm-5.1
Signed-off-by: Henry Schreiner <henryfs@princeton.edu>

---------

Signed-off-by: Henry Schreiner <henryfs@princeton.edu>
Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com>
Co-authored-by: Henry Schreiner <henryfs@princeton.edu>
2026-05-14 07:41:14 -07:00

140 lines
5.6 KiB
Python

import contextlib
import subprocess
from pathlib import Path
from unittest.mock import Mock, patch
import pytest
from cibuildwheel import errors
from cibuildwheel.audit import needs_audit, run_audit
def mock_virtualenv() -> contextlib.AbstractContextManager[Mock]:
return patch(
"cibuildwheel.audit.virtualenv",
return_value={
"PATH": "/bin",
"VIRTUAL_ENV": "/tmp/v",
},
)
class TestNeedsAudit:
def test_empty_commands(self) -> None:
assert needs_audit([], "example-1.0.0-cp310-cp310-manylinux_2_17_x86_64.whl") is False
def test_wheel_placeholder_matches_any_wheel(self) -> None:
assert needs_audit(
["my-tool {wheel}"], "example-1.0.0-cp310-cp310-manylinux_2_17_x86_64.whl"
)
def test_abi3_placeholder_skips_non_abi3(self) -> None:
assert (
needs_audit(
["abi3audit {abi3_wheel}"], "example-1.0.0-cp310-cp310-manylinux_2_17_x86_64.whl"
)
is False
)
def test_abi3_placeholder_matches_abi3(self) -> None:
assert needs_audit(
["abi3audit {abi3_wheel}"], "example-1.0.0-cp38-abi3-manylinux_2_17_x86_64.whl"
)
def test_mixed_commands_matches_if_any_applies(self) -> None:
commands = ["abi3audit {abi3_wheel}", "twine check {wheel}"]
# non-abi3 wheel still needs audit because of the {wheel} command
assert needs_audit(commands, "example-1.0.0-cp310-cp310-manylinux_2_17_x86_64.whl")
def test_command_without_placeholder_raises(self) -> None:
with pytest.raises(errors.ConfigurationError, match="must contain either"):
needs_audit(["my-tool"], "example-1.0.0-cp310-cp310-manylinux_2_17_x86_64.whl")
class TestRunAudit:
@pytest.fixture
def mock_build_options(self) -> Mock:
opts = Mock()
opts.audit_command = []
opts.audit_requires = []
opts.package_dir = Path("/fake/package")
opts.build_frontend.name = "build"
opts.dependency_constraints.get_for_python_version.return_value = None
return opts
def test_no_commands_does_nothing(self, tmp_path: Path, mock_build_options: Mock) -> None:
wheel = tmp_path / "example-1.0.0-cp310-cp310-manylinux_2_17_x86_64.whl"
mock_build_options.audit_command = []
with patch("cibuildwheel.audit.shell") as mock_shell:
run_audit(tmp_dir=tmp_path, build_options=mock_build_options, wheel=wheel)
mock_shell.assert_not_called()
def test_runs_wheel_command(self, tmp_path: Path, mock_build_options: Mock) -> None:
wheel = tmp_path / "example-1.0.0-cp310-cp310-manylinux_2_17_x86_64.whl"
mock_build_options.audit_command = ["my-tool {wheel}"]
with mock_virtualenv(), patch("cibuildwheel.audit.shell") as mock_shell:
run_audit(tmp_dir=tmp_path, build_options=mock_build_options, wheel=wheel)
mock_shell.assert_called_once()
cmd = mock_shell.call_args[0][0]
assert str(wheel) in cmd
def test_abi3_command_skipped_for_non_abi3(
self, tmp_path: Path, mock_build_options: Mock
) -> None:
wheel = tmp_path / "example-1.0.0-cp310-cp310-manylinux_2_17_x86_64.whl"
mock_build_options.audit_command = ["abi3audit {abi3_wheel}"]
with patch("cibuildwheel.audit.shell") as mock_shell:
run_audit(tmp_dir=tmp_path, build_options=mock_build_options, wheel=wheel)
mock_shell.assert_not_called()
def test_abi3_command_runs_for_abi3(self, tmp_path: Path, mock_build_options: Mock) -> None:
wheel = tmp_path / "example-1.0.0-cp38-abi3-manylinux_2_17_x86_64.whl"
mock_build_options.audit_command = ["abi3audit {abi3_wheel}"]
with (
mock_virtualenv(),
patch("cibuildwheel.audit.shell") as mock_shell,
):
run_audit(tmp_dir=tmp_path, build_options=mock_build_options, wheel=wheel)
mock_shell.assert_called_once()
cmd = mock_shell.call_args[0][0]
assert str(wheel) in cmd
def test_raises_on_command_failure(self, tmp_path: Path, mock_build_options: Mock) -> None:
wheel = tmp_path / "example-1.0.0-cp310-cp310-manylinux_2_17_x86_64.whl"
mock_build_options.audit_command = ["failing-tool {wheel}"]
with (
mock_virtualenv(),
patch(
"cibuildwheel.audit.shell",
side_effect=subprocess.CalledProcessError(1, "failing-tool"),
),
pytest.raises(errors.AuditCommandFailedError),
):
run_audit(tmp_dir=tmp_path, build_options=mock_build_options, wheel=wheel)
def test_multiple_commands_all_run(self, tmp_path: Path, mock_build_options: Mock) -> None:
wheel = tmp_path / "example-1.0.0-cp310-cp310-manylinux_2_17_x86_64.whl"
mock_build_options.audit_command = ["tool-a {wheel}", "tool-b {wheel}"]
with (
mock_virtualenv(),
patch("cibuildwheel.audit.shell") as mock_shell,
):
run_audit(tmp_dir=tmp_path, build_options=mock_build_options, wheel=wheel)
assert mock_shell.call_count == 2
def test_both_placeholders_raises(self, tmp_path: Path, mock_build_options: Mock) -> None:
wheel = tmp_path / "example-1.0.0-cp38-abi3-manylinux_2_17_x86_64.whl"
mock_build_options.audit_command = ["my-tool {wheel} {abi3_wheel}"]
with (
mock_virtualenv(),
pytest.raises(errors.ConfigurationError, match="cannot contain both"),
):
run_audit(tmp_dir=tmp_path, build_options=mock_build_options, wheel=wheel)