summaryrefslogtreecommitdiff
path: root/src/mailman/testing
diff options
context:
space:
mode:
authorBarry Warsaw2009-01-25 13:01:41 -0500
committerBarry Warsaw2009-01-25 13:01:41 -0500
commiteefd06f1b88b8ecbb23a9013cd223b72ca85c20d (patch)
tree72c947fe16fce0e07e996ee74020b26585d7e846 /src/mailman/testing
parent07871212f74498abd56bef3919bf3e029eb8b930 (diff)
downloadmailman-eefd06f1b88b8ecbb23a9013cd223b72ca85c20d.tar.gz
mailman-eefd06f1b88b8ecbb23a9013cd223b72ca85c20d.tar.zst
mailman-eefd06f1b88b8ecbb23a9013cd223b72ca85c20d.zip
Diffstat (limited to 'src/mailman/testing')
-rw-r--r--src/mailman/testing/__init__.py0
-rw-r--r--src/mailman/testing/helpers.py248
-rw-r--r--src/mailman/testing/layers.py204
-rw-r--r--src/mailman/testing/mta.py46
-rw-r--r--src/mailman/testing/smtplistener.py97
-rw-r--r--src/mailman/testing/testing.cfg87
6 files changed, 682 insertions, 0 deletions
diff --git a/src/mailman/testing/__init__.py b/src/mailman/testing/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/mailman/testing/__init__.py
diff --git a/src/mailman/testing/helpers.py b/src/mailman/testing/helpers.py
new file mode 100644
index 000000000..f92c5f012
--- /dev/null
+++ b/src/mailman/testing/helpers.py
@@ -0,0 +1,248 @@
+# Copyright (C) 2008-2009 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman 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 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman 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
+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+
+"""Various test helpers."""
+
+from __future__ import absolute_import, unicode_literals
+
+__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 threading
+
+from Queue import Empty, Queue
+
+from mailman.bin.master import Loop as Master
+from mailman.config import config
+from mailman.testing.smtplistener import Server
+
+
+log = logging.getLogger('mailman.debug')
+
+
+
+def make_testable_runner(runner_class, name=None):
+ """Create a queue runner that runs until its queue is empty.
+
+ :param runner_class: The queue runner's class.
+ :type runner_class: class
+ :param name: Optional queue name; if not given, it is calculated from the
+ class name.
+ :type name: string or None
+ :return: A runner instance.
+ """
+
+ if name is None:
+ assert runner_class.__name__.endswith('Runner'), (
+ 'Unparseable runner class name: %s' % runner_class.__name__)
+ name = runner_class.__name__[:-6].lower()
+
+ class EmptyingRunner(runner_class):
+ """Stop processing when the queue is empty."""
+
+ def _do_periodic(self):
+ """Stop when the queue is empty."""
+ self._stop = (len(self.switchboard.files) == 0)
+
+ return EmptyingRunner(name)
+
+
+
+class _Bag:
+ def __init__(self, **kws):
+ for key, value in kws.items():
+ setattr(self, key, value)
+
+
+def get_queue_messages(queue_name):
+ """Return and clear all the messages in the given queue.
+
+ :param queue_name: A string naming a queue.
+ :return: A list of 2-tuples where each item contains the message and
+ message metadata.
+ """
+ queue = config.switchboards[queue_name]
+ 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."""
+
+ def __init__(self):
+ self._messages = []
+ self._queue = Queue()
+ self.host = config.mta.smtp_host
+ self.port = int(config.mta.smtp_port)
+ 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()
+ smtpd = smtplib.SMTP()
+ log.info('connecting to %s:%s', self.host, self.port)
+ smtpd.connect(self.host, self.port)
+ response = smtpd.helo('test.localhost')
+ smtpd.quit()
+ log.info('SMTP server is running: %s', response)
+
+ 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.mta.lmtp_host, int(config.mta.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/src/mailman/testing/layers.py b/src/mailman/testing/layers.py
new file mode 100644
index 000000000..19300ba1e
--- /dev/null
+++ b/src/mailman/testing/layers.py
@@ -0,0 +1,204 @@
+# Copyright (C) 2008-2009 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman 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 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman 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
+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+
+"""Mailman test layers."""
+
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'ConfigLayer',
+ 'SMTPLayer',
+ ]
+
+
+import os
+import sys
+import shutil
+import logging
+import tempfile
+
+from pkg_resources import resource_string
+from textwrap import dedent
+
+from mailman.config import config
+from mailman.core import initialize
+from mailman.core.logging import get_handler
+from mailman.i18n import _
+from mailman.testing.helpers import SMTPServer
+from mailman.utilities.string import expand
+
+
+NL = '\n'
+
+
+
+class ConfigLayer:
+ """Layer for pushing and popping test configurations."""
+
+ var_dir = None
+ styles = None
+
+ @classmethod
+ def setUp(cls):
+ # Set up the basic configuration stuff.
+ initialize.initialize_1()
+ assert cls.var_dir is None, 'Layer already set up'
+ # Calculate a temporary VAR_DIR directory so that run-time artifacts
+ # of the tests won't tread on the installation's data. This also
+ # makes it easier to clean up after the tests are done, and insures
+ # isolation of test suite runs.
+ cls.var_dir = tempfile.mkdtemp()
+ # We need a test configuration both for the foreground process and any
+ # child processes that get spawned. lazr.config would allow us to do
+ # it all in a string that gets pushed, and we'll do that for the
+ # foreground, but because we may be spawning processes (such as queue
+ # runners) we'll need a file that we can specify to the with the -C
+ # option. Craft the full test configuration string here, push it, and
+ # also write it out to a temp file for -C.
+ test_config = dedent("""
+ [mailman]
+ var_dir: %s
+ """ % cls.var_dir)
+ # Read the testing config and push it.
+ test_config += resource_string('mailman.testing', 'testing.cfg')
+ config.push('test config', test_config)
+ # Initialize everything else.
+ initialize.initialize_2()
+ initialize.initialize_3()
+ # When stderr debugging is enabled, subprocess root loggers should
+ # also be more verbose.
+ if cls.stderr:
+ test_config += dedent("""
+ [logging.root]
+ propagate: yes
+ level: debug
+ """)
+ # Enable log message propagation and reset the log paths so that the
+ # doctests can check the output.
+ for logger_config in config.logger_configs:
+ sub_name = logger_config.name.split('.')[-1]
+ if sub_name == 'root':
+ continue
+ logger_name = 'mailman.' + sub_name
+ log = logging.getLogger(logger_name)
+ log.propagate = True
+ # Reopen the file to a new path that tests can get at. Instead of
+ # using the configuration file path though, use a path that's
+ # specific to the logger so that tests can find expected output
+ # more easily.
+ path = os.path.join(config.LOG_DIR, sub_name)
+ get_handler(sub_name).reopen(path)
+ log.setLevel(logging.DEBUG)
+ # If stderr debugging is enabled, make sure subprocesses are also
+ # more verbose.
+ if cls.stderr:
+ test_config += expand(dedent("""
+ [logging.$name]
+ propagate: yes
+ level: debug
+ """), dict(name=sub_name, path=path))
+ # zope.testing sets up logging before we get to our own initialization
+ # function. This messes with the root logger, so explicitly set it to
+ # go to stderr.
+ if cls.stderr:
+ console = logging.StreamHandler(sys.stderr)
+ formatter = logging.Formatter(config.logging.root.format,
+ config.logging.root.datefmt)
+ console.setFormatter(formatter)
+ logging.getLogger().addHandler(console)
+ # Write the configuration file for subprocesses and set up the config
+ # object to pass that properly on the -C option.
+ config_file = os.path.join(cls.var_dir, 'test.cfg')
+ with open(config_file, 'w') as fp:
+ fp.write(test_config)
+ print >> fp
+ config.filename = config_file
+
+ @classmethod
+ def tearDown(cls):
+ assert cls.var_dir is not None, 'Layer not set up'
+ config.pop('test config')
+ shutil.rmtree(cls.var_dir)
+ cls.var_dir = None
+
+ @classmethod
+ def testSetUp(cls):
+ pass
+
+ @classmethod
+ def testTearDown(cls):
+ # Reset the database between tests.
+ config.db._reset()
+ # Remove all residual queue files.
+ for dirpath, dirnames, filenames in os.walk(config.QUEUE_DIR):
+ for filename in filenames:
+ os.remove(os.path.join(dirpath, filename))
+ # Clear out messages in the message store.
+ for message in config.db.message_store.messages:
+ config.db.message_store.delete_message(message['message-id'])
+ config.db.commit()
+ # Reset the global style manager.
+ config.style_manager.populate()
+
+ # Flag to indicate that loggers should propagate to the console.
+ stderr = False
+
+ @classmethod
+ def handle_stderr(cls, *ignore):
+ cls.stderr = True
+
+ @classmethod
+ def hack_options_parser(cls):
+ """Hack our way into the zc.testing framework.
+
+ Add our custom command line option parsing into zc.testing's. We do
+ the imports here so that if zc.testing isn't invoked, this stuff never
+ gets in the way. This is pretty fragile, depend on changes in the
+ zc.testing package. There should be a better way!
+ """
+ from zope.testing.testrunner.options import parser
+ parser.add_option(str('-e'), str('--stderr'),
+ action='callback', callback=cls.handle_stderr,
+ help=_('Propagate log errors to stderr.'))
+
+
+
+class SMTPLayer(ConfigLayer):
+ """Layer for starting, stopping, and accessing a test SMTP server."""
+
+ smtpd = None
+
+ @classmethod
+ def setUp(cls):
+ assert cls.smtpd is None, 'Layer already set up'
+ cls.smtpd = SMTPServer()
+ cls.smtpd.start()
+
+ @classmethod
+ def tearDown(cls):
+ assert cls.smtpd is not None, 'Layer not set up'
+ cls.smtpd.clear()
+ cls.smtpd.stop()
+
+ @classmethod
+ def testSetUp(cls):
+ pass
+
+ @classmethod
+ def testTearDown(cls):
+ pass
diff --git a/src/mailman/testing/mta.py b/src/mailman/testing/mta.py
new file mode 100644
index 000000000..a10ba3c81
--- /dev/null
+++ b/src/mailman/testing/mta.py
@@ -0,0 +1,46 @@
+# Copyright (C) 2009 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman 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 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman 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
+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+
+"""Fake MTA for testing purposes."""
+
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'FakeMTA',
+ ]
+
+
+from zope.interface import implements
+
+from mailman.interfaces.mta import IMailTransportAgent
+
+
+
+class FakeMTA:
+ """Fake MTA for testing purposes."""
+
+ implements(IMailTransportAgent)
+
+ def create(self, mlist):
+ pass
+
+ def delete(self, mlist):
+ pass
+
+ def regenerate(self):
+ pass
diff --git a/src/mailman/testing/smtplistener.py b/src/mailman/testing/smtplistener.py
new file mode 100644
index 000000000..2094e20de
--- /dev/null
+++ b/src/mailman/testing/smtplistener.py
@@ -0,0 +1,97 @@
+# Copyright (C) 2007-2009 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman 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 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman 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
+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+
+"""A test SMTP listener."""
+
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'Server',
+ ]
+
+
+import smtpd
+import socket
+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)
+ log.info('[SMTPServer] listening: %s', localaddr)
+ self._queue = queue
+
+ def handle_accept(self):
+ """Handle connections by creating our own Channel object."""
+ conn, addr = self.accept()
+ log.info('[SMTPServer] 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'] = '{0}:{1}'.format(*peer)
+ message['X-MailFrom'] = mailfrom
+ message['X-RcptTo'] = COMMASPACE.join(rcpttos)
+ log.info('[SMTPServer] 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/src/mailman/testing/testing.cfg b/src/mailman/testing/testing.cfg
new file mode 100644
index 000000000..107db86ed
--- /dev/null
+++ b/src/mailman/testing/testing.cfg
@@ -0,0 +1,87 @@
+# Copyright (C) 2008-2009 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman 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 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman 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
+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+
+# A testing configuration.
+
+[mta]
+smtp_port: 9025
+incoming: mailman.testing.mta.FakeMTA
+
+[qrunner.archive]
+max_restarts: 1
+
+[qrunner.bounces]
+max_restarts: 1
+
+[qrunner.command]
+max_restarts: 1
+
+[qrunner.in]
+max_restarts: 1
+
+[qrunner.lmtp]
+max_restarts: 1
+
+[qrunner.maildir]
+max_restarts: 1
+
+[qrunner.news]
+max_restarts: 1
+
+[qrunner.out]
+max_restarts: 1
+
+[qrunner.pipeline]
+max_restarts: 1
+
+[qrunner.retry]
+max_restarts: 1
+
+[qrunner.shunt]
+max_restarts: 1
+
+[qrunner.virgin]
+max_restarts: 1
+
+[archiver.prototype]
+enable: yes
+
+[archiver.mail_archive]
+enable: yes
+base_url: http://go.mail-archive.dev/
+recipient: archive@mail-archive.dev
+
+[archiver.pipermail]
+enable: yes
+base_url: http://www.example.com/pipermail/$listname
+
+[archiver.mhonarc]
+enable: yes
+command: /bin/echo "/usr/bin/mhonarc -add -dbfile $PRIVATE_ARCHIVE_FILE_DIR/${listname}.mbox/mhonarc.db -outdir $VAR_DIR/mhonarc/${listname} -stderr $LOG_DIR/mhonarc -stdout $LOG_DIR/mhonarc -spammode -umask 022"
+
+[domain.example_dot_com]
+email_host: example.com
+base_url: http://lists.example.com
+contact_address: postmaster@example.com
+
+[language.ja]
+description: Japanese
+charset: euc-jp
+
+[language.fr]
+description: French
+charset: iso-8859-1