diff --git a/.gitignore b/.gitignore index 4ea4967853..7d1d5d3908 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,6 @@ env/ MANIFEST doc/build/* logs/* -tests sopel.egg-info/* *.db *.pyc diff --git a/docs/source/index.rst b/docs/source/index.rst index f8ef05f507..7717d2daf4 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -25,3 +25,4 @@ Documentation run package + tests diff --git a/docs/source/tests.rst b/docs/source/tests.rst new file mode 100644 index 0000000000..ab4e2c9864 --- /dev/null +++ b/docs/source/tests.rst @@ -0,0 +1,32 @@ +============= +Testing tools +============= + +.. contents:: + :local: + :depth: 2 + +Fixtures with py.test +===================== + +.. automodule:: sopel.tests.pytest_plugin + :members: + +Factories +========= + +.. automodule:: sopel.tests.factories + :members: + +Mocks +===== + +.. automodule:: sopel.tests.mocks + :members: + + +Old testing tools +================= + +.. automodule:: sopel.test_tools + :members: diff --git a/setup.py b/setup.py index b95d07f0e5..2e5e3ed35b 100755 --- a/setup.py +++ b/setup.py @@ -65,6 +65,9 @@ def read_reqs(path): 'sopel-config = sopel.cli.config:main', 'sopel-plugins = sopel.cli.plugins:main', ], + 'pytest11': [ + 'pytest-sopel = sopel.tests.pytest_plugin', + ], }, python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, <4', ) diff --git a/sopel/test_tools.py b/sopel/test_tools.py index 75c356dbf8..45d3ff934c 100644 --- a/sopel/test_tools.py +++ b/sopel/test_tools.py @@ -1,12 +1,8 @@ # coding=utf-8 -"""This module has classes and functions that can help in writing tests. - -test_tools.py - Sopel misc tools -Copyright 2013, Ari Koivula, -Licensed under the Eiffel Forum License 2. - -https://sopel.chat -""" +"""This module has classes and functions that can help in writing tests.""" +# Copyright 2013, Ari Koivula, +# Copyright 2019, Florian Strzelecki +# Licensed under the Eiffel Forum License 2. from __future__ import unicode_literals, absolute_import, print_function, division import os @@ -20,9 +16,9 @@ import configparser as ConfigParser from sopel.bot import SopelWrapper -from sopel.irc.abstract_backends import AbstractIRCBackend import sopel.config import sopel.config.core_section +import sopel.plugins import sopel.tools import sopel.tools.target import sopel.trigger @@ -32,7 +28,6 @@ 'MockConfig', 'MockSopel', 'MockSopelWrapper', - 'MockIRCBackend', 'get_example_test', 'get_disable_setup', 'insert_into_module', @@ -43,30 +38,6 @@ basestring = str -def rawlist(*args): - """Build a list of raw IRC messages from the lines given as ``*args``. - - :return: a list of raw IRC messages as seen by the bot - - This is a helper function to build a list of messages without having to - care about encoding or this pesky carriage return:: - - >>> rawlist('PRIVMSG :Hello!') - [b'PRIVMSG :Hello!\r\n'] - - """ - return ['{0}\r\n'.format(arg).encode('utf-8') for arg in args] - - -class MockIRCBackend(AbstractIRCBackend): - def __init__(self, *args, **kwargs): - super(MockIRCBackend, self).__init__(*args, **kwargs) - self.message_sent = [] - - def send(self, data): - self.message_sent.append(data) - - class MockConfig(sopel.config.Config): def __init__(self): self.filename = tempfile.mkstemp()[1] @@ -149,22 +120,23 @@ class MockSopelWrapper(SopelWrapper): def get_example_test(tested_func, msg, results, privmsg, admin, owner, repeat, use_regexp, ignore=[]): - """Get a function that calls tested_func with fake wrapper and trigger. - - Args: - tested_func - A sopel callable that accepts SopelWrapper and Trigger. - msg - Message that is supposed to trigger the command. - results - Expected output from the callable. - privmsg - If true, make the message appear to have sent in a private - message to the bot. If false, make it appear to have come from a - channel. - admin - If true, make the message appear to have come from an admin. - owner - If true, make the message appear to have come from an owner. - repeat - How many times to repeat the test. Useful for tests that - return random stuff. - use_regexp = Bool. If true, results is in regexp format. - ignore - List of strings to ignore. - + """Get a function that calls ``tested_func`` with fake wrapper and trigger. + + :param callable tested_func: a Sopel callable that accepts a + ``SopelWrapper`` and a ``Trigger`` + :param str msg: message that is supposed to trigger the command + :param list results: expected output from the callable + :param bool privmsg: if ``True``, make the message appear to have arrived + in a private message to the bot; otherwise make it + appear to have come from a channel + :param bool admin: make the message appear to have come from an admin + :param bool owner: make the message appear to have come from an owner + :param int repeat: how many times to repeat the test; useful for tests that + return random stuff + :param bool use_regexp: pass ``True`` if ``results`` are in regexp format + :param list ignore: strings to ignore + :return: a test function for ``tested_func`` + :rtype: ``callable`` """ def test(): bot = MockSopel("NickName", admin=admin, owner=owner) diff --git a/sopel/tests/__init__.py b/sopel/tests/__init__.py new file mode 100644 index 0000000000..5afa43a11c --- /dev/null +++ b/sopel/tests/__init__.py @@ -0,0 +1,18 @@ +# coding=utf-8 +"""Tests tools, factories, pytest fixtures, and mocks.""" +from __future__ import unicode_literals, absolute_import, print_function, division + + +def rawlist(*args): + """Build a list of raw IRC messages from the lines given as ``*args``. + + :return: a list of raw IRC messages as seen by the bot + :rtype: list + + This is a helper function to build a list of messages without having to + care about encoding or this pesky carriage return:: + + >>> rawlist('PRIVMSG :Hello!') + [b'PRIVMSG :Hello!\\r\\n'] + """ + return ['{0}\r\n'.format(arg).encode('utf-8') for arg in args] diff --git a/sopel/tests/factories.py b/sopel/tests/factories.py new file mode 100644 index 0000000000..c8aed56493 --- /dev/null +++ b/sopel/tests/factories.py @@ -0,0 +1,120 @@ +# coding=utf-8 +"""Test factories: they create objects for testing purposes.""" +from __future__ import unicode_literals, absolute_import, print_function, division + +import re + +from sopel import bot, config, plugins, trigger +from .mocks import MockIRCServer, MockUser, MockIRCBackend + + +class BotFactory(object): + """Factory to create bot. + + .. seealso:: + + The :func:`~sopel.tests.pytest_plugin.botfactory` fixture can be used + to instantiate this factory. + """ + def preloaded(self, settings, preloads=None): + """Create a bot and preload its plugins. + + :param settings: Sopel's configuration for testing purposes + :type settings: :class:`sopel.config.Config` + :param list preloads: list of plugins to preload, setup, and register + :return: a test instance of the bot + :rtype: :class:`sopel.bot.Sopel` + + This will instantiate a :class:`~sopel.bot.Sopel` object, replace its + backend with a :class:`~MockIRCBackend`, and then preload plugins. + This will automatically load the ``coretasks`` plugin, and every other + plugin from ``preloads``:: + + factory = BotFactory() + bot = factory.with_autoloads(settings, ['emoticons', 'remind']) + + .. note:: + + This will automatically setup plugins: be careful with plugins that + require access to external services on setup. + + You may also need to manually call shutdown routines for the + loaded plugins. + + """ + preloads = set(preloads or []) | {'coretasks'} + mockbot = self(settings) + + usable_plugins = plugins.get_usable_plugins(settings) + for name in preloads: + plugin = usable_plugins[name][0] + plugin.load() + plugin.setup(mockbot) + plugin.register(mockbot) + + return mockbot + + def __call__(self, settings): + obj = bot.Sopel(settings, daemon=False) + obj.backend = MockIRCBackend(obj) + return obj + + +class ConfigFactory(object): + """Factory to create settings. + + .. seealso:: + + The :func:`~sopel.tests.pytest_plugin.configfactory` fixture can be + used to instantiate this factory. + """ + def __init__(self, tmpdir): + self.tmpdir = tmpdir + + def __call__(self, name, data): + tmpfile = self.tmpdir.join(name) + tmpfile.write(data) + return config.Config(tmpfile.strpath) + + +class TriggerFactory(object): + """Factory to create trigger. + + .. seealso:: + + The :func:`~sopel.tests.pytest_plugin.triggerfactory` fixture can be + used to instantiate this factory. + """ + def wrapper(self, mockbot, raw, pattern=None): + trigger = self(mockbot, raw, pattern=pattern) + return bot.SopelWrapper(mockbot, trigger) + + def __call__(self, mockbot, raw, pattern=None): + return trigger.Trigger( + mockbot.settings, + trigger.PreTrigger(mockbot.nick, raw), + re.match(pattern or r'.*', raw)) + + +class IRCFactory(object): + """Factory to create mock IRC server. + + .. seealso:: + + The :func:`~sopel.tests.pytest_plugin.ircfactory` fixture can be used + to create this factory. + """ + def __call__(self, mockbot): + return MockIRCServer(mockbot) + + +class UserFactory(object): + """Factory to create mock user. + + .. seealso:: + + The :func:`~sopel.tests.pytest_plugin.userfactory` fixture can be used + to create this factory. + """ + def __call__(self, nick=None, user=None, host=None): + return MockUser(nick, user, host) diff --git a/sopel/tests/mocks.py b/sopel/tests/mocks.py new file mode 100644 index 0000000000..36109079ec --- /dev/null +++ b/sopel/tests/mocks.py @@ -0,0 +1,237 @@ +# coding=utf-8 +"""Test mocks: they fake objects for testing.""" +from __future__ import unicode_literals, absolute_import, print_function, division + + +from sopel.irc.abstract_backends import AbstractIRCBackend + + +class MockIRCBackend(AbstractIRCBackend): + """Fake IRC connection backend for testing purpose. + + This backend doesn't require an actual connection. Instead, it stores every + message sent in the :attr:`message_sent` list. + """ + def __init__(self, *args, **kwargs): + super(MockIRCBackend, self).__init__(*args, **kwargs) + self.message_sent = [] + """List of raw messages sent by the bot.""" + + def send(self, data): + """Store ``data`` into :attr:`message_sent`.""" + self.message_sent.append(data) + + +class MockIRCServer(object): + """Fake IRC Server that can send messages to a test bot. + + :param bot: test bot instance to send messages to + :type bot: :class:`sopel.bot.Sopel` + + This mock object helps developers when they want to simulate an IRC server + sending messages to the bot. + + The :class:`~sopel.tests.factories.IRCFactory` factory can be used to + create such mock object, either directly or by using ``py.test`` and the + :func:`~sopel.tests.pytest_plugin.ircfactory` fixture. + """ + def __init__(self, bot): + self.bot = bot + + @property + def chanserv(self): + """ChanServ's message prefix.""" + return 'ChanServ!ChanServ@services.' + + def channel_joined(self, channel, users=None): + """Send events as if the bot just joined a channel. + + :param str channel: channel to send message for + :param list users: list (or tuple) of nicknames that will be present + in the ``RPL_NAMREPLY`` event + + This will send 2 messages to the bot: + + * a ``RPL_NAMREPLY`` event (353), giving information about ``users`` + present in ``channel`` + * a ``RPL_ENDOFNAMES`` event (366) for completion + + Use this to emulate when the bot joins a channel, and the server + replies with the list of connected users:: + + factory.channel_joined('#test', ['Owner', '@ChanServ']) + + In this example, the bot will know that there are 2 other users present + in ``#test``: "Owner" (a regular user) and "ChanServ" (which is a + channel operator). Note that the bot itself will be added to the list + of users automatically, and you **should not** pass it in the ``users`` + parameter. + + This is particulary useful to populate the bot's memory of who is in + a channel. + + .. note:: + + To add a user to a channel after using this method, you should + use the :meth:`join` method. + """ + # automatically add the bot's nick to the list + users = set(users or []) + users.add(self.bot.nick) + message = ':irc.example.com 353 {bot} = {channel} :{users}'.format( + bot=self.bot.nick, + users=' '.join(list(users)), + channel=channel, + ) + self.bot.on_message(message) + + message = ( + ':irc.example.com 366 {bot} = {channel} ' + ':End of /NAMES list.' + ).format( + bot=self.bot.nick, + channel=channel, + ) + self.bot.on_message(message) + + def mode_set(self, channel, flags, users): + """Send a MODE event for a ``channel`` + + :param str channel: channel receiving the MODE event + :param str flags: MODE flags set + :param list users: users getting the MODE flags + + This will send a MODE message as if ``ChanServ`` added/removed channel + modes for a set of ``users``. This method assumes the ``flags`` + parameter follows the `IRC specification for MODE`__:: + + factory.mode_set('#test', '+vo-v', ['UserV', UserOP', 'UserAnon']) + + .. __: https://tools.ietf.org/html/rfc1459#section-4.2.3 + """ + message = ':{chanserv} MODE {channel} {flags} {users}'.format( + chanserv=self.chanserv, + channel=channel, + flags=flags, + users=' '.join(users), + ) + self.bot.on_message(message) + + def join(self, user, channel): + """Send a ``channel`` JOIN event from ``user``. + + :param user: factory for the user who joins the ``channel`` + :type user: :class:`MockUser` + :param str channel: channel the ``user`` joined + + This will send a ``JOIN`` message as if ``user`` just joined the + channel:: + + factory.join(MockUser('NewUser'), '#test') + + .. seealso:: + + This function is a shortcut to call the bot with the result from + the user factory's :meth:`~MockUser.join` method. + """ + self.bot.on_message(user.join(channel)) + + def say(self, user, channel, text): + """Send a ``PRIVMSG`` to ``channel`` by ``user``. + + :param user: factory for the user who sends a message to ``channel`` + :type user: :class:`MockUser` + :param str channel: recipient of the ``user``'s ``PRIVMSG`` + :param str text: content of the message sent to the ``channel`` + + This will send a ``PRIVMSG`` message as if ``user`` sent it to the + ``channel``, and the server forwarded it to its clients:: + + factory.say(MockUser('NewUser'), '#test', '.shrug') + + .. seealso:: + + This function is a shortcut to call the bot with the result from + the user's :meth:`~MockUser.privmsg` method. + """ + self.bot.on_message(user.privmsg(channel, text)) + + def pm(self, user, text): + """Send a ``PRIVMSG`` to the bot by a ``user``. + + :param user: factory for the user object who sends a message + :type user: :class:`MockUser` + :param str text: content of the message sent to the bot + + This will send a ``PRIVMSG`` message as forwarded by the server for + a ``user`` sending it to the bot:: + + factory.pm(MockUser('NewUser'), 'A private word.') + + .. seealso:: + + This function is a shortcut to call the bot with the result from + the user factory's :meth:`~MockUser.privmsg` method, using the + bot's nick as recipient. + """ + self.bot.on_message(user.privmsg(self.bot.nick, text)) + + +class MockUser(object): + """Fake user that can generate messages to send to a bot. + + :param str nick: nickname + :param str user: IRC username + :param str host: user's host + + The :class:`~sopel.tests.factories.UserFactory` factory can be used to + create such mock object, either directly or by using ``py.test`` and the + :func:`~sopel.tests.pytest_plugin.userfactory` fixture. + """ + def __init__(self, nick=None, user=None, host=None): + self.nick = nick or 'Test' + self.user = user or self.nick.lower() + self.host = host or 'example.com' + + @property + def prefix(self): + """User's hostmask as seen by other users on the server. + + When the server forwards a User's command, it uses this prefix. + """ + return '{nick}!{user}@{host}'.format( + nick=self.nick, user=self.user, host=self.host) + + def privmsg(self, recipient, text): + """Generate a ``PRIVMSG`` command forwarded by a server for the user. + + :param str recipient: a channel name or the bot's nick + :param str text: content of the message + :return: a ``PRIVMSG`` command forwarded by the server as if it + originated from the user's hostmask + :rtype: str + """ + message = ':{prefix} PRIVMSG {recipient} :{text}\r\n'.format( + prefix=self.prefix, + recipient=recipient, + text=text, + ) + + assert len(message.encode('utf-8')) <= 512, ( + 'PRIVMSG command must NOT exceed the 512 bytes limit ' + '(\\r\\n included). Trying to send this command:\n`%r`' % message + ) + + return message + + def join(self, channel): + """Generate a ``JOIN`` command forwarded by the server for the user. + + :param str channel: channel the user joined + :return: the ``JOIN`` command the server sends to its clients present + in the same ``channel`` when the user joins it. + :rtype: str + """ + return ':{prefix} JOIN {channel}\r\n'.format( + prefix=self.prefix, + channel=channel) diff --git a/sopel/tests/pytest_plugin.py b/sopel/tests/pytest_plugin.py new file mode 100644 index 0000000000..16bc6d816c --- /dev/null +++ b/sopel/tests/pytest_plugin.py @@ -0,0 +1,97 @@ +# coding=utf-8 +"""Pytest plugin for Sopel.""" +from __future__ import unicode_literals, absolute_import, print_function, division + +import pytest + +from .factories import BotFactory, ConfigFactory, TriggerFactory, IRCFactory, UserFactory + + +@pytest.fixture +def botfactory(): + """Fixture to get a Bot factory. + + :return: a factory to create a mocked bot instance + :rtype: :class:`sopel.tests.factories.BotFactory` + + This is very useful in unit tests:: + + def test_bot(configfactory, botfactory): + settings = configfactory('... skip for clarity ...') + bot = botfactory(settings) # no plugins loaded + # ... do something with the bot + + def test_bot_loaded(configfactory, botfactory): + settings = configfactory('... skip for clarity ...') + bot = botfactory.preloaded(settings, ['myplugin']) + # now the bot has `coretasks` and `myplugin` loaded + """ + return BotFactory() + + +@pytest.fixture +def configfactory(tmpdir): + """Fixture to get a config factory. + + :return: a factory to create test settings + :rtype: :class:`sopel.tests.factories.ConfigFactory` + + The factory will be automatically configured with a ``tmpdir`` object. + """ + return ConfigFactory(tmpdir) + + +@pytest.fixture +def triggerfactory(): + """Fixture to get a trigger factory. + + :return: a factory to create triggers + :rtype: :class:`sopel.tests.factories.TriggerFactory` + """ + return TriggerFactory() + + +@pytest.fixture +def ircfactory(): + """Fixture to get an IRC factory. + + :return: a factory to create mock IRC servers + :rtype: :class:`sopel.tests.factories.IRCFactory` + + For example, a plugin command could be tested with this:: + + from sopel.tests import rawlist + + def test_mycommand(configfactory, botfactory, ircfactory, userfactory): + settings = configfactory('... skip for clarity ...') + bot = botfactory(settings, ['myplugin']) + irc = ircfactory(bot) + user = userfactory('User') + + irc.say(user, '#test', '.mycommand')) + + assert bot.backend.message_sent == rawlist( + 'PRIVMSG #test :My plugin replied this.' + ) + """ + return IRCFactory() + + +@pytest.fixture +def userfactory(): + """Fixture to get a user factory. + + :return: a factory to create mock users + :rtype: :class:`sopel.tests.factories.UserFactory` + + :: + + def test_mycommand(userfactory): + user = userfactory('User') + + assert user.nick == 'User' + assert user.user == 'user' + assert user.host == 'example.com' + assert user.prefix == 'User!user@example.com' + """ + return UserFactory() diff --git a/test/irc/test_irc_abstract_backends.py b/test/irc/test_irc_abstract_backends.py index 47ea19b518..4475661223 100644 --- a/test/irc/test_irc_abstract_backends.py +++ b/test/irc/test_irc_abstract_backends.py @@ -4,7 +4,7 @@ from sopel.irc.abstract_backends import AbstractIRCBackend -from sopel.test_tools import MockIRCBackend +from sopel.tests.mocks import MockIRCBackend class BotCollector: diff --git a/test/test_bot.py b/test/test_bot.py index d7c38eb65d..c4b948addf 100644 --- a/test/test_bot.py +++ b/test/test_bot.py @@ -6,41 +6,31 @@ import pytest -from sopel import bot, config, plugins, trigger -from sopel.test_tools import MockIRCBackend, rawlist +from sopel import bot, plugins +from sopel.tests import rawlist -@pytest.fixture -def tmpconfig(tmpdir): - conf_file = tmpdir.join('conf.ini') - conf_file.write("\n".join([ - "[core]", - "owner = testnick", - "nick = TestBot", - "enable = coretasks" - "" - ])) - return config.Config(conf_file.strpath) +TMP_CONFIG = """ +[core] +owner = testnick +nick = TestBot +enable = coretasks +""" @pytest.fixture -def mockbot(tmpconfig): - obj = bot.Sopel(tmpconfig, daemon=False) - obj.backend = MockIRCBackend(obj) - return obj +def tmpconfig(configfactory): + return configfactory('test.cfg', TMP_CONFIG) -def line(sopel, raw): - return trigger.Trigger( - sopel.settings, - trigger.PreTrigger(sopel.nick, raw), - re.match('.*', raw)) +@pytest.fixture +def mockbot(tmpconfig, botfactory): + return botfactory(tmpconfig) -def test_wrapper_say(mockbot): - message = line( +def test_wrapper_say(mockbot, triggerfactory): + wrapper = triggerfactory.wrapper( mockbot, ':Test!test@example.com PRIVMSG #channel :test message') - wrapper = bot.SopelWrapper(mockbot, message) wrapper.say('Hi!') assert mockbot.backend.message_sent == rawlist( @@ -48,10 +38,9 @@ def test_wrapper_say(mockbot): ) -def test_wrapper_say_override_destination(mockbot): - message = line( +def test_wrapper_say_override_destination(mockbot, triggerfactory): + wrapper = triggerfactory.wrapper( mockbot, ':Test!test@example.com PRIVMSG #channel :test message') - wrapper = bot.SopelWrapper(mockbot, message) wrapper.say('Hi!', destination='#different') assert mockbot.backend.message_sent == rawlist( @@ -59,10 +48,9 @@ def test_wrapper_say_override_destination(mockbot): ) -def test_wrapper_notice(mockbot): - message = line( +def test_wrapper_notice(mockbot, triggerfactory): + wrapper = triggerfactory.wrapper( mockbot, ':Test!test@example.com PRIVMSG #channel :test message') - wrapper = bot.SopelWrapper(mockbot, message) wrapper.notice('Hi!') assert mockbot.backend.message_sent == rawlist( @@ -70,10 +58,9 @@ def test_wrapper_notice(mockbot): ) -def test_wrapper_notice_override_destination(mockbot): - message = line( +def test_wrapper_notice_override_destination(mockbot, triggerfactory): + wrapper = triggerfactory.wrapper( mockbot, ':Test!test@example.com PRIVMSG #channel :test message') - wrapper = bot.SopelWrapper(mockbot, message) wrapper.notice('Hi!', destination='#different') assert mockbot.backend.message_sent == rawlist( @@ -81,10 +68,9 @@ def test_wrapper_notice_override_destination(mockbot): ) -def test_wrapper_action(mockbot): - message = line( +def test_wrapper_action(mockbot, triggerfactory): + wrapper = triggerfactory.wrapper( mockbot, ':Test!test@example.com PRIVMSG #channel :test message') - wrapper = bot.SopelWrapper(mockbot, message) wrapper.action('Hi!') assert mockbot.backend.message_sent == rawlist( @@ -92,10 +78,9 @@ def test_wrapper_action(mockbot): ) -def test_wrapper_action_override_destination(mockbot): - message = line( +def test_wrapper_action_override_destination(mockbot, triggerfactory): + wrapper = triggerfactory.wrapper( mockbot, ':Test!test@example.com PRIVMSG #channel :test message') - wrapper = bot.SopelWrapper(mockbot, message) wrapper.action('Hi!', destination='#different') assert mockbot.backend.message_sent == rawlist( @@ -103,10 +88,9 @@ def test_wrapper_action_override_destination(mockbot): ) -def test_wrapper_reply(mockbot): - message = line( +def test_wrapper_reply(mockbot, triggerfactory): + wrapper = triggerfactory.wrapper( mockbot, ':Test!test@example.com PRIVMSG #channel :test message') - wrapper = bot.SopelWrapper(mockbot, message) wrapper.reply('Hi!') assert mockbot.backend.message_sent == rawlist( @@ -114,10 +98,9 @@ def test_wrapper_reply(mockbot): ) -def test_wrapper_reply_override_destination(mockbot): - message = line( +def test_wrapper_reply_override_destination(mockbot, triggerfactory): + wrapper = triggerfactory.wrapper( mockbot, ':Test!test@example.com PRIVMSG #channel :test message') - wrapper = bot.SopelWrapper(mockbot, message) wrapper.reply('Hi!', destination='#another') assert mockbot.backend.message_sent == rawlist( @@ -125,10 +108,9 @@ def test_wrapper_reply_override_destination(mockbot): ) -def test_wrapper_reply_override_reply_to(mockbot): - message = line( +def test_wrapper_reply_override_reply_to(mockbot, triggerfactory): + wrapper = triggerfactory.wrapper( mockbot, ':Test!test@example.com PRIVMSG #channel :test message') - wrapper = bot.SopelWrapper(mockbot, message) wrapper.reply('Hi!', reply_to='Admin') assert mockbot.backend.message_sent == rawlist( @@ -136,10 +118,9 @@ def test_wrapper_reply_override_reply_to(mockbot): ) -def test_wrapper_reply_override_destination_reply_to(mockbot): - message = line( +def test_wrapper_reply_override_destination_reply_to(mockbot, triggerfactory): + wrapper = triggerfactory.wrapper( mockbot, ':Test!test@example.com PRIVMSG #channel :test message') - wrapper = bot.SopelWrapper(mockbot, message) wrapper.reply('Hi!', destination='#another', reply_to='Admin') assert mockbot.backend.message_sent == rawlist( @@ -147,10 +128,9 @@ def test_wrapper_reply_override_destination_reply_to(mockbot): ) -def test_wrapper_kick(mockbot): - message = line( +def test_wrapper_kick(mockbot, triggerfactory): + wrapper = triggerfactory.wrapper( mockbot, ':Test!test@example.com PRIVMSG #channel :test message') - wrapper = bot.SopelWrapper(mockbot, message) wrapper.kick('SpamUser') assert mockbot.backend.message_sent == rawlist( @@ -158,10 +138,9 @@ def test_wrapper_kick(mockbot): ) -def test_wrapper_kick_message(mockbot): - message = line( +def test_wrapper_kick_message(mockbot, triggerfactory): + wrapper = triggerfactory.wrapper( mockbot, ':Test!test@example.com PRIVMSG #channel :test message') - wrapper = bot.SopelWrapper(mockbot, message) wrapper.kick('SpamUser', message='Test reason') assert mockbot.backend.message_sent == rawlist( @@ -169,30 +148,27 @@ def test_wrapper_kick_message(mockbot): ) -def test_wrapper_kick_error_nick(mockbot): - message = line( +def test_wrapper_kick_error_nick(mockbot, triggerfactory): + wrapper = triggerfactory.wrapper( mockbot, ':Test!test@example.com PRIVMSG #channel :test message') - wrapper = bot.SopelWrapper(mockbot, message) with pytest.raises(RuntimeError): wrapper.kick(None) assert mockbot.backend.message_sent == [] -def test_wrapper_kick_error_channel(mockbot): - message = line( +def test_wrapper_kick_error_channel(mockbot, triggerfactory): + wrapper = triggerfactory.wrapper( mockbot, ':Test!test@example.com PRIVMSG OtherUser :test message') - wrapper = bot.SopelWrapper(mockbot, message) with pytest.raises(RuntimeError): wrapper.kick('SpamUser') assert mockbot.backend.message_sent == [] -def test_wrapper_kick_override_destination(mockbot): - message = line( +def test_wrapper_kick_override_destination(mockbot, triggerfactory): + wrapper = triggerfactory.wrapper( mockbot, ':Test!test@example.com PRIVMSG #channel :test message') - wrapper = bot.SopelWrapper(mockbot, message) wrapper.kick('SpamUser', channel='#another') assert mockbot.backend.message_sent == rawlist( @@ -200,10 +176,9 @@ def test_wrapper_kick_override_destination(mockbot): ) -def test_wrapper_kick_override_destination_message(mockbot): - message = line( +def test_wrapper_kick_override_destination_message(mockbot, triggerfactory): + wrapper = triggerfactory.wrapper( mockbot, ':Test!test@example.com PRIVMSG #channel :test message') - wrapper = bot.SopelWrapper(mockbot, message) wrapper.kick('SpamUser', channel='#another', message='Test reason') assert mockbot.backend.message_sent == rawlist( diff --git a/test/test_coretasks.py b/test/test_coretasks.py index dbef429e59..00da15a061 100644 --- a/test/test_coretasks.py +++ b/test/test_coretasks.py @@ -4,128 +4,110 @@ import pytest -from sopel import coretasks from sopel.module import VOICE, HALFOP, OP, ADMIN, OWNER from sopel.tools import Identifier -from sopel.test_tools import MockSopel, MockSopelWrapper -from sopel.trigger import PreTrigger, Trigger +from sopel.tests import rawlist + + +TMP_CONFIG = """ +[core] +owner = Uowner +nick = TestBot +enable = coretasks +""" @pytest.fixture -def sopel(): - bot = MockSopel("Sopel") - return bot +def tmpconfig(configfactory): + return configfactory('conf.ini', TMP_CONFIG) -def test_bot_mixed_modes(sopel): - """ - Ensure mixed modes like +vha are tracked correctly. - Sopel 6.6.6 and older would assign all modes to all users. #1575 - """ +@pytest.fixture +def mockbot(tmpconfig, botfactory): + return botfactory.preloaded(tmpconfig) - # RPL_NAMREPLY to create Users and (zeroed) privs - for user in set("Unothing Uvoice Uhalfop Uop Uadmin Uowner".split(" ")): - pretrigger = PreTrigger( - "Foo", ":test.example.com 353 Foo = #test :Foo %s" % user - ) - trigger = Trigger(sopel.config, pretrigger, None) - coretasks.handle_names(MockSopelWrapper(sopel, trigger), trigger) - pretrigger = PreTrigger("Foo", "MODE #test +qvhao Uowner Uvoice Uhalfop Uadmin Uop") - trigger = Trigger(sopel.config, pretrigger, None) - coretasks.track_modes(MockSopelWrapper(sopel, trigger), trigger) +def test_bot_mixed_modes(mockbot, ircfactory): + """Ensure mixed modes like ``+vha`` are tracked correctly. - assert sopel.channels["#test"].privileges[Identifier("Unothing")] == 0 - assert sopel.channels["#test"].privileges[Identifier("Uvoice")] == VOICE - assert sopel.channels["#test"].privileges[Identifier("Uhalfop")] == HALFOP - assert sopel.channels["#test"].privileges[Identifier("Uop")] == OP - assert sopel.channels["#test"].privileges[Identifier("Uadmin")] == ADMIN - assert sopel.channels["#test"].privileges[Identifier("Uowner")] == OWNER + Sopel 6.6.6 and older would assign all modes to all users. + .. seealso:: -def test_bot_mixed_mode_removal(sopel): - """ - Ensure mixed mode types like -h+a are handled - Sopel 6.6.6 and older did not handle this correctly. #1575 + GitHub issue #1575 (https://github.com/sopel-irc/sopel/pull/1575). """ + irc = ircfactory(mockbot) + irc.channel_joined('#test', [ + 'Uowner', 'Uvoice', 'Uhalfop', 'Uadmin', 'Uop', 'Unothing']) + irc.mode_set('#test', '+qvhao', [ + 'Uowner', 'Uvoice', 'Uhalfop', 'Uadmin', 'Uop']) - # RPL_NAMREPLY to create Users and (zeroed) privs - for user in set("Uvoice Uop".split(" ")): - pretrigger = PreTrigger( - "Foo", ":test.example.com 353 Foo = #test :Foo %s" % user - ) - trigger = Trigger(sopel.config, pretrigger, None) - coretasks.handle_names(MockSopelWrapper(sopel, trigger), trigger) + assert mockbot.channels["#test"].privileges[Identifier("Uowner")] == OWNER + assert mockbot.channels["#test"].privileges[Identifier("Uvoice")] == VOICE + assert mockbot.channels["#test"].privileges[Identifier("Uhalfop")] == HALFOP + assert mockbot.channels["#test"].privileges[Identifier("Uadmin")] == ADMIN + assert mockbot.channels["#test"].privileges[Identifier("Uop")] == OP + assert mockbot.channels["#test"].privileges[Identifier("Unothing")] == 0 - pretrigger = PreTrigger("Foo", "MODE #test +qao Uvoice Uvoice Uvoice") - trigger = Trigger(sopel.config, pretrigger, None) - coretasks.track_modes(MockSopelWrapper(sopel, trigger), trigger) - pretrigger = PreTrigger( - "Foo", "MODE #test -o+o-qa+v Uvoice Uop Uvoice Uvoice Uvoice" - ) - trigger = Trigger(sopel.config, pretrigger, None) - coretasks.track_modes(MockSopelWrapper(sopel, trigger), trigger) +def test_bot_mixed_mode_removal(mockbot, ircfactory): + """Ensure mixed mode types like ``-h+a`` are handled. - assert sopel.channels["#test"].privileges[Identifier("Uvoice")] == VOICE - assert sopel.channels["#test"].privileges[Identifier("Uop")] == OP + Sopel 6.6.6 and older did not handle this correctly. + .. seealso:: -def test_bot_mixed_mode_types(sopel): - """ - Ensure mixed argument- and non-argument- modes are handled - Sopel 6.6.6 and older did not behave well. #1575 + GitHub issue #1575 (https://github.com/sopel-irc/sopel/pull/1575). """ + irc = ircfactory(mockbot) + irc.channel_joined('#test', ['Uvoice', 'Uop']) - # RPL_NAMREPLY to create Users and (zeroed) privs - for user in set("Uvoice Uop Uadmin Uvoice2 Uop2 Uadmin2".split(" ")): - pretrigger = PreTrigger( - "Foo", ":test.example.com 353 Foo = #test :Foo %s" % user - ) - trigger = Trigger(sopel.config, pretrigger, None) - coretasks.handle_names(MockSopelWrapper(sopel, trigger), trigger) - - # Non-attribute-requiring non-permission mode - pretrigger = PreTrigger("Foo", "MODE #test +amov Uadmin Uop Uvoice") - trigger = Trigger(sopel.config, pretrigger, None) - coretasks.track_modes(MockSopelWrapper(sopel, trigger), trigger) - - assert sopel.channels["#test"].privileges[Identifier("Uvoice")] == VOICE - assert sopel.channels["#test"].privileges[Identifier("Uop")] == OP - assert sopel.channels["#test"].privileges[Identifier("Uadmin")] == ADMIN - - # Attribute-requiring non-permission modes - # This results in a _send_who, which isn't supported in MockSopel or this - # test, so we just make sure it results in an exception instead of privesc. - pretrigger = PreTrigger("Foo", "MODE #test +abov Uadmin2 x!y@z Uop2 Uvoice2") - trigger = Trigger(sopel.config, pretrigger, None) - try: - coretasks.track_modes(MockSopelWrapper(sopel, trigger), trigger) - except AttributeError as e: - if e.args[0] == "'MockSopel' object has no attribute 'enabled_capabilities'": - return - - assert sopel.channels["#test"].privileges[Identifier("Uvoice2")] == VOICE - assert sopel.channels["#test"].privileges[Identifier("Uop2")] == OP - assert sopel.channels["#test"].privileges[Identifier("Uadmin2")] == ADMIN - - -def test_mode_colon(sopel): - """ - Ensure mode messages with colons are parsed properly + irc.mode_set('#test', '+qao', ['Uvoice', 'Uvoice', 'Uvoice']) + assert mockbot.channels["#test"].privileges[Identifier("Uop")] == 0 + assert mockbot.channels["#test"].privileges[Identifier("Uvoice")] == ( + ADMIN + OWNER + OP), 'Uvoice got +q, +a, and +o modes' + + irc.mode_set('#test', '-o+o-qa+v', [ + 'Uvoice', 'Uop', 'Uvoice', 'Uvoice', 'Uvoice']) + assert mockbot.channels["#test"].privileges[Identifier("Uop")] == OP, ( + 'OP got +o only') + assert mockbot.channels["#test"].privileges[Identifier("Uvoice")] == VOICE, ( + 'Uvoice got -o, -q, -a, then +v') + + +def test_bot_mixed_mode_types(mockbot, ircfactory): + """Ensure mixed argument-required and -not-required modes are handled. + + Sopel 6.6.6 and older did not behave well. + + .. seealso:: + + GitHub issue #1575 (https://github.com/sopel-irc/sopel/pull/1575). """ + irc = ircfactory(mockbot) + irc.channel_joined('#test', [ + 'Uvoice', 'Uop', 'Uadmin', 'Uvoice2', 'Uop2', 'Uadmin2']) + irc.mode_set('#test', '+amov', ['Uadmin', 'Uop', 'Uvoice']) + + assert mockbot.channels["#test"].privileges[Identifier("Uadmin")] == ADMIN + assert mockbot.channels["#test"].privileges[Identifier("Uop")] == OP + assert mockbot.channels["#test"].privileges[Identifier("Uvoice")] == VOICE + + irc.mode_set('#test', '+abov', ['Uadmin2', 'x!y@z', 'Uop2', 'Uvoice2']) + + assert mockbot.channels["#test"].privileges[Identifier("Uadmin2")] == 0 + assert mockbot.channels["#test"].privileges[Identifier("Uop2")] == 0 + assert mockbot.channels["#test"].privileges[Identifier("Uvoice2")] == 0 + + assert mockbot.backend.message_sent == rawlist('WHO #test'), ( + 'Upon finding an unexpected nick, the bot must send a WHO request.') - # RPL_NAMREPLY to create Users and (zeroed) privs - for user in set("Uvoice Uadmin".split(" ")): - pretrigger = PreTrigger( - "Foo", ":test.example.com 353 Foo = #test :Foo %s" % user - ) - trigger = Trigger(sopel.config, pretrigger, None) - coretasks.handle_names(MockSopelWrapper(sopel, trigger), trigger) - pretrigger = PreTrigger("Foo", "MODE #test +av Uadmin :Uvoice") - trigger = Trigger(sopel.config, pretrigger, None) - coretasks.track_modes(MockSopelWrapper(sopel, trigger), trigger) +def test_mode_colon(mockbot, ircfactory): + """Ensure mode messages with colons are parsed properly.""" + irc = ircfactory(mockbot) + irc.channel_joined('#test', ['Uadmin', 'Uvoice']) + irc.mode_set('#test', '+av', ['Uadmin', ':Uvoice']) - assert sopel.channels["#test"].privileges[Identifier("Uvoice")] == VOICE - assert sopel.channels["#test"].privileges[Identifier("Uadmin")] == ADMIN + assert mockbot.channels["#test"].privileges[Identifier("Uvoice")] == VOICE + assert mockbot.channels["#test"].privileges[Identifier("Uadmin")] == ADMIN diff --git a/test/test_irc.py b/test/test_irc.py index 1d7cb74615..631c2a436a 100644 --- a/test/test_irc.py +++ b/test/test_irc.py @@ -4,41 +4,28 @@ import pytest -from sopel import config -from sopel.irc import AbstractBot -from sopel.test_tools import MockIRCBackend, rawlist +from sopel.tests import rawlist -@pytest.fixture -def tmpconfig(tmpdir): - conf_file = tmpdir.join('conf.ini') - conf_file.write("\n".join([ - "[core]", - "owner = Exirel", - "nick = Sopel", - "user = sopel", - "name = Sopel (https://sopel.chat)", - "flood_burst_lines = 1000", # we don't want flood protection here - ])) - return config.Config(conf_file.strpath) +TMP_CONFIG = """ +[core] +owner = Exirel +nick = Sopel +user = sopel +name = Sopel (https://sopel.chat) +# we don't want flood protection here +flood_burst_lines = 1000 +""" @pytest.fixture -def bot(tmpconfig): - bot = MockBot(tmpconfig) - bot.backend = bot.get_irc_backend() - return bot - +def tmpconfig(configfactory): + return configfactory('conf.ini', TMP_CONFIG) -class MockBot(AbstractBot): - hostmask = 'test.hostmask.localhost' - def get_irc_backend(self): - return MockIRCBackend(self) - - def dispatch(self, pretrigger): - # override to prevent RuntimeError - pass +@pytest.fixture +def bot(tmpconfig, botfactory): + return botfactory(tmpconfig) def test_on_connect(bot): diff --git a/test/test_loader.py b/test/test_loader.py index 145c1136d4..da20ca1ce4 100644 --- a/test/test_loader.py +++ b/test/test_loader.py @@ -6,7 +6,7 @@ import pytest -from sopel import loader, config, module, plugins +from sopel import loader, module, plugins MOCK_MODULE_CONTENT = """# coding=utf-8 @@ -62,16 +62,16 @@ def bot_command(): return bot_command +TMP_CONFIG = """ +[core] +owner = testnick +nick = TestBot +""" + + @pytest.fixture -def tmpconfig(tmpdir): - conf_file = tmpdir.join('conf.ini') - conf_file.write("\n".join([ - "[core]", - "owner=testnick", - "nick = TestBot", - "" - ])) - return config.Config(conf_file.strpath) +def tmpconfig(configfactory): + return configfactory('conf.ini', TMP_CONFIG) @pytest.fixture