Skip to content

Commit 5fbfc29

Browse files
authored
Fix basilisp.test/is macro error reporting when used within macros (#831)
Fixes #829
1 parent 491a136 commit 5fbfc29

File tree

4 files changed

+107
-12
lines changed

4 files changed

+107
-12
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,5 @@ lispcore.py
6969
.idea/
7070
.envrc
7171
poetry.lock
72-
**/*.~undo-tree~
72+
**/*.~undo-tree~
73+
.nrepl-port

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
* Added a subcommand for bootstrapping the Python installation with Basilisp (#790)
1414
* Added support for executing Basilisp namespaces directly via `basilisp run` and by `python -m` (#791)
1515
* Added the `memoize` core fn (#812)
16+
* Added support for `thrown-with-msg?` assertions to `basilisp.test/is` (#831)
1617

1718
### Changed
1819
* Optimize calls to Python's `operator` module into their corresponding native operators (#754)
@@ -37,6 +38,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3738
* Fix issue with the variadic ampersand operator treated as a binding in macros (#772)
3839
* Fix a bug the variadic arg symbol was not correctly bound to `nil` when no variadic arguments were provided (#801)
3940
* Fix a bug where the quotient of very large numbers was incorrect (#822)
41+
* Fix a bug where `basilisp.test/is` may fail to generate expected/actual info on failures when declared inside a macro (#829)
4042

4143
### Removed
4244
* Removed support for PyPy 3.8 (#785)

src/basilisp/test.lpy

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,12 +73,20 @@
7373
"Implementation detail of :lpy:fn:`is` for generating macros."
7474
(fn [expr _ _]
7575
(cond
76-
(list? expr) (first expr)
77-
:else :default)))
78-
76+
(vector? expr) :default
77+
(sequential? expr) (let [maybe-f (first expr)]
78+
(if (and (symbol? maybe-f)
79+
(or (= 'basilisp.core/= maybe-f)
80+
(#{'thrown-with-msg? 'thrown?} (symbol (name maybe-f)))))
81+
(symbol (name maybe-f))
82+
maybe-f))
83+
:else :default)))
84+
85+
;; clojure.test does not special case '=, but this means that its expected/actual
86+
;; results just show expected `expr` and `(not expr)`
7987
(defmethod gen-assert '=
8088
[expr msg line-num]
81-
`(let [actual# ~(nth expr 2)
89+
`(let [actual# ~(nth expr 2)
8290
expected# ~(second expr)]
8391
(when-not (= expected# actual#)
8492
(vswap! *test-failures*
@@ -121,6 +129,50 @@
121129
:line ~line-num
122130
:type :failure})))))
123131

132+
(defmethod gen-assert 'thrown-with-msg?
133+
[expr msg line-num]
134+
(let [exc-type (second expr)
135+
pattern (nth expr 2)
136+
body (nthnext expr 3)]
137+
`(try
138+
(let [result# (do ~@body)]
139+
(vswap! *test-failures*
140+
conj
141+
{:test-name *test-name*
142+
:test-section *test-section*
143+
:message ~msg
144+
:expr (quote ~expr)
145+
:actual result#
146+
:expected (quote ~exc-type)
147+
:line ~line-num
148+
:type :failure}))
149+
(catch ~exc-type e#
150+
;; Use python/str rather than Basilisp str to get the raw "message"
151+
;; from the exception.
152+
(let [string-exc# (python/str e#)]
153+
(when-not (re-find ~pattern string-exc#)
154+
(vswap! *test-failures*
155+
conj
156+
{:test-name *test-name*
157+
:test-section *test-section*
158+
:message "Regex pattern did not match"
159+
:expr (quote ~expr)
160+
:actual string-exc#
161+
:expected ~pattern
162+
:line ~line-num
163+
:type :failure}))))
164+
(catch python/Exception e#
165+
(vswap! *test-failures*
166+
conj
167+
{:test-name *test-name*
168+
:test-section *test-section*
169+
:message (str "Expected " ~exc-type "; got " (python/type e#) " instead")
170+
:expr (quote ~expr)
171+
:actual e#
172+
:expected ~exc-type
173+
:line ~line-num
174+
:type :failure})))))
175+
124176
(defmethod gen-assert :default
125177
[expr msg line-num]
126178
`(let [computed# ~expr]
@@ -148,6 +200,10 @@
148200
is the expected value and the second element is the actual value
149201
- ``(is (thrown? ExceptionType expr))`` generates a basic assertion that ``expr``
150202
does generate an exception of the type ``ExceptionType``
203+
- ``(is (thrown-with-msg? ExceptionType pattern expr))`` generates a basic assertion
204+
that ``expr`` does generate an exception of the type ``ExceptionType`` and that
205+
the stringified exception (as by ``python/str``) matches the regular expression
206+
``pattern`` using :lpy:fn:`re-find`
151207
- ``(is expr)`` is the most basic assertion type that just asserts that ``expr`` is
152208
truthy
153209

tests/basilisp/testrunner_test.py

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,15 @@ def run_result(self, pytester: pytest.Pytester) -> pytest.RunResult:
2222
(is (= "string" "string"))
2323
(is (thrown? basilisp.lang.exception/ExceptionInfo (throw (ex-info "Exception" {}))))
2424
(is (thrown? basilisp.lang.exception/ExceptionInfo (throw (python/Exception))))
25-
(is (throw (ex-info "Uncaught exception" {}))))
25+
(is (throw (ex-info "Uncaught exception" {})))
26+
(is (thrown-with-msg?
27+
basilisp.lang.exception/ExceptionInfo
28+
#"Caught exception"
29+
(throw (ex-info "Caught exception message" {}))))
30+
(is (thrown-with-msg?
31+
basilisp.lang.exception/ExceptionInfo
32+
#"Known exception"
33+
(throw (ex-info "Unexpected exception" {})))))
2634
2735
(testing "are assertions"
2836
(are [exp actual] (= exp actual)
@@ -37,14 +45,20 @@ def run_result(self, pytester: pytest.Pytester) -> pytest.RunResult:
3745
(deftest error-test
3846
(throw
3947
(ex-info "This test will count as an error." {})))
48+
49+
;; Test that syntax quoted forms still get expanded correctly into assertions
50+
(defmacro syntax-quote-test-make []
51+
`(deftest syntax-quote-seq-test
52+
(is (= 5 4))))
53+
(syntax-quote-test-make)
4054
"""
4155
pytester.makefile(".lpy", test_testrunner=code)
4256
pytester.syspathinsert()
4357
yield pytester.runpytest()
4458
runtime.Namespace.remove(sym.symbol("test-testrunner"))
4559

4660
def test_outcomes(self, run_result: pytest.RunResult):
47-
run_result.assert_outcomes(passed=1, failed=2)
61+
run_result.assert_outcomes(passed=1, failed=3)
4862

4963
def test_failure_repr(self, run_result: pytest.RunResult):
5064
run_result.stdout.fnmatch_lines(
@@ -65,7 +79,19 @@ def test_failure_repr(self, run_result: pytest.RunResult):
6579
"",
6680
" expected: <class 'basilisp.lang.exception.ExceptionInfo'>",
6781
" actual: Exception()",
68-
]
82+
],
83+
consecutive=True,
84+
)
85+
86+
run_result.stdout.fnmatch_lines(
87+
[
88+
"FAIL in (assertion-test) (test_testrunner.lpy:17)",
89+
" is assertions :: Regex pattern did not match",
90+
"",
91+
' expected: #"Known exception"',
92+
' actual: "Unexpected exception {}"',
93+
],
94+
consecutive=True,
6995
)
7096

7197
run_result.stdout.fnmatch_lines(
@@ -81,6 +107,17 @@ def test_failure_repr(self, run_result: pytest.RunResult):
81107
consecutive=True,
82108
)
83109

110+
run_result.stdout.fnmatch_lines(
111+
[
112+
"FAIL in (syntax-quote-seq-test) (test_testrunner.lpy)",
113+
" Test failure: (basilisp.core/= 5 4)",
114+
"",
115+
" expected: 5",
116+
" actual: 4",
117+
],
118+
consecutive=True,
119+
)
120+
84121
@pytest.mark.xfail(
85122
platform.python_implementation() == "PyPy" and sys.version_info < (3, 9),
86123
reason=(
@@ -95,7 +132,7 @@ def test_error_repr(self, run_result: pytest.RunResult):
95132
"",
96133
"Traceback (most recent call last):",
97134
' File "*test_testrunner.lpy", line 12, in assertion_test',
98-
' (is (throw (ex-info "Uncaught exception" {}))))',
135+
' (is (throw (ex-info "Uncaught exception" {})))',
99136
"basilisp.lang.exception.ExceptionInfo: Uncaught exception {}",
100137
]
101138
else:
@@ -104,8 +141,7 @@ def test_error_repr(self, run_result: pytest.RunResult):
104141
"",
105142
"Traceback (most recent call last):",
106143
' File "*test_testrunner.lpy", line 12, in assertion_test',
107-
' (is (throw (ex-info "Uncaught exception" {}))))',
108-
" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^",
144+
' (is (throw (ex-info "Uncaught exception" {})))',
109145
"basilisp.lang.exception.ExceptionInfo: Uncaught exception {}",
110146
]
111147

@@ -118,7 +154,7 @@ def test_error_repr(self, run_result: pytest.RunResult):
118154
[
119155
"ERROR in (error-test) (test_testrunner.lpy)",
120156
"Traceback (most recent call last):",
121-
' File "*test_testrunner.lpy", line 25, in error_test',
157+
' File "*test_testrunner.lpy", line 33, in error_test',
122158
" (throw",
123159
"basilisp.lang.exception.ExceptionInfo: This test will count as an error. {}",
124160
]

0 commit comments

Comments
 (0)