From d10ddf3cb16f0e36b713b9882ffdec1830d54473 Mon Sep 17 00:00:00 2001 From: Jon Frisby Date: Mon, 28 Sep 2026 12:14:04 -0700 Subject: [PATCH 1/2] Skip tool-index tests when ctags, cscope, or tree-sitter is missing. tree-sitter comes from the optional `analysis` extra and the binaries are system installs, so these tests failed on any machine without them. CI installs all three, so coverage there is unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_011FeZUmAZbyQWDJNAPkUA3S --- tests/test_tool_index.py | 42 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/test_tool_index.py b/tests/test_tool_index.py index f83635c..5112be1 100644 --- a/tests/test_tool_index.py +++ b/tests/test_tool_index.py @@ -3,6 +3,8 @@ from __future__ import annotations import json +import shutil +import subprocess import textwrap from pathlib import Path from unittest.mock import MagicMock, patch @@ -29,20 +31,52 @@ ) +def _has_universal_ctags() -> bool: + # macOS ships BSD ctags at /usr/bin/ctags, which pact deliberately ignores. + try: + out = subprocess.run(["ctags", "--version"], capture_output=True, text=True).stdout + except OSError: + return False + return "Universal Ctags" in out + + +def _has_tree_sitter_python() -> bool: + try: + import tree_sitter # noqa: F401 + import tree_sitter_python # noqa: F401 + except ImportError: + return False + return True + + +requires_ctags = pytest.mark.skipif( + not _has_universal_ctags(), reason="universal-ctags not installed", +) +requires_cscope = pytest.mark.skipif( + shutil.which("cscope") is None, reason="cscope not installed", +) +requires_tree_sitter = pytest.mark.skipif( + not _has_tree_sitter_python(), reason="tree-sitter not installed (analysis extra)", +) + + # ── Tool Detection ──────────────────────────────────────────────── class TestDetectTools: + @requires_ctags def test_detects_ctags(self): avail = detect_tools() # ctags should be installed (universal-ctags via brew) assert avail.ctags is True assert avail.ctags_version != "" + @requires_cscope def test_detects_cscope(self): avail = detect_tools() assert avail.cscope is True + @requires_tree_sitter def test_detects_tree_sitter(self): avail = detect_tools() assert avail.tree_sitter is True @@ -72,6 +106,7 @@ def test_run_quiet_missing_command(self): # ── ctags ───────────────────────────────────────────────────────── +@requires_ctags class TestCtags: def test_run_ctags_on_sample(self, tmp_path): """ctags should find functions in a simple Python file.""" @@ -152,6 +187,7 @@ def test_cscope_empty_dir(self, tmp_path): class TestTreeSitter: + @requires_tree_sitter def test_run_tree_sitter_on_sample(self, tmp_path): """tree-sitter should extract function and class definitions.""" (tmp_path / "sample.py").write_text(textwrap.dedent("""\ @@ -169,6 +205,7 @@ def greet(self, name: str) -> str: assert "Greeter" in names assert "greet" in names + @requires_tree_sitter def test_tree_sitter_symbol_metadata(self, tmp_path): (tmp_path / "funcs.py").write_text(textwrap.dedent("""\ class Calculator: @@ -184,6 +221,7 @@ def add(self, a, b): assert add_sym.parent == "Calculator" assert add_sym.parent_kind == "class" + @requires_tree_sitter def test_tree_sitter_class_definition(self, tmp_path): (tmp_path / "models.py").write_text("class User:\n pass\n") symbols = run_tree_sitter(tmp_path, "python") @@ -208,6 +246,7 @@ def test_tree_sitter_unsupported_language(self, tmp_path): symbols = run_tree_sitter(tmp_path, "definitely_not_supported") assert symbols == [] + @requires_tree_sitter def test_tree_sitter_multiline_function(self, tmp_path): (tmp_path / "big.py").write_text(textwrap.dedent("""\ def complex_function( @@ -410,6 +449,8 @@ def test_cscope_handles_no_binary(self): class TestIntegration: + @requires_ctags + @requires_tree_sitter def test_build_tool_index_real(self, tmp_path): """Integration test: build_tool_index on a real (small) codebase.""" (tmp_path / "app.py").write_text(textwrap.dedent("""\ @@ -430,6 +471,7 @@ def run(self): assert idx.total_symbols > 0 assert idx.total_tree_sitter_symbols > 0 + @requires_ctags def test_analyze_codebase_includes_tool_index(self, tmp_path): """analyze_codebase() should attach tool_index to the result.""" (tmp_path / "main.py").write_text("def hello(): pass\n") From 3f67388e40fead1c8490722ab8d7c4b88f40644f Mon Sep 17 00:00:00 2001 From: Jon Frisby Date: Mon, 28 Sep 2026 12:14:04 -0700 Subject: [PATCH 2/2] Stop tests from assuming the venv is activated. `make test` runs `.venv/bin/python -m pytest`, which leaves the venv's bin directory off `PATH`. The adopt smoke test then ran a different `python3`, and the environment test could not find the `pytest` script. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_011FeZUmAZbyQWDJNAPkUA3S --- tests/test_adopt.py | 3 ++- tests/test_environment.py | 8 ++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/test_adopt.py b/tests/test_adopt.py index 739f21b..4cc7860 100644 --- a/tests/test_adopt.py +++ b/tests/test_adopt.py @@ -354,8 +354,9 @@ def hello(name): # Run the generated tests with the source on PYTHONPATH import subprocess + import sys proc = subprocess.run( - ["python3", "-m", "pytest", str(test_file), "-v"], + [sys.executable, "-m", "pytest", str(test_file), "-v"], capture_output=True, text=True, cwd=str(tmp_path), env={**__import__("os").environ, "PYTHONPATH": str(tmp_path)}, diff --git a/tests/test_environment.py b/tests/test_environment.py index 49087f2..388febd 100644 --- a/tests/test_environment.py +++ b/tests/test_environment.py @@ -1,5 +1,6 @@ """Tests for EnvironmentSpec.""" import os +import sysconfig from pact.config import EnvironmentSpec, resolve_environment, GlobalConfig, ProjectConfig @@ -48,10 +49,13 @@ def test_build_env_fallback_minimal_path(self): env = spec.build_env("src") assert "/usr/bin" in env["PATH"] - def test_validate_finds_pytest(self): + def test_validate_finds_pytest(self, monkeypatch): + # The pytest script sits beside this interpreter, which is only on PATH + # when the venv is activated (not under `.venv/bin/python -m pytest`). + scripts = sysconfig.get_path("scripts") + monkeypatch.setenv("PATH", scripts + os.pathsep + os.environ.get("PATH", "")) spec = EnvironmentSpec(required_tools=["pytest"]) missing = spec.validate_environment() - # pytest should be installed in our test environment assert "pytest" not in missing def test_validate_finds_missing_tool(self):