diff options
| -rw-r--r-- | Mailman/bin/testall.py | 59 | ||||
| -rw-r--r-- | Mailman/configuration.py | 2 | ||||
| -rw-r--r-- | Mailman/initialize.py | 15 | ||||
| -rw-r--r-- | Mailman/testing/Makefile.in | 3 | ||||
| -rw-r--r-- | Mailman/testing/base.py | 45 | ||||
| -rw-r--r-- | Mailman/testing/emailbase.py | 13 | ||||
| -rw-r--r-- | Mailman/testing/inmemory.py | 488 | ||||
| -rw-r--r-- | Mailman/testing/test_enum.py | 3 | ||||
| -rw-r--r-- | Mailman/testing/testing.cfg.in | 14 |
9 files changed, 575 insertions, 67 deletions
diff --git a/Mailman/bin/testall.py b/Mailman/bin/testall.py index 61fa98dd8..b6dc07ec2 100644 --- a/Mailman/bin/testall.py +++ b/Mailman/bin/testall.py @@ -17,15 +17,26 @@ """Mailman unit test driver.""" +from __future__ import with_statement + import os import re +import grp +import pwd import sys +import shutil import optparse +import tempfile import unittest +import Mailman +import Mailman.testing + from Mailman import Version +from Mailman.configuration import config +from Mailman.database.dbcontext import dbcontext from Mailman.i18n import _ -from Mailman.initialize import initialize +from Mailman.initialize import initialize_1, initialize_2 __i18n_templates__ = True @@ -63,8 +74,6 @@ Reduce verbosity by 1 (but not below 0).""")) parser.add_option('-e', '--stderr', default=False, action='store_true', help=_('Propagate log errors to stderr.')) - parser.add_option('-C', '--config', - help=_('Alternative configuration file to use')) opts, args = parser.parse_args() return parser, opts, args @@ -137,14 +146,48 @@ def main(): global basedir parser, opts, args = parseargs() - initialize(opts.config, propagate_logs=opts.stderr) if not args: args = ['.'] - import Mailman - basedir = os.path.dirname(Mailman.__file__) - runner = unittest.TextTestRunner(verbosity=opts.verbosity) - results = runner.run(suite(args)) + # Set up the testing configuration file both for this process, and for all + # sub-processes testing will spawn (e.g. the qrunners). + # + # Calculate various temporary files needed by the test suite, but only for + # those files which must also go into shared configuration file. + cfg_in = os.path.join(os.path.dirname(Mailman.testing.__file__), + 'testing.cfg.in') + fd, cfg_out = tempfile.mkstemp(suffix='.cfg') + os.close(fd) + shutil.copyfile(cfg_in, cfg_out) + + initialize_1(cfg_out, propagate_logs=opts.stderr) + mailman_uid = pwd.getpwnam(config.MAILMAN_USER).pw_uid + mailman_gid = grp.getgrnam(config.MAILMAN_GROUP).gr_gid + os.chmod(cfg_out, 0660) + os.chown(cfg_out, mailman_uid, mailman_gid) + + fd, config.dbfile = tempfile.mkstemp(dir=config.DATA_DIR, suffix='.db') + os.close(fd) + os.chmod(config.dbfile, 0660) + os.chown(config.dbfile, mailman_uid, mailman_gid) + + # Patch ups + test_engine_url = 'sqlite:///' + config.dbfile + config.SQLALCHEMY_ENGINE_URL = test_engine_url + + with open(cfg_out, 'a') as fp: + print >> fp, 'SQLALCHEMY_ENGINE_URL = "%s"' % test_engine_url + + initialize_2() + + try: + basedir = os.path.dirname(Mailman.__file__) + runner = unittest.TextTestRunner(verbosity=opts.verbosity) + results = runner.run(suite(args)) + finally: + os.remove(cfg_out) + os.remove(config.dbfile) + sys.exit(bool(results.failures or results.errors)) diff --git a/Mailman/configuration.py b/Mailman/configuration.py index a489025ca..a0d35e483 100644 --- a/Mailman/configuration.py +++ b/Mailman/configuration.py @@ -72,12 +72,14 @@ class Configuration(object): path = os.path.abspath(os.path.expanduser(filename)) try: execfile(path, ns, ns) + self.filename = path except EnvironmentError, e: if e.errno <> errno.ENOENT or original_filename: raise # The file didn't exist, so try mm_cfg.py from Mailman import mm_cfg ns.update(mm_cfg.__dict__) + self.filename = None # Based on values possibly set in mailman.cfg, add additional qrunners if ns['USE_MAILDIR']: self.add_qrunner('Maildir') diff --git a/Mailman/initialize.py b/Mailman/initialize.py index ddd4b1c3e..a47fbd045 100644 --- a/Mailman/initialize.py +++ b/Mailman/initialize.py @@ -32,7 +32,12 @@ import Mailman.loginit -def initialize(config=None, propagate_logs=False): +# These initialization calls are separated for the testing framework, which +# needs to do some internal calculations after config file loading and log +# initialization, but before database initialization. Generally all other +# code will just call initialize(). + +def initialize_1(config, propagate_logs): # By default, set the umask so that only owner and group can read and # write our files. Specifically we must have g+rw and we probably want # o-rwx although I think in most cases it doesn't hurt if other can read @@ -42,4 +47,12 @@ def initialize(config=None, propagate_logs=False): os.umask(007) Mailman.configuration.config.load(config) Mailman.loginit.initialize(propagate_logs) + + +def initialize_2(): Mailman.database.initialize() + + +def initialize(config=None, propagate_logs=False): + initialize_1(config, propagate_logs) + initialize_2() diff --git a/Mailman/testing/Makefile.in b/Mailman/testing/Makefile.in index 2bbfdc7c0..1326f67e5 100644 --- a/Mailman/testing/Makefile.in +++ b/Mailman/testing/Makefile.in @@ -42,6 +42,7 @@ PACKAGEDIR= $(prefix)/Mailman/testing SHELL= /bin/sh MODULES= *.py +OTHERFILES= testing.cfg.in # Modes for directories and executables created by the install # process. Default to group-writable directories but @@ -59,7 +60,7 @@ SUBDIRS= bounces all: install: - for f in $(MODULES); \ + for f in $(MODULES) $(OTHERFILES); \ do \ $(INSTALL) -m $(FILEMODE) $(srcdir)/$$f $(DESTDIR)$(PACKAGEDIR); \ done diff --git a/Mailman/testing/base.py b/Mailman/testing/base.py index 8a131175e..562443b1e 100644 --- a/Mailman/testing/base.py +++ b/Mailman/testing/base.py @@ -41,26 +41,7 @@ NL = '\n' -def dummy_mta_function(*args, **kws): - pass - - - class TestBase(unittest.TestCase): - def _configure(self, fp): - # Make sure that we don't pollute the real database with our test - # mailing list. - test_engine_url = 'sqlite:///' + self._dbfile - print >> fp, 'SQLALCHEMY_ENGINE_URL = "%s"' % test_engine_url - config.SQLALCHEMY_ENGINE_URL = test_engine_url - # Use the Mailman.MTA.stub module - print >> fp, 'MTA = "stub"' - config.MTA = 'stub' - print >> fp, 'add_domain("example.com", "www.example.com")' - # Only add this domain once to the current process - if 'example.com' not in config.domains: - config.add_domain('example.com', 'www.example.com') - def ndiffAssertEqual(self, first, second): """Like failUnlessEqual except use ndiff for readable output.""" if first <> second: @@ -72,30 +53,6 @@ class TestBase(unittest.TestCase): raise self.failureException(fp.getvalue()) def setUp(self): - mailman_uid = pwd.getpwnam(config.MAILMAN_USER).pw_uid - mailman_gid = grp.getgrnam(config.MAILMAN_GROUP).gr_gid - # Write a temporary configuration file, but allow for subclasses to - # add additional data. Make sure the config and db files, which - # mkstemp creates, has the proper ownership and permissions. - fd, self._config = tempfile.mkstemp(dir=config.DATA_DIR, suffix='.cfg') - os.close(fd) - os.chmod(self._config, 0440) - os.chown(self._config, mailman_uid, mailman_gid) - fd, self._dbfile = tempfile.mkstemp(dir=config.DATA_DIR, suffix='.db') - os.close(fd) - os.chmod(self._dbfile, 0660) - os.chown(self._dbfile, mailman_uid, mailman_gid) - fp = open(self._config, 'w') - try: - self._configure(fp) - finally: - fp.close() - # Create a fake new Mailman.MTA module which stubs out the create() - # and remove() functions. - stubmta_module = new.module('Mailman.MTA.stub') - sys.modules['Mailman.MTA.stub'] = stubmta_module - stubmta_module.create = dummy_mta_function - stubmta_module.remove = dummy_mta_function # Be sure to close the connection to the current database, and then # reconnect to the new temporary SQLite database. Otherwise we end up # with turds in the main database and our qrunner subprocesses won't @@ -116,8 +73,6 @@ class TestBase(unittest.TestCase): self._mlist.Unlock() rmlist.delete_list(self._mlist.fqdn_listname, self._mlist, archives=True, quiet=True) - os.unlink(self._config) - os.unlink(self._dbfile) # Clear out any site locks, which can be left over if tests fail. for filename in os.listdir(config.LOCK_DIR): if filename.startswith('<site>'): diff --git a/Mailman/testing/emailbase.py b/Mailman/testing/emailbase.py index e033195de..d0fdbf7d4 100644 --- a/Mailman/testing/emailbase.py +++ b/Mailman/testing/emailbase.py @@ -52,15 +52,6 @@ class SinkServer(smtpd.SMTPServer): class EmailBase(TestBase): - def _configure(self, fp): - TestBase._configure(self, fp) - print >> fp, 'SMTPPORT =', TESTPORT - config.SMTPPORT = TESTPORT - # Don't go nuts on mailmanctl restarts. If a qrunner fails once, it - # will keep failing. - print >> fp, 'MAX_RESTARTS = 1' - config.MAX_RESTARTS = 1 - def setUp(self): TestBase.setUp(self) try: @@ -70,7 +61,7 @@ class EmailBase(TestBase): TestBase.tearDown(self) raise try: - os.system('bin/mailmanctl -C %s -q start' % self._config) + os.system('bin/mailmanctl -C %s -q start' % config.filename) # If any errors occur in the above, be sure to manually call # tearDown(). unittest doesn't call tearDown() for errors in # setUp(). @@ -79,7 +70,7 @@ class EmailBase(TestBase): raise def tearDown(self): - os.system('bin/mailmanctl -C %s -q stop' % self._config) + os.system('bin/mailmanctl -C %s -q stop' % config.filename) self._server.close() # Wait a while until the server actually goes away while True: diff --git a/Mailman/testing/inmemory.py b/Mailman/testing/inmemory.py new file mode 100644 index 000000000..ca5313452 --- /dev/null +++ b/Mailman/testing/inmemory.py @@ -0,0 +1,488 @@ +# Copyright (C) 2007 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. + +"""In-memory implementations of Mailman interfaces, for testing purposes.""" + +import datetime +import urlparse + +from Mailman import Utils +from Mailman import passwords +from Mailman.interfaces import * + +from zope.interface import implements + + + +class UserManager(object): + implements(IUserManager) + + def __init__(self): + self._users = set() + self._next_id = 1 + + @property + def users(self): + for user in self._users: + yield user + + def create_user(self): + user = User(self._next_id, self) + self._next_id += 1 + self._users.add(user) + return user + + def remove(self, user): + self._users.discard(user) + + def get(self, address): + # Yes, this is slow and icky, but it's only for testing purposes + for user in self._users: + if user.controls(address): + return user + return None + + + +class User(object): + implements(IUser) + + def __init__(self, user_id, user_mgr): + self._user_id = user_id + self._user_mgr = user_mgr + self._addresses = set() + self.real_name = u'' + self.password = passwords.NoPasswordScheme.make_secret('ignore') + self.default_profile = None + + def __eq__(self, other): + return (IUser.implementedBy(other) and + self.user_id == other.user_id and + self.user_manager is other.user_manager) + + def __ne__(self, other): + return not self.__eq__(other) + + @property + def user_id(self): + return self._user_id + + @property + def user_manager(self): + return self._user_mgr + + @property + def addresses(self): + for address in self._addresses: + yield address + + def add_address(self, address): + if self.controls(address): + return + user_address = Address(address, self) + self._addresses.add(user_address) + + def remove_address(self, address): + if not self.controls(address): + return + user_address = Address(address, self) + self._addresses.discard(user_address) + + def controls(self, address): + for user_address in self.addresses: + if user_address == address: + return True + return False + + + +class Address(object): + implements(IAddress) + + def __init__(self, email_address, user, profile=None): + self._address = email_address + self._user = user + self.profile = profile or Profile() + self.validated_on = None + + def __eq__(self, other): + return (IAddress.implementedBy(other) and + self.address == other.address and + self.user == other.user) + + @property + def address(self): + return self._address + + @property + def user(self): + return self._user + + + +class RegularDelivery(object): + implements(IRegularDelivery) + + +class PlainTextDigestDelivery(object): + implements(IPlainTextDigestDelivery) + + +class MIMEDigestDelivery(object): + implements(IMIMEDigestDeliver) + + + +class DeliveryEnabled(object): + implements(IDeliveryStatus) + + @property + def enabled(self): + return True + + +class DeliveryDisabled(object): + implements(IDeliveryStatus) + + @property + def enabled(self): + return False + + +class DeliveryDisabledByUser(DeliveryDisabled): + implements(IDeliveryDisabledByUser) + + +class DeliveryDisabledbyAdministrator(DeliveryDisabled): + implements(IDeliveryDisabledByAdministrator) + + reason = u'Unknown' + + +class DeliveryDisabledByBounces(DeliveryDisabled): + implements(IDeliveryDisabledByBounces) + + bounce_info = 'XXX' + + +class DeliveryTemporarilySuspended(object): + implements(IDeliveryTemporarilySuspended) + + def __init__(self, start_date, end_date): + self.start_date = start_date + self.end_date = end_date + + @property + def enabled(self): + now = datetime.datetime.now() + return not (self.start_date <= now < self.end_date) + + + +class OkayToPost(object): + implements(IPostingPermission) + + # XXX + okay_to_post = True + + + +class Profile(object): + implements(IProfile) + + # System defaults + acknowledge = False + hide = True + language = 'en' + list_copy = True + own_postings = True + delivery_mode = RegularDelivery() + delivery_status = DeliveryEnabled() + posting_permission = OkayToPost() + + + +class Roster(object): + implements(IRoster) + + def __init__(self, name): + self._name = name + self._members = set() + + def __eq__(self, other): + return (IRoster.implementedBy(other) and + self.name == other.name) + + def __ne__(self, other): + return not self.__eq__(other) + + @property + def name(self): + return self._name + + def add(self, member): + self._members.add(member) + + def remove(self, member): + self._members.remove(member) + + @property + def members(self): + for member in self._members: + yield member + + + +class Member(object): + implements(IMember) + + def __init__(self, address, roster, profile=None): + self._address = address + self._roster = roster + self.profile = profile or Profile() + + @property + def address(self): + return self._address + + @property + def roster(self): + return self._roster + + + +class ListManager(object): + implements(IListManager) + + def __init__(self): + self._mlists = {} + + def add(self, mlist): + self._mlists[mlist.fqdn_listname] = mlist + + def remove(self, mlist): + del self._mlists[mlist.fqdn_listname] + + @property + def mailing_lists(self): + return self._mlists.itervalues() + + @property + def names(self): + return self._mlists.iterkeys() + + def get(self, fqdn_listname): + return self._mlists.get(fqdn_listname) + + + +class MailingList(object): + implements(IMailingListIdentity, + IMailingListAddresses, + IMailingListURLs, + IMailingListRosters, + IMailingListStatistics, + ) + + def __init__(self, list_name, host_name, web_host): + self._listname = list_name + self._hostname = hostname + self._webhost = web_host + self._fqdn_listname = Utils.fqdn_listname(list_name, host_name) + # Rosters + self._owners = set(Roster(self.owner_address)) + self._moderators = set(Roster(self._listname + '-moderators@' + + self._hostname)) + self._members = set(Roster(self.posting_address)) + # Statistics + self._created_on = datetime.datetime.now() + self._last_posting = None + self._post_number = 0 + self._last_digest = None + + # IMailingListIdentity + + @property + def list_name(self): + return self._listname + + @property + def host_name(self): + return self._hostname + + @property + def fqdn_listname(self): + return self._fqdn_listname + + # IMailingListAddresses + + @property + def posting_address(self): + return self._fqdn_listname + + @property + def noreply_address(self): + return self._listname + '-noreply@' + self._hostname + + @property + def owner_address(self): + return self._listname + '-owner@' + self._hostname + + @property + def request_address(self): + return self._listname + '-request@' + self._hostname + + @property + def bounces_address(self): + return self._listname + '-bounces@' + self._hostname + + @property + def confirm_address(self): + return self._listname + '-confirm@' + self._hostname + + @property + def join_address(self): + return self._listname + '-join@' + self._hostname + + @property + def leave_address(self): + return self._listname + '-leave@' + self._hostname + + @property + def subscribe_address(self): + return self._listname + '-subscribe@' + self._hostname + + @property + def unsubscribe_address(self): + return self._listname + '-unsubscribe@' + self._hostname + + # IMailingListURLs + + protocol = 'http' + + @property + def web_host(self): + return self._webhost + + def script_url(self, target, context=None): + if context is None: + return urlparse.urlunsplit((self.protocol, self.web_host, target, + # no extra query or fragment + '', '')) + return urlparse.urljoin(context.location, target) + + # IMailingListRosters + + @property + def owner_rosters(self): + return iter(self._owners) + + @property + def moderator_rosters(self): + return iter(self._moderators) + + @property + def member_rosters(self): + return iter(self._members) + + def add_owner_roster(self, roster): + self._owners.add(roster) + + def add_moderator_roster(self, roster): + self._moderators.add(roster) + + def add_member_roster(self, roster): + self._members.add(roster) + + def remove_owner_roster(self, roster): + self._owners.discard(roster) + + def remove_moderator_roster(self, roster): + self._moderators.discard(roster) + + def remove_member_roster(self, roster): + self._members.discard(roster) + + @property + def owners(self): + for roster in self._owners: + for member in roster.members: + yield member + + @property + def moderators(self): + for roster in self._moderators: + for member in roster.members: + yield member + + @property + def administrators(self): + for member in self.owners: + yield member + for member in self.moderators: + yield member + + @property + def members(self): + for roster in self._members: + for member in roster.members: + yield member + + @property + def regular_members(self): + for member in self.members: + if IRegularDelivery.implementedBy(member.profile.delivery_mode): + yield member + + @property + def digest_member(self): + for member in self.members: + if IDigestDelivery.implementedBy(member.profile.delivery_mode): + yield member + + # Statistic + + @property + def creation_date(self): + return self._created_on + + @property + def last_post_date(self): + return self._last_posting + + @property + def post_number(self): + return self._post_number + + @property + def last_digest_date(self): + return self._last_digest + + + +class MailingListRequest(object): + implements(IMailingListRequest) + + location = '' + + + +def initialize(): + from Mailman.configuration import config + config.user_manager = UserManager() + config.list_manager = ListManager() + config.message_manager = None diff --git a/Mailman/testing/test_enum.py b/Mailman/testing/test_enum.py index de6877afc..a8c389bb4 100644 --- a/Mailman/testing/test_enum.py +++ b/Mailman/testing/test_enum.py @@ -93,9 +93,10 @@ class TestEnum(unittest.TestCase): eq(int(Colors.blue), 3) eq(int(MoreColors.red), 1) eq(int(OtherColors.blue), 2) - + def test_enum_duplicates(self): try: + # This is bad because kyle and kenny have the same integer value. class Bad(Enum): cartman = 1 stan = 2 diff --git a/Mailman/testing/testing.cfg.in b/Mailman/testing/testing.cfg.in new file mode 100644 index 000000000..80e5e8bfc --- /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. + +MANAGERS_INIT_FUNCTION = 'Mailman.testing.inmemory.initialize' +SMTPPORT = 10825 +MAX_RESTARTS = 1 +MTA = None + +add_domain('example.com', 'www.example.com') + +# bin/testall will add a SQLALCHEMY_ENGINE_URL below |
