我们在实时消息驱动的环境中将asyncio.get_event_loop()
用作调度程序。
出于(回退)测试的目的,我们重播带有时间戳的消息,并将其时间戳视为模拟/合成/模拟时间。 仅当我们替换事件循环的内部时钟时,此方法才有效。我们该怎么做?
请注意,Python 3.6.6,BaseEventLoop实现了
def time(self):
return time.monotonic()
我们希望拥有
def time(self):
return current_message.timestamp
可能的解决方案,都有缺陷:
相关:Unit-testing a periodic coroutine with mock time
注意:sched.scheduler()
可以注入一个合成时间(!),但它确实会阻止睡眠。
答案 0 :(得分:0)
这是对我的问题的否回答。我试图猴子修补BaseEventLoop.time,请参见下面的代码。但是结果不是我们所想到的。 some_callback
在时间2运行,而不是在时间1.5:
0: Message(timestamp=0, msg='Beautiful is better than ugly.')
1: Message(timestamp=1, msg='Explicit is better than implicit.')
2: some_callback
2: Message(timestamp=2, msg='Simple is better than complex.')
3: Message(timestamp=3, msg='Complex is better than complicated.')
可能我们必须弄乱asyncio.sleep
才能解决,但这太怪异了。到目前为止的结论:使用asyncio无法做到。这是代码:
import asyncio
from typing import Iterator
import dataclasses
@dataclasses.dataclass
class Message:
timestamp: float
msg: str
def some_callback():
loop = asyncio.get_event_loop()
print(f'{loop.time()}: some_callback')
def zen_generator() -> Iterator[Message]:
from this import d, s
lines = ''.join([d.get(c, c) for c in s]).splitlines()[2:6]
for timestamp, msg in enumerate(lines):
yield Message(timestamp, msg)
def loop_pulse(head_message: Message, tail_messages: Iterator[Message]):
"""Self-scheduling callback driving the loop's internal clock"""
loop = asyncio.get_event_loop()
print(f'{loop.time()}: {head_message}')
try:
head_message = next(tail_messages)
except StopIteration:
loop.stop()
return
# Monkey-patch
loop.time = lambda: head_message.timestamp
loop.call_at(head_message.timestamp, lambda: loop_pulse(head_message, tail_messages))
def main():
messages = zen_generator()
head_message = next(messages)
loop = asyncio.get_event_loop()
loop.time = lambda: head_message.timestamp
loop_pulse(head_message, messages)
loop.call_at(loop.time() + .5, some_callback)
loop.run_forever()
main()
答案 1 :(得分:0)
不幸的是——即使在撰写本文时——asyncio
显然缺乏像 Twisted has 这样的测试中允许确定性改进时钟的适当抽象。
替换 loop
本身怎么样? async-solipsism
是否适用于您的用例? (它有一些 limitations 在您的世界中可能没有意义。)
来自README:
<块引用>一个非常方便的功能是时间跑得无限快!更何况时间 仅在显式等待时前进。例如,此代码将打印出 两次正好相隔 60 秒,并且将花费可以忽略不计的实时时间 运行:
print(loop.time())
await asyncio.sleep(60)
print(loop.time())
这也提供了一种方便的方法来确保所有挂起的回调都有一个 跑的机会:睡一会。
模拟时钟具有微秒分辨率,独立于任何 系统时钟的分辨率。这有助于确保测试的行为相同 跨操作系统。
有时有问题的代码或有问题的测试会等待一个永远不会发生的事件
发生。例如,它可能会等待数据到达套接字,但忘记了
将数据插入另一端。如果 async-solipsism 检测到它会
永远不会再次醒来,它会引发 SleepForeverError
而不是离开
你的测试挂起。
…
async-solipsism 和 pytest-asyncio 相得益彰:只需编写一个
在您的测试文件或 event_loop
中自定义 conftest.py
固定装置,它将
覆盖 pytest-asyncio 提供的默认值:
@pytest.fixture
def event_loop():
loop = async_solipsism.EventLoop()
yield loop
loop.close()
或者,您可以使用 asynctest
的 ClockedTestCase
吗? (请参阅 tutorial。)该框架(不幸的是)需要从该基类派生,这可能不是您的选择。 更新:我最近还了解到 aiotools
' VirtualClock
,这可能会有所帮助。