panda3d/tests/event/test_futures.py
rdb cdf5b16ddd task: Add AsyncFuture::shield() ability, part of #1136
This is modelled after `asyncio.shield()` and can be used to protect an inner future from cancellation when the outer future is cancelled.
2021-04-09 18:28:07 +02:00

441 lines
11 KiB
Python

from panda3d import core
import pytest
import time
import sys
if sys.version_info >= (3, 8):
from asyncio.exceptions import TimeoutError, CancelledError
else:
from concurrent.futures._base import TimeoutError, CancelledError
def test_future_cancelled():
fut = core.AsyncFuture()
assert not fut.done()
assert not fut.cancelled()
fut.cancel()
assert fut.done()
assert fut.cancelled()
with pytest.raises(CancelledError):
fut.result()
# Works more than once
with pytest.raises(CancelledError):
fut.result()
def test_future_timeout():
fut = core.AsyncFuture()
with pytest.raises(TimeoutError):
fut.result(0.001)
# Works more than once
with pytest.raises(TimeoutError):
fut.result(0.001)
@pytest.mark.skipif(not core.Thread.is_threading_supported(),
reason="Threading support disabled")
def test_future_wait():
threading = pytest.importorskip("direct.stdpy.threading")
fut = core.AsyncFuture()
# Launch a thread to set the result value.
def thread_main():
time.sleep(0.001)
fut.set_result(None)
thread = threading.Thread(target=thread_main)
assert not fut.done()
thread.start()
assert fut.result() is None
assert fut.done()
assert not fut.cancelled()
assert fut.result() is None
@pytest.mark.skipif(not core.Thread.is_threading_supported(),
reason="Threading support disabled")
def test_future_wait_cancel():
threading = pytest.importorskip("direct.stdpy.threading")
fut = core.AsyncFuture()
# Launch a thread to cancel the future.
def thread_main():
time.sleep(0.001)
fut.cancel()
thread = threading.Thread(target=thread_main)
assert not fut.done()
thread.start()
with pytest.raises(CancelledError):
fut.result()
assert fut.done()
assert fut.cancelled()
with pytest.raises(CancelledError):
fut.result()
def test_task_cancel():
task_mgr = core.AsyncTaskManager.get_global_ptr()
task = core.PythonTask(lambda task: task.done)
task_mgr.add(task)
assert not task.done()
task_mgr.remove(task)
assert task.done()
assert task.cancelled()
with pytest.raises(CancelledError):
task.result()
def test_task_cancel_during_run():
task_mgr = core.AsyncTaskManager.get_global_ptr()
task_chain = task_mgr.make_task_chain("test_task_cancel_during_run")
def task_main(task):
task.remove()
# It won't yet be marked done until after it returns.
assert not task.done()
return task.done
task = core.PythonTask(task_main)
task.set_task_chain(task_chain.name)
task_mgr.add(task)
task_chain.wait_for_tasks()
assert task.done()
assert task.cancelled()
with pytest.raises(CancelledError):
task.result()
def test_task_result():
task_mgr = core.AsyncTaskManager.get_global_ptr()
task_chain = task_mgr.make_task_chain("test_task_result")
def task_main(task):
task.set_result(42)
# It won't yet be marked done until after it returns.
assert not task.done()
return core.PythonTask.done
task = core.PythonTask(task_main)
task.set_task_chain(task_chain.name)
task_mgr.add(task)
task_chain.wait_for_tasks()
assert task.done()
assert not task.cancelled()
assert task.result() == 42
def test_coro_exception():
task_mgr = core.AsyncTaskManager.get_global_ptr()
task_chain = task_mgr.make_task_chain("test_coro_exception")
def coro_main():
raise RuntimeError
yield None
task = core.PythonTask(coro_main())
task.set_task_chain(task_chain.name)
task_mgr.add(task)
task_chain.wait_for_tasks()
assert task.done()
assert not task.cancelled()
with pytest.raises(RuntimeError):
task.result()
def test_future_result():
# Cancelled
fut = core.AsyncFuture()
assert not fut.done()
fut.cancel()
with pytest.raises(CancelledError):
fut.result()
# None
fut = core.AsyncFuture()
fut.set_result(None)
assert fut.done()
assert fut.result() is None
# Store int
fut = core.AsyncFuture()
fut.set_result(123)
assert fut.result() == 123
# Store string
fut = core.AsyncFuture()
fut.set_result("test\000\u1234")
assert fut.result() == "test\000\u1234"
# Store TypedWritableReferenceCount
tex = core.Texture()
rc = tex.get_ref_count()
fut = core.AsyncFuture()
fut.set_result(tex)
assert tex.get_ref_count() == rc + 1
assert fut.result() == tex
assert tex.get_ref_count() == rc + 1
assert fut.result() == tex
assert tex.get_ref_count() == rc + 1
fut = None
assert tex.get_ref_count() == rc
# Store EventParameter (no longer gets unwrapped)
ep = core.EventParameter(0.5)
fut = core.AsyncFuture()
fut.set_result(ep)
assert fut.result() is ep
assert fut.result() is ep
# Store TypedObject
dg = core.Datagram(b"test")
fut = core.AsyncFuture()
fut.set_result(dg)
assert fut.result() == dg
assert fut.result() == dg
# Store arbitrary Python object
obj = object()
rc = sys.getrefcount(obj)
fut = core.AsyncFuture()
fut.set_result(obj)
assert sys.getrefcount(obj) == rc + 1
assert fut.result() is obj
assert sys.getrefcount(obj) == rc + 1
assert fut.result() is obj
assert sys.getrefcount(obj) == rc + 1
fut = None
assert sys.getrefcount(obj) == rc
def test_future_gather():
fut1 = core.AsyncFuture()
fut2 = core.AsyncFuture()
# 0 and 1 arguments are special
assert core.AsyncFuture.gather().done()
assert core.AsyncFuture.gather(fut1) == fut1
# Gathering not-done futures
gather = core.AsyncFuture.gather(fut1, fut2)
assert not gather.done()
# One future done
fut1.set_result(1)
assert not gather.done()
# Two futures done
fut2.set_result(2)
assert gather.done()
assert not gather.cancelled()
assert tuple(gather.result()) == (1, 2)
def test_future_gather_cancel_inner():
fut1 = core.AsyncFuture()
fut2 = core.AsyncFuture()
# Gathering not-done futures
gather = core.AsyncFuture.gather(fut1, fut2)
assert not gather.done()
# One future cancelled
fut1.cancel()
assert not gather.done()
# Two futures cancelled
fut2.set_result(2)
assert gather.done()
assert not gather.cancelled()
with pytest.raises(CancelledError):
assert gather.result()
def test_future_gather_cancel_outer():
fut1 = core.AsyncFuture()
fut2 = core.AsyncFuture()
# Gathering not-done futures
gather = core.AsyncFuture.gather(fut1, fut2)
assert not gather.done()
assert gather.cancel()
assert gather.done()
assert gather.cancelled()
with pytest.raises(CancelledError):
assert gather.result()
def test_future_shield():
# An already done future is returned as-is (no cancellation can occur)
inner = core.AsyncFuture()
inner.set_result(None)
outer = core.AsyncFuture.shield(inner)
assert inner == outer
# Normally finishing future
inner = core.AsyncFuture()
outer = core.AsyncFuture.shield(inner)
assert not outer.done()
inner.set_result(None)
assert outer.done()
assert not outer.cancelled()
assert inner.result() is None
# Normally finishing future with result
inner = core.AsyncFuture()
outer = core.AsyncFuture.shield(inner)
assert not outer.done()
inner.set_result(123)
assert outer.done()
assert not outer.cancelled()
assert inner.result() == 123
# Cancelled inner future does propagate cancellation outward
inner = core.AsyncFuture()
outer = core.AsyncFuture.shield(inner)
assert not outer.done()
inner.cancel()
assert outer.done()
assert outer.cancelled()
# Finished outer future does nothing to inner
inner = core.AsyncFuture()
outer = core.AsyncFuture.shield(inner)
outer.set_result(None)
assert not inner.done()
inner.cancel()
assert not outer.cancelled()
# Cancelled outer future does nothing to inner
inner = core.AsyncFuture()
outer = core.AsyncFuture.shield(inner)
outer.cancel()
assert not inner.done()
inner.cancel()
# Can be shielded multiple times
inner = core.AsyncFuture()
outer1 = core.AsyncFuture.shield(inner)
outer2 = core.AsyncFuture.shield(inner)
outer1.cancel()
assert not inner.done()
assert not outer2.done()
inner.cancel()
assert outer1.done()
assert outer2.done()
def test_future_done_callback():
fut = core.AsyncFuture()
# Use the list hack since Python 2 doesn't have the "nonlocal" keyword.
called = [False]
def on_done(arg):
assert arg == fut
called[0] = True
fut.add_done_callback(on_done)
fut.cancel()
assert fut.done()
task_mgr = core.AsyncTaskManager.get_global_ptr()
task_mgr.poll()
assert called[0]
def test_future_done_callback_already_done():
# Same as above, but with the future already done when add_done_callback
# is called.
fut = core.AsyncFuture()
fut.cancel()
assert fut.done()
# Use the list hack since Python 2 doesn't have the "nonlocal" keyword.
called = [False]
def on_done(arg):
assert arg == fut
called[0] = True
fut.add_done_callback(on_done)
task_mgr = core.AsyncTaskManager.get_global_ptr()
task_mgr.poll()
assert called[0]
def test_event_future():
queue = core.EventQueue()
handler = core.EventHandler(queue)
fut = handler.get_future("test")
# If we ask again, we should get the same one.
assert handler.get_future("test") == fut
event = core.Event("test")
handler.dispatch_event(event)
assert fut.done()
assert not fut.cancelled()
assert fut.result() == event
def test_event_future_cancel():
# This is a very strange thing to do, but it's possible, so let's make
# sure it gives defined behavior.
queue = core.EventQueue()
handler = core.EventHandler(queue)
fut = handler.get_future("test")
fut.cancel()
assert fut.done()
assert fut.cancelled()
event = core.Event("test")
handler.dispatch_event(event)
assert fut.done()
assert fut.cancelled()
def test_event_future_cancel2():
queue = core.EventQueue()
handler = core.EventHandler(queue)
# Make sure we get a new future if we cancelled the first one.
fut = handler.get_future("test")
fut.cancel()
fut2 = handler.get_future("test")
assert fut != fut2
assert fut.done()
assert fut.cancelled()
assert not fut2.done()
assert not fut2.cancelled()