Skip to content

Commit 6b49680

Browse files
committed
Add async generator tests (.NET)
1 parent 9728021 commit 6b49680

2 files changed

Lines changed: 262 additions & 0 deletions

File tree

tests/IronPython.Tests/Cases/IronPythonCasesManifest.ini

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ WorkingDirectory=$(TEST_FILE_DIR)
44
Redirect=false
55
Timeout=120000 # 2 minute timeout
66

7+
[IronPython.test_asyncgen]
8+
Ignore=true
9+
RunCondition='$(FRAMEWORK)' <> '.NETFramework,Version=v4.6.2'
10+
Reason=Requires IAsyncEnumerable & IAsyncDisposable
11+
712
[IronPython.test_builtin_stdlib]
813
RunCondition=NOT $(IS_MONO)
914
Reason=Exception on adding DocTestSuite

tests/suite/test_asyncgen.py

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
# Licensed to the .NET Foundation under one or more agreements.
2+
# The .NET Foundation licenses this file to you under the Apache 2.0 License.
3+
# See the LICENSE file in the project root for more information.
4+
5+
"""Tests for async constructs that lower to .NET IAsyncEnumerable / IAsyncDisposable:
6+
async generators (PEP 525) and `async with` over a .NET IAsyncDisposable."""
7+
8+
import unittest
9+
10+
from iptest import run_test, skipUnlessIronPython
11+
12+
13+
def run_coro(coro):
14+
"""Run a coroutine to completion, blocking on yielded .NET Tasks."""
15+
value = None
16+
while True:
17+
try:
18+
task = coro.send(value)
19+
# .NET Task yielded — block on it (test runner is synchronous)
20+
task.Wait()
21+
value = None
22+
except StopIteration as e:
23+
return e.value
24+
25+
26+
@skipUnlessIronPython()
27+
class DotNetAsyncDisposableTest(unittest.TestCase):
28+
"""Tests for async with over a .NET IAsyncDisposable."""
29+
30+
def test_async_with_dotnet_disposable(self):
31+
"""async with over a .NET IAsyncDisposable enters with the resource and disposes on exit."""
32+
from System.IO import MemoryStream
33+
s = MemoryStream()
34+
self.assertTrue(hasattr(s, '__aenter__'))
35+
self.assertTrue(hasattr(s, '__aexit__'))
36+
37+
async def test():
38+
async with s as entered:
39+
self.assertIs(entered, s)
40+
self.assertTrue(entered.CanWrite)
41+
return s.CanWrite # disposed on exit -> False
42+
43+
self.assertFalse(run_coro(test()))
44+
45+
46+
@skipUnlessIronPython()
47+
class AsyncGeneratorTest(unittest.TestCase):
48+
"""Tests for PEP 525 async generators (await + yield). Bodies await .NET Tasks."""
49+
50+
51+
def test_await_and_yield(self):
52+
"""An async generator body mixing await and yield, consumed via async for."""
53+
from System.Threading.Tasks import Task
54+
55+
async def agen():
56+
yield 1
57+
x = await Task.FromResult(10)
58+
yield x + 1 # await result feeds the next yield
59+
if x > 5:
60+
return # bare return ends the async iteration
61+
yield 'unreached'
62+
63+
async def consume():
64+
out = []
65+
async for v in agen():
66+
out.append(v)
67+
return out
68+
69+
self.assertEqual(run_coro(consume()), [1, 11])
70+
71+
72+
def test_yield_task_is_value_not_await(self):
73+
"""A yielded Task is the produced value, not a suspension point."""
74+
from System.Threading.Tasks import Task
75+
76+
async def agen():
77+
yield Task.FromResult(99)
78+
79+
async def consume():
80+
out = []
81+
async for item in agen():
82+
out.append(item)
83+
return out
84+
85+
items = run_coro(consume())
86+
self.assertEqual(len(items), 1)
87+
# The item is the Task itself; if it had been awaited we'd have gotten 99.
88+
self.assertEqual(items[0].Result, 99)
89+
90+
91+
def test_async_generator_type(self):
92+
async def agen():
93+
yield 1
94+
g = agen()
95+
self.assertEqual(type(g).__name__, 'async_generator')
96+
97+
98+
def test_asend(self):
99+
"""asend feeds a value into `x = yield z` (PEP 525 two-way communication)."""
100+
async def agen():
101+
got1 = yield 1
102+
got2 = yield ('after1', got1)
103+
yield ('after2', got2)
104+
105+
async def drive():
106+
g = agen()
107+
out = []
108+
out.append(await g.asend(None)) # start -> yields 1
109+
out.append(await g.asend('A')) # got1 == 'A' -> ('after1', 'A')
110+
out.append(await g.asend('B')) # got2 == 'B' -> ('after2', 'B')
111+
try:
112+
await g.asend('C')
113+
except StopAsyncIteration:
114+
out.append('stop')
115+
return out
116+
117+
self.assertEqual(run_coro(drive()), [1, ('after1', 'A'), ('after2', 'B'), 'stop'])
118+
119+
120+
def test_athrow(self):
121+
"""athrow injects an exception at the yield; the body can catch and continue."""
122+
async def agen():
123+
try:
124+
yield 1
125+
except ValueError as e:
126+
yield ('caught', str(e))
127+
yield 3
128+
129+
async def drive():
130+
g = agen()
131+
out = []
132+
out.append(await g.asend(None)) # -> 1
133+
out.append(await g.athrow(ValueError('boom'))) # caught -> ('caught', 'boom')
134+
out.append(await g.asend(None)) # -> 3
135+
try:
136+
await g.asend(None)
137+
except StopAsyncIteration:
138+
out.append('stop')
139+
return out
140+
141+
self.assertEqual(run_coro(drive()), [1, ('caught', 'boom'), 3, 'stop'])
142+
143+
144+
def test_aclose_runs_finally(self):
145+
"""aclose injects GeneratorExit at the yield so the body's finally runs."""
146+
log = []
147+
148+
async def agen():
149+
try:
150+
yield 1
151+
yield 2
152+
finally:
153+
log.append('cleanup')
154+
155+
async def drive():
156+
g = agen()
157+
first = await g.asend(None) # -> 1
158+
await g.aclose() # GeneratorExit at the yield -> finally runs
159+
return first
160+
161+
self.assertEqual(run_coro(drive()), 1)
162+
self.assertEqual(log, ['cleanup'])
163+
164+
165+
@skipUnlessIronPython()
166+
class NestedAsyncGeneratorTest(unittest.TestCase):
167+
"""An async generator lexically nested inside another async generator.
168+
169+
Both functions own the async send/throw slots; their slot variables must stay
170+
distinct per function so the inner generator's slots don't collide with the
171+
outer's when the inner lambda is nested inside the outer.
172+
"""
173+
174+
175+
def test_nested_async_generator(self):
176+
"""Outer async gen consumes a nested async gen via async for and re-yields."""
177+
async def outer():
178+
async def inner():
179+
yield 'a'
180+
yield 'b'
181+
async for x in inner():
182+
yield x.upper()
183+
184+
async def consume():
185+
out = []
186+
async for v in outer():
187+
out.append(v)
188+
return out
189+
190+
self.assertEqual(run_coro(consume()), ['A', 'B'])
191+
192+
193+
def test_nested_async_generator_with_await(self):
194+
"""Both nested async gens await a .NET Task between yields."""
195+
from System.Threading.Tasks import Task
196+
197+
async def outer():
198+
async def inner():
199+
yield 1
200+
n = await Task.FromResult(10)
201+
yield n + 1
202+
async for v in inner():
203+
doubled = await Task.FromResult(v * 2)
204+
yield doubled
205+
206+
async def consume():
207+
out = []
208+
async for v in outer():
209+
out.append(v)
210+
return out
211+
212+
self.assertEqual(run_coro(consume()), [2, 22])
213+
214+
215+
def test_nested_inner_asend(self):
216+
"""Inner nested async gen driven with asend (sendSlot); outer re-yields the pairs."""
217+
async def outer():
218+
async def inner():
219+
a = yield 1
220+
b = yield ('a', a)
221+
yield ('b', b)
222+
g = inner()
223+
yield await g.asend(None) # -> 1
224+
yield await g.asend('X') # a == 'X' -> ('a', 'X')
225+
yield await g.asend('Y') # b == 'Y' -> ('b', 'Y')
226+
227+
async def consume():
228+
out = []
229+
async for v in outer():
230+
out.append(v)
231+
return out
232+
233+
self.assertEqual(run_coro(consume()), [1, ('a', 'X'), ('b', 'Y')])
234+
235+
236+
def test_nested_inner_athrow(self):
237+
"""athrow into the inner nested async gen (throwSlot); outer re-yields the recovery."""
238+
async def outer():
239+
async def inner():
240+
try:
241+
yield 1
242+
except ValueError as e:
243+
yield ('caught', str(e))
244+
g = inner()
245+
yield await g.asend(None) # -> 1
246+
yield await g.athrow(ValueError('boom')) # inner catches -> ('caught', 'boom')
247+
248+
async def consume():
249+
out = []
250+
async for v in outer():
251+
out.append(v)
252+
return out
253+
254+
self.assertEqual(run_coro(consume()), [1, ('caught', 'boom')])
255+
256+
257+
run_test(__name__)

0 commit comments

Comments
 (0)