File size: 6,482 Bytes
53c4c46
 
 
ce644a9
53c4c46
 
ce644a9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53c4c46
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ce644a9
 
 
 
53c4c46
 
 
 
 
 
 
 
 
 
 
ce644a9
 
53c4c46
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ce644a9
53c4c46
 
 
 
 
 
 
 
 
 
ce644a9
53c4c46
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
#!/usr/bin/env python3
"""Cross-platform pytest runner that syncs dependencies before running tests."""

import shutil
import subprocess
import sys
from pathlib import Path


def clean_caches(project_root: Path) -> None:
    """Remove pytest and Python cache directories and files.

    Only scans specific directories (src/, tests/) to avoid resource
    exhaustion from scanning large directories like .venv on Windows.
    """
    # Directories to scan for caches (only project code, not dependencies)
    scan_dirs = ["src", "tests", ".pre-commit-hooks"]

    # Directories to exclude (to avoid resource issues)
    exclude_dirs = {
        ".venv",
        "venv",
        "ENV",
        "env",
        ".git",
        "node_modules",
        "dist",
        "build",
        ".eggs",
        "reference_repos",
        "folder",
    }

    cache_patterns = [
        ".pytest_cache",
        "__pycache__",
        "*.pyc",
        "*.pyo",
        "*.pyd",
        ".mypy_cache",
        ".ruff_cache",
    ]

    def should_exclude(path: Path) -> bool:
        """Check if a path should be excluded from cache cleanup."""
        # Check if any parent directory is in exclude list
        for parent in path.parents:
            if parent.name in exclude_dirs:
                return True
        # Check if the path itself is excluded
        if path.name in exclude_dirs:
            return True
        return False

    cleaned = []

    # Only scan specific directories to avoid resource exhaustion
    for scan_dir in scan_dirs:
        scan_path = project_root / scan_dir
        if not scan_path.exists():
            continue

        for pattern in cache_patterns:
            if "*" in pattern:
                # Handle glob patterns for files
                try:
                    for cache_file in scan_path.rglob(pattern):
                        if should_exclude(cache_file):
                            continue
                        try:
                            if cache_file.is_file():
                                cache_file.unlink()
                                cleaned.append(str(cache_file.relative_to(project_root)))
                        except OSError:
                            pass  # Ignore errors (file might be locked or already deleted)
                except OSError:
                    pass  # Ignore errors during directory traversal
            else:
                # Handle directory patterns
                try:
                    for cache_dir in scan_path.rglob(pattern):
                        if should_exclude(cache_dir):
                            continue
                        try:
                            if cache_dir.is_dir():
                                shutil.rmtree(cache_dir, ignore_errors=True)
                                cleaned.append(str(cache_dir.relative_to(project_root)))
                        except OSError:
                            pass  # Ignore errors (directory might be locked)
                except OSError:
                    pass  # Ignore errors during directory traversal

    # Also clean root-level caches (like .pytest_cache in project root)
    for pattern in [".pytest_cache", ".mypy_cache", ".ruff_cache"]:
        cache_path = project_root / pattern
        if cache_path.exists() and cache_path.is_dir():
            try:
                shutil.rmtree(cache_path, ignore_errors=True)
                cleaned.append(pattern)
            except OSError:
                pass

    if cleaned:
        print(f"Cleaned {len(cleaned)} cache items")
    else:
        print("No cache files found to clean")


def run_command(
    cmd: list[str], check: bool = True, shell: bool = False, cwd: str | None = None
) -> int:
    """Run a command and return exit code."""
    try:
        result = subprocess.run(
            cmd,
            check=check,
            shell=shell,
            cwd=cwd,
            env=None,  # Use current environment, uv will handle venv
        )
        return result.returncode
    except subprocess.CalledProcessError as e:
        return e.returncode
    except FileNotFoundError:
        print(f"Error: Command not found: {cmd[0]}")
        return 1


def main() -> int:
    """Main entry point."""
    import os

    # Get the project root (where pyproject.toml is)
    script_dir = Path(__file__).parent
    project_root = script_dir.parent

    # Change to project root to ensure uv works correctly
    os.chdir(project_root)

    # Clean caches before running tests
    print("Cleaning pytest and Python caches...")
    clean_caches(project_root)

    # Check if uv is available
    if run_command(["uv", "--version"], check=False) != 0:
        print("Error: uv not found. Please install uv: https://github.com/astral-sh/uv")
        return 1

    # Parse arguments
    test_type = sys.argv[1] if len(sys.argv) > 1 else "unit"
    extra_args = sys.argv[2:] if len(sys.argv) > 2 else []

    # Sync dependencies - always include dev
    # Note: embeddings dependencies are now in main dependencies, not optional
    # Use --extra dev for [project.optional-dependencies].dev (not --dev which is for [dependency-groups])
    sync_cmd = ["uv", "sync", "--extra", "dev"]

    print(f"Syncing dependencies for {test_type} tests...")
    if run_command(sync_cmd, cwd=project_root) != 0:
        return 1

    # Build pytest command - use uv run to ensure correct environment
    if test_type == "unit":
        pytest_args = [
            "tests/unit/",
            "-v",
            "-m",
            "not openai and not embedding_provider",
            "--tb=short",
            "-p",
            "no:logfire",
            "--cache-clear",  # Clear pytest cache before running
        ]
    elif test_type == "embeddings":
        pytest_args = [
            "tests/",
            "-v",
            "-m",
            "local_embeddings",
            "--tb=short",
            "-p",
            "no:logfire",
            "--cache-clear",  # Clear pytest cache before running
        ]
    else:
        pytest_args = []

    pytest_args.extend(extra_args)

    # Use uv run python -m pytest to ensure we use the venv's pytest
    # This is more reliable than uv run pytest which might find system pytest
    pytest_cmd = ["uv", "run", "python", "-m", "pytest", *pytest_args]

    print(f"Running {test_type} tests...")
    return run_command(pytest_cmd, cwd=project_root)


if __name__ == "__main__":
    sys.exit(main())