Skip to content

Commit a3b7edc

Browse files
test(rewrite): collapse the harness onto one exec helper
The five helpers each repeated dedent -> rewrite -> compile -> build namespace -> exec -> fetch check(). Extract _exec_check() and build all of them on it, including _run_both, which assert_semantically_equivalent now shares with assert_evaluation_order instead of open-coding a second double-execution. Drop the extra_ns parameter. It was threaded through six functions and no test ever passed it: assert_single_evaluation seeds its own counter and every other source defines what it needs inline. With each run getting a fresh namespace, the deepcopy that kept plain and rewritten runs from leaking into each other is no longer needed either. Add smoke tests for must_not_contain, whose only callers sit behind strict xfails that fail before reaching it. The harness is now fully exercised by its own tests, which is what the PRs stacked on it need in order to be independently reviewable. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent ff0f7c0 commit a3b7edc

1 file changed

Lines changed: 78 additions & 116 deletions

File tree

testing/test_assertrewrite_coverage.py

Lines changed: 78 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,6 @@
3737

3838
import ast
3939
from collections.abc import Callable
40-
from collections.abc import Mapping
41-
import copy
4240
import sys
4341
import textwrap
4442
from typing import cast
@@ -52,32 +50,42 @@
5250
# ---------------------------------------------------------------------------
5351

5452

55-
def _rewrite_source(src: str) -> ast.Module:
56-
"""Parse and rewrite assertions in source code."""
53+
def _exec_check(
54+
src: str,
55+
*,
56+
rewrite: bool = True,
57+
ns: dict[str, object] | None = None,
58+
) -> Callable[[], object]:
59+
"""Compile and execute ``src``, returning the ``check`` function it defines.
60+
61+
``src`` is dedented first. When ``rewrite`` is true the assertions are
62+
rewritten before compiling, which is the usual case; the axes that compare
63+
against unrewritten behaviour ask for both.
64+
65+
``ns`` is the namespace the source executes in. Pass one in to seed it
66+
(``assert_single_evaluation`` needs a ``counter``) or to read side effects
67+
back out afterwards; each call otherwise gets a fresh namespace, which is
68+
what keeps a plain and a rewritten run from leaking state into each other.
69+
"""
70+
src = textwrap.dedent(src)
5771
tree = ast.parse(src)
58-
rewrite_asserts(tree, src.encode())
59-
return tree
72+
if rewrite:
73+
rewrite_asserts(tree, src.encode())
74+
if ns is None:
75+
ns = {}
76+
exec(compile(tree, "<test-rewritten>" if rewrite else "<test-plain>", "exec"), ns)
77+
return cast(Callable[[], object], ns["check"])
6078

6179

