This instance was upgraded to Heptapod 0.21.0 (final) today

Commit 73faa091 authored by Manuel Jacob's avatar Manuel Jacob

tests: port from nosetests to pytest

Nosetests is not actively developed anymore. Pytest is actively developed and
has a much larger ecosystem around it.

Two features of pytest are particularly helpful for us:

Pytest captures stdio by redirecting at the file descriptor-level instead of
replacing sys.stdout. This is required for Mercurial, as it has its own
reference to stdout. Before with nosetests, most output was silently swallowed.

Pytest offers the `ALLOW_UNICODE` and `ALLOW_BYTES` options for doctests, making
it much easier to make our docstrings Python 2- and Python 3-compatiable.

During this patch, it was attempted to leave the structure of the test code
similar to before instead of converting everything to idiomatic pytest code.
Luckily, pytest implements enough interoperability for most of our test code.
parent 1e10fa7d6bc2
......@@ -32,8 +32,8 @@ local:
tests:
@[ -n "$(HGPATH)" ] || { echo "HGPATH not specified"; false; }
@[ -d "$(HGPATH)" ] || { echo "HGPATH not found: $(HGPATH)"; false; }
$(PYTHON) tests/run-tests.py -a '!extensions' --with-doctest
$(PYTHON) tests/run-tests.py -a extensions=largefiles
$(PYTHON) tests/run-tests.py -m 'not largefiles' --doctest-modules tests
$(PYTHON) tests/run-tests.py -m largefiles tests
$(PYTHON) tests/run-hgtests.py
.PHONY: pytype
......
import functools
import os
from nose.tools import *
import pytest
from tortoisehg.hgqt import filedata, thgrepo
......@@ -40,30 +42,32 @@ def loaddata(reponame, rev, path, prev=None):
return fd
def check_error(fd, message):
@pytest.mark.parametrize('get_fd, message', [
(functools.partial(loaddata, 'status', rev, path + sfx), msg)
for rev, sfx in [(0, ''), (1, '.copied'), (2, '.renamed')]
for path, msg in [('binary', 'is binary'),
('text-over1kb', 'is larger than')]
])
def test_error_added(get_fd, message):
fd = get_fd()
assert message in fd.error, (message, fd.error)
# no data should be loaded
assert_false(fd.contents)
assert_false(fd.olddata)
assert_false(fd.diff)
def test_error_added():
for rev, sfx in [(0, ''), (1, '.copied'), (2, '.renamed')]:
for path, msg in [('binary', 'is binary'),
('text-over1kb', 'is larger than')]:
fd = loaddata('status', rev, path + sfx)
yield check_error, fd, msg
def check_flabel(fd, message):
assert message in fd.flabel, (message, fd.flabel)
def test_flabel_added():
for path in ['text', 'binary', 'text-over1kb']:
for rev, sfx, msg in [(0, '', 'added'),
(1, '.copied', 'copied from'),
(2, '.renamed', 'renamed from')]:
fd = loaddata('status', rev, path + sfx)
yield check_flabel, fd, msg
@pytest.mark.parametrize('get_fd, message', [
(functools.partial(loaddata, 'status', rev, path + sfx), msg)
for path in ['text', 'binary', 'text-over1kb']
for rev, sfx, msg in [(0, '', 'added'),
(1, '.copied', 'copied from'),
(2, '.renamed', 'renamed from')]
])
def test_flabel_added(get_fd, message):
check_flabel(get_fd(), message)
def test_flabel_far_rename():
fd = loaddata('status', 2, 'text.copied', 0)
......
......@@ -2,6 +2,7 @@
import os
from nose.tools import *
import pytest
from mercurial import hg
from tortoisehg.util import hglib
......@@ -44,15 +45,14 @@ def setup_repoid():
hg = helpers.HgClient(os.path.join(_tmpdir, 'repoid-rev0hidden'))
hg.debugobsolete(_repoidnodes[0])
def check_repoid(reponame, rootrev):
@pytest.mark.parametrize('reponame, rootrev', [
('empty', -1),
('repoid-trivial', 0),
('repoid-allhidden', -1),
('repoid-rev0hidden', 1)
])
def test_repoid(reponame, rootrev):
repo = openrepo(reponame)
nodehex = _repoidnodes[rootrev]
assert_equal(nodehex[:12], hglib.shortrepoid(repo))
assert_equal(nodehex.decode('hex'), hglib.repoidnode(repo))
def test_repoid():
for reponame, rev in [('empty', -1),
('repoid-trivial', 0),
('repoid-allhidden', -1),
('repoid-rev0hidden', 1)]:
yield check_repoid, reponame, rev
"""Test for compatibility with Mercurial API"""
import inspect
import pytest
from nose.tools import *
from tortoisehg.util import hglib, pipeui
def assert_same_argspec(f, g):
def test_same_argspec(f, g):
fa, ga = inspect.getargspec(f), inspect.getargspec(g)
assert_equals(fa, ga,
'%s != %s' % (inspect.formatargspec(*fa),
......@@ -18,8 +20,8 @@ def overridden_methods(cls):
if basemeth.__func__ is not getattr(cls, name).__func__]
def test_pipeui():
def pytest_generate_tests(metafunc):
ui = hglib.loadui()
pipeui.uisetup(ui)
for basemeth, meth in overridden_methods(ui.__class__):
yield assert_same_argspec, basemeth, meth
if metafunc.function == test_same_argspec:
metafunc.parametrize('f, g', overridden_methods(ui.__class__))
"""Nose plugin to capture unhandled exception"""
import sys
from nose import plugins
class CaptureExcPlugin(plugins.Plugin):
"""Capture unhandled exception (probably raised inside event loop)"""
enabled = False
name = 'capture-exc'
def options(self, parser, env):
parser.add_option('--no-capture-exc', dest='capture_exc',
action='store_false', default=True,
help='Catch unhandled exception to report as error')
def configure(self, options, conf):
self.enabled = options.capture_exc
def prepareTestResult(self, result):
# dirty hack to access result.addError()
self._result = result
def startTest(self, test):
self._origexcepthook = sys.excepthook
sys.excepthook = self._excepthook
self._excepts = []
def stopTest(self, test):
sys.excepthook = self._origexcepthook
del self._origexcepthook
if self._excepts:
# BUG: because the corresponding addSuccess/Failure/Error for the
# given test is already called, this increases the test counts.
self._result.addError(test, self._excepts.pop(0))
def _excepthook(self, type, value, traceback):
self._excepts.append((type, value, traceback))
[pytest]
markers =
largefiles: enable largefiles extensions
"""Pytest plugin to capture unhandled exception"""
from __future__ import print_function
try:
from cStringIO import StringIO
except ImportError:
try:
from StringIO import StringIO
except ImportError:
from io import StringIO
import sys
import traceback
import pytest
class CaptureExcPlugin(object):
"""Capture unhandled exception (probably raised inside event loop)"""
def pytest_addoption(self, parser):
parser.addoption('--no-capture-exc', dest='capture_exc',
action='store_false', default=True,
help='Catch unhandled exception to report as error')
def pytest_configure(self, config):
options = config.known_args_namespace
self.enabled = options.capture_exc
@pytest.mark.hookwrapper
def pytest_runtest_call(self, item):
self._origexcepthook = sys.excepthook
sys.excepthook = self._excepthook
self._excepts = []
try:
yield
finally:
sys.excepthook = self._origexcepthook
del self._origexcepthook
if self._excepts:
out = StringIO()
print("%s uncaught exception(s):" % len(self._excepts), file=out)
for exc_info in self._excepts:
print(file=out)
traceback.print_exception(*exc_info, file=out)
del self._excepts[:]
pytest.fail(out.getvalue(), pytrace=False)
def _excepthook(self, type, value, traceback):
self._excepts.append((type, value, traceback))
"""Nose plugin to set up test environment"""
"""Pytest plugin to set up test environment"""
from __future__ import absolute_import
......@@ -7,27 +7,28 @@ import shutil
import sys
import tempfile
from nose import plugins
import pytest
# don't import mercurial or tortoisehg before setting up test environment
class HgEnvPlugin(plugins.Plugin):
class HgEnvPlugin(object):
"""Set up temporary environment"""
enabled = True
name = 'hgenv'
def options(self, parser, env):
parser.add_option('--keep-tmpdir', action='store_true', default=False,
help='Keep temporary directory after running tests')
parser.add_option('--tmpdir',
help=('Put temporary files in the given directory '
'(implies --keep-tmpdir)'))
def pytest_addoption(self, parser):
parser.addoption('--keep-tmpdir', action='store_true', default=False,
help='Keep temporary directory after running tests')
parser.addoption('--tmpdir',
help=('Put temporary files in the given directory '
'(implies --keep-tmpdir)'))
def configure(self, options, conf):
def pytest_configure(self, config):
options = config.known_args_namespace
self.keep_tmpdir = options.keep_tmpdir or bool(options.tmpdir)
self.tmpdir = options.tmpdir
def begin(self):
@pytest.hookimpl(tryfirst=True)
def pytest_collection(self):
if 'mercurial' in sys.modules:
raise Exception('loaded mercurial module before setting up '
'test environment')
......@@ -124,7 +125,7 @@ class HgEnvPlugin(plugins.Plugin):
QSettings.setPath(QSettings.IniFormat, QSettings.UserScope,
self.configdir)
def finalize(self, result):
def pytest_sessionfinish(self):
del self._qapp
if not self.keep_tmpdir:
......
......@@ -55,7 +55,7 @@ def waitForCmdStarted(session, timeout=5000):
loop.exec_()
class _CmdAgentTestBase(unittest.TestCase):
class _CmdAgentTestBase(object):
@classmethod
def setUpClass(cls):
tmpdir = helpers.mktmpdir(cls.__name__)
......@@ -247,11 +247,11 @@ class _CmdAgentTestBase(unittest.TestCase):
self.assertFalse(self.agent.isBusy())
class CmdAgentProcTest(_CmdAgentTestBase):
class CmdAgentProcTest(_CmdAgentTestBase, unittest.TestCase):
workername = 'proc'
class CmdAgentServerTest(_CmdAgentTestBase):
class CmdAgentServerTest(_CmdAgentTestBase, unittest.TestCase):
workername = 'server'
@classmethod
......
......@@ -11,6 +11,8 @@ from mercurial import (
from nose.plugins.skip import SkipTest
import pytest
from tortoisehg.hgqt.qtcore import (
QModelIndex,
Qt,
......@@ -637,9 +639,8 @@ class ManifestModelReloadTest(unittest.TestCase):
self.assertEqual(['bar', 'foo'], alldata(m))
@pytest.mark.largefiles
class ManifestModelLargeFilesTest(unittest.TestCase):
extensions = ['largefiles']
@classmethod
def setUpClass(cls):
cls.hg = hg = helpers.HgClient(os.path.join(_tmpdir, cls.__name__))
......
......@@ -50,7 +50,7 @@ class RepoAgentAttributeTest(unittest.TestCase):
self.assertFalse(self.agent.hiddenRevsIncluded())
class _RepoAgentChangedSignalTestBase(unittest.TestCase):
class _RepoAgentChangedSignalTestBase(object):
@classmethod
def setUpClass(cls):
tmpdir = helpers.mktmpdir(cls.__name__)
......@@ -198,7 +198,7 @@ class _RepoAgentChangedSignalTestBase(unittest.TestCase):
self.wait_changed(thgrepo.LogChanged)
class RepoAgentChangedSignalWithMonitorTest(_RepoAgentChangedSignalTestBase):
class RepoAgentChangedSignalWithMonitorTest(_RepoAgentChangedSignalTestBase, unittest.TestCase):
monitorrepo = b'always'
def test_filesystem_watcher(self):
......@@ -225,5 +225,5 @@ class RepoAgentChangedSignalWithMonitorTest(_RepoAgentChangedSignalTestBase):
| thgrepo.WorkingStateChanged)
class RepoAgentChangedSignalWithoutMonitorTest(_RepoAgentChangedSignalTestBase):
class RepoAgentChangedSignalWithoutMonitorTest(_RepoAgentChangedSignalTestBase, unittest.TestCase):
monitorrepo = b'never'
#!/usr/bin/env python
"""Run a set of tests by nosetests
"""Run a set of tests by pytest
To run all tests::
% export HGPATH=path/to/mercurial
% ./run-tests.py -a '!extensions' --with-doctest
% ./run-tests.py -a 'extensions=largefiles'
% ./run-tests.py -m 'not largefiles' --doctest-modules tests
% ./run-tests.py -m largefiles tests
For details, please see:
- `./run-tests.py --help`
- http://readthedocs.org/docs/nose/en/latest/
- https://docs.pytest.org/en/stable/
- http://docs.python.org/library/unittest.html
"""
import nose, os
import nosecaptureexc, nosehgenv
ignorefiles = [
r'^[._]',
r'^setup\.py$',
r'^TortoiseHgOverlayServer\.py$',
# exclude platform-dependent modules
r'^bugtraq\.py$',
r'^shellconf\.py$',
]
import sys
import pytest
import pytestcaptureexc, pytesthgenv
def main():
env = os.environ.copy()
if 'NOSE_IGNORE_FILES' not in env:
env['NOSE_IGNORE_FILES'] = ignorefiles
nose.main(env=env,
addplugins=[nosecaptureexc.CaptureExcPlugin(),
nosehgenv.HgEnvPlugin()])
sys.exit(pytest.main(plugins=[pytestcaptureexc.CaptureExcPlugin(),
pytesthgenv.HgEnvPlugin()]))
if __name__ == '__main__':
main()
import os, tempfile
from nose.tools import *
from nose.plugins.skip import SkipTest
from mercurial import (
config,
error,
pycompat,
)
import pytest
from tortoisehg.util import wconfig
......@@ -41,33 +41,40 @@ def writetempfile(s):
os.close(fd)
return path
class _Collector(list):
def __call__(self, func):
self.append(func)
return func
with_rconfig = _Collector()
with_wconfig = _Collector()
with_both = _Collector()
def with_rconfig(f):
f.__test__ = True
return pytest.mark.usefixtures('rconfig')(f)
def with_wconfig(f):
f.__test__ = True
return pytest.mark.usefixtures('wconfig')(f)
def test_both():
for e in with_wconfig + with_both:
if wconfig._hasiniparse:
yield e
else:
def skipped():
raise SkipTest
yield skipped
def with_both(f):
f.__test__ = True
return pytest.mark.usefixtures('both')(f)
@pytest.fixture(name='wconfig')
def fixture_wconfig():
if wconfig._hasiniparse:
yield
else:
pytest.skip()
@pytest.fixture(name='rconfig')
def fixture_rconfig():
orighasiniparse = wconfig._hasiniparse
wconfig._hasiniparse = False
try:
for e in with_rconfig + with_both:
yield e
yield
finally:
wconfig._hasiniparse = orighasiniparse
@pytest.fixture(name='both', params=['wconfig', 'rconfig'])
def fixture_both(request):
request.getfixturevalue(request.param)
@with_both
def check_copy():
......
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