From 66c451d2dc9466a761b0aaf57deb8773e7fc12e4 Mon Sep 17 00:00:00 2001 From: Alan Briolat Date: Tue, 15 Feb 2022 12:41:43 +0000 Subject: [PATCH] tests: extend capabilities of bot_helper.assert_sent() to allow more complex matching --- tests/conftest.py | 45 +++++++++++++++++++++++++++++++---- tests/test_irc.py | 38 ++++++----------------------- tests/test_plugin_last.py | 0 tests/test_plugin_linkinfo.py | 10 +++----- tests/test_plugin_xkcd.py | 4 +--- 5 files changed, 51 insertions(+), 46 deletions(-) create mode 100644 tests/test_plugin_last.py diff --git a/tests/conftest.py b/tests/conftest.py index 97907c96..7cfd91bc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -124,16 +124,51 @@ def receive(self, lines): lines = [lines] return [self.client.line_received(line) for line in lines] - def assert_sent(self, lines): + def assert_sent(self, matchers, *, any_order=False, reset_mock=True): """Check that a list of (unicode) strings have been sent. Resets the mock so the next call will not contain what was checked by this call. """ - if isinstance(lines, str): - lines = [lines] - self.client.send_line.assert_has_calls([mock.call(line) for line in lines]) - self.client.send_line.reset_mock() + sent_lines = [args[0] for name, args, kwargs in self.client.send_line.mock_calls] + + if callable(matchers) or isinstance(matchers, str): + matchers = [matchers] + matchers = [LineMatcher.equals(matcher) if not callable(matcher) else matcher + for matcher in matchers] + + if not matchers: + pass + elif any_order: + for matcher in matchers: + assert any(matcher(line) for line in sent_lines), f"sent line not found: {matcher}" + else: + # Find the start of the matching run of sent messages + start = 0 + while start < len(sent_lines) and not matchers[0](sent_lines[start]): + start += 1 + for i, matcher in enumerate(matchers): + assert start + i < len(sent_lines), f"no line matching {matcher} in {sent_lines}" + assert matcher(sent_lines[start + i]), f"expected {sent_lines[start + i]!r} to match {matcher}" + + if reset_mock: + self.client.send_line.reset_mock() + + +class LineMatcher: + def __init__(self, f, description): + self.f = f + self.description = description + + def __call__(self, line): + return self.f(line) + + def __repr__(self): + return self.description + + @classmethod + def equals(cls, other): + return cls(lambda line: line == other, f"`line == {other!r}`") @pytest.fixture diff --git a/tests/test_irc.py b/tests/test_irc.py index 950bbb04..9b88dbb9 100644 --- a/tests/test_irc.py +++ b/tests/test_irc.py @@ -193,29 +193,15 @@ async def test_client_PING(self, fast_forward, run_client): run_client.client.send_line.assert_not_called() # Advance time, test that a ping was sent await fast_forward(4) - assert run_client.client.send_line.mock_calls == [ - mock.call('PING 1'), - ] + run_client.assert_sent(['PING 1'], reset_mock=False) # Advance time again, test that the right number of pings was sent await fast_forward(12) - assert run_client.client.send_line.mock_calls == [ - mock.call('PING 1'), - mock.call('PING 2'), - mock.call('PING 3'), - mock.call('PING 4'), - mock.call('PING 5'), - ] + run_client.assert_sent(['PING 1', 'PING 2', 'PING 3', 'PING 4', 'PING 5'], reset_mock=False) # Disconnect, advance time, test that no more pings were sent run_client.client.disconnect() await run_client.client.disconnected.wait() await fast_forward(12) - assert run_client.client.send_line.mock_calls == [ - mock.call('PING 1'), - mock.call('PING 2'), - mock.call('PING 3'), - mock.call('PING 4'), - mock.call('PING 5'), - ] + run_client.assert_sent(['PING 1', 'PING 2', 'PING 3', 'PING 4', 'PING 5'], reset_mock=False) async def test_client_PING_only_when_needed(self, fast_forward, run_client): """Check that client PING commands are sent relative to the last received message.""" @@ -223,32 +209,22 @@ async def test_client_PING_only_when_needed(self, fast_forward, run_client): run_client.client.send_line.assert_not_called() # Advance time to just before the second PING, check that the first PING was sent await fast_forward(5) - assert run_client.client.send_line.mock_calls == [ - mock.call('PING 1'), - ] + run_client.assert_sent(['PING 1'], reset_mock=False) # Receive a message, this should reset the PING timer run_client.receive(':nick!user@host PRIVMSG #channel :foo') # Advance time to just after when the second PING would happen without any messages # received, check that still only one PING was sent await fast_forward(2) - assert run_client.client.send_line.mock_calls == [ - mock.call('PING 1'), - ] + run_client.assert_sent(['PING 1'], reset_mock=False) # Advance time to 4 seconds after the last message was received, and check that another # PING has now been sent await fast_forward(2) - assert run_client.client.send_line.mock_calls == [ - mock.call('PING 1'), - mock.call('PING 2'), - ] + run_client.assert_sent(['PING 1', 'PING 2'], reset_mock=False) # Disconnect, advance time, test that no more pings were sent run_client.client.disconnect() await run_client.client.disconnected.wait() await fast_forward(12) - assert run_client.client.send_line.mock_calls == [ - mock.call('PING 1'), - mock.call('PING 2'), - ] + run_client.assert_sent(['PING 1', 'PING 2'], reset_mock=False) def test_PING_PONG(irc_client_helper): diff --git a/tests/test_plugin_last.py b/tests/test_plugin_last.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_plugin_linkinfo.py b/tests/test_plugin_linkinfo.py index 94a256db..ecd789ee 100644 --- a/tests/test_plugin_linkinfo.py +++ b/tests/test_plugin_linkinfo.py @@ -249,9 +249,7 @@ async def handler(url, **kwargs): event.set() await asyncio.wait(futures, timeout=0.1) assert all(f.done() for f in futures) - bot_helper.client.send_line.assert_has_calls([ - mock.call('NOTICE #channel :foo'), - ]) + bot_helper.assert_sent('NOTICE #channel :foo') async def test_non_blocking_command(self, event_loop, bot_helper, aioresponses): bot_helper.reset_mock() @@ -281,7 +279,5 @@ async def handler(url, **kwargs): event.set() await asyncio.wait(futures, timeout=0.1) assert all(f.done() for f in futures) - bot_helper.client.send_line.assert_has_calls([ - mock.call('NOTICE #channel :Error: Content-Type not HTML-ish: ' - 'application/octet-stream (http://example.com/)'), - ]) + bot_helper.assert_sent('NOTICE #channel :Error: Content-Type not HTML-ish: ' + 'application/octet-stream (http://example.com/)') diff --git a/tests/test_plugin_xkcd.py b/tests/test_plugin_xkcd.py index e335b660..ff08adb8 100644 --- a/tests/test_plugin_xkcd.py +++ b/tests/test_plugin_xkcd.py @@ -173,9 +173,7 @@ async def test_command(self, bot_helper, num, url, content_type, fixture, expect _, title, alt = expected incoming = f":nick!user@host PRIVMSG #channel :!xkcd {num}" await asyncio.wait(bot_helper.receive(incoming)) - _, (outgoing,), _ = bot_helper.client.send_line.mock_calls[-1] - assert title in outgoing - assert alt in outgoing + bot_helper.assert_sent(lambda line: title in line and alt in line) @pytest.mark.usefixtures("populate_responses") async def test_integration_error(self, bot_helper):