-
Notifications
You must be signed in to change notification settings - Fork 1
test: skip optional-extra suites when the sandbox lacks them #503
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
|
Comment on lines
+57
to
+67
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📝 Info: Substring import matching can over-skip files The import scan in Was this helpful? React with 👍 or 👎 to provide feedback. |
||
| return False | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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(), | ||
| ) | ||
|
Comment on lines
+13
to
+19
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 Backend test suite still fails collection in the sandbox The ignore hook lives in conftest.py, so Prompt for agentsWas this helpful? React with 👍 or 👎 to provide feedback. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📝 Info: Two backend-path checks are dead code
pytest passes absolute paths, so
posix == "backend"andposix.startswith("backend/")at optional_extra_collection.py never match; only the"/backend/" in posixsubstring ever fires. Harmless, but the first two conditions are unreachable.Was this helpful? React with 👍 or 👎 to provide feedback.