From ad19131eae4f3207a1e117cbc1291a481a0309d4 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 27 May 2026 01:12:02 +0000 Subject: [PATCH 1/2] Add CI workflow to run discovery tests Runs tests/discovery/test_discover.py on push to main and on pull requests, across Python 3.10/3.13 and Ubuntu/Windows (the discovery logic has cross-platform paths). Stdlib-only, so no dependency install step. https://claude.ai/code/session_0121EsSJYijy1gbqZSJ1xFxv --- .github/workflows/tests.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..da0f757 --- /dev/null +++ b/.github/workflows/tests.yml @@ -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 From 9603d9281479568160d6cd6092b05de4918b1c45 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 27 May 2026 01:17:38 +0000 Subject: [PATCH 2/2] Fix discovery tests on Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two assertions compared str(Path(...)) against forward-slash literals (["/v/bin/python"]), which fail on Windows where Path renders with backslashes — the cause of the Windows CI failures. Compute the expected value the same way the code does, and exercise filesystem behavior against real temp directories instead of patching pathlib.Path.is_file, so the path-detection tests are correct and identical across POSIX and Windows. https://claude.ai/code/session_0121EsSJYijy1gbqZSJ1xFxv --- tests/discovery/test_discover.py | 107 +++++++++++++++++-------------- 1 file changed, 60 insertions(+), 47 deletions(-) diff --git a/tests/discovery/test_discover.py b/tests/discovery/test_discover.py index 29362d9..acc6e45 100644 --- a/tests/discovery/test_discover.py +++ b/tests/discovery/test_discover.py @@ -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 @@ -10,6 +14,7 @@ import os import subprocess import sys +import tempfile import unittest from pathlib import Path from unittest import mock @@ -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): @@ -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): @@ -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)