diff options
Diffstat (limited to 'mailman/testing')
| -rw-r--r-- | mailman/testing/__init__.py | 0 | ||||
| -rw-r--r-- | mailman/testing/helpers.py | 237 | ||||
| -rw-r--r-- | mailman/testing/smtplistener.py | 86 | ||||
| -rw-r--r-- | mailman/testing/testing.cfg.in | 14 |
4 files changed, 337 insertions, 0 deletions
diff --git a/mailman/testing/__init__.py b/mailman/testing/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/mailman/testing/__init__.py diff --git a/mailman/testing/helpers.py b/mailman/testing/helpers.py new file mode 100644 index 000000000..af687828f --- /dev/null +++ b/mailman/testing/helpers.py @@ -0,0 +1,237 @@ +# Copyright (C) 2008 by the Free Software Foundation, Inc. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, +# USA. + +"""Various test helpers.""" + +from __future__ import with_statement + +__metaclass__ = type +__all__ = [ + 'TestableMaster', + 'digest_mbox', + 'get_lmtp_client', + 'get_queue_messages', + 'make_testable_runner', + ] + + +import os +import time +import errno +import signal +import socket +import logging +import mailbox +import smtplib +import tempfile +import threading + +from Queue import Empty, Queue +from datetime import datetime, timedelta + +from mailman.bin.master import Loop as Master +from mailman.configuration import config +from mailman.queue import Switchboard +from mailman.testing.smtplistener import Server + + +log = logging.getLogger('mailman.debug') + + + +def make_testable_runner(runner_class): + """Create a queue runner that runs until its queue is empty. + + :param runner_class: An IRunner + :return: A runner instance. + """ + + class EmptyingRunner(runner_class): + """Stop processing when the queue is empty.""" + + def _doperiodic(self): + """Stop when the queue is empty.""" + self._stop = (len(self._switchboard.files) == 0) + + return EmptyingRunner() + + + +class _Bag: + def __init__(self, **kws): + for key, value in kws.items(): + setattr(self, key, value) + + +def get_queue_messages(queue): + """Return and clear all the messages in the given queue. + + :param queue: An ISwitchboard or a string naming a queue. + :return: A list of 2-tuples where each item contains the message and + message metadata. + """ + if isinstance(queue, basestring): + queue = Switchboard(queue) + messages = [] + for filebase in queue.files: + msg, msgdata = queue.dequeue(filebase) + messages.append(_Bag(msg=msg, msgdata=msgdata)) + queue.finish(filebase) + return messages + + + +def digest_mbox(mlist): + """The mailing list's pending digest as a mailbox. + + :param mlist: The mailing list. + :return: The mailing list's pending digest as a mailbox. + """ + path = os.path.join(mlist.data_path, 'digest.mbox') + return mailbox.mbox(path) + + + +class TestableMaster(Master): + """A testable master loop watcher.""" + + def __init__(self): + super(TestableMaster, self).__init__( + restartable=False, config_file=config.filename) + self.event = threading.Event() + self.thread = threading.Thread(target=self.loop) + self._started_kids = None + + def start(self, *qrunners): + """Start the master.""" + self.start_qrunners(qrunners) + self.thread.start() + # Wait until all the children are definitely started. + self.event.wait() + + def stop(self): + """Stop the master by killing all the children.""" + for pid in self.qrunner_pids: + os.kill(pid, signal.SIGTERM) + self.cleanup() + self.thread.join() + + def loop(self): + """Wait until all the qrunners are actually running before looping.""" + starting_kids = set(self._kids) + while starting_kids: + for pid in self._kids: + try: + os.kill(pid, 0) + starting_kids.remove(pid) + except OSError, error: + if error.errno == errno.ESRCH: + # The child has not yet started. + pass + raise + # Keeping a copy of all the started child processes for use by the + # testing environment, even after all have exited. + self._started_kids = set(self._kids) + # Let the blocking thread know everything's running. + self.event.set() + super(TestableMaster, self).loop() + + @property + def qrunner_pids(self): + """The pids of all the child qrunner processes.""" + for pid in self._started_kids: + yield pid + + + +class SMTPServer: + """An smtp server for testing.""" + + host = 'localhost' + port = 9025 + + def __init__(self): + self._messages = [] + self._queue = Queue() + self._server = Server((self.host, self.port), self._queue) + self._thread = threading.Thread(target=self._server.start) + + def start(self): + """Start the smtp server in a thread.""" + log.info('test SMTP server starting') + self._thread.start() + + def stop(self): + """Stop the smtp server.""" + smtpd = smtplib.SMTP() + smtpd.connect(self.host, self.port) + smtpd.docmd('EXIT') + self.clear() + # Wait for the thread to exit. + self._thread.join() + log.info('test SMTP server stopped') + + @property + def messages(self): + """Return all the messages received by the smtp server.""" + # Look at the thread queue and append any messages from there to our + # internal list of messages. + while True: + try: + message = self._queue.get_nowait() + except Empty: + break + else: + self._messages.append(message) + # Now return all the messages we know about. + for message in self._messages: + yield message + + def clear(self): + """Clear all messages from the queue.""" + # Just throw these away. + list(self._messages) + self._messages = [] + + + +class LMTP(smtplib.SMTP): + """Like a normal SMTP client, but for LMTP.""" + def lhlo(self, name=''): + self.putcmd('lhlo', name or self.local_hostname) + code, msg = self.getreply() + self.helo_resp = msg + return code, msg + + +def get_lmtp_client(): + """Return a connected LMTP client.""" + # It's possible the process has started but is not yet accepting + # connections. Wait a little while. + lmtp = LMTP() + for attempts in range(3): + try: + response = lmtp.connect(config.LMTP_HOST, config.LMTP_PORT) + print response + return lmtp + except socket.error, error: + if error[0] == errno.ECONNREFUSED: + time.sleep(1) + else: + raise + else: + raise RuntimeError('Connection refused') diff --git a/mailman/testing/smtplistener.py b/mailman/testing/smtplistener.py new file mode 100644 index 000000000..1dc11e3e0 --- /dev/null +++ b/mailman/testing/smtplistener.py @@ -0,0 +1,86 @@ +# Copyright (C) 2007-2008 by the Free Software Foundation, Inc. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, +# USA. + +"""A test SMTP listener.""" + +import smtpd +import logging +import asyncore + +from email import message_from_string + + +COMMASPACE = ', ' +log = logging.getLogger('mailman.debug') + + + +class Channel(smtpd.SMTPChannel): + """A channel that can reset the mailbox.""" + + def __init__(self, server, conn, addr): + smtpd.SMTPChannel.__init__(self, server, conn, addr) + # Stash this here since the subclass uses private attributes. :( + self._server = server + + def smtp_EXIT(self, arg): + """Respond to a new command EXIT by exiting the server.""" + self.push('250 Ok') + self._server.stop() + + def send(self, data): + """Silence the bloody asynchat/asyncore broken pipe errors!""" + try: + return smtpd.SMTPChannel.send(self, data) + except socket.error: + # Nothing here can affect the outcome, and these messages are just + # plain annoying! So ignore them. + pass + + + +class Server(smtpd.SMTPServer): + """An SMTP server that stores messages to a mailbox.""" + + def __init__(self, localaddr, queue): + smtpd.SMTPServer.__init__(self, localaddr, None) + self._queue = queue + + def handle_accept(self): + """Handle connections by creating our own Channel object.""" + conn, addr = self.accept() + log.info('accepted: %s', addr) + Channel(self, conn, addr) + + def process_message(self, peer, mailfrom, rcpttos, data): + """Process a message by adding it to the mailbox.""" + message = message_from_string(data) + message['X-Peer'] = '%s:%s' % peer + message['X-MailFrom'] = mailfrom + message['X-RcptTo'] = COMMASPACE.join(rcpttos) + log.info('processed message: %s', message.get('message-id', 'n/a')) + self._queue.put(message) + + def start(self): + """Start the asyncore loop.""" + asyncore.loop() + + def stop(self): + """Stop the asyncore loop.""" + asyncore.socket_map.clear() + asyncore.close_all() + self.close() diff --git a/mailman/testing/testing.cfg.in b/mailman/testing/testing.cfg.in new file mode 100644 index 000000000..e7b23ac6d --- /dev/null +++ b/mailman/testing/testing.cfg.in @@ -0,0 +1,14 @@ +# -*- python -*- + +# Configuration file template for the unit test suite. We need this because +# both the process running the tests and all sub-processes (e.g. qrunners) +# must share the same configuration file. + +SMTPPORT = 10825 +MAX_RESTARTS = 1 +MTA = None +USE_LMTP = Yes + +add_domain('example.com', 'www.example.com') + +# bin/testall will add additional runtime configuration variables here. |
