From adf63e65a0b861cb02a6e8cc1e5b2c2c09e57726 Mon Sep 17 00:00:00 2001 From: Akarys42 Date: Sun, 9 Feb 2020 17:05:09 +0100 Subject: Create an AsyncContextManagerMock mock for testing asynchronous context managers It can be used to test aiohttp request functions, since they are async context managers --- tests/helpers.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) (limited to 'tests') diff --git a/tests/helpers.py b/tests/helpers.py index 5df796c23..6aee8623f 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -127,6 +127,18 @@ class AsyncMock(CustomMockMixin, unittest.mock.MagicMock): return super().__call__(*args, **kwargs) +class AsyncContextManagerMock(unittest.mock.MagicMock): + def __init__(self, return_value: Any): + super().__init__() + self._return_value = return_value + + async def __aenter__(self): + return self._return_value + + async def __aexit__(self, *args): + pass + + class AsyncIteratorMock: """ A class to mock asynchronous iterators. -- cgit v1.2.3 From 8c8f8dd50f376c3398c04e7bbe76b5028b69ff83 Mon Sep 17 00:00:00 2001 From: Akarys42 Date: Sun, 9 Feb 2020 17:11:47 +0100 Subject: Write tests for bot/cogs/test_snekbox.py --- tests/bot/cogs/test_snekbox.py | 363 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 363 insertions(+) create mode 100644 tests/bot/cogs/test_snekbox.py (limited to 'tests') diff --git a/tests/bot/cogs/test_snekbox.py b/tests/bot/cogs/test_snekbox.py new file mode 100644 index 000000000..293efed0f --- /dev/null +++ b/tests/bot/cogs/test_snekbox.py @@ -0,0 +1,363 @@ +import asyncio +import logging +import unittest +from unittest.mock import MagicMock, Mock, call, patch + +from bot.cogs import snekbox +from bot.cogs.snekbox import Snekbox +from bot.constants import URLs +from tests.helpers import ( + AsyncContextManagerMock, AsyncMock, MockBot, MockContext, MockMessage, MockReaction, MockUser, async_test +) + + +class SnekboxTests(unittest.TestCase): + def setUp(self): + """Add mocked bot and cog to the instance.""" + self.bot = MockBot() + + self.mocked_post = MagicMock() + self.mocked_post.json = AsyncMock() + self.bot.http_session.post = MagicMock(return_value=AsyncContextManagerMock(self.mocked_post)) + + self.cog = Snekbox(bot=self.bot) + + @async_test + async def test_post_eval(self): + """Post the eval code to the URLs.snekbox_eval_api endpoint.""" + await self.cog.post_eval("import random") + self.bot.http_session.post.assert_called_once_with( + URLs.snekbox_eval_api, + json={"input": "import random"}, + raise_for_status=True + ) + + @async_test + async def test_upload_output_reject_too_long(self): + """Reject output longer than MAX_PASTE_LEN.""" + self.assertEqual(await self.cog.upload_output("-" * (snekbox.MAX_PASTE_LEN + 1)), "too long to upload") + + @async_test + async def test_upload_output(self): + """Upload the eval output to the URLs.paste_service.format(key="documents") endpoint.""" + key = "RainbowDash" + self.mocked_post.json.return_value = {"key": key} + + self.assertEqual( + await self.cog.upload_output("My awesome output"), + URLs.paste_service.format(key=key) + ) + self.bot.http_session.post.assert_called_once_with( + URLs.paste_service.format(key="documents"), + data="My awesome output", + raise_for_status=True + ) + + @async_test + async def test_upload_output_gracefully_fallback_if_exception_during_request(self): + """Output upload gracefully fallback if the upload fail.""" + self.mocked_post.json.side_effect = Exception + log = logging.getLogger("bot.cogs.snekbox") + with self.assertLogs(logger=log, level='ERROR'): + await self.cog.upload_output('My awesome output!') + + @async_test + async def test_upload_output_gracefully_fallback_if_no_key_in_response(self): + """Output upload gracefully fallback if there is no key entry in the response body.""" + self.mocked_post.json.return_value = {} + self.assertEqual((await self.cog.upload_output('My awesome output!')), None) + + def test_prepare_input(self): + cases = ( + ('print("Hello world!")', 'print("Hello world!")', 'non-formatted'), + ('`print("Hello world!")`', 'print("Hello world!")', 'one line code block'), + ('```\nprint("Hello world!")```', 'print("Hello world!")', 'multiline code block'), + ('```py\nprint("Hello world!")```', 'print("Hello world!")', 'multiline python code block'), + ) + for case, expected, testname in cases: + with self.subTest(msg=f'Extract code from {testname}.', case=case, expected=expected): + self.assertEqual(self.cog.prepare_input(case), expected) + + def test_get_results_message(self): + """Return error and message according to the eval result.""" + cases = ( + ('ERROR', None, ('Your eval job has failed', 'ERROR')), + ('', 128 + snekbox.SIGKILL, ('Your eval job timed out or ran out of memory', '')), + ('', 255, ('Your eval job has failed', 'A fatal NsJail error occurred')) + ) + for stdout, returncode, expected in cases: + with self.subTest(stdout=stdout, returncode=returncode, expected=expected): + self.assertEqual(self.cog.get_results_message({'stdout': stdout, 'returncode': returncode}), expected) + + @patch('bot.cogs.snekbox.Signals', side_effect=ValueError) + def test_get_results_message_invalid_signal(self, mock_Signals: Mock): + self.assertEqual( + self.cog.get_results_message({'stdout': '', 'returncode': 127}), + ('Your eval job has completed with return code 127', '') + ) + + @patch('bot.cogs.snekbox.Signals') + def test_get_results_message_valid_signal(self, mock_Signals: Mock): + mock_Signals.return_value.name = 'SIGTEST' + self.assertEqual( + self.cog.get_results_message({'stdout': '', 'returncode': 127}), + ('Your eval job has completed with return code 127 (SIGTEST)', '') + ) + + def test_get_status_emoji(self): + """Return emoji according to the eval result.""" + cases = ( + ('', -1, ':warning:'), + ('Hello world!', 0, ':white_check_mark:'), + ('Invalid beard size', -1, ':x:') + ) + for stdout, returncode, expected in cases: + with self.subTest(stdout=stdout, returncode=returncode, expected=expected): + self.assertEqual(self.cog.get_status_emoji({'stdout': stdout, 'returncode': returncode}), expected) + + @async_test + async def test_format_output(self): + """Test output formatting.""" + self.cog.upload_output = AsyncMock(return_value='https://testificate.com/') + + too_many_lines = ( + '001 | v\n002 | e\n003 | r\n004 | y\n005 | l\n006 | o\n' + '007 | n\n008 | g\n009 | b\n010 | e\n011 | a\n... (truncated - too many lines)' + ) + too_long_too_many_lines = ( + "\n".join( + f"{i:03d} | {line}" for i, line in enumerate(['verylongbeard' * 10] * 15, 1) + )[:1000] + "\n... (truncated - too long, too many lines)" + ) + + cases = ( + ('', ('[No output]', None), 'No output'), + ('My awesome output', ('My awesome output', None), 'One line output'), + ('<@', ("<@\u200B", None), r'Convert <@ to <@\u200B'), + (' Date: Tue, 18 Feb 2020 18:23:38 +0100 Subject: Split assertion onto separate lines Co-Authored-By: Mark --- tests/bot/cogs/test_snekbox.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/bot/cogs/test_snekbox.py b/tests/bot/cogs/test_snekbox.py index 293efed0f..b5190cd7f 100644 --- a/tests/bot/cogs/test_snekbox.py +++ b/tests/bot/cogs/test_snekbox.py @@ -35,7 +35,8 @@ class SnekboxTests(unittest.TestCase): @async_test async def test_upload_output_reject_too_long(self): """Reject output longer than MAX_PASTE_LEN.""" - self.assertEqual(await self.cog.upload_output("-" * (snekbox.MAX_PASTE_LEN + 1)), "too long to upload") + result = await self.cog.upload_output("-" * (snekbox.MAX_PASTE_LEN + 1)) + self.assertEqual(result, "too long to upload") @async_test async def test_upload_output(self): -- cgit v1.2.3 From 2974d489494370f364c44899a529f5e93c89bedc Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Tue, 18 Feb 2020 18:25:55 +0100 Subject: Split assertions onto separate lines Reads better as separate lines Co-Authored-By: Mark --- tests/bot/cogs/test_snekbox.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/test_snekbox.py b/tests/bot/cogs/test_snekbox.py index b5190cd7f..01525110d 100644 --- a/tests/bot/cogs/test_snekbox.py +++ b/tests/bot/cogs/test_snekbox.py @@ -88,7 +88,8 @@ class SnekboxTests(unittest.TestCase): ) for stdout, returncode, expected in cases: with self.subTest(stdout=stdout, returncode=returncode, expected=expected): - self.assertEqual(self.cog.get_results_message({'stdout': stdout, 'returncode': returncode}), expected) + actual = self.cog.get_results_message({'stdout': stdout, 'returncode': returncode}) + self.assertEqual(actual, expected) @patch('bot.cogs.snekbox.Signals', side_effect=ValueError) def test_get_results_message_invalid_signal(self, mock_Signals: Mock): @@ -114,7 +115,8 @@ class SnekboxTests(unittest.TestCase): ) for stdout, returncode, expected in cases: with self.subTest(stdout=stdout, returncode=returncode, expected=expected): - self.assertEqual(self.cog.get_status_emoji({'stdout': stdout, 'returncode': returncode}), expected) + actual = self.cog.get_status_emoji({'stdout': stdout, 'returncode': returncode}) + self.assertEqual(actual, expected) @async_test async def test_format_output(self): @@ -321,7 +323,8 @@ class SnekboxTests(unittest.TestCase): with self.subTest(msg=f'Messages with {testname} return {expected}'): ctx = MockContext() ctx.message = ctx_msg - self.assertEqual(snekbox.predicate_eval_message_edit(ctx, ctx_msg, new_msg), expected) + actual = snekbox.predicate_eval_message_edit(ctx, ctx_msg, new_msg) + self.assertEqual(actual, expected) def test_predicate_eval_emoji_reaction(self): """Test the predicate_eval_emoji_reaction function.""" @@ -351,7 +354,8 @@ class SnekboxTests(unittest.TestCase): ) for reaction, user, expected, testname in cases: with self.subTest(msg=f'Test with {testname} and expected return {expected}'): - self.assertEqual(snekbox.predicate_eval_emoji_reaction(valid_ctx, reaction, user), expected) + actual = snekbox.predicate_eval_emoji_reaction(valid_ctx, reaction, user) + self.assertEqual(actual, expected) class SnekboxSetupTests(unittest.TestCase): -- cgit v1.2.3 From dd9c250253a7bb24751f2de274dfa168efafb717 Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Tue, 18 Feb 2020 18:29:22 +0100 Subject: Delete additional informations from subtest Reduce visual clutter Co-Authored-By: Mark --- tests/bot/cogs/test_snekbox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/bot/cogs/test_snekbox.py b/tests/bot/cogs/test_snekbox.py index 01525110d..f1f03ab2f 100644 --- a/tests/bot/cogs/test_snekbox.py +++ b/tests/bot/cogs/test_snekbox.py @@ -76,7 +76,7 @@ class SnekboxTests(unittest.TestCase): ('```py\nprint("Hello world!")```', 'print("Hello world!")', 'multiline python code block'), ) for case, expected, testname in cases: - with self.subTest(msg=f'Extract code from {testname}.', case=case, expected=expected): + with self.subTest(msg=f'Extract code from {testname}.'): self.assertEqual(self.cog.prepare_input(case), expected) def test_get_results_message(self): -- cgit v1.2.3 From b5a1bf7c6ca467cef40a429cc8ca02314526801c Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Tue, 18 Feb 2020 18:30:39 +0100 Subject: Use a space instead of an empty string in test_get_status_emoji Because of the stripping, it should still be considered as empty Co-Authored-By: Mark --- tests/bot/cogs/test_snekbox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/bot/cogs/test_snekbox.py b/tests/bot/cogs/test_snekbox.py index f1f03ab2f..94ff685c4 100644 --- a/tests/bot/cogs/test_snekbox.py +++ b/tests/bot/cogs/test_snekbox.py @@ -109,7 +109,7 @@ class SnekboxTests(unittest.TestCase): def test_get_status_emoji(self): """Return emoji according to the eval result.""" cases = ( - ('', -1, ':warning:'), + (' ', -1, ':warning:'), ('Hello world!', 0, ':white_check_mark:'), ('Invalid beard size', -1, ':x:') ) -- cgit v1.2.3 From c1a998ca246e153333438ff91bce200bf74cc0f5 Mon Sep 17 00:00:00 2001 From: Matteo Date: Tue, 18 Feb 2020 18:38:24 +0100 Subject: Assert return value of Snekbox.post_eval --- tests/bot/cogs/test_snekbox.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/bot/cogs/test_snekbox.py b/tests/bot/cogs/test_snekbox.py index 94ff685c4..cf07aad71 100644 --- a/tests/bot/cogs/test_snekbox.py +++ b/tests/bot/cogs/test_snekbox.py @@ -25,7 +25,9 @@ class SnekboxTests(unittest.TestCase): @async_test async def test_post_eval(self): """Post the eval code to the URLs.snekbox_eval_api endpoint.""" - await self.cog.post_eval("import random") + self.mocked_post.json.return_value = {'lemon': 'AI'} + + self.assertEqual(await self.cog.post_eval("import random"), {'lemon': 'AI'}) self.bot.http_session.post.assert_called_once_with( URLs.snekbox_eval_api, json={"input": "import random"}, -- cgit v1.2.3 From 12f43fc09406dee8cb1b36757fc2af7ae799a9d5 Mon Sep 17 00:00:00 2001 From: Matteo Date: Tue, 18 Feb 2020 18:44:00 +0100 Subject: Use kwargs to set mock attributes --- tests/bot/cogs/test_snekbox.py | 36 +++++++++++------------------------- 1 file changed, 11 insertions(+), 25 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/test_snekbox.py b/tests/bot/cogs/test_snekbox.py index cf07aad71..112c923c8 100644 --- a/tests/bot/cogs/test_snekbox.py +++ b/tests/bot/cogs/test_snekbox.py @@ -306,15 +306,9 @@ class SnekboxTests(unittest.TestCase): def test_predicate_eval_message_edit(self): """Test the predicate_eval_message_edit function.""" - msg0 = MockMessage() - msg0.id = 1 - msg0.content = 'abc' - msg1 = MockMessage() - msg1.id = 2 - msg1.content = 'abcdef' - msg2 = MockMessage() - msg2.id = 1 - msg2.content = 'abcdef' + msg0 = MockMessage(id=1, content='abc') + msg1 = MockMessage(id=2, content='abcdef') + msg2 = MockMessage(id=1, content='abcdef') cases = ( (msg0, msg0, False, 'same ID, same content'), @@ -323,29 +317,21 @@ class SnekboxTests(unittest.TestCase): ) for ctx_msg, new_msg, expected, testname in cases: with self.subTest(msg=f'Messages with {testname} return {expected}'): - ctx = MockContext() - ctx.message = ctx_msg + ctx = MockContext(message=ctx_msg) actual = snekbox.predicate_eval_message_edit(ctx, ctx_msg, new_msg) self.assertEqual(actual, expected) def test_predicate_eval_emoji_reaction(self): """Test the predicate_eval_emoji_reaction function.""" - valid_reaction = MockReaction() - valid_reaction.message.id = 1 + valid_reaction = MockReaction(message=MockMessage(id=1)) valid_reaction.__str__.return_value = '🔁' - valid_ctx = MockContext() - valid_ctx.message.id = 1 - valid_ctx.author.id = 2 - valid_user = MockUser() - valid_user.id = 2 - - invalid_reaction_id = MockReaction() - invalid_reaction_id.message.id = 42 + valid_ctx = MockContext(message=MockMessage(id=1), author=MockUser(id=2)) + valid_user = MockUser(id=2) + + invalid_reaction_id = MockReaction(message=MockMessage(id=42)) invalid_reaction_id.__str__.return_value = '🔁' - invalid_user_id = MockUser() - invalid_user_id.id = 42 - invalid_reaction_str = MockReaction() - invalid_reaction_str.message.id = 1 + invalid_user_id = MockUser(id=42) + invalid_reaction_str = MockReaction(message=MockMessage(id=1)) invalid_reaction_str.__str__.return_value = ':longbeard:' cases = ( -- cgit v1.2.3 From f87f7559db8b352490324a535fb77e88f2f68b41 Mon Sep 17 00:00:00 2001 From: Matteo Date: Thu, 27 Feb 2020 11:37:02 +0100 Subject: Split the eval command procedure into two functions. Two functions were created: send_eval and continue_eval, in order to facilitate testing. The corresponding tests are also changed in this commit. --- bot/cogs/snekbox.py | 112 +++++++++++++++++------------- tests/bot/cogs/test_snekbox.py | 150 ++++++++++++++++++++++------------------- 2 files changed, 148 insertions(+), 114 deletions(-) (limited to 'tests') diff --git a/bot/cogs/snekbox.py b/bot/cogs/snekbox.py index efa4696b5..25b2455e8 100644 --- a/bot/cogs/snekbox.py +++ b/bot/cogs/snekbox.py @@ -179,6 +179,68 @@ class Snekbox(Cog): return output, paste_link + async def send_eval(self, ctx: Context, code: str) -> Message: + """ + Evaluate code, format it, and send the output to the corresponding channel. + + Return the bot response. + """ + async with ctx.typing(): + results = await self.post_eval(code) + msg, error = self.get_results_message(results) + + if error: + output, paste_link = error, None + else: + output, paste_link = await self.format_output(results["stdout"]) + + icon = self.get_status_emoji(results) + msg = f"{ctx.author.mention} {icon} {msg}.\n\n```py\n{output}\n```" + if paste_link: + msg = f"{msg}\nFull output: {paste_link}" + + response = await ctx.send(msg) + self.bot.loop.create_task( + wait_for_deletion(response, user_ids=(ctx.author.id,), client=ctx.bot) + ) + + log.info(f"{ctx.author}'s job had a return code of {results['returncode']}") + return response + + async def continue_eval(self, ctx: Context, response: Message) -> Tuple[bool, Optional[str]]: + """ + Check if the eval session should continue. + + First item of the returned tuple is if the eval session should continue, + the second is the new code to evaluate. + """ + _predicate_eval_message_edit = partial(predicate_eval_message_edit, ctx) + _predicate_emoji_reaction = partial(predicate_eval_emoji_reaction, ctx) + + try: + _, new_message = await self.bot.wait_for( + 'message_edit', + check=_predicate_eval_message_edit, + timeout=10 + ) + await ctx.message.add_reaction('🔁') + await self.bot.wait_for( + 'reaction_add', + check=_predicate_emoji_reaction, + timeout=10 + ) + + code = new_message.content.split(' ', maxsplit=1)[1] + await ctx.message.clear_reactions() + with contextlib.suppress(HTTPException): + await response.delete() + + except asyncio.TimeoutError: + await ctx.message.clear_reactions() + return False, None + + return True, code + @command(name="eval", aliases=("e",)) @guild_only() @in_channel(Channels.bot, hidden_channels=(Channels.esoteric,), bypass_roles=EVAL_ROLES) @@ -203,58 +265,18 @@ class Snekbox(Cog): log.info(f"Received code from {ctx.author} for evaluation:\n{code}") - _predicate_eval_message_edit = partial(predicate_eval_message_edit, ctx) - _predicate_emoji_reaction = partial(predicate_eval_emoji_reaction, ctx) - while True: self.jobs[ctx.author.id] = datetime.datetime.now() code = self.prepare_input(code) - try: - async with ctx.typing(): - results = await self.post_eval(code) - msg, error = self.get_results_message(results) - - if error: - output, paste_link = error, None - else: - output, paste_link = await self.format_output(results["stdout"]) - - icon = self.get_status_emoji(results) - msg = f"{ctx.author.mention} {icon} {msg}.\n\n```py\n{output}\n```" - if paste_link: - msg = f"{msg}\nFull output: {paste_link}" - - response = await ctx.send(msg) - self.bot.loop.create_task( - wait_for_deletion(response, user_ids=(ctx.author.id,), client=ctx.bot) - ) - - log.info(f"{ctx.author}'s job had a return code of {results['returncode']}") + response = await self.send_eval(ctx, code) finally: del self.jobs[ctx.author.id] - try: - _, new_message = await self.bot.wait_for( - 'message_edit', - check=_predicate_eval_message_edit, - timeout=10 - ) - await ctx.message.add_reaction('🔁') - await self.bot.wait_for( - 'reaction_add', - check=_predicate_emoji_reaction, - timeout=10 - ) - - log.info(f"Re-evaluating message {ctx.message.id}") - code = new_message.content.split(' ', maxsplit=1)[1] - await ctx.message.clear_reactions() - with contextlib.suppress(HTTPException): - await response.delete() - except asyncio.TimeoutError: - await ctx.message.clear_reactions() - return + continue_eval, code = await self.continue_eval(ctx, response) + if not continue_eval: + break + log.info(f"Re-evaluating message {ctx.message.id}") def predicate_eval_message_edit(ctx: Context, old_msg: Message, new_msg: Message) -> bool: diff --git a/tests/bot/cogs/test_snekbox.py b/tests/bot/cogs/test_snekbox.py index 112c923c8..c1c0f8d47 100644 --- a/tests/bot/cogs/test_snekbox.py +++ b/tests/bot/cogs/test_snekbox.py @@ -1,6 +1,7 @@ import asyncio import logging import unittest +from functools import partial from unittest.mock import MagicMock, Mock, call, patch from bot.cogs import snekbox @@ -175,28 +176,33 @@ class SnekboxTests(unittest.TestCase): async def test_eval_command_evaluate_once(self): """Test the eval command procedure.""" ctx = MockContext() - ctx.message = MockMessage() - ctx.send = AsyncMock() - ctx.author.mention = '@LemonLemonishBeard#0042' - ctx.typing = MagicMock(return_value=AsyncContextManagerMock(None)) - self.cog.post_eval = AsyncMock(return_value={'stdout': '', 'returncode': 0}) - self.cog.get_results_message = MagicMock(return_value=('Return code 0', '')) - self.cog.get_status_emoji = MagicMock(return_value=':yay!:') - self.cog.format_output = AsyncMock(return_value=('[No output]', None)) - self.bot.wait_for.side_effect = asyncio.TimeoutError + response = MockMessage() + self.cog.prepare_input = MagicMock(return_value='MyAwesomeFormattedCode') + self.cog.send_eval = AsyncMock(return_value=response) + self.cog.continue_eval = AsyncMock(return_value=(False, None)) await self.cog.eval_command.callback(self.cog, ctx=ctx, code='MyAwesomeCode') + self.cog.prepare_input.assert_called_once_with('MyAwesomeCode') + self.cog.send_eval.assert_called_once_with(ctx, 'MyAwesomeFormattedCode') + self.cog.continue_eval.assert_called_once_with(ctx, response) - ctx.send.assert_called_once_with( - '@LemonLemonishBeard#0042 :yay!: Return code 0.\n\n```py\n[No output]\n```' - ) - self.cog.post_eval.assert_called_once_with('MyAwesomeCode') - self.cog.get_status_emoji.assert_called_once_with({'stdout': '', 'returncode': 0}) - self.cog.get_results_message.assert_called_once_with({'stdout': '', 'returncode': 0}) - self.cog.format_output.assert_called_once_with('') + @async_test + async def test_eval_command_evaluate_twice(self): + """Test the eval and re-eval command procedure.""" + ctx = MockContext() + response = MockMessage() + self.cog.prepare_input = MagicMock(return_value='MyAwesomeFormattedCode') + self.cog.send_eval = AsyncMock(return_value=response) + self.cog.continue_eval = AsyncMock() + self.cog.continue_eval.side_effect = ((True, 'MyAwesomeCode-2'), (False, None)) + + await self.cog.eval_command.callback(self.cog, ctx=ctx, code='MyAwesomeCode') + self.cog.prepare_input.has_calls(call('MyAwesomeCode'), call('MyAwesomeCode-2')) + self.cog.send_eval.assert_called_with(ctx, 'MyAwesomeFormattedCode') + self.cog.continue_eval.assert_called_with(ctx, response) @async_test - async def test_eval_command_reject_two_eval(self): + async def test_eval_command_reject_two_eval_at_the_same_time(self): """Test if the eval command rejects an eval if the author already have a running eval.""" ctx = MockContext() ctx.author.id = 42 @@ -217,92 +223,98 @@ class SnekboxTests(unittest.TestCase): ctx.invoke.assert_called_once_with(self.bot.get_command("help"), "eval") @async_test - async def test_eval_command_return_error(self): - """Test the eval command error handling.""" + async def test_send_eval(self): + """Test the send_eval function.""" ctx = MockContext() ctx.message = MockMessage() ctx.send = AsyncMock() ctx.author.mention = '@LemonLemonishBeard#0042' ctx.typing = MagicMock(return_value=AsyncContextManagerMock(None)) - self.cog.post_eval = AsyncMock(return_value={'stdout': 'ERROR', 'returncode': 127}) - self.cog.get_results_message = MagicMock(return_value=('Return code 127', 'Error occurred')) - self.cog.get_status_emoji = MagicMock(return_value=':nope!:') - self.cog.format_output = AsyncMock() - self.bot.wait_for.side_effect = asyncio.TimeoutError - - await self.cog.eval_command.callback(self.cog, ctx=ctx, code='MyAwesomeCode') + self.cog.post_eval = AsyncMock(return_value={'stdout': '', 'returncode': 0}) + self.cog.get_results_message = MagicMock(return_value=('Return code 0', '')) + self.cog.get_status_emoji = MagicMock(return_value=':yay!:') + self.cog.format_output = AsyncMock(return_value=('[No output]', None)) + await self.cog.send_eval(ctx, 'MyAwesomeCode') ctx.send.assert_called_once_with( - '@LemonLemonishBeard#0042 :nope!: Return code 127.\n\n```py\nError occurred\n```' + '@LemonLemonishBeard#0042 :yay!: Return code 0.\n\n```py\n[No output]\n```' ) self.cog.post_eval.assert_called_once_with('MyAwesomeCode') - self.cog.get_results_message.assert_called_once_with({'stdout': 'ERROR', 'returncode': 127}) - self.cog.get_status_emoji.assert_called_once_with({'stdout': 'ERROR', 'returncode': 127}) - self.cog.format_output.assert_not_called() + self.cog.get_status_emoji.assert_called_once_with({'stdout': '', 'returncode': 0}) + self.cog.get_results_message.assert_called_once_with({'stdout': '', 'returncode': 0}) + self.cog.format_output.assert_called_once_with('') @async_test - async def test_eval_command_with_paste_link(self): - """Test the eval command procedure with the use of a paste link.""" + async def test_send_eval_with_paste_link(self): + """Test the send_eval function with a too long output that generate a paste link.""" ctx = MockContext() ctx.message = MockMessage() ctx.send = AsyncMock() ctx.author.mention = '@LemonLemonishBeard#0042' ctx.typing = MagicMock(return_value=AsyncContextManagerMock(None)) - self.cog.post_eval = AsyncMock(return_value={'stdout': 'SuperLongBeard', 'returncode': 0}) + self.cog.post_eval = AsyncMock(return_value={'stdout': 'Way too long beard', 'returncode': 0}) self.cog.get_results_message = MagicMock(return_value=('Return code 0', '')) self.cog.get_status_emoji = MagicMock(return_value=':yay!:') - self.cog.format_output = AsyncMock(return_value=('Truncated - too long beard', 'https://testificate.com/')) - self.bot.wait_for.side_effect = asyncio.TimeoutError - - await self.cog.eval_command.callback(self.cog, ctx=ctx, code='MyAwesomeCode') + self.cog.format_output = AsyncMock(return_value=('Way too long beard', 'lookatmybeard.com')) + await self.cog.send_eval(ctx, 'MyAwesomeCode') ctx.send.assert_called_once_with( - '@LemonLemonishBeard#0042 :yay!: Return code 0.\n\n```py\n' - 'Truncated - too long beard\n```\nFull output: https://testificate.com/' + '@LemonLemonishBeard#0042 :yay!: Return code 0.' + '\n\n```py\nWay too long beard\n```\nFull output: lookatmybeard.com' ) self.cog.post_eval.assert_called_once_with('MyAwesomeCode') - self.cog.get_status_emoji.assert_called_once_with({'stdout': 'SuperLongBeard', 'returncode': 0}) - self.cog.get_results_message.assert_called_once_with({'stdout': 'SuperLongBeard', 'returncode': 0}) - self.cog.format_output.assert_called_with('SuperLongBeard') + self.cog.get_status_emoji.assert_called_once_with({'stdout': 'Way too long beard', 'returncode': 0}) + self.cog.get_results_message.assert_called_once_with({'stdout': 'Way too long beard', 'returncode': 0}) + self.cog.format_output.assert_called_once_with('Way too long beard') @async_test - async def test_eval_command_evaluate_twice(self): - """Test the eval command re-evaluation procedure.""" + async def test_send_eval_with_non_zero_eval(self): + """Test the send_eval function with a code returning a non-zero code.""" ctx = MockContext() ctx.message = MockMessage() - ctx.message.content = '!e MyAwesomeCode' - updated_msg = MockMessage() - updated_msg .content = '!e MyAwesomeCode-2' - response_msg = MockMessage() - response_msg.delete = AsyncMock() - ctx.send = AsyncMock(return_value=response_msg) + ctx.send = AsyncMock() ctx.author.mention = '@LemonLemonishBeard#0042' ctx.typing = MagicMock(return_value=AsyncContextManagerMock(None)) - self.cog.post_eval = AsyncMock(return_value={'stdout': '', 'returncode': 0}) - self.cog.get_results_message = MagicMock(return_value=('Return code 0', '')) - self.cog.get_status_emoji = MagicMock(return_value=':yay!:') - self.cog.format_output = AsyncMock(return_value=('[No output]', None)) - self.bot.wait_for.side_effect = ((None, updated_msg), None, asyncio.TimeoutError) - - await self.cog.eval_command.callback(self.cog, ctx=ctx, code='MyAwesomeCode') - - self.cog.post_eval.assert_has_calls((call('MyAwesomeCode'), call('MyAwesomeCode-2'))) + self.cog.post_eval = AsyncMock(return_value={'stdout': 'ERROR', 'returncode': 127}) + self.cog.get_results_message = MagicMock(return_value=('Return code 127', 'Beard got stuck in the eval')) + self.cog.get_status_emoji = MagicMock(return_value=':nope!:') + self.cog.format_output = AsyncMock() # This function isn't called - # Multiplied by 2 because we expect it to be called twice - ctx.send.assert_has_calls( - [call('@LemonLemonishBeard#0042 :yay!: Return code 0.\n\n```py\n[No output]\n```')] * 2 + await self.cog.send_eval(ctx, 'MyAwesomeCode') + ctx.send.assert_called_once_with( + '@LemonLemonishBeard#0042 :nope!: Return code 127.\n\n```py\nBeard got stuck in the eval\n```' ) - self.cog.get_status_emoji.assert_has_calls([call({'stdout': '', 'returncode': 0})] * 2) - self.cog.get_results_message.assert_has_calls([call({'stdout': '', 'returncode': 0})] * 2) - self.cog.format_output.assert_has_calls([call('')] * 2) + self.cog.post_eval.assert_called_once_with('MyAwesomeCode') + self.cog.get_status_emoji.assert_called_once_with({'stdout': 'ERROR', 'returncode': 127}) + self.cog.get_results_message.assert_called_once_with({'stdout': 'ERROR', 'returncode': 127}) + self.cog.format_output.assert_not_called() + @async_test + async def test_continue_eval_does_continue(self): + """Test that the continue_eval function does continue if required conditions are met.""" + ctx = MockContext(message=MockMessage(add_reaction=AsyncMock(), clear_reactions=AsyncMock())) + response = MockMessage(delete=AsyncMock()) + new_msg = MockMessage(content='!e NewCode') + self.bot.wait_for.side_effect = ((None, new_msg), None) + + actual = await self.cog.continue_eval(ctx, response) + self.assertEqual(actual, (True, 'NewCode')) self.bot.wait_for.has_calls( - call('message_edit', check=snekbox.predicate_eval_message_edit, timeout=10), - call('reaction_add', check=snekbox.predicate_eval_emoji_reaction, timeout=10) + call('message_edit', partial(snekbox.predicate_eval_message_edit, ctx), timeout=10), + call('reaction_add', partial(snekbox.predicate_eval_emoji_reaction, ctx), timeout=10) ) ctx.message.add_reaction.assert_called_once_with('🔁') - ctx.message.clear_reactions.assert_called() - response_msg.delete.assert_called_once() + ctx.message.clear_reactions.assert_called_once() + response.delete.assert_called_once() + + @async_test + async def test_continue_eval_does_not_continue(self): + ctx = MockContext(message=MockMessage(clear_reactions=AsyncMock())) + self.bot.wait_for.side_effect = asyncio.TimeoutError + + actual = await self.cog.continue_eval(ctx, MockMessage()) + self.assertEqual(actual, (False, None)) + ctx.message.clear_reactions.assert_called_once() def test_predicate_eval_message_edit(self): """Test the predicate_eval_message_edit function.""" -- cgit v1.2.3 From afc74faadd5fb4d3fd3003bed9f2a1f241c0dc58 Mon Sep 17 00:00:00 2001 From: Matteo Date: Thu, 27 Feb 2020 11:47:01 +0100 Subject: Use unicode code point instead of literal for the snekbox re-eval emoji Unicode literals aren't really safe compared to code points --- bot/cogs/snekbox.py | 8 +++++--- tests/bot/cogs/test_snekbox.py | 6 +++--- 2 files changed, 8 insertions(+), 6 deletions(-) (limited to 'tests') diff --git a/bot/cogs/snekbox.py b/bot/cogs/snekbox.py index 25b2455e8..52d830fa8 100644 --- a/bot/cogs/snekbox.py +++ b/bot/cogs/snekbox.py @@ -42,6 +42,8 @@ EVAL_ROLES = (Roles.helpers, Roles.moderator, Roles.admin, Roles.owner, Roles.ro SIGKILL = 9 +REEVAL_EMOJI = '\U0001f501' # :repeat: + class Snekbox(Cog): """Safe evaluation of Python code using Snekbox.""" @@ -223,7 +225,7 @@ class Snekbox(Cog): check=_predicate_eval_message_edit, timeout=10 ) - await ctx.message.add_reaction('🔁') + await ctx.message.add_reaction(REEVAL_EMOJI) await self.bot.wait_for( 'reaction_add', check=_predicate_emoji_reaction, @@ -285,8 +287,8 @@ def predicate_eval_message_edit(ctx: Context, old_msg: Message, new_msg: Message def predicate_eval_emoji_reaction(ctx: Context, reaction: Reaction, user: User) -> bool: - """Return True if the reaction 🔁 was added by the context message author on this message.""" - return reaction.message.id == ctx.message.id and user.id == ctx.author.id and str(reaction) == '🔁' + """Return True if the reaction REEVAL_EMOJI was added by the context message author on this message.""" + return reaction.message.id == ctx.message.id and user.id == ctx.author.id and str(reaction) == REEVAL_EMOJI def setup(bot: Bot) -> None: diff --git a/tests/bot/cogs/test_snekbox.py b/tests/bot/cogs/test_snekbox.py index c1c0f8d47..e7a1e3362 100644 --- a/tests/bot/cogs/test_snekbox.py +++ b/tests/bot/cogs/test_snekbox.py @@ -303,7 +303,7 @@ class SnekboxTests(unittest.TestCase): call('message_edit', partial(snekbox.predicate_eval_message_edit, ctx), timeout=10), call('reaction_add', partial(snekbox.predicate_eval_emoji_reaction, ctx), timeout=10) ) - ctx.message.add_reaction.assert_called_once_with('🔁') + ctx.message.add_reaction.assert_called_once_with(snekbox.REEVAL_EMOJI) ctx.message.clear_reactions.assert_called_once() response.delete.assert_called_once() @@ -336,12 +336,12 @@ class SnekboxTests(unittest.TestCase): def test_predicate_eval_emoji_reaction(self): """Test the predicate_eval_emoji_reaction function.""" valid_reaction = MockReaction(message=MockMessage(id=1)) - valid_reaction.__str__.return_value = '🔁' + valid_reaction.__str__.return_value = snekbox.REEVAL_EMOJI valid_ctx = MockContext(message=MockMessage(id=1), author=MockUser(id=2)) valid_user = MockUser(id=2) invalid_reaction_id = MockReaction(message=MockMessage(id=42)) - invalid_reaction_id.__str__.return_value = '🔁' + invalid_reaction_id.__str__.return_value = snekbox.REEVAL_EMOJI invalid_user_id = MockUser(id=42) invalid_reaction_str = MockReaction(message=MockMessage(id=1)) invalid_reaction_str.__str__.return_value = ':longbeard:' -- cgit v1.2.3 From b62e15f835ff4b6c808a9b571919bcfa479d004b Mon Sep 17 00:00:00 2001 From: Matteo Date: Fri, 28 Feb 2020 09:41:12 +0100 Subject: Return only the new code in continue_eval and check for truthiness instead --- bot/cogs/snekbox.py | 13 ++++++------- tests/bot/cogs/test_snekbox.py | 8 ++++---- 2 files changed, 10 insertions(+), 11 deletions(-) (limited to 'tests') diff --git a/bot/cogs/snekbox.py b/bot/cogs/snekbox.py index 52d830fa8..381b309e0 100644 --- a/bot/cogs/snekbox.py +++ b/bot/cogs/snekbox.py @@ -209,12 +209,11 @@ class Snekbox(Cog): log.info(f"{ctx.author}'s job had a return code of {results['returncode']}") return response - async def continue_eval(self, ctx: Context, response: Message) -> Tuple[bool, Optional[str]]: + async def continue_eval(self, ctx: Context, response: Message) -> Optional[str]: """ Check if the eval session should continue. - First item of the returned tuple is if the eval session should continue, - the second is the new code to evaluate. + Return the new code to evaluate or None if the eval session should be terminated. """ _predicate_eval_message_edit = partial(predicate_eval_message_edit, ctx) _predicate_emoji_reaction = partial(predicate_eval_emoji_reaction, ctx) @@ -239,9 +238,9 @@ class Snekbox(Cog): except asyncio.TimeoutError: await ctx.message.clear_reactions() - return False, None + return None - return True, code + return code @command(name="eval", aliases=("e",)) @guild_only() @@ -275,8 +274,8 @@ class Snekbox(Cog): finally: del self.jobs[ctx.author.id] - continue_eval, code = await self.continue_eval(ctx, response) - if not continue_eval: + code = await self.continue_eval(ctx, response) + if not code: break log.info(f"Re-evaluating message {ctx.message.id}") diff --git a/tests/bot/cogs/test_snekbox.py b/tests/bot/cogs/test_snekbox.py index e7a1e3362..985bc66a1 100644 --- a/tests/bot/cogs/test_snekbox.py +++ b/tests/bot/cogs/test_snekbox.py @@ -179,7 +179,7 @@ class SnekboxTests(unittest.TestCase): response = MockMessage() self.cog.prepare_input = MagicMock(return_value='MyAwesomeFormattedCode') self.cog.send_eval = AsyncMock(return_value=response) - self.cog.continue_eval = AsyncMock(return_value=(False, None)) + self.cog.continue_eval = AsyncMock(return_value=None) await self.cog.eval_command.callback(self.cog, ctx=ctx, code='MyAwesomeCode') self.cog.prepare_input.assert_called_once_with('MyAwesomeCode') @@ -194,7 +194,7 @@ class SnekboxTests(unittest.TestCase): self.cog.prepare_input = MagicMock(return_value='MyAwesomeFormattedCode') self.cog.send_eval = AsyncMock(return_value=response) self.cog.continue_eval = AsyncMock() - self.cog.continue_eval.side_effect = ((True, 'MyAwesomeCode-2'), (False, None)) + self.cog.continue_eval.side_effect = ('MyAwesomeCode-2', None) await self.cog.eval_command.callback(self.cog, ctx=ctx, code='MyAwesomeCode') self.cog.prepare_input.has_calls(call('MyAwesomeCode'), call('MyAwesomeCode-2')) @@ -298,7 +298,7 @@ class SnekboxTests(unittest.TestCase): self.bot.wait_for.side_effect = ((None, new_msg), None) actual = await self.cog.continue_eval(ctx, response) - self.assertEqual(actual, (True, 'NewCode')) + self.assertEqual(actual, 'NewCode') self.bot.wait_for.has_calls( call('message_edit', partial(snekbox.predicate_eval_message_edit, ctx), timeout=10), call('reaction_add', partial(snekbox.predicate_eval_emoji_reaction, ctx), timeout=10) @@ -313,7 +313,7 @@ class SnekboxTests(unittest.TestCase): self.bot.wait_for.side_effect = asyncio.TimeoutError actual = await self.cog.continue_eval(ctx, MockMessage()) - self.assertEqual(actual, (False, None)) + self.assertEqual(actual, None) ctx.message.clear_reactions.assert_called_once() def test_predicate_eval_message_edit(self): -- cgit v1.2.3