From cd62cea8577179da49ab288309a30f1bdbc5d016 Mon Sep 17 00:00:00 2001 From: ikappaki Date: Sun, 26 Jan 2025 22:03:36 +0000 Subject: [PATCH 1/2] fix testrunner to handle relative entries in sys.path --- CHANGELOG.md | 3 ++ src/basilisp/cli.py | 6 ++-- src/basilisp/contrib/pytest/testrunner.py | 12 +++++-- tests/basilisp/testrunner_test.py | 44 +++++++++++++++++++++-- 4 files changed, 58 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0b96428..d8b3d86c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + * Fixed a regression introduced in #1176 where the testrunner couldn't handle relative paths in `sys.path`, causing `basilisp test` to fail when no arugments were provided (#1204) + ## [v0.3.6] ### Added * Added support for the `:decorators` meta key in anonymous `fn`s (#1178) diff --git a/src/basilisp/cli.py b/src/basilisp/cli.py index 889454f6..7015567e 100644 --- a/src/basilisp/cli.py +++ b/src/basilisp/cli.py @@ -745,7 +745,7 @@ def test( "Cannot run tests without dependency PyTest. Please install PyTest and try again.", ) else: - pytest.main(args=list(extra)) + sys.exit(pytest.main(args=list(extra))) @_subcommand( @@ -768,7 +768,9 @@ def test( If all options are unambiguous (e.g. they are only either used by Basilisp or by PyTest), then you can omit the `--`: - `basilisp test -k vector -p other_dir`""" + `basilisp test -k vector -p other_dir` + + Returns the PyTest exit code as the exit code.""" ), handler=test, allows_extra=True, diff --git a/src/basilisp/contrib/pytest/testrunner.py b/src/basilisp/contrib/pytest/testrunner.py index 4f0cb23d..e573f9a3 100644 --- a/src/basilisp/contrib/pytest/testrunner.py +++ b/src/basilisp/contrib/pytest/testrunner.py @@ -17,6 +17,7 @@ from basilisp.lang import symbol as sym from basilisp.lang import vector as vec from basilisp.lang.obj import lrepr +from basilisp.lang.util import munge from basilisp.util import Maybe _EACH_FIXTURES_META_KW = kw.keyword("each-fixtures", "basilisp.test") @@ -183,9 +184,10 @@ def _get_fully_qualified_module_names(file: Path) -> list[str]: there, we derive a Python module name referring to the given module path.""" paths = [] for pth in sys.path: - root = Path(pth) + root = Path(pth).resolve() if file.is_relative_to(root): - elems = list(file.with_suffix("").relative_to(pth).parts) + elems = list(file.with_suffix("").relative_to(root).parts) + if elems[-1] == "__init__": elems.pop() paths.append(".".join(elems)) @@ -269,6 +271,12 @@ def collect(self): filename = self.path.name module = self._import_module() ns = module.__basilisp_namespace__ + + # Ensure the test module was loaded because it was directly + # relative to an entry in `sys.path`. + if module.__name__ != munge(str(ns)): + raise ModuleNotFoundError(f"Module named '{ns}' is not in sys.path") + once_fixtures, each_fixtures = self._collected_fixtures(ns) self._fixture_manager = FixtureManager(once_fixtures) for test in self._collected_tests(ns): diff --git a/tests/basilisp/testrunner_test.py b/tests/basilisp/testrunner_test.py index 8791543d..5c29e689 100644 --- a/tests/basilisp/testrunner_test.py +++ b/tests/basilisp/testrunner_test.py @@ -1,4 +1,6 @@ import platform +import shutil +import subprocess import sys import pytest @@ -263,6 +265,35 @@ def test_fixtures_with_errors( result.assert_outcomes(passed=passes, failed=failures, errors=errors) +def test_basilisp_test_noargs(pytester: pytest.Pytester): + runtime.Namespace.remove(sym.symbol("a.test-path")) + + code = """ + (ns tests.test-path + (:require + [basilisp.test :refer [deftest is]])) + (deftest passing-test + (is true)) + """ + pytester.makefile(".lpy", **{"./tests/test_path": code}) + + # I couldn't find a way to directly manipulate the pytester's + # `sys.path` with the precise control needed by this test, so we're + # invoking `basilisp test` directly as a subprocess instead ... + basilisp = shutil.which("basilisp") + cmd = [basilisp, "test"] + result = subprocess.run(cmd, capture_output=True, text=True, cwd=pytester.path) + + print(f"\n\n--cmd--start: {' '.join(cmd)}") + print(f"\n\n--stdout--\n\n {result.stdout.strip()}") + print(f"\n\n--stderr--:\n\n {result.stderr.strip()}") + print(f"\n\n--cmd--end--: {' '.join(cmd)}\n\n") + + assert "==== 1 passed" in result.stdout.strip() + + assert result.returncode == 0 + + def test_ns_in_syspath(pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch): runtime.Namespace.remove(sym.symbol("a.test-path")) @@ -324,11 +355,18 @@ def test_ns_not_in_syspath(pytester: pytest.Pytester): (:require [basilisp.test :refer [deftest is]])) """ - pytester.makefile(".lpy", **{"./test/a/test_path": code}) + # In this test, we use a `testabc` directory instead of `test`, as + # the latter can cause issues on macOS. Specifically, macOS has a + # `/Library/Frameworks/Python.framework/Versions/3.xx/lib/python3.13/test` + # directory is picked up, resulting in a slightly different error + # message. + pytester.makefile(".lpy", **{"./testabc/a/test_path": code}) pytester.syspathinsert() - result: pytest.RunResult = pytester.runpytest("test") + result: pytest.RunResult = pytester.runpytest("testabc") assert result.ret != 0 - result.stdout.fnmatch_lines(["*ModuleNotFoundError: No module named 'test.a'"]) + result.stdout.fnmatch_lines( + ["*ModuleNotFoundError: Module named 'a.test-path' is not in sys.path"] + ) def test_ns_with_underscore(pytester: pytest.Pytester): From 96813dcc312e25241347464af9aed79ab3558912 Mon Sep 17 00:00:00 2001 From: ikappaki Date: Sun, 2 Feb 2025 16:34:24 +0000 Subject: [PATCH 2/2] address review comment (remove test debugging entries) --- tests/basilisp/testrunner_test.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/basilisp/testrunner_test.py b/tests/basilisp/testrunner_test.py index 5c29e689..3b259b56 100644 --- a/tests/basilisp/testrunner_test.py +++ b/tests/basilisp/testrunner_test.py @@ -284,11 +284,6 @@ def test_basilisp_test_noargs(pytester: pytest.Pytester): cmd = [basilisp, "test"] result = subprocess.run(cmd, capture_output=True, text=True, cwd=pytester.path) - print(f"\n\n--cmd--start: {' '.join(cmd)}") - print(f"\n\n--stdout--\n\n {result.stdout.strip()}") - print(f"\n\n--stderr--:\n\n {result.stderr.strip()}") - print(f"\n\n--cmd--end--: {' '.join(cmd)}\n\n") - assert "==== 1 passed" in result.stdout.strip() assert result.returncode == 0