模拟Asyncio事件循环的内部时钟

时间:2018-12-18 08:55:24

标签: python-3.x python-asyncio

我们在实时消息驱动的环境中将asyncio.get_event_loop()用作调度程序。

出于(回退)测试的目的,我们重播带有时间戳的消息,并将其时间戳视为模拟/合成/模拟时间。 仅当我们替换事件循环的内部时钟时,此方法才有效。我们该怎么做?

请注意,Python 3.6.6,BaseEventLoop实现了

def time(self):
    return time.monotonic()

我们希望拥有

def time(self):
    return current_message.timestamp

可能的解决方案,都有缺陷:

  1. 子类AbstractEventLoop? ->并非如此,因为AbstractEventLoop非常裸露。
  2. 子类BaseEventLoop? ->文档状态:不应直接使用;改用AbstractEventLoop。
  3. 猴子补丁asyncio.get_event_loop()。time()吗? ->可能有效,但随时可能会中断。
  4. 猴子补丁time.monotonic()?比(3)还差。
  5. 尝试了解asyncio.test_utils.TestLoop吗? ->内部,未记录。
  6. 完全踢异步?

相关:Unit-testing a periodic coroutine with mock time

注意:sched.scheduler()可以注入一个合成时间(!),但它确实会阻止睡眠。

2 个答案:

答案 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 而不是离开 你的测试挂起。

与 pytest-asyncio 集成

async-solipsism 和 pytest-asyncio 相得益彰:只需编写一个 在您的测试文件或 event_loop 中自定义 conftest.py 固定装置,它将 覆盖 pytest-asyncio 提供的默认值:

@pytest.fixture
def event_loop():
    loop = async_solipsism.EventLoop()
    yield loop
    loop.close()

或者,您可以使用 asynctestClockedTestCase 吗? (请参阅 tutorial。)该框架(不幸的是)需要从该基类派生,这可能不是您的选择。 更新:我最近还了解到 aiotools' VirtualClock,这可能会有所帮助。