From 8d292184d4748de22311798205a25221f50a18cb Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Thu, 23 Jul 2026 01:18:47 +0200 Subject: [PATCH] ci: execute tagged module pytest suites --- .gitea/workflows/release-integration.yml | 3 +- requirements-dev.txt | 1 + tests/test_release_integration.py | 38 +++++++++++++++++++++++ tools/checks/check-release-integration.sh | 25 +++------------ tools/checks/release_integration.py | 25 ++++++++++++++- 5 files changed, 68 insertions(+), 24 deletions(-) diff --git a/.gitea/workflows/release-integration.yml b/.gitea/workflows/release-integration.yml index cb7225a..a75113e 100644 --- a/.gitea/workflows/release-integration.yml +++ b/.gitea/workflows/release-integration.yml @@ -28,8 +28,7 @@ jobs: run: | python -m venv .venv .venv/bin/python tools/repo/sync-python-environment.py --requirements requirements-release.txt --python .venv/bin/python --upgrade-pip - .venv/bin/python -m pip install '../govoplan-core[dev]' - .venv/bin/python -m pip install -r requirements-release-tests.txt + .venv/bin/python -m pip install -r requirements-release-tests.txt '../govoplan-core[dev]' - name: Install WebUI release dependencies working-directory: govoplan run: bash tools/release/install-webui-release-dependencies.sh ../govoplan-core/webui diff --git a/requirements-dev.txt b/requirements-dev.txt index a4aa7ba..60c38f6 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -32,5 +32,6 @@ idna>=3.15 jsonschema>=4,<5 pip>=26.1.2 pip-audit>=2.9,<3 +pytest>=8,<9 python-multipart>=0.0.31 ruff>=0.14,<1 diff --git a/tests/test_release_integration.py b/tests/test_release_integration.py index f637b3d..f0b9b5f 100644 --- a/tests/test_release_integration.py +++ b/tests/test_release_integration.py @@ -1,8 +1,10 @@ from __future__ import annotations +import os import tempfile import unittest from pathlib import Path +from unittest.mock import patch from tools.checks.release_integration import ( SOURCE_COUPLED_CORE_TESTS, @@ -10,6 +12,7 @@ from tools.checks.release_integration import ( artifact_contract_issues, filter_test_suite, release_package_names, + run_module_release_tests, ) @@ -89,8 +92,43 @@ class ReleaseIntegrationTests(unittest.TestCase): self.assertLess(artifact_check, core_tests) self.assertLess(core_tests, module_tests) + self.assertNotIn("unittest discover", script) self.assertNotIn('"$PYTHON" -m unittest \\\n tests.test_module_system', script) + def test_tagged_module_runner_executes_pytest_functions_from_module_root(self) -> None: + with tempfile.TemporaryDirectory(prefix="govoplan-release-module-tests-") as temp_dir: + module_root = Path(temp_dir) + tests_root = module_root / "tests" + tests_root.mkdir() + marker = module_root / "pytest-function-ran" + (tests_root / "test_probe.py").write_text( + """from pathlib import Path +import os + + +def test_pytest_style_function(): + expected_root = Path(os.environ[\"GOVOPLAN_TEST_MODULE_ROOT\"]) + assert Path.cwd() == expected_root + Path(os.environ[\"GOVOPLAN_TEST_MARKER\"]).write_text(\"ran\", encoding=\"utf-8\") +""", + encoding="utf-8", + ) + with patch.dict( + os.environ, + { + "GOVOPLAN_TEST_MODULE_ROOT": str(module_root), + "GOVOPLAN_TEST_MARKER": str(marker), + }, + ): + status = run_module_release_tests(module_root) + + self.assertEqual(status, 0) + self.assertEqual(marker.read_text(encoding="utf-8"), "ran") + + def test_tagged_module_runner_skips_missing_test_tree(self) -> None: + with tempfile.TemporaryDirectory(prefix="govoplan-release-module-tests-") as temp_dir: + self.assertEqual(run_module_release_tests(Path(temp_dir)), 0) + def test_module_matrix_uses_artifact_aware_core_suite(self) -> None: meta_root = Path(__file__).resolve().parents[1] script = (meta_root / "tools" / "checks" / "check-module-matrix.sh").read_text(encoding="utf-8") diff --git a/tools/checks/check-release-integration.sh b/tools/checks/check-release-integration.sh index 2b62f36..b319cb4 100644 --- a/tools/checks/check-release-integration.sh +++ b/tools/checks/check-release-integration.sh @@ -60,24 +60,6 @@ retry() { done } -run_discovered_tests() { - local suite_dir="$1" - local status - if ! find "$suite_dir" -type f -name 'test*.py' -print -quit | grep -q .; then - echo "No unittest test files found under $suite_dir; skipping." - return 0 - fi - set +e - "$PYTHON" -m unittest discover -s "$suite_dir" - status=$? - set -e - if [[ "$status" == 5 ]]; then - echo "No unittest tests discovered under $suite_dir; skipping." - return 0 - fi - return "$status" -} - install_cloned_webui_dependencies() { local webui_dir="$1" if [[ ! -f "$webui_dir/package.json" ]]; then @@ -253,9 +235,10 @@ run_step "Run core backend release tests" "$PYTHON" "$META_ROOT/tools/checks/check_dependency_boundaries.py" run_step "Run cloned module backend tests" -run_discovered_tests "$WORK_ROOT/govoplan-mail/tests" -run_discovered_tests "$WORK_ROOT/govoplan-calendar/tests" -run_discovered_tests "$WORK_ROOT/govoplan-campaign/tests" +for repo in govoplan-mail govoplan-calendar govoplan-campaign; do + "$PYTHON" "$META_ROOT/tools/checks/release_integration.py" module-tests \ + --module-root "$WORK_ROOT/$repo" +done run_step "Run core WebUI release tests and build" cd "$ROOT/webui" diff --git a/tools/checks/release_integration.py b/tools/checks/release_integration.py index 51e427a..6f2915f 100644 --- a/tools/checks/release_integration.py +++ b/tools/checks/release_integration.py @@ -5,6 +5,7 @@ import argparse import importlib.metadata as metadata import os import re +import subprocess import sys import tempfile import unittest @@ -354,6 +355,24 @@ def run_core_release_tests(core_root: Path) -> int: return 0 if result.wasSuccessful() else 1 +def run_module_release_tests(module_root: Path) -> int: + resolved_root = module_root.resolve() + tests_root = resolved_root / "tests" + if not tests_root.is_dir() or not any(tests_root.rglob("test*.py")): + print(f"No test files found under {tests_root}; skipping.") + return 0 + + result = subprocess.run( + (sys.executable, "-m", "pytest", "-q", "tests"), + cwd=resolved_root, + check=False, + ) + if result.returncode == 5: + print(f"No tests collected under {tests_root}; skipping.") + return 0 + return result.returncode + + def _parser() -> argparse.ArgumentParser: meta_root = Path(__file__).resolve().parents[2] parser = argparse.ArgumentParser(description="Artifact-aware GovOPlaN release integration checks") @@ -366,6 +385,8 @@ def _parser() -> argparse.ArgumentParser: ) core_parser = subparsers.add_parser("core-tests", help="Run Core tests that are valid for tagged module artifacts") core_parser.add_argument("--core-root", type=Path, required=True) + module_parser = subparsers.add_parser("module-tests", help="Run tests from one tagged module source checkout") + module_parser.add_argument("--module-root", type=Path, required=True) return parser @@ -373,7 +394,9 @@ def main(argv: Sequence[str] | None = None) -> int: args = _parser().parse_args(argv) if args.command == "artifacts": return run_installed_artifact_checks(args.requirements) - return run_core_release_tests(args.core_root) + if args.command == "core-tests": + return run_core_release_tests(args.core_root) + return run_module_release_tests(args.module_root) if __name__ == "__main__":