Skip to content
Merged
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
23 changes: 23 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: tests

on:
push:
branches: [main]
pull_request:

jobs:
discovery:
name: discovery (py${{ matrix.python-version }}, ${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
python-version: ["3.10", "3.13"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Run discovery tests
run: python -m unittest tests.discovery.test_discover -v
107 changes: 60 additions & 47 deletions tests/discovery/test_discover.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

Stdlib-only (unittest) so it runs anywhere with `python -m unittest discover`
or under pytest, without adding a dependency to this plugin repo.

Filesystem-dependent behavior is exercised against real temp directories rather
than by patching ``pathlib.Path`` methods — that keeps the tests correct and
identical across POSIX and Windows.
"""

from __future__ import annotations
Expand All @@ -10,6 +14,7 @@
import os
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
from unittest import mock
Expand All @@ -29,24 +34,28 @@ def _load_module():
discover = _load_module()


def _touch(path: Path) -> Path:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text("", encoding="utf-8")
return path


class FindVenvPythonTests(unittest.TestCase):
def test_posix_layout(self):
with mock.patch.object(Path, "is_file", autospec=True) as is_file:
is_file.side_effect = lambda p: p.name == "python"
root = Path("/x/.venv")
self.assertEqual(discover.find_venv_python(root), root / "bin" / "python")
with tempfile.TemporaryDirectory() as tmp:
venv = Path(tmp) / ".venv"
expected = _touch(venv / "bin" / "python")
self.assertEqual(discover.find_venv_python(venv), expected)

def test_windows_layout(self):
with mock.patch.object(Path, "is_file", autospec=True) as is_file:
is_file.side_effect = lambda p: p.name == "python.exe"
root = Path("/x/.venv")
self.assertEqual(
discover.find_venv_python(root), root / "Scripts" / "python.exe"
)
with tempfile.TemporaryDirectory() as tmp:
venv = Path(tmp) / ".venv"
expected = _touch(venv / "Scripts" / "python.exe")
self.assertEqual(discover.find_venv_python(venv), expected)

def test_missing(self):
with mock.patch.object(Path, "is_file", autospec=True, return_value=False):
self.assertIsNone(discover.find_venv_python(Path("/x/.venv")))
with tempfile.TemporaryDirectory() as tmp:
self.assertIsNone(discover.find_venv_python(Path(tmp) / ".venv"))


class DetectInterpreterTests(unittest.TestCase):
Expand All @@ -55,50 +64,52 @@ def test_virtual_env_wins(self):
mock.patch.object(discover, "find_venv_python", return_value=Path("/v/bin/python")):
cmd, tag = discover.detect_interpreter(Path("/proj"))
self.assertEqual(tag, "virtual-env")
self.assertEqual(cmd, ["/v/bin/python"])
self.assertEqual(cmd, [str(Path("/v/bin/python"))])

def test_local_venv_before_parent(self):
seen: list[Path] = []
project = Path("/proj")

def fake_find(root: Path):
seen.append(root)
return Path("/proj/.venv/bin/python") if root == Path("/proj/.venv") else None
return Path("/proj/.venv/bin/python") if root == project / ".venv" else None

with mock.patch.dict(os.environ, {}, clear=True), \
mock.patch.object(discover, "find_venv_python", side_effect=fake_find):
cmd, tag = discover.detect_interpreter(Path("/proj"))
cmd, tag = discover.detect_interpreter(project)
self.assertEqual(tag, "venv-local")
self.assertIn(Path("/proj/.venv"), seen)
self.assertEqual(cmd, [str(Path("/proj/.venv/bin/python"))])

def test_uv_lockfile(self):
with mock.patch.dict(os.environ, {}, clear=True), \
mock.patch.object(discover, "find_venv_python", return_value=None), \
mock.patch.object(discover, "find_git_root", return_value=None), \
mock.patch.object(discover.shutil, "which", side_effect=lambda n: n == "uv"), \
mock.patch.object(Path, "is_file", autospec=True,
side_effect=lambda p: p.name == "uv.lock"):
cmd, tag = discover.detect_interpreter(Path("/proj"))
self.assertEqual(tag, "uv")
self.assertEqual(cmd, ["uv", "run", "--quiet", "python"])
with tempfile.TemporaryDirectory() as tmp:
project = Path(tmp)
_touch(project / "uv.lock")
with mock.patch.dict(os.environ, {}, clear=True), \
mock.patch.object(discover, "find_venv_python", return_value=None), \
mock.patch.object(discover, "find_git_root", return_value=None), \
mock.patch.object(discover.shutil, "which", side_effect=lambda n: n == "uv"):
cmd, tag = discover.detect_interpreter(project)
self.assertEqual(tag, "uv")
self.assertEqual(cmd, ["uv", "run", "--quiet", "python"])

def test_system_fallback(self):
with mock.patch.dict(os.environ, {}, clear=True), \
mock.patch.object(discover, "find_venv_python", return_value=None), \
mock.patch.object(discover, "find_git_root", return_value=None), \
mock.patch.object(discover.shutil, "which",
side_effect=lambda n: n == "python3"), \
mock.patch.object(Path, "is_file", autospec=True, return_value=False):
cmd, tag = discover.detect_interpreter(Path("/proj"))
self.assertEqual(tag, "system")
self.assertEqual(cmd, ["python3"])
with tempfile.TemporaryDirectory() as tmp:
project = Path(tmp) # no lockfiles present
with mock.patch.dict(os.environ, {}, clear=True), \
mock.patch.object(discover, "find_venv_python", return_value=None), \
mock.patch.object(discover, "find_git_root", return_value=None), \
mock.patch.object(discover.shutil, "which",
side_effect=lambda n: n == "python3"):
cmd, tag = discover.detect_interpreter(project)
self.assertEqual(tag, "system")
self.assertEqual(cmd, ["python3"])

def test_none_when_nothing_found(self):
with mock.patch.dict(os.environ, {}, clear=True), \
mock.patch.object(discover, "find_venv_python", return_value=None), \
mock.patch.object(discover, "find_git_root", return_value=None), \
mock.patch.object(discover.shutil, "which", return_value=None), \
mock.patch.object(Path, "is_file", autospec=True, return_value=False):
self.assertIsNone(discover.detect_interpreter(Path("/proj")))
with tempfile.TemporaryDirectory() as tmp:
project = Path(tmp)
with mock.patch.dict(os.environ, {}, clear=True), \
mock.patch.object(discover, "find_venv_python", return_value=None), \
mock.patch.object(discover, "find_git_root", return_value=None), \
mock.patch.object(discover.shutil, "which", return_value=None):
self.assertIsNone(discover.detect_interpreter(project))


class InstallAdviceTests(unittest.TestCase):
Expand All @@ -125,11 +136,13 @@ def test_system_mentions_venv(self):

class CliTests(unittest.TestCase):
def test_bad_project_dir_exits_5(self):
result = subprocess.run(
[sys.executable, str(DISCOVER), "--project-dir", "/no/such/dir/xyz"],
capture_output=True,
text=True,
)
with tempfile.TemporaryDirectory() as tmp:
missing = Path(tmp) / "does-not-exist"
result = subprocess.run(
[sys.executable, str(DISCOVER), "--project-dir", str(missing)],
capture_output=True,
text=True,
)
self.assertEqual(result.returncode, 5)
self.assertIn("not a directory", result.stderr)

Expand Down
Loading