diff --git a/CHANGELOG b/CHANGELOG index 939214f1b..9310172e5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,10 @@ Changes between 1.0.0 and 1.0.1 ===================================== +* added a 'pytest_nose' plugin which handles nose.SkipTest, + nose-style function/method/generator setup/teardown and + tries to report functions correctly. + * unicode fixes: capturing and unicode writes to sys.stdout (through e.g a print statement) now work within tests, they are encoded as "utf8" by default, also terminalwriting diff --git a/doc/test/plugin/nose.txt b/doc/test/plugin/nose.txt new file mode 100644 index 000000000..1f5775a11 --- /dev/null +++ b/doc/test/plugin/nose.txt @@ -0,0 +1,64 @@ + +pytest_nose plugin +================== + +nose-compatibility plugin: allow to run nose test suites natively. + +.. contents:: + :local: + +This is an experimental plugin for allowing to run tests written +in the 'nosetests' style with py.test. +nosetests is a popular clone +of py.test and thus shares some philosophy. This plugin is an +attempt to understand and neutralize differences. It allows to +run nosetests' own test suite and a number of other test suites +without problems. + +Usage +------------- + +If you type:: + + py.test -p nose + +where you would type ``nosetests``, you can run your nose style tests. +You might also try to run without the nose plugin to see where your test +suite is incompatible to the default py.test. + +To avoid the need for specifying a command line option you can set an environment +variable:: + + PYTEST_PLUGINS=nose + +or create a ``conftest.py`` file in your test directory or below:: + + # conftest.py + pytest_plugins = "nose", + +If you find issues or have suggestions you may run:: + + py.test -p nose --pastebin=all + +to create a URL of a test run session and send it with comments to the issue +tracker or mailing list. + +Known issues +------------------ + +- nose-style doctests are not collected and executed correctly, + also fixtures don't work. + +Start improving this plugin in 30 seconds +========================================= + + +Do you find the above documentation or the plugin itself lacking? + +1. Download `pytest_nose.py`_ plugin source code +2. put it somewhere as ``pytest_nose.py`` into your import path +3. a subsequent ``py.test`` run will use your local version + +Further information: extend_ documentation, other plugins_ or contact_. + +.. include:: links.txt diff --git a/makepluginlist.py b/makepluginlist.py index d220238a9..c4bf6f2ba 100644 --- a/makepluginlist.py +++ b/makepluginlist.py @@ -7,7 +7,7 @@ plugins = [ ('Plugins related to Python test functions and programs', 'xfail figleaf monkeypatch capture recwarn',), ('Plugins for other testing styles and languages', - 'unittest doctest oejskit restdoc'), + 'oejskit unittest nose doctest restdoc'), ('Plugins for generic reporting and failure logging', 'pastebin resultlog terminal',), ('internal plugins / core functionality', diff --git a/py/test/collect.py b/py/test/collect.py index 8a2e9467b..722f8e0c8 100644 --- a/py/test/collect.py +++ b/py/test/collect.py @@ -395,7 +395,12 @@ class Directory(FSCollector): def _ignore(self, path): ignore_paths = self.config.getconftest_pathlist("collect_ignore", path=path) - return ignore_paths and path in ignore_paths + return ignore_paths and path in ignore_paths + # XXX more refined would be: + if ignore_paths: + for p in ignore_paths: + if path == p or path.relto(p): + return True def consider(self, path): if self._ignore(path): diff --git a/py/test/plugin/conftest.py b/py/test/plugin/conftest.py index a4542a643..93d239708 100644 --- a/py/test/plugin/conftest.py +++ b/py/test/plugin/conftest.py @@ -15,10 +15,15 @@ def pytest_funcarg__testdir(request): # testdir.plugins.append(obj.testplugin) # break #else: - basename = request.module.__name__.split(".")[-1] - if basename.startswith("pytest_"): + modname = request.module.__name__.split(".")[-1] + if modname.startswith("pytest_"): testdir.plugins.append(vars(request.module)) - testdir.plugins.append(basename) + testdir.plugins.append(modname) + #elif modname.startswith("test_pytest"): + # pname = modname[5:] + # assert pname not in testdir.plugins + # testdir.plugins.append(pname) + # #testdir.plugins.append(vars(request.module)) else: pass # raise ValueError("need better support code") return testdir diff --git a/py/test/plugin/pytest_nose.py b/py/test/plugin/pytest_nose.py new file mode 100644 index 000000000..c77b01f08 --- /dev/null +++ b/py/test/plugin/pytest_nose.py @@ -0,0 +1,97 @@ +"""nose-compatibility plugin: allow to run nose test suites natively. + +This is an experimental plugin for allowing to run tests written +in the 'nosetests' style with py.test. +nosetests is a popular clone +of py.test and thus shares some philosophy. This plugin is an +attempt to understand and neutralize differences. It allows to +run nosetests' own test suite and a number of other test suites +without problems. + +Usage +------------- + +If you type:: + + py.test -p nose + +where you would type ``nosetests``, you can run your nose style tests. +You might also try to run without the nose plugin to see where your test +suite is incompatible to the default py.test. + +To avoid the need for specifying a command line option you can set an environment +variable:: + + PYTEST_PLUGINS=nose + +or create a ``conftest.py`` file in your test directory or below:: + + # conftest.py + pytest_plugins = "nose", + +If you find issues or have suggestions you may run:: + + py.test -p nose --pastebin=all + +to create a URL of a test run session and send it with comments to the issue +tracker or mailing list. + +Known issues +------------------ + +- nose-style doctests are not collected and executed correctly, + also fixtures don't work. + +""" +import py +import inspect +import sys + +def pytest_runtest_makereport(__call__, item, call): + SkipTest = getattr(sys.modules.get('nose', None), 'SkipTest', None) + if SkipTest: + if call.excinfo and call.excinfo.errisinstance(SkipTest): + # let's substitute the excinfo with a py.test.skip one + call2 = call.__class__(lambda: py.test.skip(str(call.excinfo.value)), call.when) + call.excinfo = call2.excinfo + +def pytest_report_iteminfo(item): + # nose 0.11.1 uses decorators for "raises" and other helpers. + # for reporting progress by filename we fish for the filename + if isinstance(item, py.test.collect.Function): + obj = item.obj + if hasattr(obj, 'compat_co_firstlineno'): + fn = sys.modules[obj.__module__].__file__ + if fn.endswith(".pyc"): + fn = fn[:-1] + #assert 0 + #fn = inspect.getsourcefile(obj) or inspect.getfile(obj) + lineno = obj.compat_co_firstlineno + return py.path.local(fn), lineno, obj.__module__ + +def pytest_runtest_setup(item): + if isinstance(item, (py.test.collect.Function)): + if isinstance(item.parent, py.test.collect.Generator): + gen = item.parent + if not hasattr(gen, '_nosegensetup'): + call_optional(gen.obj, 'setup') + if isinstance(gen.parent, py.test.collect.Instance): + call_optional(gen.parent.obj, 'setup') + gen._nosegensetup = True + call_optional(item.obj, 'setup') + +def pytest_runtest_teardown(item): + if isinstance(item, py.test.collect.Function): + call_optional(item.obj, 'teardown') + #if hasattr(item.parent, '_nosegensetup'): + # #call_optional(item._nosegensetup, 'teardown') + # del item.parent._nosegensetup + +def pytest_make_collect_report(collector): + if isinstance(collector, py.test.collect.Generator): + call_optional(collector.obj, 'setup') + +def call_optional(obj, name): + method = getattr(obj, name, None) + if method: + method() diff --git a/py/test/plugin/pytest_terminal.py b/py/test/plugin/pytest_terminal.py index 158204b1e..c2f83c124 100644 --- a/py/test/plugin/pytest_terminal.py +++ b/py/test/plugin/pytest_terminal.py @@ -241,15 +241,10 @@ class TerminalReporter: if self.config.option.traceconfig: plugins = [] for plugin in self.config.pluginmanager.comregistry: - name = plugin.__class__.__name__ - if name.endswith("Plugin"): - name = name[:-6] - #if name == "Conftest": - # XXX get filename - plugins.append(name) - else: - plugins.append(str(plugin)) - + name = getattr(plugin, '__name__', None) + if name is None: + name = plugin.__class__.__name__ + plugins.append(name) plugins = ", ".join(plugins) self.write_line("active plugins: %s" %(plugins,)) for i, testarg in py.builtin.enumerate(self.config.args): diff --git a/py/test/plugin/pytest_unittest.py b/py/test/plugin/pytest_unittest.py index 6e17a1c77..1340cae99 100644 --- a/py/test/plugin/pytest_unittest.py +++ b/py/test/plugin/pytest_unittest.py @@ -55,6 +55,9 @@ class UnitTestFunction(py.test.collect.Function): if obj is not _dummy: self._obj = obj self._sort_value = sort_value + if hasattr(self.parent, 'newinstance'): + self.parent.newinstance() + self.obj = self._getobj() def runtest(self): target = self.obj @@ -87,7 +90,6 @@ def test_simple_unittest(testdir): def test_setup(testdir): testpath = testdir.makepyfile(test_two=""" import unittest - pytest_plugins = "pytest_unittest" # XXX class MyTestCase(unittest.TestCase): def setUp(self): self.foo = 1 @@ -98,6 +100,18 @@ def test_setup(testdir): rep = reprec.matchreport("test_setUp") assert rep.passed +def test_new_instances(testdir): + testpath = testdir.makepyfile(""" + import unittest + class MyTestCase(unittest.TestCase): + def test_func1(self): + self.x = 2 + def test_func2(self): + assert not hasattr(self, 'x') + """) + reprec = testdir.inline_run(testpath) + reprec.assertoutcome(passed=2) + def test_teardown(testdir): testpath = testdir.makepyfile(test_three=""" import unittest diff --git a/py/test/plugin/test_pytest_nose.py b/py/test/plugin/test_pytest_nose.py new file mode 100644 index 000000000..4950095dd --- /dev/null +++ b/py/test/plugin/test_pytest_nose.py @@ -0,0 +1,87 @@ +import py +py.test.importorskip("nose") + +def test_nose_setup(testdir): + p = testdir.makepyfile(""" + l = [] + + def test_hello(): + assert l == [1] + def test_world(): + assert l == [1,2] + test_hello.setup = lambda: l.append(1) + test_hello.teardown = lambda: l.append(2) + """) + result = testdir.runpytest(p, '-p', 'nose') + result.stdout.fnmatch_lines([ + "*2 passed*" + ]) + +def test_nose_test_generator_fixtures(testdir): + p = testdir.makepyfile(""" + # taken from nose-0.11.1 unit_tests/test_generator_fixtures.py + from nose.tools import eq_ + called = [] + + def outer_setup(): + called.append('outer_setup') + + def outer_teardown(): + called.append('outer_teardown') + + def inner_setup(): + called.append('inner_setup') + + def inner_teardown(): + called.append('inner_teardown') + + def test_gen(): + called[:] = [] + for i in range(0, 5): + yield check, i + + def check(i): + expect = ['outer_setup'] + for x in range(0, i): + expect.append('inner_setup') + expect.append('inner_teardown') + expect.append('inner_setup') + eq_(called, expect) + + + test_gen.setup = outer_setup + test_gen.teardown = outer_teardown + check.setup = inner_setup + check.teardown = inner_teardown + + class TestClass(object): + def setup(self): + print "setup called in", self + self.called = ['setup'] + + def teardown(self): + print "teardown called in", self + eq_(self.called, ['setup']) + self.called.append('teardown') + + def test(self): + print "test called in", self + for i in range(0, 5): + yield self.check, i + + def check(self, i): + print "check called in", self + expect = ['setup'] + #for x in range(0, i): + # expect.append('setup') + # expect.append('teardown') + #expect.append('setup') + eq_(self.called, expect) + + """) + result = testdir.runpytest(p, '-p', 'nose') + result.stdout.fnmatch_lines([ + "*10 passed*" + ]) + +