Skip to content

Commit 82de046

Browse files
authored
Refactor and simplify the testrunner (#495)
* Refactor and simplify the testrunner * Changelog
1 parent fbbc1c1 commit 82de046

File tree

6 files changed

+63
-79
lines changed

6 files changed

+63
-79
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2121
* Changed the Basilisp module type from `types.ModuleType` to a custom subtype with support for custom attributes (#482)
2222
* Basilisp's runtime function `Namespace.get_or_create` no longer refers `basilisp.core` by default, which allows callers to exclude `basilisp.core` names in the `ns` macro (#481)
2323
* Namespaces now use a single internal lock rather than putting each property inside of an Atom (#494)
24+
* Refactor the testrunner to use fewer `atom`s in `basilisp.test` (#495)
2425

2526
### Fixed
2627
* Fixed a reader bug where no exception was being thrown splicing reader conditional forms appeared outside of valid splicing contexts (#470)

src/basilisp/importer.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
import basilisp.lang.reader as reader
1515
import basilisp.lang.runtime as runtime
1616
import basilisp.lang.symbol as sym
17-
from basilisp.lang.typing import BasilispModule, ReaderForm
17+
from basilisp.lang.runtime import BasilispModule
18+
from basilisp.lang.typing import ReaderForm
1819
from basilisp.lang.util import demunge
1920
from basilisp.util import timed
2021

@@ -275,10 +276,7 @@ def _exec_module(
275276
# During compilation, bytecode objects are added to the list via the closure
276277
# add_bytecode below, which is passed to the compiler. The collected bytecodes
277278
# will be used to generate an .lpyc file for caching the compiled file.
278-
all_bytecode = []
279-
280-
def add_bytecode(bytecode: types.CodeType):
281-
all_bytecode.append(bytecode)
279+
all_bytecode: List[types.CodeType] = []
282280

283281
logger.debug(f"Reading and compiling Basilisp module '{fullname}'")
284282
# Cast to basic ReaderForm since the reader can never return a reader conditional
@@ -292,7 +290,7 @@ def add_bytecode(bytecode: types.CodeType):
292290
forms,
293291
compiler.CompilerContext(filename=filename),
294292
ns,
295-
collect_bytecode=add_bytecode,
293+
collect_bytecode=all_bytecode.append,
296294
)
297295

298296
# Cache the bytecode that was collected through the compilation run.
@@ -324,6 +322,7 @@ def exec_module(self, module):
324322
ns_name = demunge(fullname)
325323
ns: runtime.Namespace = runtime.Namespace.get_or_create(sym.symbol(ns_name))
326324
ns.module = module
325+
module.__basilisp_namespace__ = ns
327326

328327
# Check if a valid, cached version of this Basilisp namespace exists and, if so,
329328
# load it and bypass the expensive compilation process below.
@@ -351,7 +350,7 @@ def hook_imports():
351350
352351
Once this is called, Basilisp code may be called from within Python code
353352
using standard `import module.submodule` syntax."""
354-
if any([isinstance(o, BasilispImporter) for o in sys.meta_path]):
353+
if any(isinstance(o, BasilispImporter) for o in sys.meta_path):
355354
return
356355
sys.meta_path.insert(
357356
0, BasilispImporter() # pylint:disable=abstract-class-instantiated

src/basilisp/lang/runtime.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
ISeqable,
4848
)
4949
from basilisp.lang.reference import ReferenceBase
50-
from basilisp.lang.typing import BasilispModule, LispNumber
50+
from basilisp.lang.typing import LispNumber
5151
from basilisp.lang.util import munge
5252
from basilisp.logconfig import TRACE
5353
from basilisp.util import Maybe
@@ -122,6 +122,11 @@
122122
CompletionTrimmer = Callable[[Tuple[sym.Symbol, Any]], str]
123123

124124

125+
class BasilispModule(types.ModuleType):
126+
__basilisp_namespace__: "Namespace"
127+
__basilisp_bootstrapped__: bool = False
128+
129+
125130
def _new_module(name: str, doc=None) -> BasilispModule:
126131
"""Create a new empty Basilisp Python module.
127132
Modules are created for each Namespace when it is created."""

src/basilisp/lang/typing.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
from datetime import datetime
33
from decimal import Decimal
44
from fractions import Fraction
5-
from types import ModuleType
65
from typing import Pattern, Union
76

87
import basilisp.lang.keyword as kw
@@ -37,7 +36,3 @@
3736
PyCollectionForm = Union[dict, list, set, tuple]
3837
ReaderForm = Union[LispForm, IRecord, ISeq, IType, PyCollectionForm]
3938
SpecialForm = Union[llist.List, ISeq]
40-
41-
42-
class BasilispModule(ModuleType):
43-
__basilisp_bootstrapped__: bool = False

src/basilisp/test.lpy

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,8 @@
44
(:require
55
[basilisp.core.template :as template]))
66

7-
(def ^:private collected-tests
8-
(atom []))
9-
10-
(def ^:private current-ns
11-
(atom nil))
12-
13-
(defn ^:private add-test!
14-
"Add the test named by test-var to the test suite for ns."
15-
[test-var]
16-
(swap! collected-tests conj test-var))
7+
(def ^:private current-test-number
8+
(atom 0))
179

1810
(def ^:dynamic *test-name* nil)
1911
(def ^:dynamic *test-section* nil)
@@ -134,18 +126,18 @@
134126
Tests defined by deftest will be run by default by the PyTest test
135127
runner using Basilisp's builtin PyTest hook."
136128
[name-sym & body]
137-
(let [test-name-sym (vary-meta name-sym assoc :test true)
129+
(let [test-num (swap! current-test-number inc)
130+
test-name-sym (vary-meta name-sym
131+
assoc
132+
:basilisp.test/test true
133+
:basilisp.test/order test-num)
138134
test-name-str (name test-name-sym)
139135
test-ns-name `(quote ~(symbol (name *ns*)))]
140-
`(do
141-
(defn ~test-name-sym
142-
[]
143-
(binding [*ns* (the-ns ~test-ns-name)
144-
*test-name* ~test-name-str
145-
*test-section* nil
146-
*test-failures* (atom [])]
147-
~@body
148-
{:failures (deref *test-failures*)}))
149-
150-
(add-test! (var ~test-name-sym))
151-
(reset! current-ns (the-ns ~test-ns-name)))))
136+
`(defn ~test-name-sym
137+
[]
138+
(binding [*ns* (the-ns ~test-ns-name)
139+
*test-name* ~test-name-str
140+
*test-section* nil
141+
*test-failures* (atom [])]
142+
~@body
143+
{:failures (deref *test-failures*)}))))

src/basilisp/testrunner.py

Lines changed: 35 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import importlib
22
import traceback
3-
from typing import Callable, Optional
3+
from typing import Callable, Iterable, Optional
44

55
import pytest
66

@@ -13,8 +13,9 @@
1313
from basilisp.lang.obj import lrepr
1414
from basilisp.util import Maybe
1515

16-
_COLLECTED_TESTS_SYM = sym.symbol("collected-tests", ns="basilisp.test")
1716
_CURRENT_NS_SYM = sym.symbol("current-ns", ns="basilisp.test")
17+
_TEST_META_KW = kw.keyword("test", "basilisp.test")
18+
_TEST_NUM_META_KW = kw.keyword("order", "basilisp.test")
1819

1920

2021
# pylint: disable=unused-argument
@@ -31,37 +32,6 @@ def pytest_collect_file(parent, path):
3132
return None
3233

3334

34-
def _collected_tests() -> Optional[vec.Vector]:
35-
"""Fetch the collected tests for the namespace ns from
36-
basilisp.test/collected-tests atom. If no tests are found, return
37-
None."""
38-
var = Maybe(runtime.Var.find(_COLLECTED_TESTS_SYM)).or_else_raise(
39-
lambda: runtime.RuntimeException(
40-
f"Unable to find test Var {_COLLECTED_TESTS_SYM}."
41-
)
42-
)
43-
return var.value.deref()
44-
45-
46-
def _current_ns() -> str:
47-
"""Fetch the current namespace from basilisp.test/current-ns."""
48-
var = Maybe(runtime.Var.find(_CURRENT_NS_SYM)).or_else_raise(
49-
lambda: runtime.RuntimeException(f"Unable to find test Var {_CURRENT_NS_SYM}.")
50-
)
51-
ns = var.value.deref()
52-
return ns.name
53-
54-
55-
def _reset_collected_tests() -> None:
56-
"""Reset the collected tests."""
57-
var = Maybe(runtime.Var.find(_COLLECTED_TESTS_SYM)).or_else_raise(
58-
lambda: runtime.RuntimeException(
59-
f"Unable to find test Var {_COLLECTED_TESTS_SYM}."
60-
)
61-
)
62-
return var.value.reset(vec.Vector.empty())
63-
64-
6535
class TestFailuresInfo(Exception):
6636
__slots__ = ("_msg", "_data")
6737

@@ -91,21 +61,43 @@ def message(self) -> str:
9161
class BasilispFile(pytest.File):
9262
"""Files represent a test module in Python or a test namespace in Basilisp."""
9363

64+
@staticmethod
65+
def _collected_tests(ns: runtime.Namespace) -> Iterable[runtime.Var]:
66+
"""Return the set of collected tests from the Namespace `ns`.
67+
68+
Tests defined by `deftest` are annotated with `:basilisp.test/test` metadata
69+
and `:basilisp.test/order` is a monotonically increasing integer added by
70+
`deftest` at compile-time to run tests in the order they are defined (which
71+
matches the default behavior of PyTest)."""
72+
73+
def _test_num(var: runtime.Var) -> int:
74+
assert var.meta is not None
75+
order = var.meta.val_at(_TEST_NUM_META_KW)
76+
assert isinstance(order, int)
77+
return order
78+
79+
return sorted(
80+
(
81+
var
82+
for _, var in ns.interns.items()
83+
if var.meta is not None and var.meta.val_at(_TEST_META_KW)
84+
),
85+
key=_test_num,
86+
)
87+
9488
def collect(self):
9589
"""Collect all of the tests in the namespace (module) given.
9690
9791
Basilisp's test runner imports the namespace which will (as a side
9892
effect) collect all of the test functions in a namespace (represented
99-
by `deftest` forms in Basilisp) into an atom in `basilisp.test`.
100-
BasilispFile.collect fetches those test functions and generates
101-
BasilispTestItems for PyTest to run the tests."""
102-
_reset_collected_tests()
93+
by `deftest` forms in Basilisp). BasilispFile.collect fetches those
94+
test functions and generates BasilispTestItems for PyTest to run the
95+
tests."""
10396
filename = self.fspath.basename
104-
self.fspath.pyimport()
105-
ns = _current_ns()
106-
tests = _collected_tests()
107-
assert tests is not None, "Must have collected tests"
108-
for test in tests:
97+
module = self.fspath.pyimport()
98+
assert isinstance(module, runtime.BasilispModule)
99+
ns = module.__basilisp_namespace__
100+
for test in self._collected_tests(ns):
109101
f: TestFunction = test.value
110102
yield BasilispTestItem(test.name.name, self, f, ns, filename)
111103

@@ -137,7 +129,7 @@ def __init__( # pylint: disable=too-many-arguments
137129
name: str,
138130
parent: BasilispFile,
139131
run_test: TestFunction,
140-
namespace: str,
132+
namespace: runtime.Namespace,
141133
filename: str,
142134
) -> None:
143135
super(BasilispTestItem, self).__init__(name, parent)

0 commit comments

Comments
 (0)