Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add Task methods to make _asyncio more similar to cpython #8647

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
asyncio: Add asyncio tests for new task features.
imnotjames committed Nov 22, 2023
commit e0d3123138116ce315d92fbe4825854f8995df0d
54 changes: 54 additions & 0 deletions tests/extmod/asyncio_task_add_done_callback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Test the Task.add_done_callback() method

try:
import asyncio
except ImportError:
print("SKIP")
raise SystemExit


async def task(t, exc=None):
if t >= 0:
await asyncio.sleep(t)
if exc:
raise exc


def done_callback(t, er):
print("done", repr(t), repr(er))


async def main():
# Tasks that aren't done only execute done callback after finishing
print("=" * 10)
t = asyncio.create_task(task(-1))
t.add_done_callback(done_callback)
print("Waiting for task to complete")
await asyncio.sleep(0)
print("Task has completed")

# Task that are done run the callback immediately
print("=" * 10)
t = asyncio.create_task(task(-1))
await asyncio.sleep(0)
print("Task has completed")
t.add_done_callback(done_callback)
print("Callback Added")

# Task that starts, runs and finishes without an exception should return None
print("=" * 10)
t = asyncio.create_task(task(0.01))
t.add_done_callback(done_callback)
try:
t.add_done_callback(done_callback)
except RuntimeError as e:
print("Second call to add_done_callback emits error:", repr(e))

# Task that raises immediately should still run done callback
print("=" * 10)
t = asyncio.create_task(task(-1, ValueError))
t.add_done_callback(done_callback)
await asyncio.sleep(0)


asyncio.run(main())
12 changes: 12 additions & 0 deletions tests/extmod/asyncio_task_add_done_callback.py.exp
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
==========
Waiting for task to complete
done <Task> StopIteration()
Task has completed
==========
Task has completed
done <Task> StopIteration()
Callback Added
==========
Second call to add_done_callback emits error: RuntimeError('>1 callback unsupported',)
==========
done <Task> ValueError()
54 changes: 54 additions & 0 deletions tests/extmod/asyncio_task_cancelled.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Test the `Task.cancelled` method

try:
import asyncio
except ImportError:
print("SKIP")
raise SystemExit


async def task(t):
await asyncio.sleep(t)


async def main():
# Cancel task immediately doesn't mark the task as cancelled
print("=" * 10)
t = asyncio.create_task(task(2))
t.cancel()
print("Expecting task to not be cancelled because it is not done:", t.cancelled())

# Cancel task immediately and wait for cancellation to complete
print("=" * 10)
t = asyncio.create_task(task(2))
t.cancel()
await asyncio.sleep(0)
print("Expecting Task to be Cancelled:", t.cancelled())

# Cancel task and wait for cancellation to complete
print("=" * 10)
t = asyncio.create_task(task(2))
await asyncio.sleep(0.01)
t.cancel()
await asyncio.sleep(0)
print("Expecting Task to be Cancelled:", t.cancelled())

# Cancel task multiple times after it has started
print("=" * 10)
t = asyncio.create_task(task(2))
await asyncio.sleep(0.01)
for _ in range(4):
t.cancel()
await asyncio.sleep(0.01)

print("Expecting Task to be Cancelled:", t.cancelled())

# Cancel task after it has finished
print("=" * 10)
t = asyncio.create_task(task(0.01))
await asyncio.sleep(0.05)
t.cancel()
print("Expecting task to not be Cancelled:", t.cancelled())


asyncio.run(main())
10 changes: 10 additions & 0 deletions tests/extmod/asyncio_task_cancelled.py.exp
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
==========
Expecting task to not be cancelled because it is not done: False
==========
Expecting Task to be Cancelled: True
==========
Expecting Task to be Cancelled: True
==========
Expecting Task to be Cancelled: True
==========
Expecting task to not be Cancelled: False
64 changes: 64 additions & 0 deletions tests/extmod/asyncio_task_exception.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Test the Task.exception() method

try:
import asyncio
except ImportError:
print("SKIP")
raise SystemExit


async def task(t, exc=None):
if t >= 0:
await asyncio.sleep(t)
if exc:
raise exc


async def main():
# Task that is not done yet raises an InvalidStateError
print("=" * 10)
t = asyncio.create_task(task(1))
await asyncio.sleep(0)
try:
t.exception()
assert False, "Should not get here"
except Exception as e:
print("Tasks that aren't done yet raise an InvalidStateError:", repr(e))

# Task that is cancelled raises CancelledError
print("=" * 10)
t = asyncio.create_task(task(1))
t.cancel()
await asyncio.sleep(0)
try:
print(repr(t.exception()))
print(t.cancelled())
assert False, "Should not get here"
except asyncio.CancelledError as e:
print("Cancelled tasks cannot retrieve exception:", repr(e))

