Files
MeshTerm/tests/test_notices.py
T
Jean-Pierre MartineauandClaude Fable 5.1 bca1c83dcd
Installers / windows-x64 (push) Canceled after 0s
Installers / macos-x64 (push) Canceled after 0s
Installers / macos-arm64 (push) Canceled after 0s
Installers / linux-x64 (push) Canceled after 0s
Installers / linux-arm64 (push) Canceled after 0s
GitHub release / build (push) Canceled after 0s
GitHub release / create the release (push) Canceled after 0s
MeshTerm 0.3.4
The macOS installers of 0.3.3 never built: the notices step refused pyobjc-core and
pyobjc-framework-libdispatch, whose wheels ship no licence text. Their MIT text is vendored
now, and a test checks every vendored path on every platform.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 15:50:53 -04:00

165 lines
7.4 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""Tests for ``packaging/notices.py``, the third-party notices generator.
Loaded by file path rather than as ``packaging.notices``: ``packaging`` is also the name
of a PyPI library that module itself imports (``packaging.markers``,
``packaging.requirements``), and the repository's ``packaging/`` directory is not a real
package — it has no ``__init__.py``, deliberately, so it can never shadow that library
(a regular package anywhere on ``sys.path`` always wins over a same-named directory with
no ``__init__.py``, which is what keeps ``import packaging.markers`` finding the real one
even with the repo root on the path). Reaching the module by file avoids relying on that
distinction holding forever.
Skips cleanly when ``mesh-term`` isn't installed in the interpreter running the tests —
the generator walks the *installed* dependency closure, so without an installed
distribution to walk there is nothing here to test.
"""
from __future__ import annotations
import importlib.util
import sys
from importlib.metadata import PackageNotFoundError, distribution
from pathlib import Path
import pytest
from packaging.requirements import Requirement
if sys.version_info >= (3, 11):
import tomllib
else: # pragma: no cover - exercised only on 3.10
import tomli as tomllib
try:
distribution("mesh-term")
except PackageNotFoundError:
pytest.skip("mesh-term is not installed in this interpreter", allow_module_level=True)
_REPO_ROOT = Path(__file__).resolve().parent.parent
_spec = importlib.util.spec_from_file_location(
"meshterm_packaging_notices", _REPO_ROOT / "packaging" / "notices.py"
)
assert _spec is not None and _spec.loader is not None
notices = importlib.util.module_from_spec(_spec)
# Registered before exec: `Notice` is a dataclass under `from __future__ import
# annotations`, and dataclasses resolves deferred annotations via `sys.modules[cls.
# __module__]` — a module never registered there fails with a confusing `NoneType has
# no attribute '__dict__'` the moment the class body runs, unrelated to anything this
# test is actually checking.
sys.modules[_spec.name] = notices
_spec.loader.exec_module(notices)
#: The copyleft licenses no *runtime* dependency of MeshTerm may carry. Checking for
#: "GPL" alone also catches LGPL and AGPL (both contain it as a substring); MPL needs
#: its own check. PyInstaller itself is GPL-with-bootloader-exception, and must never
#: appear here precisely because it is a build tool, not part of the runtime closure
#: this module walks — its absence from the generated notices is the thing this test
#: guards, not an oversight to work around.
_COPYLEFT_MARKERS = ("GPL", "MPL")
@pytest.fixture(scope="module")
def all_notices() -> list:
"""Every notice this interpreter's install of ``mesh-term`` would generate."""
return notices.collect_notices()
def _direct_runtime_dependencies() -> list[Requirement]:
"""The ``[project.dependencies]`` MeshTerm itself declares, applicable here.
Mirrors :func:`notices.dependency_closure`'s own marker evaluation (``extra`` forced
to ``""``, meaning "no extras requested") so a dependency gated to a platform or
Python version this interpreter isn't — the pre-3.11 ``tomli`` backport, one day a
Windows- or macOS-only package — is excluded here exactly as it would be from the
generated notices, rather than flagged as "missing".
"""
from packaging.markers import default_environment
environment = default_environment()
environment["extra"] = ""
pyproject = tomllib.loads((_REPO_ROOT / "pyproject.toml").read_text(encoding="utf-8"))
requirements = [Requirement(raw) for raw in pyproject["project"]["dependencies"]]
return [req for req in requirements if req.marker is None or req.marker.evaluate(environment)]
def test_every_direct_dependency_has_a_notice(all_notices: list) -> None:
"""Every applicable ``pyproject.toml`` dependency turns up as a generated notice."""
from packaging.utils import canonicalize_name
covered = {canonicalize_name(n.name) for n in all_notices}
missing = [
req.name
for req in _direct_runtime_dependencies()
if canonicalize_name(req.name) not in covered
]
assert not missing, f"no notice generated for: {missing}"
def test_every_notice_has_license_text(all_notices: list) -> None:
"""Nothing in the generated set carries empty or whitespace-only license text."""
empty = [f"{n.name} {n.version}" for n in all_notices if not n.text.strip()]
assert not empty, f"empty license text for: {empty}"
def test_python_entry_present_and_mentions_psf(all_notices: list) -> None:
"""Python's own PSF license is the first entry, and it is actually the PSF text."""
assert all_notices, "no notices generated at all"
python_entry = all_notices[0]
assert python_entry.name == "Python"
assert "PSF" in python_entry.summary or "PSF" in python_entry.text
def test_no_copyleft_dependency(all_notices: list) -> None:
"""No runtime dependency's declared license is GPL/LGPL/AGPL/MPL.
MeshTerm's dependency closure is entirely MIT/BSD/ISC/PSF/Apache-2.0. PyInstaller
itself is GPL-with-bootloader-exception but is a *build* tool, never a runtime
dependency of the frozen app, so it must never appear in ``all_notices`` at all —
this both confirms that and guards against a future dependency quietly adding one.
"""
offenders = [
f"{n.name} {n.version} ({n.summary})"
for n in all_notices
if any(marker in n.summary.upper() for marker in _COPYLEFT_MARKERS)
]
assert not offenders, f"copyleft license found: {offenders}"
def test_output_is_deterministic() -> None:
"""Rendering the same closure twice produces byte-identical output."""
first = notices.render(notices.collect_notices())
second = notices.render(notices.collect_notices())
assert first == second
def test_rendered_output_is_newline_terminated_utf8() -> None:
"""The rendered document ends with a single trailing newline and encodes as UTF-8."""
rendered = notices.render(notices.collect_notices())
assert rendered.endswith("\n")
assert not rendered.endswith("\n\n")
rendered.encode("utf-8") # raises on anything that wouldn't round-trip
def test_write_third_party_notices_writes_utf8_lf(tmp_path: Path) -> None:
"""The file on disk matches ``render()`` exactly, with no newline translation."""
out = notices.write_third_party_notices(tmp_path / "THIRD-PARTY-NOTICES.txt")
raw = out.read_bytes()
assert b"\r\n" not in raw
assert raw.decode("utf-8") == notices.render(notices.collect_notices())
def test_every_vendored_license_gap_has_its_file() -> None:
"""A gap-table entry whose file is missing would fail only on the platform that hits it.
The pyobjc entries are exercised by a macOS build alone and the winrt ones by a
Windows build alone, so a typo in a path would surface on a release runner rather
than here. Check every entry on every platform: the file exists, is non-empty, and
the key is in the canonical (lowercase, hyphenated) form ``collect_notices`` looks up.
"""
for name, paths in notices._VENDORED_LICENSE_GAPS.items():
assert name == name.lower().replace("_", "-"), name
for rel in paths:
path = Path(notices.__file__).parent / rel
assert path.is_file(), f"{name}: {rel} is missing"
assert path.read_text(encoding="utf-8").strip(), f"{name}: {rel} is empty"