Files
orisys-sdk/tests/test_wheel_contents.py
T
ykcheungab bb15b750fb feat: 添加 ARM64 manylinux wheel 构建选项
复用现有 Docker 和 Nuitka 流程,隔离架构镜像并支持 aarch64 标签。已通过 47 项测试、代码及文档门禁,并完成 Python 3.10 ARM64 wheel 构建、隔离导入与内容审计。
2026-10-14 16:08:54 +08:00

380 lines
14 KiB
Python

"""Tests for :mod:`common.verify.wheel_contents`.
Every check is exercised in both directions: a well-formed synthetic wheel must
pass, and each specific defect must fail with the check named in the report.
Synthetic wheels keep the tests fast and keep them independent of whatever
happens to sit in ``SDK/``.
"""
from __future__ import annotations
import base64
import hashlib
import sys
import zipfile
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).resolve().parents[1]
BUILD_ROOT = REPO_ROOT / "build"
if str(BUILD_ROOT) not in sys.path:
sys.path.insert(0, str(BUILD_ROOT))
from common.verify import wheel_contents # noqa: E402
from common.verify.wheel_contents import inspect_wheel # noqa: E402
VERSION = "1.2.3"
DIST_INFO = f"orisys-{VERSION}.dist-info"
CORE_DEPS = ("numpy>=1.24.4", "opencv-python>=4.10.0.84")
EXTRAS = {"cuda12": ("cupy-cuda12x>=12.3.0",), "gui": ("open3d>=0.17.0",)}
PYPROJECT = f"""
[project]
name = "orisys"
version = "{VERSION}"
requires-python = ">=3.10"
dependencies = [{", ".join(repr(dep) for dep in CORE_DEPS)}]
[project.optional-dependencies]
cuda12 = ["cupy-cuda12x>=12.3.0"]
gui = ["open3d>=0.17.0"]
[[tool.orisys.bundle.items]]
name = "configs"
source = "config"
target = "configs"
patterns = ["*.json"]
[[tool.orisys.bundle.items]]
name = "sensors"
source = "sensors"
target = "sensors"
patterns = ["**/*.json"]
preserve_tree = true
[[tool.orisys.bundle.items]]
name = "docs"
source = "docs"
target = "docs"
patterns = ["*.md"]
[[tool.orisys.bundle.items]]
name = "examples"
source = "examples"
target = "examples"
patterns = ["*.py", "README.md"]
preserve_tree = true
"""
# Source tree files, keyed by path relative to the synthetic repo root.
SOURCE_FILES = {
"config/box.json": '{"name": "box"}',
"sensors/box/sensor.json": '{"name": "box"}',
"sensors/finger/manifest.json": '{"name": "finger"}',
"docs/api.md": "# api\n",
"examples/basic_pipeline.py": "print('hi')\n",
"examples/README.md": "# examples\n",
}
# Where each source file must land inside the wheel.
BUNDLED_ENTRIES = {
"orisys/configs/box.json": SOURCE_FILES["config/box.json"],
"orisys/sensors/box/sensor.json": SOURCE_FILES["sensors/box/sensor.json"],
"orisys/sensors/finger/manifest.json": SOURCE_FILES["sensors/finger/manifest.json"],
"orisys/docs/api.md": SOURCE_FILES["docs/api.md"],
"orisys/examples/basic_pipeline.py": SOURCE_FILES["examples/basic_pipeline.py"],
"orisys/examples/README.md": SOURCE_FILES["examples/README.md"],
}
def _metadata_text(
*,
version: str = VERSION,
requires_python: str | None = ">=3.10",
classifiers: tuple[str, ...] = ("Programming Language :: Python :: 3.10",),
project_urls: tuple[str, ...] = ("Homepage, https://example.invalid",),
license_line: str | None = "License: Proprietary",
core_deps: tuple[str, ...] = CORE_DEPS,
extras: dict[str, tuple[str, ...]] | None = None,
dynamic: tuple[str, ...] = (),
name: str = "orisys",
) -> str:
if extras is None:
extras = EXTRAS
lines = ["Metadata-Version: 2.4", f"Name: {name}", f"Version: {version}", "Summary: synthetic"]
if requires_python:
lines.append(f"Requires-Python: {requires_python}")
if license_line:
lines.append(license_line)
lines.extend(f"Classifier: {value}" for value in classifiers)
lines.extend(f"Project-URL: {value}" for value in project_urls)
lines.extend(f"Dynamic: {value}" for value in dynamic)
lines.extend(f"Requires-Dist: {dep}" for dep in core_deps)
for extra, requirements in extras.items():
lines.append(f"Provides-Extra: {extra}")
lines.extend(f'Requires-Dist: {req}; extra == "{extra}"' for req in requirements)
return "\n".join(lines) + "\n"
def _write_source_tree(root: Path) -> None:
root.mkdir(parents=True, exist_ok=True)
(root / "pyproject.toml").write_text(PYPROJECT, encoding="utf-8")
for relative, text in SOURCE_FILES.items():
path = root / relative
path.parent.mkdir(parents=True, exist_ok=True)
# write_bytes, not write_text: text mode would translate "\n" to "\r\n"
# on Windows and the byte-for-byte bundle comparison would fail.
path.write_bytes(text.encode("utf-8"))
def _record(entries: dict[str, bytes]) -> str:
lines = []
for name, data in sorted(entries.items()):
digest = base64.urlsafe_b64encode(hashlib.sha256(data).digest()).rstrip(b"=").decode()
lines.append(f"{name},sha256={digest},{len(data)}")
lines.append(f"{DIST_INFO}/RECORD,,")
return "\n".join(lines) + "\n"
def _make_wheel(
tmp_path: Path,
*,
filename: str | None = None,
metadata: str | None = None,
drop: tuple[str, ...] = (),
extra_entries: dict[str, bytes] | None = None,
override_bytes: dict[str, bytes] | None = None,
drop_record: bool = False,
corrupt_record_hash: bool = False,
) -> tuple[Path, Path]:
"""Build a synthetic wheel plus its matching source tree."""
repo = tmp_path / "repo"
_write_source_tree(repo)
if metadata is None:
metadata = _metadata_text()
entries: dict[str, bytes] = {name: data.encode("utf-8") for name, data in BUNDLED_ENTRIES.items()}
entries["orisys/__init__.cp312-win_amd64.pyd"] = b"compiled"
entries["orisys_blend/__init__.py"] = b"# blend\n"
entries["orisys_blend/kernel.py"] = b"# kernel\n"
entries[f"{DIST_INFO}/METADATA"] = metadata.encode("utf-8")
entries[f"{DIST_INFO}/WHEEL"] = b"Wheel-Version: 1.0\n"
entries.update(extra_entries or {})
entries.update(override_bytes or {})
for name in drop:
entries.pop(name, None)
if not drop_record:
record = _record(entries)
if corrupt_record_hash:
record = record.replace("sha256=", "sha256=AAAA", 1)
entries[f"{DIST_INFO}/RECORD"] = record.encode("utf-8")
wheel = tmp_path / (filename or f"orisys-{VERSION}-cp312-cp312-win_amd64.whl")
with zipfile.ZipFile(wheel, "w", zipfile.ZIP_DEFLATED) as zf:
for name, data in entries.items():
zf.writestr(name, data)
return wheel, repo
def _failed_names(result) -> set[str]:
return {check.name for check in result.failures}
@pytest.fixture(autouse=True)
def _isolated_repo_root(monkeypatch, tmp_path):
"""Keep inspect_wheel's default repo root from leaking the real repo in."""
monkeypatch.setattr(wheel_contents, "REPO_ROOT", tmp_path / "unused")
def test_good_wheel_passes(tmp_path):
wheel, repo = _make_wheel(tmp_path)
result = inspect_wheel(wheel, expected_version=VERSION, repo_root=repo)
assert result.ok, result.render()
assert result.version == VERSION
assert result.platform_tag == "win_amd64"
assert not result.warnings, result.warnings
def test_missing_wheel_file_fails(tmp_path):
result = inspect_wheel(tmp_path / "nope-1.2.3-cp312-cp312-win_amd64.whl")
assert not result.ok
assert "wheel-exists" in _failed_names(result)
def test_unparsable_filename_fails(tmp_path):
wheel, repo = _make_wheel(tmp_path, filename="orisys-1.2.3-cp312-cp312.whl")
result = inspect_wheel(wheel, repo_root=repo)
assert "filename" in _failed_names(result)
def test_version_mismatch_fails(tmp_path):
wheel, repo = _make_wheel(tmp_path)
result = inspect_wheel(wheel, expected_version="9.9.9", repo_root=repo)
failed = _failed_names(result)
assert {"filename", "metadata-version"} <= failed
def test_missing_requires_python_fails(tmp_path):
wheel, repo = _make_wheel(tmp_path, metadata=_metadata_text(requires_python=None))
assert "metadata-requires-python" in _failed_names(inspect_wheel(wheel, repo_root=repo))
def test_missing_classifier_fails(tmp_path):
wheel, repo = _make_wheel(tmp_path, metadata=_metadata_text(classifiers=()))
assert "metadata-classifiers" in _failed_names(inspect_wheel(wheel, repo_root=repo))
def test_missing_project_url_fails(tmp_path):
wheel, repo = _make_wheel(tmp_path, metadata=_metadata_text(project_urls=()))
assert "metadata-project-urls" in _failed_names(inspect_wheel(wheel, repo_root=repo))
def test_missing_license_fails(tmp_path):
wheel, repo = _make_wheel(
tmp_path,
metadata=_metadata_text(license_line=None, classifiers=("Intended Audience :: Developers",)),
)
assert "metadata-license" in _failed_names(inspect_wheel(wheel, repo_root=repo))
def test_dynamic_version_is_rejected(tmp_path):
wheel, repo = _make_wheel(tmp_path, metadata=_metadata_text(dynamic=("version", "summary")))
result = inspect_wheel(wheel, repo_root=repo)
assert "metadata-no-dynamic-version" in _failed_names(result)
assert any("Dynamic" in warning for warning in result.warnings)
def test_missing_core_requirement_fails(tmp_path):
wheel, repo = _make_wheel(tmp_path, metadata=_metadata_text(core_deps=("numpy>=1.24.4",)))
assert "metadata-requires-dist" in _failed_names(inspect_wheel(wheel, repo_root=repo))
def test_missing_extra_fails(tmp_path):
wheel, repo = _make_wheel(tmp_path, metadata=_metadata_text(extras={"cuda12": EXTRAS["cuda12"]}))
assert "metadata-extras" in _failed_names(inspect_wheel(wheel, repo_root=repo))
def test_missing_compiled_extension_fails(tmp_path):
wheel, repo = _make_wheel(tmp_path, drop=("orisys/__init__.cp312-win_amd64.pyd",))
assert "compiled-extension" in _failed_names(inspect_wheel(wheel, repo_root=repo))
def test_python_source_leak_fails(tmp_path):
wheel, repo = _make_wheel(
tmp_path, extra_entries={"orisys/sensor.py": b"# leaked\n"}
)
result = inspect_wheel(wheel, repo_root=repo)
assert "no-python-sources" in _failed_names(result)
assert "orisys/sensor.py" in result.render()
def test_example_python_files_are_allowed(tmp_path):
wheel, repo = _make_wheel(
tmp_path, extra_entries={"orisys/examples/plot_data.py": b"# shipped on purpose\n"}
)
assert inspect_wheel(wheel, repo_root=repo).ok
def test_missing_bundled_file_fails(tmp_path):
wheel, repo = _make_wheel(tmp_path, drop=("orisys/sensors/box/sensor.json",))
result = inspect_wheel(wheel, repo_root=repo)
assert "bundle-completeness" in _failed_names(result)
assert "orisys/sensors/box/sensor.json" in result.render()
def test_missing_bundled_file_warns_when_not_strict(tmp_path):
"""A file added to the tree after an old wheel was built is not a defect."""
wheel, repo = _make_wheel(tmp_path, drop=("orisys/sensors/box/sensor.json",))
result = inspect_wheel(wheel, repo_root=repo, strict_bundle=False)
assert result.ok, result.render()
assert any("absent from the wheel" in warning for warning in result.warnings)
def test_bundle_drift_is_fatal_when_strict(tmp_path):
wheel, repo = _make_wheel(
tmp_path, override_bytes={"orisys/docs/api.md": b"# stale\n"}
)
assert "bundle-fidelity" in _failed_names(inspect_wheel(wheel, repo_root=repo, strict_bundle=True))
def test_bundle_drift_warns_when_not_strict(tmp_path):
wheel, repo = _make_wheel(
tmp_path, override_bytes={"orisys/docs/api.md": b"# stale\n"}
)
result = inspect_wheel(wheel, repo_root=repo, strict_bundle=False)
assert result.ok, result.render()
assert any("differ from the source tree" in warning for warning in result.warnings)
def test_pycache_entry_fails(tmp_path):
wheel, repo = _make_wheel(
tmp_path, extra_entries={"orisys/examples/__pycache__/x.pyc": b"junk"}
)
assert "no-forbidden-entries" in _failed_names(inspect_wheel(wheel, repo_root=repo))
def test_scratch_directory_fails(tmp_path):
wheel, repo = _make_wheel(tmp_path, extra_entries={"tests/test_smoke.py": b"x"})
assert "no-forbidden-entries" in _failed_names(inspect_wheel(wheel, repo_root=repo))
def test_traversing_entry_fails(tmp_path):
wheel, repo = _make_wheel(tmp_path, extra_entries={"../escape.txt": b"x"})
assert "archive-paths" in _failed_names(inspect_wheel(wheel, repo_root=repo))
def test_record_hash_mismatch_fails(tmp_path):
wheel, repo = _make_wheel(tmp_path, corrupt_record_hash=True)
assert "record" in _failed_names(inspect_wheel(wheel, repo_root=repo))
def test_missing_record_fails(tmp_path):
wheel, repo = _make_wheel(tmp_path, drop_record=True)
assert "record" in _failed_names(inspect_wheel(wheel, repo_root=repo))
def test_size_limit_fails(tmp_path):
wheel, repo = _make_wheel(tmp_path)
assert "size-limit" in _failed_names(inspect_wheel(wheel, repo_root=repo, max_bytes=1))
def test_bad_manylinux_tag_fails(tmp_path):
wheel, repo = _make_wheel(
tmp_path, filename=f"orisys-{VERSION}-cp312-cp312-linux_x86_64.whl"
)
assert "platform-tag" in _failed_names(inspect_wheel(wheel, repo_root=repo))
@pytest.mark.parametrize("architecture", ["x86_64", "aarch64"])
def test_good_manylinux_tag_passes(tmp_path, architecture):
wheel, repo = _make_wheel(
tmp_path,
filename=f"orisys-{VERSION}-cp312-cp312-manylinux_2_28_{architecture}.whl",
)
assert inspect_wheel(wheel, repo_root=repo).ok
def test_wrong_distribution_name_fails(tmp_path):
wheel, repo = _make_wheel(
tmp_path,
filename=f"notorisys-{VERSION}-cp312-cp312-win_amd64.whl",
metadata=_metadata_text(name="notorisys"),
)
failed = _failed_names(inspect_wheel(wheel, repo_root=repo))
assert {"filename", "metadata-name"} <= failed
def test_wheel_layout_module_resolves_to_build_tree():
import wheel
resolved = Path(wheel.__file__).resolve()
assert BUILD_ROOT.resolve() in resolved.parents, (
f"`wheel` resolved to {resolved}; build/ must precede site-packages so the "
"build tooling imports the repository's wheel package."
)