Files
voculinux/tests/test_check_build_deps.py
Sergey SilaevandGitHub b59797f1af chore(dev): widen uv range and harden Fedora/AppImage builds (#952)
* chore(deps): allow uv versions below 0.14

* fix(ci): align uv version and generated exports

* fix(dev): check Fedora build headers and AppImage GI bundle

Fail early with distro-specific native dependency hints before dev sync. Verify the AppImage's PyGObject installation and Python path after linuxdeploy, and report isolated GI import failures with actionable path diagnostics. Guard the CI uv pin and cover both preflights with tests.
2026-10-23 22:16:16 -07:00

251 lines
9.4 KiB
Python

"""Guards for the `just deps` native build-dependency preflight."""
import importlib.metadata
import importlib.util
import subprocess
from pathlib import Path
from typing import Any
import pytest
SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "check_build_deps.py"
SPEC = importlib.util.spec_from_file_location("check_build_deps", SCRIPT)
assert SPEC is not None and SPEC.loader is not None
PREFLIGHT: Any = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(PREFLIGHT)
@pytest.fixture(autouse=True)
def linux_platform(monkeypatch: pytest.MonkeyPatch) -> None:
"""Keep Linux checks covered even when contributors run the suite on macOS."""
monkeypatch.setattr(PREFLIGHT.sys, "platform", "linux")
def test_os_release_parser_strips_quotes_and_ignores_comments(tmp_path: Path) -> None:
"""os-release is parsed, not sourced; quoted values lose their quotes."""
release = tmp_path / "os-release"
release.write_text(
'ID=fedora\nID_LIKE="rhel centos"\n# comment\nVERSION_ID="45"\n',
encoding="utf-8",
)
assert PREFLIGHT.read_os_release(release) == {
"ID": "fedora",
"ID_LIKE": "rhel centos",
"VERSION_ID": "45",
}
@pytest.mark.parametrize(
"distro,like,expected",
[
("ubuntu", "debian", "sudo apt install"),
("fedora", "rhel", "sudo dnf install"),
("rocky", "rhel fedora", "sudo dnf install"),
("arch", "", "sudo pacman -S"),
("opensuse-tumbleweed", "suse", "sudo zypper install"),
],
)
def test_install_hint_matches_the_distro_family(distro: str, like: str, expected: str) -> None:
"""The failure names the package manager the contributor actually has."""
hint = "\n".join(PREFLIGHT.install_hint({"ID": distro, "ID_LIKE": like}))
assert expected in hint
def test_debian_hint_points_older_releases_at_the_installer() -> None:
"""Debian 12 cannot build PyGObject 3.56, so the hint says what works."""
hint = "\n".join(PREFLIGHT.install_hint({"ID": "debian", "ID_LIKE": ""}))
assert "sudo apt install" in hint
assert "./install.sh --dev" in hint
def test_fedora_hint_keeps_the_split_cairo_gobject_package() -> None:
"""Fedora splits cairo-gobject.pc and girepository-2.0.pc across packages."""
hint = "\n".join(PREFLIGHT.install_hint({"ID": "fedora", "ID_LIKE": ""}))
assert "cairo-gobject-devel" in hint
assert "glib2-devel" in hint
def test_old_fedora_hint_points_at_the_installer() -> None:
"""Installing more headers cannot make Fedora 39's glib new enough."""
hint = "\n".join(PREFLIGHT.install_hint({"ID": "fedora", "VERSION_ID": "39"}))
assert "./install.sh --dev" in hint
assert "sudo dnf install" not in hint
def test_unknown_distro_gets_a_generic_hint() -> None:
"""No false package-manager command when the family is unknown."""
hint = "\n".join(PREFLIGHT.install_hint({"ID": "plan9", "ID_LIKE": ""}))
assert "development packages" in hint
assert "sudo " not in hint
def test_missing_pkg_config_is_reported_without_probing_modules(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""The tool itself missing is the first actionable failure."""
monkeypatch.setattr(
PREFLIGHT.shutil,
"which",
lambda name: "/usr/bin/cc" if name == "cc" else None,
)
def fail_run(*args: Any, **kwargs: Any) -> Any:
raise AssertionError("pkg-config cannot be queried when it is absent")
monkeypatch.setattr(PREFLIGHT.subprocess, "run", fail_run)
assert PREFLIGHT.missing_build_dependencies() == ["pkg-config"]
def test_missing_modules_keep_their_explanations(monkeypatch: pytest.MonkeyPatch) -> None:
"""Every absent pkg-config module is reported with the package it comes from."""
monkeypatch.setattr(PREFLIGHT.shutil, "which", lambda name: f"/usr/bin/{name}")
class Result:
def __init__(self, returncode: int) -> None:
self.returncode = returncode
def fake_run(args: list[str], **kwargs: Any) -> Result:
return Result(0 if args[-1] == "portaudio-2.0" else 1)
monkeypatch.setattr(PREFLIGHT.subprocess, "run", fake_run)
missing = PREFLIGHT.missing_build_dependencies()
assert any("cairo headers" in item for item in missing)
assert any("libffi" in item for item in missing)
assert not any("portaudio" in item.lower() for item in missing)
def test_girepository_requires_glib_280(monkeypatch: pytest.MonkeyPatch) -> None:
"""A too-old girepository-2.0.pc must not pass the preflight."""
monkeypatch.setattr(PREFLIGHT.shutil, "which", lambda name: f"/usr/bin/{name}")
checked: list[list[str]] = []
class Result:
def __init__(self, returncode: int) -> None:
self.returncode = returncode
def fake_run(args: list[str], **kwargs: Any) -> Result:
checked.append(args)
return Result(1 if args[-1] == "girepository-2.0" else 0)
monkeypatch.setattr(PREFLIGHT.subprocess, "run", fake_run)
assert PREFLIGHT.missing_build_dependencies() == ["GObject Introspection headers (glib 2.80+)"]
assert ["pkg-config", "--atleast-version=2.80", "girepository-2.0"] in checked
def test_fresh_macos_environment_does_not_need_gi_headers(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
"""PyAudio's build prerequisites suffice when the lock excludes PyGObject."""
monkeypatch.setattr(PREFLIGHT.sys, "platform", "darwin")
monkeypatch.setattr(PREFLIGHT, "VENV_PYTHON", tmp_path / "absent-python")
monkeypatch.setattr(PREFLIGHT.shutil, "which", lambda name: f"/usr/bin/{name}")
checked: list[str] = []
def fake_run(args: list[str], **kwargs: Any) -> subprocess.CompletedProcess[str]:
checked.append(args[-1])
return subprocess.CompletedProcess(args, 0 if args[-1] == "portaudio-2.0" else 1)
monkeypatch.setattr(PREFLIGHT.subprocess, "run", fake_run)
assert PREFLIGHT.main() == 0
assert checked == ["portaudio-2.0"]
@pytest.mark.parametrize(
"platform,installed,expected",
[
("darwin", {"pyaudio"}, True),
("darwin", set(), False),
("linux", {"pyaudio"}, False),
("linux", {"pyaudio", "pygobject"}, False),
("linux", {"pyaudio", "pycairo"}, False),
("linux", {"pyaudio", "pycairo", "pygobject"}, True),
],
)
def test_venv_probe_matches_platform_dependencies(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
platform: str,
installed: set[str],
expected: bool,
) -> None:
"""Run the metadata probe against complete and partial per-platform environments."""
monkeypatch.setattr(PREFLIGHT.sys, "platform", platform)
python = tmp_path / "python"
python.touch()
monkeypatch.setattr(PREFLIGHT, "VENV_PYTHON", python)
def version(package: str) -> str:
if package not in installed:
raise importlib.metadata.PackageNotFoundError(package)
return "1.0"
def fake_run(args: list[str], **kwargs: Any) -> subprocess.CompletedProcess[str]:
assert args[:2] == [str(python), "-c"]
try:
exec(args[2], {})
except importlib.metadata.PackageNotFoundError:
return subprocess.CompletedProcess(args, 1)
return subprocess.CompletedProcess(args, 0)
monkeypatch.setattr(importlib.metadata, "version", version)
monkeypatch.setattr(PREFLIGHT.subprocess, "run", fake_run)
assert PREFLIGHT.venv_has_compiled_packages() is expected
def test_macos_missing_portaudio_has_no_gi_install_hint(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""Failure guidance should request only packages macOS tooling actually needs."""
monkeypatch.setattr(PREFLIGHT.sys, "platform", "darwin")
monkeypatch.setattr(PREFLIGHT, "VENV_PYTHON", tmp_path / "absent-python")
monkeypatch.setattr(PREFLIGHT.shutil, "which", lambda name: f"/usr/bin/{name}")
monkeypatch.setattr(
PREFLIGHT.subprocess, "run", lambda args, **kwargs: subprocess.CompletedProcess(args, 1)
)
assert PREFLIGHT.main() == 1
error = capsys.readouterr().err
assert "PortAudio headers" in error
assert "brew install portaudio pkg-config" in error
assert "xcode-select --install" in error
assert "PyGObject" not in error
assert "cairo" not in error
assert "libffi" not in error
def test_existing_environment_short_circuits_the_check(monkeypatch: pytest.MonkeyPatch) -> None:
"""An environment that already holds its native packages needs no header check."""
monkeypatch.setattr(PREFLIGHT, "venv_has_compiled_packages", lambda: True)
monkeypatch.setattr(
PREFLIGHT,
"missing_build_dependencies",
lambda: pytest.fail("the preflight should not inspect headers"),
)
assert PREFLIGHT.main() == 0
def test_missing_dependencies_fail_with_install_commands(
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
"""The default failure is the actionable report, not a compiler traceback."""
monkeypatch.setattr(PREFLIGHT, "venv_has_compiled_packages", lambda: False)
monkeypatch.setattr(
PREFLIGHT, "missing_build_dependencies", lambda: ["PortAudio headers (pyaudio)"]
)
monkeypatch.setattr(PREFLIGHT, "read_os_release", lambda: {"ID": "fedora"})
assert PREFLIGHT.main() == 1
error = capsys.readouterr().err
assert "missing native build dependencies" in error
assert "PortAudio headers (pyaudio)" in error
assert "sudo dnf install" in error
assert "CONTRIBUTING.md" in error