Skip to content

Commit 3e483c6

Browse files
authored
Capture line numbers in Basilisp tests (#270)
1 parent 6c4a711 commit 3e483c6

File tree

3 files changed

+131
-34
lines changed

3 files changed

+131
-34
lines changed

src/basilisp/test.lpy

Lines changed: 91 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
(ns basilisp.test)
1+
(ns basilisp.test
2+
(:import
3+
inspect))
24

35
(def ^:private collected-tests
46
(atom []))
@@ -15,38 +17,98 @@
1517
(def ^:dynamic *test-section* nil)
1618
(def ^:dynamic *test-failures* nil)
1719

20+
(defn line-no
21+
"Get the line number from the current interpreter stack.
22+
23+
This is a horrible hack and it requires each particular assertion case
24+
to define their frame offset from here, but it seems to be the most
25+
accessible way of determining the line number without capturing it
26+
for every test (including non-failing tests)."
27+
[n]
28+
(.-lineno (nth (inspect/stack) n)))
29+
30+
(defmulti gen-assert
31+
(fn [expr _]
32+
(cond
33+
(list? expr) (first expr)
34+
:else :default)))
35+
36+
(defmethod gen-assert '=
37+
[expr msg]
38+
`(when-not ~expr
39+
(swap! *test-failures*
40+
conj
41+
{:test-name *test-name*
42+
:test-section *test-section*
43+
:message ~msg
44+
:expr (quote ~expr)
45+
:actual ~(nth expr 2)
46+
:expected ~(second expr)
47+
:line (line-no 4)
48+
:type :failure})))
49+
50+
(defmethod gen-assert 'thrown?
51+
[expr msg]
52+
(let [exc-type (second expr)
53+
body (nthnext expr 2)]
54+
`(try
55+
(let [result# (do ~@body)]
56+
(swap! *test-failures*
57+
conj
58+
{:test-name *test-name*
59+
:test-section *test-section*
60+
:message ~msg
61+
:expr (quote ~expr)
62+
:actual result#
63+
:expected (quote ~exc-type)
64+
:line (line-no 3)
65+
:type :failure}))
66+
(catch ~exc-type _ nil)
67+
(catch builtins/Exception e#
68+
(swap! *test-failures*
69+
conj
70+
{:test-name *test-name*
71+
:test-section *test-section*
72+
:message (str "Expected " ~exc-type "; got " (builtins/type e#) " instead")
73+
:expr (quote ~expr)
74+
:actual e#
75+
:expected ~exc-type
76+
:line (line-no 3)
77+
:type :failure})))))
78+
79+
(defmethod gen-assert :default
80+
[expr msg]
81+
`(let [computed# ~expr]
82+
(when-not computed#
83+
(swap! *test-failures*
84+
conj
85+
{:test-name *test-name*
86+
:test-section *test-section*
87+
:message ~msg
88+
:expr (quote ~expr)
89+
:actual computed#
90+
:expected computed#
91+
:line (line-no 6)
92+
:type :failure}))))
93+
1894
(defmacro is
1995
"Assert that expr is true. Must appear inside of a deftest form."
2096
([expr]
21-
`(is ~expr (str "Test failure: " ~expr)))
97+
`(is ~expr (str "Test failure: " (pr-str (quote ~expr)))))
2298
([expr msg]
23-
(let [line-no (:basilisp.lang.reader/line (meta &form))
24-
25-
eq-sym `=
26-
27-
deconstruct? (and (list? expr) (= eq-sym (first expr)))
28-
29-
expected (if deconstruct?
30-
(second expr)
31-
expr)
32-
actual (if deconstruct?
33-
(nth expr 2)
34-
expr)]
35-
`(let [computed# ~expr
36-
actual# ~actual
37-
expected# ~expected]
38-
(when-not computed#
39-
;; Collect test failures in the atom bound to
40-
;; `*test-failures*` by `deftest`.
41-
(swap! *test-failures*
42-
conj
43-
[~msg
44-
{:test-name *test-name*
45-
:test-section *test-section*
46-
:expr (quote ~expr)
47-
:line ~line-no
48-
:actual actual#
49-
:expected expected#}]))))))
99+
`(try
100+
~(gen-assert expr msg)
101+
(catch builtins/Exception e#
102+
(swap! *test-failures*
103+
conj
104+
{:test-name *test-name*
105+
:test-section *test-section*
106+
:message (str "Unexpected exception thrown during test run: " (builtins/repr e#))
107+
:expr (quote ~expr)
108+
:actual e#
109+
:expected (quote ~expr)
110+
:line (line-no 2)
111+
:type :failure})))))
50112

51113
(defmacro testing
52114
"Wrapper for test cases to provide additional messaging and context

src/basilisp/testrunner.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ def collect(self):
101101
_ACTUAL_KW = kw.keyword('actual')
102102
_EXPECTED_KW = kw.keyword('expected')
103103
_FAILURES_KW = kw.keyword('failures')
104+
_MESSAGE_KW = kw.keyword('message')
104105
_LINE_KW = kw.keyword('line')
105106
_EXPR_KW = kw.keyword('expr')
106107
_TEST_SECTION_KW = kw.keyword('test-section')
@@ -145,9 +146,8 @@ def repr_failure(self, excinfo):
145146
failures = exc.data.entry(_FAILURES_KW)
146147
messages = []
147148

148-
for failure in failures:
149-
msg: str = failure.entry(0)
150-
details: lmap.Map = failure.entry(1)
149+
for details in failures:
150+
msg: str = details.entry(_MESSAGE_KW)
151151

152152
actual = details.entry(_ACTUAL_KW)
153153
expected = details.entry(_EXPECTED_KW)

tests/basilisp/testrunner_test.py

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import _pytest.pytester as pytester
22

33

4-
def test_testrunner(testdir: pytester.Testdir):
4+
def test_testrunner(testdir: pytester.Testdir, capsys):
55
code = """
66
(ns test-fixture
77
(:require
@@ -11,9 +11,44 @@ def test_testrunner(testdir: pytester.Testdir):
1111
(is (= 1 1))
1212
(is (= :hi :hi))
1313
(is true)
14-
(is (= "true" false)))
14+
(is false)
15+
(is (= "true" false))
16+
(is (thrown? basilisp.lang.exception/ExceptionInfo (throw (ex-info "Exception" {}))))
17+
(is (thrown? basilisp.lang.exception/ExceptionInfo (throw (builtins/Exception))))
18+
(is (= 4.6 4.6))
19+
(is (throw (ex-info "Uncaught exception" {}))))
1520
"""
1621
testdir.makefile('.lpy', test_fixture=code)
1722

1823
result: pytester.RunResult = testdir.runpytest()
1924
result.assert_outcomes(failed=1)
25+
26+
captured = capsys.readouterr()
27+
28+
expected_out = """FAIL in (fixture-test) (test_fixture.lpy:9)
29+
Test failure: false
30+
31+
expected: false
32+
actual: false"""
33+
assert expected_out in captured.out
34+
35+
expected_out = """FAIL in (fixture-test) (test_fixture.lpy:10)
36+
Test failure: (= "true" false)
37+
38+
expected: "true"
39+
actual: false"""
40+
assert expected_out in captured.out
41+
42+
expected_out = """FAIL in (fixture-test) (test_fixture.lpy:12)
43+
Expected <class 'basilisp.lang.exception.ExceptionInfo'>; got <class 'Exception'> instead
44+
45+
expected: <class 'basilisp.lang.exception.ExceptionInfo'>
46+
actual: Exception()"""
47+
assert expected_out in captured.out
48+
49+
expected_out = """FAIL in (fixture-test) (test_fixture.lpy:14)
50+
Unexpected exception thrown during test run: basilisp.lang.exception.ExceptionInfo(Uncaught exception, {})
51+
52+
expected: (throw (ex-info "Uncaught exception" {}))
53+
actual: basilisp.lang.exception.ExceptionInfo(Uncaught exception, {})"""
54+
assert expected_out in captured.out

0 commit comments

Comments
 (0)