Commit 25861efe authored by Abhilash Raj's avatar Abhilash Raj

Move from nose2 to pytest.

This commit changes the default test runner from nose2 to mailman.client. It
also adds the functionality that was previously implemented to run tests using
vcrpy and nose2 using pytest plugins.

pytest-vcr can be used to run tests using VCR.py and adds a custom command line
option --vcr-record-mode which can be used to change the recording mode for the
VCR.py.
parent 8498838d
# Copyright (C) 2017 The Free Software Foundation, Inc.
#
# This file is part of mailman.client.
#
# mailman.client is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by the
# Free Software Foundation, version 3 of the License.
#
# mailman.client 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 Lesser General Public
# License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with mailman.client. If not, see <http://www.gnu.org/licenses/>.
"""pytest conftest"""
__metaclass__ = type
import os
import pytest
@pytest.fixture
def vcr_cassette_path(request, vcr_cassette_name):
return os.path.join('src/mailmanclient/tests/data', vcr_cassette_name)
[pytest]
addopts = --doctest-glob='*.rst'
# Copyright (C) 2017 The Free Software Foundation, Inc.
#
# This file is part of mailman.client.
#
# mailman.client is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by the
# Free Software Foundation, version 3 of the License.
#
# mailman.client 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 Lesser General Public
# License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with mailman.client. If not, see <http://www.gnu.org/licenses/>.
"""Wrappers for doctests to run with pytest"""
from __future__ import absolute_import, print_function, unicode_literals
import pytest
from mailmanclient.testing.documentation import dump
def pytest_collection_modifyitems(items):
for item in items:
item.add_marker(pytest.mark.vcr)
@pytest.fixture(autouse=True)
def import_stuff(doctest_namespace):
doctest_namespace['absolute_import'] = absolute_import
doctest_namespace['print_function'] = print_function
doctest_namespace['unicode_literals'] = unicode_literals
doctest_namespace['dump'] = dump
......@@ -20,7 +20,7 @@ We can retrieve basic information about the server.
>>> dump(client.system)
api_version: 3.1
http_etag: "..."
mailman_version: GNU Mailman 3.1... (...)
mailman_version: GNU Mailman ... (...)
python_version: ...
self_link: http://localhost:9001/3.1/system/versions
......@@ -360,8 +360,7 @@ If you use an address which is not a member of test_two `ValueError` is raised:
>>> test_two.unsubscribe('nomember@example.com')
Traceback (most recent call last):
...
ValueError: nomember@example.com is not a member address of
test-2@example.com
ValueError: nomember@example.com is not a member address of test-2@example.com
After a while, Anna decides to unsubscribe from the Test One mailing list,
though she keeps her Test Two membership active.
......@@ -389,8 +388,7 @@ If you try to unsubscribe an address which is not a member address
>>> test_one.unsubscribe('nomember@example.com')
Traceback (most recent call last):
...
ValueError: nomember@example.com is not a member address of
test-1@example.com
ValueError: nomember@example.com is not a member address of test-1@example.com
Non-Members
......@@ -624,7 +622,7 @@ We can access all valid list settings as attributes.
>>> print(settings['fqdn_listname'])
test-1@example.com
>>> print(settings['description'])
<BLANKLINE>
>>> settings['description'] = 'A very meaningful description.'
>>> settings['display_name'] = 'Test Numero Uno'
......@@ -1205,15 +1203,13 @@ Each configuration object is a dictionary and you can iterate over them:
listname_chars : [-_.0-9a-z]
noreply_address : noreply
pending_request_life : 3d
post_hook :
pre_hook :
self_link : ...
post_hook :
pre_hook :
self_link : http://localhost:9001/3.1/system/configuration/mailman
sender_headers : from from_ reply-to sender
site_owner : changeme@example.com
..
Clean up.
>>> for domain in client.domains:
... domain.delete()
>>> for user in client.users:
... user.delete()
.. >>> for domain in client.domains:
... domain.delete()
>>> for user in client.users:
... user.delete()
......@@ -15,32 +15,16 @@
# 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.
"""
"""Harness for testing Mailman's documentation."""
from __future__ import absolute_import, print_function, unicode_literals
from inspect import isfunction, ismethod
__metaclass__ = type
__all__ = [
'setup',
'teardown'
'dump',
]
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(results):
if results is None:
print(None)
......@@ -54,28 +38,3 @@ def dump(results):
print(' {0}: {1}'.format(entry_key, entry[entry_key]))
else:
print('{0}: {1}'.format(key, results[key]))
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['print_function'] = print_function
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['stop'] = stop
testobj.globs['dump'] = dump
# 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:])
# Copyright (C) 2013-2017 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."""
import os
import re
import errno
import doctest
import mailmanclient
from contextlib2 import ExitStack
from mailmanclient.testing.documentation import setup, teardown
from nose2.events import Plugin
from .vcr_helpers import get_vcr
__all__ = [
'NosePlugin',
]
DOT = '.'
FLAGS = doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE | doctest.REPORT_NDIFF
TOPDIR = os.path.dirname(mailmanclient.__file__)
class NosePlugin(Plugin):
configSection = 'mailman'
def __init__(self):
super(NosePlugin, self).__init__()
self.patterns = []
self.stderr = False
self.record = False
def set_stderr(ignore):
self.stderr = True
self.addArgument(self.patterns, 'P', 'pattern',
'Add a test matching pattern')
self.addFlag(set_stderr, 'E', 'stderr',
'Enable stderr logging to sub-runners')
def set_record(ignore):
self.record = True
self.addFlag(set_record, 'R', 'rerecord',
"""Force re-recording of test responses. Requires
Mailman to be running.""")
self._data_path = os.path.join(TOPDIR, 'tests', 'data')
self._resources = ExitStack()
self._recorder = get_vcr()
def startTest(self, event):
# Check to see if we're running the test suite in record mode. If so,
# delete any existing recording.
if isinstance(event.test, doctest.DocFileCase):
cassette_filename = event.test.id() + ".yaml"
else:
cassette_filename = ".".join([
event.test.__class__.__name__,
event.test._testMethodName,
'yaml'])
cassette_path = os.path.join(self._data_path, cassette_filename)
if self.record:
try:
os.remove(cassette_path)
except OSError as error:
if error.errno != errno.ENOENT:
raise
# This will automatically create the recording file.
self._resources.enter_context(
self._recorder.use_cassette(cassette_path))
def stopTest(self, event):
# Stop all recording.
self._resources.close()
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_class_name = '{}.{}'.format(
event.testCase.__module__, event.testCase.__name__)
if re.search(pattern, full_class_name):
# Don't suppress this test class.
return
names = filter(event.isTestMethod, dir(event.testCase))
for name in names:
full_test_name = '{}.{}.{}'.format(
event.testCase.__module__,
event.testCase.__name__,
name)
for pattern in self.patterns:
if re.search(pattern, full_test_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
test = doctest.DocFileTest(
path, package=mailmanclient,
optionflags=FLAGS,
setUp=setup,
tearDown=teardown)
# Suppress the extra "Doctest: ..." line.
test.shortDescription = lambda: None
event.extraTests.append(test)
# def startTest(self, event):
# import sys; print('vvvvv', event.test, file=sys.stderr)
# def stopTest(self, event):
# import sys; print('^^^^^', event.test, file=sys.stderr)
# Copyright (C) 2015-2017 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/>.
"""Helpers for VCR"""
import vcr
from functools import update_wrapper
from six import binary_type, text_type, PY3
from six.moves.urllib.parse import urlparse, urlunparse, parse_qsl, urlencode
__all__ = [
'get_vcr',
]
def filter_response_headers(response):
for header in ('Date', 'Server', 'date', 'server'):
# The headers are lowercase on Python 2 and capitalized on Python 3
if header in response['headers']:
del response['headers'][header]
return response
def reorder_request_params(request):
def reorder_params(params):
if PY3:
if isinstance(params, binary_type):
params = params.decode("ascii")
parsed = parse_qsl(params, encoding="utf-8")
else:
parsed = parse_qsl(params)
if parsed:
return urlencode(sorted(parsed, key=lambda kv: kv[0]))
else:
# Parsing failed, it may be a simple string.
return params
# sort the URL query-string by key names.
uri_parts = urlparse(request.uri)
if uri_parts.query:
request.uri = urlunparse((
uri_parts.scheme, uri_parts.netloc, uri_parts.path,
uri_parts.params, reorder_params(uri_parts.query),
uri_parts.fragment,
))
# convert the request body to text and sort the parameters.
if isinstance(request.body, binary_type):
try:
request._body = request._body.decode('utf-8')
except UnicodeDecodeError:
pass
if isinstance(request.body, text_type):
request._body = reorder_params(request._body.encode('utf-8'))
return request
def get_vcr(**kwargs):
return vcr.VCR(
filter_headers=['authorization', 'user-agent', 'date'],
before_record=reorder_request_params,
before_record_response=filter_response_headers,
**kwargs
)
class vcr_testcase:
"""
Decorator for TestCases that use VCR.
It automatically sets up a different cassette for each test function.
"""
def __init__(self, vcr_instance):
self.vcr = vcr_instance
def __call__(self, testcase):
return self.decorate_class(testcase)
def decorate_class(self, testcase):
"""Create a subclass that will add setUp instructions."""
vcr_instance = self.vcr
class VCRTestCase(testcase):
vcr = vcr_instance
def setUp(self):
cm = self.vcr.use_cassette('.'.join([
# testcase.__module__.rpartition('.')[2],
testcase.__name__, self._testMethodName, 'yaml']))
self.cassette = cm.__enter__()
self.addCleanup(cm.__exit__, None, None, None)
super(VCRTestCase, self).setUp()
return update_wrapper(
VCRTestCase, testcase,
assigned=('__module__', '__name__'), updated=[])
......@@ -18,7 +18,9 @@
from __future__ import absolute_import, print_function, unicode_literals
import unittest
import pytest
from mailmanclient import Client
from six.moves.urllib_error import HTTPError
......@@ -30,6 +32,7 @@ __all__ = [
]
@pytest.mark.vcr()
class TestDomains(unittest.TestCase):
def setUp(self):
self._client = Client(
......
......@@ -18,6 +18,7 @@
from __future__ import absolute_import, print_function, unicode_literals
import pytest
import unittest
from mailmanclient._client import Page, DEFAULT_PAGE_ITEM_COUNT
......@@ -31,6 +32,7 @@ __all__ = [
]
@pytest.mark.vcr()
class TestPage(unittest.TestCase):
def test_url_simple(self):
......
......@@ -21,6 +21,7 @@
from __future__ import absolute_import, print_function, unicode_literals
import pytest
import unittest
from mailmanclient import Client
......@@ -33,6 +34,7 @@ __all__ = [
]
@pytest.mark.vcr()
class TestUnicode(unittest.TestCase):
def setUp(self):
self._client = Client(
......
......@@ -3,17 +3,18 @@ envlist = py{27,34,35,36},lint
[testenv]
usedevelop = True
commands = python -m nose2 -v
commands = python -m pytest --vcr-record-mode=none
deps =
WebTest
contextlib2
mock
nose2
vcrpy
requests
pytest
pytest-vcr
[testenv:record]
basepython = python2
commands = python -m nose2 -v -R
commands = pytest --vcr-record-mode=once
[testenv:lint]
deps =
......
[unittest]
verbose = 2
plugins = mailmanclient.testing.nose
nose2.plugins.layers
[mailman]
always-on = True
[log-capture]
always-on = False
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment