From fc00538b43b8bf4cc34cafccf62b379c6d97f9cf Mon Sep 17 00:00:00 2001 From: rookiestar28 Date: Tue, 21 Apr 2026 00:08:35 +0800 Subject: [PATCH] fix(ci): install coverage toml support --- .github/workflows/ci.yml | 4 +- scripts/pre_push_checks.sh | 8 ++-- scripts/registry_publish_guard.py | 7 +++- scripts/run_backend_coverage.py | 18 ++++++++ scripts/run_full_tests_windows.ps1 | 14 ++++--- ...est_bf12_registry_publish_version_guard.py | 11 ++--- tests/test_bf13_coverage_toml_parity.py | 41 +++++++++++++++++++ tests/test_r184_backend_coverage_gate.py | 24 +++++++++++ 8 files changed, 109 insertions(+), 18 deletions(-) create mode 100644 tests/test_bf13_coverage_toml_parity.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2427391..e4066b7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -94,8 +94,10 @@ jobs: python -m pip install --upgrade pip # Keep aligned with local pre-push/full-test scripts. # aiohttp is required by multiple unit-test import paths. + # CRITICAL: Python 3.10 coverage reads pyproject.toml only when the + # TOML extra is present; do not downgrade this back to plain coverage. python -m pip install -r requirements.txt - python -m pip install numpy pillow aiohttp coverage + python -m pip install numpy pillow aiohttp "coverage[toml]" - name: R120 preflight run: | python scripts/preflight_check.py --strict diff --git a/scripts/pre_push_checks.sh b/scripts/pre_push_checks.sh index eabcd13..8076617 100644 --- a/scripts/pre_push_checks.sh +++ b/scripts/pre_push_checks.sh @@ -218,9 +218,11 @@ if ! "$VENV_PY" -c "import defusedxml" >/dev/null 2>&1; then echo "[pre-push] INFO: installing defusedxml into project venv ($VENV_DIR) ..." >&2 pip_install_or_fail "required for S85 fail-closed XML parsing paths/tests" defusedxml fi -if ! "$VENV_PY" -c "import coverage" >/dev/null 2>&1; then - echo "[pre-push] INFO: installing coverage into project venv ($VENV_DIR) ..." >&2 - pip_install_or_fail "required for backend coverage gate" coverage +if ! "$VENV_PY" -c "import sys, importlib.util; has_coverage = importlib.util.find_spec('coverage') is not None; has_toml = sys.version_info >= (3, 11) or importlib.util.find_spec('tomli') is not None; sys.exit(0 if has_coverage and has_toml else 1)" >/dev/null 2>&1; then + # CRITICAL: Python 3.10 coverage cannot read pyproject.toml without TOML + # support; plain coverage here causes local pass/CI fail drift. + echo "[pre-push] INFO: installing coverage[toml] into project venv ($VENV_DIR) ..." >&2 + pip_install_or_fail "required for backend coverage gate" "coverage[toml]" fi require_cmd npm diff --git a/scripts/registry_publish_guard.py b/scripts/registry_publish_guard.py index d5365dc..798a880 100644 --- a/scripts/registry_publish_guard.py +++ b/scripts/registry_publish_guard.py @@ -89,7 +89,12 @@ def decide_should_publish( def _write_outputs( - *, output_path: Path | None, should_publish: bool, reason: str, current_version: str, previous_version: str | None + *, + output_path: Path | None, + should_publish: bool, + reason: str, + current_version: str, + previous_version: str | None, ) -> None: lines = [ f"should_publish={'true' if should_publish else 'false'}", diff --git a/scripts/run_backend_coverage.py b/scripts/run_backend_coverage.py index be74e6b..b67c57d 100644 --- a/scripts/run_backend_coverage.py +++ b/scripts/run_backend_coverage.py @@ -7,6 +7,7 @@ coverage-governance reporting. from __future__ import annotations import argparse +import importlib.util import subprocess import sys from pathlib import Path @@ -17,6 +18,14 @@ def _run_command(command: list[str]) -> int: return int(completed.returncode) +def _coverage_has_pyproject_toml_support() -> bool: + # IMPORTANT: keep this probe in sync with the CI/local bootstrap checks so + # Python 3.10 fails fast with a clear remediation instead of a coverage crash. + if sys.version_info >= (3, 11): + return True + return importlib.util.find_spec("tomli") is not None + + def _build_unittest_args(args: argparse.Namespace) -> list[str]: command = [ sys.executable, @@ -65,6 +74,15 @@ def run_backend_coverage(argv: list[str] | None = None) -> int: coverage_json = Path(args.coverage_json) coverage_json.parent.mkdir(parents=True, exist_ok=True) + if not _coverage_has_pyproject_toml_support(): + # CRITICAL: coverage reads repo config from pyproject.toml; Python 3.10 + # needs the TOML extra or CI/local coverage gates fail before tests run. + print( + "Coverage pyproject support is unavailable on this interpreter. " + "Install with `coverage[toml]` before running the backend coverage gate." + ) + return 2 + erase_cmd = [sys.executable, "-m", "coverage", "erase"] if (code := _run_command(erase_cmd)) != 0: return code diff --git a/scripts/run_full_tests_windows.ps1 b/scripts/run_full_tests_windows.ps1 index 0f41bef..8e7547b 100644 --- a/scripts/run_full_tests_windows.ps1 +++ b/scripts/run_full_tests_windows.ps1 @@ -183,14 +183,16 @@ if (-not $hasDefusedXml) { Invoke-Checked "pip install defusedxml" { & $venvPython -m pip install defusedxml } } -$hasCoverage = $true -& $venvPython -c "import coverage" | Out-Null +$hasCoverageTomlSupport = $true +# CRITICAL: keep this aligned with CI/pre-push bootstrap so Python 3.10 lanes +# install coverage[toml] before the shared backend coverage gate runs. +& $venvPython -c "import sys, importlib.util; has_coverage = importlib.util.find_spec('coverage') is not None; has_toml = sys.version_info >= (3, 11) or importlib.util.find_spec('tomli') is not None; sys.exit(0 if has_coverage and has_toml else 1)" | Out-Null if ($LASTEXITCODE -ne 0) { - $hasCoverage = $false + $hasCoverageTomlSupport = $false } -if (-not $hasCoverage) { - Write-Host "[tests] Installing coverage into project venv (R184 backend coverage gate) ..." - Invoke-Checked "pip install coverage" { & $venvPython -m pip install coverage } +if (-not $hasCoverageTomlSupport) { + Write-Host "[tests] Installing coverage[toml] into project venv (R184 backend coverage gate) ..." + Invoke-Checked "pip install coverage[toml]" { & $venvPython -m pip install "coverage[toml]" } } # Ensure Node >= 18 diff --git a/tests/test_bf12_registry_publish_version_guard.py b/tests/test_bf12_registry_publish_version_guard.py index ee8a1b3..e545b2c 100644 --- a/tests/test_bf12_registry_publish_version_guard.py +++ b/tests/test_bf12_registry_publish_version_guard.py @@ -4,18 +4,13 @@ import tempfile import unittest from pathlib import Path - ROOT = Path(__file__).resolve().parents[1] SCRIPT = ROOT / "scripts" / "registry_publish_guard.py" PUBLISH_WORKFLOW = ROOT / ".github" / "workflows" / "publish.yml" def _pyproject_text(version: str) -> str: - return ( - "[project]\n" - 'name = "comfyui-openclaw"\n' - f'version = "{version}"\n' - ) + return "[project]\n" 'name = "comfyui-openclaw"\n' f'version = "{version}"\n' class RegistryPublishVersionGuardTests(unittest.TestCase): @@ -23,7 +18,9 @@ class RegistryPublishVersionGuardTests(unittest.TestCase): workflow = PUBLISH_WORKFLOW.read_text(encoding="utf-8") self.assertIn("fetch-depth: 2", workflow) self.assertIn("scripts/registry_publish_guard.py", workflow) - self.assertIn("if: steps.publish_guard.outputs.should_publish == 'true'", workflow) + self.assertIn( + "if: steps.publish_guard.outputs.should_publish == 'true'", workflow + ) def test_same_version_sets_should_publish_false(self): with tempfile.TemporaryDirectory() as tmpdir: diff --git a/tests/test_bf13_coverage_toml_parity.py b/tests/test_bf13_coverage_toml_parity.py new file mode 100644 index 0000000..66a5003 --- /dev/null +++ b/tests/test_bf13_coverage_toml_parity.py @@ -0,0 +1,41 @@ +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] + + +class CoverageTomlParityContractTests(unittest.TestCase): + def test_ci_unit_test_lane_installs_coverage_with_toml_support(self): + ci_workflow = (ROOT / ".github" / "workflows" / "ci.yml").read_text( + encoding="utf-8" + ) + self.assertIn( + 'python -m pip install numpy pillow aiohttp "coverage[toml]"', ci_workflow + ) + self.assertNotIn( + "python -m pip install numpy pillow aiohttp coverage\n", ci_workflow + ) + + def test_local_bootstrap_scripts_install_coverage_with_toml_support(self): + pre_push = (ROOT / "scripts" / "pre_push_checks.sh").read_text(encoding="utf-8") + windows_full = (ROOT / "scripts" / "run_full_tests_windows.ps1").read_text( + encoding="utf-8" + ) + + self.assertIn( + 'pip_install_or_fail "required for backend coverage gate" "coverage[toml]"', + pre_push, + ) + self.assertIn('Invoke-Checked "pip install coverage[toml]"', windows_full) + self.assertNotIn( + 'pip_install_or_fail "required for backend coverage gate" coverage', + pre_push, + ) + self.assertNotIn( + 'Invoke-Checked "pip install coverage" { & $venvPython -m pip install coverage }', + windows_full, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_r184_backend_coverage_gate.py b/tests/test_r184_backend_coverage_gate.py index 64ac81e..a1c25aa 100644 --- a/tests/test_r184_backend_coverage_gate.py +++ b/tests/test_r184_backend_coverage_gate.py @@ -1,6 +1,7 @@ import subprocess import sys import unittest +from io import StringIO from pathlib import Path from unittest.mock import patch @@ -68,6 +69,29 @@ class TestR184BackendCoverageGate(unittest.TestCase): self.assertEqual(result, 1) self.assertEqual(mock_run.call_count, 2) + @patch("scripts.run_backend_coverage._coverage_has_pyproject_toml_support") + @patch("scripts.run_backend_coverage.subprocess.run") + def test_missing_toml_support_fails_closed_before_running_coverage( + self, mock_run, mock_support + ): + from scripts.run_backend_coverage import run_backend_coverage + + mock_support.return_value = False + stdout = StringIO() + with patch("sys.stdout", stdout): + result = run_backend_coverage( + [ + "--module", + "tests.test_r156_quality_governance", + "--coverage-json", + str(ROOT / ".tmp" / "coverage" / "missing_toml.json"), + ] + ) + + self.assertEqual(result, 2) + self.assertEqual(mock_run.call_count, 0) + self.assertIn("coverage[toml]", stdout.getvalue()) + if __name__ == "__main__": unittest.main()