summaryrefslogtreecommitdiff
path: root/src/mailman/testing
diff options
context:
space:
mode:
Diffstat (limited to 'src/mailman/testing')
-rw-r--r--src/mailman/testing/__init__.py39
-rw-r--r--src/mailman/testing/documentation.py176
-rw-r--r--src/mailman/testing/layers.py2
-rw-r--r--src/mailman/testing/nose.py107
4 files changed, 284 insertions, 40 deletions
diff --git a/src/mailman/testing/__init__.py b/src/mailman/testing/__init__.py
index e6a5047b6..e69de29bb 100644
--- a/src/mailman/testing/__init__.py
+++ b/src/mailman/testing/__init__.py
@@ -1,39 +0,0 @@
-# Copyright (C) 2011-2013 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/>.
-
-"""Set up testing.
-
-This is used as an interface to buildout.cfg's [test] section.
-zope.testrunner supports an initialization variable. It is set to import and
-run the following test initialization method.
-"""
-
-from __future__ import absolute_import, unicode_literals
-
-__metaclass__ = type
-__all__ = [
- 'initialize',
- ]
-
-
-
-def initialize(root_directory):
- """Initialize the test infrastructure."""
- from mailman.testing import layers
- layers.MockAndMonkeyLayer.testing_mode = True
- layers.ConfigLayer.enable_stderr();
- layers.ConfigLayer.set_root_directory(root_directory)
diff --git a/src/mailman/testing/documentation.py b/src/mailman/testing/documentation.py
new file mode 100644
index 000000000..b1cc36f91
--- /dev/null
+++ b/src/mailman/testing/documentation.py
@@ -0,0 +1,176 @@
+# Copyright (C) 2007-2013 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/>.
+
+"""Harness for testing Mailman's documentation.
+
+Note that doctest extraction does not currently work for zip file
+distributions. doctest discovery currently requires file system traversal.
+"""
+
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'setup',
+ 'teardown'
+ ]
+
+
+from inspect import isfunction, ismethod
+
+from mailman.app.lifecycle import create_list
+from mailman.config import config
+from mailman.testing.helpers import call_api, specialized_message_from_string
+from mailman.testing.layers import SMTPLayer
+
+
+DOT = '.'
+COMMASPACE = ', '
+
+
+
+def stop():
+ """Call into pdb.set_trace()"""
+ # Do the import here so that you get the wacky special hacked pdb instead
+ # of Python's normal pdb.
+ import pdb
+ pdb.set_trace()
+
+
+def dump_msgdata(msgdata, *additional_skips):
+ """Dump in a more readable way a message metadata dictionary."""
+ if len(msgdata) == 0:
+ print '*Empty*'
+ return
+ skips = set(additional_skips)
+ # Some stuff we always want to skip, because their values will always be
+ # variable data.
+ skips.add('received_time')
+ longest = max(len(key) for key in msgdata if key not in skips)
+ for key in sorted(msgdata):
+ if key in skips:
+ continue
+ print '{0:{2}}: {1}'.format(key, msgdata[key], longest)
+
+
+def dump_list(list_of_things, key=str):
+ """Print items in a string to get rid of stupid u'' prefixes."""
+ # List of things may be a generator.
+ list_of_things = list(list_of_things)
+ if len(list_of_things) == 0:
+ print '*Empty*'
+ if key is not None:
+ list_of_things = sorted(list_of_things, key=key)
+ for item in list_of_things:
+ print item
+
+
+def call_http(url, data=None, method=None, username=None, password=None):
+ """'Call a URL with a given HTTP method and return the resulting object.
+
+ The object will have been JSON decoded.
+
+ :param url: The url to open, read, and print.
+ :type url: string
+ :param data: Data to use to POST to a URL.
+ :type data: dict
+ :param method: Alternative HTTP method to use.
+ :type method: str
+ :param username: The HTTP Basic Auth user name. None means use the value
+ from the configuration.
+ :type username: str
+ :param password: The HTTP Basic Auth password. None means use the value
+ from the configuration.
+ :type username: str
+ :return: The decoded JSON data structure.
+ :raises HTTPError: when a non-2xx return code is received.
+ """
+ content, response = call_api(url, data, method, username, password)
+ if content is None:
+ for header in sorted(response):
+ print '{0}: {1}'.format(header, response[header])
+ return None
+ return content
+
+
+def dump_json(url, data=None, method=None, username=None, password=None):
+ """Print the JSON dictionary read from a URL.
+
+ :param url: The url to open, read, and print.
+ :type url: string
+ :param data: Data to use to POST to a URL.
+ :type data: dict
+ :param method: Alternative HTTP method to use.
+ :type method: str
+ :param username: The HTTP Basic Auth user name. None means use the value
+ from the configuration.
+ :type username: str
+ :param password: The HTTP Basic Auth password. None means use the value
+ from the configuration.
+ :type username: str
+ """
+ results = call_http(url, data, method, username, password)
+ if results is None:
+ return
+ for key in sorted(results):
+ value = results[key]
+ if key == 'entries':
+ for i, entry in enumerate(value):
+ # entry is a dictionary.
+ print 'entry %d:' % i
+ for entry_key in sorted(entry):
+ print ' {0}: {1}'.format(entry_key, entry[entry_key])
+ elif isinstance(value, list):
+ printable_value = COMMASPACE.join(
+ "'{0}'".format(s) for s in sorted(value))
+ print '{0}: [{1}]'.format(key, printable_value)
+ else:
+ print '{0}: {1}'.format(key, value)
+
+
+
+def setup(testobj):
+ """Test setup."""
+ # Make sure future statements in our doctests are the same as everywhere
+ # else.
+ testobj.globs['absolute_import'] = absolute_import
+ testobj.globs['unicode_literals'] = unicode_literals
+ # In general, I don't like adding convenience functions, since I think
+ # doctests should do the imports themselves. It makes for better
+ # documentation that way. However, a few are really useful, or help to
+ # hide some icky test implementation details.
+ testobj.globs['call_http'] = call_http
+ testobj.globs['config'] = config
+ testobj.globs['create_list'] = create_list
+ testobj.globs['dump_json'] = dump_json
+ testobj.globs['dump_msgdata'] = dump_msgdata
+ testobj.globs['dump_list'] = dump_list
+ testobj.globs['message_from_string'] = specialized_message_from_string
+ testobj.globs['smtpd'] = SMTPLayer.smtpd
+ testobj.globs['stop'] = stop
+ testobj.globs['transaction'] = config.db
+ # Add this so that cleanups can be automatically added by the doctest.
+ testobj.globs['cleanups'] = []
+
+
+
+def teardown(testobj):
+ for cleanup in testobj.globs['cleanups']:
+ if isfunction(cleanup) or ismethod(cleanup):
+ cleanup()
+ else:
+ cleanup[0](*cleanup[1:])
diff --git a/src/mailman/testing/layers.py b/src/mailman/testing/layers.py
index e47d5c9e0..6d150815f 100644
--- a/src/mailman/testing/layers.py
+++ b/src/mailman/testing/layers.py
@@ -154,7 +154,7 @@ class ConfigLayer(MockAndMonkeyLayer):
continue
logger_name = 'mailman.' + sub_name
log = logging.getLogger(logger_name)
- log.propagate = True
+ #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
diff --git a/src/mailman/testing/nose.py b/src/mailman/testing/nose.py
new file mode 100644
index 000000000..86a3e6a01
--- /dev/null
+++ b/src/mailman/testing/nose.py
@@ -0,0 +1,107 @@
+# Copyright (C) 2013 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/>.
+
+"""nose2 test infrastructure."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'NosePlugin',
+ ]
+
+
+import os
+import re
+import doctest
+import mailman
+import importlib
+
+from mailman.testing.documentation import setup, teardown
+from mailman.testing.layers import ConfigLayer, MockAndMonkeyLayer, SMTPLayer
+from nose2.events import Plugin
+
+DOT = '.'
+FLAGS = doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE | doctest.REPORT_NDIFF
+TOPDIR = os.path.dirname(mailman.__file__)
+
+
+
+class NosePlugin(Plugin):
+ configSection = 'mailman'
+
+ def __init__(self):
+ super(NosePlugin, self).__init__()
+ self.patterns = []
+ self.addArgument(self.patterns, 'P', 'pattern',
+ 'Add a test matching pattern')
+
+ def startTestRun(self, event):
+ MockAndMonkeyLayer.testing_mode = True
+ ConfigLayer.enable_stderr()
+
+ def getTestCaseNames(self, event):
+ if len(self.patterns) == 0:
+ # No filter patterns, so everything should be tested.
+ return
+ # Does the pattern match the fully qualified class name?
+ for pattern in self.patterns:
+ full_name = '{}.{}'.format(
+ event.testCase.__module__, event.testCase.__name__)
+ if re.search(pattern, full_name):
+ # Don't suppress this test class.
+ return
+ names = filter(event.isTestMethod, dir(event.testCase))
+ for name in names:
+ for pattern in self.patterns:
+ if re.search(pattern, name):
+ break
+ else:
+ event.excludedNames.append(name)
+
+ def handleFile(self, event):
+ path = event.path[len(TOPDIR)+1:]
+ if len(self.patterns) > 0:
+ for pattern in self.patterns:
+ if re.search(pattern, path):
+ break
+ else:
+ # Skip this doctest.
+ return
+ base, ext = os.path.splitext(path)
+ if ext != '.rst':
+ return
+ # Look to see if the package defines a test layer, otherwise use the
+ # default layer. First turn the file system path into a dotted Python
+ # module path.
+ parent = os.path.dirname(path)
+ dotted = 'mailman.' + DOT.join(parent.split(os.path.sep))
+ try:
+ module = importlib.import_module(dotted)
+ except ImportError:
+ layer = SMTPLayer
+ else:
+ layer = getattr(module, 'layer', SMTPLayer)
+ test = doctest.DocFileTest(
+ path, package='mailman',
+ optionflags=FLAGS,
+ setUp=setup,
+ tearDown=teardown)
+ test.layer = layer
+ # Suppress the extra "Doctest: ..." line.
+ test.shortDescription = lambda: None
+ event.extraTests.append(test)