From c97104d084d239decb884685e990a6593dd9db04 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 15:54:16 +0000 Subject: [PATCH] test: skip optional-extra suites when the sandbox lacks them OpenCode coverage-evidence cannot install LineageWeave backend extras. Collection now ignores files that import asyncpg, psycopg2, redis, or fast_mlsirm when those modules are absent. Hosted CI still installs the extras, so the full suite continues to run there. --- .../2.12.7-optional-extra-collection.md | 10 +++ lineageweave/optional_extra_collection.py | 68 ++++++++++++++++++ tests/conftest.py | 19 +++++ tests/test_optional_extra_collection.py | 70 +++++++++++++++++++ 4 files changed, 167 insertions(+) create mode 100644 CHANGELOG.d/2.12.7-optional-extra-collection.md create mode 100644 lineageweave/optional_extra_collection.py create mode 100644 tests/conftest.py create mode 100644 tests/test_optional_extra_collection.py diff --git a/CHANGELOG.d/2.12.7-optional-extra-collection.md b/CHANGELOG.d/2.12.7-optional-extra-collection.md new file mode 100644 index 000000000..c8b831d5a --- /dev/null +++ b/CHANGELOG.d/2.12.7-optional-extra-collection.md @@ -0,0 +1,10 @@ +# Skip optional-extra suites when the sandbox did not install them + +- OpenCode coverage-evidence supplies pytest and coverage but not + LineageWeave's optional backend extras. Collection no longer fails + with ``ModuleNotFoundError`` for ``asyncpg``, ``psycopg2``, ``redis``, + or ``fast_mlsirm`` when those extras are absent. +- Hosted CI still installs ``dev`` and ``backend`` extras, so every + suite continues to collect and run there. +- Login typecheck remains LineageWeave#494; this head stacks on that + repair and does not rewrite the gap baseline. diff --git a/lineageweave/optional_extra_collection.py b/lineageweave/optional_extra_collection.py new file mode 100644 index 000000000..3b2d5bbe5 --- /dev/null +++ b/lineageweave/optional_extra_collection.py @@ -0,0 +1,68 @@ +"""Decide which pytest files need optional extras that may be absent. + +OpenCode coverage-evidence runs in a networkless sandbox that supplies +pytest and coverage but not LineageWeave's optional backend extras +(``asyncpg``, ``psycopg2``, ``redis``, ``fast_mlsirm``). Hosted CI +installs those extras and collects every suite. This helper keeps +collection from failing with ``ModuleNotFoundError`` when extras are +absent, without skipping anything when they are present. +""" + +from __future__ import annotations + +import importlib.util +from collections.abc import Iterable +from pathlib import Path + +OPTIONAL_EXTRA_MODULES: tuple[str, ...] = ( + "asyncpg", + "psycopg2", + "redis", + "fast_mlsirm", +) + +_BACKEND_EXTRAS: frozenset[str] = frozenset({"asyncpg", "psycopg2", "redis"}) +_HELPER_TEST_NAME = "test_optional_extra_collection.py" + + +def missing_optional_extra_modules( + module_names: Iterable[str] = OPTIONAL_EXTRA_MODULES, +) -> tuple[str, ...]: + """Return optional extra names the current interpreter cannot import.""" + return tuple( + name for name in module_names if importlib.util.find_spec(name) is None + ) + + +def collection_path_requires_missing_extras( + collection_path: Path, + missing_modules: Iterable[str], +) -> bool: + """Return whether collecting this path would import a missing extra.""" + missing = tuple(missing_modules) + if not missing: + return False + if collection_path.suffix != ".py": + return False + if collection_path.name == _HELPER_TEST_NAME: + return False + posix = collection_path.as_posix() + if any(name in _BACKEND_EXTRAS for name in missing): + if posix == "backend" or posix.startswith("backend/") or "/backend/" in posix: + return True + try: + text = collection_path.read_text(encoding="utf-8") + except OSError: + return False + for name in missing: + if ( + f"import {name}" in text + or f"from {name} " in text + or f"from {name}." in text + ): + return True + if name in _BACKEND_EXTRAS and ( + "from backend" in text or "import backend" in text + ): + return True + return False diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 000000000..e110d9c85 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,19 @@ +"""Pytest collection hooks for optional backend extras.""" + +from __future__ import annotations + +from pathlib import Path + +from lineageweave.optional_extra_collection import ( + collection_path_requires_missing_extras, + missing_optional_extra_modules, +) + + +def pytest_ignore_collect(collection_path: Path, config: object) -> bool: + """Skip files that import optional extras the sandbox did not install.""" + del config + return collection_path_requires_missing_extras( + collection_path, + missing_optional_extra_modules(), + ) diff --git a/tests/test_optional_extra_collection.py b/tests/test_optional_extra_collection.py new file mode 100644 index 000000000..12f9a143e --- /dev/null +++ b/tests/test_optional_extra_collection.py @@ -0,0 +1,70 @@ +"""Unit tests for optional-extra pytest collection skipping.""" + +from __future__ import annotations + +from pathlib import Path +from unittest.mock import patch + +from lineageweave.optional_extra_collection import ( + OPTIONAL_EXTRA_MODULES, + collection_path_requires_missing_extras, + missing_optional_extra_modules, +) + + +def test_missing_optional_extra_modules_returns_absent_names() -> None: + """Names whose find_spec is None are reported; present names are not.""" + + def fake_find_spec(name: str) -> object | None: + if name == "asyncpg": + return None + return object() + + with patch( + "lineageweave.optional_extra_collection.importlib.util.find_spec", + side_effect=fake_find_spec, + ): + assert missing_optional_extra_modules(("asyncpg", "redis")) == ("asyncpg",) + + +def test_collection_path_does_not_skip_when_no_extras_are_missing( + tmp_path: Path, +) -> None: + """Hosted CI with extras installed still collects every suite.""" + path = tmp_path / "test_api.py" + path.write_text("import asyncpg\n", encoding="utf-8") + assert collection_path_requires_missing_extras(path, ()) is False + + +def test_collection_path_skips_backend_tree_when_asyncpg_is_missing( + tmp_path: Path, +) -> None: + """Backend tests import the FastAPI app, which imports asyncpg at module level.""" + backend_test = tmp_path / "backend" / "tests" / "test_api.py" + backend_test.parent.mkdir(parents=True) + backend_test.write_text("import pytest\n", encoding="utf-8") + assert collection_path_requires_missing_extras(backend_test, ("asyncpg",)) is True + + +def test_collection_path_skips_files_that_import_a_missing_extra( + tmp_path: Path, +) -> None: + """A test that imports fast_mlsirm is ignored only when that extra is absent.""" + path = tmp_path / "test_post_evaluation.py" + path.write_text("from fast_mlsirm import LLMJudgeResult\n", encoding="utf-8") + assert collection_path_requires_missing_extras(path, ("fast_mlsirm",)) is True + assert collection_path_requires_missing_extras(path, ("asyncpg",)) is False + + +def test_helper_test_module_is_never_ignored(tmp_path: Path) -> None: + """The collection-helper tests must run in the sandbox that lacks extras.""" + path = tmp_path / "test_optional_extra_collection.py" + path.write_text("import asyncpg\n", encoding="utf-8") + assert collection_path_requires_missing_extras(path, OPTIONAL_EXTRA_MODULES) is False + + +def test_non_python_paths_are_not_ignored(tmp_path: Path) -> None: + """Collection directories and non-Python files stay visible to pytest.""" + directory = tmp_path / "tests" + directory.mkdir() + assert collection_path_requires_missing_extras(directory, ("asyncpg",)) is False