62-
def get_failure_message(
63-
src: str,
64-
extra_ns: Mapping[str, object] | None = None,
65-
) -> str:
80+
def get_failure_message(src: str) -> str:
6681
"""Compile rewritten source, execute it, and return the failure message.
6782
6883
The source should contain a function named ``check`` with a failing assert.
6984
Returns the AssertionError message string.
7085
7186
Raises AssertionError via pytest.fail if the code does not raise.
7287
"""
73-
src = textwrap.dedent(src)
74-
mod = _rewrite_source(src)
75-
code = compile(mod, "<test>", "exec")
76-
ns: dict[str, object] = {}
77-
if extra_ns is not None:
78-
ns.update(extra_ns)
79-
exec(code, ns)
80-
func = cast(Callable[[], None], ns["check"])
88+
func = _exec_check(src)
8189
try:
8290
func()
8391
except AssertionError:
@@ -94,7 +102,6 @@ def assert_introspects(
94102
*,
95103
must_contain: list[str],
96104
must_not_contain: list[str] | None = None,
97-
extra_ns: Mapping[str, object] | None = None,
98105
) -> str:
99106
"""Verify a failing assert produces a message with expected intermediate values.
100107
@@ -106,15 +113,13 @@ def assert_introspects(
106113
Substrings that MUST appear in the failure message.
107114
must_not_contain : list[str] | None
108115
Substrings that must NOT appear in the failure message.
109-
extra_ns : Mapping[str, object] | None
110-
Additional namespace entries available during execution.
111116
112117
Returns
113118
-------
114119
str
115120
The full failure message (for further inspection if needed).
116121
"""
117-
msg = get_failure_message(src, extra_ns=extra_ns)
122+
msg = get_failure_message(src)
118123
for expected in must_contain:
119124
assert expected in msg, (
120125
f"Expected {expected!r} in failure message.\nGot:\n{msg}"
@@ -130,31 +135,22 @@ def assert_single_evaluation(
130135
src: str,
131136
*,
132137
expected_call_count: int = 1,
133-
extra_ns: Mapping[str, object] | None = None,
134138
) -> None:
135139
"""Verify side-effecting expressions in assert are evaluated exactly once.
136140
137-
The source should define a ``check()`` function and use a ``counter`` list
138-
(provided via extra_ns or defined in the source) that tracks how many times
139-
a side-effecting expression is evaluated.
141+
The source should define a ``check()`` function and use the ``counter``
142+
list seeded here, which tracks how many times a side-effecting expression
143+
is evaluated.
140144
141145
Parameters
142146
----------
143147
src : str
144148
Source containing a ``check()`` function whose assert has side effects.
145149
expected_call_count : int
146150
How many times the side-effecting expression should be evaluated.
147-
extra_ns : Mapping[str, object] | None
148-
Additional namespace. Should include ``counter`` if not defined in src.
149151
"""
150-
src = textwrap.dedent(src)
151-
mod = _rewrite_source(src)
152-
code = compile(mod, "<test>", "exec")
153152
ns: dict[str, object] = {"counter": [0]}
154-
if extra_ns is not None:
155-
ns.update(extra_ns)
156-
exec(code, ns)
157-
func = cast(Callable[[], None], ns["check"])
153+
func = _exec_check(src, ns=ns)
158154
counter = cast(list[int], ns["counter"])
159155
counter[0] = 0
160156
try:
@@ -167,108 +163,53 @@ def assert_single_evaluation(
167163
)
168164

169165

170-
def assert_passes_when_true(
171-
src: str,
172-
*,
173-
extra_ns: Mapping[str, object] | None = None,
174-
) -> None:
166+
def assert_passes_when_true(src: str) -> None:
175167
"""Verify rewritten assertion does not raise when the condition is true.
176168
177169
Parameters
178170
----------
179171
src : str
180172
Source containing a ``check()`` function with a passing assertion.
181-
extra_ns : Mapping[str, object] | None
182-
Additional namespace entries available during execution.
183173
"""
184-
src = textwrap.dedent(src)
185-
mod = _rewrite_source(src)
186-
code = compile(mod, "<test>", "exec")
187-
ns: dict[str, object] = {}
188-
if extra_ns is not None:
189-
ns.update(extra_ns)
190-
exec(code, ns)
191-
func = cast(Callable[[], None], ns["check"])
192-
func()
174+
_exec_check(src)()
193175

194176

195-
def assert_semantically_equivalent(
196-
src: str,
197-
*,
198-
extra_ns: Mapping[str, object] | None = None,
199-
) -> None:
177+
_Outcome = tuple[bool, object]
178+
179+
180+
def _run_both(src: str) -> tuple[_Outcome, _Outcome]:
181+
"""Execute ``check()`` plain and rewritten, returning (raised, result) pairs."""
182+
outcomes: list[_Outcome] = []
183+
for rewrite in (False, True):
184+
func = _exec_check(src, rewrite=rewrite)
185+
try:
186+
outcomes.append((False, func()))
187+
except AssertionError:
188+
outcomes.append((True, None))
189+
plain, rewritten = outcomes
190+
return plain, rewritten
191+
192+
193+
def assert_semantically_equivalent(src: str) -> None:
200194
"""Verify rewritten code has same pass/fail semantics as unrewritten code.
201195
202196
Runs the source both with and without rewriting, and asserts they agree
203-
on whether an AssertionError is raised.
197+
on whether an AssertionError is raised. Only the raise is compared --
198+
:func:`assert_evaluation_order` compares the returned observations too.
204199
205200
Parameters
206201
----------
207202
src : str
208203
Source containing a ``check()`` function with an assertion.
209-
extra_ns : Mapping[str, object] | None
210-
Additional namespace entries available during execution.
211204
"""
212-
src = textwrap.dedent(src)
213-
214-
# Run without rewriting — use deepcopy of extra_ns to isolate mutable state
215-
plain_code = compile(src, "<test-plain>", "exec")
216-
plain_ns: dict[str, object] = {}
217-
if extra_ns is not None:
218-
plain_ns.update(copy.deepcopy(dict(extra_ns)))
219-
exec(plain_code, plain_ns)
220-
plain_func = cast(Callable[[], None], plain_ns["check"])
221-
plain_raised = False
222-
try:
223-
plain_func()
224-
except AssertionError:
225-
plain_raised = True
226-
227-
# Run with rewriting — fresh deepcopy so mutations from first run don't leak
228-
mod = _rewrite_source(src)
229-
rewritten_code = compile(mod, "<test-rewritten>", "exec")
230-
rewritten_ns: dict[str, object] = {}
231-
if extra_ns is not None:
232-
rewritten_ns.update(copy.deepcopy(dict(extra_ns)))
233-
exec(rewritten_code, rewritten_ns)
234-
rewritten_func = cast(Callable[[], None], rewritten_ns["check"])
235-
rewritten_raised = False
236-
try:
237-
rewritten_func()
238-
except AssertionError:
239-
rewritten_raised = True
240-
205+
(plain_raised, _), (rewritten_raised, _) = _run_both(src)
241206
assert plain_raised == rewritten_raised, (
242207
f"Semantic mismatch: plain {'raised' if plain_raised else 'passed'}, "
243208
f"rewritten {'raised' if rewritten_raised else 'passed'}"
244209
)
245210

246211

247-
def _run_both(src: str, extra_ns: Mapping[str, object] | None) -> list[object]:
248-
"""Execute ``check()`` plain and rewritten, returning (raised, result) pairs."""
249-
src = textwrap.dedent(src)
250-
outcomes: list[object] = []
251-
for code in (
252-
compile(src, "<test-plain>", "exec"),
253-
compile(_rewrite_source(src), "<test-rewritten>", "exec"),
254-
):
255-
ns: dict[str, object] = {}
256-
if extra_ns is not None:
257-
ns.update(copy.deepcopy(dict(extra_ns)))
258-
exec(code, ns)
259-
func = cast(Callable[[], object], ns["check"])
260-
try:
261-
outcomes.append((False, func()))
262-
except AssertionError:
263-
outcomes.append((True, None))
264-
return outcomes
265-
266-
267-
def assert_evaluation_order(
268-
src: str,
269-
*,
270-
extra_ns: Mapping[str, object] | None = None,
271-
) -> None:
212+
def assert_evaluation_order(src: str) -> None:
272213
"""Verify rewriting preserves the values Python's evaluation order produces.
273214
274215
``check()`` should return whatever the order is observable through -- the
@@ -291,10 +232,8 @@ def assert_evaluation_order(
291232
----------
292233
src : str
293234
Source containing a ``check()`` function that returns its observations.
294-
extra_ns : Mapping[str, object] | None
295-
Additional namespace entries available during execution.
296235
"""
297-
plain, rewritten = _run_both(src, extra_ns)
236+
plain, rewritten = _run_both(src)
298237
assert plain == rewritten, (
299238
f"Evaluation order mismatch:\n plain (raised, result) = {plain}\n"
300239
f" rewritten (raised, result) = {rewritten}"
@@ -343,6 +282,29 @@ def check():
343282
must_contain=["this is not in the message"],
344283
)
345284

285+
def test_assert_introspects_must_not_contain(self) -> None:
286+
assert_introspects(
287+
"""
288+
def check():
289+
x = 3
290+
assert x == 5
291+
""",
292+
must_contain=["assert 3 == 5"],
293+
must_not_contain=["this is not in the message"],
294+
)
295+
296+
def test_assert_introspects_fails_on_unexpected(self) -> None:
297+
with pytest.raises(AssertionError, match=r"Did NOT expect.*in failure"):
298+
assert_introspects(
299+
"""
300+
def check():
301+
x = 3
302+
assert x == 5
303+
""",
304+
must_contain=["assert 3 == 5"],
305+
must_not_contain=["assert 3"],
306+
)
307+
346308
def test_assert_single_evaluation(self) -> None:
347309
assert_single_evaluation("""
348310
def check():

0 commit comments

Comments
 (0)