Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion tests/test_adopt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)},
Expand Down
8 changes: 6 additions & 2 deletions tests/test_environment.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Tests for EnvironmentSpec."""
import os
import sysconfig
from pact.config import EnvironmentSpec, resolve_environment, GlobalConfig, ProjectConfig


Expand Down Expand Up @@ -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):
Expand Down
42 changes: 42 additions & 0 deletions tests/test_tool_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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("""\
Expand All @@ -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:
Expand All @@ -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")
Expand All @@ -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(
Expand Down Expand Up @@ -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("""\
Expand All @@ -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")
Expand Down
Loading