From b0d64a3eedc7314a75d1fd79270163748b183014 Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Tue, 18 Aug 2026 07:26:30 +0400 Subject: [PATCH] test: add a packaging test, and quieten pytest by default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `make test` searches TESTS_FOLDER for test_*.py / *_test.py and, finding none, prints a warning and exits 0 — so a project with no tests passes `make test` and therefore `make all` while measuring nothing. This adds the one test that closes that hole: it asserts the version declared in pyproject.toml is the version actually installed, which is the invariant that breaks most often on a fresh checkout (a stale editable install, a `uv sync` that never ran, a package directory the build backend does not pick up). Each of those otherwise surfaces as a confusing ImportError several files later. It is deliberately self-contained — no fixtures, no conftest — so it does not depend on anything under .rhiza/tests. pytest.ini alongside it: - log_cli goes from true to false. Live DEBUG logging on every run buries the result; opt back in per-run with `pytest -o log_cli=true --log-cli-level=DEBUG`. - pythonpath = .rhiza/tests, resolved relative to rootdir and harmless when that directory is absent, so the synced suite is importable without each conftest editing sys.path at import time. - the class-scoped-instance-method fixture deprecation becomes an error, so it cannot regress silently. Matched by message rather than category, because the category name is version-specific (PytestRemovedIn9Warning vs ...In10) and naming a missing class breaks `--resolution lowest-direct` at parse time. Third-party DeprecationWarnings stay non-fatal. Both files come from rhiza v1.3.3; they are the part of that sync that needs nothing else from it. Verified on this branch: 173 passed. Co-Authored-By: Claude Opus 5 (1M context) --- pytest.ini | 15 +++++- tests/test_rhiza_packaging.py | 97 +++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 tests/test_rhiza_packaging.py diff --git a/pytest.ini b/pytest.ini index da5b41e..0dd0ef0 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,7 +1,11 @@ [pytest] testpaths = tests -# Enable live logs on console -log_cli = true +# Make the synced template test-suite importable (test_utils, api/, sync/, ...) +# without each conftest manipulating sys.path at import time. Resolved relative +# to rootdir; harmless when .rhiza/tests is absent. +pythonpath = .rhiza/tests +# Disable live logs on console by default (opt in with: pytest -o log_cli=true --log-cli-level=DEBUG) +log_cli = false # Show DEBUG+ messages log_cli_level = DEBUG log_cli_format = %(asctime)s %(levelname)s %(name)s: %(message)s @@ -9,6 +13,13 @@ log_cli_date_format = %H:%M:%S # Show extra summary info for skipped/failed tests addopts = -ra timeout = 60 +# Treat the class-scoped-instance-method fixture deprecation as an error so it +# cannot silently regress. Matched by message rather than by warning category: +# the category name is version-specific (PytestRemovedIn9Warning vs ...In10Warning) +# and referencing a missing class breaks `--resolution lowest-direct` runs at +# parse time. Third-party DeprecationWarnings stay non-fatal. +filterwarnings = + error:Class-scoped fixture defined as instance method is deprecated # Register custom markers markers = stress: marks tests as stress tests (deselect with '-m "not stress"') diff --git a/tests/test_rhiza_packaging.py b/tests/test_rhiza_packaging.py new file mode 100644 index 0000000..8c103c4 --- /dev/null +++ b/tests/test_rhiza_packaging.py @@ -0,0 +1,97 @@ +"""The first test a freshly synced Python project has. + +This file flows down via a SYNC action from the jebel-quant/rhiza repository +(https://github.com/jebel-quant/rhiza). + +**Why it exists.** Two reasons, and the second is the one that is easy to lose. + +It checks a real invariant: that the version the project *declares* in +``pyproject.toml`` is the version actually installed into the environment. Those drift +apart more often than anything else on a fresh checkout — an editable install left over +from a rename, a `uv sync` that never ran, a package directory the build backend is not +configured to pick up. Each shows up here as a mismatch rather than as a confusing +ImportError three files later. + +And it is the only test a freshly synced project has. ``make test`` searches +``TESTS_FOLDER`` for ``test_*.py``/``*_test.py``, and finding none it prints a warning +and **exits 0** — so a new repo passed ``make test``, and therefore ``make all``, while +measuring nothing (#1476). That was the third instance of one pattern: Go had it until +``go-core`` shipped ``internal/version/version_test.go`` (#1467), and ``rhiza-test`` had +it until the ``.rhiza/tests`` suite was actually delivered to every layer (#1469). Rust +never did, because ``cargo init --lib`` leaves an ``it_works`` test behind. + +Writing your own tests alongside this is the point. Deleting it and shipping nothing +else puts the vacuum back. + +**Why not simply fail when no tests exist?** Because that turns ``make all`` red on the +output of ``/rhiza:init``, before the author has written a line. The warning branch is +deliberate; what it needed was something to find. + +Deliberately self-contained: it uses no fixtures, because this lives in *your* ``tests/`` +directory and must not depend on the ``conftest.py`` that ships with ``.rhiza/tests``. +""" + +from __future__ import annotations + +import tomllib +from importlib.metadata import PackageNotFoundError +from importlib.metadata import version as installed_version +from pathlib import Path + +import pytest + +# tests/ sits at the project root, so the parent of this file's directory is the root. +# +# `.absolute()`, never `.resolve()`. In a synced project the difference is nil, but in +# rhiza's own repository this file is a *symlink* into `bundles/python-core/tests/`, and +# `.resolve()` follows it — making the "project root" come out as `bundles/python-core`, +# which has no pyproject.toml. The suite then skips for a plausible-looking wrong reason +# instead of running. `.rhiza/tests/conftest.py` avoids the same trap the same way. +_ROOT = Path(__file__).absolute().parent.parent + + +def _project_table() -> dict: + """Return the ``[project]`` table from pyproject.toml, skipping when unusable. + + Returns: + The parsed ``[project]`` table. + """ + pyproject = _ROOT / "pyproject.toml" + if not pyproject.is_file(): + pytest.skip("no pyproject.toml at the project root") + with pyproject.open("rb") as handle: + table = tomllib.load(handle).get("project") + if not isinstance(table, dict): + pytest.skip("pyproject.toml declares no [project] table") + return table + + +def test_the_installed_version_matches_pyproject() -> None: + """The distribution installed in the environment must match the declared version. + + Skips rather than fails when the project is not installed as a distribution at all — + a ``uv`` *virtual* project (no ``[build-system]``, ``source = {{ virtual = "." }}`` in + the lockfile) has no metadata to read, and rhiza's own repository is one. That is a + real configuration, not a broken one, so it is not this test's business. + """ + project = _project_table() + + name = project.get("name") + if not isinstance(name, str) or not name.strip(): + pytest.skip("[project].name is missing or not a plain string") + + declared = project.get("version") + if not isinstance(declared, str): + pytest.skip("[project].version is dynamic — there is no static value to compare") + + try: + found = installed_version(name) + except PackageNotFoundError: + pytest.skip(f"{name!r} is not installed as a distribution (a virtual project has no metadata)") + + assert found == declared, ( + f"pyproject.toml declares version {declared!r} but the installed {name!r} reports " + f"{found!r}. The environment is stale or the build backend is picking up a different " + f"tree — re-run `make install`, and check [build-system] and the packages it is told " + f"to include if that does not fix it." + )