# Task that starts, runs and finishes without an exception should return None
print("=" * 10)
t = asyncio.create_task(task(0.01))
await t
print("None when no exception:", t.exception())

# Task that raises immediately should return that exception
print("=" * 10)
t = asyncio.create_task(task(-1, ValueError))
try:
await t
assert False, "Should not get here"
except ValueError as e:
pass
print("Returned Exception:", repr(t.exception()))

# Task returns `none` when somehow an exception isn't in data
print("=" * 10)
t = asyncio.create_task(task(-1))
await t
t.data = "Example"
print(t.exception())


asyncio.run(main())
10 changes: 10 additions & 0 deletions tests/extmod/asyncio_task_exception.py.exp
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
==========
Tasks that aren't done yet raise an InvalidStateError: InvalidStateError()
==========
Cancelled tasks cannot retrieve exception: CancelledError()
==========
None when no exception: None
==========
Returned Exception: ValueError()
==========
None
28 changes: 28 additions & 0 deletions tests/extmod/asyncio_task_get_coro.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Test the `Task.get_coro()` method

try:
import asyncio
except ImportError:
print("SKIP")
raise SystemExit


async def action():
pass


async def main():
# Check that the coro we include is the same coro we get back
print("=" * 10)

coro = action()
t = asyncio.create_task(coro)
print(t.get_coro() == coro)

# Check that the coro prop matches the get_coro() result
print("=" * 10)
t = asyncio.create_task(action())
print(t.get_coro() == t.coro)


asyncio.run(main())
4 changes: 4 additions & 0 deletions tests/extmod/asyncio_task_get_coro.py.exp
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
==========
True
==========
True
39 changes: 39 additions & 0 deletions tests/extmod/asyncio_task_hash.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Test hash unary operator for a Task

try:
import asyncio
except ImportError:
print("SKIP")
raise SystemExit


async def task():
pass


async def main():
# Confirm that the hash is an int
print("=" * 10)
t1 = asyncio.create_task(task())
t2 = asyncio.create_task(task())
print(type(hash(t2)))
print(type(hash(t1)))

# Check that two tasks don't have the same hash
print("=" * 10)
t1 = asyncio.create_task(task())
t2 = asyncio.create_task(task())
print(hash(t1) != hash(t2))

# Add tasks to a set
print("=" * 10)
t1 = asyncio.create_task(task())
t2 = asyncio.create_task(task())

tasks = set()
tasks.add(t1)
print(t1 in tasks)
print(t2 in tasks)


asyncio.run(main())
8 changes: 8 additions & 0 deletions tests/extmod/asyncio_task_hash.py.exp
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
==========
<class 'int'>
<class 'int'>
==========
True
==========
True
False
59 changes: 59 additions & 0 deletions tests/extmod/asyncio_task_remove_done_callback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Test the Task.remove_done_callback() method

try:
import asyncio
except ImportError:
print("SKIP")
raise SystemExit


async def task(t, exc=None):
if t >= 0:
await asyncio.sleep(t)
if exc:
raise exc


def done_callback():
print("done")


def done_callback_2():
print("done 2")


async def main():
# Removing a callback returns 0 when no callbacks have been set
print("=" * 10)
t = asyncio.create_task(task(1))
print("Returns 0 when no done callback has been set:", t.remove_done_callback(done_callback))

# Done callback removal only works once
print("=" * 10)
t = asyncio.create_task(task(1))
t.add_done_callback(done_callback)
print(
"Returns 1 when a callback matches and is removed:", t.remove_done_callback(done_callback)
)
print(
"Returns 0 on second attempt to remove the callback:",
t.remove_done_callback(done_callback),
)

# Only removes done callback when match
print("=" * 10)
t = asyncio.create_task(task(0.01))
t.add_done_callback(done_callback)
print("Returns 0 when done callbacks don't match:", t.remove_done_callback(done_callback_2))

# A removed done callback does not execute
print("=" * 10)
t = asyncio.create_task(task(-1))
t.add_done_callback(done_callback)
t.remove_done_callback(done_callback)
print("Waiting for task to complete")
await t
print("Task completed")


asyncio.run(main())
10 changes: 10 additions & 0 deletions tests/extmod/asyncio_task_remove_done_callback.py.exp
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
==========
Returns 0 when no done callback has been set: 0
==========
Returns 1 when a callback matches and is removed: 1
Returns 0 on second attempt to remove the callback: 0
==========
Returns 0 when done callbacks don't match: 0
==========
Waiting for task to complete
Task completed
Loading