mirror of
https://github.com/panda3d/panda3d.git
synced 2025-10-04 10:54:24 -04:00

This is modelled after `asyncio.shield()` and can be used to protect an inner future from cancellation when the outer future is cancelled.
441 lines
11 KiB
Python
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()
|
|
|