Skip to content

Commit 2084efe

Browse files
committed
tests(feat[async_doctest]): Add comprehensive async doctest tests
why: Verify async doctest functionality works correctly what: - Test basic top-level await with asyncio.sleep - Test async functions returning values - Test mixed sync/async examples in same block - Test state persistence across examples - Test async context managers (async with) - Test async iteration (async for) - Test async comprehensions - Test expected exceptions in async code - Test Markdown file support - Test AsyncDocTestRunner directly
1 parent 00199e5 commit 2084efe

File tree

1 file changed

+255
-0
lines changed

1 file changed

+255
-0
lines changed

tests/test_async_doctest.py

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
"""Tests for async doctest support.
2+
3+
These tests verify that top-level await works in doctests without requiring
4+
asyncio.run() boilerplate.
5+
"""
6+
7+
from __future__ import annotations
8+
9+
import textwrap
10+
import typing as t
11+
12+
from doctest_docutils import (
13+
AsyncDocTestRunner,
14+
DocutilsDocTestFinder,
15+
run_doctest_docutils,
16+
)
17+
18+
if t.TYPE_CHECKING:
19+
import pathlib
20+
21+
22+
class TestAsyncDoctestBasic:
23+
"""Basic async doctest functionality tests."""
24+
25+
def test_top_level_await_sleep(self, tmp_path: pathlib.Path) -> None:
26+
"""Test basic top-level await with asyncio.sleep."""
27+
doc = tmp_path / "test.rst"
28+
doc.write_text(
29+
textwrap.dedent("""
30+
Test async doctest
31+
==================
32+
33+
>>> import asyncio
34+
>>> await asyncio.sleep(0)
35+
>>> 1 + 1
36+
2
37+
"""),
38+
)
39+
result = run_doctest_docutils(str(doc), module_relative=False, verbose=True)
40+
assert result.failed == 0
41+
assert result.attempted >= 2
42+
43+
def test_top_level_await_with_result(self, tmp_path: pathlib.Path) -> None:
44+
"""Test top-level await that returns a value."""
45+
doc = tmp_path / "test.rst"
46+
doc.write_text(
47+
textwrap.dedent("""
48+
Async with result
49+
=================
50+
51+
>>> import asyncio
52+
>>> async def get_value():
53+
... await asyncio.sleep(0)
54+
... return 42
55+
>>> await get_value()
56+
42
57+
"""),
58+
)
59+
result = run_doctest_docutils(str(doc), module_relative=False, verbose=True)
60+
assert result.failed == 0
61+
62+
def test_mixed_sync_async(self, tmp_path: pathlib.Path) -> None:
63+
"""Test mixing sync and async examples in same block."""
64+
doc = tmp_path / "test.rst"
65+
doc.write_text(
66+
textwrap.dedent("""
67+
Mixed sync and async
68+
====================
69+
70+
>>> x = 1
71+
>>> import asyncio
72+
>>> await asyncio.sleep(0)
73+
>>> x + 1
74+
2
75+
>>> y = await asyncio.sleep(0) or 10
76+
>>> y
77+
10
78+
"""),
79+
)
80+
result = run_doctest_docutils(str(doc), module_relative=False, verbose=True)
81+
assert result.failed == 0
82+
83+
def test_state_persistence_across_examples(self, tmp_path: pathlib.Path) -> None:
84+
"""Test that state persists across async examples in same block."""
85+
doc = tmp_path / "test.rst"
86+
doc.write_text(
87+
textwrap.dedent("""
88+
State persistence
89+
=================
90+
91+
>>> import asyncio
92+
>>> async def set_value():
93+
... global shared_value
94+
... await asyncio.sleep(0)
95+
... shared_value = 'hello'
96+
>>> await set_value()
97+
>>> shared_value
98+
'hello'
99+
"""),
100+
)
101+
result = run_doctest_docutils(str(doc), module_relative=False, verbose=True)
102+
assert result.failed == 0
103+
104+
105+
class TestAsyncDoctestAdvanced:
106+
"""Advanced async doctest scenarios."""
107+
108+
def test_async_context_manager(self, tmp_path: pathlib.Path) -> None:
109+
"""Test async with statement."""
110+
doc = tmp_path / "test.rst"
111+
doc.write_text(
112+
textwrap.dedent("""
113+
Async context manager
114+
=====================
115+
116+
>>> import asyncio
117+
>>> class AsyncCM:
118+
... async def __aenter__(self):
119+
... await asyncio.sleep(0)
120+
... return 'entered'
121+
... async def __aexit__(self, *args):
122+
... await asyncio.sleep(0)
123+
>>> async with AsyncCM() as value:
124+
... print(value)
125+
entered
126+
"""),
127+
)
128+
result = run_doctest_docutils(str(doc), module_relative=False, verbose=True)
129+
assert result.failed == 0
130+
131+
def test_async_for(self, tmp_path: pathlib.Path) -> None:
132+
"""Test async for statement."""
133+
doc = tmp_path / "test.rst"
134+
doc.write_text(
135+
textwrap.dedent("""
136+
Async iteration
137+
===============
138+
139+
>>> import asyncio
140+
>>> async def async_range(n):
141+
... for i in range(n):
142+
... await asyncio.sleep(0)
143+
... yield i
144+
>>> result = []
145+
>>> async for x in async_range(3):
146+
... result.append(x)
147+
>>> result
148+
[0, 1, 2]
149+
"""),
150+
)
151+
result = run_doctest_docutils(str(doc), module_relative=False, verbose=True)
152+
assert result.failed == 0
153+
154+
def test_async_comprehension(self, tmp_path: pathlib.Path) -> None:
155+
"""Test async list comprehension."""
156+
doc = tmp_path / "test.rst"
157+
doc.write_text(
158+
textwrap.dedent("""
159+
Async comprehension
160+
===================
161+
162+
>>> import asyncio
163+
>>> async def async_range(n):
164+
... for i in range(n):
165+
... await asyncio.sleep(0)
166+
... yield i
167+
>>> [x async for x in async_range(3)]
168+
[0, 1, 2]
169+
"""),
170+
)
171+
result = run_doctest_docutils(str(doc), module_relative=False, verbose=True)
172+
assert result.failed == 0
173+
174+
175+
class TestAsyncDoctestExceptions:
176+
"""Test exception handling in async doctests."""
177+
178+
def test_expected_async_exception(self, tmp_path: pathlib.Path) -> None:
179+
"""Test that expected exceptions from async code are handled."""
180+
doc = tmp_path / "test.rst"
181+
doc.write_text(
182+
textwrap.dedent("""
183+
Expected exception
184+
==================
185+
186+
>>> import asyncio
187+
>>> async def raise_error():
188+
... await asyncio.sleep(0)
189+
... raise ValueError('test error')
190+
>>> await raise_error()
191+
Traceback (most recent call last):
192+
...
193+
ValueError: test error
194+
"""),
195+
)
196+
result = run_doctest_docutils(str(doc), module_relative=False, verbose=True)
197+
assert result.failed == 0
198+
199+
200+
class TestAsyncDoctestMarkdown:
201+
"""Test async doctests in Markdown files."""
202+
203+
def test_markdown_async_doctest(self, tmp_path: pathlib.Path) -> None:
204+
"""Test async doctest in Markdown file."""
205+
doc = tmp_path / "test.md"
206+
doc.write_text(
207+
textwrap.dedent("""
208+
# Async Doctest in Markdown
209+
210+
```python
211+
>>> import asyncio
212+
>>> await asyncio.sleep(0)
213+
>>> 1 + 1
214+
2
215+
```
216+
"""),
217+
)
218+
result = run_doctest_docutils(str(doc), module_relative=False, verbose=True)
219+
assert result.failed == 0
220+
221+
222+
class TestAsyncDocTestRunner:
223+
"""Test AsyncDocTestRunner directly."""
224+
225+
def test_runner_creates_event_loop(self, tmp_path: pathlib.Path) -> None:
226+
"""Test that runner properly creates and manages event loop."""
227+
doc = tmp_path / "test.rst"
228+
doc.write_text(">>> import asyncio\n>>> await asyncio.sleep(0)\n")
229+
230+
finder = DocutilsDocTestFinder()
231+
runner = AsyncDocTestRunner(verbose=True)
232+
233+
text = doc.read_text()
234+
for test in finder.find(text, str(doc)):
235+
result = runner.run(test)
236+
assert result.failed == 0
237+
238+
def test_sync_code_still_works(self, tmp_path: pathlib.Path) -> None:
239+
"""Test that sync code works normally with AsyncDocTestRunner."""
240+
doc = tmp_path / "test.rst"
241+
doc.write_text(
242+
textwrap.dedent("""
243+
Sync code
244+
=========
245+
246+
>>> 1 + 1
247+
2
248+
>>> x = 'hello'
249+
>>> x.upper()
250+
'HELLO'
251+
"""),
252+
)
253+
result = run_doctest_docutils(str(doc), module_relative=False, verbose=True)
254+
assert result.failed == 0
255+
assert result.attempted >= 2

0 commit comments

Comments
 (0)