Files
cibuildwheel/test/test_abi3audit.py
T

221 lines
6.9 KiB
Python

from __future__ import annotations
import subprocess
import textwrap
import pytest
from . import test_projects, utils
TYPE_CHECKING = False
if TYPE_CHECKING:
from pathlib import Path
pyproject_toml = r"""
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"
"""
limited_api_project = test_projects.new_c_project(
setup_py_add=textwrap.dedent(
r"""
import sys
import sysconfig
IS_CPYTHON = sys.implementation.name == "cpython"
Py_GIL_DISABLED = sysconfig.get_config_var("Py_GIL_DISABLED")
CAN_USE_ABI3 = IS_CPYTHON and not Py_GIL_DISABLED
setup_options = {}
extension_kwargs = {}
if CAN_USE_ABI3 and sys.version_info[:2] >= (3, 10):
extension_kwargs["define_macros"] = [("Py_LIMITED_API", "0x030A0000")]
extension_kwargs["py_limited_api"] = True
setup_options = {"bdist_wheel": {"py_limited_api": "cp310"}}
"""
),
setup_py_extension_args_add="**extension_kwargs",
setup_py_setup_args_add="options=setup_options",
)
limited_api_project.files["pyproject.toml"] = pyproject_toml
# Project that claims abi3 but violates the stable ABI by calling
# PyUnicode_AsUTF8 (not in stable ABI until 3.13) without defining
# Py_LIMITED_API in the C code.
violating_abi3_project = test_projects.new_c_project(
setup_py_add=textwrap.dedent(
r"""
import sys
import sysconfig
IS_CPYTHON = sys.implementation.name == "cpython"
Py_GIL_DISABLED = sysconfig.get_config_var("Py_GIL_DISABLED")
CAN_USE_ABI3 = IS_CPYTHON and not Py_GIL_DISABLED
setup_options = {}
extension_kwargs = {}
if CAN_USE_ABI3 and sys.version_info[:2] >= (3, 10):
# Intentionally NOT defining Py_LIMITED_API as a C macro,
# but still tagging the wheel as abi3.
extension_kwargs["py_limited_api"] = True
setup_options = {"bdist_wheel": {"py_limited_api": "cp310"}}
"""
),
spam_c_function_add=textwrap.dedent(
r"""
// Call a function not in the stable ABI until Python 3.13.
// Without Py_LIMITED_API defined, the compiler allows it.
PyObject *str_obj = PyUnicode_FromString(content);
const char *utf8 = PyUnicode_AsUTF8(str_obj);
(void)utf8;
Py_DECREF(str_obj);
"""
),
setup_py_extension_args_add="**extension_kwargs",
setup_py_setup_args_add="options=setup_options",
)
violating_abi3_project.files["pyproject.toml"] = pyproject_toml
@utils.skip_if_pyodide("abi3audit is disabled on Pyodide (wasm shared objects are not supported)")
def test_abi3audit_runs_on_abi3_wheel(tmp_path: Path, capfd: pytest.CaptureFixture[str]) -> None:
"""Test that abi3audit runs automatically on abi3 wheels."""
project_dir = tmp_path / "project"
limited_api_project.generate(project_dir)
actual_wheels = utils.cibuildwheel_run(
project_dir,
add_env={
# Let's only build one cpython version to keep the test fast.
"CIBW_BUILD": "cp310-*",
"CIBW_ARCHS": "native",
},
)
assert len(actual_wheels) >= 1
captured = capfd.readouterr()
assert "Running audit command: abi3audit" in captured.out
def test_abi3audit_skipped_for_non_abi3_wheel(
tmp_path: Path, capfd: pytest.CaptureFixture[str]
) -> None:
"""Test that abi3audit does not run for non-abi3 wheels."""
project_dir = tmp_path / "project"
basic_project = test_projects.new_c_project()
basic_project.generate(project_dir)
actual_wheels = utils.cibuildwheel_run(
project_dir,
add_env={
"CIBW_ARCHS": "native",
},
single_python=True,
)
assert len(actual_wheels) >= 1
captured = capfd.readouterr()
assert "Running audit command: abi3audit" not in captured.out
@utils.skip_if_pyodide("abi3audit is disabled on Pyodide (wasm shared objects are not supported)")
def test_abi3audit_detects_violation(tmp_path: Path, capfd: pytest.CaptureFixture[str]) -> None:
"""Test that abi3audit catches stable ABI violations and fails the build.
This project tags the wheel as cp310-abi3 but uses PyUnicode_AsUTF8,
which was not part of the stable ABI until Python 3.13.
"""
project_dir = tmp_path / "project"
violating_abi3_project.generate(project_dir)
with pytest.raises(subprocess.CalledProcessError):
utils.cibuildwheel_run(
project_dir,
add_env={
"CIBW_BUILD": "cp310-*",
"CIBW_ARCHS": "native",
},
)
captured = capfd.readouterr()
assert "Running audit command: abi3audit" in captured.out
def test_custom_audit_command(tmp_path: Path, capfd: pytest.CaptureFixture[str]) -> None:
project_dir = tmp_path / "project"
test_projects.new_c_project().generate(project_dir)
actual_wheels = utils.cibuildwheel_run(
project_dir,
add_env={
"CIBW_AUDIT_COMMAND": "echo custom-audit-ok {wheel}",
"CIBW_AUDIT_REQUIRES": "",
"CIBW_ARCHS": "native",
},
single_python=True,
)
assert len(actual_wheels) >= 1
captured = capfd.readouterr()
assert "Auditing wheel" in captured.out
assert "custom-audit-ok" in captured.out
def test_custom_audit_requires(tmp_path: Path, capfd: pytest.CaptureFixture[str]) -> None:
project_dir = tmp_path / "project"
test_projects.new_c_project().generate(project_dir)
actual_wheels = utils.cibuildwheel_run(
project_dir,
add_env={
"CIBW_AUDIT_REQUIRES": "pycowsay",
"CIBW_AUDIT_COMMAND": "pycowsay moo {wheel}",
"CIBW_ARCHS": "native",
},
single_python=True,
)
assert len(actual_wheels) >= 1
captured = capfd.readouterr()
assert "Installing audit dependencies: pycowsay" in captured.out
assert "moo" in captured.out
def test_empty_audit_command_disables_audit(
tmp_path: Path, capfd: pytest.CaptureFixture[str]
) -> None:
project_dir = tmp_path / "project"
test_projects.new_c_project().generate(project_dir)
actual_wheels = utils.cibuildwheel_run(
project_dir,
add_env={
"CIBW_AUDIT_COMMAND": "",
"CIBW_ARCHS": "native",
},
single_python=True,
)
assert len(actual_wheels) >= 1
captured = capfd.readouterr()
assert "Auditing wheel" not in captured.out
def test_custom_audit_command_failure(tmp_path: Path) -> None:
project_dir = tmp_path / "project"
test_projects.new_c_project().generate(project_dir)
with pytest.raises(subprocess.CalledProcessError):
utils.cibuildwheel_run(
project_dir,
add_env={
"CIBW_AUDIT_COMMAND": 'python -c "import sys; sys.exit(1)" {wheel}',
"CIBW_AUDIT_REQUIRES": "",
"CIBW_ARCHS": "native",
},
single_python=True,
)