diff --git a/pyircbot/irccore.py b/pyircbot/irccore.py index 4292240..8faadbf 100644 --- a/pyircbot/irccore.py +++ b/pyircbot/irccore.py @@ -31,6 +31,8 @@ class IRCCore(object): self.rate_max = float(rate_max) self.rate_int = float(rate_int) + self.reconnect_delay = 3.0 + self.connected = False """If we're connected or not""" @@ -60,7 +62,7 @@ class IRCCore(object): self.initHooks() self.outputq = asyncio.Queue() - self._loop.call_soon(asyncio.ensure_future, self.outputqueue()) + self._loop.call_soon_threadsafe(asyncio.ensure_future, self.outputqueue()) async def loop(self, loop): while self.alive: @@ -94,8 +96,8 @@ class IRCCore(object): self.writer.close() if self.alive: # TODO ramp down reconnect attempts - logging.info("Reconnecting in 3s...") - await asyncio.sleep(3) + logging.info("Reconnecting in {}s...".format(self.reconnect_delay)) + await asyncio.sleep(self.reconnect_delay) async def outputqueue(self): bucket = burstbucket(self.rate_max, self.rate_int) diff --git a/pyircbot/modulebase.py b/pyircbot/modulebase.py index 726f3dc..883bfe1 100644 --- a/pyircbot/modulebase.py +++ b/pyircbot/modulebase.py @@ -13,7 +13,7 @@ from .common import load as pload from .common import messageHasCommand -class ModuleBase: +class ModuleBase(object): """All modules will extend this class :param bot: A reference to the main bot passed when this module is created diff --git a/pyircbot/modules/Remind.py b/pyircbot/modules/Remind.py index 88a1332..26a114e 100644 --- a/pyircbot/modules/Remind.py +++ b/pyircbot/modules/Remind.py @@ -198,7 +198,7 @@ class Remind(ModuleBase): else: return zonestr - @info("after have the bot remind after", cmds=["after", "in"]) + @info("after have the bot remind after", cmds=["after", "in"]) @command("after", "in", allow_private=True) def remindin(self, msg, cmd): replyTo = msg.args[0] diff --git a/pyircbot/modules/SQLite.py b/pyircbot/modules/SQLite.py index 772aafd..8c92453 100755 --- a/pyircbot/modules/SQLite.py +++ b/pyircbot/modules/SQLite.py @@ -64,13 +64,7 @@ class Connection: c = self.connection.cursor() return c - def escape(self, s): - raise NotImplementedError - - def ondisable(self): - self.connection.close() - - # Connects to the database server, and selects a database (Or attempts to create it if it doesn't exist yet) + # Opens the sqlite database / attempts to create it if it doesn't exist yet def _connect(self): self.log.info("Sqlite: opening database %s" % self.master.getFilePath(self.dbname)) self.connection = sqlite3.connect(self.master.getFilePath(self.dbname), check_same_thread=False) diff --git a/requirements-test.txt b/requirements-test.txt index b85621a..428d98e 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,3 +1,4 @@ +apipkg==1.4 appdirs==1.4.3 certifi==2017.4.17 chardet==3.0.4 @@ -5,6 +6,7 @@ cheroot==5.9.1 CherryPy==12.0.1 coverage==4.4.2 decorator==4.0.11 +execnet==1.5.0 idna==2.5 ipdb==0.10.3 ipython==6.0.0 @@ -30,6 +32,8 @@ pyparsing==2.2.0 PySocks==1.6.7 pytest==3.2.5 pytest-cov==2.5.1 +pytest-forked==0.2 +pytest-xdist==1.20.1 pytz==2017.3 pyzmq==16.0.2 requests==2.18.1 diff --git a/run-tests.sh b/run-tests.sh index 9bfc287..4a6bd0c 100755 --- a/run-tests.sh +++ b/run-tests.sh @@ -3,4 +3,4 @@ export PYTHONUNBUFFERED=1 export PYTHONPATH=. -py.test --cov=pyircbot --cov-report html tests/ +py.test --cov=pyircbot --cov-report html -n 4 tests/ diff --git a/tests/lib.py b/tests/lib.py index d1e1667..311a2fc 100644 --- a/tests/lib.py +++ b/tests/lib.py @@ -1,8 +1,9 @@ import os import sys import pytest -from random import randint from threading import Thread +from random import randint +from pyircbot import PyIRCBot from pyircbot.pyircbot import PrimitiveBot from pyircbot.irccore import IRCEvent, UserPrefix from unittest.mock import MagicMock @@ -20,6 +21,7 @@ class FakeBaseBot(PrimitiveBot): def __init__(self, config): super().__init__(config) self.act_PRIVMSG = MagicMock() + self._modules = [] def feed_line(self, trailing, cmd="PRIVMSG", args=["#test"], sender=("chatter", "root", "cia.gov")): """ @@ -36,13 +38,27 @@ class FakeBaseBot(PrimitiveBot): if validation: hook.method(msg, validation) + def closeAllModules(self): + for modname in self._modules: + self.unloadmodule(modname) + + def loadmodule(self, module_name): + super().loadmodule(module_name) + self._modules.append(module_name) + + def unloadmodule(self, module_name): + super().unloadmodule(module_name) + self._modules.remove(module_name) + @pytest.fixture -def fakebot(): +def fakebot(tmpdir): # TODO copy data tree to isolated place so each fakebot() is isolated - bot = FakeBaseBot({"bot": {"datadir": "./examples/data/"}, + os.mkdir(os.path.join(tmpdir, "data")) + bot = FakeBaseBot({"bot": {"datadir": tmpdir}, "module_configs": {}}) yield bot + bot.closeAllModules() @pytest.fixture @@ -79,3 +95,70 @@ def ircserver(): server_t.start() yield port, server server.stop() + + +@pytest.fixture +def livebot(ircserver, tmpdir): + port, server = ircserver + channel = "#test" + str(randint(100000, 1000000)) + nick = "testbot" + str(randint(100000, 1000000)) + config = { + "bot": { + "datadir": tmpdir, + "rpcbind": "0.0.0.0", + "rpcport": -1, + "usermodules": [] + }, + "connection": { + "servers": [ + ["localhost", port] + ], + "force_ipv6": False, + "rate_limit": { + "rate_max": 5.0, + "rate_int": 1.1 + } + }, + "modules": [ + "PingResponder", + "Services" + ], + "module_configs": { + "Services": { + "user": { + "nick": [ + nick, + nick + "_", + nick + "__" + ], + "password": "nickservpassword", + "username": "pyircbot3", + "hostname": "pyircbot3.domain.com", + "realname": "pyircbot3" + }, + "ident": { + "enable": "no", + "to": "nickserv", + "command": "identify %(password)s", + "ghost": "no", + "ghost_to": "nickserv", + "ghost_cmd": "ghost %(nick)s %(password)s" + }, + "channels": [ + channel + ], + "privatechannels": { + "to": "chanserv", + "command": "invite %(channel)s", + "list": [] + } + } + } + } + + bot = PyIRCBot(config) + bot_t = Thread(target=bot.run, daemon=True) + # bot_t.start() + yield port, server, bot, bot_t, channel, nick + + bot.kill(message="bye", forever=True) diff --git a/tests/modules/test_ascii.py b/tests/modules/test_ascii.py index 6025329..15f3472 100644 --- a/tests/modules/test_ascii.py +++ b/tests/modules/test_ascii.py @@ -1,9 +1,20 @@ +import os import pytest from tests.lib import * # NOQA - fixtures @pytest.fixture def bot(fakebot): + fakebot.botconfig["module_configs"]["ASCII"] = { + "line_delay": 1.1, + "allow_parallel": False, + "allow_hilight": True, + "list_max": 15 + } + adir = os.path.join(fakebot.botconfig["bot"]["datadir"], "data", "ASCII") + os.makedirs(adir, exist_ok=True) + with open(os.path.join(adir, "test.txt"), "w") as f: + f.write("hello world!") fakebot.loadmodule("ASCII") return fakebot @@ -11,3 +22,8 @@ def bot(fakebot): def test_ascii(bot): bot.feed_line(".ascii test") bot.act_PRIVMSG.assert_called_once_with('#test', 'hello world!') + + +def test_listascii(bot): + bot.feed_line(".listascii") + bot.act_PRIVMSG.assert_called_once_with('#test', 'Avalable asciis: test') diff --git a/tests/modules/test_calc.py b/tests/modules/test_calc.py index da18133..1f5be1a 100644 --- a/tests/modules/test_calc.py +++ b/tests/modules/test_calc.py @@ -15,13 +15,13 @@ def calcbot(fakebot): "delayCalcSpecific": 0, "delayMatch": 0} fakebot.loadmodule("SQLite") + tables = ["calc_addedby", "calc_channels", "calc_definitions", "calc_words"] with closing(fakebot.moduleInstances["SQLite"].opendb("calc.db")) as db: - for q in ["DROP TABLE calc_addedby;", - "DROP TABLE calc_channels;", - "DROP TABLE calc_definitions;", - "DROP TABLE calc_words;"]: - db.query(q) + for t in tables: + db.query("DROP TABLE IF EXISTS `{}`;".format(t)) fakebot.loadmodule("Calc") + for t in tables: + assert fakebot.moduleInstances["Calc"].sql.tableExists("calc_addedby") return fakebot diff --git a/tests/modules/test_nickuser.py b/tests/modules/test_nickuser.py index d74f8d6..833cbc8 100644 --- a/tests/modules/test_nickuser.py +++ b/tests/modules/test_nickuser.py @@ -19,10 +19,8 @@ def nickbot(fakebot): "delayMatch": 0} fakebot.loadmodule("SQLite") with closing(fakebot.moduleInstances["SQLite"].opendb("attributes.db")) as db: - for q in ["DROP TABLE attribute;", - "DROP TABLE items;", - "DROP TABLE `values`;"]: - db.query(q) + for table in ["attribute", "items", "values"]: + db.query("DROP TABLE IF EXISTS `{}`;".format(table)) fakebot.loadmodule("AttributeStorageLite") fakebot.loadmodule("NickUser") return fakebot @@ -37,7 +35,15 @@ def test_blind_login(nickbot): nickbot.act_PRIVMSG.assert_called_once_with('chatter', '.login: You must first set a password with .setpass') +def test_no_pms(nickbot): + nickbot.feed_line(".login foobar") + nickbot.act_PRIVMSG.assert_not_called() + + def test_register(nickbot): + pm(nickbot, ".setpass") + nickbot.act_PRIVMSG.assert_called_once_with('chatter', '.setpass: usage: ".setpass newpass" or ".setpass oldpass newpass"') + nickbot.act_PRIVMSG.reset_mock() pm(nickbot, ".setpass foobar") nickbot.act_PRIVMSG.assert_called_once_with('chatter', '.setpass: Your password has been set to "foobar".') nickbot.act_PRIVMSG.reset_mock() @@ -45,6 +51,9 @@ def test_register(nickbot): def test_register_login(nickbot): test_register(nickbot) + pm(nickbot, ".login") + nickbot.act_PRIVMSG.assert_called_once_with('chatter', '.login: usage: ".login password"') + nickbot.act_PRIVMSG.reset_mock() pm(nickbot, ".login foobar") nickbot.act_PRIVMSG.assert_called_once_with('chatter', '.login: You have been logged in from: cia.gov') nickbot.act_PRIVMSG.reset_mock() diff --git a/tests/modules/test_remind.py b/tests/modules/test_remind.py new file mode 100644 index 0000000..dc4ddea --- /dev/null +++ b/tests/modules/test_remind.py @@ -0,0 +1,41 @@ +import pytest +from contextlib import closing +from tests.lib import * # NOQA - fixtures +from time import sleep +import datetime + + +@pytest.fixture +def rbot(fakebot): + """ + Provide a bot loaded with the Calc module. Clear the database. + """ + fakebot.botconfig["module_configs"]["Remind"] = {"mytimezone": "US/Pacific", "precision": 0.2} + fakebot.loadmodule("SQLite") + with closing(fakebot.moduleInstances["SQLite"].opendb("remind.db")) as db: + db.query("DROP TABLE IF EXISTS `reminders`;") + fakebot.loadmodule("Remind") + return fakebot + + +@pytest.mark.slow +def test_remind_in(rbot): + rbot.feed_line(".in 3s frig off") + rbot.act_PRIVMSG.assert_called_once_with('#test', 'chatter: Ok, talk to you in approx 0h0m') + rbot.act_PRIVMSG.reset_mock() + sleep(2.5) + rbot.act_PRIVMSG.assert_not_called() + sleep(1) + rbot.act_PRIVMSG.assert_called_once_with('#test', 'chatter: Reminder: frig off') + + +@pytest.mark.slow +def test_remind_at(rbot): + then = datetime.datetime.now() + datetime.timedelta(seconds=3) + rbot.feed_line(".at {} frig off".format(then.strftime("%H:%M:%SPDT"))) + rbot.act_PRIVMSG.assert_called_once_with('#test', 'chatter: Ok, will do. Approx 0h0m to go.') + rbot.act_PRIVMSG.reset_mock() + sleep(2) + rbot.act_PRIVMSG.assert_not_called() + sleep(2) + rbot.act_PRIVMSG.assert_called_once_with('#test', 'chatter: Reminder: frig off') diff --git a/tests/modules/test_tell.py b/tests/modules/test_tell.py index 140511a..99da257 100644 --- a/tests/modules/test_tell.py +++ b/tests/modules/test_tell.py @@ -11,7 +11,7 @@ def tellbot(fakebot): fakebot.botconfig["module_configs"]["Tell"] = {"max": 10, "maxage": 2678400} fakebot.loadmodule("SQLite") with closing(fakebot.moduleInstances["SQLite"].opendb("tell.db")) as db: - db.query("DROP TABLE tells;") + db.query("DROP TABLE IF EXISTS tells;") fakebot.loadmodule("Tell") return fakebot diff --git a/tests/test_rpcclient.py b/tests/test_rpcclient.py new file mode 100644 index 0000000..a13537c --- /dev/null +++ b/tests/test_rpcclient.py @@ -0,0 +1,87 @@ +from tests.lib import * # NOQA - fixtures + +from unittest.mock import MagicMock, call +from pyircbot.rpc import BotRPC +from pyircbot.rpcclient import connect +from random import randint +from time import sleep + + +def test_rpc(monkeypatch): + port = randint(40000, 65000) + m = MagicMock() + m.botconfig = {"bot": {"rpcbind": "127.0.0.1", "rpcport": port}} + server = BotRPC(m) + sleep(0.05) + + calltrack = MagicMock() + + def fake(*args): + calltrack(*args) + return args + + for k, v in server.server.funcs.items(): + server.server.funcs[k] = fake + + methods = [["importModule", "foo"], + ["deportModule", "foo"], + ["loadModule", "foo"], + ["unloadModule", "foo"], + ["reloadModule", "foo"], + ["redoModule", "foo"], + ["getLoadedModules"], + ["pluginCommand", "foo", "foo", "foo"], + ["setPluginVar", "foo", "foo"], + ["getPluginVar", "foo", "foo", "foo"], + ["eval", "foo"], + ["exec", "foo"], + ["quit", "foo"]] + + client = connect("127.0.0.1", port) + + for test in methods: + method = test[0] + args = test[1:] + server.server.funcs[method] = fake + print("Calling {} with: {}".format(method, args)) + getattr(client, method)(*args) + calltrack.assert_called_once_with(*args) + calltrack.reset_mock() + + +def test_rpc_internal(monkeypatch): + port = randint(40000, 65000) + m = MagicMock() + m.botconfig = {"bot": {"rpcbind": "127.0.0.1", "rpcport": port}} + server = BotRPC(m) + + methods = [["importModule", "foo"], + ["deportModule", "foo"], + ["loadModule", "foo"], + ["unloadModule", "foo"], + ["redoModule", "foo"],] + + for test in methods: + method = test[0] + args = test[1:] + getattr(server, method)(*args) + getattr(m, method.lower()).assert_called_once_with(*args) + getattr(m, method.lower()).reset_mock() + + m.moduleInstances = {"Foo": None, "Bar": None} + assert server.getLoadedModules() == ["Foo", "Bar"] + + m.reset_mock() + + server.reloadModule("Foo") + m.unloadmodule.assert_called_once_with("Foo") + m.loadmodule.assert_called_once_with("Foo") + + m.reset_mock() + + # ["pluginCommand", "foo", "foo", "foo"], + # ["setPluginVar", "foo", "foo"], + # ["getPluginVar", "foo", "foo", "foo"] + # ["eval", "foo"], + # ["exec", "foo"], + # ["quit", "foo"]]