From a8f8d7c07728756069ad7c9752581f868f47ffc0 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 19 Dec 2015 23:09:06 +0100 Subject: [PATCH] Start supporting server testing. --- irctest/__main__.py | 8 ++-- irctest/basecontrollers.py | 32 ++++++++++++++++ irctest/cases.py | 66 +++++++++++++++++++++++++++++--- irctest/controllers/inspircd.py | 35 +++++++++++++++++ irctest/controllers/limnoria.py | 31 ++------------- irctest/server_tests/__init__.py | 8 ++++ irctest/server_tests/test_cap.py | 12 ++++++ setup.py | 1 + 8 files changed, 155 insertions(+), 38 deletions(-) create mode 100644 irctest/controllers/inspircd.py create mode 100644 irctest/server_tests/__init__.py create mode 100644 irctest/server_tests/test_cap.py diff --git a/irctest/__main__.py b/irctest/__main__.py index 61458f9..580f2c6 100644 --- a/irctest/__main__.py +++ b/irctest/__main__.py @@ -17,11 +17,11 @@ def main(args): controller_class = module.get_irctest_controller_class() if issubclass(controller_class, BaseClientController): module = 'irctest.client_tests' - elif issubclass(controller_class, BaseClientController): - module = 'irctest.servertests' + elif issubclass(controller_class, BaseServerController): + module = 'irctest.server_tests' else: - print('{}.Controller should be a subclass of ' - 'irctest.basecontroller.Base{Client,Server}Controller' + print(r'{}.Controller should be a subclass of ' + r'irctest.basecontroller.Base{{Client,Server}}Controller' .format(args.module), file=sys.stderr) exit(1) diff --git a/irctest/basecontrollers.py b/irctest/basecontrollers.py index 9d3ab3f..d280843 100644 --- a/irctest/basecontrollers.py +++ b/irctest/basecontrollers.py @@ -1,6 +1,38 @@ +import os +import shutil +import socket +import tempfile +import subprocess + class _BaseController: pass +class DirectoryBasedController(_BaseController): + def __init__(self): + super().__init__() + self.directory = None + self.proc = None + def kill(self): + if self.proc: + self.proc.terminate() + try: + self.proc.wait(5) + except subprocess.TimeoutExpired: + self.proc.kill() + self.proc = None + if self.directory: + shutil.rmtree(self.directory) + def open_file(self, name, mode='a'): + assert self.directory + if os.sep in name: + dir_ = os.path.join(self.directory, os.path.dirname(name)) + if not os.path.isdir(dir_): + os.makedirs(dir_) + assert os.path.isdir(dir_) + return open(os.path.join(self.directory, name), mode) + def create_config(self): + self.directory = tempfile.mkdtemp() + class BaseClientController(_BaseController): def run(self, hostname, port, auth): raise NotImplementedError() diff --git a/irctest/cases.py b/irctest/cases.py index 3f9b85f..6c8bc82 100644 --- a/irctest/cases.py +++ b/irctest/cases.py @@ -1,5 +1,6 @@ import socket import unittest +import collections from . import authentication from .irc_utils import message_parser @@ -9,18 +10,18 @@ class _IrcTestCase(unittest.TestCase): def getLine(self): raise NotImplementedError() - def getMessage(self, filter_pred=None): + def getMessage(self, *args, filter_pred=None): """Gets a message and returns it. If a filter predicate is given, fetches messages until the predicate returns a False on a message, and returns this message.""" while True: - msg = message_parser.parse_message(self.getLine()) + msg = message_parser.parse_message(self.getLine(*args)) if not filter_pred or filter_pred(msg): return msg class BaseClientTestCase(_IrcTestCase): - """Basic class for client tests. Handles spawning a client and getting - messages from it.""" + """Basic class for client tests. Handles spawning a client and exchanging + messages with it.""" nick = None user = None def setUp(self): @@ -50,9 +51,11 @@ class BaseClientTestCase(_IrcTestCase): print('C: {}'.format(line.strip())) return line def sendLine(self, line): - assert self.conn.sendall(line.encode()) is None + ret = self.conn.sendall(line.encode()) + assert ret is None if not line.endswith('\r\n'): - assert self.conn.sendall(b'\r\n') is None + ret = self.conn.sendall(b'\r\n') + assert ret is None if self.show_io: print('S: {}'.format(line.strip())) @@ -122,3 +125,54 @@ class ClientNegociationHelper: else: return m +Client = collections.namedtuple('Client', + 'conn conn_file') + +class BaseServerTestCase(_IrcTestCase): + """Basic class for server tests. Handles spawning a server and exchanging + messages with it.""" + def setUp(self): + self.find_hostname_and_port() + self.controller = self.controllerClass() + self.controller.run(self.hostname, self.port) + self.clients = {} + def tearDown(self): + self.controller.kill() + for client in list(self.clients): + self.removeClient(client) + def find_hostname_and_port(self): + """Find available hostname/port to listen on.""" + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.bind(("",0)) + (self.hostname, self.port) = s.getsockname() + s.close() + + def addClient(self, name=None): + """Connects a client to the server and adds it to the dict. + If 'name' is not given, uses the lowest unused non-negative integer.""" + if not name: + name = max(map(int, list(self.clients)+[0]))+1 + conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + conn.connect((self.hostname, self.port)) + conn_file = conn.makefile(newline='\r\n', encoding='utf8') + self.clients[name] = Client(conn=conn, conn_file=conn_file) + + def removeClient(self, name): + assert name in self.clients + self.clients[name].conn.close() + del self.clients[name] + + def getLine(self, client): + assert client in self.clients + line = self.clients[client].conn_file.readline() + if self.show_io: + print('S -> {}: {}'.format(client, line.strip())) + return line + def sendLine(self, client, line): + ret = self.clients[client].conn.sendall(line.encode()) + assert ret is None + if not line.endswith('\r\n'): + ret = self.clients[client].conn.sendall(b'\r\n') + assert ret is None + if self.show_io: + print('{} -> S: {}'.format(client, line.strip())) diff --git a/irctest/controllers/inspircd.py b/irctest/controllers/inspircd.py new file mode 100644 index 0000000..5ffaaca --- /dev/null +++ b/irctest/controllers/inspircd.py @@ -0,0 +1,35 @@ +import os +import time +import shutil +import tempfile +import subprocess + +from irctest import authentication +from irctest.basecontrollers import BaseServerController, DirectoryBasedController + +TEMPLATE_CONFIG = """ + + +""" + +class InspircdController(BaseServerController, DirectoryBasedController): + def create_config(self): + super().create_config() + with self.open_file('server.conf'): + pass + + def run(self, hostname, port): + assert self.proc is None + self.create_config() + with self.open_file('server.conf') as fd: + fd.write(TEMPLATE_CONFIG.format( + hostname=hostname, + port=port, + )) + self.proc = subprocess.Popen(['inspircd', '--nofork', '--config', + os.path.join(self.directory, 'server.conf')]) + time.sleep(0.1) # FIXME: do better than this to wait for InspIRCd to start + +def get_irctest_controller_class(): + return InspircdController + diff --git a/irctest/controllers/limnoria.py b/irctest/controllers/limnoria.py index 774c880..960dde3 100644 --- a/irctest/controllers/limnoria.py +++ b/irctest/controllers/limnoria.py @@ -1,10 +1,8 @@ import os -import shutil -import tempfile import subprocess from irctest import authentication -from irctest.basecontrollers import BaseClientController +from irctest.basecontrollers import BaseClientController, DirectoryBasedController TEMPLATE_CONFIG = """ supybot.log.stdout.level: {loglevel} @@ -15,32 +13,9 @@ supybot.networks.testnet.sasl.password: {password} supybot.networks.testnet.sasl.mechanisms: {mechanisms} """ -class LimnoriaController(BaseClientController): - def __init__(self): - super().__init__() - self.directory = None - self.proc = None - def kill(self): - if self.proc: - self.proc.terminate() - try: - self.proc.wait(5) - except subprocess.TimeoutExpired: - self.proc.kill() - self.proc = None - if self.directory: - shutil.rmtree(self.directory) - def open_file(self, name, mode='a'): - assert self.directory - if os.sep in name: - dir_ = os.path.join(self.directory, os.path.dirname(name)) - if not os.path.isdir(dir_): - os.makedirs(dir_) - assert os.path.isdir(dir_) - return open(os.path.join(self.directory, name), mode) - +class LimnoriaController(BaseClientController, DirectoryBasedController): def create_config(self): - self.directory = tempfile.mkdtemp() + super().create_config() with self.open_file('bot.conf'): pass with self.open_file('conf/users.conf'): diff --git a/irctest/server_tests/__init__.py b/irctest/server_tests/__init__.py new file mode 100644 index 0000000..ba11145 --- /dev/null +++ b/irctest/server_tests/__init__.py @@ -0,0 +1,8 @@ +import os +import unittest + + +def discover(): + ts = unittest.TestSuite() + ts.addTests(unittest.defaultTestLoader.discover(os.path.dirname(__file__))) + return ts diff --git a/irctest/server_tests/test_cap.py b/irctest/server_tests/test_cap.py new file mode 100644 index 0000000..feb8445 --- /dev/null +++ b/irctest/server_tests/test_cap.py @@ -0,0 +1,12 @@ +from irctest import cases +from irctest.irc_utils.message_parser import Message + +class CapTestCase(cases.BaseServerTestCase): + def testNoCap(self): + self.addClient(1) + self.sendLine(1, 'CAP LS 302') + while True: # Ignore boring connection messages + m = self.getMessage(1) + if m.command != 'NOTICE': + break + print(m) diff --git a/setup.py b/setup.py index 026a7fc..d9e692b 100755 --- a/setup.py +++ b/setup.py @@ -43,6 +43,7 @@ setup( 'irctest.client_tests', 'irctest.controllers', 'irctest.irc_utils', + 'irctest.server_tests', ], )