From c17a09adafeb8bc36b70f182c904ce7bcfa0e97e Mon Sep 17 00:00:00 2001 From: hpk Date: Fri, 27 Feb 2009 11:18:27 +0100 Subject: [PATCH] [svn r62211] merge 60797:HEAD of pytestplugin branch: this merge contains: * a new plugin architecture * a pluginized pytest core * many pytest related refactorings * refactorings/streamlining of pytest's own tests --HG-- branch : trunk --- contrib/py_unittest/conftest.py | 86 -- contrib/py_unittest/readme.txt | 7 - contrib/py_unittest/test_conftest.py | 68 -- py/__init__.py | 22 +- py/_com.py | 150 ++++ py/apigen/apigen.py | 12 +- py/apigen/conftest.py | 10 +- py/apigen/htmlgen.py | 9 +- py/apigen/layout.py | 3 +- py/apigen/project.py | 8 +- py/apigen/rest/testing/test_rest.py | 9 +- py/apigen/testing/test_apigen_example.py | 8 +- py/bin/gendoc.py | 2 +- py/cmdline/testing/test_cmdline.py | 22 +- py/conftest.py | 47 +- py/doc/confrest.py | 49 +- py/doc/conftest.py | 322 -------- py/doc/contact.txt | 2 - py/doc/example/pytest/test_failures.py | 9 +- py/doc/future.txt | 6 +- py/doc/impl-test.txt | 2 + py/doc/path.txt | 12 +- py/doc/pytest-plugins.txt | 67 ++ py/doc/test.txt | 32 +- py/doc/test_conftest.py | 115 --- py/doc/xml.txt | 1 - py/execnet/testing/test_gateway.py | 18 +- py/initpkg.py | 27 +- py/misc/rest.py | 8 + py/misc/testing/test_cache.py | 2 +- py/misc/testing/test_com.py | 233 ++++++ py/misc/testing/test_initpkg.py | 18 +- py/misc/testing/test_warn.py | 34 +- py/misc/warn.py | 27 +- py/path/common.py | 24 + py/path/svn/testing/test_auth.py | 3 +- py/path/svn/testing/test_wccommand.py | 5 +- py/path/testing/common.py | 12 + py/rest/testing/test_rst.py | 2 +- py/test/{report => attic}/__init__.py | 0 py/test/{report => attic}/rest.py | 0 py/test/{report => attic}/testing/__init__.py | 0 .../{report => attic}/testing/test_rest.py | 0 py/test/{report => attic}/testing/test_web.py | 0 .../{report => attic}/testing/test_webjs.py | 0 py/test/{report => attic}/web.py | 0 py/test/{report => attic}/webdata/__init__.py | 0 py/test/{report => attic}/webdata/index.html | 0 py/test/{report => attic}/webdata/json.py | 0 py/test/{report => attic}/webdata/source.js | 0 py/test/{report => attic}/webjs.py | 0 py/test/cmdline.py | 4 +- py/test/collect.py | 143 ++-- py/test/config.py | 126 ++- py/test/conftesthandle.py | 33 +- py/test/defaultconftest.py | 101 +-- py/test/dsession/dsession.py | 66 +- py/test/dsession/hostmanage.py | 10 +- py/test/dsession/masterslave.py | 51 +- py/test/dsession/testing/test_dsession.py | 160 ++-- .../testing/test_functional_dsession.py | 102 +-- py/test/dsession/testing/test_hostmanage.py | 182 ++--- py/test/dsession/testing/test_masterslave.py | 142 ++-- py/test/event.py | 93 ++- py/test/looponfail/remote.py | 34 +- py/test/looponfail/testing/test_remote.py | 45 +- py/test/looponfail/testing/test_util.py | 42 +- py/test/looponfail/util.py | 16 - py/test/outcome.py | 7 + py/test/parseopt.py | 92 +++ py/test/plugin/__init__.py | 1 + py/test/plugin/conftest.py | 10 + py/test/plugin/pytest_apigen.py | 82 ++ py/test/plugin/pytest_default.py | 83 ++ py/test/plugin/pytest_doctest.py | 170 ++++ py/test/plugin/pytest_eventlog.py | 41 + py/test/plugin/pytest_iocapture.py | 54 ++ py/test/plugin/pytest_monkeypatch.py | 66 ++ py/test/plugin/pytest_plugintester.py | 188 +++++ py/test/plugin/pytest_pocoo.py | 62 ++ py/test/plugin/pytest_pytester.py | 438 ++++++++++ py/test/plugin/pytest_restdoc.py | 418 ++++++++++ py/test/plugin/pytest_resultlog.py | 253 ++++++ py/test/plugin/pytest_terminal.py | 628 ++++++++++++++ py/test/plugin/pytest_tmpdir.py | 45 + py/test/plugin/pytest_unittest.py | 124 +++ py/test/plugin/pytest_xfail.py | 63 ++ py/test/pycollect.py | 127 +-- py/test/pytestplugin.py | 123 +++ py/test/report/base.py | 74 -- py/test/report/collectonly.py | 43 - py/test/report/terminal.py | 219 ----- py/test/report/testing/test_basereporter.py | 94 --- py/test/report/testing/test_collectonly.py | 53 -- py/test/report/testing/test_terminal.py | 247 ------ py/test/resultlog.py | 54 -- py/test/runner.py | 55 +- py/test/session.py | 77 +- py/test/testing/acceptance_test.py | 302 +++---- py/test/testing/conftest.py | 2 + py/test/testing/suptest.py | 230 ------ py/test/testing/test_collect.py | 713 ++++------------ py/test/testing/test_collect_pickle.py | 52 ++ py/test/testing/test_config.py | 773 ++++++++---------- py/test/testing/test_conftesthandle.py | 9 + py/test/testing/test_deprecated_api.py | 57 +- py/test/testing/test_doctest.py | 34 - py/test/testing/test_event.py | 61 -- py/test/testing/test_genitems.py | 123 +++ py/test/testing/test_parseopt.py | 90 ++ py/test/testing/test_pycollect.py | 387 +++++++++ py/test/testing/test_pytestplugin.py | 273 +++++++ py/test/testing/test_recording.py | 9 +- py/test/testing/test_resultlog.py | 184 ----- py/test/testing/test_runner_functional.py | 117 +-- py/test/testing/test_session.py | 172 +--- py/test/testing/test_setup_nested.py | 27 +- 117 files changed, 6079 insertions(+), 4370 deletions(-) delete mode 100644 contrib/py_unittest/conftest.py delete mode 100644 contrib/py_unittest/readme.txt delete mode 100644 contrib/py_unittest/test_conftest.py create mode 100644 py/_com.py delete mode 100644 py/doc/conftest.py create mode 100644 py/doc/pytest-plugins.txt delete mode 100644 py/doc/test_conftest.py create mode 100644 py/misc/testing/test_com.py rename py/test/{report => attic}/__init__.py (100%) rename py/test/{report => attic}/rest.py (100%) rename py/test/{report => attic}/testing/__init__.py (100%) rename py/test/{report => attic}/testing/test_rest.py (100%) rename py/test/{report => attic}/testing/test_web.py (100%) rename py/test/{report => attic}/testing/test_webjs.py (100%) rename py/test/{report => attic}/web.py (100%) rename py/test/{report => attic}/webdata/__init__.py (100%) rename py/test/{report => attic}/webdata/index.html (100%) rename py/test/{report => attic}/webdata/json.py (100%) rename py/test/{report => attic}/webdata/source.js (100%) rename py/test/{report => attic}/webjs.py (100%) create mode 100644 py/test/parseopt.py create mode 100644 py/test/plugin/__init__.py create mode 100644 py/test/plugin/conftest.py create mode 100644 py/test/plugin/pytest_apigen.py create mode 100644 py/test/plugin/pytest_default.py create mode 100644 py/test/plugin/pytest_doctest.py create mode 100644 py/test/plugin/pytest_eventlog.py create mode 100644 py/test/plugin/pytest_iocapture.py create mode 100644 py/test/plugin/pytest_monkeypatch.py create mode 100644 py/test/plugin/pytest_plugintester.py create mode 100644 py/test/plugin/pytest_pocoo.py create mode 100644 py/test/plugin/pytest_pytester.py create mode 100644 py/test/plugin/pytest_restdoc.py create mode 100644 py/test/plugin/pytest_resultlog.py create mode 100644 py/test/plugin/pytest_terminal.py create mode 100644 py/test/plugin/pytest_tmpdir.py create mode 100644 py/test/plugin/pytest_unittest.py create mode 100644 py/test/plugin/pytest_xfail.py create mode 100644 py/test/pytestplugin.py delete mode 100644 py/test/report/base.py delete mode 100644 py/test/report/collectonly.py delete mode 100644 py/test/report/terminal.py delete mode 100644 py/test/report/testing/test_basereporter.py delete mode 100644 py/test/report/testing/test_collectonly.py delete mode 100644 py/test/report/testing/test_terminal.py delete mode 100644 py/test/resultlog.py create mode 100644 py/test/testing/conftest.py delete mode 100644 py/test/testing/suptest.py create mode 100644 py/test/testing/test_collect_pickle.py delete mode 100644 py/test/testing/test_doctest.py delete mode 100644 py/test/testing/test_event.py create mode 100644 py/test/testing/test_genitems.py create mode 100644 py/test/testing/test_parseopt.py create mode 100644 py/test/testing/test_pycollect.py create mode 100644 py/test/testing/test_pytestplugin.py delete mode 100644 py/test/testing/test_resultlog.py diff --git a/contrib/py_unittest/conftest.py b/contrib/py_unittest/conftest.py deleted file mode 100644 index 38cd73072..000000000 --- a/contrib/py_unittest/conftest.py +++ /dev/null @@ -1,86 +0,0 @@ -""" -automatically collect and run traditional "unittest.py" style tests. - -drop this conftest.py into your project directory so that -all testing directories are below it. - -you can mix unittest TestCase subclasses and -py.test style tests (discovery based on name). - -user-extensions such as a custom test_suite() -will not be considered (see XXX). - -$HeadURL: https://codespeak.net/svn/py/trunk/contrib/py_unittest/conftest.py $ -$Id: conftest.py 58288 2008-09-21 08:17:11Z hpk $ -""" -import py -import unittest -import sys - -__version__ = "$Rev: 58288 $".split()[1] - -def configproperty(name): - def fget(self): - ret = self._config.getvalue(name, self.fspath) - return ret - return property(fget) - -class Module(py.test.collect.Module): - UnitTestCase = configproperty('UnitTestCase') - def makeitem(self, name, obj, usefilters=True): - # XXX add generic test_suite() support(?) - if py.std.inspect.isclass(obj) and issubclass(obj, unittest.TestCase): - return self.UnitTestCase(name, parent=self) - elif callable(obj) and getattr(obj, 'func_name', '') == 'test_suite': - return None - return super(Module, self).makeitem(name, obj, usefilters) - -class UnitTestCase(py.test.collect.Class): - TestCaseInstance = configproperty('TestCaseInstance') - def collect(self): - return [self.TestCaseInstance("()", self)] - - def setup(self): - pass - - def teardown(self): - pass - -_dummy = object() -class TestCaseInstance(py.test.collect.Instance): - UnitTestFunction = configproperty('UnitTestFunction') - def collect(self): - loader = unittest.TestLoader() - names = loader.getTestCaseNames(self.obj.__class__) - l = [] - for name in names: - callobj = getattr(self.obj, name) - if callable(callobj): - l.append(self.UnitTestFunction(name, parent=self)) - return l - - def _getobj(self): - x = self.parent.obj - return self.parent.obj(methodName='run') - -class UnitTestFunction(py.test.collect.Function): - def __init__(self, name, parent, args=(), obj=_dummy, sort_value=None): - super(UnitTestFunction, self).__init__(name, parent) - self._args = args - if obj is not _dummy: - self._obj = obj - self._sort_value = sort_value - - def runtest(self): - target = self.obj - args = self._args - target(*args) - - def setup(self): - instance = self.obj.im_self - instance.setUp() - - def teardown(self): - instance = self.obj.im_self - instance.tearDown() - diff --git a/contrib/py_unittest/readme.txt b/contrib/py_unittest/readme.txt deleted file mode 100644 index d185d6fde..000000000 --- a/contrib/py_unittest/readme.txt +++ /dev/null @@ -1,7 +0,0 @@ -code for collecting traditional unit tests. -This conftest is based on - - http://johnnydebris.net/svn/projects/py_unittest - -from Guido Wesdorp. - diff --git a/contrib/py_unittest/test_conftest.py b/contrib/py_unittest/test_conftest.py deleted file mode 100644 index 90f75e61c..000000000 --- a/contrib/py_unittest/test_conftest.py +++ /dev/null @@ -1,68 +0,0 @@ -import py -from py.__.test.outcome import Failed -from py.__.test.testing import suptest -conftestpath = py.magic.autopath().dirpath("conftest.py") - -def test_version(): - mod = conftestpath.pyimport() - assert hasattr(mod, "__version__") - -class TestTestCaseInstance(suptest.InlineSession): - def setup_method(self, method): - super(TestTestCaseInstance, self).setup_method(method) - self.tmpdir.ensure("__init__.py") - conftestpath.copy(self.tmpdir.join(conftestpath.basename)) - - def test_simple_unittest(self): - test_one = self.makepyfile(test_one=""" - import unittest - class MyTestCase(unittest.TestCase): - def testpassing(self): - self.assertEquals('foo', 'foo') - """) - sorter = self.parse_and_run(test_one) - rep = sorter.getreport("testpassing") - assert rep.passed - - def test_simple_failing(self): - test_one = self.makepyfile(test_one=""" - import unittest - class MyTestCase(unittest.TestCase): - def test_failing(self): - self.assertEquals('foo', 'bar') - """) - sorter = self.parse_and_run(test_one) - rep = sorter.getreport("test_failing") - assert rep.failed - - def test_setup(self): - test_one = self.makepyfile(test_one=""" - import unittest - class MyTestCase(unittest.TestCase): - def setUp(self): - self.foo = 1 - def test_setUp(self): - self.assertEquals(1, self.foo) - """) - sorter = self.parse_and_run(test_one) - rep = sorter.getreport("test_setUp") - assert rep.passed - - def test_teardown(self): - test_one = self.makepyfile(test_one=""" - import unittest - class MyTestCase(unittest.TestCase): - l = [] - def test_one(self): - pass - def tearDown(self): - self.l.append(None) - class Second(unittest.TestCase): - def test_check(self): - self.assertEquals(MyTestCase.l, [None]) - """) - sorter = self.parse_and_run(test_one) - passed, skipped, failed = sorter.countoutcomes() - assert passed + skipped + failed == 2 - assert failed == 0, failed - assert passed == 2 diff --git a/py/__init__.py b/py/__init__.py index 08c38f068..e8e6771e6 100644 --- a/py/__init__.py +++ b/py/__init__.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- """ -The py lib is a development support library featuring these tools and APIs: +The py lib is an extensible library for testing, distributed processing and +interacting with filesystems. - `py.test`_: cross-project testing tool with many advanced features - `py.execnet`_: ad-hoc code distribution to SSH, Socket and local sub processes @@ -26,8 +27,8 @@ version = "1.0.0a1" initpkg(__name__, description = "pylib and py.test: agile development and test support library", - revision = int('$LastChangedRevision: 58385 $'.split(':')[1][:-1]), - lastchangedate = '$LastChangedDate: 2008-09-23 16:28:13 +0200 (Tue, 23 Sep 2008) $', + revision = int('$LastChangedRevision: 62211 $'.split(':')[1][:-1]), + lastchangedate = '$LastChangedDate: 2009-02-27 11:18:27 +0100 (Fri, 27 Feb 2009) $', version = version, url = "http://pylib.org", download_url = "http://codespeak.net/py/0.9.2/download.html", @@ -37,7 +38,7 @@ initpkg(__name__, author_email = "holger at merlinux.eu, py-dev at codespeak.net", long_description = globals()['__doc__'], classifiers = [ - "Development Status :: 4 - Beta", + "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: POSIX", @@ -53,6 +54,12 @@ initpkg(__name__, # EXPORTED API exportdefs = { + + # py lib events and plugins + '_com.PyPlugins' : ('./_com.py', 'PyPlugins'), + '_com.MultiCall' : ('./_com.py', 'MultiCall'), + '_com.pyplugins' : ('./_com.py', 'pyplugins'), + # py lib cmdline tools 'cmdline.pytest' : ('./cmdline/pytest.py', 'main',), 'cmdline.pyrest' : ('./cmdline/pyrest.py', 'main',), @@ -64,7 +71,9 @@ initpkg(__name__, # helpers for use from test functions or collectors 'test.__doc__' : ('./test/__init__.py', '__doc__'), + 'test._PytestPlugins' : ('./test/pytestplugin.py', 'PytestPlugins'), 'test.raises' : ('./test/outcome.py', 'raises'), + 'test.keywords' : ('./test/outcome.py', 'keywords',), 'test.deprecated_call' : ('./test/outcome.py', 'deprecated_call'), 'test.skip' : ('./test/outcome.py', 'skip'), 'test.importorskip' : ('./test/outcome.py', 'importorskip'), @@ -83,7 +92,6 @@ initpkg(__name__, 'test.collect.File' : ('./test/collect.py', 'File'), 'test.collect.Item' : ('./test/collect.py', 'Item'), 'test.collect.Module' : ('./test/pycollect.py', 'Module'), - 'test.collect.DoctestFile' : ('./test/pycollect.py', 'DoctestFile'), 'test.collect.Class' : ('./test/pycollect.py', 'Class'), 'test.collect.Instance' : ('./test/pycollect.py', 'Instance'), 'test.collect.Generator' : ('./test/pycollect.py', 'Generator'), @@ -187,3 +195,7 @@ initpkg(__name__, 'compat.subprocess' : ('./compat/subprocess.py', '*'), }) +import py +py._com.pyplugins.consider_env() + + diff --git a/py/_com.py b/py/_com.py new file mode 100644 index 000000000..a76829abd --- /dev/null +++ b/py/_com.py @@ -0,0 +1,150 @@ +""" +py lib plugins and events. + +you can write plugins that extend the py lib API. +currently this is mostly used by py.test + +registering a plugin +++++++++++++++++++++++++++++++++++ + +:: + >>> class MyPlugin: + ... def pyevent_plugin_registered(self, plugin): + ... print "registering", plugin.__class__.__name__ + ... + >>> import py + >>> py._com.pyplugins.register(MyPlugin()) + registering MyPlugin + +""" + +import py + +class MultiCall: + """ Manage a specific call into many python functions/methods. + + Simple example: + MultiCall([list1.append, list2.append], 42).execute() + """ + NONEASRESULT = object() + + def __init__(self, methods, *args, **kwargs): + self.methods = methods + self.args = args + self.kwargs = kwargs + self.results = [] + + def execute(self, firstresult=False): + while self.methods: + self.currentmethod = self.methods.pop() + # provide call introspection if "__call__" is the first positional argument + if hasattr(self.currentmethod, 'im_self'): + varnames = self.currentmethod.im_func.func_code.co_varnames + needscall = varnames[1:2] == ('__call__',) + else: + try: + varnames = self.currentmethod.func_code.co_varnames + except AttributeError: + # builtin function + varnames = () + needscall = varnames[:1] == ('__call__',) + if needscall: + res = self.currentmethod(self, *self.args, **self.kwargs) + else: + res = self.currentmethod(*self.args, **self.kwargs) + if res is not None: + if res is self.NONEASRESULT: + res = None + self.results.append(res) + if firstresult: + break + if not firstresult: + return self.results + if self.results: + return self.results[-1] + +class PyPlugins: + """ + Manage Plugins: Load plugins and manage calls to plugins. + """ + MultiCall = MultiCall + + def __init__(self, plugins=None): + if plugins is None: + plugins = [] + self._plugins = plugins + self._callbacks = [] + + def import_module(self, modspec): + # XXX allow modspec to specify version / lookup + modpath = modspec + self.notify("importingmodule", modpath) + __import__(modpath) + + def consider_env(self): + """ consider ENV variable for loading modules. """ + for spec in self._envlist("PYLIB"): + self.import_module(spec) + + def _envlist(self, varname): + val = py.std.os.environ.get(varname, None) + if val is not None: + return val.split(',') + return () + + def consider_module(self, mod, varname="pylib"): + speclist = getattr(mod, varname, ()) + if not isinstance(speclist, (list, tuple)): + speclist = (speclist,) + for spec in speclist: + self.import_module(spec) + + def register(self, plugin): + assert not isinstance(plugin, str) + self._plugins.append(plugin) + self.notify("plugin_registered", plugin) + + def unregister(self, plugin): + self._plugins.remove(plugin) + self.notify("plugin_unregistered", plugin) + + def getplugins(self): + return list(self._plugins) + + def isregistered(self, plugin): + return plugin in self._plugins + + def listattr(self, attrname, plugins=None, extra=()): + l = [] + if plugins is None: + plugins = self._plugins + if extra: + plugins += list(extra) + for plugin in plugins: + try: + l.append(getattr(plugin, attrname)) + except AttributeError: + continue + return l + + def call_each(self, methname, *args, **kwargs): + """ return call object for executing a plugin call. """ + return MultiCall(self.listattr(methname), *args, **kwargs).execute() + + def call_firstresult(self, methname, *args, **kwargs): + """ return first non-None result of a plugin method. """ + return MultiCall(self.listattr(methname), *args, **kwargs).execute(firstresult=True) + + def call_plugin(self, plugin, methname, *args, **kwargs): + return MultiCall(self.listattr(methname, plugins=[plugin]), + *args, **kwargs).execute(firstresult=True) + + def notify(self, eventname, *args, **kwargs): + #print "notifying", eventname, args, kwargs + MultiCall(self.listattr("pyevent_" + eventname), + *args, **kwargs).execute() + #print "calling anonymous hooks", args, kwargs + MultiCall(self.listattr("pyevent"), + eventname, *args, **kwargs).execute() + +pyplugins = PyPlugins() diff --git a/py/apigen/apigen.py b/py/apigen/apigen.py index 808dcd346..4a0c42c8a 100644 --- a/py/apigen/apigen.py +++ b/py/apigen/apigen.py @@ -8,7 +8,6 @@ from py.__.apigen import htmlgen from py.__.apigen import linker from py.__.apigen import project from py.__.apigen.tracer.docstorage import pkg_to_dict -from py.__.doc.conftest import get_apigenpath from layout import LayoutPage @@ -26,9 +25,9 @@ def get_documentable_items_pkgdir(pkgdir): def get_documentable_items(pkgdir): pkgname, pkgdict = get_documentable_items_pkgdir(pkgdir) - from py.__.execnet.channel import Channel - pkgdict['execnet.Channel'] = Channel - Channel.__apigen_hide_from_nav__ = True + #from py.__.execnet.channel import Channel + #pkgdict['execnet.Channel'] = Channel + #Channel.__apigen_hide_from_nav__ = True return pkgname, pkgdict def sourcedirfilter(p): @@ -36,7 +35,7 @@ def sourcedirfilter(p): not p.basename.startswith('.') and str(p).find('c-extension%sgreenlet%sbuild' % (p.sep, p.sep)) == -1) -def build(pkgdir, dsa, capture): +def build(config, pkgdir, dsa, capture): # create a linker (link database) for cross-linking l = linker.TempLinker() @@ -44,8 +43,7 @@ def build(pkgdir, dsa, capture): proj = project.Project() # output dir - from py.__.conftest import option - targetdir = get_apigenpath() + targetdir = proj.apigenpath targetdir.ensure(dir=True) # find out what to build diff --git a/py/apigen/conftest.py b/py/apigen/conftest.py index fdc987376..e607545ef 100644 --- a/py/apigen/conftest.py +++ b/py/apigen/conftest.py @@ -1,10 +1,8 @@ import py -Option = py.test.config.Option -option = py.test.config.addoptions("apigen test options", - Option('', '--webcheck', +class ConftestPlugin: + def pytest_addoption(self, parser): + parser.addoption('--webcheck', action="store_true", dest="webcheck", default=False, help="run XHTML validation tests" - ), -) - + ) diff --git a/py/apigen/htmlgen.py b/py/apigen/htmlgen.py index cef17be00..3b672ebf9 100644 --- a/py/apigen/htmlgen.py +++ b/py/apigen/htmlgen.py @@ -430,8 +430,9 @@ class ApiPageBuilder(AbstractPageBuilder): relpath = get_rel_sourcepath(self.projroot, sourcefile, sourcefile) text = 'source: %s' % (relpath,) if is_in_pkg: - href = self.linker.get_lazyhref(sourcefile, - self.get_anchor(func)) + #href = self.linker.get_lazyhref(sourcefile, + # self.get_anchor(func)) + href = self.linker.get_lazyhref(sourcefile) # csource = H.SourceSnippet(text, href, colored) cslinks = self.build_callsites(dotted_name) snippet = H.FunctionDescription(localname, argdesc, docstring, @@ -464,8 +465,8 @@ class ApiPageBuilder(AbstractPageBuilder): if sourcefile[-1] in ['o', 'c']: sourcefile = sourcefile[:-1] sourcelink = H.div(H.a('view source', - href=self.linker.get_lazyhref(sourcefile, - self.get_anchor(cls)))) + href=self.linker.get_lazyhref(sourcefile) #, self.get_anchor(cls) + )) snippet = H.ClassDescription( # XXX bases HTML diff --git a/py/apigen/layout.py b/py/apigen/layout.py index 81cd953d3..e86b04567 100644 --- a/py/apigen/layout.py +++ b/py/apigen/layout.py @@ -6,7 +6,6 @@ import py from py.__.doc import confrest from py.__.apigen import linker -from py.__.doc.conftest import get_apigenpath, get_docpath here = py.magic.autopath().dirpath() @@ -25,7 +24,7 @@ class LayoutPage(confrest.PyPage): def get_relpath(self): return linker.relpath(self.targetpath.strpath, - get_apigenpath().strpath) + '/' + self.project.apigenpath.strpath) + '/' def set_content(self, contentel): self.contentspace.append(contentel) diff --git a/py/apigen/project.py b/py/apigen/project.py index 4a9569d16..15564f69e 100644 --- a/py/apigen/project.py +++ b/py/apigen/project.py @@ -8,14 +8,18 @@ import py from layout import LayoutPage -class Project(py.__.doc.confrest.Project): +# XXX don't import from an internal py lib class +from py.__.doc import confrest + +class Project(confrest.Project): """ a full project this takes care of storing information on the first pass, and building pages + indexes on the second """ - def __init__(self): + def __init__(self, *args, **kwargs): + confrest.Project.__init__(self, *args, **kwargs) self.content_items = {} def add_item(self, path, content): diff --git a/py/apigen/rest/testing/test_rest.py b/py/apigen/rest/testing/test_rest.py index 44ee41176..b798b41c4 100644 --- a/py/apigen/rest/testing/test_rest.py +++ b/py/apigen/rest/testing/test_rest.py @@ -15,7 +15,6 @@ from py.__.apigen.tracer.permastore import PermaDocStorage import pickle from py.__.apigen.tracer.testing.runtest import cut_pyc -from py.__.doc.conftest import genlinkchecks from py.__.rest.rst import Rest, Paragraph from py.__.rest.transform import HTMLHandler # XXX: UUuuuuuuuuuuuuuuuuuuuuuuu, dangerous import @@ -186,9 +185,11 @@ class TestRest(object): py.test.skip('skipping rest generation because docutils is ' 'not installed (this is a partial skip, the rest ' 'of the test was successful)') - for path in tempdir.listdir('*.txt'): - for item, arg1, arg2, arg3 in genlinkchecks(path): - item(arg1, arg2, arg3) + py.test.skip("partial skip: find a nice way to re-use pytest_restdoc's genlinkchecks") + # XXX find a nice way check pytest_restdoc's genlinkchecks() + #for path in tempdir.listdir('*.txt'): + # for item, arg1, arg2, arg3 in genlinkchecks(path): + # item(arg1, arg2, arg3) def test_generation_simple_api(self): ds = self.get_filled_docstorage() diff --git a/py/apigen/testing/test_apigen_example.py b/py/apigen/testing/test_apigen_example.py index 92e3c28e1..62e8820cb 100644 --- a/py/apigen/testing/test_apigen_example.py +++ b/py/apigen/testing/test_apigen_example.py @@ -8,11 +8,8 @@ from py.__.apigen.tracer.tracer import Tracer from py.__.apigen.layout import LayoutPage from py.__.apigen.project import Project from py.__.test.web import webcheck -from py.__.apigen.conftest import option from py.__.path.svn.testing.svntestbase import make_test_repo -py.test.skip("apigen needs work on py.test to work again") - def run_string_sequence_test(data, seq): currpos = -1 for s in seq: @@ -84,7 +81,7 @@ def _checkhtml(htmlstring): if isinstance(htmlstring, unicode): htmlstring = htmlstring.encode('UTF-8', 'replace') assert isinstance(htmlstring, str) - if option.webcheck: + if py.test.config.option.webcheck: webcheck.check_html(htmlstring) else: py.test.skip("pass --webcheck to validate html produced in tests " @@ -238,7 +235,8 @@ class TestApiPageBuilder(AbstractBuilderTest): self.linker.replace_dirpath(self.base, False) funchtml = self.base.join('api/main.SomeClass.html').read() print funchtml - assert funchtml.find('href="../source/pkg/someclass.py.html#SomeClass"') > -1 + #assert funchtml.find('href="../source/pkg/someclass.py.html#SomeClass"') > -1 + assert funchtml.find('href="../source/pkg/someclass.py.html"') > -1 _checkhtml(funchtml) def test_build_namespace_pages(self): diff --git a/py/bin/gendoc.py b/py/bin/gendoc.py index 2c73cd410..b1abfd841 100644 --- a/py/bin/gendoc.py +++ b/py/bin/gendoc.py @@ -50,7 +50,7 @@ if __name__ == '__main__': if apigendir.check(): print apigendir, "exists, not re-generating - remove to trigger regeneration" else: - sysexec('%(env)s %(pytest)s --apigen=%(pypath)s/apigen/apigen.py py' % locals()) + sysexec('%(env)s %(pytest)s py' % locals()) print print "*" * 30, "static generation", "*" * 30 sysexec('%(env)s %(pytest)s --forcegen %(pypath)s/doc' % locals()) diff --git a/py/cmdline/testing/test_cmdline.py b/py/cmdline/testing/test_cmdline.py index 7bc7ee786..9be71b69c 100644 --- a/py/cmdline/testing/test_cmdline.py +++ b/py/cmdline/testing/test_cmdline.py @@ -1,18 +1,18 @@ -from py.__.test.testing import suptest -from py.__.test.testing.acceptance_test import AcceptBase -class TestPyLookup(AcceptBase): - def test_basic(self): - p = self.makepyfile(hello="def x(): pass") - result = self.runpybin("py.lookup", "pass") - suptest.assert_lines_contain_lines(result.outlines, +pytest_plugins = "pytest_pytester" + +class TestPyLookup: + def test_basic(self, testdir): + p = testdir.makepyfile(hello="def x(): pass") + result = testdir.runpybin("py.lookup", "pass") + result.stdout.fnmatch_lines( ['%s:*def x(): pass' %(p.basename)] ) - def test_search_in_filename(self): - p = self.makepyfile(hello="def x(): pass") - result = self.runpybin("py.lookup", "hello") - suptest.assert_lines_contain_lines(result.outlines, + def test_search_in_filename(self, testdir): + p = testdir.makepyfile(hello="def x(): pass") + result = testdir.runpybin("py.lookup", "hello") + result.stdout.fnmatch_lines( ['*%s:*' %(p.basename)] ) diff --git a/py/conftest.py b/py/conftest.py index 9499c5765..01ae62cb3 100644 --- a/py/conftest.py +++ b/py/conftest.py @@ -1,41 +1,18 @@ -#pythonexecutables = ('python2.2', 'python2.3',) -#pythonexecutable = 'python2.2' +dist_rsync_roots = ['.'] # XXX -# in the future we want to be able to say here: -#def setup_module(extpy): -# mod = extpy.resolve() -# mod.module = 23 -# directory = pypath.root.dirpath() - -# default values for options (modified from cmdline) -verbose = 0 -nocapture = False -collectonly = False -exitfirst = False -fulltrace = False -showlocals = False -nomagic = False +pytest_plugins = 'pytest_doctest', 'pytest_pytester', 'pytest_restdoc' import py -Option = py.test.config.Option - -option = py.test.config.addoptions("execnet options", - Option('-S', '', - action="store", dest="sshtarget", default=None, +class PylibTestPlugin: + def pytest_addoption(self, parser): + group = parser.addgroup("pylib", "py lib testing options") + group.addoption('--sshhost', + action="store", dest="sshhost", default=None, help=("target to run tests requiring ssh, e.g. " - "user@codespeak.net")), - Option('', '--apigenpath', - action="store", dest="apigenpath", - default="../apigen", - type="string", - help="relative path to apigen doc output location (relative from py/)"), - Option('', '--docpath', - action='store', dest='docpath', - default="doc", type='string', - help="relative path to doc output location (relative from py/)"), - Option('', '--runslowtests', + "user@codespeak.net")) + group.addoption('--runslowtests', action="store_true", dest="runslowtests", default=False, - help="run slow tests"), - ) + help="run slow tests") + +ConftestPlugin = PylibTestPlugin -dist_rsync_roots = ['.'] diff --git a/py/doc/confrest.py b/py/doc/confrest.py index 8132e7db8..2ebf292d3 100644 --- a/py/doc/confrest.py +++ b/py/doc/confrest.py @@ -1,7 +1,6 @@ import py from py.__.misc.rest import convert_rest_html, strip_html_header from py.__.misc.difftime import worded_time -from py.__.doc.conftest import get_apigenpath, get_docpath from py.__.apigen.linker import relpath html = py.xml.html @@ -25,13 +24,13 @@ class Page(object): self.fill() def a_docref(self, name, relhtmlpath): - docpath = self.project.get_docpath() + docpath = self.project.docpath return html.a(name, class_="menu", href=relpath(self.targetpath.strpath, docpath.join(relhtmlpath).strpath)) def a_apigenref(self, name, relhtmlpath): - apipath = get_apigenpath() + apipath = self.project.apigenpath return html.a(name, class_="menu", href=relpath(self.targetpath.strpath, apipath.join(relhtmlpath).strpath)) @@ -107,8 +106,6 @@ def getrealname(username): class Project: mydir = py.magic.autopath().dirpath() - # string for url, path for local file - stylesheet = mydir.join('style.css') title = "py lib" prefix_title = "" # we have a logo already containing "py lib" encoding = 'latin1' @@ -119,31 +116,53 @@ class Project: href="http://codespeak.net")) Page = PyPage + def __init__(self, sourcepath=None): + if sourcepath is None: + sourcepath = self.mydir + self.setpath(sourcepath) + + def setpath(self, sourcepath, docpath=None, + apigenpath=None, stylesheet=None): + self.sourcepath = sourcepath + if docpath is None: + docpath = sourcepath + self.docpath = docpath + if apigenpath is None: + apigenpath = docpath + self.apigenpath = apigenpath + if stylesheet is None: + p = sourcepath.join("style.css") + if p.check(): + self.stylesheet = p + else: + self.stylesheet = None + else: + p = py.path.local(stylesheet) + if p.check(): + stylesheet = p + self.stylesheet = stylesheet + self.apigen_relpath = relpath( + self.docpath.strpath + '/', self.apigenpath.strpath + '/') def get_content(self, txtpath, encoding): return unicode(txtpath.read(), encoding) - def get_docpath(self): - return get_docpath() - def get_htmloutputpath(self, txtpath): - docpath = self.get_docpath() - reloutputpath = txtpath.new(ext='.html').relto(self.mydir) - return docpath.join(reloutputpath) + reloutputpath = txtpath.new(ext='.html').relto(self.sourcepath) + return self.docpath.join(reloutputpath) def process(self, txtpath): encoding = self.encoding content = self.get_content(txtpath, encoding) - docpath = self.get_docpath() outputpath = self.get_htmloutputpath(txtpath) stylesheet = self.stylesheet - if isinstance(self.stylesheet, py.path.local): - if not docpath.join(stylesheet.basename).check(): + if isinstance(stylesheet, py.path.local): + if not self.docpath.join(stylesheet.basename).check(): docpath.ensure(dir=True) stylesheet.copy(docpath) stylesheet = relpath(outputpath.strpath, - docpath.join(stylesheet.basename).strpath) + self.docpath.join(stylesheet.basename).strpath) content = convert_rest_html(content, txtpath, stylesheet=stylesheet, encoding=encoding) diff --git a/py/doc/conftest.py b/py/doc/conftest.py deleted file mode 100644 index 7bb922f4d..000000000 --- a/py/doc/conftest.py +++ /dev/null @@ -1,322 +0,0 @@ -from __future__ import generators -import py -from py.__.misc import rest -from py.__.apigen.linker import relpath -import os - -pypkgdir = py.path.local(py.__file__).dirpath() - -mypath = py.magic.autopath().dirpath() - -TIMEOUT_URLOPEN = 5.0 - -Option = py.test.config.Option -option = py.test.config.addoptions("documentation check options", - Option('-R', '--checkremote', - action="store_true", dest="checkremote", default=False, - help="urlopen() remote links found in ReST text files.", - ), - Option('', '--forcegen', - action="store_true", dest="forcegen", default=False, - help="force generation of html files even if they appear up-to-date" - ), -) - -def get_apigenpath(): - from py.__.conftest import option - path = os.environ.get('APIGENPATH') - if path is None: - path = option.apigenpath - return pypkgdir.join(path, abs=True) - -def get_docpath(): - from py.__.conftest import option - path = os.environ.get('DOCPATH') - if path is None: - path = option.docpath - return pypkgdir.join(path, abs=True) - -def get_apigen_relpath(): - return relpath(get_docpath().strpath + '/', - get_apigenpath().strpath + '/') - -def deindent(s, sep='\n'): - leastspaces = -1 - lines = s.split(sep) - for line in lines: - if not line.strip(): - continue - spaces = len(line) - len(line.lstrip()) - if leastspaces == -1 or spaces < leastspaces: - leastspaces = spaces - if leastspaces == -1: - return s - for i, line in py.builtin.enumerate(lines): - if not line.strip(): - lines[i] = '' - else: - lines[i] = line[leastspaces:] - return sep.join(lines) - -_initialized = False -def checkdocutils(): - global _initialized - py.test.importorskip("docutils") - if not _initialized: - from py.__.rest import directive - directive.register_linkrole('api', resolve_linkrole) - directive.register_linkrole('source', resolve_linkrole) - _initialized = True - -def restcheck(path): - localpath = path - if hasattr(path, 'localpath'): - localpath = path.localpath - checkdocutils() - import docutils.utils - - try: - cur = localpath - for x in cur.parts(reverse=True): - confrest = x.dirpath('confrest.py') - if confrest.check(file=1): - confrest = confrest.pyimport() - project = confrest.Project() - _checkskip(path, project.get_htmloutputpath(path)) - project.process(path) - break - else: - # defer to default processor - _checkskip(path) - rest.process(path) - except KeyboardInterrupt: - raise - except docutils.utils.SystemMessage: - # we assume docutils printed info on stdout - py.test.fail("docutils processing failed, see captured stderr") - -def _checkskip(lpath, htmlpath=None): - if not option.forcegen: - lpath = py.path.local(lpath) - if htmlpath is not None: - htmlpath = py.path.local(htmlpath) - if lpath.ext == '.txt': - htmlpath = htmlpath or lpath.new(ext='.html') - if htmlpath.check(file=1) and htmlpath.mtime() >= lpath.mtime(): - py.test.skip("html file is up to date, use --forcegen to regenerate") - #return [] # no need to rebuild - -class ReSTSyntaxTest(py.test.collect.Item): - def runtest(self): - mypath = self.fspath - restcheck(py.path.svnwc(mypath)) - -class DoctestText(py.test.collect.Item): - def runtest(self): - s = self._normalize_linesep() - l = [] - prefix = '.. >>> ' - mod = py.std.types.ModuleType(self.fspath.purebasename) - skipchunk = False - for line in deindent(s).split('\n'): - stripped = line.strip() - if skipchunk and line.startswith(skipchunk): - print "skipping", line - continue - skipchunk = False - if stripped.startswith(prefix): - try: - exec py.code.Source(stripped[len(prefix):]).compile() in \ - mod.__dict__ - except ValueError, e: - if e.args and e.args[0] == "skipchunk": - skipchunk = " " * (len(line) - len(line.lstrip())) - else: - raise - else: - l.append(line) - docstring = "\n".join(l) - mod.__doc__ = docstring - failed, tot = py.compat.doctest.testmod(mod, verbose=1) - if failed: - py.test.fail("doctest %s: %s failed out of %s" %( - self.fspath, failed, tot)) - - def _normalize_linesep(self): - # XXX quite nasty... but it works (fixes win32 issues) - s = self.fspath.read() - linesep = '\n' - if '\r' in s: - if '\n' not in s: - linesep = '\r' - else: - linesep = '\r\n' - s = s.replace(linesep, '\n') - return s - -class LinkCheckerMaker(py.test.collect.Collector): - def collect(self): - l = [] - for call, tryfn, path, lineno in genlinkchecks(self.fspath): - name = "%s:%d" %(tryfn, lineno) - l.append( - CheckLink(name, parent=self, args=(tryfn, path, lineno), callobj=call) - ) - return l - -class CheckLink(py.test.collect.Function): - def repr_metainfo(self): - return self.ReprMetaInfo(fspath=self.fspath, lineno=self._args[2], - modpath="checklink: %s" % (self._args[0],)) - def setup(self): - pass - def teardown(self): - pass - -class DocfileTests(py.test.collect.File): - DoctestText = DoctestText - ReSTSyntaxTest = ReSTSyntaxTest - LinkCheckerMaker = LinkCheckerMaker - - def collect(self): - return [ - self.ReSTSyntaxTest(self.fspath.basename, parent=self), - self.LinkCheckerMaker("checklinks", self), - self.DoctestText("doctest", self), - ] - -# generating functions + args as single tests -def genlinkchecks(path): - for lineno, line in py.builtin.enumerate(path.readlines()): - line = line.strip() - if line.startswith('.. _'): - if line.startswith('.. _`'): - delim = '`:' - else: - delim = ':' - l = line.split(delim, 1) - if len(l) != 2: - continue - tryfn = l[1].strip() - if tryfn.startswith('http:') or tryfn.startswith('https'): - if option.checkremote: - yield urlcheck, tryfn, path, lineno - elif tryfn.startswith('webcal:'): - continue - else: - i = tryfn.find('#') - if i != -1: - checkfn = tryfn[:i] - else: - checkfn = tryfn - if checkfn.strip() and (1 or checkfn.endswith('.html')): - yield localrefcheck, tryfn, path, lineno - -def urlcheck(tryfn, path, lineno): - old = py.std.socket.getdefaulttimeout() - py.std.socket.setdefaulttimeout(TIMEOUT_URLOPEN) - try: - try: - print "trying remote", tryfn - py.std.urllib2.urlopen(tryfn) - finally: - py.std.socket.setdefaulttimeout(old) - except (py.std.urllib2.URLError, py.std.urllib2.HTTPError), e: - if e.code in (401, 403): # authorization required, forbidden - py.test.skip("%s: %s" %(tryfn, str(e))) - else: - py.test.fail("remote reference error %r in %s:%d\n%s" %( - tryfn, path.basename, lineno+1, e)) - -def localrefcheck(tryfn, path, lineno): - # assume it should be a file - i = tryfn.find('#') - if tryfn.startswith('javascript:'): - return # don't check JS refs - if i != -1: - anchor = tryfn[i+1:] - tryfn = tryfn[:i] - else: - anchor = '' - fn = path.dirpath(tryfn) - ishtml = fn.ext == '.html' - fn = ishtml and fn.new(ext='.txt') or fn - print "filename is", fn - if not fn.check(): # not ishtml or not fn.check(): - if not py.path.local(tryfn).check(): # the html could be there - py.test.fail("reference error %r in %s:%d" %( - tryfn, path.basename, lineno+1)) - if anchor: - source = unicode(fn.read(), 'latin1') - source = source.lower().replace('-', ' ') # aehem - - anchor = anchor.replace('-', ' ') - match2 = ".. _`%s`:" % anchor - match3 = ".. _%s:" % anchor - candidates = (anchor, match2, match3) - print "candidates", repr(candidates) - for line in source.split('\n'): - line = line.strip() - if line in candidates: - break - else: - py.test.fail("anchor reference error %s#%s in %s:%d" %( - tryfn, anchor, path.basename, lineno+1)) - - -# ___________________________________________________________ -# -# hooking into py.test Directory collector's chain ... - -class DocDirectory(py.test.collect.Directory): - DocfileTests = DocfileTests - def collect(self): - results = super(DocDirectory, self).collect() - for x in self.fspath.listdir('*.txt', sort=True): - results.append(self.DocfileTests(x, parent=self)) - return results - -Directory = DocDirectory - -def resolve_linkrole(name, text, check=True): - apigen_relpath = get_apigen_relpath() - if name == 'api': - if text == 'py': - return ('py', apigen_relpath + 'api/index.html') - else: - assert text.startswith('py.'), ( - 'api link "%s" does not point to the py package') % (text,) - dotted_name = text - if dotted_name.find('(') > -1: - dotted_name = dotted_name[:text.find('(')] - # remove pkg root - path = dotted_name.split('.')[1:] - dotted_name = '.'.join(path) - obj = py - if check: - for chunk in path: - try: - obj = getattr(obj, chunk) - except AttributeError: - raise AssertionError( - 'problem with linkrole :api:`%s`: can not resolve ' - 'dotted name %s' % (text, dotted_name,)) - return (text, apigen_relpath + 'api/%s.html' % (dotted_name,)) - elif name == 'source': - assert text.startswith('py/'), ('source link "%s" does not point ' - 'to the py package') % (text,) - relpath = '/'.join(text.split('/')[1:]) - if check: - pkgroot = py.__pkg__.getpath() - abspath = pkgroot.join(relpath) - assert pkgroot.join(relpath).check(), ( - 'problem with linkrole :source:`%s`: ' - 'path %s does not exist' % (text, relpath)) - if relpath.endswith('/') or not relpath: - relpath += 'index.html' - else: - relpath += '.html' - return (text, apigen_relpath + 'source/%s' % (relpath,)) - -# legacy -ReSTChecker = DocfileTests diff --git a/py/doc/contact.txt b/py/doc/contact.txt index b13218d5b..d33270012 100644 --- a/py/doc/contact.txt +++ b/py/doc/contact.txt @@ -16,8 +16,6 @@ py lib contact and communication .. _`merlinux.eu`: http://merlinux.eu -.. _`zope3`: http://zope3.zwiki.org/ -.. _twisted: http://www.twistedmatrix.org .. _future: future.html .. _`get an account`: diff --git a/py/doc/example/pytest/test_failures.py b/py/doc/example/pytest/test_failures.py index f692af61c..ba0cc579d 100644 --- a/py/doc/example/pytest/test_failures.py +++ b/py/doc/example/pytest/test_failures.py @@ -2,14 +2,13 @@ import py failure_demo = py.magic.autopath().dirpath('failure_demo.py') -from py.__.test.testing import suptest -from py.__.test import event +pytest_plugins = "pytest_pytester" -def test_failure_demo_fails_properly(): - sorter = suptest.events_from_cmdline([failure_demo]) +def test_failure_demo_fails_properly(testdir): + sorter = testdir.inline_run(failure_demo) passed, skipped, failed = sorter.countoutcomes() assert passed == 0 assert failed == 20, failed - colreports = sorter.get(event.CollectionReport) + colreports = sorter.getnamed("collectionreport") failed = len([x.failed for x in colreports]) assert failed == 5 diff --git a/py/doc/future.txt b/py/doc/future.txt index 84356a192..0a8553b35 100644 --- a/py/doc/future.txt +++ b/py/doc/future.txt @@ -105,10 +105,7 @@ be directly usable for such linux-filesystem glue code. In other words, implementing a `memoryfs`_ or a `dictfs`_ would give you two things for free: a filesystem mountable at kernel level as well as a uniform "path" object allowing you to access your -filesystem in convenient ways. (At some point it might -even become interesting to think about interfacing to -`reiserfs v4 features`_ at the Filesystem level but that -is a can of subsequent worms). +filesystem in convenient ways. Also interesting to check out is Will McGugan's work on his `fs package`_. @@ -128,7 +125,6 @@ is Matthew Scotts `dictproxy patch`_ which adds .. _`dictfs`: http://codespeak.net/pipermail/py-dev/2005-January/000191.html .. _`pylufs`: http://codespeak.net/svn/user/arigo/hack/pylufs/ .. _`pyfuse`: http://codespeak.net/svn/user/arigo/hack/pyfuse/ -.. _`reiserfs v4 features`: http://www.namesys.com/v4/v4.html Integrate interactive completion diff --git a/py/doc/impl-test.txt b/py/doc/impl-test.txt index ab8c3ec3a..a24b698cf 100644 --- a/py/doc/impl-test.txt +++ b/py/doc/impl-test.txt @@ -270,3 +270,5 @@ Customizing execution of Functions function with the given (usually empty set of) arguments. .. _`py-dev mailing list`: http://codespeak.net/mailman/listinfo/py-dev + + diff --git a/py/doc/path.txt b/py/doc/path.txt index aae977c9c..836059e36 100644 --- a/py/doc/path.txt +++ b/py/doc/path.txt @@ -52,7 +52,7 @@ solutions. Some example usage of :api:`py.path.svnurl`:: .. >>> import py - .. >>> if not py.test.config.option.checkremote: raise ValueError('skipchunk') + .. >>> if not py.test.config.option.urlcheck: raise ValueError('skipchunk') >>> url = py.path.svnurl('http://codespeak.net/svn/py') >>> info = url.info() >>> info.kind @@ -64,7 +64,7 @@ Some example usage of :api:`py.path.svnurl`:: Example usage of :api:`py.path.svnwc`:: - .. >>> if not py.test.config.option.checkremote: raise ValueError('skipchunk') + .. >>> if not py.test.config.option.urlcheck: raise ValueError('skipchunk') >>> temp = py.test.ensuretemp('py.path_documentation') >>> wc = py.path.svnwc(temp.join('svnwc')) >>> wc.checkout('http://codespeak.net/svn/py/dist/py/path/local') @@ -132,6 +132,10 @@ don't have to exist, either):: >>> sep = py.path.local.sep >>> p2.relto(p1).replace(sep, '/') # os-specific path sep in the string 'baz/qux' + >>> p2.bestrelpath(p1) + '../..' + >>> p2.join(p2.bestrelpath(p1)) == p1 + True >>> p3 = p1 / 'baz/qux' # the / operator allows joining, too >>> p2 == p3 True @@ -178,7 +182,7 @@ Setting svn-properties As an example of 'uncommon' methods, we'll show how to read and write properties in an :api:`py.path.svnwc` instance:: - .. >>> if not py.test.config.option.checkremote: raise ValueError('skipchunk') + .. >>> if not py.test.config.option.urlcheck: raise ValueError('skipchunk') >>> wc.propget('foo') '' >>> wc.propset('foo', 'bar') @@ -196,7 +200,7 @@ SVN authentication Some uncommon functionality can also be provided as extensions, such as SVN authentication:: - .. >>> if not py.test.config.option.checkremote: raise ValueError('skipchunk') + .. >>> if not py.test.config.option.urlcheck: raise ValueError('skipchunk') >>> auth = py.path.SvnAuth('anonymous', 'user', cache_auth=False, ... interactive=False) >>> wc.auth = auth diff --git a/py/doc/pytest-plugins.txt b/py/doc/pytest-plugins.txt new file mode 100644 index 000000000..8abdd0d7b --- /dev/null +++ b/py/doc/pytest-plugins.txt @@ -0,0 +1,67 @@ + +pytest plugins +================== + +specifying plugins for directories or test modules +--------------------------------------------------------- + +py.test loads and configures plugins at tool startup and whenever +it encounters new confest or test modules which +contain a ``pytest_plugins`` definition. At tool +startup the ``PYTEST_PLUGINS`` environment variable +is considered as well. + +Example +++++++++++ + +If you create a ``conftest.py`` file with the following content:: + + pytest_plugins = "pytest_plugin1", MyLocalPluginClass + +then test execution within that directory can make use +of the according instantiated plugins: + +* the module ``pytest_plugin1`` will be imported and + and its contained `Plugin1`` class instantiated. + A plugin module can put its dependencies into + a "pytest_plugins" attribute at module level as well. + +* the ``MyLocalPluginClass`` will be instantiated + and added to the pluginmanager. + + +Plugin methods +---------------------------------- + +A Plugin class may implement the following attributes and methods: + +* pytest_cmdlineoptions: a list of optparse-style py.test.config.Option objects +* pytest_configure(self, config): called after command line options have been parsed +* pytest_unconfigure(self, config): called before the test process quits +* pytest_event(self, event): called for each `pytest event`_ + +XXX reference APIcheck'ed full documentation + +_`pytest event`: + +Pytest Events +------------------- + +XXX Various reporting events. + +Example plugins +----------------------- + +XXX here are a few existing plugins: + +* adding reporting facilities, e.g. + pytest_terminal: default reporter for writing info to terminals + pytest_resultlog: log test results in machine-readable form to a file + pytest_eventlog: log all internal pytest events to a file + pytest_xfail: "expected to fail" test marker + pytest_tmpdir: provide temporary directories to test functions + pytest_plugintester: generic apichecks, support for functional plugin tests + pytest_pytester: support for testing py.test runs + +* extending test execution, e.g. + pytest_apigen: tracing values of function/method calls when running tests diff --git a/py/doc/test.txt b/py/doc/test.txt index 847bb2a82..9fee5f9d7 100644 --- a/py/doc/test.txt +++ b/py/doc/test.txt @@ -142,23 +142,18 @@ To make it easier to distinguish the generated tests it is possible to specify a selecting/unselecting tests by keyword --------------------------------------------- +Pytest's keyword mechanism provides a powerful way to +group and selectively run tests in your test code base. You can selectively run tests by specifiying a keyword -on the command line. Example:: +on the command line. Examples: - py.test -k test_simple + py.test -k test_simple + py.test -k "-test_simple" -will run all tests that are found from the current directory -and where the word "test_simple" equals the start of one part of the -path leading up to the test item. Directory and file basenames as well -as function, class and function/method names each form a possibly -matching name. You can also unselect tests by preceding a keyword -with a dash:: - - py.test. -k "-test_simple" - -will run all tests except where the word "test_simple" matches a tests keyword. -Note that you need to quote the keyword if the shell recognizes "-" as an intro -to a cmdline option. Lastly, you may use +will run all tests matching (or not matching) the +"test_simple" keyword. Note that you need to quote +the keyword if "-" is recognized as an indicator +for a commandline option. Lastly, you may use py.test. -k "test_simple:" @@ -166,6 +161,15 @@ which will run all tests after the expression has *matched once*, i.e. all tests that are seen after a test that matches the "test_simple" keyword. +By default, all filename parts and +class/function names of a test function are put into the set +of keywords for a given test. You may specify additional +kewords like this:: + + @py.test.keywords("webtest") + def test_send_http(): + ... + testing with multiple python versions / executables --------------------------------------------------- diff --git a/py/doc/test_conftest.py b/py/doc/test_conftest.py deleted file mode 100644 index 40eb13053..000000000 --- a/py/doc/test_conftest.py +++ /dev/null @@ -1,115 +0,0 @@ - -import py -from py.__.test import event -from py.__.test.testing import suptest -from py.__.doc import conftest as doc_conftest - - -class TestDoctest(suptest.InlineCollection): - def setup_method(self, method): - super(TestDoctest, self).setup_method(method) - p = py.path.local(doc_conftest.__file__) - if p.ext == ".pyc": - p = p.new(ext=".py") - p.copy(self.tmpdir.join("conftest.py")) - - def test_doctest_extra_exec(self): - xtxt = self.maketxtfile(x=""" - hello:: - .. >>> raise ValueError - >>> None - """) - sorter = suptest.events_from_cmdline([xtxt]) - passed, skipped, failed = sorter.countoutcomes() - assert failed == 1 - - def test_doctest_basic(self): - xtxt = self.maketxtfile(x=""" - .. - >>> from os.path import abspath - - hello world - - >>> assert abspath - >>> i=3 - >>> print i - 3 - - yes yes - - >>> i - 3 - - end - """) - sorter = suptest.events_from_cmdline([xtxt]) - passed, skipped, failed = sorter.countoutcomes() - assert failed == 0 - assert passed + skipped == 2 - - def test_doctest_eol(self): - ytxt = self.maketxtfile(y=".. >>> 1 + 1\r\n 2\r\n\r\n") - sorter = suptest.events_from_cmdline([ytxt]) - passed, skipped, failed = sorter.countoutcomes() - assert failed == 0 - assert passed + skipped == 2 - - def test_doctest_indentation(self): - footxt = self.maketxtfile(foo= - '..\n >>> print "foo\\n bar"\n foo\n bar\n') - sorter = suptest.events_from_cmdline([footxt]) - passed, skipped, failed = sorter.countoutcomes() - assert failed == 0 - assert skipped + passed == 2 - - def test_js_ignore(self): - xtxt = self.maketxtfile(xtxt=""" - `blah`_ - - .. _`blah`: javascript:some_function() - """) - sorter = suptest.events_from_cmdline([xtxt]) - passed, skipped, failed = sorter.countoutcomes() - assert failed == 0 - assert skipped + passed == 3 - -def test_deindent(): - deindent = doc_conftest.deindent - assert deindent('foo') == 'foo' - assert deindent('foo\n bar') == 'foo\n bar' - assert deindent(' foo\n bar\n') == 'foo\nbar\n' - assert deindent(' foo\n\n bar\n') == 'foo\n\nbar\n' - assert deindent(' foo\n bar\n') == 'foo\n bar\n' - assert deindent(' foo\n bar\n') == ' foo\nbar\n' - - -def test_resolve_linkrole(): - from py.__.doc.conftest import get_apigen_relpath - apigen_relpath = get_apigen_relpath() - from py.__.doc.conftest import resolve_linkrole - assert resolve_linkrole('api', 'py.foo.bar', False) == ( - 'py.foo.bar', apigen_relpath + 'api/foo.bar.html') - assert resolve_linkrole('api', 'py.foo.bar()', False) == ( - 'py.foo.bar()', apigen_relpath + 'api/foo.bar.html') - assert resolve_linkrole('api', 'py', False) == ( - 'py', apigen_relpath + 'api/index.html') - py.test.raises(AssertionError, 'resolve_linkrole("api", "foo.bar")') - assert resolve_linkrole('source', 'py/foo/bar.py', False) == ( - 'py/foo/bar.py', apigen_relpath + 'source/foo/bar.py.html') - assert resolve_linkrole('source', 'py/foo/', False) == ( - 'py/foo/', apigen_relpath + 'source/foo/index.html') - assert resolve_linkrole('source', 'py/', False) == ( - 'py/', apigen_relpath + 'source/index.html') - py.test.raises(AssertionError, 'resolve_linkrole("source", "/foo/bar/")') - -def test_resolve_linkrole_check_api(): - from py.__.doc.conftest import resolve_linkrole - assert resolve_linkrole('api', 'py.test.ensuretemp') - py.test.raises(AssertionError, "resolve_linkrole('api', 'py.foo.baz')") - -def test_resolve_linkrole_check_source(): - from py.__.doc.conftest import resolve_linkrole - assert resolve_linkrole('source', 'py/path/common.py') - py.test.raises(AssertionError, - "resolve_linkrole('source', 'py/foo/bar.py')") - diff --git a/py/doc/xml.txt b/py/doc/xml.txt index b1e686b33..2ed4c07e8 100644 --- a/py/doc/xml.txt +++ b/py/doc/xml.txt @@ -168,5 +168,4 @@ complete the probably request-specific serialization of your Tags. Hum, it's probably harder to explain this than to actually code it :-) -.. _Nevow: http://www.divmod.org/projects/nevow .. _`py.test`: test.html diff --git a/py/execnet/testing/test_gateway.py b/py/execnet/testing/test_gateway.py index 94d2ec68f..2ec050ae6 100644 --- a/py/execnet/testing/test_gateway.py +++ b/py/execnet/testing/test_gateway.py @@ -2,7 +2,6 @@ from __future__ import generators import os, sys, time, signal import py from py.__.execnet import gateway -from py.__.conftest import option mypath = py.magic.autopath() from StringIO import StringIO @@ -247,7 +246,10 @@ class BasicRemoteExecution: channel.waitclose(TESTTIMEOUT) assert l == [42] - def test_channel_callback_stays_active(self, earlyfree=True): + def test_channel_callback_stays_active(self): + self.check_channel_callback_stays_active(earlyfree=True) + + def check_channel_callback_stays_active(self, earlyfree=True): # with 'earlyfree==True', this tests the "sendonly" channel state. l = [] channel = self.gw.remote_exec(source=''' @@ -278,7 +280,7 @@ class BasicRemoteExecution: return subchannel def test_channel_callback_remote_freed(self): - channel = self.test_channel_callback_stays_active(False) + channel = self.check_channel_callback_stays_active(earlyfree=False) channel.waitclose(TESTTIMEOUT) # freed automatically at the end of producer() def test_channel_endmarker_callback(self): @@ -568,16 +570,16 @@ class TestSocketGateway(SocketGatewaySetup, BasicRemoteExecution): class TestSshGateway(BasicRemoteExecution): def setup_class(cls): - if option.sshtarget is None: - py.test.skip("no known ssh target, use -S to set one") - cls.gw = py.execnet.SshGateway(option.sshtarget) + if py.test.config.option.sshhost is None: + py.test.skip("no known ssh target, use --sshhost to set one") + cls.gw = py.execnet.SshGateway(py.test.config.option.sshhost) def test_sshconfig_functional(self): tmpdir = py.test.ensuretemp("test_sshconfig") ssh_config = tmpdir.join("ssh_config") ssh_config.write( "Host alias123\n" - " HostName %s\n" % (option.sshtarget,)) + " HostName %s\n" % (py.test.config.option.sshhost,)) gw = py.execnet.SshGateway("alias123", ssh_config=ssh_config) assert gw._cmd.find("-F") != -1 assert gw._cmd.find(str(ssh_config)) != -1 @@ -585,7 +587,7 @@ class TestSshGateway(BasicRemoteExecution): gw.exit() def test_sshaddress(self): - assert self.gw.remoteaddress == option.sshtarget + assert self.gw.remoteaddress == py.test.config.option.sshhost def test_connexion_failes_on_non_existing_hosts(self): py.test.raises(IOError, diff --git a/py/initpkg.py b/py/initpkg.py index d865d4a98..1bf620230 100644 --- a/py/initpkg.py +++ b/py/initpkg.py @@ -64,12 +64,6 @@ class Package(object): def _resolve(self, extpyish): """ resolve a combined filesystem/python extpy-ish path. """ fspath, modpath = extpyish - if not fspath.endswith('.py'): - import py - e = py.path.local(self.implmodule.__file__) - e = e.dirpath(fspath, abs=True) - e = py.path.extpy(e, modpath) - return e.resolve() assert fspath.startswith('./'), \ "%r is not an implementation path (XXX)" % (extpyish,) implmodule = self._loadimpl(fspath[:-3]) @@ -166,15 +160,18 @@ def setmodule(modpath, module): sys.modules[modpath] = module # --------------------------------------------------- -# Virtual Module Object +# API Module Object # --------------------------------------------------- -class Module(ModuleType): +class ApiModule(ModuleType): def __init__(self, pkg, name): self.__pkg__ = pkg self.__name__ = name self.__map__ = {} + def __repr__(self): + return '' % (self.__name__,) + def __getattr__(self, name): if '*' in self.__map__: extpy = self.__map__['*'][0], name @@ -209,9 +206,6 @@ class Module(ModuleType): except (AttributeError, TypeError): pass - def __repr__(self): - return '' % (self.__name__, ) - def getdict(self): # force all the content of the module to be loaded when __dict__ is read dictdescr = ModuleType.__dict__['__dict__'] @@ -254,7 +248,7 @@ def initpkg(pkgname, exportdefs, **kw): previous = current current += '.' + name if current not in seen: - seen[current] = mod = Module(pkg, current) + seen[current] = mod = ApiModule(pkg, current) setattr(seen[previous], name, mod) setmodule(current, mod) @@ -272,3 +266,12 @@ def initpkg(pkgname, exportdefs, **kw): for mod, pypart, extpy in deferred_imports: setattr(mod, pypart, pkg._resolve(extpy)) + autoimport(pkgname) + +def autoimport(pkgname): + import py + ENVKEY = pkgname.upper() + "_AUTOIMPORT" + if ENVKEY in os.environ: + for impname in os.environ[ENVKEY].split(","): + py._com.pyplugins.notify("autoimport", impname) + __import__(impname) diff --git a/py/misc/rest.py b/py/misc/rest.py index 5094716ed..b4318bc97 100644 --- a/py/misc/rest.py +++ b/py/misc/rest.py @@ -71,3 +71,11 @@ def strip_html_header(string, encoding='utf8'): break uni = match.group(1) return uni + +class Project: # used for confrest.py files + def __init__(self, sourcepath): + self.sourcepath = sourcepath + def process(self, path): + return process(path) + def get_htmloutputpath(self, path): + return path.new(ext='html') diff --git a/py/misc/testing/test_cache.py b/py/misc/testing/test_cache.py index be4e3406b..6b6c4302b 100644 --- a/py/misc/testing/test_cache.py +++ b/py/misc/testing/test_cache.py @@ -52,7 +52,7 @@ class TestBuildcostAccess(BasicCacheAPITest): class TestAging(BasicCacheAPITest): - maxsecs = 0.02 + maxsecs = 0.10 cache = AgingCache(maxentries=128, maxseconds=maxsecs) def test_cache_eviction(self): diff --git a/py/misc/testing/test_com.py b/py/misc/testing/test_com.py new file mode 100644 index 000000000..801389901 --- /dev/null +++ b/py/misc/testing/test_com.py @@ -0,0 +1,233 @@ + +import py +import os +from py._com import PyPlugins, MultiCall + +pytest_plugins = "xfail" + +class TestMultiCall: + def test_call_passing(self): + class P1: + def m(self, __call__, x): + assert __call__.currentmethod == self.m + assert len(__call__.results) == 1 + assert not __call__.methods + return 17 + + class P2: + def m(self, __call__, x): + assert __call__.currentmethod == self.m + assert __call__.args + assert __call__.results == [] + assert __call__.methods + return 23 + + p1 = P1() + p2 = P2() + multicall = MultiCall([p1.m, p2.m], 23) + reslist = multicall.execute() + assert len(reslist) == 2 + # ensure reversed order + assert reslist == [23, 17] + + def test_optionalcallarg(self): + class P1: + def m(self, x): + return x + call = MultiCall([P1().m], 23) + assert call.execute() == [23] + assert call.execute(firstresult=True) == 23 + + def test_call_subexecute(self): + def m(__call__): + subresult = __call__.execute(firstresult=True) + return subresult + 1 + + def n(): + return 1 + + call = MultiCall([n, m]) + res = call.execute(firstresult=True) + assert res == 2 + +class TestPyPlugins: + def test_MultiCall(self): + plugins = PyPlugins() + assert hasattr(plugins, "MultiCall") + + def test_register(self): + plugins = PyPlugins() + class MyPlugin: + pass + my = MyPlugin() + plugins.register(my) + assert plugins.getplugins() == [my] + my2 = MyPlugin() + plugins.register(my2) + assert plugins.getplugins() == [my, my2] + + assert plugins.isregistered(my) + assert plugins.isregistered(my2) + plugins.unregister(my) + assert not plugins.isregistered(my) + assert plugins.getplugins() == [my2] + + #@py.test.keywords(xfail=True) + def test_onregister(self): + py.test.skip("implement exitfirst plugin and " + "modify xfail plugin to override exitfirst behaviour?") + plugins = PyPlugins() + l = [] + class MyApi: + def pyevent_plugin_registered(self, plugin): + l.append(plugin) + def pyevent_plugin_unregistered(self, plugin): + l.remove(plugin) + myapi = MyApi() + plugins.register(myapi) + assert len(l) == 1 + assert l[0] is myapi + plugins.unregister(myapi) + assert not l + + def test_call_methods(self): + plugins = PyPlugins() + class api1: + def m(self, __call__, x): + return x + class api2: + def m(self, __call__, x, y=33): + return y + plugins.register(api1()) + plugins.register(api2()) + res = plugins.call_firstresult("m", x=5) + assert plugins.call_firstresult("notexist") is None + + assert res == 33 + reslist = plugins.call_each("m", x=5) + assert len(reslist) == 2 + assert 5 in reslist + assert 33 in reslist + assert plugins.call_each("notexist") == [] + + assert plugins.call_plugin(api1(), 'm', x=12) == 12 + assert plugins.call_plugin(api2(), 't') is None + + def test_call_none_is_no_result(self): + plugins = PyPlugins() + class api1: + def m(self): + return None + class api2: + def m(self, __call__): + return 41 + plugins.register(api1()) + plugins.register(api1()) + plugins.register(api2()) + assert plugins.call_firstresult('m') == 41 + assert plugins.call_each('m') == [41] + + def test_call_noneasresult(self): + plugins = PyPlugins() + class api1: + def m(self, __call__): + return __call__.NONEASRESULT + plugins.register(api1()) + plugins.register(api1()) + assert plugins.call_firstresult('m') is None + assert plugins.call_each('m') == [None, None] + + def test_listattr(self): + plugins = PyPlugins() + class api1: + x = 42 + class api2: + x = 41 + plugins.register(api1()) + plugins.register(api2()) + l = list(plugins.listattr('x')) + l.sort() + assert l == [41, 42] + + def test_notify_anonymous_ordered(self): + plugins = PyPlugins() + l = [] + class api1: + def pyevent_hello(self): + l.append("hellospecific") + class api2: + def pyevent(self, name, *args): + if name == "hello": + l.append(name + "anonymous") + plugins.register(api1()) + plugins.register(api2()) + plugins.notify('hello') + assert l == ["hellospecific", "helloanonymous"] + + def test_consider_env(self, monkeypatch): + # XXX write a helper for preserving os.environ + plugins = PyPlugins() + monkeypatch.setitem(os.environ, 'PYLIB', "unknownconsider_env") + py.test.raises(ImportError, "plugins.consider_env()") + + def test_consider_module(self): + plugins = PyPlugins() + mod = py.std.new.module("temp") + mod.pylib = ["xxx nomod"] + excinfo = py.test.raises(ImportError, "plugins.consider_module(mod)") + mod.pylib = "os" + class Events(list): + def pyevent_importingmodule(self, mod): + self.append(mod) + l = Events() + plugins.register(l) + plugins.consider_module(mod) + assert len(l) == 1 + assert l[0] == (mod.pylib) + +def test_api_and_defaults(): + assert isinstance(py._com.pyplugins, PyPlugins) + +def test_subprocess_env(): + # XXX write a helper for preserving os.environ + plugins = PyPlugins() + KEY = "PYLIB" + old = os.environ.get(KEY, None) + olddir = py.path.local(py.__file__).dirpath().dirpath().chdir() + try: + os.environ[KEY] = "unknownconsider_env" + excinfo = py.test.raises(py.process.cmdexec.Error, """ + py.process.cmdexec("python -c 'import py'") + """) + assert str(excinfo.value).find("ImportError") != -1 + assert str(excinfo.value).find("unknownconsider") != -1 + finally: + olddir.chdir() + if old is None: + del os.environ[KEY] + else: + os.environ[KEY] = old + +class TestPyPluginsEvents: + def test_pyevent_named_dispatch(self): + plugins = PyPlugins() + l = [] + class A: + def pyevent_name(self, x): + l.append(x) + plugins.register(A()) + plugins.notify("name", 13) + assert l == [13] + + def test_pyevent_anonymous_dispatch(self): + plugins = PyPlugins() + l = [] + class A: + def pyevent(self, name, *args, **kwargs): + if name == "name": + l.extend([args, kwargs]) + + plugins.register(A()) + plugins.notify("name", 13, x=15) + assert l == [(13, ), {'x':15}] + diff --git a/py/misc/testing/test_initpkg.py b/py/misc/testing/test_initpkg.py index 442face2d..29cdfe699 100644 --- a/py/misc/testing/test_initpkg.py +++ b/py/misc/testing/test_initpkg.py @@ -12,20 +12,20 @@ def checksubpackage(name): assert getattr(obj, '__map__') == {} def test_dir(): - from py.__.initpkg import Module + from py.__.initpkg import ApiModule for name in dir(py): if name == 'magic': # greenlets don't work everywhere, we don't care here continue if not name.startswith('_'): yield checksubpackage, name -from py.initpkg import Module +from py.initpkg import ApiModule glob = [] -class MyModule(Module): +class MyModule(ApiModule): def __init__(self, *args): glob.append(self.__dict__) assert isinstance(glob[-1], (dict, type(None))) - Module.__init__(self, *args) + ApiModule.__init__(self, *args) def test_early__dict__access(): mymod = MyModule("whatever", "myname") @@ -68,7 +68,10 @@ def test_importall(): base.join('execnet', 'script'), base.join('compat', 'testing'), ) - for p in base.visit('*.py', lambda x: x.check(dotfile=0)): + def recurse(p): + return p.check(dotfile=0) and p.basename != "attic" + + for p in base.visit('*.py', recurse): if p.basename == '__init__.py': continue relpath = p.new(ext='').relto(base) @@ -255,3 +258,8 @@ class TestRealModule: # help(std.path) # #assert False + +def test_autoimport(): + from py.initpkg import autoimport + py.std.os.environ['AUTOTEST_AUTOIMPORT'] = "nonexistmodule" + py.test.raises(ImportError, "autoimport('autotest')") diff --git a/py/misc/testing/test_warn.py b/py/misc/testing/test_warn.py index e0b27f048..38c59ccbd 100644 --- a/py/misc/testing/test_warn.py +++ b/py/misc/testing/test_warn.py @@ -1,20 +1,20 @@ import py -from py.__.misc.warn import WarningBus +from py.__.misc.warn import WarningPlugin mypath = py.magic.autopath() -class TestWarningBus: +class TestWarningPlugin: def setup_method(self, method): - self.wb = WarningBus() + self.bus = py._com.PyPlugins() + self.wb = WarningPlugin(self.bus) + self.bus.register(self) self.warnings = [] - self.wb.subscribe(self.warnings.append) - def test_basic(self): + def pyevent_WARNING(self, warning): + self.warnings.append(warning) + + def test_event_generation(self): self.wb.warn("hello") assert len(self.warnings) == 1 - self.wb.unsubscribe(self.warnings.append) - self.wb.warn("this") - assert len(self.warnings) == 1 - w = self.warnings[0] def test_location(self): self.wb.warn("again") @@ -27,19 +27,16 @@ class TestWarningBus: assert str(warning) == warning.msg def test_stacklevel(self): - l = [] - self.wb.subscribe(l.append) def f(): self.wb.warn("x", stacklevel=2) - # 5 - # 6 + # 3 + # 4 f() - lno = self.test_stacklevel.im_func.func_code.co_firstlineno + 7 - warning = l[0] + lno = self.test_stacklevel.im_func.func_code.co_firstlineno + 5 + warning = self.warnings[0] assert warning.lineno == lno def test_forwarding_to_warnings_module(self): - self.wb._setforwarding() py.test.deprecated_call(self.wb.warn, "x") def test_apiwarn(self): @@ -47,7 +44,6 @@ class TestWarningBus: warning = self.warnings[0] assert warning.msg == "xxx (since version 3.0)" -def test_APIWARN(): +def test_default(): from py.__.misc.warn import APIWARN - wb = APIWARN.im_self - assert wb._forward in wb._eventbus._subscribers + assert py._com.pyplugins.isregistered(APIWARN.im_self) diff --git a/py/misc/warn.py b/py/misc/warn.py index 146a295bb..6cbce7d35 100644 --- a/py/misc/warn.py +++ b/py/misc/warn.py @@ -1,5 +1,4 @@ import py, sys -from py.__.test.event import EventBus class Warning(py.std.exceptions.DeprecationWarning): def __init__(self, msg, path, lineno): @@ -11,19 +10,16 @@ class Warning(py.std.exceptions.DeprecationWarning): def __str__(self): return self.msg -class WarningBus(object): - def __init__(self): - self._eventbus = EventBus() +# XXX probably only apiwarn() + py._com.pyplugins forwarding +# warn_explicit is actually needed - def subscribe(self, callable): - self._eventbus.subscribe(callable) - - def unsubscribe(self, callable): - self._eventbus.unsubscribe(callable) - - def _setforwarding(self): - self._eventbus.subscribe(self._forward) - def _forward(self, warning): +class WarningPlugin(object): + def __init__(self, bus): + self.bus = bus + bus.register(self) + + def pyevent_WARNING(self, warning): + # forward to python warning system py.std.warnings.warn_explicit(warning, category=Warning, filename=str(warning.path), lineno=warning.lineno, @@ -66,9 +62,8 @@ class WarningBus(object): filename = module path = py.path.local(filename) warning = Warning(msg, path, lineno) - self._eventbus.notify(warning) + self.bus.notify("WARNING", warning) # singleton api warner for py lib -apiwarner = WarningBus() -apiwarner._setforwarding() +apiwarner = WarningPlugin(py._com.pyplugins) APIWARN = apiwarner.apiwarn diff --git a/py/path/common.py b/py/path/common.py index 931e399e7..92505dd36 100644 --- a/py/path/common.py +++ b/py/path/common.py @@ -152,6 +152,30 @@ class PathBase(object): return strself[len(strrelpath):] return "" + def bestrelpath(self, dest): + """ return relative path from self to dest + such that self.join(bestrelpath) == dest. + if not such path can be determined return dest. + """ + try: + base = self.common(dest) + if not base: # can be the case on windows + return dest + self2base = self.relto(base) + reldest = dest.relto(base) + if self2base: + n = self2base.count(self.sep) + 1 + else: + n = 0 + l = ['..'] * n + if reldest: + l.append(reldest) + target = dest.sep.join(l) + return target + except AttributeError: + return dest + + def parts(self, reverse=False): """ return a root-first list of all ancestor directories plus the path itself. diff --git a/py/path/svn/testing/test_auth.py b/py/path/svn/testing/test_auth.py index fce4817e0..ea1b633f3 100644 --- a/py/path/svn/testing/test_auth.py +++ b/py/path/svn/testing/test_auth.py @@ -3,7 +3,6 @@ from py.path import SvnAuth import svntestbase from threading import Thread import time -from py.__.conftest import option def make_repo_auth(repo, userdata): """ write config to repo @@ -251,7 +250,7 @@ class TestSvnURLAuth(object): class SvnAuthFunctionalTestBase(object): def setup_class(cls): svntestbase.getsvnbin() - if not option.runslowtests: + if not py.test.config.option.runslowtests: py.test.skip('skipping slow functional tests - use --runslowtests ' 'to override') diff --git a/py/path/svn/testing/test_wccommand.py b/py/path/svn/testing/test_wccommand.py index d9e1738b4..8067f80f5 100644 --- a/py/path/svn/testing/test_wccommand.py +++ b/py/path/svn/testing/test_wccommand.py @@ -4,7 +4,6 @@ from py.__.path.svn.testing.svntestbase import CommonSvnTests, getrepowc, getsvn from py.__.path.svn.wccommand import InfoSvnWCCommand, XMLWCStatus from py.__.path.svn.wccommand import parse_wcinfotime from py.__.path.svn import svncommon -from py.__.conftest import option if sys.platform != 'win32': def normpath(p): @@ -157,7 +156,7 @@ class TestWCSvnCommandPath(CommonSvnTests): self.root.revert(rec=1) def test_status_conflict(self): - if not option.runslowtests: + if not py.test.config.option.runslowtests: py.test.skip('skipping slow unit tests - use --runslowtests ' 'to override') wc = self.root @@ -177,7 +176,7 @@ class TestWCSvnCommandPath(CommonSvnTests): assert [x.basename for x in s.conflict] == ['conflictsamplefile'] def test_status_external(self): - if not option.runslowtests: + if not py.test.config.option.runslowtests: py.test.skip('skipping slow unit tests - use --runslowtests ' 'to override') otherrepo, otherwc = getrepowc('externalrepo', 'externalwc') diff --git a/py/path/testing/common.py b/py/path/testing/common.py index f8bc26cc4..c56fda0b5 100644 --- a/py/path/testing/common.py +++ b/py/path/testing/common.py @@ -120,6 +120,18 @@ class CommonPathTests(object): assert self.root.check(notrelto=l) assert not self.root.check(relto=l) + def test_bestrelpath(self): + curdir = self.root + sep = curdir.sep + s = curdir.bestrelpath(curdir.join("hello", "world")) + assert s == "hello" + sep + "world" + + s = curdir.bestrelpath(curdir.dirpath().join("sister")) + assert s == ".." + sep + "sister" + assert curdir.bestrelpath(curdir.dirpath()) == ".." + + assert curdir.bestrelpath("hello") == "hello" + def test_relto_not_relative(self): l1=self.root.join("bcde") l2=self.root.join("b") diff --git a/py/rest/testing/test_rst.py b/py/rest/testing/test_rst.py index 9ff404738..2dfbd5b8c 100644 --- a/py/rest/testing/test_rst.py +++ b/py/rest/testing/test_rst.py @@ -3,7 +3,7 @@ """ from py.__.rest.rst import * -from py.__.doc.conftest import restcheck +from py.__.misc.rest import process as restcheck import traceback tempdir = py.test.ensuretemp('rest') diff --git a/py/test/report/__init__.py b/py/test/attic/__init__.py similarity index 100% rename from py/test/report/__init__.py rename to py/test/attic/__init__.py diff --git a/py/test/report/rest.py b/py/test/attic/rest.py similarity index 100% rename from py/test/report/rest.py rename to py/test/attic/rest.py diff --git a/py/test/report/testing/__init__.py b/py/test/attic/testing/__init__.py similarity index 100% rename from py/test/report/testing/__init__.py rename to py/test/attic/testing/__init__.py diff --git a/py/test/report/testing/test_rest.py b/py/test/attic/testing/test_rest.py similarity index 100% rename from py/test/report/testing/test_rest.py rename to py/test/attic/testing/test_rest.py diff --git a/py/test/report/testing/test_web.py b/py/test/attic/testing/test_web.py similarity index 100% rename from py/test/report/testing/test_web.py rename to py/test/attic/testing/test_web.py diff --git a/py/test/report/testing/test_webjs.py b/py/test/attic/testing/test_webjs.py similarity index 100% rename from py/test/report/testing/test_webjs.py rename to py/test/attic/testing/test_webjs.py diff --git a/py/test/report/web.py b/py/test/attic/web.py similarity index 100% rename from py/test/report/web.py rename to py/test/attic/web.py diff --git a/py/test/report/webdata/__init__.py b/py/test/attic/webdata/__init__.py similarity index 100% rename from py/test/report/webdata/__init__.py rename to py/test/attic/webdata/__init__.py diff --git a/py/test/report/webdata/index.html b/py/test/attic/webdata/index.html similarity index 100% rename from py/test/report/webdata/index.html rename to py/test/attic/webdata/index.html diff --git a/py/test/report/webdata/json.py b/py/test/attic/webdata/json.py similarity index 100% rename from py/test/report/webdata/json.py rename to py/test/attic/webdata/json.py diff --git a/py/test/report/webdata/source.js b/py/test/attic/webdata/source.js similarity index 100% rename from py/test/report/webdata/source.js rename to py/test/attic/webdata/source.js diff --git a/py/test/report/webjs.py b/py/test/attic/webjs.py similarity index 100% rename from py/test/report/webjs.py rename to py/test/attic/webjs.py diff --git a/py/test/cmdline.py b/py/test/cmdline.py index a8799b1ed..f4e555f8c 100644 --- a/py/test/cmdline.py +++ b/py/test/cmdline.py @@ -9,9 +9,11 @@ def main(args=None): if args is None: args = py.std.sys.argv[1:] config = py.test.config - config.parse(args) + config.parse(args) + config.pytestplugins.configure(config) session = config.initsession() exitstatus = session.main() + config.pytestplugins.unconfigure(config) raise SystemExit(exitstatus) def warn_about_missing_assertion(): diff --git a/py/test/collect.py b/py/test/collect.py index 9edad0c3d..debe9a6f6 100644 --- a/py/test/collect.py +++ b/py/test/collect.py @@ -53,7 +53,6 @@ class SetupState(object): col = self.stack.pop() col.teardown() for col in needed_collectors[len(self.stack):]: - #print "setting up", col col.setup() self.stack.append(col) @@ -67,7 +66,7 @@ class ReprMetaInfo(object): params = self.__dict__.copy() if self.fspath: if basedir is not None: - params['fspath'] = getrelpath(basedir, self.fspath) + params['fspath'] = basedir.bestrelpath(self.fspath) if self.lineno is not None: params['lineno'] = self.lineno + 1 @@ -108,6 +107,7 @@ class Node(object): self._config = config self.fspath = getattr(parent, 'fspath', None) + # # note to myself: Pickling is uh. # @@ -172,15 +172,17 @@ class Node(object): setattr(self, attrname, res) return res - def listchain(self): - """ return list of all parent collectors up to self. """ + def listchain(self, rootfirst=False): + """ return list of all parent collectors up to self, + starting form root of collection tree. """ l = [self] while 1: x = l[-1] if x.parent is not None: l.append(x.parent) else: - l.reverse() + if not rootfirst: + l.reverse() return l def listnames(self): @@ -200,6 +202,53 @@ class Node(object): cur = next return cur + + def _getfsnode(self, path): + # this method is usually called from + # config.getfsnode() which returns a colitem + # from filename arguments + # + # pytest's collector tree does not neccessarily + # follow the filesystem and we thus need to do + # some special matching code here because + # _getitembynames() works by colitem names, not + # basenames. + if path == self.fspath: + return self + basenames = path.relto(self.fspath).split(path.sep) + cur = self + while basenames: + basename = basenames.pop(0) + assert basename + fspath = cur.fspath.join(basename) + colitems = cur._memocollect() + l = [] + for colitem in colitems: + if colitem.fspath == fspath or colitem.name == basename: + l.append(colitem) + if not l: + msg = ("Collector %r does not provide %r colitem " + "existing colitems are: %s" % + (cur, fspath, colitems)) + raise AssertionError(msg) + if basenames: + if len(l) > 1: + msg = ("Collector %r has more than one %r colitem " + "existing colitems are: %s" % + (cur, fspath, colitems)) + raise AssertionError(msg) + cur = l[0] + else: + if len(l) > 1: + cur = l + else: + cur = l[0] + break + return cur + + def readkeywords(self): + return dict([(x, True) for x in self._keywords()]) + def _keywords(self): return [self.name] @@ -284,7 +333,6 @@ class Node(object): return repr repr_failure = _repr_failure_py - shortfailurerepr = "F" class Collector(Node): @@ -298,7 +346,7 @@ class Collector(Node): """ Directory = configproperty('Directory') Module = configproperty('Module') - DoctestFile = configproperty('DoctestFile') + #DoctestFile = configproperty('DoctestFile') def collect(self): """ returns a list of children (items and collectors) @@ -407,41 +455,45 @@ class Directory(FSCollector): return l l = [] for path in self.fspath.listdir(sort=True): - res = self.consider(path, usefilters=True) + res = self.consider(path) if res is not None: - l.append(res) + if isinstance(res, (list, tuple)): + l.extend(res) + else: + l.append(res) return l - def consider(self, path, usefilters=True): + def consider(self, path): if path.check(file=1): - return self.consider_file(path, usefilters=usefilters) + return self.consider_file(path) elif path.check(dir=1): - return self.consider_dir(path, usefilters=usefilters) + return self.consider_dir(path) - def consider_file(self, path, usefilters=True): - ext = path.ext - pb = path.purebasename - if not usefilters or pb.startswith("test_") or pb.endswith("_test"): - if ext == ".py": - return self.Module(path, parent=self) - elif ext == ".txt": - return self.DoctestFile(path, parent=self) + def consider_file(self, path): + res = self._config.pytestplugins.call_each( + 'pytest_collect_file', path=path, parent=self) + l = [] + # throw out identical modules + for x in res: + if x not in l: + l.append(x) + return l - def consider_dir(self, path, usefilters=True): - if not usefilters or self.recfilter(path): - # not use self.Directory here as - # dir/conftest.py shall be able to - # define Directory(dir) already - Directory = self._config.getvalue('Directory', path) - return Directory(path, parent=self) - - def collect_by_name(self, name): - """ get a child with the given name. """ - res = super(Directory, self).collect_by_name(name) - if res is None: - p = self.fspath.join(name) - res = self.consider(p, usefilters=False) - return res + def consider_dir(self, path, usefilters=None): + if usefilters is not None: + APIWARN("0.99", "usefilters argument not needed") + if not self.recfilter(path): + # check if cmdline specified this dir or a subdir + for arg in self._config.args: + if path == arg or arg.relto(path): + break + else: + return + # not use self.Directory here as + # dir/conftest.py shall be able to + # define Directory(dir) already + Directory = self._config.getvalue('Directory', path) + return Directory(path, parent=self) from py.__.test.runner import basic_run_report, forked_run_report class Item(Node): @@ -479,27 +531,6 @@ class Item(Node): def runtest(self): """ execute this test item.""" - -def getrelpath(curdir, dest): - try: - base = curdir.common(dest) - if not base: # can be the case on windows - return dest - curdir2base = curdir.relto(base) - reldest = dest.relto(base) - if curdir2base: - n = curdir2base.count(curdir.sep) + 1 - else: - n = 0 - l = ['..'] * n - if reldest: - l.append(reldest) - target = dest.sep.join(l) - return target - except AttributeError: - return dest - - def warnoldcollect(): APIWARN("1.0", "implement collector.collect() instead of " diff --git a/py/test/config.py b/py/test/config.py index f00706eb6..7e61fd3ac 100644 --- a/py/test/config.py +++ b/py/test/config.py @@ -2,9 +2,9 @@ from __future__ import generators import py from conftesthandle import Conftest -from py.__.test.defaultconftest import adddefaultoptions -optparse = py.compat.optparse +from py.__.test import parseopt +from py.__.misc.warn import APIWARN # XXX move to Config class basetemp = None @@ -26,14 +26,26 @@ class CmdOptions(object): class Config(object): """ central bus for dealing with configuration/initialization data. """ - Option = optparse.Option + Option = py.compat.optparse.Option # deprecated _initialized = False - def __init__(self): + def __init__(self, pytestplugins=None): self.option = CmdOptions() - self._parser = optparse.OptionParser( - usage="usage: %prog [options] [query] [filenames of tests]") - self._conftest = Conftest() + self._parser = parseopt.Parser( + usage="usage: %prog [options] [file_or_dir] [file_or_dir] [...]", + processopt=self._processopt, + ) + if pytestplugins is None: + pytestplugins = py.test._PytestPlugins() + assert isinstance(pytestplugins, py.test._PytestPlugins) + self.bus = pytestplugins.pyplugins + self.pytestplugins = pytestplugins + self._conftest = Conftest(onimport=self.pytestplugins.consider_conftest) + + def _processopt(self, opt): + if hasattr(opt, 'default') and opt.dest: + if not hasattr(self.option, opt.dest): + setattr(self.option, opt.dest, opt.default) def parse(self, args): """ parse cmdline arguments into this config object. @@ -42,15 +54,14 @@ class Config(object): assert not self._initialized, ( "can only parse cmdline args at most once per Config object") self._initialized = True - adddefaultoptions(self) self._conftest.setinitial(args) - args = [str(x) for x in args] - cmdlineoption, args = self._parser.parse_args(args) - self.option.__dict__.update(vars(cmdlineoption)) + self.pytestplugins.consider_env() + self.pytestplugins.do_addoption(self._parser) + args = self._parser.parse_setoption(args, self.option) if not args: args.append(py.std.os.getcwd()) self.topdir = gettopdir(args) - self.args = args + self.args = [py.path.local(x) for x in args] # config objects are usually pickled across system # barriers but they contain filesystem paths. @@ -62,11 +73,15 @@ class Config(object): self._repr = repr def _initafterpickle(self, topdir): - self.__init__() + self.__init__( + #issue1 + #pytestplugins=py.test._PytestPlugins(py._com.pyplugins) + ) self._initialized = True self.topdir = py.path.local(topdir) self._mergerepr(self._repr) del self._repr + self.pytestplugins.configure(config=self) def _makerepr(self): l = [] @@ -87,6 +102,9 @@ class Config(object): self.option = cmdlineopts self._conftest.setinitial(self.args) + def getcolitems(self): + return [self.getfsnode(arg) for arg in self.args] + def getfsnode(self, path): path = py.path.local(path) assert path.check(), "%s: path does not exist" %(path,) @@ -96,8 +114,7 @@ class Config(object): pkgpath = path.check(file=1) and path.dirpath() or path Dir = self._conftest.rget("Directory", pkgpath) col = Dir(pkgpath, config=self) - names = path.relto(col.fspath).split(path.sep) - return col._getitembynames(names) + return col._getfsnode(path) def getvalue_pathlist(self, name, path=None): """ return a matching value, which needs to be sequence @@ -119,24 +136,14 @@ class Config(object): """ add a named group of options to the current testing session. This function gets invoked during testing session initialization. """ - for spec in specs: - for shortopt in spec._short_opts: - if not shortopt.isupper(): - raise ValueError( - "custom options must be capital letter " - "got %r" %(spec,) - ) - return self._addoptions(groupname, *specs) + APIWARN("1.0", "define plugins to add options", stacklevel=2) + group = self._parser.addgroup(groupname) + for opt in specs: + group._addoption_instance(opt) + return self.option - def _addoptions(self, groupname, *specs): - optgroup = optparse.OptionGroup(self._parser, groupname) - optgroup.add_options(specs) - self._parser.add_option_group(optgroup) - for opt in specs: - if hasattr(opt, 'default') and opt.dest: - if not hasattr(self.option, opt.dest): - setattr(self.option, opt.dest, opt.default) - return self.option + def addoption(self, *optnames, **attrs): + return self._parser.addoption(*optnames, **attrs) def getvalue(self, name, path=None): """ return 'name' value looked up from the 'options' @@ -150,30 +157,21 @@ class Config(object): except AttributeError: return self._conftest.rget(name, path) - def initreporter(self, bus): - if self.option.collectonly: - from py.__.test.report.collectonly import Reporter - else: - from py.__.test.report.terminal import Reporter - rep = Reporter(self, bus=bus) - return rep - def initsession(self): """ return an initialized session object. """ - cls = self._getsessionclass() + cls = self._getestdirclass() session = cls(self) session.fixoptions() - session.reporter = self.initreporter(session.bus) return session - def _getsessionclass(self): + def _getestdirclass(self): """ return Session class determined from cmdline options and looked up in initial config modules. """ if self.option.session is not None: return self._conftest.rget(self.option.session) else: - name = self._getsessionname() + name = self._getestdirname() try: return self._conftest.rget(name) except KeyError: @@ -182,7 +180,7 @@ class Config(object): mod = __import__(importpath, None, None, '__doc__') return getattr(mod, name) - def _getsessionname(self): + def _getestdirname(self): """ return default session name as determined from options. """ if self.option.collectonly: name = 'Session' @@ -221,42 +219,10 @@ class Config(object): raise ValueError("unknown io capturing: " + iocapture) - def gettracedir(self): - """ return a tracedirectory or None, depending on --tracedir. """ - if self.option.tracedir is not None: - return py.path.local(self.option.tracedir) - - def maketrace(self, name, flush=True): - """ return a tracedirectory or None, depending on --tracedir. """ - tracedir = self.gettracedir() - if tracedir is None: - return NullTracer() - tracedir.ensure(dir=1) - return Tracer(tracedir.join(name), flush=flush) - -class Tracer(object): - file = None - def __init__(self, path, flush=True): - self.file = path.open(mode='w') - self.flush = flush - - def __call__(self, *args): - time = round(py.std.time.time(), 3) - print >>self.file, time, " ".join(map(str, args)) - if self.flush: - self.file.flush() - - def close(self): - self.file.close() - -class NullTracer: - def __call__(self, *args): - pass - def close(self): - pass - # this is the one per-process instance of py.test configuration -config_per_process = Config() +config_per_process = Config( + pytestplugins=py.test._PytestPlugins(py._com.pyplugins) +) # default import paths for sessions diff --git a/py/test/conftesthandle.py b/py/test/conftesthandle.py index ecad6decb..4194e883b 100644 --- a/py/test/conftesthandle.py +++ b/py/test/conftesthandle.py @@ -9,8 +9,9 @@ class Conftest(object): conftest.py files may result in added cmdline options. XXX """ - def __init__(self, path=None): + def __init__(self, path=None, onimport=None): self._path2confmods = {} + self._onimport = onimport if path is not None: self.setinitial([path]) @@ -37,11 +38,11 @@ class Conftest(object): except KeyError: dp = path.dirpath() if dp == path: - return [importconfig(defaultconftestpath)] + return [self.importconftest(defaultconftestpath)] clist = self.getconftestmodules(dp) conftestpath = path.join("conftest.py") if conftestpath.check(file=1): - clist.append(importconfig(conftestpath)) + clist.append(self.importconftest(conftestpath)) self._path2confmods[path] = clist # be defensive: avoid changes from caller side to # affect us by always returning a copy of the actual list @@ -61,15 +62,17 @@ class Conftest(object): continue raise KeyError, name -def importconfig(configpath): - # We could have used caching here, but it's redundant since - # they're cached on path anyway, so we use it only when doing rget_path - assert configpath.check(), configpath - if not configpath.dirpath('__init__.py').check(file=1): - # HACK: we don't want any "globally" imported conftest.py, - # prone to conflicts and subtle problems - modname = str(configpath).replace('.', configpath.sep) - mod = configpath.pyimport(modname=modname) - else: - mod = configpath.pyimport() - return mod + def importconftest(self, conftestpath): + # Using caching here looks redundant since ultimately + # sys.modules caches already + assert conftestpath.check(), conftestpath + if not conftestpath.dirpath('__init__.py').check(file=1): + # HACK: we don't want any "globally" imported conftest.py, + # prone to conflicts and subtle problems + modname = str(conftestpath).replace('.', conftestpath.sep) + mod = conftestpath.pyimport(modname=modname) + else: + mod = conftestpath.pyimport() + if self._onimport: + self._onimport(mod) + return mod diff --git a/py/test/defaultconftest.py b/py/test/defaultconftest.py index 4cf7ae6fe..dca474e47 100644 --- a/py/test/defaultconftest.py +++ b/py/test/defaultconftest.py @@ -1,7 +1,7 @@ import py Module = py.test.collect.Module -DoctestFile = py.test.collect.DoctestFile +#DoctestFile = py.test.collect.DoctestFile Directory = py.test.collect.Directory Class = py.test.collect.Class Generator = py.test.collect.Generator @@ -10,13 +10,16 @@ Instance = py.test.collect.Instance conf_iocapture = "fd" # overridable from conftest.py +# XXX resultlog should go, pypy's nightrun depends on it +pytest_plugins = "default terminal xfail tmpdir resultlog monkeypatch".split() + # =================================================== # Distributed testing specific options #dist_hosts: needs to be provided by user #dist_rsync_roots: might be provided by user, if not present or None, # whole pkgdir will be rsynced -# XXX deprecated dist_remotepython = None + dist_taskspernode = 15 dist_boxed = False if hasattr(py.std.os, 'nice'): @@ -25,97 +28,3 @@ else: dist_nicelevel = 0 dist_rsync_ignore = [] -# =================================================== - -def adddefaultoptions(config): - Option = config.Option - config._addoptions('general options', - Option('-v', '--verbose', - action="count", dest="verbose", default=0, - help="increase verbosity."), - Option('-x', '--exitfirst', - action="store_true", dest="exitfirst", default=False, - help="exit instantly on first error or failed test."), - Option('-s', '--nocapture', - action="store_true", dest="nocapture", default=False, - help="disable catching of sys.stdout/stderr output."), - Option('-k', - action="store", dest="keyword", default='', - help="only run test items matching the given " - "space separated keywords. precede a keyword with '-' to negate. " - "Terminate the expression with ':' to treat a match as a signal to run all subsequent tests. " - ), - Option('-l', '--showlocals', - action="store_true", dest="showlocals", default=False, - help="show locals in tracebacks (disabled by default)."), - Option('--showskipsummary', - action="store_true", dest="showskipsummary", default=False, - help="always show summary of skipped tests"), - Option('', '--pdb', - action="store_true", dest="usepdb", default=False, - help="start pdb (the Python debugger) on errors."), - Option('', '--eventlog', - action="store", dest="eventlog", default=None, - help="write reporting events to given file."), - Option('', '--tracedir', - action="store", dest="tracedir", default=None, - help="write tracing information to the given directory."), - Option('', '--tb', - action="store", dest="tbstyle", default='long', - type="choice", choices=['long', 'short', 'no'], - help="traceback verboseness (long/short/no)."), - Option('', '--fulltrace', - action="store_true", dest="fulltrace", default=False, - help="don't cut any tracebacks (default is to cut)."), - Option('', '--nomagic', - action="store_true", dest="nomagic", default=False, - help="refrain from using magic as much as possible."), - Option('', '--collectonly', - action="store_true", dest="collectonly", default=False, - help="only collect tests, don't execute them."), - Option('', '--traceconfig', - action="store_true", dest="traceconfig", default=False, - help="trace considerations of conftest.py files."), - Option('-f', '--looponfailing', - action="store_true", dest="looponfailing", default=False, - help="loop on failing test set."), - Option('', '--exec', - action="store", dest="executable", default=None, - help="python executable to run the tests with."), - Option('-n', '--numprocesses', dest="numprocesses", default=0, - action="store", type="int", - help="number of local test processes."), - Option('', '--debug', - action="store_true", dest="debug", default=False, - help="turn on debugging information."), - ) - - config._addoptions('EXPERIMENTAL options', - Option('-d', '--dist', - action="store_true", dest="dist", default=False, - help="ad-hoc distribute tests across machines (requires conftest settings)"), - Option('-w', '--startserver', - action="store_true", dest="startserver", default=False, - help="starts local web server for displaying test progress.", - ), - Option('-r', '--runbrowser', - action="store_true", dest="runbrowser", default=False, - help="run browser (implies --startserver)." - ), - Option('', '--boxed', - action="store_true", dest="boxed", default=False, - help="box each test run in a separate process"), - Option('', '--rest', - action='store_true', dest="restreport", default=False, - help="restructured text output reporting."), - Option('', '--apigen', - action="store", dest="apigen", - help="generate api documentation while testing (requires " - "argument pointing to a script)."), - Option('', '--session', - action="store", dest="session", default=None, - help="lookup given sessioname in conftest.py files and use it."), - Option('--resultlog', action="store", - default=None, dest="resultlog", - help="path for machine-readable result log") - ) diff --git a/py/test/dsession/dsession.py b/py/test/dsession/dsession.py index fd8ac5624..95bf27979 100644 --- a/py/test/dsession/dsession.py +++ b/py/test/dsession/dsession.py @@ -8,11 +8,10 @@ import py from py.__.test import event import py.__.test.custompdb from py.__.test.dsession.hostmanage import HostManager -Item = (py.test.collect.Item, py.test.collect.Item) -Collector = (py.test.collect.Collector, py.test.collect.Collector) +Item = py.test.collect.Item +Collector = py.test.collect.Collector from py.__.test.runner import basic_run_report, basic_collect_report from py.__.test.session import Session -from py.__.test.runner import OutcomeRepr from py.__.test import outcome import Queue @@ -42,7 +41,6 @@ class DSession(Session): self.host2pending = {} self.item2host = {} self._testsfailed = False - self.trace = config.maketrace("dsession.log") def fixoptions(self): """ check, fix and determine conflicting options. """ @@ -79,11 +77,13 @@ class DSession(Session): def main(self, colitems=None): colitems = self.getinitialitems(colitems) - self.bus.notify(event.TestrunStart()) + #self.bus.notify(event.TestrunStart()) + self.sessionstarts() self.setup_hosts() exitstatus = self.loop(colitems) - self.bus.notify(event.TestrunFinish(exitstatus=exitstatus)) + #self.bus.notify(event.TestrunFinish(exitstatus=exitstatus)) self.teardown_hosts() + self.sessionfinishes() return exitstatus def loop_once(self, loopstate): @@ -96,28 +96,34 @@ class DSession(Session): # we use a timeout here so that control-C gets through while 1: try: - ev = self.queue.get(timeout=2.0) + eventcall = self.queue.get(timeout=2.0) break except Queue.Empty: continue loopstate.dowork = True - self.bus.notify(ev) - if isinstance(ev, event.ItemTestReport): + + eventname, args, kwargs = eventcall + self.bus.notify(eventname, *args, **kwargs) + if args: + ev, = args + else: + ev = None + if eventname == "itemtestreport": self.removeitem(ev.colitem) if ev.failed: loopstate.testsfailed = True - elif isinstance(ev, event.CollectionReport): + elif eventname == "collectionreport": if ev.passed: colitems.extend(ev.result) - elif isinstance(ev, event.HostUp): + elif eventname == "hostup": self.addhost(ev.host) - elif isinstance(ev, event.HostDown): + elif eventname == "hostdown": pending = self.removehost(ev.host) if pending: crashitem = pending.pop(0) self.handle_crashitem(crashitem, ev.host) colitems.extend(pending) - elif isinstance(ev, event.RescheduleItems): + elif eventname == "rescheduleitems": colitems.extend(ev.items) loopstate.dowork = False # avoid busywait @@ -132,9 +138,10 @@ class DSession(Session): def loop_once_shutdown(self, loopstate): # once we are in shutdown mode we dont send # events other than HostDown upstream - ev = self.queue.get() - if isinstance(ev, event.HostDown): - self.bus.notify(ev) + eventname, args, kwargs = self.queue.get() + if eventname == "hostdown": + ev, = args + self.bus.notify("hostdown", ev) self.removehost(ev.host) if not self.host2pending: # finished @@ -155,7 +162,7 @@ class DSession(Session): except KeyboardInterrupt: exitstatus = outcome.EXIT_INTERRUPTED except: - self.bus.notify(event.InternalException()) + self.bus.notify("internalerror", event.InternalException()) exitstatus = outcome.EXIT_INTERNALERROR if exitstatus == 0 and self._testsfailed: exitstatus = outcome.EXIT_TESTSFAILED @@ -173,7 +180,6 @@ class DSession(Session): pending = self.host2pending.pop(host) for item in pending: del self.item2host[item] - self.trace("removehost %s, pending=%r" %(host.hostid, pending)) return pending def triggertesting(self, colitems): @@ -183,11 +189,13 @@ class DSession(Session): if isinstance(next, Item): senditems.append(next) else: - ev = basic_collect_report(next) - self.bus.notify(event.CollectionStart(next)) - self.queue.put(ev) + self.bus.notify("collectionstart", event.CollectionStart(next)) + self.queueevent("collectionreport", basic_collect_report(next)) self.senditems(senditems) + def queueevent(self, eventname, *args, **kwargs): + self.queue.put((eventname, args, kwargs)) + def senditems(self, tosend): if not tosend: return @@ -196,38 +204,36 @@ class DSession(Session): if room > 0: sending = tosend[:room] host.node.sendlist(sending) - self.trace("sent to host %s: %r" %(host.hostid, sending)) for item in sending: #assert item not in self.item2host, ( # "sending same item %r to multiple hosts " # "not implemented" %(item,)) self.item2host[item] = host - self.bus.notify(event.ItemStart(item, host)) + self.bus.notify("itemstart", event.ItemStart(item, host)) pending.extend(sending) tosend[:] = tosend[room:] # update inplace if not tosend: break if tosend: # we have some left, give it to the main loop - self.queue.put(event.RescheduleItems(tosend)) + self.queueevent("rescheduleitems", event.RescheduleItems(tosend)) def removeitem(self, item): if item not in self.item2host: raise AssertionError(item, self.item2host) host = self.item2host.pop(item) self.host2pending[host].remove(item) - self.trace("removed %r, host=%r" %(item,host.hostid)) + #self.config.bus.notify("removeitem", item, host.hostid) def handle_crashitem(self, item, host): - longrepr = "%r CRASHED THE HOST %r" %(item, host) - outcome = OutcomeRepr(when="execute", shortrepr="c", longrepr=longrepr) - rep = event.ItemTestReport(item, failed=outcome) - self.bus.notify(rep) + longrepr = "!!! Host %r crashed during running of test %r" %(host, item) + rep = event.ItemTestReport(item, when="???", excinfo=longrepr) + self.bus.notify("itemtestreport", rep) def setup_hosts(self): """ setup any neccessary resources ahead of the test run. """ self.hm = HostManager(self) - self.hm.setup_hosts(notify=self.queue.put) + self.hm.setup_hosts(putevent=self.queue.put) def teardown_hosts(self): """ teardown any resources after a test run. """ diff --git a/py/test/dsession/hostmanage.py b/py/test/dsession/hostmanage.py index 319ccc7e8..35087ec8c 100644 --- a/py/test/dsession/hostmanage.py +++ b/py/test/dsession/hostmanage.py @@ -148,7 +148,7 @@ class HostManager(object): def prepare_gateways(self): for host in self.hosts: host.initgateway() - self.session.bus.notify(event.HostGatewayReady(host, self.roots)) + self.session.bus.notify("hostgatewayready", event.HostGatewayReady(host, self.roots)) def init_rsync(self): self.prepare_gateways() @@ -164,16 +164,14 @@ class HostManager(object): for host in self.hosts: rsync.add_target_host(host, destrelpath) rsync.send(raises=False) - self.session.bus.notify(event.RsyncFinished()) + self.session.bus.notify("rsyncfinished", event.RsyncFinished()) - def setup_hosts(self, notify=None): - if notify is None: - notify = self.session.bus.notify + def setup_hosts(self, putevent): self.init_rsync() for host in self.hosts: host.node = MasterNode(host, self.session.config, - notify) + putevent) # # helpers diff --git a/py/test/dsession/masterslave.py b/py/test/dsession/masterslave.py index 099a4552a..ee4e8e8d9 100644 --- a/py/test/dsession/masterslave.py +++ b/py/test/dsession/masterslave.py @@ -8,16 +8,19 @@ from py.__.test.dsession.mypickle import PickleChannel class MasterNode(object): ENDMARK = -1 - def __init__(self, host, config, notify): + def __init__(self, host, config, putevent): self.host = host self.config = config - self.notify = notify + self.putevent = putevent self.channel = install_slave(host, config) self.channel.setcallback(self.callback, endmarker=self.ENDMARK) self._down = False + + def notify(self, eventname, *args, **kwargs): + self.putevent((eventname, args, kwargs)) - def callback(self, ev): - """ this gets called for each item we receive from + def callback(self, eventcall): + """ this gets called for each object we receive from the other side and if the channel closes. Note that the callback runs in the receiver @@ -25,24 +28,27 @@ class MasterNode(object): avoid raising exceptions or doing heavy work. """ try: - if ev == self.ENDMARK: + if eventcall == self.ENDMARK: err = self.channel._getremoteerror() if not self._down: if not err: err = "TERMINATED" - self.notify(event.HostDown(self.host, err)) + self.notify("hostdown", event.HostDown(self.host, err)) return - if ev is None: + elif eventcall is None: self._down = True - self.notify(event.HostDown(self.host, None)) + self.notify("hostdown", event.HostDown(self.host, None)) return except KeyboardInterrupt: raise except: excinfo = py.code.ExceptionInfo() print "!" * 20, excinfo - ev = event.InternalException(excinfo) - self.notify(ev) + self.notify("internalerror", event.InternalException(excinfo)) + else: + # XXX we need to have the proper event name + eventname, args, kwargs = eventcall + self.notify(eventname, *args, **kwargs) def send(self, item): assert item is not None @@ -101,24 +107,22 @@ class SlaveNode(object): def __repr__(self): host = getattr(self, 'host', '') - return "<%s host=%s>" %(self.__class__.__name__, host.hostid) + return "<%s host=%s>" %(self.__class__.__name__, host) + + def sendevent(self, eventname, *args, **kwargs): + self.channel.send((eventname, args, kwargs)) def run(self): from py.__.test.dsession.hostmanage import makehostup channel = self.channel self.host = host = channel.receive() - channel.send(makehostup(host)) - self.trace = self.config.maketrace(host.hostid) - self.trace("initialized") - + self.sendevent("hostup", makehostup(host)) try: while 1: task = channel.receive() - self.trace("received", task) - + self.config.bus.notify("masterslave_receivedtask", task) if task is None: # shutdown - channel.send(None) - self.trace("shutting down, send None to", channel) + self.channel.send(None) break if isinstance(task, list): for item in task: @@ -128,15 +132,10 @@ class SlaveNode(object): except KeyboardInterrupt: raise except: - rep = event.InternalException() - self.trace("sending back internal exception report, breaking loop") - channel.send(rep) + self.sendevent("internalerror", event.InternalException()) raise - else: - self.trace("normal shutdown") def runtest(self, item): runner = item._getrunner() testrep = runner(item) - self.channel.send(testrep) - self.trace("sent back testreport", testrep) + self.sendevent("itemtestreport", testrep) diff --git a/py/test/dsession/testing/test_dsession.py b/py/test/dsession/testing/test_dsession.py index ca9e9239c..edf8df677 100644 --- a/py/test/dsession/testing/test_dsession.py +++ b/py/test/dsession/testing/test_dsession.py @@ -1,4 +1,3 @@ -from py.__.test.testing.suptest import InlineCollection from py.__.test.dsession.dsession import DSession, LoopState from py.__.test.dsession.hostmanage import Host, makehostup from py.__.test.runner import basic_collect_report @@ -24,17 +23,17 @@ def dumpqueue(queue): while queue.qsize(): print queue.get() -class TestDSession(InlineCollection): - def test_fixoptions(self): - config = self.parseconfig("--exec=xxx") +class TestDSession: + def test_fixoptions(self, testdir): + config = testdir.parseconfig("--exec=xxx") config.initsession().fixoptions() assert config.option.numprocesses == 1 - config = self.parseconfig("--exec=xxx", '-n3') + config = testdir.parseconfig("--exec=xxx", '-n3') config.initsession().fixoptions() assert config.option.numprocesses == 3 - def test_add_remove_host(self): - item = self.getitem("def test_func(): pass") + def test_add_remove_host(self, testdir): + item = testdir.getitem("def test_func(): pass") rep = run(item) session = DSession(item._config) host = Host("localhost") @@ -48,8 +47,8 @@ class TestDSession(InlineCollection): assert item not in session.item2host py.test.raises(Exception, "session.removehost(host)") - def test_senditems_removeitems(self): - item = self.getitem("def test_func(): pass") + def test_senditems_removeitems(self, testdir): + item = testdir.getitem("def test_func(): pass") rep = run(item) session = DSession(item._config) host = Host("localhost") @@ -62,19 +61,20 @@ class TestDSession(InlineCollection): assert not session.host2pending[host] assert not session.item2host - def test_triggertesting_collect(self): - modcol = self.getmodulecol(""" + def test_triggertesting_collect(self, testdir): + modcol = testdir.getmodulecol(""" def test_func(): pass """) session = DSession(modcol._config) session.triggertesting([modcol]) - rep = session.queue.get(block=False) - assert isinstance(rep, event.CollectionReport) + name, args, kwargs = session.queue.get(block=False) + assert name == 'collectionreport' + rep, = args assert len(rep.result) == 1 - def test_triggertesting_item(self): - item = self.getitem("def test_func(): pass") + def test_triggertesting_item(self, testdir): + item = testdir.getitem("def test_func(): pass") session = DSession(item._config) host1 = Host("localhost") host1.node = MockNode() @@ -89,51 +89,52 @@ class TestDSession(InlineCollection): assert host2_sent == [item] * session.MAXITEMSPERHOST assert session.host2pending[host1] == host1_sent assert session.host2pending[host2] == host2_sent - ev = session.queue.get(block=False) - assert isinstance(ev, event.RescheduleItems) + name, args, kwargs = session.queue.get(block=False) + assert name == "rescheduleitems" + ev, = args assert ev.items == [item] - def test_keyboardinterrupt(self): - item = self.getitem("def test_func(): pass") + def test_keyboardinterrupt(self, testdir): + item = testdir.getitem("def test_func(): pass") session = DSession(item._config) def raise_(timeout=None): raise KeyboardInterrupt() session.queue.get = raise_ exitstatus = session.loop([]) assert exitstatus == outcome.EXIT_INTERRUPTED - def test_internalerror(self): - item = self.getitem("def test_func(): pass") + def test_internalerror(self, testdir): + item = testdir.getitem("def test_func(): pass") session = DSession(item._config) def raise_(): raise ValueError() session.queue.get = raise_ exitstatus = session.loop([]) assert exitstatus == outcome.EXIT_INTERNALERROR - def test_rescheduleevent(self): - item = self.getitem("def test_func(): pass") + def test_rescheduleevent(self, testdir): + item = testdir.getitem("def test_func(): pass") session = DSession(item._config) host1 = Host("localhost") host1.node = MockNode() session.addhost(host1) ev = event.RescheduleItems([item]) loopstate = LoopState([]) - session.queue.put(ev) + session.queueevent("rescheduleitems", ev) session.loop_once(loopstate) # check that RescheduleEvents are not immediately # rescheduled if there are no hosts assert loopstate.dowork == False - session.queue.put(event.NOP()) + session.queueevent("anonymous", event.NOP()) session.loop_once(loopstate) - session.queue.put(event.NOP()) + session.queueevent("anonymous", event.NOP()) session.loop_once(loopstate) assert host1.node.sent == [[item]] - session.queue.put(run(item)) + session.queueevent("itemtestreport", run(item)) session.loop_once(loopstate) assert loopstate.shuttingdown assert not loopstate.testsfailed - def test_no_hosts_remaining_for_tests(self): - item = self.getitem("def test_func(): pass") + def test_no_hosts_remaining_for_tests(self, testdir): + item = testdir.getitem("def test_func(): pass") # setup a session with one host session = DSession(item._config) host1 = Host("localhost") @@ -142,7 +143,7 @@ class TestDSession(InlineCollection): # setup a HostDown event ev = event.HostDown(host1, None) - session.queue.put(ev) + session.queueevent("hostdown", ev) loopstate = LoopState([item]) loopstate.dowork = False @@ -150,8 +151,8 @@ class TestDSession(InlineCollection): dumpqueue(session.queue) assert loopstate.exitstatus == outcome.EXIT_NOHOSTS - def test_hostdown_causes_reschedule_pending(self): - modcol = self.getmodulecol(""" + def test_hostdown_causes_reschedule_pending(self, testdir, EventRecorder): + modcol = testdir.getmodulecol(""" def test_crash(): assert 0 def test_fail(): @@ -172,41 +173,39 @@ class TestDSession(InlineCollection): session.senditems([item1, item2]) host = session.item2host[item1] ev = event.HostDown(host, None) - session.queue.put(ev) - - events = [] ; session.bus.subscribe(events.append) + session.queueevent("hostdown", ev) + evrec = EventRecorder(session.bus) loopstate = LoopState([]) session.loop_once(loopstate) assert loopstate.colitems == [item2] # do not reschedule crash item - testrep = [x for x in events if isinstance(x, event.ItemTestReport)][0] + testrep = evrec.getfirstnamed("itemtestreport") assert testrep.failed assert testrep.colitem == item1 - assert str(testrep.outcome.longrepr).find("CRASHED") != -1 - assert str(testrep.outcome.longrepr).find(host.hostname) != -1 + assert str(testrep.longrepr).find("crashed") != -1 + assert str(testrep.longrepr).find(host.hostname) != -1 - def test_hostup_adds_to_available(self): - item = self.getitem("def test_func(): pass") + def test_hostup_adds_to_available(self, testdir): + item = testdir.getitem("def test_func(): pass") # setup a session with two hosts session = DSession(item._config) host1 = Host("localhost") hostup = makehostup(host1) - session.queue.put(hostup) + session.queueevent("hostup", hostup) loopstate = LoopState([item]) loopstate.dowork = False assert len(session.host2pending) == 0 session.loop_once(loopstate) assert len(session.host2pending) == 1 - def test_event_propagation(self): - item = self.getitem("def test_func(): pass") + def test_event_propagation(self, testdir, EventRecorder): + item = testdir.getitem("def test_func(): pass") session = DSession(item._config) - ev = event.NOP() - events = [] ; session.bus.subscribe(events.append) - session.queue.put(ev) + evrec = EventRecorder(session.bus) + session.queueevent("NOPevent", 42) session.loop_once(LoopState([])) - assert events[0] == ev + assert evrec.getfirstnamed('NOPevent') def runthrough(self, item): session = DSession(item._config) @@ -215,31 +214,31 @@ class TestDSession(InlineCollection): session.addhost(host1) loopstate = LoopState([item]) - session.queue.put(event.NOP()) + session.queueevent("NOP") session.loop_once(loopstate) assert host1.node.sent == [[item]] ev = run(item) - session.queue.put(ev) + session.queueevent("itemtestreport", ev) session.loop_once(loopstate) assert loopstate.shuttingdown - session.queue.put(event.HostDown(host1, None)) + session.queueevent("hostdown", event.HostDown(host1, None)) session.loop_once(loopstate) dumpqueue(session.queue) return session, loopstate.exitstatus - def test_exit_completed_tests_ok(self): - item = self.getitem("def test_func(): pass") + def test_exit_completed_tests_ok(self, testdir): + item = testdir.getitem("def test_func(): pass") session, exitstatus = self.runthrough(item) assert exitstatus == outcome.EXIT_OK - def test_exit_completed_tests_fail(self): - item = self.getitem("def test_func(): 0/0") + def test_exit_completed_tests_fail(self, testdir): + item = testdir.getitem("def test_func(): 0/0") session, exitstatus = self.runthrough(item) assert exitstatus == outcome.EXIT_TESTSFAILED - def test_exit_on_first_failing(self): - modcol = self.getmodulecol(""" + def test_exit_on_first_failing(self, testdir): + modcol = testdir.getmodulecol(""" def test_fail(): assert 0 def test_pass(): @@ -258,33 +257,32 @@ class TestDSession(InlineCollection): # run tests ourselves and produce reports ev1 = run(items[0]) ev2 = run(items[1]) - session.queue.put(ev1) # a failing one - session.queue.put(ev2) + session.queueevent("itemtestreport", ev1) # a failing one + session.queueevent("itemtestreport", ev2) # now call the loop loopstate = LoopState(items) session.loop_once(loopstate) assert loopstate.testsfailed assert loopstate.shuttingdown - def test_shuttingdown_filters_events(self): - item = self.getitem("def test_func(): pass") + def test_shuttingdown_filters_events(self, testdir, EventRecorder): + item = testdir.getitem("def test_func(): pass") session = DSession(item._config) host = Host("localhost") session.addhost(host) loopstate = LoopState([]) loopstate.shuttingdown = True - l = [] - session.bus.subscribe(l.append) - session.queue.put(run(item)) + evrec = EventRecorder(session.bus) + session.queueevent("itemtestreport", run(item)) session.loop_once(loopstate) - assert not l + assert not evrec.getfirstnamed("hostdown") ev = event.HostDown(host) - session.queue.put(ev) + session.queueevent("hostdown", ev) session.loop_once(loopstate) - assert l == [ev] + assert evrec.getfirstnamed('hostdown') == ev - def test_filteritems(self): - modcol = self.getmodulecol(""" + def test_filteritems(self, testdir, EventRecorder): + modcol = testdir.getmodulecol(""" def test_fail(): assert 0 def test_pass(): @@ -296,42 +294,42 @@ class TestDSession(InlineCollection): dsel = session.filteritems([modcol]) assert dsel == [modcol] items = modcol.collect() - events = [] ; session.bus.subscribe(events.append) + evrec = EventRecorder(session.bus) remaining = session.filteritems(items) assert remaining == [] - ev = events[-1] - assert isinstance(ev, event.Deselected) + evname, ev = evrec.events[-1] + assert evname == "deselected" assert ev.items == items modcol._config.option.keyword = "test_fail" remaining = session.filteritems(items) assert remaining == [items[0]] - ev = events[-1] - assert isinstance(ev, event.Deselected) + evname, ev = evrec.events[-1] + assert evname == "deselected" assert ev.items == [items[1]] - def test_hostdown_shutdown_after_completion(self): - item = self.getitem("def test_func(): pass") + def test_hostdown_shutdown_after_completion(self, testdir): + item = testdir.getitem("def test_func(): pass") session = DSession(item._config) host = Host("localhost") host.node = MockNode() session.addhost(host) session.senditems([item]) - session.queue.put(run(item)) + session.queueevent("itemtestreport", run(item)) loopstate = LoopState([]) session.loop_once(loopstate) assert host.node._shutdown is True assert loopstate.exitstatus is None, "loop did not wait for hostdown" assert loopstate.shuttingdown - session.queue.put(event.HostDown(host, None)) + session.queueevent("hostdown", event.HostDown(host, None)) session.loop_once(loopstate) assert loopstate.exitstatus == 0 - def test_nopending_but_collection_remains(self): - modcol = self.getmodulecol(""" + def test_nopending_but_collection_remains(self, testdir): + modcol = testdir.getmodulecol(""" def test_fail(): assert 0 def test_pass(): @@ -347,10 +345,10 @@ class TestDSession(InlineCollection): session.senditems([item1]) # host2pending will become empty when the loop sees # the report - session.queue.put(run(item1)) + session.queueevent("itemtestreport", run(item1)) # but we have a collection pending - session.queue.put(colreport) + session.queueevent("collectionreport", colreport) loopstate = LoopState([]) session.loop_once(loopstate) diff --git a/py/test/dsession/testing/test_functional_dsession.py b/py/test/dsession/testing/test_functional_dsession.py index 09d05f6b8..8bdc00902 100644 --- a/py/test/dsession/testing/test_functional_dsession.py +++ b/py/test/dsession/testing/test_functional_dsession.py @@ -3,74 +3,48 @@ """ import py -from py.__.test import event from py.__.test.dsession.dsession import DSession from py.__.test.dsession.hostmanage import HostManager, Host -from py.__.test.testing import suptest +from test_masterslave import EventQueue + import os -def eventreader(session): - queue = py.std.Queue.Queue() - session.bus.subscribe(queue.put) - def readevent(eventtype=event.ItemTestReport, timeout=2.0): - events = [] - while 1: - try: - ev = queue.get(timeout=timeout) - except py.std.Queue.Empty: - print "seen events", events - raise IOError("did not see %r events" % (eventtype)) - else: - if isinstance(ev, eventtype): - #print "other events seen", events - return ev - events.append(ev) - return readevent -class TestAsyncFunctional(suptest.InlineCollection): - def test_dist_no_disthost(self): - config = self.parseconfig(self.tmpdir, '-d') +class TestAsyncFunctional: + def test_dist_no_disthost(self, testdir): + config = testdir.parseconfig(testdir.tmpdir, '-d') py.test.raises(SystemExit, "config.initsession()") - def test_session_eventlog_dist(self): - self.makepyfile(conftest="dist_hosts=['localhost']\n") - eventlog = self.tmpdir.join("mylog") - config = self.parseconfig(self.tmpdir, '-d', '--eventlog=%s' % eventlog) - session = config.initsession() - session.bus.notify(event.TestrunStart()) - s = eventlog.read() - assert s.find("TestrunStart") != -1 - - def test_conftest_options(self): - self.makepyfile(conftest=""" + def test_conftest_options(self, testdir): + testdir.makepyfile(conftest=""" print "importing conftest" import py Option = py.test.config.Option option = py.test.config.addoptions("someopt", - Option('', '--forcegen', action="store_true", dest="forcegen", default=False)) - """) - self.makepyfile(__init__="#") - p1 = self.makepyfile(test_one=""" + Option('--someopt', action="store_true", dest="someopt", default=False)) + """, + ) + p1 = testdir.makepyfile(""" def test_1(): import py, conftest - print "test_1: py.test.config.option.forcegen", py.test.config.option.forcegen + print "test_1: py.test.config.option.someopt", py.test.config.option.someopt print "test_1: conftest", conftest - print "test_1: conftest.option.forcegen", conftest.option.forcegen - assert conftest.option.forcegen - """) + print "test_1: conftest.option.someopt", conftest.option.someopt + assert conftest.option.someopt + """, __init__="#") print p1 - config = self.parseconfig('-n1', p1, '--forcegen') + config = py.test.config._reparse(['-n1', p1, '--someopt']) dsession = DSession(config) - readevent = eventreader(dsession) + eq = EventQueue(config.bus) dsession.main() - ev = readevent(event.ItemTestReport) + ev, = eq.geteventargs("itemtestreport") if not ev.passed: - print ev.outcome.longrepr + print ev assert 0 - def test_dist_some_tests(self): - self.makepyfile(conftest="dist_hosts=['localhost']\n") - p1 = self.makepyfile(test_one=""" + def test_dist_some_tests(self, testdir): + testdir.makepyfile(conftest="dist_hosts=['localhost']\n") + p1 = testdir.makepyfile(test_one=""" def test_1(): pass def test_x(): @@ -79,22 +53,22 @@ class TestAsyncFunctional(suptest.InlineCollection): def test_fail(): assert 0 """) - config = self.parseconfig('-d', p1) + config = testdir.parseconfig('-d', p1) dsession = DSession(config) - readevent = eventreader(dsession) + eq = EventQueue(config.bus) dsession.main([config.getfsnode(p1)]) - ev = readevent(event.ItemTestReport) + ev, = eq.geteventargs("itemtestreport") assert ev.passed - ev = readevent(event.ItemTestReport) + ev, = eq.geteventargs("itemtestreport") assert ev.skipped - ev = readevent(event.ItemTestReport) + ev, = eq.geteventargs("itemtestreport") assert ev.failed # see that the host is really down - ev = readevent(event.HostDown) + ev, = eq.geteventargs("hostdown") assert ev.host.hostname == "localhost" - ev = readevent(event.TestrunFinish) + ev, = eq.geteventargs("testrunfinish") - def test_distribution_rsync_roots_example(self): + def test_distribution_rsync_roots_example(self, testdir): py.test.skip("testing for root rsync needs rework") destdir = py.test.ensuretemp("example_dist_destdir") subdir = "sub_example_dist" @@ -124,28 +98,26 @@ class TestAsyncFunctional(suptest.InlineCollection): assert config.topdir == tmpdir assert not tmpdir.join("__init__.py").check() dist = DSession(config) - sorter = suptest.events_from_session(dist) - testevents = sorter.get(event.ItemTestReport) + sorter = testdir.inline_runsession(dist) + testevents = sorter.getnamed('itemtestreport') assert len([x for x in testevents if x.passed]) == 2 assert len([x for x in testevents if x.failed]) == 3 assert len([x for x in testevents if x.skipped]) == 0 - def test_nice_level(self): + def test_nice_level(self, testdir): """ Tests if nice level behaviour is ok """ if not hasattr(os, 'nice'): py.test.skip("no os.nice() available") - self.makepyfile(conftest=""" + testdir.makepyfile(conftest=""" dist_hosts=['localhost'] dist_nicelevel = 10 """) - p1 = self.makepyfile(test_nice=""" + p1 = testdir.makepyfile(""" def test_nice(): import os assert os.nice(0) == 10 """) - config = self.parseconfig('-d', p1) - session = config.initsession() - events = suptest.events_from_session(session) - ev = events.getreport('test_nice') + evrec = testdir.inline_run('-d', p1) + ev = evrec.getreport('test_nice') assert ev.passed diff --git a/py/test/dsession/testing/test_hostmanage.py b/py/test/dsession/testing/test_hostmanage.py index b3636935a..2025587cc 100644 --- a/py/test/dsession/testing/test_hostmanage.py +++ b/py/test/dsession/testing/test_hostmanage.py @@ -3,23 +3,15 @@ """ import py -from py.__.test.testing import suptest from py.__.test.dsession.hostmanage import HostRSync, Host, HostManager, gethosts from py.__.test.dsession.hostmanage import sethomedir, gethomedir, getpath_relto_home from py.__.test import event -class TmpWithSourceDest(suptest.FileCreation): - def setup_method(self, method): - super(TmpWithSourceDest, self).setup_method(method) - self.source = self.tmpdir.mkdir("source") - self.dest = self.tmpdir.mkdir("dest") - -class TestHost(suptest.FileCreation): - def _gethostinfo(self, relpath=""): - exampledir = self.tmpdir.join("gethostinfo") +class TestHost: + def _gethostinfo(self, testdir, relpath=""): + exampledir = testdir.mkdir("gethostinfo") if relpath: exampledir = exampledir.join(relpath) - assert not exampledir.check() hostinfo = Host("localhost:%s" % exampledir) return hostinfo @@ -62,8 +54,8 @@ class TestHost(suptest.FileCreation): py.test.raises((py.process.cmdexec.Error, IOError, EOFError), host.initgateway) - def test_remote_has_homedir_as_currentdir(self): - host = self._gethostinfo() + def test_remote_has_homedir_as_currentdir(self, testdir): + host = self._gethostinfo(testdir) old = py.path.local.get_temproot().chdir() try: host.initgateway() @@ -104,10 +96,10 @@ class TestHost(suptest.FileCreation): assert l[0] == host.python def test_initgateway_ssh_and_remotepath(self): - from py.__.conftest import option - if not option.sshtarget: + hostspec = py.test.config.option.sshhost + if not hostspec: py.test.skip("no known ssh target, use -S to set one") - host = Host("%s" % (option.sshtarget, )) + host = Host("%s" % (hostspec)) # this test should be careful to not write/rsync anything # as the remotepath is the default location # and may be used in the real world @@ -125,18 +117,23 @@ class TestHost(suptest.FileCreation): res = channel.receive() assert res == host.gw_remotepath -class TestSyncing(TmpWithSourceDest): - def _gethostinfo(self): - hostinfo = Host("localhost:%s" % self.dest) +def pytest_pyfuncarg_source(pyfuncitem): + return py.test.ensuretemp(pyfuncitem.getmodpath()).mkdir("source") +def pytest_pyfuncarg_dest(pyfuncitem): + return py.test.ensuretemp(pyfuncitem.getmodpath()).mkdir("dest") + +class TestSyncing: + def _gethostinfo(self, dest): + hostinfo = Host("localhost:%s" % dest) return hostinfo - def test_hrsync_filter(self): - self.source.ensure("dir", "file.txt") - self.source.ensure(".svn", "entries") - self.source.ensure(".somedotfile", "moreentries") - self.source.ensure("somedir", "editfile~") - syncer = HostRSync(self.source) - l = list(self.source.visit(rec=syncer.filter, + def test_hrsync_filter(self, source, dest): + source.ensure("dir", "file.txt") + source.ensure(".svn", "entries") + source.ensure(".somedotfile", "moreentries") + source.ensure("somedir", "editfile~") + syncer = HostRSync(source) + l = list(source.visit(rec=syncer.filter, fil=syncer.filter)) assert len(l) == 3 basenames = [x.basename for x in l] @@ -144,10 +141,10 @@ class TestSyncing(TmpWithSourceDest): assert 'file.txt' in basenames assert 'somedir' in basenames - def test_hrsync_localhost_inplace(self): + def test_hrsync_localhost_inplace(self, source, dest): h1 = Host("localhost") events = [] - rsync = HostRSync(self.source) + rsync = HostRSync(source) h1.initgateway() rsync.add_target_host(h1, notify=events.append) assert events @@ -161,24 +158,24 @@ class TestSyncing(TmpWithSourceDest): if isinstance(x, event.HostRSyncRootReady)] assert len(l) == 1 ev = l[0] - assert ev.root == self.source + assert ev.root == source assert ev.host == h1 - def test_hrsync_one_host(self): - h1 = self._gethostinfo() + def test_hrsync_one_host(self, source, dest): + h1 = self._gethostinfo(dest) finished = [] - rsync = HostRSync(self.source) + rsync = HostRSync(source) h1.initgateway() rsync.add_target_host(h1) - self.source.join("hello.py").write("world") + source.join("hello.py").write("world") rsync.send() - assert self.dest.join("hello.py").check() + assert dest.join("hello.py").check() - def test_hrsync_same_host_twice(self): - h1 = self._gethostinfo() - h2 = self._gethostinfo() + def test_hrsync_same_host_twice(self, source, dest): + h1 = self._gethostinfo(dest) + h2 = self._gethostinfo(dest) finished = [] - rsync = HostRSync(self.source) + rsync = HostRSync(source) l = [] h1.initgateway() h2.initgateway() @@ -187,87 +184,87 @@ class TestSyncing(TmpWithSourceDest): res2 = rsync.add_target_host(h2) assert not res2 -class TestHostManager(TmpWithSourceDest): - def gethostmanager(self, dist_hosts, dist_rsync_roots=None): +class TestHostManager: + def gethostmanager(self, source, dist_hosts, dist_rsync_roots=None): l = ["dist_hosts = %r" % dist_hosts] if dist_rsync_roots: l.append("dist_rsync_roots = %r" % dist_rsync_roots) - self.source.join("conftest.py").write("\n".join(l)) - config = py.test.config._reparse([self.source]) - assert config.topdir == self.source + source.join("conftest.py").write("\n".join(l)) + config = py.test.config._reparse([source]) + assert config.topdir == source session = config.initsession() hm = HostManager(session) assert hm.hosts return hm - def test_hostmanager_custom_hosts(self): - session = py.test.config._reparse([self.source]).initsession() + def test_hostmanager_custom_hosts(self, source, dest): + session = py.test.config._reparse([source]).initsession() hm = HostManager(session, hosts=[1,2,3]) assert hm.hosts == [1,2,3] - def test_hostmanager_init_rsync_topdir(self): - dir2 = self.source.ensure("dir1", "dir2", dir=1) + def test_hostmanager_init_rsync_topdir(self, source, dest): + dir2 = source.ensure("dir1", "dir2", dir=1) dir2.ensure("hello") - hm = self.gethostmanager( - dist_hosts = ["localhost:%s" % self.dest] + hm = self.gethostmanager(source, + dist_hosts = ["localhost:%s" % dest] ) - assert hm.session.config.topdir == self.source + assert hm.session.config.topdir == source hm.init_rsync() - dest = self.dest.join(self.source.basename) + dest = dest.join(source.basename) assert dest.join("dir1").check() assert dest.join("dir1", "dir2").check() assert dest.join("dir1", "dir2", 'hello').check() - def test_hostmanager_init_rsync_topdir_explicit(self): - dir2 = self.source.ensure("dir1", "dir2", dir=1) + def test_hostmanager_init_rsync_topdir_explicit(self, source, dest): + dir2 = source.ensure("dir1", "dir2", dir=1) dir2.ensure("hello") - hm = self.gethostmanager( - dist_hosts = ["localhost:%s" % self.dest], - dist_rsync_roots = [str(self.source)] + hm = self.gethostmanager(source, + dist_hosts = ["localhost:%s" % dest], + dist_rsync_roots = [str(source)] ) - assert hm.session.config.topdir == self.source + assert hm.session.config.topdir == source hm.init_rsync() - dest = self.dest.join(self.source.basename) + dest = dest.join(source.basename) assert dest.join("dir1").check() assert dest.join("dir1", "dir2").check() assert dest.join("dir1", "dir2", 'hello').check() - def test_hostmanager_init_rsync_roots(self): - dir2 = self.source.ensure("dir1", "dir2", dir=1) - self.source.ensure("dir1", "somefile", dir=1) + def test_hostmanager_init_rsync_roots(self, source, dest): + dir2 = source.ensure("dir1", "dir2", dir=1) + source.ensure("dir1", "somefile", dir=1) dir2.ensure("hello") - self.source.ensure("bogusdir", "file") - self.source.join("conftest.py").write(py.code.Source(""" + source.ensure("bogusdir", "file") + source.join("conftest.py").write(py.code.Source(""" dist_rsync_roots = ['dir1/dir2'] """)) - session = py.test.config._reparse([self.source]).initsession() + session = py.test.config._reparse([source]).initsession() hm = HostManager(session, - hosts=[Host("localhost:" + str(self.dest))]) + hosts=[Host("localhost:" + str(dest))]) hm.init_rsync() - assert self.dest.join("dir2").check() - assert not self.dest.join("dir1").check() - assert not self.dest.join("bogus").check() + assert dest.join("dir2").check() + assert not dest.join("dir1").check() + assert not dest.join("bogus").check() - def test_hostmanager_rsync_ignore(self): - dir2 = self.source.ensure("dir1", "dir2", dir=1) - dir5 = self.source.ensure("dir5", "dir6", "bogus") - dirf = self.source.ensure("dir5", "file") + def test_hostmanager_rsync_ignore(self, source, dest): + dir2 = source.ensure("dir1", "dir2", dir=1) + dir5 = source.ensure("dir5", "dir6", "bogus") + dirf = source.ensure("dir5", "file") dir2.ensure("hello") - self.source.join("conftest.py").write(py.code.Source(""" + source.join("conftest.py").write(py.code.Source(""" dist_rsync_ignore = ['dir1/dir2', 'dir5/dir6'] """)) - session = py.test.config._reparse([self.source]).initsession() + session = py.test.config._reparse([source]).initsession() hm = HostManager(session, - hosts=[Host("localhost:" + str(self.dest))]) + hosts=[Host("localhost:" + str(dest))]) hm.init_rsync() - assert self.dest.join("dir1").check() - assert not self.dest.join("dir1", "dir2").check() - assert self.dest.join("dir5","file").check() - assert not self.dest.join("dir6").check() + assert dest.join("dir1").check() + assert not dest.join("dir1", "dir2").check() + assert dest.join("dir5","file").check() + assert not dest.join("dir6").check() - def test_hostmanage_optimise_localhost(self): + def test_hostmanage_optimise_localhost(self, source, dest): hosts = [Host("localhost") for i in range(3)] - session = py.test.config._reparse([self.source]).initsession() + session = py.test.config._reparse([source]).initsession() hm = HostManager(session, hosts=hosts) hm.init_rsync() for host in hosts: @@ -275,28 +272,31 @@ class TestHostManager(TmpWithSourceDest): assert host.gw_remotepath is None assert not host.relpath - def test_hostmanage_setup_hosts(self): + def test_hostmanage_setup_hosts(self, source): hosts = [Host("localhost") for i in range(3)] - session = py.test.config._reparse([self.source]).initsession() + session = py.test.config._reparse([source]).initsession() hm = HostManager(session, hosts=hosts) queue = py.std.Queue.Queue() - hm.setup_hosts(notify=queue.put) + hm.setup_hosts(putevent=queue.put) for host in hm.hosts: - ev = queue.get(timeout=2.0) - assert isinstance(ev, event.HostUp) + eventcall = queue.get(timeout=2.0) + name, args, kwargs = eventcall + assert name == "hostup" for host in hm.hosts: host.node.shutdown() for host in hm.hosts: - ev = queue.get(timeout=2.0) - assert isinstance(ev, event.HostDown) + eventcall = queue.get(timeout=2.0) + name, args, kwargs = eventcall + print name, args, kwargs + assert name == "hostdown" def XXXtest_ssh_rsync_samehost_twice(self): #XXX we have no easy way to have a temp directory remotely! option = py.test.config.option - if option.sshtarget is None: + if option.sshhost is None: py.test.skip("no known ssh target, use -S to set one") - host1 = Host("%s" % (option.sshtarget, )) - host2 = Host("%s" % (option.sshtarget, )) + host1 = Host("%s" % (option.sshhost, )) + host2 = Host("%s" % (option.sshhost, )) hm = HostManager(config, hosts=[host1, host2]) events = [] hm.init_rsync(events.append) diff --git a/py/test/dsession/testing/test_masterslave.py b/py/test/dsession/testing/test_masterslave.py index 5729ee31b..4df5e74b3 100644 --- a/py/test/dsession/testing/test_masterslave.py +++ b/py/test/dsession/testing/test_masterslave.py @@ -2,99 +2,116 @@ import py from py.__.test.dsession.masterslave import MasterNode from py.__.test.dsession.hostmanage import Host -from py.__.test import event -from py.__.test.testing import suptest -class TestMasterSlaveConnection(suptest.InlineCollection): - def getevent(self, eventtype=event.ItemTestReport, timeout=2.0): +class EventQueue: + def __init__(self, bus, queue=None): + if queue is None: + queue = py.std.Queue.Queue() + self.queue = queue + bus.register(self) + + def pyevent(self, eventname, *args, **kwargs): + self.queue.put((eventname, args, kwargs)) + + def geteventargs(self, eventname, timeout=2.0): events = [] while 1: try: - ev = self.queue.get(timeout=timeout) + eventcall = self.queue.get(timeout=timeout) except py.std.Queue.Empty: - print "node channel", self.node.channel - print "remoteerror", self.node.channel._getremoteerror() + #print "node channel", self.node.channel + #print "remoteerror", self.node.channel._getremoteerror() print "seen events", events - raise IOError("did not see %r events" % (eventtype)) + raise IOError("did not see %r events" % (eventname)) else: - if isinstance(ev, eventtype): - return ev - events.append(ev) + name, args, kwargs = eventcall + assert isinstance(name, str) + if name == eventname: + return args + events.append(name) - def setup_method(self, method): - super(TestMasterSlaveConnection, self).setup_method(method) - self.makepyfile(__init__="") - self.config = self.parseconfig(self.tmpdir) +class MySetup: + def __init__(self, pyfuncitem): + self.pyfuncitem = pyfuncitem + + def geteventargs(self, eventname, timeout=2.0): + eq = EventQueue(self.config.bus, self.queue) + return eq.geteventargs(eventname, timeout=timeout) + + def makenode(self, config=None): + if config is None: + config = py.test.config._reparse([]) + self.config = config self.queue = py.std.Queue.Queue() self.host = Host("localhost") self.host.initgateway() - self.node = MasterNode(self.host, self.config, self.queue.put) + self.node = MasterNode(self.host, self.config, putevent=self.queue.put) assert not self.node.channel.isclosed() + return self.node - def getitem(self, source): - kw = {"test_" + self.tmpdir.basename: py.code.Source(source).strip()} - path = self.makepyfile(**kw) - fscol = self.config.getfsnode(path) - return fscol.collect_by_name("test_func") + def finalize(self): + if hasattr(self, 'host'): + print "exiting:", self.host.gw + self.host.gw.exit() - def getitems(self, source): - kw = {"test_" + self.tmpdir.basename: py.code.Source(source).strip()} - path = self.makepyfile(**kw) - fscol = self.config.getfsnode(path) - return fscol.collect() - - def teardown_method(self, method): - print "at teardown:", self.node.channel - #if not self.node.channel.isclosed(): - # self.node.shutdown() - self.host.gw.exit() +def pytest_pyfuncarg_mysetup(pyfuncitem): + mysetup = MySetup(pyfuncitem) + pyfuncitem.addfinalizer(mysetup.finalize) + return mysetup - def test_crash_invalid_item(self): - self.node.send(123) # invalid item - ev = self.getevent(event.HostDown) - assert ev.host == self.host +class TestMasterSlaveConnection: + + def test_crash_invalid_item(self, mysetup): + node = mysetup.makenode() + node.send(123) # invalid item + ev, = mysetup.geteventargs("hostdown") + assert ev.host == mysetup.host assert str(ev.error).find("AttributeError") != -1 - def test_crash_killed(self): + def test_crash_killed(self, testdir, mysetup): if not hasattr(py.std.os, 'kill'): py.test.skip("no os.kill") - item = self.getitem(""" + item = testdir.getitem(""" def test_func(): import os os.kill(os.getpid(), 15) """) - self.node.send(item) - ev = self.getevent(event.HostDown) - assert ev.host == self.host + node = mysetup.makenode(item._config) + node.send(item) + ev, = mysetup.geteventargs("hostdown") + assert ev.host == mysetup.host assert str(ev.error).find("TERMINATED") != -1 - def test_node_down(self): - self.node.shutdown() - ev = self.getevent(event.HostDown) - assert ev.host == self.host + def test_node_down(self, mysetup): + node = mysetup.makenode() + node.shutdown() + ev, = mysetup.geteventargs("hostdown") + assert ev.host == mysetup.host assert not ev.error - self.node.callback(self.node.ENDMARK) + node.callback(node.ENDMARK) excinfo = py.test.raises(IOError, - "self.getevent(event.HostDown, timeout=0.01)") + "mysetup.geteventargs('hostdown', timeout=0.01)") - def test_send_on_closed_channel(self): - item = self.getitem("def test_func(): pass") - self.node.channel.close() - py.test.raises(IOError, "self.node.send(item)") - #ev = self.getevent(event.InternalException) + def test_send_on_closed_channel(self, testdir, mysetup): + item = testdir.getitem("def test_func(): pass") + node = mysetup.makenode(item._config) + node.channel.close() + py.test.raises(IOError, "node.send(item)") + #ev = self.geteventargs(event.InternalException) #assert ev.excinfo.errisinstance(IOError) - def test_send_one(self): - item = self.getitem("def test_func(): pass") - self.node.send(item) - ev = self.getevent() + def test_send_one(self, testdir, mysetup): + item = testdir.getitem("def test_func(): pass") + node = mysetup.makenode(item._config) + node.send(item) + ev, = mysetup.geteventargs("itemtestreport") assert ev.passed assert ev.colitem == item #assert event.item == item #assert event.item is not item - def test_send_some(self): - items = self.getitems(""" + def test_send_some(self, testdir, mysetup): + items = testdir.getitems(""" def test_pass(): pass def test_fail(): @@ -103,13 +120,14 @@ class TestMasterSlaveConnection(suptest.InlineCollection): import py py.test.skip("x") """) + node = mysetup.makenode(items[0]._config) for item in items: - self.node.send(item) + node.send(item) for outcome in "passed failed skipped".split(): - ev = self.getevent() + ev, = mysetup.geteventargs("itemtestreport") assert getattr(ev, outcome) - self.node.sendlist(items) + node.sendlist(items) for outcome in "passed failed skipped".split(): - ev = self.getevent() + ev, = mysetup.geteventargs("itemtestreport") assert getattr(ev, outcome) diff --git a/py/test/event.py b/py/test/event.py index 95ef8771c..36dd30cec 100644 --- a/py/test/event.py +++ b/py/test/event.py @@ -6,24 +6,6 @@ import py import time from py.__.test.outcome import Skipped -class EventBus(object): - """ General Event Bus for distributing events. """ - def __init__(self): - self._subscribers = [] - - def subscribe(self, callback): - """ subscribe given callback to bus events. """ - self._subscribers.append(callback) - - def unsubscribe(self, callback): - """ unsubscribe given callback from bus events. """ - self._subscribers.remove(callback) - - def notify(self, event): - for subscriber in self._subscribers: - subscriber(event) - - class BaseEvent(object): def __repr__(self): l = ["%s=%s" %(key, value) @@ -75,33 +57,73 @@ class Deselected(BaseEvent): class BaseReport(BaseEvent): - failed = passed = skipped = None - def __init__(self, colitem, **kwargs): - self.colitem = colitem - assert len(kwargs) == 1, kwargs - name, value = kwargs.items()[0] - setattr(self, name, True) - self.outcome = value - def toterminal(self, out): - longrepr = self.outcome.longrepr + longrepr = self.longrepr if hasattr(longrepr, 'toterminal'): longrepr.toterminal(out) else: out.line(str(longrepr)) - + class ItemTestReport(BaseReport): """ Test Execution Report. """ + failed = passed = skipped = False + def __init__(self, colitem, excinfo=None, when=None, outerr=None): + self.colitem = colitem + self.keywords = colitem and colitem.readkeywords() + if not excinfo: + self.passed = True + self.shortrepr = "." + else: + self.when = when + if not isinstance(excinfo, py.code.ExceptionInfo): + self.failed = True + shortrepr = "?" + longrepr = excinfo + elif excinfo.errisinstance(Skipped): + self.skipped = True + shortrepr = "s" + longrepr = self.colitem._repr_failure_py(excinfo, outerr) + else: + self.failed = True + shortrepr = self.colitem.shortfailurerepr + if self.when == "execute": + longrepr = self.colitem.repr_failure(excinfo, outerr) + else: # exception in setup or teardown + longrepr = self.colitem._repr_failure_py(excinfo, outerr) + shortrepr = shortrepr.lower() + self.shortrepr = shortrepr + self.longrepr = longrepr + class CollectionStart(BaseEvent): def __init__(self, collector): self.collector = collector class CollectionReport(BaseReport): """ Collection Report. """ - def __init__(self, colitem, result, **kwargs): - super(CollectionReport, self).__init__(colitem, **kwargs) - self.result = result + skipped = failed = passed = False + + def __init__(self, colitem, result, excinfo=None, when=None, outerr=None): + self.colitem = colitem + if not excinfo: + self.passed = True + self.result = result + else: + self.when = when + self.outerr = outerr + self.longrepr = self.colitem._repr_failure_py(excinfo, outerr) + if excinfo.errisinstance(Skipped): + self.skipped = True + self.reason = str(excinfo.value) + else: + self.failed = True + + def toterminal(self, out): + longrepr = self.longrepr + if hasattr(longrepr, 'toterminal'): + longrepr.toterminal(out) + else: + out.line(str(longrepr)) class LooponfailingInfo(BaseEvent): def __init__(self, failreports, rootdirs): @@ -151,3 +173,12 @@ class HostRSyncRootReady(BaseEvent): self.host = host self.root = root + +# make all eventclasses available on BaseEvent so that +# consumers of events can easily filter by +# 'isinstance(event, event.Name)' checks + +for name, cls in vars().items(): + if hasattr(cls, '__bases__') and issubclass(cls, BaseEvent): + setattr(BaseEvent, name, cls) +# diff --git a/py/test/looponfail/remote.py b/py/test/looponfail/remote.py index 33c38ab5c..8308f20f1 100644 --- a/py/test/looponfail/remote.py +++ b/py/test/looponfail/remote.py @@ -13,7 +13,6 @@ import py from py.__.test.session import Session from py.__.test.outcome import Failed, Passed, Skipped from py.__.test.dsession.mypickle import PickleChannel -from py.__.test.report.terminal import TerminalReporter from py.__.test import event from py.__.test.looponfail import util @@ -83,8 +82,8 @@ class RemoteControl(object): from py.__.test.looponfail.remote import slave_runsession from py.__.test.dsession import masterslave config = masterslave.receive_and_send_pickled_config(channel) - width, hasmarkup = channel.receive() - slave_runsession(channel, config, width, hasmarkup) + fullwidth, hasmarkup = channel.receive() + slave_runsession(channel, config, fullwidth, hasmarkup) """, stdout=out, stderr=out) channel = PickleChannel(channel) masterslave.send_and_receive_pickled_config( @@ -117,7 +116,7 @@ class RemoteControl(object): finally: self.ensure_teardown() -def slave_runsession(channel, config, width, hasmarkup): +def slave_runsession(channel, config, fullwidth, hasmarkup): """ we run this on the other side. """ if config.option.debug: def DEBUG(*args): @@ -134,8 +133,10 @@ def slave_runsession(channel, config, width, hasmarkup): DEBUG("SLAVE: initsession()") session = config.initsession() - session.reporter._tw.hasmarkup = hasmarkup - session.reporter._tw.fullwidth = width + # XXX configure the reporter object's terminal writer more directly + # XXX and write a test for this remote-terminal setting logic + config.pytest_terminal_hasmarkup = hasmarkup + config.pytest_terminal_fullwidth = fullwidth if trails: colitems = [] for trail in trails: @@ -148,19 +149,18 @@ def slave_runsession(channel, config, width, hasmarkup): else: colitems = None session.shouldclose = channel.isclosed - #def sendevent(ev): - # channel.send(ev) - #session.bus.subscribe(sendevent) - failreports = [] - def recordfailures(ev): - if isinstance(ev, event.BaseReport): + + class Failures(list): + def pyevent_itemtestreport(self, ev): if ev.failed: - failreports.append(ev) - session.bus.subscribe(recordfailures) + self.append(ev) + pyevent_collectionreport = pyevent_itemtestreport + + failreports = Failures() + session.bus.register(failreports) DEBUG("SLAVE: starting session.main()") session.main(colitems) - session.bus.unsubscribe(recordfailures) - ev = event.LooponfailingInfo(failreports, [config.topdir]) - session.bus.notify(ev) + ev = event.LooponfailingInfo(list(failreports), [config.topdir]) + session.bus.notify("looponfailinginfo", ev) channel.send([x.colitem._totrail() for x in failreports if x.failed]) diff --git a/py/test/looponfail/testing/test_remote.py b/py/test/looponfail/testing/test_remote.py index e661b5f17..3fcbbcd66 100644 --- a/py/test/looponfail/testing/test_remote.py +++ b/py/test/looponfail/testing/test_remote.py @@ -1,33 +1,16 @@ import py -from py.__.test.testing import suptest from py.__.test.looponfail.remote import LooponfailingSession, LoopState, RemoteControl -from py.__.test import event -def getevent(l, evtype): - result = getevents(l, evtype) - if not result: - raise ValueError("event %r not found in %r" %(evtype, l)) - return result[0] - -def getevents(l, evtype): - result = [] - for ev in l: - if isinstance(ev, evtype): - result.append(ev) - return result - - -class TestRemoteControl(suptest.InlineCollection): - def test_nofailures(self): - item = self.getitem("def test_func(): pass\n") - events = [] +class TestRemoteControl: + def test_nofailures(self, testdir): + item = testdir.getitem("def test_func(): pass\n") control = RemoteControl(item._config) control.setup() failures = control.runsession() assert not failures - def test_failures(self): - item = self.getitem("def test_func(): assert 0\n") + def test_failures_somewhere(self, testdir): + item = testdir.getitem("def test_func(): assert 0\n") control = RemoteControl(item._config) control.setup() failures = control.runsession() @@ -38,8 +21,8 @@ class TestRemoteControl(suptest.InlineCollection): failures = control.runsession(failures) assert not failures - def test_failure_change(self): - modcol = self.getitem(""" + def test_failure_change(self, testdir): + modcol = testdir.getitem(""" def test_func(): assert 0 """) @@ -62,9 +45,9 @@ class TestRemoteControl(suptest.InlineCollection): assert failures assert str(failures).find("test_new") != -1 -class TestLooponFailing(suptest.InlineCollection): - def test_looponfailing_from_fail_to_ok(self): - modcol = self.getmodulecol(""" +class TestLooponFailing: + def test_looponfailing_from_fail_to_ok(self, testdir): + modcol = testdir.getmodulecol(""" def test_one(): x = 0 assert x == 1 @@ -88,8 +71,8 @@ class TestLooponFailing(suptest.InlineCollection): session.loop_once(loopstate) assert not loopstate.colitems - def test_looponfailing_from_one_to_two_tests(self): - modcol = self.getmodulecol(""" + def test_looponfailing_from_one_to_two_tests(self, testdir): + modcol = testdir.getmodulecol(""" def test_one(): assert 0 """) @@ -113,8 +96,8 @@ class TestLooponFailing(suptest.InlineCollection): session.loop_once(loopstate) assert len(loopstate.colitems) == 1 - def test_looponfailing_removed_test(self): - modcol = self.getmodulecol(""" + def test_looponfailing_removed_test(self, testdir): + modcol = testdir.getmodulecol(""" def test_one(): assert 0 def test_two(): diff --git a/py/test/looponfail/testing/test_util.py b/py/test/looponfail/testing/test_util.py index a149c1d24..3a77f6f5b 100644 --- a/py/test/looponfail/testing/test_util.py +++ b/py/test/looponfail/testing/test_util.py @@ -1,9 +1,8 @@ import py -from py.__.test.looponfail.util import StatRecorder, EventRecorder -from py.__.test import event +from py.__.test.looponfail.util import StatRecorder -def test_filechange(): - tmp = py.test.ensuretemp("test_filechange") +def test_filechange(tmpdir): + tmp = tmpdir hello = tmp.ensure("hello.py") sd = StatRecorder([tmp]) changed = sd.check() @@ -35,8 +34,8 @@ def test_filechange(): changed = sd.check() assert changed -def test_pycremoval(): - tmp = py.test.ensuretemp("test_pycremoval") +def test_pycremoval(tmpdir): + tmp = tmpdir hello = tmp.ensure("hello.py") sd = StatRecorder([tmp]) changed = sd.check() @@ -52,8 +51,8 @@ def test_pycremoval(): assert not pycfile.check() -def test_waitonchange(): - tmp = py.test.ensuretemp("test_waitonchange") +def test_waitonchange(tmpdir): + tmp = tmpdir sd = StatRecorder([tmp]) wp = py._thread.WorkerPool(1) @@ -62,29 +61,4 @@ def test_waitonchange(): tmp.ensure("newfile.py") reply.get(timeout=0.5) wp.shutdown() - -def test_eventrecorder(): - bus = event.EventBus() - recorder = EventRecorder(bus) - bus.notify(event.NOP()) - assert recorder.events - assert not recorder.getfailures() - rep = event.ItemTestReport(None, failed=True) - bus.notify(rep) - failures = recorder.getfailures() - assert failures == [rep] - recorder.clear() - assert not recorder.events - assert not recorder.getfailures() - recorder.unsubscribe() - bus.notify(rep) - assert not recorder.events - assert not recorder.getfailures() - - - - - - - - + diff --git a/py/test/looponfail/util.py b/py/test/looponfail/util.py index dfbd68b35..04599389c 100644 --- a/py/test/looponfail/util.py +++ b/py/test/looponfail/util.py @@ -52,19 +52,3 @@ class StatRecorder: self.statcache = newstat return changed - -class EventRecorder(object): - def __init__(self, bus): - self.events = [] - self.bus = bus - self.bus.subscribe(self.events.append) - - def getfailures(self): - return [ev for ev in self.events - if isinstance(ev, event.BaseReport) and \ - ev.failed] - def clear(self): - self.events[:] = [] - - def unsubscribe(self): - self.bus.unsubscribe(self.events.append) diff --git a/py/test/outcome.py b/py/test/outcome.py index 468f91316..49e292d6d 100644 --- a/py/test/outcome.py +++ b/py/test/outcome.py @@ -139,6 +139,13 @@ def deprecated_call(func, *args, **kwargs): raise AssertionError("%r did not produce DeprecationWarning" %(func,)) return ret +class keywords: + """ decorator for setting function attributes. """ + def __init__(self, **kw): + self.kw = kw + def __call__(self, func): + func.func_dict.update(self.kw) + return func # exitcodes for the command line EXIT_OK = 0 diff --git a/py/test/parseopt.py b/py/test/parseopt.py new file mode 100644 index 000000000..ba43330f0 --- /dev/null +++ b/py/test/parseopt.py @@ -0,0 +1,92 @@ +""" +thin wrapper around Python's optparse.py +adding some extra checks and ways to systematically +have Environment variables provide default values +for options. basic usage: + + >>> parser = Parser() + >>> parser.addoption("--hello", action="store_true", dest="hello") + >>> option, args = parser.parse(['--hello']) + >>> option.hello + True + >>> args + [] + +""" +import py +from py.compat import optparse + +class Parser: + """ Parser for command line arguments. """ + + def __init__(self, usage=None, processopt=None): + self._anonymous = OptionGroup("misc", parser=self) + self._groups = [self._anonymous] + self._processopt = processopt + self._usage = usage + + def processoption(self, option): + if self._processopt: + if option.dest: + self._processopt(option) + + def addgroup(self, name, description=""): + for group in self._groups: + if group.name == name: + raise ValueError("group %r already exists" % name) + group = OptionGroup(name, description, parser=self) + self._groups.append(group) + return group + + def getgroup(self, name): + for group in self._groups: + if group.name == name: + return group + raise ValueError("group %r not found" %(name,)) + + def addoption(self, *opts, **attrs): + """ add an optparse-style option. """ + self._anonymous.addoption(*opts, **attrs) + + def parse(self, args): + optparser = optparse.OptionParser(usage=self._usage) + for group in self._groups: + if group.options: + optgroup = optparse.OptionGroup(optparser, group.name) + optgroup.add_options(group.options) + optparser.add_option_group(optgroup) + return optparser.parse_args([str(x) for x in args]) + + def parse_setoption(self, args, option): + parsedoption, args = self.parse(args) + for name, value in parsedoption.__dict__.items(): + setattr(option, name, value) + return args + + +class OptionGroup: + def __init__(self, name, description="", parser=None): + self.name = name + self.description = description + self.options = [] + self.parser = parser + + def addoption(self, *optnames, **attrs): + """ add an option to this group. """ + option = py.compat.optparse.Option(*optnames, **attrs) + self._addoption_instance(option, shortupper=False) + + def _addoption(self, *optnames, **attrs): + option = py.compat.optparse.Option(*optnames, **attrs) + self._addoption_instance(option, shortupper=True) + + def _addoption_instance(self, option, shortupper=False): + if not shortupper: + for opt in option._short_opts: + if opt[0] == '-' and opt[1].islower(): + raise ValueError("lowercase shortoptions reserved") + if self.parser: + self.parser.processoption(option) + self.options.append(option) + + diff --git a/py/test/plugin/__init__.py b/py/test/plugin/__init__.py new file mode 100644 index 000000000..792d60054 --- /dev/null +++ b/py/test/plugin/__init__.py @@ -0,0 +1 @@ +# diff --git a/py/test/plugin/conftest.py b/py/test/plugin/conftest.py new file mode 100644 index 000000000..ef6dbd2c8 --- /dev/null +++ b/py/test/plugin/conftest.py @@ -0,0 +1,10 @@ +import py + +pytest_plugins = "pytester", "plugintester" + +class ConftestPlugin: + def pytest_collect_file(self, path, parent): + if path.basename.startswith("pytest_") and path.ext == ".py": + mod = parent.Module(path, parent=parent) + return mod + diff --git a/py/test/plugin/pytest_apigen.py b/py/test/plugin/pytest_apigen.py new file mode 100644 index 000000000..1f4fdbacc --- /dev/null +++ b/py/test/plugin/pytest_apigen.py @@ -0,0 +1,82 @@ +import py + +class ApigenPlugin: + def pytest_addoption(self, parser): + group = parser.addgroup("apigen options") + group.addoption('--apigen', action="store_true", dest="apigen", + help="generate api documentation") + #group.addoption('--apigenpath', + # action="store", dest="apigenpath", + # default="../apigen", + # type="string", + # help="relative path to apigen doc output location (relative from py/)") + #group.addoption('--docpath', + # action='store', dest='docpath', + # default="doc", type='string', + # help="relative path to doc output location (relative from py/)") + + def pytest_configure(self, config): + if config.option.apigen: + from py.__.apigen.tracer.tracer import Tracer, DocStorage + self.pkgdir = py.path.local(config.args[0]).pypkgpath() + apigenscriptpath = py.path.local(py.__file__).dirpath("apigen", "apigen.py") + apigenscript = apigenscriptpath.pyimport() + if not hasattr(apigenscript, 'get_documentable_items'): + raise NotImplementedError("%r needs to provide get_documentable_items" %( + apigenscriptpath,)) + self.apigenscript = apigenscript + pkgname, items = apigenscript.get_documentable_items(self.pkgdir) + self.docstorage = DocStorage().from_dict(items, + module_name=pkgname) + self.tracer = Tracer(self.docstorage) + + def pytest_pyfunc_call(self, pyfuncitem, args, kwargs): + if hasattr(self, 'tracer'): + self.tracer.start_tracing() + try: + pyfuncitem.obj(*args, **kwargs) + finally: + self.tracer.end_tracing() + return True + + def pytest_terminal_summary(self, terminalreporter): + if hasattr(self, 'tracer'): + tr = terminalreporter + from py.__.apigen.tracer.docstorage import DocStorageAccessor + terminalreporter.write_sep("=", "apigen: building documentation") + #assert hasattr(tr.config.option, 'apigenpath') + capture = py.io.StdCaptureFD() + try: + self.apigenscript.build( + tr.config, + self.pkgdir, + DocStorageAccessor(self.docstorage), + capture) + finally: + capture.reset() + terminalreporter.write_line("apigen build completed") + +def test_generic(plugintester): + plugintester.apicheck(ApigenPlugin) + +def test_functional_simple(testdir): + sub = testdir.tmpdir.mkdir("test_simple") + sub.join("__init__.py").write(py.code.Source(""" + from py import initpkg + initpkg(__name__, exportdefs={ + 'simple.f': ('./test_simple.py', 'f',), + }) + """)) + pyfile = sub.join("test_simple.py") + pyfile.write(py.code.Source(""" + def f(arg): + pass + def test_f(): + f(42) + """)) + testdir.makepyfile(conftest="pytest_plugins='apigen'") + result = testdir.runpytest(pyfile, "--apigen") + result.stdout.fnmatch_lines([ + "*apigen: building documentation*", + "apigen build completed", + ]) diff --git a/py/test/plugin/pytest_default.py b/py/test/plugin/pytest_default.py new file mode 100644 index 000000000..25d6fec5c --- /dev/null +++ b/py/test/plugin/pytest_default.py @@ -0,0 +1,83 @@ +class DefaultPlugin: + """ Plugin implementing defaults and general options. """ + + def pytest_collect_file(self, path, parent): + ext = path.ext + pb = path.purebasename + if pb.startswith("test_") or pb.endswith("_test") or \ + path in parent._config.args: + if ext == ".py": + return parent.Module(path, parent=parent) + + def pytest_addoption(self, parser): + group = parser.addgroup("general", "general options") + group._addoption('-v', '--verbose', action="count", + dest="verbose", default=0, help="increase verbosity."), + group._addoption('-x', '--exitfirst', + action="store_true", dest="exitfirst", default=False, + help="exit instantly on first error or failed test."), + group._addoption('-s', '--nocapture', + action="store_true", dest="nocapture", default=False, + help="disable catching of sys.stdout/stderr output."), + group._addoption('-k', + action="store", dest="keyword", default='', + help="only run test items matching the given " + "space separated keywords. precede a keyword with '-' to negate. " + "Terminate the expression with ':' to treat a match as a signal " + "to run all subsequent tests. ") + group._addoption('-l', '--showlocals', + action="store_true", dest="showlocals", default=False, + help="show locals in tracebacks (disabled by default)."), + group._addoption('--showskipsummary', + action="store_true", dest="showskipsummary", default=False, + help="always show summary of skipped tests"), + group._addoption('', '--pdb', + action="store_true", dest="usepdb", default=False, + help="start pdb (the Python debugger) on errors."), + group._addoption('', '--tb', + action="store", dest="tbstyle", default='long', + type="choice", choices=['long', 'short', 'no'], + help="traceback verboseness (long/short/no)."), + group._addoption('', '--fulltrace', + action="store_true", dest="fulltrace", default=False, + help="don't cut any tracebacks (default is to cut)."), + group._addoption('', '--nomagic', + action="store_true", dest="nomagic", default=False, + help="refrain from using magic as much as possible."), + group._addoption('', '--traceconfig', + action="store_true", dest="traceconfig", default=False, + help="trace considerations of conftest.py files."), + group._addoption('-f', '--looponfailing', + action="store_true", dest="looponfailing", default=False, + help="loop on failing test set."), + group._addoption('', '--exec', + action="store", dest="executable", default=None, + help="python executable to run the tests with."), + group._addoption('-n', '--numprocesses', dest="numprocesses", default=0, + action="store", type="int", + help="number of local test processes."), + group._addoption('', '--debug', + action="store_true", dest="debug", default=False, + help="turn on debugging information."), + + group = parser.addgroup("experimental", "experimental options") + group._addoption('-d', '--dist', + action="store_true", dest="dist", default=False, + help="ad-hoc distribute tests across machines (requires conftest settings)"), + group._addoption('-w', '--startserver', + action="store_true", dest="startserver", default=False, + help="starts local web server for displaying test progress.", + ), + group._addoption('-r', '--runbrowser', + action="store_true", dest="runbrowser", default=False, + help="run browser (implies --startserver)." + ), + group._addoption('', '--boxed', + action="store_true", dest="boxed", default=False, + help="box each test run in a separate process"), + group._addoption('', '--rest', + action='store_true', dest="restreport", default=False, + help="restructured text output reporting."), + group._addoption('', '--session', + action="store", dest="session", default=None, + help="lookup given sessioname in conftest.py files and use it."), diff --git a/py/test/plugin/pytest_doctest.py b/py/test/plugin/pytest_doctest.py new file mode 100644 index 000000000..204fe173d --- /dev/null +++ b/py/test/plugin/pytest_doctest.py @@ -0,0 +1,170 @@ +import py + +class DoctestPlugin: + def pytest_addoption(self, parser): + parser.addoption("--doctest-modules", + action="store_true", dest="doctestmodules") + + def pytest_collect_file(self, path, parent): + if path.ext == ".py": + if parent._config.getvalue("doctestmodules"): + return DoctestModule(path, parent) + if path.check(fnmatch="test_*.txt"): + return DoctestTextfile(path, parent) + +from py.__.code.excinfo import Repr, ReprFileLocation + +class ReprFailDoctest(Repr): + def __init__(self, reprlocation, lines): + self.reprlocation = reprlocation + self.lines = lines + def toterminal(self, tw): + for line in self.lines: + tw.line(line) + self.reprlocation.toterminal(tw) + +class DoctestItem(py.test.collect.Item): + def __init__(self, path, parent): + name = self.__class__.__name__ + ":" + path.basename + super(DoctestItem, self).__init__(name=name, parent=parent) + self.fspath = path + + def repr_failure(self, excinfo, outerr): + if excinfo.errisinstance(py.compat.doctest.DocTestFailure): + doctestfailure = excinfo.value + example = doctestfailure.example + test = doctestfailure.test + filename = test.filename + lineno = example.lineno + 1 + message = excinfo.type.__name__ + reprlocation = ReprFileLocation(filename, lineno, message) + checker = py.compat.doctest.OutputChecker() + REPORT_UDIFF = py.compat.doctest.REPORT_UDIFF + filelines = py.path.local(filename).readlines(cr=0) + i = max(0, lineno - 10) + lines = [] + for line in filelines[i:lineno]: + lines.append("%03d %s" % (i+1, line)) + i += 1 + lines += checker.output_difference(example, + doctestfailure.got, REPORT_UDIFF).split("\n") + return ReprFailDoctest(reprlocation, lines) + elif excinfo.errisinstance(py.compat.doctest.UnexpectedException): + excinfo = py.code.ExceptionInfo(excinfo.value.exc_info) + return super(DoctestItem, self).repr_failure(excinfo, outerr) + else: + return super(DoctestItem, self).repr_failure(excinfo, outerr) + +class DoctestTextfile(DoctestItem): + def runtest(self): + if not self._deprecated_testexecution(): + failed, tot = py.compat.doctest.testfile( + str(self.fspath), module_relative=False, + raise_on_error=True, verbose=0) + +class DoctestModule(DoctestItem): + def runtest(self): + module = self.fspath.pyimport() + failed, tot = py.compat.doctest.testmod( + module, raise_on_error=True, verbose=0) + + +# +# Plugin tests +# + +class TestDoctests: + def test_collect_testtextfile(self, testdir): + testdir.plugins.append(DoctestPlugin()) + testdir.maketxtfile(whatever="") + checkfile = testdir.maketxtfile(test_something=""" + alskdjalsdk + >>> i = 5 + >>> i-1 + 4 + """) + for x in (testdir.tmpdir, checkfile): + #print "checking that %s returns custom items" % (x,) + items, events = testdir.inline_genitems(x) + print events.events + assert len(items) == 1 + assert isinstance(items[0], DoctestTextfile) + + def test_collect_module(self, testdir): + testdir.plugins.append(DoctestPlugin()) + path = testdir.makepyfile(whatever="#") + for p in (path, testdir.tmpdir): + items, evrec = testdir.inline_genitems(p, '--doctest-modules') + print evrec.events + assert len(items) == 1 + assert isinstance(items[0], DoctestModule) + + def test_simple_doctestfile(self, testdir): + testdir.plugins.append(DoctestPlugin()) + p = testdir.maketxtfile(test_doc=""" + >>> x = 1 + >>> x == 1 + False + """) + events = testdir.inline_run_with_plugins(p) + ev, = events.getnamed("itemtestreport") + assert ev.failed + + def test_doctest_unexpected_exception(self, testdir): + from py.__.test.outcome import Failed + + testdir.plugins.append(DoctestPlugin()) + p = testdir.maketxtfile(""" + >>> i = 0 + >>> i = 1 + >>> x + 2 + """) + sorter = testdir.inline_run(p) + events = sorter.getnamed("itemtestreport") + assert len(events) == 1 + ev, = events + assert ev.failed + assert ev.longrepr + # XXX + #testitem, = items + #excinfo = py.test.raises(Failed, "testitem.runtest()") + #repr = testitem.repr_failure(excinfo, ("", "")) + #assert repr.reprlocation + + def test_doctestmodule(self, testdir): + testdir.plugins.append(DoctestPlugin()) + p = testdir.makepyfile(""" + ''' + >>> x = 1 + >>> x == 1 + False + + ''' + """) + events = testdir.inline_run_with_plugins(p, "--doctest-modules") + ev, = events.getnamed("itemtestreport") + assert ev.failed + + def test_txtfile_failing(self, testdir): + testdir.plugins.append('pytest_doctest') + p = testdir.maketxtfile(""" + >>> i = 0 + >>> i + 1 + 2 + """) + result = testdir.runpytest(p) + result.stdout.fnmatch_lines([ + '001 >>> i = 0', + '002 >>> i + 1', + 'Expected:', + " 2", + "Got:", + " 1", + "*test_txtfile_failing.txt:2: DocTestFailure" + ]) + + +def test_generic(plugintester): + plugintester.apicheck(DoctestPlugin) + diff --git a/py/test/plugin/pytest_eventlog.py b/py/test/plugin/pytest_eventlog.py new file mode 100644 index 000000000..0a0986ebe --- /dev/null +++ b/py/test/plugin/pytest_eventlog.py @@ -0,0 +1,41 @@ +import py + +class EventlogPlugin: + """ log pytest events to a file. """ + def pytest_addoption(self, parser): + parser.addoption("--eventlog", dest="eventlog", + help="write all pytest events to the given file.") + + def pytest_configure(self, config): + eventlog = config.getvalue("eventlog") + if eventlog: + self.eventlogfile = open(eventlog, 'w') + + def pytest_unconfigure(self, config): + if hasattr(self, 'eventlogfile'): + self.eventlogfile.close() + del self.eventlogfile + + def pyevent(self, eventname, *args, **kwargs): + if hasattr(self, 'eventlogfile'): + f = self.eventlogfile + print >>f, eventname, args, kwargs + f.flush() + +# =============================================================================== +# plugin tests +# =============================================================================== + +def test_generic(plugintester): + plugintester.apicheck(EventlogPlugin) + + testdir = plugintester.testdir() + testdir.makepyfile(""" + def test_pass(): + pass + """) + testdir.runpytest("--eventlog=event.log") + s = testdir.tmpdir.join("event.log").read() + assert s.find("TestrunStart") != -1 + assert s.find("ItemTestReport") != -1 + assert s.find("TestrunFinish") != -1 diff --git a/py/test/plugin/pytest_iocapture.py b/py/test/plugin/pytest_iocapture.py new file mode 100644 index 000000000..ec6e6807b --- /dev/null +++ b/py/test/plugin/pytest_iocapture.py @@ -0,0 +1,54 @@ +import py + +class IocapturePlugin: + """ capture sys.stdout/sys.stderr / fd1/fd2. """ + def pytest_pyfuncarg_stdcapture(self, pyfuncitem): + capture = Capture(py.io.StdCapture) + pyfuncitem.addfinalizer(capture.finalize) + return capture + + def pytest_pyfuncarg_stdcapturefd(self, pyfuncitem): + capture = Capture(py.io.StdCaptureFD) + pyfuncitem.addfinalizer(capture.finalize) + return capture + +class Capture: + def __init__(self, captureclass): + self._captureclass = captureclass + self._capture = self._captureclass() + + def finalize(self): + self._capture.reset() + + def reset(self): + res = self._capture.reset() + self._capture = self._captureclass() + return res + +def test_generic(plugintester): + plugintester.apicheck(IocapturePlugin) + +class TestCapture: + def test_std_functional(self, testdir): + testdir.plugins.append(IocapturePlugin()) + evrec = testdir.inline_runsource(""" + def test_hello(stdcapture): + print 42 + out, err = stdcapture.reset() + assert out.startswith("42") + """) + ev, = evrec.getnamed("itemtestreport") + assert ev.passed + + def test_stdfd_functional(self, testdir): + testdir.plugins.append(IocapturePlugin()) + evrec = testdir.inline_runsource(""" + def test_hello(stdcapturefd): + import os + os.write(1, "42") + out, err = stdcapturefd.reset() + assert out.startswith("42") + """) + ev, = evrec.getnamed("itemtestreport") + assert ev.passed + diff --git a/py/test/plugin/pytest_monkeypatch.py b/py/test/plugin/pytest_monkeypatch.py new file mode 100644 index 000000000..752c6145e --- /dev/null +++ b/py/test/plugin/pytest_monkeypatch.py @@ -0,0 +1,66 @@ +class MonkeypatchPlugin: + """ setattr-monkeypatching with automatical reversal after test. """ + def pytest_pyfuncarg_monkeypatch(self, pyfuncitem): + monkeypatch = MonkeyPatch() + pyfuncitem.addfinalizer(monkeypatch.finalize) + return monkeypatch + +notset = object() + +class MonkeyPatch: + def __init__(self): + self._setattr = [] + self._setitem = [] + + def setattr(self, obj, name, value): + self._setattr.insert(0, (obj, name, getattr(obj, name, notset))) + setattr(obj, name, value) + + def setitem(self, dictionary, name, value): + self._setitem.insert(0, (dictionary, name, dictionary.get(name, notset))) + dictionary[name] = value + + def finalize(self): + for obj, name, value in self._setattr: + if value is not notset: + setattr(obj, name, value) + for dictionary, name, value in self._setitem: + if value is notset: + del dictionary[name] + else: + dictionary[name] = value + + +def test_setattr(): + class A: + x = 1 + monkeypatch = MonkeyPatch() + monkeypatch.setattr(A, 'x', 2) + assert A.x == 2 + monkeypatch.setattr(A, 'x', 3) + assert A.x == 3 + monkeypatch.finalize() + assert A.x == 1 + +def test_setitem(): + d = {'x': 1} + monkeypatch = MonkeyPatch() + monkeypatch.setitem(d, 'x', 2) + monkeypatch.setitem(d, 'y', 1700) + assert d['x'] == 2 + assert d['y'] == 1700 + monkeypatch.setitem(d, 'x', 3) + assert d['x'] == 3 + monkeypatch.finalize() + assert d['x'] == 1 + assert 'y' not in d + +def test_monkeypatch_plugin(testdir): + sorter = testdir.inline_runsource(""" + pytest_plugins = 'pytest_monkeypatch', + def test_method(monkeypatch): + assert monkeypatch.__class__.__name__ == "MonkeyPatch" + """) + res = sorter.countoutcomes() + assert tuple(res) == (1, 0, 0), res + diff --git a/py/test/plugin/pytest_plugintester.py b/py/test/plugin/pytest_plugintester.py new file mode 100644 index 000000000..a71567fbd --- /dev/null +++ b/py/test/plugin/pytest_plugintester.py @@ -0,0 +1,188 @@ +""" +plugin with support classes and functions for testing pytest functionality +""" +import py + +class PlugintesterPlugin: + """ test support code for testing pytest plugins. """ + def pytest_pyfuncarg_plugintester(self, pyfuncitem): + pt = PluginTester(pyfuncitem) + pyfuncitem.addfinalizer(pt.finalize) + return pt + +class Support(object): + def __init__(self, pyfuncitem): + """ instantiated per function that requests it. """ + self.pyfuncitem = pyfuncitem + + def getmoditem(self): + for colitem in self.pyfuncitem.listchain(): + if isinstance(colitem, colitem.Module): + return colitem + + def finalize(self): + """ called after test function finished execution""" + +class PluginTester(Support): + def testdir(self): + # XXX import differently, eg. + # FSTester = self.pyfuncitem._config.pytestplugins.getpluginattr("pytester", "FSTester") + from pytest_pytester import TmpTestdir + crunner = TmpTestdir(self.pyfuncitem) + # + for colitem in self.pyfuncitem.listchain(): + if isinstance(colitem, py.test.collect.Module) and \ + colitem.name.startswith("pytest_"): + crunner.plugins.append(colitem.fspath.purebasename) + break + return crunner + + def apicheck(self, pluginclass): + print "loading and checking", pluginclass + fail = False + pm = py.test._PytestPlugins() + methods = collectattr(pluginclass) + hooks = collectattr(PytestPluginHooks) + getargs = py.std.inspect.getargs + + def isgenerichook(name): + return name.startswith("pytest_pyfuncarg_") + + while methods: + name, method = methods.popitem() + if isgenerichook(name): + continue + if name not in hooks: + print "found unknown hook: %s" % name + fail = True + else: + hook = hooks[name] + if not hasattr(hook, 'func_code'): + continue # XXX do some checks on attributes as well? + method_args = getargs(method.func_code) + hookargs = getargs(hook.func_code) + for arg, hookarg in zip(method_args[0], hookargs[0]): + if arg != hookarg: + print "argument mismatch:" + print "actual : %s.%s" %(pluginclass.__name__, formatdef(method)) + print "required:", formatdef(hook) + fail = True + break + if not fail: + print "matching hook:", formatdef(method) + if fail: + py.test.fail("Plugin API error") + +def collectattr(obj, prefixes=("pytest_", "pyevent_")): + methods = {} + for apiname in vars(obj): + for prefix in prefixes: + if apiname.startswith(prefix): + methods[apiname] = getattr(obj, apiname) + return methods + +def formatdef(func): + formatargspec = py.std.inspect.formatargspec + getargspec = py.std.inspect.formatargspec + return "%s%s" %( + func.func_name, + py.std.inspect.formatargspec(*py.std.inspect.getargspec(func)) + ) + + +class PytestPluginHooks: + def __init__(self): + """ usually called only once per test process. """ + + def pytest_addoption(self, parser): + """ called before commandline parsing. """ + + def pytest_configure(self, config): + """ called after command line options have been parsed. + and all plugins and initial conftest files been loaded. + ``config`` provides access to all such configuration values. + """ + def pytest_unconfigure(self, config): + """ called before test process is exited. + """ + + def pytest_event(self, event): + """ called for each internal py.test event. """ + + #def pytest_pyfuncarg_NAME(self, pyfuncitem, argname): + # """ provide (value, finalizer) for an open test function argument. + # + # the finalizer (if not None) will be called after the test + # function has been executed (i.e. pyfuncitem.execute() returns). + # """ + + def pytest_pyfunc_call(self, pyfuncitem, args, kwargs): + """ return True if we consumed/did the call to the python function item. """ + + # collection hooks + def pytest_collect_file(self, path, parent): + """ return Collection node or None. """ + + def pytest_pymodule_makeitem(self, modcol, name, obj): + """ return custom item/collector or None. """ + + # from pytest_terminal plugin + def pytest_report_teststatus(self, event): + """ return shortletter and verbose word. """ + + def pytest_terminal_summary(self, terminalreporter): + """ add additional section in terminal summary reporting. """ + + # events + def pyevent(self, eventname, *args, **kwargs): + """ called for each testing event. """ + + def pyevent_internalerror(self, event): + """ called for internal errors. """ + + def pyevent_itemstart(self, event): + """ test item gets collected. """ + + def pyevent_itemtestreport(self, event): + """ test has been run. """ + + def pyevent_deselected(self, event): + """ item has been dselected. """ + + def pyevent_collectionstart(self, event): + """ collector starts collecting. """ + + def pyevent_collectionreport(self, event): + """ collector finished collecting. """ + + def pyevent_testrunstart(self, event): + """ whole test run starts. """ + + def pyevent_testrunfinish(self, event): + """ whole test run starts. """ + + def pyevent_hostup(self, event): + """ Host is up. """ + + def pyevent_hostgatewayready(self, event): + """ Connection to Host is ready. """ + + def pyevent_hostdown(self, event): + """ Host is down. """ + + def pyevent_rescheduleitems(self, event): + """ Items from a host that went down. """ + + def pyevent_looponfailinginfo(self, event): + """ info for repeating failing tests. """ + + def pyevent_plugin_registered(self, plugin): + """ a new py lib plugin got registered. """ + + +# =============================================================================== +# plugin tests +# =============================================================================== + +def test_generic(plugintester): + plugintester.apicheck(PlugintesterPlugin) diff --git a/py/test/plugin/pytest_pocoo.py b/py/test/plugin/pytest_pocoo.py new file mode 100644 index 000000000..8fc32e07e --- /dev/null +++ b/py/test/plugin/pytest_pocoo.py @@ -0,0 +1,62 @@ +""" +py.test plugin for sending testing failure information to paste.pocoo.org +""" +import py + +class url: + base = "http://paste.pocoo.org" + xmlrpc = base + "/xmlrpc/" + show = base + "/show/" + +class PocooPlugin(object): + """ report URLs from sending test failures to the pocoo paste service. """ + + def pytest_addoption(self, parser): + parser.addoption('--pocoo-sendfailures', + action='store_true', dest="pocoo_sendfailures", + help="send failures to %s" %(url.base,)) + + def getproxy(self): + return py.std.xmlrpclib.ServerProxy(url.xmlrpc).pastes + + def pytest_terminal_summary(self, terminalreporter): + if terminalreporter.config.option.pocoo_sendfailures: + tr = terminalreporter + if 'failed' in tr.stats and tr.config.option.tbstyle != "no": + terminalreporter.write_sep("=", "Sending failures to %s" %(url.base,)) + terminalreporter.write_line("xmlrpcurl: %s" %(url.xmlrpc,)) + serverproxy = self.getproxy() + for ev in terminalreporter.stats.get('failed'): + tw = py.io.TerminalWriter(stringio=True) + ev.toterminal(tw) + s = tw.stringio.getvalue() + # XXX add failure summary + assert len(s) + terminalreporter.write_line("newpaste() ...") + id = serverproxy.newPaste("python", s) + terminalreporter.write_line("%s%s\n" % (url.show, id)) + break + + +def test_apicheck(plugintester): + plugintester.apicheck(PocooPlugin) + +pytest_plugins = 'pytest_monkeypatch', +def test_toproxy(testdir, monkeypatch): + testdir.makepyfile(conftest="pytest_plugins='pytest_pocoo',") + testpath = testdir.makepyfile(""" + import py + def test_pass(): + pass + def test_fail(): + assert 0 + def test_skip(): + py.test.skip("") + """) + l = [] + class MockProxy: + def newPaste(self, language, code): + l.append((language, code)) + + monkeypatch.setattr(PocooPlugin, 'getproxy', MockProxy) + result = testdir.inline_run(testpath, "--pocoo-sendfailures") diff --git a/py/test/plugin/pytest_pytester.py b/py/test/plugin/pytest_pytester.py new file mode 100644 index 000000000..9797d0f91 --- /dev/null +++ b/py/test/plugin/pytest_pytester.py @@ -0,0 +1,438 @@ +""" +pytes plugin for easing testing of pytest runs themselves. +""" + +import py +from py.__.test import event + +class PytesterPlugin: + def pytest_pyfuncarg_linecomp(self, pyfuncitem): + return LineComp() + + def pytest_pyfuncarg_LineMatcher(self, pyfuncitem): + return LineMatcher + + def pytest_pyfuncarg_testdir(self, pyfuncitem): + tmptestdir = TmpTestdir(pyfuncitem) + pyfuncitem.addfinalizer(tmptestdir.finalize) + return tmptestdir + + def pytest_pyfuncarg_EventRecorder(self, pyfuncitem): + return EventRecorder + +def test_generic(plugintester): + plugintester.apicheck(PytesterPlugin) + +class RunResult: + def __init__(self, ret, outlines, errlines): + self.ret = ret + self.outlines = outlines + self.errlines = errlines + self.stdout = LineMatcher(outlines) + self.stderr = LineMatcher(errlines) + +class TmpTestdir: + def __init__(self, pyfuncitem): + self.pyfuncitem = pyfuncitem + # XXX remove duplication with tmpdir plugin + basetmp = py.test.ensuretemp("testdir") + name = pyfuncitem.name + for i in range(100): + try: + tmpdir = basetmp.mkdir(name + str(i)) + except py.error.EEXIST: + continue + break + # we need to create another subdir + # because Directory.collect() currently loads + # conftest.py from sibling directories + self.tmpdir = tmpdir.mkdir(name) + self.plugins = [] + self._syspathremove = [] + from py.__.test.config import Config + self.Config = Config + + def finalize(self): + for p in self._syspathremove: + py.std.sys.path.remove(p) + if hasattr(self, '_olddir'): + self._olddir.chdir() + + def chdir(self): + old = self.testdir.chdir() + if not hasattr(self, '_olddir'): + self._olddir = old + + def _makefile(self, ext, args, kwargs): + items = kwargs.items() + if args: + source = "\n".join(map(str, args)) + basename = self.pyfuncitem.name + items.insert(0, (basename, source)) + ret = None + for name, value in items: + p = self.tmpdir.join(name).new(ext=ext) + source = py.code.Source(value) + p.write(str(py.code.Source(value)).lstrip()) + if ret is None: + ret = p + return ret + + + def makefile(self, ext, *args, **kwargs): + return self._makefile(ext, args, kwargs) + + def makeconftest(self, source): + return self.makepyfile(conftest=source) + + def makepyfile(self, *args, **kwargs): + return self._makefile('.py', args, kwargs) + + def maketxtfile(self, *args, **kwargs): + return self._makefile('.txt', args, kwargs) + + def syspathinsert(self, path=None): + if path is None: + path = self.tmpdir + py.std.sys.path.insert(0, str(path)) + self._syspathremove.append(str(path)) + + def mkdir(self, name): + return self.tmpdir.mkdir(name) + + def chdir(self): + return self.tmpdir.chdir() + + def genitems(self, colitems): + return list(self.session.genitems(colitems)) + + def inline_genitems(self, *args): + #config = self.parseconfig(*args) + config = self.parseconfig(*args) + session = config.initsession() + rec = EventRecorder(config.bus) + colitems = [config.getfsnode(arg) for arg in config.args] + items = list(session.genitems(colitems)) + return items, rec + + def runitem(self, source, **runnerargs): + # used from runner functional tests + item = self.getitem(source) + # the test class where we are called from wants to provide the runner + testclassinstance = self.pyfuncitem.obj.im_self + runner = testclassinstance.getrunner() + return runner(item, **runnerargs) + + def inline_runsource(self, source, *cmdlineargs): + p = self.makepyfile(source) + l = list(cmdlineargs) + [p] + return self.inline_run(*l) + + def inline_runsession(self, session): + config = session.config + config.pytestplugins.configure(config) + sorter = EventRecorder(config.bus) + session.main() + config.pytestplugins.unconfigure(config) + return sorter + + def inline_run(self, *args): + config = self.parseconfig(*args) + config.pytestplugins.configure(config) + session = config.initsession() + sorter = EventRecorder(config.bus) + session.main() + config.pytestplugins.unconfigure(config) + return sorter + + def inline_run_with_plugins(self, *args): + config = self.parseconfig(*args) + config.pytestplugins.configure(config) + session = config.initsession() + sorter = EventRecorder(config.bus) + session.main() + config.pytestplugins.unconfigure(config) + return sorter + + def config_preparse(self): + config = self.Config() + for plugin in self.plugins: + if isinstance(plugin, str): + config.pytestplugins.import_plugin(plugin) + else: + config.pytestplugins.register(plugin) + return config + + def parseconfig(self, *args): + if not args: + args = (self.tmpdir,) + config = self.config_preparse() + config.parse(list(args)) + return config + + def getitem(self, source, funcname="test_func"): + modcol = self.getmodulecol(source) + item = modcol.join(funcname) + assert item is not None, "%r item not found in module:\n%s" %(funcname, source) + return item + + def getitems(self, source): + modcol = self.getmodulecol(source) + return list(modcol._config.initsession().genitems([modcol])) + #assert item is not None, "%r item not found in module:\n%s" %(funcname, source) + #return item + + def getfscol(self, path, configargs=()): + self.config = self.parseconfig(path, *configargs) + self.session = self.config.initsession() + return self.config.getfsnode(path) + + def getmodulecol(self, source, configargs=(), withinit=False): + kw = {self.pyfuncitem.name: py.code.Source(source).strip()} + path = self.makepyfile(**kw) + if withinit: + self.makepyfile(__init__ = "#") + self.config = self.parseconfig(path, *configargs) + self.session = self.config.initsession() + return self.config.getfsnode(path) + + def prepare(self): + p = self.tmpdir.join("conftest.py") + if not p.check(): + plugins = [x for x in self.plugins if isinstance(x, str)] + p.write("import py ; pytest_plugins = %r" % plugins) + else: + if self.plugins: + print "warning, ignoring reusing existing con", p + + def popen(self, cmdargs, stdout, stderr, **kw): + if not hasattr(py.std, 'subprocess'): + py.test.skip("no subprocess module") + return py.std.subprocess.Popen(cmdargs, stdout=stdout, stderr=stderr, **kw) + + def run(self, *cmdargs): + self.prepare() + old = self.tmpdir.chdir() + #print "chdir", self.tmpdir + try: + return self._run(*cmdargs) + finally: + old.chdir() + + def _run(self, *cmdargs): + cmdargs = map(str, cmdargs) + p1 = py.path.local("stdout") + p2 = py.path.local("stderr") + print "running", cmdargs, "curdir=", py.path.local() + popen = self.popen(cmdargs, stdout=p1.open("w"), stderr=p2.open("w")) + ret = popen.wait() + out, err = p1.readlines(cr=0), p2.readlines(cr=0) + if err: + for line in err: + print >>py.std.sys.stderr, line + if out: + for line in out: + print >>py.std.sys.stdout, line + return RunResult(ret, out, err) + + def runpybin(self, scriptname, *args): + bindir = py.path.local(py.__file__).dirpath("bin") + if py.std.sys.platform == "win32": + script = bindir.join("win32", scriptname + ".cmd") + else: + script = bindir.join(scriptname) + assert script.check() + return self.run(script, *args) + + def runpytest(self, *args): + return self.runpybin("py.test", *args) + + +class EventRecorder(object): + def __init__(self, pyplugins, debug=False): # True): + self.events = [] + self.pyplugins = pyplugins + self.debug = debug + pyplugins.register(self) + + def pyevent(self, name, *args, **kwargs): + if name == "plugin_registered" and args == (self,): + return + if self.debug: + print "[event] %s: %s **%s" %(name, ", ".join(map(str, args)), kwargs,) + if len(args) == 1: + event, = args + self.events.append((name, event)) + + def get(self, cls): + l = [] + for name, value in self.events: + if isinstance(value, cls): + l.append(value) + return l + + def getnamed(self, *names): + l = [] + for evname, event in self.events: + if evname in names: + l.append(event) + return l + + def getfirstnamed(self, name): + for evname, event in self.events: + if evname == name: + return event + + def getfailures(self, names='itemtestreport collectionreport'): + l = [] + for ev in self.getnamed(*names.split()): + if ev.failed: + l.append(ev) + return l + + def getfailedcollections(self): + return self.getfailures('collectionreport') + + def listoutcomes(self): + passed = [] + skipped = [] + failed = [] + for ev in self.getnamed('itemtestreport'): # , 'collectionreport'): + if ev.passed: + passed.append(ev) + elif ev.skipped: + skipped.append(ev) + elif ev.failed: + failed.append(ev) + return passed, skipped, failed + + def countoutcomes(self): + return map(len, self.listoutcomes()) + + def assertoutcome(self, passed=0, skipped=0, failed=0): + realpassed, realskipped, realfailed = self.listoutcomes() + assert passed == len(realpassed) + assert skipped == len(realskipped) + assert failed == len(realfailed) + + def getreport(self, inamepart): + """ return a testreport whose dotted import path matches """ + __tracebackhide__ = True + l = [] + for rep in self.get(event.ItemTestReport): + if inamepart in rep.colitem.listnames(): + l.append(rep) + if not l: + raise ValueError("could not find test report matching %r: no test reports at all!" % + (inamepart,)) + if len(l) > 1: + raise ValueError("found more than one testreport matching %r: %s" %( + inamepart, l)) + return l[0] + + def clear(self): + self.events[:] = [] + + def unregister(self): + self.pyplugins.unregister(self) + +def test_eventrecorder(): + bus = py._com.PyPlugins() + recorder = EventRecorder(bus) + bus.notify("anonymous", event.NOP()) + assert recorder.events + assert not recorder.getfailures() + rep = event.ItemTestReport(None, None) + rep.passed = False + rep.failed = True + bus.notify("itemtestreport", rep) + failures = recorder.getfailures() + assert failures == [rep] + failures = recorder.get(event.ItemTestReport) + assert failures == [rep] + failures = recorder.getnamed("itemtestreport") + assert failures == [rep] + + rep = event.ItemTestReport(None, None) + rep.passed = False + rep.skipped = True + bus.notify("itemtestreport", rep) + + rep = event.CollectionReport(None, None) + rep.passed = False + rep.failed = True + bus.notify("itemtestreport", rep) + + passed, skipped, failed = recorder.listoutcomes() + assert not passed and skipped and failed + + numpassed, numskipped, numfailed = recorder.countoutcomes() + assert numpassed == 0 + assert numskipped == 1 + assert numfailed == 2 + + recorder.clear() + assert not recorder.events + assert not recorder.getfailures() + recorder.unregister() + bus.notify("itemtestreport", rep) + assert not recorder.events + assert not recorder.getfailures() + +class LineComp: + def __init__(self): + self.stringio = py.std.StringIO.StringIO() + + def assert_contains_lines(self, lines2): + """ assert that lines2 are contained (linearly) in lines1. + return a list of extralines found. + """ + __tracebackhide__ = True + val = self.stringio.getvalue() + self.stringio.truncate(0) # remove what we got + lines1 = val.split("\n") + return LineMatcher(lines1).fnmatch_lines(lines2) + +class LineMatcher: + def __init__(self, lines): + self.lines = lines + + def str(self): + return "\n".join(self.lines) + + def fnmatch_lines(self, lines2): + if isinstance(lines2, str): + lines2 = py.code.Source(lines2) + if isinstance(lines2, py.code.Source): + lines2 = lines2.strip().lines + + from fnmatch import fnmatch + __tracebackhide__ = True + lines1 = self.lines[:] + nextline = None + extralines = [] + for line in lines2: + nomatchprinted = False + while lines1: + nextline = lines1.pop(0) + if line == nextline: + print "exact match:", repr(line) + break + elif fnmatch(nextline, line): + print "fnmatch:", repr(line) + print " with:", repr(nextline) + break + else: + if not nomatchprinted: + print "nomatch:", repr(line) + nomatchprinted = True + print " and:", repr(nextline) + extralines.append(nextline) + else: + if line != nextline: + #__tracebackhide__ = True + raise AssertionError("expected line not found: %r" % line) + extralines.extend(lines1) + return extralines + + diff --git a/py/test/plugin/pytest_restdoc.py b/py/test/plugin/pytest_restdoc.py new file mode 100644 index 000000000..f77c4ef14 --- /dev/null +++ b/py/test/plugin/pytest_restdoc.py @@ -0,0 +1,418 @@ +import py + +class RestdocPlugin: + def pytest_addoption(self, parser): + group = parser.addgroup("ReST", "ReST documentation check options") + group.addoption('-R', '--urlcheck', + action="store_true", dest="urlcheck", default=False, + help="urlopen() remote links found in ReST text files.") + group.addoption('--urlcheck-timeout', action="store", + type="int", dest="urlcheck_timeout", default=5, + help="timeout in seconds for urlcheck") + group.addoption('--forcegen', + action="store_true", dest="forcegen", default=False, + help="force generation of html files even if they appear up-to-date") + + def pytest_collect_file(self, path, parent): + if path.ext == ".txt": + project = getproject(path) + if project is not None: + return ReSTFile(path, parent=parent, project=project) + +def getproject(path): + for parent in path.parts(reverse=True): + confrest = parent.join("confrest.py") + if confrest.check(): + Project = confrest.pyimport().Project + return Project(parent.dirpath()) + +class ReSTFile(py.test.collect.File): + def __init__(self, fspath, parent, project=None): + super(ReSTFile, self).__init__(fspath=fspath, parent=parent) + if project is None: + project = getproject(fspath) + assert project is not None + self.project = project + + def collect(self): + return [ + ReSTSyntaxTest(self.project, "ReSTSyntax", parent=self), + LinkCheckerMaker("checklinks", parent=self), + DoctestText("doctest", parent=self), + ] + +def deindent(s, sep='\n'): + leastspaces = -1 + lines = s.split(sep) + for line in lines: + if not line.strip(): + continue + spaces = len(line) - len(line.lstrip()) + if leastspaces == -1 or spaces < leastspaces: + leastspaces = spaces + if leastspaces == -1: + return s + for i, line in py.builtin.enumerate(lines): + if not line.strip(): + lines[i] = '' + else: + lines[i] = line[leastspaces:] + return sep.join(lines) + +class ReSTSyntaxTest(py.test.collect.Item): + def __init__(self, project, *args, **kwargs): + super(ReSTSyntaxTest, self).__init__(*args, **kwargs) + self.project = project + + def runtest(self): + self.restcheck(py.path.svnwc(self.fspath)) + + def restcheck(self, path): + py.test.importorskip("docutils") + self.register_linkrole() + from docutils.utils import SystemMessage + try: + self._checkskip(path, self.project.get_htmloutputpath(path)) + self.project.process(path) + except KeyboardInterrupt: + raise + except SystemMessage: + # we assume docutils printed info on stdout + py.test.fail("docutils processing failed, see captured stderr") + + def register_linkrole(self): + from py.__.rest import directive + directive.register_linkrole('api', self.resolve_linkrole) + directive.register_linkrole('source', self.resolve_linkrole) + + def resolve_linkrole(self, name, text, check=True): + apigen_relpath = self.project.apigen_relpath + + if name == 'api': + if text == 'py': + return ('py', apigen_relpath + 'api/index.html') + else: + assert text.startswith('py.'), ( + 'api link "%s" does not point to the py package') % (text,) + dotted_name = text + if dotted_name.find('(') > -1: + dotted_name = dotted_name[:text.find('(')] + # remove pkg root + path = dotted_name.split('.')[1:] + dotted_name = '.'.join(path) + obj = py + if check: + for chunk in path: + try: + obj = getattr(obj, chunk) + except AttributeError: + raise AssertionError( + 'problem with linkrole :api:`%s`: can not resolve ' + 'dotted name %s' % (text, dotted_name,)) + return (text, apigen_relpath + 'api/%s.html' % (dotted_name,)) + elif name == 'source': + assert text.startswith('py/'), ('source link "%s" does not point ' + 'to the py package') % (text,) + relpath = '/'.join(text.split('/')[1:]) + if check: + pkgroot = py.__pkg__.getpath() + abspath = pkgroot.join(relpath) + assert pkgroot.join(relpath).check(), ( + 'problem with linkrole :source:`%s`: ' + 'path %s does not exist' % (text, relpath)) + if relpath.endswith('/') or not relpath: + relpath += 'index.html' + else: + relpath += '.html' + return (text, apigen_relpath + 'source/%s' % (relpath,)) + + def _checkskip(self, lpath, htmlpath=None): + if not self._config.getvalue("forcegen"): + lpath = py.path.local(lpath) + if htmlpath is not None: + htmlpath = py.path.local(htmlpath) + if lpath.ext == '.txt': + htmlpath = htmlpath or lpath.new(ext='.html') + if htmlpath.check(file=1) and htmlpath.mtime() >= lpath.mtime(): + py.test.skip("html file is up to date, use --forcegen to regenerate") + #return [] # no need to rebuild + +class DoctestText(py.test.collect.Item): + def runtest(self): + content = self._normalize_linesep() + newcontent = self._config.pytestplugins.call_firstresult( + 'pytest_doctest_prepare_content', content=content) + if newcontent is not None: + content = newcontent + s = content + l = [] + prefix = '.. >>> ' + mod = py.std.types.ModuleType(self.fspath.purebasename) + skipchunk = False + for line in deindent(s).split('\n'): + stripped = line.strip() + if skipchunk and line.startswith(skipchunk): + print "skipping", line + continue + skipchunk = False + if stripped.startswith(prefix): + try: + exec py.code.Source(stripped[len(prefix):]).compile() in \ + mod.__dict__ + except ValueError, e: + if e.args and e.args[0] == "skipchunk": + skipchunk = " " * (len(line) - len(line.lstrip())) + else: + raise + else: + l.append(line) + docstring = "\n".join(l) + mod.__doc__ = docstring + failed, tot = py.compat.doctest.testmod(mod, verbose=1) + if failed: + py.test.fail("doctest %s: %s failed out of %s" %( + self.fspath, failed, tot)) + + def _normalize_linesep(self): + # XXX quite nasty... but it works (fixes win32 issues) + s = self.fspath.read() + linesep = '\n' + if '\r' in s: + if '\n' not in s: + linesep = '\r' + else: + linesep = '\r\n' + s = s.replace(linesep, '\n') + return s + +class LinkCheckerMaker(py.test.collect.Collector): + def collect(self): + return list(self.genlinkchecks()) + + def genlinkchecks(self): + path = self.fspath + # generating functions + args as single tests + timeout = self._config.getvalue("urlcheck_timeout") + for lineno, line in py.builtin.enumerate(path.readlines()): + line = line.strip() + if line.startswith('.. _'): + if line.startswith('.. _`'): + delim = '`:' + else: + delim = ':' + l = line.split(delim, 1) + if len(l) != 2: + continue + tryfn = l[1].strip() + name = "%s:%d" %(tryfn, lineno) + if tryfn.startswith('http:') or tryfn.startswith('https'): + if self._config.getvalue("urlcheck"): + yield CheckLink(name, parent=self, + args=(tryfn, path, lineno, timeout), callobj=urlcheck) + elif tryfn.startswith('webcal:'): + continue + else: + i = tryfn.find('#') + if i != -1: + checkfn = tryfn[:i] + else: + checkfn = tryfn + if checkfn.strip() and (1 or checkfn.endswith('.html')): + yield CheckLink(name, parent=self, + args=(tryfn, path, lineno), callobj=localrefcheck) + +class CheckLink(py.test.collect.Function): + def repr_metainfo(self): + return self.ReprMetaInfo(fspath=self.fspath, lineno=self._args[2], + modpath="checklink: %s" % (self._args[0],)) + def setup(self): + pass + def teardown(self): + pass + +def urlcheck(tryfn, path, lineno, TIMEOUT_URLOPEN): + old = py.std.socket.getdefaulttimeout() + py.std.socket.setdefaulttimeout(TIMEOUT_URLOPEN) + try: + try: + print "trying remote", tryfn + py.std.urllib2.urlopen(tryfn) + finally: + py.std.socket.setdefaulttimeout(old) + except (py.std.urllib2.URLError, py.std.urllib2.HTTPError), e: + if getattr(e, 'code', None) in (401, 403): # authorization required, forbidden + py.test.skip("%s: %s" %(tryfn, str(e))) + else: + py.test.fail("remote reference error %r in %s:%d\n%s" %( + tryfn, path.basename, lineno+1, e)) + +def localrefcheck(tryfn, path, lineno): + # assume it should be a file + i = tryfn.find('#') + if tryfn.startswith('javascript:'): + return # don't check JS refs + if i != -1: + anchor = tryfn[i+1:] + tryfn = tryfn[:i] + else: + anchor = '' + fn = path.dirpath(tryfn) + ishtml = fn.ext == '.html' + fn = ishtml and fn.new(ext='.txt') or fn + print "filename is", fn + if not fn.check(): # not ishtml or not fn.check(): + if not py.path.local(tryfn).check(): # the html could be there + py.test.fail("reference error %r in %s:%d" %( + tryfn, path.basename, lineno+1)) + if anchor: + source = unicode(fn.read(), 'latin1') + source = source.lower().replace('-', ' ') # aehem + + anchor = anchor.replace('-', ' ') + match2 = ".. _`%s`:" % anchor + match3 = ".. _%s:" % anchor + candidates = (anchor, match2, match3) + print "candidates", repr(candidates) + for line in source.split('\n'): + line = line.strip() + if line in candidates: + break + else: + py.test.fail("anchor reference error %s#%s in %s:%d" %( + tryfn, anchor, path.basename, lineno+1)) + + +# +# PLUGIN tests +# +def test_generic(plugintester): + plugintester.apicheck(RestdocPlugin) + +def test_deindent(): + assert deindent('foo') == 'foo' + assert deindent('foo\n bar') == 'foo\n bar' + assert deindent(' foo\n bar\n') == 'foo\nbar\n' + assert deindent(' foo\n\n bar\n') == 'foo\n\nbar\n' + assert deindent(' foo\n bar\n') == 'foo\n bar\n' + assert deindent(' foo\n bar\n') == ' foo\nbar\n' + +class TestApigenLinkRole: + disabled = True + # these tests are moved here from the former py/doc/conftest.py + def test_resolve_linkrole(self): + from py.__.doc.conftest import get_apigen_relpath + apigen_relpath = get_apigen_relpath() + + assert resolve_linkrole('api', 'py.foo.bar', False) == ( + 'py.foo.bar', apigen_relpath + 'api/foo.bar.html') + assert resolve_linkrole('api', 'py.foo.bar()', False) == ( + 'py.foo.bar()', apigen_relpath + 'api/foo.bar.html') + assert resolve_linkrole('api', 'py', False) == ( + 'py', apigen_relpath + 'api/index.html') + py.test.raises(AssertionError, 'resolve_linkrole("api", "foo.bar")') + assert resolve_linkrole('source', 'py/foo/bar.py', False) == ( + 'py/foo/bar.py', apigen_relpath + 'source/foo/bar.py.html') + assert resolve_linkrole('source', 'py/foo/', False) == ( + 'py/foo/', apigen_relpath + 'source/foo/index.html') + assert resolve_linkrole('source', 'py/', False) == ( + 'py/', apigen_relpath + 'source/index.html') + py.test.raises(AssertionError, 'resolve_linkrole("source", "/foo/bar/")') + + def test_resolve_linkrole_check_api(self): + assert resolve_linkrole('api', 'py.test.ensuretemp') + py.test.raises(AssertionError, "resolve_linkrole('api', 'py.foo.baz')") + + def test_resolve_linkrole_check_source(self): + assert resolve_linkrole('source', 'py/path/common.py') + py.test.raises(AssertionError, + "resolve_linkrole('source', 'py/foo/bar.py')") + + +def pytest_pyfuncarg_testdir(__call__, pyfuncitem): + testdir = __call__.execute(firstresult=True) + testdir.makepyfile(confrest="from py.__.misc.rest import Project") + testdir.plugins.append(RestdocPlugin()) + return testdir + +class TestDoctest: + def test_doctest_extra_exec(self, testdir): + xtxt = testdir.maketxtfile(x=""" + hello:: + .. >>> raise ValueError + >>> None + """) + sorter = testdir.inline_run(xtxt) + passed, skipped, failed = sorter.countoutcomes() + assert failed == 1 + + def test_doctest_basic(self, testdir): + xtxt = testdir.maketxtfile(x=""" + .. + >>> from os.path import abspath + + hello world + + >>> assert abspath + >>> i=3 + >>> print i + 3 + + yes yes + + >>> i + 3 + + end + """) + sorter = testdir.inline_run(xtxt) + passed, skipped, failed = sorter.countoutcomes() + assert failed == 0 + assert passed + skipped == 2 + + def test_doctest_eol(self, testdir): + ytxt = testdir.maketxtfile(y=".. >>> 1 + 1\r\n 2\r\n\r\n") + sorter = testdir.inline_run(ytxt) + passed, skipped, failed = sorter.countoutcomes() + assert failed == 0 + assert passed + skipped == 2 + + def test_doctest_indentation(self, testdir): + footxt = testdir.maketxtfile(foo= + '..\n >>> print "foo\\n bar"\n foo\n bar\n') + sorter = testdir.inline_run(footxt) + passed, skipped, failed = sorter.countoutcomes() + assert failed == 0 + assert skipped + passed == 2 + + def test_js_ignore(self, testdir): + xtxt = testdir.maketxtfile(xtxt=""" + `blah`_ + + .. _`blah`: javascript:some_function() + """) + sorter = testdir.inline_run(xtxt) + passed, skipped, failed = sorter.countoutcomes() + assert failed == 0 + assert skipped + passed == 3 + + def test_pytest_doctest_prepare_content(self, testdir): + l = [] + class MyPlugin: + def pytest_doctest_prepare_content(self, content): + l.append(content) + return content.replace("False", "True") + + testdir.plugins.append(MyPlugin()) + + xtxt = testdir.maketxtfile(x=""" + hello: + + >>> 2 == 2 + False + + """) + sorter = testdir.inline_run(xtxt) + assert len(l) == 1 + passed, skipped, failed = sorter.countoutcomes() + assert not failed and not skipped + assert passed >= 1 diff --git a/py/test/plugin/pytest_resultlog.py b/py/test/plugin/pytest_resultlog.py new file mode 100644 index 000000000..76abad5c5 --- /dev/null +++ b/py/test/plugin/pytest_resultlog.py @@ -0,0 +1,253 @@ +import py + +class ResultlogPlugin: + """resultlog plugin for machine-readable logging of test results. + Useful for buildbot integration code. + """ + def pytest_addoption(self, parser): + parser.addoption('--resultlog', action="store", dest="resultlog", + help="path for machine-readable result log") + + def pytest_configure(self, config): + resultlog = config.option.resultlog + if resultlog: + logfile = open(resultlog, 'w', 1) # line buffered + self.resultlog = ResultLog(logfile) + config.bus.register(self.resultlog) + + def pytest_unconfigure(self, config): + if hasattr(self, 'resultlog'): + self.resultlog.logfile.close() + del self.resultlog + #config.bus.unregister(self.resultlog) + +def generic_path(item): + chain = item.listchain() + gpath = [chain[0].name] + fspath = chain[0].fspath + fspart = False + for node in chain[1:]: + newfspath = node.fspath + if newfspath == fspath: + if fspart: + gpath.append(':') + fspart = False + else: + gpath.append('.') + else: + gpath.append('/') + fspart = True + name = node.name + if name[0] in '([': + gpath.pop() + gpath.append(name) + fspath = newfspath + return ''.join(gpath) + +class ResultLog(object): + def __init__(self, logfile): + self.logfile = logfile # preferably line buffered + + def write_log_entry(self, shortrepr, name, longrepr): + print >>self.logfile, "%s %s" % (shortrepr, name) + for line in longrepr.splitlines(): + print >>self.logfile, " %s" % line + + def getoutcomecodes(self, ev): + if isinstance(ev, ev.CollectionReport): + # encode pass/fail/skip indepedent of terminal reporting semantics + # XXX handle collection and item reports more uniformly + assert not ev.passed + if ev.failed: + code = "F" + elif ev.skipped: + code = "S" + longrepr = str(ev.longrepr.reprcrash) + else: + assert isinstance(ev, ev.ItemTestReport) + code = ev.shortrepr + if ev.passed: + longrepr = "" + elif ev.failed: + longrepr = str(ev.longrepr) + elif ev.skipped: + longrepr = str(ev.longrepr.reprcrash.message) + return code, longrepr + + def log_outcome(self, event): + if (not event.passed or isinstance(event, event.ItemTestReport)): + gpath = generic_path(event.colitem) + shortrepr, longrepr = self.getoutcomecodes(event) + self.write_log_entry(shortrepr, gpath, longrepr) + + def pyevent(self, eventname, event, *args, **kwargs): + if eventname == "itemtestreport": + self.log_outcome(event) + elif eventname == "collectionreport": + if not event.passed: + self.log_outcome(event) + elif eventname == "internalerror": + path = event.repr.reprcrash.path # fishing :( + self.write_log_entry('!', path, str(event.repr)) + +# =============================================================================== +# +# plugin tests +# +# =============================================================================== + +import os, StringIO + +def test_generic_path(): + from py.__.test.collect import Node, Item, FSCollector + p1 = Node('a', config='dummy') + assert p1.fspath is None + p2 = Node('B', parent=p1) + p3 = Node('()', parent = p2) + item = Item('c', parent = p3) + + res = generic_path(item) + assert res == 'a.B().c' + + p0 = FSCollector('proj/test', config='dummy') + p1 = FSCollector('proj/test/a', parent=p0) + p2 = Node('B', parent=p1) + p3 = Node('()', parent = p2) + p4 = Node('c', parent=p3) + item = Item('[1]', parent = p4) + + res = generic_path(item) + assert res == 'test/a:B().c[1]' + +def test_write_log_entry(): + reslog = ResultLog(None) + reslog.logfile = StringIO.StringIO() + reslog.write_log_entry('.', 'name', '') + entry = reslog.logfile.getvalue() + assert entry[-1] == '\n' + entry_lines = entry.splitlines() + assert len(entry_lines) == 1 + assert entry_lines[0] == '. name' + + reslog.logfile = StringIO.StringIO() + reslog.write_log_entry('s', 'name', 'Skipped') + entry = reslog.logfile.getvalue() + assert entry[-1] == '\n' + entry_lines = entry.splitlines() + assert len(entry_lines) == 2 + assert entry_lines[0] == 's name' + assert entry_lines[1] == ' Skipped' + + reslog.logfile = StringIO.StringIO() + reslog.write_log_entry('s', 'name', 'Skipped\n') + entry = reslog.logfile.getvalue() + assert entry[-1] == '\n' + entry_lines = entry.splitlines() + assert len(entry_lines) == 2 + assert entry_lines[0] == 's name' + assert entry_lines[1] == ' Skipped' + + reslog.logfile = StringIO.StringIO() + longrepr = ' tb1\n tb 2\nE tb3\nSome Error' + reslog.write_log_entry('F', 'name', longrepr) + entry = reslog.logfile.getvalue() + assert entry[-1] == '\n' + entry_lines = entry.splitlines() + assert len(entry_lines) == 5 + assert entry_lines[0] == 'F name' + assert entry_lines[1:] == [' '+line for line in longrepr.splitlines()] + + +class TestWithFunctionIntegration: + # XXX (hpk) i think that the resultlog plugin should + # provide a Parser object so that one can remain + # ignorant regarding formatting details. + def getresultlog(self, testdir, arg): + resultlog = testdir.tmpdir.join("resultlog") + args = ["--resultlog=%s" % resultlog] + [arg] + testdir.runpytest(*args) + return filter(None, resultlog.readlines(cr=0)) + + def test_collection_report(self, plugintester): + testdir = plugintester.testdir() + ok = testdir.makepyfile(test_collection_ok="") + skip = testdir.makepyfile(test_collection_skip="import py ; py.test.skip('hello')") + fail = testdir.makepyfile(test_collection_fail="XXX") + + lines = self.getresultlog(testdir, ok) + assert not lines + + lines = self.getresultlog(testdir, skip) + assert len(lines) == 2 + assert lines[0].startswith("S ") + assert lines[0].endswith("test_collection_skip.py") + assert lines[1].startswith(" ") + assert lines[1].endswith("test_collection_skip.py:1: Skipped: 'hello'") + + lines = self.getresultlog(testdir, fail) + assert lines + assert lines[0].startswith("F ") + assert lines[0].endswith("test_collection_fail.py"), lines[0] + for x in lines[1:]: + assert x.startswith(" ") + assert "XXX" in "".join(lines[1:]) + + def test_log_test_outcomes(self, plugintester): + testdir = plugintester.testdir() + mod = testdir.makepyfile(test_mod=""" + import py + def test_pass(): pass + def test_skip(): py.test.skip("hello") + def test_fail(): raise ValueError("val") + """) + lines = self.getresultlog(testdir, mod) + assert len(lines) >= 3 + assert lines[0].startswith(". ") + assert lines[0].endswith("test_pass") + assert lines[1].startswith("s "), lines[1] + assert lines[1].endswith("test_skip") + assert lines[2].find("hello") != -1 + + assert lines[3].startswith("F ") + assert lines[3].endswith("test_fail") + tb = "".join(lines[4:]) + assert tb.find("ValueError") != -1 + + def test_internal_exception(self): + # they are produced for example by a teardown failing + # at the end of the run + from py.__.test import event + try: + raise ValueError + except ValueError: + excinfo = event.InternalException() + reslog = ResultLog(StringIO.StringIO()) + reslog.pyevent("internalerror", excinfo) + entry = reslog.logfile.getvalue() + entry_lines = entry.splitlines() + + assert entry_lines[0].startswith('! ') + assert os.path.basename(__file__)[:-1] in entry_lines[0] #.py/.pyc + assert entry_lines[-1][0] == ' ' + assert 'ValueError' in entry + +def test_generic(plugintester, LineMatcher): + plugintester.apicheck(ResultlogPlugin) + testdir = plugintester.testdir() + testdir.makepyfile(""" + import py + def test_pass(): + pass + def test_fail(): + assert 0 + def test_skip(): + py.test.skip("") + """) + testdir.runpytest("--resultlog=result.log") + lines = testdir.tmpdir.join("result.log").readlines(cr=0) + LineMatcher(lines).fnmatch_lines([ + ". *:test_pass", + "F *:test_fail", + "s *:test_skip", + ]) + diff --git a/py/test/plugin/pytest_terminal.py b/py/test/plugin/pytest_terminal.py new file mode 100644 index 000000000..78af483fc --- /dev/null +++ b/py/test/plugin/pytest_terminal.py @@ -0,0 +1,628 @@ +import py +import sys + +class TerminalPlugin(object): + """ Report a test run to a terminal. """ + def pytest_addoption(self, parser): + parser.addoption('--collectonly', + action="store_true", dest="collectonly", + help="only collect tests, don't execute them."), + + def pytest_configure(self, config): + if config.option.collectonly: + self.reporter = CollectonlyReporter(config) + else: + self.reporter = TerminalReporter(config) + # XXX see remote.py's XXX + for attr in 'pytest_terminal_hasmarkup', 'pytest_terminal_fullwidth': + if hasattr(config, attr): + #print "SETTING TERMINAL OPTIONS", attr, getattr(config, attr) + name = attr.split("_")[-1] + assert hasattr(self.reporter._tw, name), name + setattr(self.reporter._tw, name, getattr(config, attr)) + config.bus.register(self.reporter) + +class TerminalReporter: + def __init__(self, config, file=None): + self.config = config + self.stats = {} + self.curdir = py.path.local() + if file is None: + file = py.std.sys.stdout + self._tw = py.io.TerminalWriter(file) + self.currentfspath = None + + def write_fspath_result(self, fspath, res): + if fspath != self.currentfspath: + self._tw.line() + relpath = self.curdir.bestrelpath(fspath) + self._tw.write(relpath + " ") + self.currentfspath = fspath + self._tw.write(res) + + def write_ensure_prefix(self, prefix, extra=""): + if self.currentfspath != prefix: + self._tw.line() + self.currentfspath = prefix + self._tw.write(prefix) + if extra: + self._tw.write(extra) + self.currentfspath = -2 + + def ensure_newline(self): + if self.currentfspath: + self._tw.line() + self.currentfspath = None + + def write_line(self, line, **markup): + line = str(line) + self.ensure_newline() + self._tw.line(line, **markup) + + def write_sep(self, sep, title=None, **markup): + self.ensure_newline() + self._tw.sep(sep, title, **markup) + + def getcategoryletterword(self, event): + res = self.config.pytestplugins.call_firstresult("pytest_report_teststatus", event=event) + if res: + return res + for cat in 'skipped failed passed ???'.split(): + if getattr(event, cat, None): + break + return cat, self.getoutcomeletter(event), self.getoutcomeword(event) + + def getoutcomeletter(self, event): + return event.shortrepr + + def getoutcomeword(self, event): + if event.passed: + return self._tw.markup("PASS", green=True) + elif event.failed: + return self._tw.markup("FAIL", red=True) + elif event.skipped: + return "SKIP" + else: + return self._tw.markup("???", red=True) + + def pyevent_internalerror(self, event): + for line in str(event.repr).split("\n"): + self.write_line("InternalException: " + line) + + def pyevent_hostgatewayready(self, event): + if self.config.option.verbose: + self.write_line("HostGatewayReady: %s" %(event.host,)) + + def pyevent_plugin_registered(self, plugin): + if self.config.option.traceconfig: + msg = "PLUGIN registered: %s" %(plugin,) + # XXX this event may happen during setup/teardown time + # which unfortunately captures our output here + # which garbles our output if we use self.write_line + self.write_line(msg) + + def pyevent_hostup(self, event): + d = event.platinfo.copy() + d['hostid'] = event.host.hostid + d['version'] = repr_pythonversion(d['sys.version_info']) + self.write_line("HOSTUP: %(hostid)s %(sys.platform)s " + "%(sys.executable)s - Python %(version)s" % + d) + + def pyevent_hostdown(self, event): + host = event.host + error = event.error + if error: + self.write_line("HostDown %s: %s" %(host.hostid, error)) + + def pyevent_itemstart(self, event): + if self.config.option.verbose: + info = event.item.repr_metainfo() + line = info.verboseline(basedir=self.curdir) + " " + extra = "" + if event.host: + extra = "-> " + event.host.hostid + self.write_ensure_prefix(line, extra) + else: + # ensure that the path is printed before the 1st test of + # a module starts running + fspath = event.item.fspath + self.write_fspath_result(fspath, "") + + def pyevent_rescheduleitems(self, event): + if self.config.option.debug: + self.write_sep("!", "RESCHEDULING %s " %(event.items,)) + + def pyevent_deselected(self, event): + self.stats.setdefault('deselected', []).append(event) + + def pyevent_itemtestreport(self, event): + fspath = event.colitem.fspath + cat, letter, word = self.getcategoryletterword(event) + self.stats.setdefault(cat, []).append(event) + if not self.config.option.verbose: + self.write_fspath_result(fspath, letter) + else: + info = event.colitem.repr_metainfo() + line = info.verboseline(basedir=self.curdir) + " " + self.write_ensure_prefix(line, word) + + def pyevent_collectionreport(self, event): + if not event.passed: + if event.failed: + self.stats.setdefault("failed", []).append(event) + msg = event.longrepr.reprcrash.message + self.write_fspath_result(event.colitem.fspath, "F") + elif event.skipped: + self.stats.setdefault("skipped", []).append(event) + self.write_fspath_result(event.colitem.fspath, "S") + + def pyevent_testrunstart(self, event): + self.write_sep("=", "test session starts", bold=True) + self._sessionstarttime = py.std.time.time() + rev = py.__pkg__.getrev() + self.write_line("using py lib: %s " % ( + py.path.local(py.__file__).dirpath(), rev)) + plugins = [] + for x in self.config.pytestplugins._plugins: + if isinstance(x, str) and x.startswith("pytest_"): + plugins.append(x[7:]) + else: + plugins.append(str(x)) # XXX display conftest plugins more nicely + plugins = ", ".join(plugins) + self.write_line("active plugins: %s" %(plugins,)) + for i, testarg in py.builtin.enumerate(self.config.args): + self.write_line("test object %d: %s" %(i+1, testarg)) + + def pyevent_testrunfinish(self, event): + self._tw.line("") + if event.exitstatus in (0, 1, 2): + self.summary_failures() + self.summary_skips() + self.config.pytestplugins.call_each("pytest_terminal_summary", terminalreporter=self) + if event.excrepr is not None: + self.summary_final_exc(event.excrepr) + if event.exitstatus == 2: + self.write_sep("!", "KEYBOARD INTERRUPT") + self.summary_deselected() + self.summary_stats() + + def pyevent_looponfailinginfo(self, event): + if event.failreports: + self.write_sep("#", "LOOPONFAILING", red=True) + for report in event.failreports: + try: + loc = report.longrepr.reprcrash + except AttributeError: + loc = str(report.longrepr)[:50] + self.write_line(loc, red=True) + self.write_sep("#", "waiting for changes") + for rootdir in event.rootdirs: + self.write_line("### Watching: %s" %(rootdir,), bold=True) + + # + # summaries for TestrunFinish + # + + def summary_failures(self): + if 'failed' in self.stats and self.config.option.tbstyle != "no": + self.write_sep("=", "FAILURES") + for ev in self.stats['failed']: + self.write_sep("_") + ev.toterminal(self._tw) + + def summary_stats(self): + session_duration = py.std.time.time() - self._sessionstarttime + + keys = "failed passed skipped deselected".split() + parts = [] + for key in keys: + val = self.stats.get(key, None) + if val: + parts.append("%d %s" %(len(val), key)) + line = ", ".join(parts) + # XXX coloring + self.write_sep("=", "%s in %.2f seconds" %(line, session_duration)) + + def summary_deselected(self): + if 'deselected' in self.stats: + self.write_sep("=", "%d tests deselected by %r" %( + len(self.stats['deselected']), self.config.option.keyword), bold=True) + + def summary_skips(self): + if 'skipped' in self.stats: + if 'failed' not in self.stats or self.config.option.showskipsummary: + fskips = folded_skips(self.stats['skipped']) + if fskips: + self.write_sep("_", "skipped test summary") + for num, fspath, lineno, reason in fskips: + self._tw.line("%s:%d: [%d] %s" %(fspath, lineno, num, reason)) + + def summary_final_exc(self, excrepr): + self.write_sep("!") + if self.config.option.verbose: + excrepr.toterminal(self._tw) + else: + excrepr.reprcrash.toterminal(self._tw) + + def out_hostinfo(self): + self._tw.line("host 0: %s %s - Python %s" % + (py.std.sys.platform, + py.std.sys.executable, + repr_pythonversion())) + +class CollectonlyReporter: + INDENT = " " + + def __init__(self, config, out=None): + self.config = config + if out is None: + out = py.std.sys.stdout + self.out = py.io.TerminalWriter(out) + self.indent = "" + self._failed = [] + + def outindent(self, line): + self.out.line(self.indent + str(line)) + + def pyevent_collectionstart(self, event): + self.outindent(event.collector) + self.indent += self.INDENT + + def pyevent_itemstart(self, event): + self.outindent(event.item) + + def pyevent_collectionreport(self, event): + if not event.passed: + self.outindent("!!! %s !!!" % event.longrepr.reprcrash.message) + self._failed.append(event) + self.indent = self.indent[:-len(self.INDENT)] + + def pyevent_testrunfinish(self, event): + if self._failed: + self.out.sep("!", "collection failures") + for event in self._failed: + event.toterminal(self.out) + +def folded_skips(skipped): + d = {} + for event in skipped: + entry = event.longrepr.reprcrash + key = entry.path, entry.lineno, entry.message + d.setdefault(key, []).append(event) + l = [] + for key, events in d.iteritems(): + l.append((len(events),) + key) + return l + +def repr_pythonversion(v=None): + if v is None: + v = sys.version_info + try: + return "%s.%s.%s-%s-%s" % v + except (TypeError, ValueError): + return str(v) + +# =============================================================================== +# +# plugin tests +# +# =============================================================================== + +from py.__.test import event +from py.__.test.runner import basic_run_report +from py.__.test.dsession.hostmanage import Host, makehostup + +class TestTerminal: + def test_hostup(self, testdir, linecomp): + item = testdir.getitem("def test_func(): pass") + rep = TerminalReporter(item._config, linecomp.stringio) + host = Host("localhost") + rep.pyevent_hostup(makehostup(host)) + linecomp.assert_contains_lines([ + "*%s %s %s - Python %s" %(host.hostid, sys.platform, + sys.executable, repr_pythonversion(sys.version_info)) + ]) + + def test_pass_skip_fail(self, testdir, linecomp): + modcol = testdir.getmodulecol(""" + import py + def test_ok(): + pass + def test_skip(): + py.test.skip("xx") + def test_func(): + assert 0 + """) + rep = TerminalReporter(modcol._config, file=linecomp.stringio) + rep.config.bus.register(rep) + rep.config.bus.notify("testrunstart", event.TestrunStart()) + + for item in testdir.genitems([modcol]): + ev = basic_run_report(item) + rep.config.bus.notify("itemtestreport", ev) + linecomp.assert_contains_lines([ + "*test_pass_skip_fail.py .sF" + ]) + rep.config.bus.notify("testrunfinish", event.TestrunFinish()) + linecomp.assert_contains_lines([ + " def test_func():", + "> assert 0", + "E assert 0", + ]) + + def test_pass_skip_fail_verbose(self, testdir, linecomp): + modcol = testdir.getmodulecol(""" + import py + def test_ok(): + pass + def test_skip(): + py.test.skip("xx") + def test_func(): + assert 0 + """, configargs=("-v",)) + rep = TerminalReporter(modcol._config, file=linecomp.stringio) + rep.config.bus.register(rep) + rep.config.bus.notify("testrunstart", event.TestrunStart()) + items = modcol.collect() + for item in items: + rep.config.bus.notify("itemstart", event.ItemStart(item)) + s = linecomp.stringio.getvalue().strip() + assert s.endswith(item.name) + rep.config.bus.notify("itemtestreport", basic_run_report(item)) + + linecomp.assert_contains_lines([ + "*test_pass_skip_fail_verbose.py:2: *test_ok*PASS", + "*test_pass_skip_fail_verbose.py:4: *test_skip*SKIP", + "*test_pass_skip_fail_verbose.py:6: *test_func*FAIL", + ]) + rep.config.bus.notify("testrunfinish", event.TestrunFinish()) + linecomp.assert_contains_lines([ + " def test_func():", + "> assert 0", + "E assert 0", + ]) + + def test_collect_fail(self, testdir, linecomp): + modcol = testdir.getmodulecol("import xyz") + rep = TerminalReporter(modcol._config, file=linecomp.stringio) + rep.config.bus.register(rep) + rep.config.bus.notify("testrunstart", event.TestrunStart()) + l = list(testdir.genitems([modcol])) + assert len(l) == 0 + linecomp.assert_contains_lines([ + "*test_collect_fail.py F*" + ]) + rep.config.bus.notify("testrunfinish", event.TestrunFinish()) + linecomp.assert_contains_lines([ + "> import xyz", + "E ImportError: No module named xyz" + ]) + + def test_internalerror(self, testdir, linecomp): + modcol = testdir.getmodulecol("def test_one(): pass") + rep = TerminalReporter(modcol._config, file=linecomp.stringio) + excinfo = py.test.raises(ValueError, "raise ValueError('hello')") + rep.pyevent_internalerror(event.InternalException(excinfo)) + linecomp.assert_contains_lines([ + "InternalException: >*raise ValueError*" + ]) + + def test_hostready_crash(self, testdir, linecomp): + modcol = testdir.getmodulecol(""" + def test_one(): + pass + """, configargs=("-v",)) + host1 = Host("localhost") + rep = TerminalReporter(modcol._config, file=linecomp.stringio) + rep.pyevent_hostgatewayready(event.HostGatewayReady(host1, None)) + linecomp.assert_contains_lines([ + "*HostGatewayReady*" + ]) + rep.pyevent_hostdown(event.HostDown(host1, "myerror")) + linecomp.assert_contains_lines([ + "*HostDown*myerror*", + ]) + + def test_writeline(self, testdir, linecomp): + modcol = testdir.getmodulecol("def test_one(): pass") + stringio = py.std.cStringIO.StringIO() + rep = TerminalReporter(modcol._config, file=linecomp.stringio) + rep.write_fspath_result(py.path.local("xy.py"), '.') + rep.write_line("hello world") + lines = linecomp.stringio.getvalue().split('\n') + assert not lines[0] + assert lines[1].endswith("xy.py .") + assert lines[2] == "hello world" + + def test_looponfailingreport(self, testdir, linecomp): + modcol = testdir.getmodulecol(""" + def test_fail(): + assert 0 + def test_fail2(): + raise ValueError() + """) + rep = TerminalReporter(modcol._config, file=linecomp.stringio) + reports = [basic_run_report(x) for x in modcol.collect()] + rep.pyevent_looponfailinginfo(event.LooponfailingInfo(reports, [modcol._config.topdir])) + linecomp.assert_contains_lines([ + "*test_looponfailingreport.py:2: assert 0", + "*test_looponfailingreport.py:4: ValueError*", + "*waiting*", + "*%s*" % (modcol._config.topdir), + ]) + + def test_tb_option(self, testdir, linecomp): + # XXX usage of testdir and event bus + for tbopt in ["long", "short", "no"]: + print 'testing --tb=%s...' % tbopt + modcol = testdir.getmodulecol(""" + import py + def g(): + raise IndexError + def test_func(): + print 6*7 + g() # --calling-- + """, configargs=("--tb=%s" % tbopt,)) + rep = TerminalReporter(modcol._config, file=linecomp.stringio) + rep.config.bus.register(rep) + rep.config.bus.notify("testrunstart", event.TestrunStart()) + rep.config.bus.notify("testrunstart", event.TestrunStart()) + for item in testdir.genitems([modcol]): + rep.config.bus.notify("itemtestreport", basic_run_report(item)) + rep.config.bus.notify("testrunfinish", event.TestrunFinish()) + s = linecomp.stringio.getvalue() + if tbopt == "long": + print s + assert 'print 6*7' in s + else: + assert 'print 6*7' not in s + if tbopt != "no": + assert '--calling--' in s + assert 'IndexError' in s + else: + assert 'FAILURES' not in s + assert '--calling--' not in s + assert 'IndexError' not in s + linecomp.stringio.truncate(0) + + def test_show_path_before_running_test(self, testdir, linecomp): + modcol = testdir.getmodulecol(""" + def test_foobar(): + pass + """) + rep = TerminalReporter(modcol._config, file=linecomp.stringio) + modcol._config.bus.register(rep) + l = list(testdir.genitems([modcol])) + assert len(l) == 1 + rep.config.bus.notify("itemstart", event.ItemStart(l[0])) + linecomp.assert_contains_lines([ + "*test_show_path_before_running_test.py*" + ]) + + def pseudo_keyboard_interrupt(self, testdir, linecomp, verbose=False): + modcol = testdir.getmodulecol(""" + def test_foobar(): + assert 0 + def test_spamegg(): + import py; py.test.skip('skip me please!') + def test_interrupt_me(): + raise KeyboardInterrupt # simulating the user + """, configargs=("--showskipsummary",) + ("-v",)*verbose) + rep = TerminalReporter(modcol._config, file=linecomp.stringio) + modcol._config.bus.register(rep) + bus = modcol._config.bus + bus.notify("testrunstart", event.TestrunStart()) + try: + for item in testdir.genitems([modcol]): + bus.notify("itemtestreport", basic_run_report(item)) + except KeyboardInterrupt: + excinfo = py.code.ExceptionInfo() + else: + py.test.fail("no KeyboardInterrupt??") + s = linecomp.stringio.getvalue() + if not verbose: + assert s.find("_keyboard_interrupt.py Fs") != -1 + bus.notify("testrunfinish", event.TestrunFinish(exitstatus=2, excinfo=excinfo)) + text = linecomp.stringio.getvalue() + linecomp.assert_contains_lines([ + " def test_foobar():", + "> assert 0", + "E assert 0", + ]) + assert "Skipped: 'skip me please!'" in text + assert "_keyboard_interrupt.py:6: KeyboardInterrupt" in text + see_details = "raise KeyboardInterrupt # simulating the user" in text + assert see_details == verbose + + def test_keyboard_interrupt(self, testdir, linecomp): + self.pseudo_keyboard_interrupt(testdir, linecomp) + + def test_verbose_keyboard_interrupt(self, testdir, linecomp): + self.pseudo_keyboard_interrupt(testdir, linecomp, verbose=True) + + def test_skip_reasons_folding(self): + class longrepr: + class reprcrash: + path = 'xyz' + lineno = 3 + message = "justso" + + ev1 = event.CollectionReport(None, None) + ev1.when = "execute" + ev1.skipped = True + ev1.longrepr = longrepr + + ev2 = event.ItemTestReport(None, excinfo=longrepr) + ev2.skipped = True + + l = folded_skips([ev1, ev2]) + assert len(l) == 1 + num, fspath, lineno, reason = l[0] + assert num == 2 + assert fspath == longrepr.reprcrash.path + assert lineno == longrepr.reprcrash.lineno + assert reason == longrepr.reprcrash.message + +class TestCollectonly: + def test_collectonly_basic(self, testdir, linecomp): + modcol = testdir.getmodulecol(configargs=['--collectonly'], source=""" + def test_func(): + pass + """) + rep = CollectonlyReporter(modcol._config, out=linecomp.stringio) + modcol._config.bus.register(rep) + indent = rep.indent + rep.config.bus.notify("collectionstart", event.CollectionStart(modcol)) + linecomp.assert_contains_lines([ + "" + ]) + item = modcol.join("test_func") + rep.config.bus.notify("itemstart", event.ItemStart(item)) + linecomp.assert_contains_lines([ + " ", + ]) + rep.config.bus.notify( "collectionreport", + event.CollectionReport(modcol, [], excinfo=None)) + assert rep.indent == indent + + def test_collectonly_skipped_module(self, testdir, linecomp): + modcol = testdir.getmodulecol(configargs=['--collectonly'], source=""" + import py + py.test.skip("nomod") + """) + rep = CollectonlyReporter(modcol._config, out=linecomp.stringio) + modcol._config.bus.register(rep) + cols = list(testdir.genitems([modcol])) + assert len(cols) == 0 + linecomp.assert_contains_lines(""" + + !!! Skipped: 'nomod' !!! + """) + + def test_collectonly_failed_module(self, testdir, linecomp): + modcol = testdir.getmodulecol(configargs=['--collectonly'], source=""" + raise ValueError(0) + """) + rep = CollectonlyReporter(modcol._config, out=linecomp.stringio) + modcol._config.bus.register(rep) + cols = list(testdir.genitems([modcol])) + assert len(cols) == 0 + linecomp.assert_contains_lines(""" + + !!! ValueError: 0 !!! + """) + +def test_repr_python_version(): + py.magic.patch(sys, 'version_info', (2, 5, 1, 'final', 0)) + try: + assert repr_pythonversion() == "2.5.1-final-0" + py.std.sys.version_info = x = (2,3) + assert repr_pythonversion() == str(x) + finally: + py.magic.revert(sys, 'version_info') + +def test_generic(plugintester): + plugintester.apicheck(TerminalPlugin) + plugintester.apicheck(TerminalReporter) + plugintester.apicheck(CollectonlyReporter) diff --git a/py/test/plugin/pytest_tmpdir.py b/py/test/plugin/pytest_tmpdir.py new file mode 100644 index 000000000..53b7a8000 --- /dev/null +++ b/py/test/plugin/pytest_tmpdir.py @@ -0,0 +1,45 @@ +""" +example: + + pytest_plugins = "pytest_tmpdir" + + def test_plugin(tmpdir): + tmpdir.join("hello").write("hello") + +""" +import py + +class TmpdirPlugin: + """ provide temporary directories to test functions and methods. + """ + + def pytest_configure(self, config): + # XXX make ensuretemp live on config + self.basetmp = py.test.ensuretemp("tmpdir") + + def pytest_pyfuncarg_tmpdir(self, pyfuncitem): + name = pyfuncitem.name + for i in range(10000): + try: + tmpdir = self.basetmp.mkdir(name + (i and str(i) or '')) + except py.error.EEXIST: + continue + break + return tmpdir + +# =============================================================================== +# +# plugin tests +# +# =============================================================================== +# +def test_generic(plugintester): + plugintester.apicheck(TmpdirPlugin) + +def test_pyfuncarg(testdir): + item = testdir.getitem("def test_func(tmpdir): pass") + plugin = TmpdirPlugin() + plugin.pytest_configure(item._config) + p = plugin.pytest_pyfuncarg_tmpdir(item) + assert p.check() + assert p.basename.endswith("test_func") diff --git a/py/test/plugin/pytest_unittest.py b/py/test/plugin/pytest_unittest.py new file mode 100644 index 000000000..000399825 --- /dev/null +++ b/py/test/plugin/pytest_unittest.py @@ -0,0 +1,124 @@ +""" +automatically collect and run traditional "unittest.py" style tests. + +you can mix unittest TestCase subclasses and +py.test style tests in one test module. + +XXX consider user-specified test_suite() + +this code is somewhat derived from Guido Wesdorps + + http://johnnydebris.net/svn/projects/py_unittest + +$HeadURL: https://codespeak.net/svn/py/branch/pytestplugin/contrib/py_unittest/conftest.py $ +$Id: conftest.py 60979 2009-01-14 22:29:32Z hpk $ +""" +import py + +class UnittestPlugin: + """ discover and integrate traditional ``unittest.py`` tests. + """ + def pytest_pymodule_makeitem(self, modcol, name, obj): + if py.std.inspect.isclass(obj) and issubclass(obj, py.std.unittest.TestCase): + return UnitTestCase(name, parent=modcol) + +class UnitTestCase(py.test.collect.Class): + def collect(self): + return [UnitTestCaseInstance("()", self)] + + def setup(self): + pass + + def teardown(self): + pass + +_dummy = object() +class UnitTestCaseInstance(py.test.collect.Instance): + def collect(self): + loader = py.std.unittest.TestLoader() + names = loader.getTestCaseNames(self.obj.__class__) + l = [] + for name in names: + callobj = getattr(self.obj, name) + if callable(callobj): + l.append(UnitTestFunction(name, parent=self)) + return l + + def _getobj(self): + x = self.parent.obj + return self.parent.obj(methodName='run') + +class UnitTestFunction(py.test.collect.Function): + def __init__(self, name, parent, args=(), obj=_dummy, sort_value=None): + super(UnitTestFunction, self).__init__(name, parent) + self._args = args + if obj is not _dummy: + self._obj = obj + self._sort_value = sort_value + + def runtest(self): + target = self.obj + args = self._args + target(*args) + + def setup(self): + instance = self.obj.im_self + instance.setUp() + + def teardown(self): + instance = self.obj.im_self + instance.tearDown() + + +def test_generic(plugintester): + plugintester.apicheck(UnittestPlugin) + +def test_simple_unittest(testdir): + testpath = testdir.makepyfile(""" + import unittest + pytest_plugins = "pytest_unittest" + class MyTestCase(unittest.TestCase): + def testpassing(self): + self.assertEquals('foo', 'foo') + def test_failing(self): + self.assertEquals('foo', 'bar') + """) + sorter = testdir.inline_run(testpath) + assert sorter.getreport("testpassing").passed + assert sorter.getreport("test_failing").failed + +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 + def test_setUp(self): + self.assertEquals(1, self.foo) + """) + sorter = testdir.inline_run(testpath) + rep = sorter.getreport("test_setUp") + assert rep.passed + +def test_teardown(testdir): + testpath = testdir.makepyfile(test_three=""" + import unittest + pytest_plugins = "pytest_unittest" # XXX + class MyTestCase(unittest.TestCase): + l = [] + def test_one(self): + pass + def tearDown(self): + self.l.append(None) + class Second(unittest.TestCase): + def test_check(self): + self.assertEquals(MyTestCase.l, [None]) + """) + sorter = testdir.inline_run(testpath) + passed, skipped, failed = sorter.countoutcomes() + print "COUNTS", passed, skipped, failed + assert failed == 0, failed + assert passed == 2 + assert passed + skipped + failed == 2 + diff --git a/py/test/plugin/pytest_xfail.py b/py/test/plugin/pytest_xfail.py new file mode 100644 index 000000000..9926bdd3d --- /dev/null +++ b/py/test/plugin/pytest_xfail.py @@ -0,0 +1,63 @@ +""" +for marking and reporting "expected to fail" tests. + @py.test.keywords(xfail="needs refactoring") + def test_hello(): + ... + assert 0 +""" +import py + +class XfailPlugin(object): + """ mark and report specially about "expected to fail" tests. """ + def pytest_report_teststatus(self, event): + """ return shortletter and verbose word. """ + if 'xfail' in event.keywords: + if event.failed: + return "xfailed", "x", "xfail" + else: + return "xpassed", "P", "xpass" + + # a hook implemented called by the terminalreporter instance/plugin + def pytest_terminal_summary(self, terminalreporter): + tr = terminalreporter + xfailed = tr.stats.get("xfailed") + if xfailed: + tr.write_sep("_", "EXPECTED XFAILURES") + for event in xfailed: + entry = event.longrepr.reprcrash + key = entry.path, entry.lineno, entry.message + reason = event.longrepr.reprcrash.message + modpath = event.colitem.getmodpath(includemodule=True) + #tr._tw.line("%s %s:%d: %s" %(modpath, entry.path, entry.lineno, entry.message)) + tr._tw.line("%s %s:%d: " %(modpath, entry.path, entry.lineno)) + + xpassed = terminalreporter.stats.get("xpassed") + if xpassed: + tr.write_sep("_", "UNEXPECTEDLY PASSING") + for event in xpassed: + tr._tw.line("%s: xpassed" %(event.colitem,)) + +# =============================================================================== +# +# plugin tests +# +# =============================================================================== + +def test_generic(plugintester): + plugintester.apicheck(XfailPlugin) + +def test_xfail(plugintester, linecomp): + testdir = plugintester.testdir() + p = testdir.makepyfile(test_one=""" + import py + pytest_plugins="pytest_xfail", + @py.test.keywords(xfail=True) + def test_this(): + assert 0 + """) + result = testdir.runpytest(p) + extra = result.stdout.fnmatch_lines([ + "*XFAILURES*", + "*test_one.test_this*test_one.py:5*", + ]) + assert result.ret == 1 diff --git a/py/test/pycollect.py b/py/test/pycollect.py index 91e820e0b..6b7cfb2c9 100644 --- a/py/test/pycollect.py +++ b/py/test/pycollect.py @@ -36,7 +36,7 @@ class PyobjMixin(object): def _getobj(self): return getattr(self.parent.obj, self.name) - def getmodpath(self, stopatmodule=True): + def getmodpath(self, stopatmodule=True, includemodule=False): """ return python path relative to the containing module. """ chain = self.listchain() chain.reverse() @@ -46,10 +46,12 @@ class PyobjMixin(object): continue name = node.name if isinstance(node, Module): - if stopatmodule: - break assert name.endswith(".py") name = name[:-3] + if stopatmodule: + if includemodule: + parts.append(name) + break parts.append(name) parts.reverse() s = ".".join(parts) @@ -136,14 +138,18 @@ class PyCollectorMixin(PyobjMixin, py.test.collect.Collector): warnoldcollect() return self.join(name) - def makeitem(self, name, obj, usefilters=True): - if (not usefilters or self.classnamefilter(name)) and \ + def makeitem(self, name, obj): + res = self._config.pytestplugins.call_firstresult( + "pytest_pymodule_makeitem", modcol=self, name=name, obj=obj) + if res: + return res + if (self.classnamefilter(name)) and \ py.std.inspect.isclass(obj): res = self._deprecated_join(name) if res is not None: return res return self.Class(name, parent=self) - elif (not usefilters or self.funcnamefilter(name)) and callable(obj): + elif self.funcnamefilter(name) and callable(obj): res = self._deprecated_join(name) if res is not None: return res @@ -159,14 +165,23 @@ class Module(py.test.collect.File, PyCollectorMixin): return super(Module, self).collect() def _getobj(self): - return self._memoizedcall('_obj', self.fspath.pyimport) + return self._memoizedcall('_obj', self._importtestmodule) + + def _importtestmodule(self): + # we assume we are only called once per module + mod = self.fspath.pyimport() + #print "imported test module", mod + self._config.pytestplugins.consider_module(mod) + return mod def setup(self): if not self._config.option.nomagic: #print "*" * 20, "INVOKE assertion", self py.magic.invoke(assertion=1) - if hasattr(self.obj, 'setup_module'): - self.obj.setup_module(self.obj) + mod = self.obj + self._config.pytestplugins.register(mod) + if hasattr(mod, 'setup_module'): + self.obj.setup_module(mod) def teardown(self): if hasattr(self.obj, 'teardown_module'): @@ -174,6 +189,7 @@ class Module(py.test.collect.File, PyCollectorMixin): if not self._config.option.nomagic: #print "*" * 20, "revoke assertion", self py.magic.revoke(assertion=1) + self._config.pytestplugins.unregister(self.obj) class Class(PyCollectorMixin, py.test.collect.Collector): @@ -305,14 +321,57 @@ class Function(FunctionMixin, py.test.collect.Item): """ def __init__(self, name, parent=None, config=None, args=(), callobj=_dummy): super(Function, self).__init__(name, parent, config=config) + self._finalizers = [] self._args = args if callobj is not _dummy: self._obj = callobj + def addfinalizer(self, func): + self._finalizers.append(func) + + def teardown(self): + finalizers = self._finalizers + while finalizers: + call = finalizers.pop() + call() + super(Function, self).teardown() + + def readkeywords(self): + d = super(Function, self).readkeywords() + d.update(self.obj.func_dict) + return d + def runtest(self): """ execute the given test function. """ if not self._deprecated_testexecution(): - self.obj(*self._args) + kw = self.lookup_allargs() + pytest_pyfunc_call = self._config.pytestplugins.getfirst("pytest_pyfunc_call") + if pytest_pyfunc_call is not None: + if pytest_pyfunc_call(pyfuncitem=self, args=self._args, kwargs=kw): + return + self.obj(*self._args, **kw) + + def lookup_allargs(self): + kwargs = {} + if not self._args: + # standard Python Test function/method case + funcobj = self.obj + startindex = getattr(funcobj, 'im_self', None) and 1 or 0 + for argname in py.std.inspect.getargs(self.obj.func_code)[0][startindex:]: + kwargs[argname] = self.lookup_onearg(argname) + else: + pass # XXX lookup of arguments for yielded/generated tests as well + return kwargs + + def lookup_onearg(self, argname): + value = self._config.pytestplugins.call_firstresult( + "pytest_pyfuncarg_" + argname, pyfuncitem=self) + if value is not None: + return value + else: + metainfo = self.repr_metainfo() + #self._config.bus.notify("pyfuncarg_lookuperror", argname) + raise LookupError("funcargument %r not found for: %s" %(argname,metainfo.verboseline())) def __eq__(self, other): try: @@ -326,50 +385,6 @@ class Function(FunctionMixin, py.test.collect.Item): def __ne__(self, other): return not self == other -class DoctestFile(py.test.collect.File): - - def collect(self): - return [DoctestFileContent(self.fspath.basename, parent=self)] - -from py.__.code.excinfo import Repr, ReprFileLocation - -class ReprFailDoctest(Repr): - def __init__(self, reprlocation, lines): - self.reprlocation = reprlocation - self.lines = lines - def toterminal(self, tw): - for line in self.lines: - tw.line(line) - self.reprlocation.toterminal(tw) - -class DoctestFileContent(py.test.collect.Item): - def repr_failure(self, excinfo, outerr): - if excinfo.errisinstance(py.compat.doctest.DocTestFailure): - doctestfailure = excinfo.value - example = doctestfailure.example - test = doctestfailure.test - filename = test.filename - lineno = example.lineno + 1 - message = excinfo.type.__name__ - reprlocation = ReprFileLocation(filename, lineno, message) - checker = py.compat.doctest.OutputChecker() - REPORT_UDIFF = py.compat.doctest.REPORT_UDIFF - filelines = py.path.local(filename).readlines(cr=0) - i = max(0, lineno - 10) - lines = [] - for line in filelines[i:lineno]: - lines.append("%03d %s" % (i+1, line)) - i += 1 - lines += checker.output_difference(example, - doctestfailure.got, REPORT_UDIFF).split("\n") - return ReprFailDoctest(reprlocation, lines) - #elif excinfo.errisinstance(py.compat.doctest.UnexpectedException): - else: - return super(DoctestFileContent, self).repr_failure(excinfo, outerr) - - def runtest(self): - if not self._deprecated_testexecution(): - failed, tot = py.compat.doctest.testfile( - str(self.fspath), module_relative=False, - raise_on_error=True, verbose=0) +# DEPRECATED +#from py.__.test.plugin.pytest_doctest import DoctestFile diff --git a/py/test/pytestplugin.py b/py/test/pytestplugin.py new file mode 100644 index 000000000..1a9dc16c7 --- /dev/null +++ b/py/test/pytestplugin.py @@ -0,0 +1,123 @@ +""" +handling py.test plugins. +""" +import py + +class PytestPlugins(object): + def __init__(self, pyplugins=None): + if pyplugins is None: + pyplugins = py._com.PyPlugins() + self.pyplugins = pyplugins + self.MultiCall = self.pyplugins.MultiCall + self._plugins = {} + + def register(self, plugin): + self.pyplugins.register(plugin) + def unregister(self, plugin): + self.pyplugins.unregister(plugin) + def isregistered(self, plugin): + return self.pyplugins.isregistered(plugin) + + def getplugins(self): + return self.pyplugins.getplugins() + + # API for bootstrapping + # + def getplugin(self, importname): + impname, clsname = canonical_names(importname) + return self._plugins[impname] + + def consider_env(self): + for spec in self.pyplugins._envlist("PYTEST_PLUGINS"): + self.import_plugin(spec) + + def consider_conftest(self, conftestmodule): + cls = getattr(conftestmodule, 'ConftestPlugin', None) + if cls is not None and cls not in self._plugins: + self._plugins[cls] = True + self.register(cls()) + self.consider_module(conftestmodule) + + def consider_module(self, mod): + attr = getattr(mod, "pytest_plugins", ()) + if attr: + if not isinstance(attr, (list, tuple)): + attr = (attr,) + for spec in attr: + self.import_plugin(spec) + + def import_plugin(self, spec): + assert isinstance(spec, str) + modname, clsname = canonical_names(spec) + if modname in self._plugins: + return + mod = importplugin(modname) + plugin = registerplugin(self.pyplugins.register, mod, clsname) + self._plugins[modname] = plugin + self.consider_module(mod) + # + # + # API for interacting with registered and instantiated plugin objects + # + # + def getfirst(self, attrname): + for x in self.pyplugins.listattr(attrname): + return x + + def listattr(self, attrname): + return self.pyplugins.listattr(attrname) + + def call_firstresult(self, *args, **kwargs): + return self.pyplugins.call_firstresult(*args, **kwargs) + + def call_each(self, *args, **kwargs): + #print "plugins.call_each", args[0], args[1:], kwargs + return self.pyplugins.call_each(*args, **kwargs) + + def notify(self, eventname, *args, **kwargs): + return self.pyplugins.notify(eventname, *args, **kwargs) + + def do_addoption(self, parser): + self.pyplugins.call_each('pytest_addoption', parser=parser) + + def pyevent_plugin_registered(self, plugin): + if hasattr(self, '_config'): + self.pyplugins.call_plugin(plugin, "pytest_addoption", parser=self._config._parser) + self.pyplugins.call_plugin(plugin, "pytest_configure", config=self._config) + + def configure(self, config): + assert not hasattr(self, '_config') + config.bus.register(self) + self._config = config + self.pyplugins.call_each("pytest_configure", config=self._config) + + def unconfigure(self, config): + config = self._config + del self._config + self.pyplugins.call_each("pytest_unconfigure", config=config) + +# +# XXX old code to automatically load classes +# +def canonical_names(importspec): + importspec = importspec.lower() + modprefix = "pytest_" + if not importspec.startswith(modprefix): + importspec = modprefix + importspec + clsname = importspec[len(modprefix):].capitalize() + "Plugin" + return importspec, clsname + +def registerplugin(registerfunc, mod, clsname): + pluginclass = getattr(mod, clsname) + plugin = pluginclass() + registerfunc(plugin) + return plugin + +def importplugin(importspec): + try: + return __import__(importspec) + except ImportError, e: + try: + return __import__("py.__.test.plugin.%s" %(importspec), None, None, '__doc__') + except ImportError: + raise ImportError(importspec) diff --git a/py/test/report/base.py b/py/test/report/base.py deleted file mode 100644 index e50887435..000000000 --- a/py/test/report/base.py +++ /dev/null @@ -1,74 +0,0 @@ -from py.__.test import event -from py.__.test.collect import getrelpath - -import sys - -class BaseReporter(object): - def __init__(self, bus=None): - self._reset() - self._bus = bus - if bus: - self._bus.subscribe(self.processevent) - - def _reset(self): - self._passed = [] - self._skipped = [] - self._failed = [] - self._deselected = [] - - def deactivate(self): - if self._bus: - self._bus.unsubscribe(self.processevent) - - def processevent(self, ev): - evname = ev.__class__.__name__ - repmethod = getattr(self, "rep_%s" % evname, None) - if repmethod is None: - self.rep(ev) - else: - repmethod(ev) - - def rep(self, ev): - pass - - def rep_ItemTestReport(self, ev): - if ev.skipped: - self._skipped.append(ev) - elif ev.failed: - self._failed.append(ev) - elif ev.passed: - self._passed.append(ev) - - def rep_CollectionReport(self, ev): - if ev.skipped: - self._skipped.append(ev) - elif ev.failed: - self._failed.append(ev) - else: - pass # don't record passed collections - - def rep_TestrunStart(self, ev): - self._reset() - - def rep_Deselected(self, ev): - self._deselected.extend(ev.items) - - def _folded_skips(self): - d = {} - for event in self._skipped: - longrepr = event.outcome.longrepr - key = longrepr.path, longrepr.lineno, longrepr.message - d.setdefault(key, []).append(event) - l = [] - for key, events in d.iteritems(): - l.append((len(events),) + key) - return l - -def repr_pythonversion(v=None): - if v is None: - v = sys.version_info - try: - return "%s.%s.%s-%s-%s" % v - except (TypeError, ValueError): - return str(v) - diff --git a/py/test/report/collectonly.py b/py/test/report/collectonly.py deleted file mode 100644 index 892d89d17..000000000 --- a/py/test/report/collectonly.py +++ /dev/null @@ -1,43 +0,0 @@ - -""" --collectonly session, not to spread logic all over the place -""" -import py -from py.__.test.report.base import BaseReporter -from py.__.test.outcome import Skipped as Skipped2 -from py.__.test.outcome import Skipped - -class CollectonlyReporter(BaseReporter): - INDENT = " " - - def __init__(self, config, out=None, bus=None): - super(CollectonlyReporter, self).__init__(bus=bus) - self.config = config - if out is None: - out = py.std.sys.stdout - self.out = py.io.TerminalWriter(out) - self.indent = "" - self._failed = [] - - def outindent(self, line): - self.out.line(self.indent + str(line)) - - def rep_CollectionStart(self, ev): - self.outindent(ev.collector) - self.indent += self.INDENT - - def rep_ItemStart(self, event): - self.outindent(event.item) - - def rep_CollectionReport(self, ev): - super(CollectonlyReporter, self).rep_CollectionReport(ev) - if ev.failed: - self.outindent("!!! %s !!!" % ev.outcome.longrepr.reprcrash.message) - elif ev.skipped: - self.outindent("!!! %s !!!" % ev.outcome.longrepr.message) - self.indent = self.indent[:-len(self.INDENT)] - - def rep_TestrunFinish(self, session): - for ev in self._failed: - ev.toterminal(self.out) - -Reporter = CollectonlyReporter diff --git a/py/test/report/terminal.py b/py/test/report/terminal.py deleted file mode 100644 index 1f1519ab4..000000000 --- a/py/test/report/terminal.py +++ /dev/null @@ -1,219 +0,0 @@ -import py -import sys -from py.__.test import event -from py.__.test.report.base import BaseReporter -from py.__.test.report.base import getrelpath, repr_pythonversion - -class TerminalReporter(BaseReporter): - def __init__(self, config, file=None, bus=None): - super(TerminalReporter, self).__init__(bus=bus) - self.config = config - self.curdir = py.path.local() - if file is None: - file = py.std.sys.stdout - self._tw = py.io.TerminalWriter(file) - - def _reset(self): - self.currentfspath = None - super(TerminalReporter, self)._reset() - - def write_fspath_result(self, fspath, res): - if fspath != self.currentfspath: - self._tw.line() - relpath = getrelpath(self.curdir, fspath) - self._tw.write(relpath + " ") - self.currentfspath = fspath - self._tw.write(res) - - def write_ensure_prefix(self, prefix, extra=""): - if self.currentfspath != prefix: - self._tw.line() - self.currentfspath = prefix - self._tw.write(prefix) - if extra: - self._tw.write(extra) - self.currentfspath = -2 - - def ensure_newline(self): - if self.currentfspath: - self._tw.line() - self.currentfspath = None - - def write_line(self, line, **markup): - line = str(line) - self.ensure_newline() - self._tw.line(line, **markup) - - def write_sep(self, sep, title=None, **markup): - self.ensure_newline() - self._tw.sep(sep, title, **markup) - - def getoutcomeletter(self, item): - return item.outcome.shortrepr - - def getoutcomeword(self, item): - if item.passed: return self._tw.markup("PASS", green=True) - elif item.failed: return self._tw.markup("FAIL", red=True) - elif item.skipped: return "SKIP" - else: return self._tw.markup("???", red=True) - - def getcollectoutcome(self, item): - if item.skipped: - return str(item.outcome.longrepr.message) - else: - return str(item.outcome.longrepr.reprcrash.message) - - def rep_InternalException(self, ev): - for line in str(ev.repr).split("\n"): - self.write_line("InternalException: " + line) - - def rep_HostGatewayReady(self, ev): - if self.config.option.verbose: - self.write_line("HostGatewayReady: %s" %(ev.host,)) - - def rep_HostUp(self, ev): - d = ev.platinfo.copy() - d['hostid'] = ev.host.hostid - d['version'] = repr_pythonversion(d['sys.version_info']) - self.write_line("HOSTUP: %(hostid)s %(sys.platform)s " - "%(sys.executable)s - Python %(version)s" % - d) - - def rep_HostDown(self, ev): - host = ev.host - error = ev.error - if error: - self.write_line("HostDown %s: %s" %(host.hostid, error)) - - def rep_ItemStart(self, ev): - if self.config.option.verbose: - info = ev.item.repr_metainfo() - line = info.verboseline(basedir=self.curdir) + " " - extra = "" - if ev.host: - extra = "-> " + ev.host.hostid - self.write_ensure_prefix(line, extra) - else: - # ensure that the path is printed before the 1st test of - # a module starts running - fspath = ev.item.fspath - self.write_fspath_result(fspath, "") - - def rep_RescheduleItems(self, ev): - if self.config.option.debug: - self.write_sep("!", "RESCHEDULING %s " %(ev.items,)) - - def rep_ItemTestReport(self, ev): - super(TerminalReporter, self).rep_ItemTestReport(ev) - fspath = ev.colitem.fspath - if not self.config.option.verbose: - self.write_fspath_result(fspath, self.getoutcomeletter(ev)) - else: - info = ev.colitem.repr_metainfo() - line = info.verboseline(basedir=self.curdir) + " " - word = self.getoutcomeword(ev) - self.write_ensure_prefix(line, word) - - def rep_CollectionReport(self, ev): - super(TerminalReporter, self).rep_CollectionReport(ev) - fspath = ev.colitem.fspath - if ev.failed or ev.skipped: - msg = self.getcollectoutcome(ev) - self.write_fspath_result(fspath, "- " + msg) - - def rep_TestrunStart(self, ev): - super(TerminalReporter, self).rep_TestrunStart(ev) - self.write_sep("=", "test session starts", bold=True) - self._sessionstarttime = py.std.time.time() - #self.out_hostinfo() - - def rep_TestrunFinish(self, ev): - self._tw.line("") - if ev.exitstatus in (0, 1, 2): - self.summary_failures() - self.summary_skips() - if ev.excrepr is not None: - self.summary_final_exc(ev.excrepr) - if ev.exitstatus == 2: - self.write_sep("!", "KEYBOARD INTERRUPT") - self.summary_deselected() - self.summary_stats() - - def rep_LooponfailingInfo(self, ev): - if ev.failreports: - self.write_sep("#", "LOOPONFAILING", red=True) - for report in ev.failreports: - try: - loc = report.outcome.longrepr.reprcrash - except AttributeError: - loc = str(report.outcome.longrepr)[:50] - self.write_line(loc, red=True) - self.write_sep("#", "waiting for changes") - for rootdir in ev.rootdirs: - self.write_line("### Watching: %s" %(rootdir,), bold=True) - - if 0: - print "#" * 60 - print "# looponfailing: mode: %d failures args" % len(failures) - for ev in failurereports: - name = "/".join(ev.colitem.listnames()) # XXX - print "Failure at: %r" % (name,) - print "# watching py files below %s" % rootdir - print "# ", "^" * len(str(rootdir)) - failures = [ev.colitem for ev in failurereports] - if not failures: - failures = colitems - - # - # summaries for TestrunFinish - # - - def summary_failures(self): - if self._failed and self.config.option.tbstyle != "no": - self.write_sep("=", "FAILURES") - for ev in self._failed: - self.write_sep("_") - ev.toterminal(self._tw) - - def summary_stats(self): - session_duration = py.std.time.time() - self._sessionstarttime - numfailed = len(self._failed) - numskipped = len(self._skipped) - numpassed = len(self._passed) - sum = numfailed + numpassed - self.write_sep("=", "%d/%d passed + %d skips in %.2f seconds" % - (numpassed, sum, numskipped, session_duration), bold=True) - if numfailed == 0: - self.write_sep("=", "failures: no failures :)", green=True) - else: - self.write_sep("=", "failures: %d" %(numfailed), red=True) - - def summary_deselected(self): - if not self._deselected: - return - self.write_sep("=", "%d tests deselected by %r" %( - len(self._deselected), self.config.option.keyword), bold=True) - - - def summary_skips(self): - if not self._failed or self.config.option.showskipsummary: - folded_skips = self._folded_skips() - if folded_skips: - self.write_sep("_", "skipped test summary") - for num, fspath, lineno, reason in folded_skips: - self._tw.line("%s:%d: [%d] %s" %(fspath, lineno, num, reason)) - - def summary_final_exc(self, excrepr): - self.write_sep("!") - if self.config.option.verbose: - excrepr.toterminal(self._tw) - else: - excrepr.reprcrash.toterminal(self._tw) - - def out_hostinfo(self): - self._tw.line("host 0: %s %s - Python %s" % - (py.std.sys.platform, - py.std.sys.executable, - repr_pythonversion())) - -Reporter = TerminalReporter diff --git a/py/test/report/testing/test_basereporter.py b/py/test/report/testing/test_basereporter.py deleted file mode 100644 index 007f8069f..000000000 --- a/py/test/report/testing/test_basereporter.py +++ /dev/null @@ -1,94 +0,0 @@ -import py -from py.__.test.report.base import BaseReporter -from py.__.test.event import EventBus -from py.__.test import event -from py.__.test.runner import OutcomeRepr -from py.__.test.report.base import getrelpath, repr_pythonversion -import sys - -class TestBaseReporter: - def test_activate(self): - bus = EventBus() - rep = BaseReporter(bus=bus) - assert bus._subscribers - assert rep.processevent in bus._subscribers - rep.deactivate() - assert not bus._subscribers - - def test_dispatch_to_matching_method(self): - l = [] - class MyReporter(BaseReporter): - def rep_TestrunStart(self, ev): - l.append(ev) - rep = MyReporter() - ev = event.TestrunStart() - rep.processevent(ev) - assert len(l) == 1 - assert l[0] is ev - - def test_dispatch_to_default(self): - l = [] - class MyReporter(BaseReporter): - def rep(self, ev): - l.append(ev) - rep = MyReporter() - ev = event.NOP() - rep.processevent(ev) - assert len(l) == 1 - assert l[0] is ev - - def test_TestItemReport_one(self): - for outcome in 'passed skipped failed'.split(): - rep = BaseReporter() - ev = event.ItemTestReport(None, **{outcome:True}) - rep.processevent(ev) - assert getattr(rep, '_' + outcome) == [ev] - - def test_CollectionReport(self): - for outcome in 'skipped failed'.split(): - rep = BaseReporter() - ev = event.CollectionReport(None, None, **{outcome:True}) - rep.processevent(ev) - assert getattr(rep, '_' + outcome) == [ev] - - def test_skip_reasons(self): - rep = BaseReporter() - class longrepr: - path = 'xyz' - lineno = 3 - message = "justso" - out1 = OutcomeRepr(None, None, longrepr) - out2 = OutcomeRepr(None, None, longrepr) - ev1 = event.CollectionReport(None, None, skipped=out1) - ev2 = event.ItemTestReport(None, skipped=out2) - rep.processevent(ev1) - rep.processevent(ev2) - assert len(rep._skipped) == 2 - l = rep._folded_skips() - assert len(l) == 1 - num, fspath, lineno, reason = l[0] - assert num == 2 - assert fspath == longrepr.path - assert lineno == longrepr.lineno - assert reason == longrepr.message - -def test_repr_python_version(): - py.magic.patch(sys, 'version_info', (2, 5, 1, 'final', 0)) - try: - assert repr_pythonversion() == "2.5.1-final-0" - py.std.sys.version_info = x = (2,3) - assert repr_pythonversion() == str(x) - finally: - py.magic.revert(sys, 'version_info') - -def test_getrelpath(): - curdir = py.path.local() - sep = curdir.sep - s = getrelpath(curdir, curdir.join("hello", "world")) - assert s == "hello" + sep + "world" - - s = getrelpath(curdir, curdir.dirpath().join("sister")) - assert s == ".." + sep + "sister" - assert getrelpath(curdir, curdir.dirpath()) == ".." - - assert getrelpath(curdir, "hello") == "hello" diff --git a/py/test/report/testing/test_collectonly.py b/py/test/report/testing/test_collectonly.py deleted file mode 100644 index 9a34984b3..000000000 --- a/py/test/report/testing/test_collectonly.py +++ /dev/null @@ -1,53 +0,0 @@ -import py -from py.__.test.report.collectonly import CollectonlyReporter -from py.__.test import event -from py.__.test.testing.suptest import InlineCollection, popvalue -from py.__.test.testing.suptest import assert_stringio_contains_lines - -class TestCollectonly(InlineCollection): - def test_collectonly_basic(self): - modcol = self.getmodulecol(configargs=['--collectonly'], source=""" - def test_func(): - pass - """) - stringio = py.std.cStringIO.StringIO() - rep = CollectonlyReporter(modcol._config, out=stringio) - indent = rep.indent - rep.processevent(event.CollectionStart(modcol)) - s = popvalue(stringio) - assert s == "" - - item = modcol.join("test_func") - rep.processevent(event.ItemStart(item)) - s = popvalue(stringio) - assert s.find("Function 'test_func'") != -1 - rep.processevent(event.CollectionReport(modcol, [], passed="")) - assert rep.indent == indent - - def test_collectonly_skipped_module(self): - modcol = self.getmodulecol(configargs=['--collectonly'], source=""" - import py - py.test.skip("nomod") - """, withsession=True) - stringio = py.std.cStringIO.StringIO() - rep = CollectonlyReporter(modcol._config, bus=self.session.bus, out=stringio) - cols = list(self.session.genitems([modcol])) - assert len(cols) == 0 - assert_stringio_contains_lines(stringio, """ - - !!! Skipped: 'nomod' !!! - """) - - def test_collectonly_failed_module(self): - modcol = self.getmodulecol(configargs=['--collectonly'], source=""" - raise ValueError(0) - """, withsession=True) - stringio = py.std.cStringIO.StringIO() - rep = CollectonlyReporter(modcol._config, bus=self.session.bus, out=stringio) - cols = list(self.session.genitems([modcol])) - assert len(cols) == 0 - assert_stringio_contains_lines(stringio, """ - - !!! ValueError: 0 !!! - """) - diff --git a/py/test/report/testing/test_terminal.py b/py/test/report/testing/test_terminal.py deleted file mode 100644 index 0baaccb0a..000000000 --- a/py/test/report/testing/test_terminal.py +++ /dev/null @@ -1,247 +0,0 @@ -import py -import sys -from py.__.test.report.terminal import TerminalReporter -from py.__.test import event -#from py.__.test.testing import suptest -from py.__.test.runner import basic_run_report -from py.__.test.testing.suptest import InlineCollection, popvalue -from py.__.test.testing.suptest import assert_stringio_contains_lines -from py.__.test.dsession.hostmanage import Host, makehostup -from py.__.test.report.base import repr_pythonversion - -class TestTerminal(InlineCollection): - def test_session_reporter_subscription(self): - config = py.test.config._reparse(['xxx']) - session = config.initsession() - session.sessionstarts() - rep = session.reporter - assert isinstance(rep, TerminalReporter) - assert rep.processevent in session.bus._subscribers - session.sessionfinishes() - #assert rep.processevent not in session.bus._subscribers - - def test_hostup(self): - item = self.getitem("def test_func(): pass") - stringio = py.std.cStringIO.StringIO() - rep = TerminalReporter(item._config, file=stringio) - rep.processevent(event.TestrunStart()) - host = Host("localhost") - rep.processevent(makehostup(host)) - s = popvalue(stringio) - expect = "%s %s %s - Python %s" %(host.hostid, sys.platform, - sys.executable, repr_pythonversion(sys.version_info)) - assert s.find(expect) != -1 - - def test_pass_skip_fail(self): - modcol = self.getmodulecol(""" - import py - def test_ok(): - pass - def test_skip(): - py.test.skip("xx") - def test_func(): - assert 0 - """, withsession=True) - stringio = py.std.cStringIO.StringIO() - rep = TerminalReporter(modcol._config, file=stringio) - rep.processevent(event.TestrunStart()) - for item in self.session.genitems([modcol]): - ev = basic_run_report(item) - rep.processevent(ev) - s = popvalue(stringio) - assert s.find("test_pass_skip_fail.py .sF") != -1 - rep.processevent(event.TestrunFinish()) - assert_stringio_contains_lines(stringio, [ - " def test_func():", - "> assert 0", - "E assert 0", - ]) - - def test_pass_skip_fail_verbose(self): - modcol = self.getmodulecol(""" - import py - def test_ok(): - pass - def test_skip(): - py.test.skip("xx") - def test_func(): - assert 0 - """, configargs=("-v",), withsession=True) - stringio = py.std.cStringIO.StringIO() - rep = TerminalReporter(modcol._config, file=stringio) - rep.processevent(event.TestrunStart()) - items = modcol.collect() - for item in items: - rep.processevent(event.ItemStart(item)) - s = stringio.getvalue().strip() - assert s.endswith(item.name) - ev = basic_run_report(item) - rep.processevent(ev) - - assert_stringio_contains_lines(stringio, [ - "*test_pass_skip_fail_verbose.py:2: *test_ok*PASS", - "*test_pass_skip_fail_verbose.py:4: *test_skip*SKIP", - "*test_pass_skip_fail_verbose.py:6: *test_func*FAIL", - ]) - rep.processevent(event.TestrunFinish()) - assert_stringio_contains_lines(stringio, [ - " def test_func():", - "> assert 0", - "E assert 0", - ]) - - def test_collect_fail(self): - modcol = self.getmodulecol(""" - import xyz - """, withsession=True) - stringio = py.std.cStringIO.StringIO() - rep = TerminalReporter(modcol._config, bus=self.session.bus, file=stringio) - rep.processevent(event.TestrunStart()) - l = list(self.session.genitems([modcol])) - assert len(l) == 0 - s = popvalue(stringio) - print s - assert s.find("test_collect_fail.py - ImportError: No module named") != -1 - rep.processevent(event.TestrunFinish()) - assert_stringio_contains_lines(stringio, [ - "> import xyz", - "E ImportError: No module named xyz" - ]) - - def test_internal_exception(self): - modcol = self.getmodulecol("def test_one(): pass") - stringio = py.std.cStringIO.StringIO() - rep = TerminalReporter(modcol._config, file=stringio) - excinfo = py.test.raises(ValueError, "raise ValueError('hello')") - rep.processevent(event.InternalException(excinfo)) - s = popvalue(stringio) - assert s.find("InternalException:") != -1 - - def test_hostready_crash(self): - modcol = self.getmodulecol(""" - def test_one(): - pass - """, configargs=("-v",)) - stringio = py.std.cStringIO.StringIO() - host1 = Host("localhost") - rep = TerminalReporter(modcol._config, file=stringio) - rep.processevent(event.HostGatewayReady(host1, None)) - s = popvalue(stringio) - assert s.find("HostGatewayReady") != -1 - rep.processevent(event.HostDown(host1, "myerror")) - s = popvalue(stringio) - assert s.find("HostDown") != -1 - assert s.find("myerror") != -1 - - def test_writeline(self): - modcol = self.getmodulecol("def test_one(): pass") - stringio = py.std.cStringIO.StringIO() - rep = TerminalReporter(modcol._config, file=stringio) - rep.write_fspath_result(py.path.local("xy.py"), '.') - rep.write_line("hello world") - lines = popvalue(stringio).split('\n') - assert not lines[0] - assert lines[1].endswith("xy.py .") - assert lines[2] == "hello world" - - def test_looponfailingreport(self): - modcol = self.getmodulecol(""" - def test_fail(): - assert 0 - def test_fail2(): - raise ValueError() - """) - stringio = py.std.cStringIO.StringIO() - rep = TerminalReporter(modcol._config, file=stringio) - reports = [basic_run_report(x) for x in modcol.collect()] - rep.processevent(event.LooponfailingInfo(reports, [modcol._config.topdir])) - assert_stringio_contains_lines(stringio, [ - "*test_looponfailingreport.py:2: assert 0", - "*test_looponfailingreport.py:4: ValueError*", - "*waiting*", - "*%s*" % (modcol._config.topdir), - ]) - - def test_tb_option(self): - for tbopt in ["no", "short", "long"]: - print 'testing --tb=%s...' % tbopt - modcol = self.getmodulecol(""" - import py - def g(): - raise IndexError - def test_func(): - print 6*7 - g() # --calling-- - """, configargs=("--tb=%s" % tbopt,), withsession=True) - stringio = py.std.cStringIO.StringIO() - rep = TerminalReporter(modcol._config, file=stringio) - rep.processevent(event.TestrunStart()) - for item in self.session.genitems([modcol]): - ev = basic_run_report(item) - rep.processevent(ev) - rep.processevent(event.TestrunFinish()) - s = popvalue(stringio) - if tbopt == "long": - assert 'print 6*7' in s - else: - assert 'print 6*7' not in s - if tbopt != "no": - assert '--calling--' in s - assert 'IndexError' in s - else: - assert 'FAILURES' not in s - assert '--calling--' not in s - assert 'IndexError' not in s - - def test_show_path_before_running_test(self): - modcol = self.getmodulecol(""" - def test_foobar(): - pass - """, withsession=True) - stringio = py.std.cStringIO.StringIO() - rep = TerminalReporter(modcol._config, bus=self.session.bus, file=stringio) - l = list(self.session.genitems([modcol])) - assert len(l) == 1 - rep.processevent(event.ItemStart(l[0])) - s = popvalue(stringio) - print s - assert s.find("test_show_path_before_running_test.py") != -1 - - def test_keyboard_interrupt(self, verbose=False): - modcol = self.getmodulecol(""" - def test_foobar(): - assert 0 - def test_spamegg(): - import py; py.test.skip('skip me please!') - def test_interrupt_me(): - raise KeyboardInterrupt # simulating the user - """, configargs=("--showskipsummary",) + ("-v",)*verbose, - withsession=True) - stringio = py.std.cStringIO.StringIO() - rep = TerminalReporter(modcol._config, bus=self.session.bus, file=stringio) - rep.processevent(event.TestrunStart()) - try: - for item in self.session.genitems([modcol]): - ev = basic_run_report(item) - rep.processevent(ev) - except KeyboardInterrupt: - excinfo = py.code.ExceptionInfo() - else: - py.test.fail("no KeyboardInterrupt??") - s = popvalue(stringio) - if not verbose: - assert s.find("_keyboard_interrupt.py Fs") != -1 - rep.processevent(event.TestrunFinish(exitstatus=2, excinfo=excinfo)) - assert_stringio_contains_lines(stringio, [ - " def test_foobar():", - "> assert 0", - "E assert 0", - ]) - text = stringio.getvalue() - assert "Skipped: 'skip me please!'" in text - assert "_keyboard_interrupt.py:6: KeyboardInterrupt" in text - see_details = "raise KeyboardInterrupt # simulating the user" in text - assert see_details == verbose - - def test_verbose_keyboard_interrupt(self): - self.test_keyboard_interrupt(verbose=True) diff --git a/py/test/resultlog.py b/py/test/resultlog.py deleted file mode 100644 index aed5581d4..000000000 --- a/py/test/resultlog.py +++ /dev/null @@ -1,54 +0,0 @@ -import py -from py.__.test import event - -def generic_path(item): - chain = item.listchain() - gpath = [chain[0].name] - fspath = chain[0].fspath - fspart = False - for node in chain[1:]: - newfspath = node.fspath - if newfspath == fspath: - if fspart: - gpath.append(':') - fspart = False - else: - gpath.append('.') - else: - gpath.append('/') - fspart = True - name = node.name - if name[0] in '([': - gpath.pop() - gpath.append(name) - fspath = newfspath - return ''.join(gpath) - -class ResultLog(object): - - def __init__(self, bus, logfile): - bus.subscribe(self.log_event_to_file) - self.logfile = logfile # preferably line buffered - - def write_log_entry(self, shortrepr, name, longrepr): - print >>self.logfile, "%s %s" % (shortrepr, name) - for line in longrepr.splitlines(): - print >>self.logfile, " %s" % line - - def log_outcome(self, ev): - outcome = ev.outcome - gpath = generic_path(ev.colitem) - self.write_log_entry(outcome.shortrepr, gpath, str(outcome.longrepr)) - - def log_event_to_file(self, ev): - if isinstance(ev, event.ItemTestReport): - self.log_outcome(ev) - elif isinstance(ev, event.CollectionReport): - if not ev.passed: - self.log_outcome(ev) - elif isinstance(ev, event.InternalException): - path = ev.repr.reprcrash.path # fishing :( - self.write_log_entry('!', path, str(ev.repr)) - - - diff --git a/py/test/runner.py b/py/test/runner.py index b30805495..377df4f87 100644 --- a/py/test/runner.py +++ b/py/test/runner.py @@ -9,12 +9,14 @@ import py, os, sys from py.__.test import event -from py.__.test.outcome import Skipped, Exit +from py.__.test.outcome import Exit from py.__.test.dsession.mypickle import ImmutablePickler import py.__.test.custompdb class RobustRun(object): - """ a robust setup/execute/teardown protocol. """ + """ a robust setup/execute/teardown protocol used both for test collectors + and test items. + """ def __init__(self, colitem, pdb=None): self.colitem = colitem self.getcapture = colitem._config._getcapture @@ -46,35 +48,18 @@ class RobustRun(object): excinfo = py.code.ExceptionInfo() return self.makereport(res, when, excinfo, outerr) - def getkw(self, when, excinfo, outerr): - if excinfo.errisinstance(Skipped): - outcome = "skipped" - shortrepr = "s" - longrepr = excinfo._getreprcrash() - else: - outcome = "failed" - if when == "execute": - longrepr = self.colitem.repr_failure(excinfo, outerr) - shortrepr = self.colitem.shortfailurerepr - else: - longrepr = self.colitem._repr_failure_py(excinfo, outerr) - shortrepr = self.colitem.shortfailurerepr.lower() - kw = { outcome: OutcomeRepr(when, shortrepr, longrepr)} - return kw - class ItemRunner(RobustRun): def setup(self): self.colitem._setupstate.prepare(self.colitem) def teardown(self): self.colitem._setupstate.teardown_exact(self.colitem) def execute(self): + #self.colitem.config.pytestplugins.pre_execute(self.colitem) self.colitem.runtest() + #self.colitem.config.pytestplugins.post_execute(self.colitem) + def makereport(self, res, when, excinfo, outerr): - if excinfo: - kw = self.getkw(when, excinfo, outerr) - else: - kw = {'passed': OutcomeRepr(when, '.', "")} - testrep = event.ItemTestReport(self.colitem, **kw) + testrep = event.ItemTestReport(self.colitem, excinfo, when, outerr) if self.pdb and testrep.failed: tw = py.io.TerminalWriter() testrep.toterminal(tw) @@ -89,26 +74,13 @@ class CollectorRunner(RobustRun): def execute(self): return self.colitem._memocollect() def makereport(self, res, when, excinfo, outerr): - if excinfo: - kw = self.getkw(when, excinfo, outerr) - else: - kw = {'passed': OutcomeRepr(when, '', "")} - return event.CollectionReport(self.colitem, res, **kw) - + return event.CollectionReport(self.colitem, res, excinfo, when, outerr) + NORESULT = object() # # public entrypoints / objects # -class OutcomeRepr(object): - def __init__(self, when, shortrepr, longrepr): - self.when = when - self.shortrepr = shortrepr - self.longrepr = longrepr - def __str__(self): - return ">f, ev - f.flush() - self.bus.subscribe(eventwrite) - resultlog = self.config.option.resultlog - if resultlog: - f = open(resultlog, 'w', 1) # line buffered - self.resultlog = ResultLog(self.bus, f) def fixoptions(self): """ check, fix and determine conflicting options. """ @@ -56,19 +42,24 @@ class Session(object): """ yield Items from iterating over the given colitems. """ while colitems: next = colitems.pop(0) + if isinstance(next, (tuple, list)): + colitems[:] = list(next) + colitems + continue + assert self.bus is next._config.bus + notify = self.bus.notify if isinstance(next, Item): remaining = self.filteritems([next]) if remaining: - self.bus.notify(event.ItemStart(next)) + notify("itemstart", event.ItemStart(next)) yield next else: assert isinstance(next, Collector) - self.bus.notify(event.CollectionStart(next)) + notify("collectionstart", event.CollectionStart(next)) ev = basic_collect_report(next) if ev.passed: for x in self.genitems(ev.result, keywordexpr): yield x - self.bus.notify(ev) + notify("collectionreport", ev) def filteritems(self, colitems): """ return items to process (some may be deselected)""" @@ -86,7 +77,7 @@ class Session(object): continue remaining.append(colitem) if deselected: - self.bus.notify(event.Deselected(deselected, )) + self.bus.notify("deselected", event.Deselected(deselected, )) if self.config.option.keyword.endswith(":"): self._nomatch = True return remaining @@ -98,23 +89,19 @@ class Session(object): def sessionstarts(self): """ setup any neccessary resources ahead of the test run. """ - self.bus.notify(event.TestrunStart()) - self._failurelist = [] - self.bus.subscribe(self._processfailures) - - def _processfailures(self, ev): - if isinstance(ev, event.BaseReport) and ev.failed: - self._failurelist.append(ev) - if self.config.option.exitfirst: - self.shouldstop = True + self.bus.notify("testrunstart", event.TestrunStart()) + + def pyevent_itemtestreport(self, rep): + if rep.failed: + self._testsfailed = True + if self.config.option.exitfirst: + self.shouldstop = True + pyevent_collectionreport = pyevent_itemtestreport def sessionfinishes(self, exitstatus=0, excinfo=None): """ teardown any resources after a test run. """ - self.bus.notify(event.TestrunFinish(exitstatus=exitstatus, - excinfo=excinfo)) - self.bus.unsubscribe(self._processfailures) - #self.reporter.deactivate() - return self._failurelist + self.bus.notify("testrunfinish", + event.TestrunFinish(exitstatus=exitstatus, excinfo=excinfo)) def getinitialitems(self, colitems): if colitems is None: @@ -127,7 +114,7 @@ class Session(object): colitems = self.getinitialitems(colitems) self.shouldstop = False self.sessionstarts() - self.bus.notify(makehostup()) + self.bus.notify("hostup", makehostup()) exitstatus = outcome.EXIT_OK captured_excinfo = None try: @@ -136,24 +123,26 @@ class Session(object): break if not self.config.option.collectonly: self.runtest(item) + py.test.collect.Item._setupstate.teardown_all() except KeyboardInterrupt: captured_excinfo = py.code.ExceptionInfo() exitstatus = outcome.EXIT_INTERRUPTED except: - self.bus.notify(event.InternalException()) + captured_excinfo = py.code.ExceptionInfo() + self.bus.notify("internalerror", event.InternalException(captured_excinfo)) exitstatus = outcome.EXIT_INTERNALERROR - if self._failurelist and exitstatus == 0: + if exitstatus == 0 and self._testsfailed: exitstatus = outcome.EXIT_TESTSFAILED self.sessionfinishes(exitstatus=exitstatus, excinfo=captured_excinfo) return exitstatus def runpdb(self, excinfo): - py.__.test.custompdb.post_mortem(excinfo._excinfo[2]) + from py.__.test.custompdb import post_mortem + post_mortem(excinfo._excinfo[2]) def runtest(self, item): runner = item._getrunner() pdb = self.config.option.usepdb and self.runpdb or None testrep = runner(item, pdb=pdb) - self.bus.notify(testrep) - + self.bus.notify("itemtestreport", testrep) diff --git a/py/test/testing/acceptance_test.py b/py/test/testing/acceptance_test.py index 3d234712a..0e8164b5c 100644 --- a/py/test/testing/acceptance_test.py +++ b/py/test/testing/acceptance_test.py @@ -1,110 +1,73 @@ import py -from suptest import assert_lines_contain_lines, FileCreation pydir = py.path.local(py.__file__).dirpath() pytestpath = pydir.join("bin", "py.test") EXPECTTIMEOUT=10.0 -def setup_module(mod): - mod.modtmpdir = py.test.ensuretemp(mod.__name__) - -class Result: - def __init__(self, ret, outlines, errlines): - self.ret = ret - self.outlines = outlines - self.errlines = errlines - -class AcceptBase(FileCreation): - def popen(self, cmdargs, stdout, stderr, **kw): - if not hasattr(py.std, 'subprocess'): - py.test.skip("no subprocess module") - return py.std.subprocess.Popen(cmdargs, stdout=stdout, stderr=stderr, **kw) - - def run(self, *cmdargs): - cmdargs = map(str, cmdargs) - p1 = py.path.local("stdout") - p2 = py.path.local("stderr") - print "running", cmdargs, "curdir=", py.path.local() - popen = self.popen(cmdargs, stdout=p1.open("w"), stderr=p2.open("w")) - ret = popen.wait() - out, err = p1.readlines(cr=0), p2.readlines(cr=0) - if err: - for line in err: - print >>py.std.sys.stderr, line - return Result(ret, out, err) - - def runpybin(self, scriptname, *args): - bindir = py.path.local(py.__file__).dirpath("bin") - if py.std.sys.platform == "win32": - script = bindir.join("win32", scriptname + ".cmd") - else: - script = bindir.join(scriptname) - assert script.check() - return self.run(script, *args) - - def runpytest(self, *args): - return self.runpybin("py.test", *args) - - def setup_method(self, method): - super(AcceptBase, self).setup_method(method) - self.old = self.tmpdir.chdir() - - def teardown_method(self, method): - self.old.chdir() - -class TestPyTest(AcceptBase): - def test_assertion_magic(self): - p = self.makepyfile(test_one=""" +class TestPyTest: + def test_assertion_magic(self, testdir): + p = testdir.makepyfile(""" def test_this(): x = 0 assert x """) - result = self.runpytest(p) - extra = assert_lines_contain_lines(result.outlines, [ + result = testdir.runpytest(p) + extra = result.stdout.fnmatch_lines([ "> assert x", "E assert 0", ]) assert result.ret == 1 - - def test_collectonly_simple(self): - p = self.makepyfile(test_one=""" + def test_collectonly_simple(self, testdir): + p = testdir.makepyfile(""" def test_func1(): pass class TestClass: def test_method(self): pass """) - result = self.runpytest("--collectonly", p) - err = "".join(result.errlines) - assert err.strip().startswith("inserting into sys.path") + result = testdir.runpytest("--collectonly", p) + stderr = result.stderr.str().strip() + assert stderr.startswith("inserting into sys.path") assert result.ret == 0 - extra = assert_lines_contain_lines(result.outlines, py.code.Source(""" - + extra = result.stdout.fnmatch_lines(py.code.Source(""" + """).strip()) - def test_nested_import_error(self): - p = self.makepyfile( - test_one=""" + def test_collectonly_error(self, testdir): + p = testdir.makepyfile("import Errlkjqweqwe") + result = testdir.runpytest("--collectonly", p) + stderr = result.stderr.str().strip() + assert stderr.startswith("inserting into sys.path") + assert result.ret == 1 + extra = result.stdout.fnmatch_lines(py.code.Source(""" + + *ImportError* + !!!*failures*!!! + *test_collectonly_error.py:1* + """).strip()) + + + def test_nested_import_error(self, testdir): + p = testdir.makepyfile(""" import import_fails def test_this(): assert import_fails.a == 1 - """, - import_fails="import does_not_work" - ) - result = self.runpytest(p) - extra = assert_lines_contain_lines(result.outlines, [ + """) + testdir.makepyfile(import_fails="import does_not_work") + result = testdir.runpytest(p) + extra = result.stdout.fnmatch_lines([ "> import import_fails", "E ImportError: No module named does_not_work", ]) assert result.ret == 1 - def test_skipped_reasons(self): - p1 = self.makepyfile( + def test_skipped_reasons(self, testdir): + testdir.makepyfile( test_one=""" from conftest import doskip def setup_function(func): @@ -125,34 +88,34 @@ class TestPyTest(AcceptBase): py.test.skip('test') """ ) - result = self.runpytest() - extra = assert_lines_contain_lines(result.outlines, [ + result = testdir.runpytest() + extra = result.stdout.fnmatch_lines([ "*test_one.py ss", - "*test_two.py - Skipped*", + "*test_two.py S", "___* skipped test summary *_", "*conftest.py:3: *3* Skipped: 'test'", ]) assert result.ret == 0 - def test_deselected(self): - p1 = self.makepyfile(test_one=""" + def test_deselected(self, testdir): + testpath = testdir.makepyfile(""" def test_one(): pass def test_two(): pass def test_three(): pass - """, + """ ) - result = self.runpytest("-k", "test_two:") - extra = assert_lines_contain_lines(result.outlines, [ - "*test_one.py ..", + result = testdir.runpytest("-k", "test_two:", testpath) + extra = result.stdout.fnmatch_lines([ + "*test_deselected.py ..", "=* 1 test*deselected by 'test_two:'*=", ]) assert result.ret == 0 - def test_no_skip_summary_if_failure(self): - p1 = self.makepyfile(test_one=""" + def test_no_skip_summary_if_failure(self, testdir): + testdir.makepyfile(""" import py def test_ok(): pass @@ -161,12 +124,12 @@ class TestPyTest(AcceptBase): def test_skip(): py.test.skip("dontshow") """) - result = self.runpytest() - assert str(result.outlines).find("skip test summary") == -1 + result = testdir.runpytest() + assert result.stdout.str().find("skip test summary") == -1 assert result.ret == 1 - def test_passes(self): - p1 = self.makepyfile(test_one=""" + def test_passes(self, testdir): + p1 = testdir.makepyfile(""" def test_passes(): pass class TestClass: @@ -175,33 +138,32 @@ class TestPyTest(AcceptBase): """) old = p1.dirpath().chdir() try: - result = self.runpytest() + result = testdir.runpytest() finally: old.chdir() - extra = assert_lines_contain_lines(result.outlines, [ - "test_one.py ..", - "* failures: no failures*", + extra = result.stdout.fnmatch_lines([ + "test_passes.py ..", + "* 2 pass*", ]) assert result.ret == 0 - def test_header_trailer_info(self): - p1 = self.makepyfile(test_one=""" + def test_header_trailer_info(self, testdir): + p1 = testdir.makepyfile(""" def test_passes(): pass """) - result = self.runpytest() + result = testdir.runpytest() verinfo = ".".join(map(str, py.std.sys.version_info[:3])) - extra = assert_lines_contain_lines(result.outlines, [ + extra = result.stdout.fnmatch_lines([ "*===== test session starts ====*", "*localhost* %s %s - Python %s*" %( py.std.sys.platform, py.std.sys.executable, verinfo), - "*test_one.py .", - "=* 1/1 passed + 0 skips in *.[0-9][0-9] seconds *=", - "=* no failures :)*=", + "*test_header_trailer_info.py .", + "=* 1 passed in *.[0-9][0-9] seconds *=", ]) - def test_traceback_failure(self): - p1 = self.makepyfile(test_fail=""" + def test_traceback_failure(self, testdir): + p1 = testdir.makepyfile(""" def g(): return 2 def f(x): @@ -209,16 +171,16 @@ class TestPyTest(AcceptBase): def test_onefails(): f(3) """) - result = self.runpytest(p1) - assert_lines_contain_lines(result.outlines, [ - "*test_fail.py F", + result = testdir.runpytest(p1) + result.stdout.fnmatch_lines([ + "*test_traceback_failure.py F", "====* FAILURES *====", "____*____", "", " def test_onefails():", "> f(3)", "", - "*test_fail.py:6: ", + "*test_*.py:6: ", "_ _ _ *", #"", " def f(x):", @@ -226,11 +188,11 @@ class TestPyTest(AcceptBase): "E assert 3 == 2", "E + where 2 = g()", "", - "*test_fail.py:4: AssertionError" + "*test_traceback_failure.py:4: AssertionError" ]) - def test_capturing_outerr(self): - p1 = self.makepyfile(test_one=""" + def test_capturing_outerr(self, testdir): + p1 = testdir.makepyfile(""" import sys def test_capturing(): print 42 @@ -240,52 +202,35 @@ class TestPyTest(AcceptBase): print >>sys.stderr, 2 raise ValueError """) - result = self.runpytest(p1) - assert_lines_contain_lines(result.outlines, [ - "*test_one.py .F", + result = testdir.runpytest(p1) + result.stdout.fnmatch_lines([ + "*test_capturing_outerr.py .F", "====* FAILURES *====", "____*____", - "*test_one.py:8: ValueError", + "*test_capturing_outerr.py:8: ValueError", "*--- Captured stdout ---*", "1", "*--- Captured stderr ---*", "2", ]) - def test_showlocals(self): - p1 = self.makepyfile(test_one=""" + def test_showlocals(self, testdir): + p1 = testdir.makepyfile(""" def test_showlocals(): x = 3 y = "x" * 5000 assert 0 """) - result = self.runpytest(p1, '-l') - assert_lines_contain_lines(result.outlines, [ + result = testdir.runpytest(p1, '-l') + result.stdout.fnmatch_lines([ #"_ _ * Locals *", "x* = 3", "y* = 'xxxxxx*" ]) - def test_doctest_simple_failing(self): - p = self.maketxtfile(doc=""" - >>> i = 0 - >>> i + 1 - 2 - """) - result = self.runpytest(p) - assert_lines_contain_lines(result.outlines, [ - '001 >>> i = 0', - '002 >>> i + 1', - 'Expected:', - " 2", - "Got:", - " 1", - "*doc.txt:2: DocTestFailure" - ]) - def test_dist_testing(self): - p1 = self.makepyfile( - test_one=""" + def test_dist_testing(self, testdir): + p1 = testdir.makepyfile(""" import py def test_fail0(): assert 0 @@ -300,22 +245,20 @@ class TestPyTest(AcceptBase): dist_hosts = ['localhost'] * 3 """ ) - result = self.runpytest(p1, '-d') - assert_lines_contain_lines(result.outlines, [ + result = testdir.runpytest(p1, '-d') + result.stdout.fnmatch_lines([ "HOSTUP: localhost*Python*", #"HOSTUP: localhost*Python*", #"HOSTUP: localhost*Python*", - "*1/3 passed + 1 skip*", - "*failures: 2*", + "*2 failed, 1 passed, 1 skipped*", ]) assert result.ret == 1 - def test_dist_tests_with_crash(self): + def test_dist_tests_with_crash(self, testdir): if not hasattr(py.std.os, 'kill'): py.test.skip("no os.kill") - p1 = self.makepyfile( - test_one=""" + p1 = testdir.makepyfile(""" import py def test_fail0(): assert 0 @@ -335,34 +278,33 @@ class TestPyTest(AcceptBase): dist_hosts = ['localhost'] * 3 """ ) - result = self.runpytest(p1, '-d') - assert_lines_contain_lines(result.outlines, [ + result = testdir.runpytest(p1, '-d') + result.stdout.fnmatch_lines([ "*localhost*Python*", "*localhost*Python*", "*localhost*Python*", "HostDown*localhost*TERMINATED*", - "*1/4 passed + 1 skip*", - "*failures: 3*", + "*3 failed, 1 passed, 1 skipped*" ]) assert result.ret == 1 - def test_keyboard_interrupt(self): - p1 = self.makepyfile(test_one=""" + def test_keyboard_interrupt(self, testdir): + p1 = testdir.makepyfile(""" import py def test_fail(): raise ValueError() def test_inter(): raise KeyboardInterrupt() """) - result = self.runpytest(p1) - assert_lines_contain_lines(result.outlines, [ + result = testdir.runpytest(p1) + result.stdout.fnmatch_lines([ #"*test_inter() INTERRUPTED", "*KEYBOARD INTERRUPT*", - "*0/1 passed*", + "*1 failed*", ]) - def test_verbose_reporting(self): - p1 = self.makepyfile(test_one=""" + def test_verbose_reporting(self, testdir): + p1 = testdir.makepyfile(""" import py def test_fail(): raise ValueError() @@ -376,20 +318,20 @@ class TestPyTest(AcceptBase): assert x == 1 yield check, 0 """) - result = self.runpytest(p1, '-v') - assert_lines_contain_lines(result.outlines, [ - "*test_one.py:2: test_fail*FAIL", - "*test_one.py:4: test_pass*PASS", - "*test_one.py:7: TestClass.test_skip*SKIP", - "*test_one.py:10: test_gen*FAIL", + result = testdir.runpytest(p1, '-v') + result.stdout.fnmatch_lines([ + "*test_verbose_reporting.py:2: test_fail*FAIL", + "*test_verbose_reporting.py:4: test_pass*PASS", + "*test_verbose_reporting.py:7: TestClass.test_skip*SKIP", + "*test_verbose_reporting.py:10: test_gen*FAIL", ]) assert result.ret == 1 -class TestInteractive(AcceptBase): - def getspawn(self): +class TestInteractive: + def getspawn(self, tmpdir): pexpect = py.test.importorskip("pexpect") def spawn(cmd): - return pexpect.spawn(cmd, logfile=self.tmpdir.join("spawn.out").open("w")) + return pexpect.spawn(cmd, logfile=tmpdir.join("spawn.out").open("w")) return spawn def requirespexpect(self, version_needed): @@ -398,45 +340,43 @@ class TestInteractive(AcceptBase): if ver < version_needed: py.test.skip("pexpect version %s needed" %(".".join(map(str, version_needed)))) - def test_pdb_interaction(self): + def test_pdb_interaction(self, testdir): self.requirespexpect((2,3)) - spawn = self.getspawn() - self.makepyfile(test_one=""" + spawn = self.getspawn(testdir.tmpdir) + p1 = testdir.makepyfile(""" def test_1(): - #hello - assert 1 == 0 + i = 0 + assert i == 1 """) - child = spawn("%s %s --pdb test_one.py" % (py.std.sys.executable, - pytestpath)) + child = spawn("%s %s --pdb %s" % (py.std.sys.executable, pytestpath, p1)) child.timeout = EXPECTTIMEOUT - child.expect(".*def test_1.*") - child.expect(".*hello.*") + #child.expect(".*def test_1.*") + child.expect(".*i = 0.*") child.expect("(Pdb)") child.sendeof() - child.expect("failures: 1") + child.expect("1 failed") if child.isalive(): child.wait() - def test_simple_looponfailing_interaction(self): - spawn = self.getspawn() - test_one = self.makepyfile(test_one=""" + def test_simple_looponfailing_interaction(self, testdir): + spawn = self.getspawn(testdir.tmpdir) + p1 = testdir.makepyfile(""" def test_1(): assert 1 == 0 """) - test_one.setmtime(test_one.mtime() - 5.0) - child = spawn("%s %s --looponfailing test_one.py" % (py.std.sys.executable, - str(pytestpath))) + p1.setmtime(p1.mtime() - 50.0) + child = spawn("%s %s --looponfailing %s" % (py.std.sys.executable, pytestpath, p1)) child.timeout = EXPECTTIMEOUT child.expect("assert 1 == 0") - child.expect("test_one.py:") - child.expect("failures: 1") + child.expect("test_simple_looponfailing_interaction.py:") + child.expect("1 failed") child.expect("waiting for changes") - test_one.write(py.code.Source(""" + p1.write(py.code.Source(""" def test_1(): assert 1 == 1 """)) - child.expect("MODIFIED.*test_one.py", timeout=4.0) - child.expect("failures: no failures", timeout=5.0) + child.expect("MODIFIED.*test_simple_looponfailing_interaction.py", timeout=4.0) + child.expect("1 passed", timeout=5.0) child.kill(15) diff --git a/py/test/testing/conftest.py b/py/test/testing/conftest.py new file mode 100644 index 000000000..213bfa969 --- /dev/null +++ b/py/test/testing/conftest.py @@ -0,0 +1,2 @@ + +pytest_plugins = "pytest_xfail", "pytest_pytester", "pytest_tmpdir" diff --git a/py/test/testing/suptest.py b/py/test/testing/suptest.py deleted file mode 100644 index f6af96575..000000000 --- a/py/test/testing/suptest.py +++ /dev/null @@ -1,230 +0,0 @@ -""" - test support code - - makeuniquepyfile(source) generates a per-test-run-unique directory and test_*.py file - - for analyzing events an EventSorter instance is returned for both of: - * events_from_cmdline(args): inprocess-run of cmdline invocation - * events_from_session(session): inprocess-run of given session - - eventappender(config): for getting all events in a list: -""" -import py -from py.__.test import event -from fnmatch import fnmatch - -def eventappender(session): - l = [] - def app(ev): - print ev - l.append(ev) - session.bus.subscribe(app) - return l - -def initsorter_from_cmdline(args=None): - if args is None: - args = [] - config = py.test.config._reparse(args) - session = config.initsession() - sorter = EventSorter(config, session) - return sorter - -def getcolitems(config): - return [config.getfsnode(arg) for arg in config.args] - -def events_from_cmdline(args=None): - sorter = initsorter_from_cmdline(args) - sorter.session.main(getcolitems(sorter.session.config)) - return sorter - -def events_from_runsource(source): - source = py.code.Source(source) - tfile = makeuniquepyfile(source) - return events_from_cmdline([tfile]) - -def events_from_session(session): - sorter = EventSorter(session.config, session) - session.main(getcolitems(session.config)) - return sorter - -class EventSorter(object): - def __init__(self, config, session=None): - self.config = config - self.session = session - self.cls2events = d = {} - def app(event): - print "[event]", event - for cls in py.std.inspect.getmro(event.__class__): - if cls is not object: - d.setdefault(cls, []).append(event) - session.bus.subscribe(app) - - def get(self, cls): - return self.cls2events.get(cls, []) - - def listoutcomes(self): - passed = [] - skipped = [] - failed = [] - for ev in self.get(event.ItemTestReport): - if ev.passed: - passed.append(ev) - elif ev.skipped: - skipped.append(ev) - elif ev.failed: - failed.append(ev) - return passed, skipped, failed - - def countoutcomes(self): - return map(len, self.listoutcomes()) - - def assertoutcome(self, passed=0, skipped=0, failed=0): - realpassed, realskipped, realfailed = self.listoutcomes() - assert passed == len(realpassed) - assert skipped == len(realskipped) - assert failed == len(realfailed) - - def getfailedcollections(self): - l = [] - for ev in self.get(event.CollectionReport): - if ev.failed: - l.append(ev) - return l - - def getreport(self, inamepart): - """ return a testreport whose dotted import path matches """ - l = [] - for rep in self.get(event.ItemTestReport): - if inamepart in rep.colitem.listnames(): - l.append(rep) - if len(l) != 1: - raise ValueError("did not find exactly one testreport" - "found" + str(l)) - return l[0] - - -counter = py.std.itertools.count().next -def makeuniquepyfile(source): - dirname = "test_%d" %(counter(),) - tmpdir = py.test.ensuretemp(dirname) - p = tmpdir.join(dirname + ".py") - assert not p.check() - p.write(py.code.Source(source)) - print "created test file", p - p.dirpath("__init__.py").ensure() - return p - -def getItemTestReport(source, tb="long"): - tfile = makeuniquepyfile(source) - sorter = events_from_cmdline([tfile, "--tb=%s" %tb]) - # get failure base info - failevents = sorter.get(event.ItemTestReport) - assert len(failevents) == 1 - return failevents[0],tfile - -def assert_lines_contain_lines(lines1, lines2): - """ assert that lines2 are contained (linearly) in lines1. - return a list of extralines found. - """ - __tracebackhide__ = True - if isinstance(lines2, str): - lines2 = py.code.Source(lines2) - if isinstance(lines2, py.code.Source): - lines2 = lines2.strip().lines - - extralines = [] - lines1 = lines1[:] - nextline = None - for line in lines2: - nomatchprinted = False - while lines1: - nextline = lines1.pop(0) - if line == nextline: - print "exact match:", repr(line) - break - elif fnmatch(nextline, line): - print "fnmatch:", repr(line) - print " with:", repr(nextline) - break - else: - if not nomatchprinted: - print "nomatch:", repr(line) - nomatchprinted = True - print " and:", repr(nextline) - extralines.append(nextline) - else: - if line != nextline: - #__tracebackhide__ = True - raise AssertionError("expected line not found: %r" % line) - extralines.extend(lines1) - return extralines - -# XXX below some code to help with inlining examples -# as source code. -# -class FileCreation(object): - def setup_method(self, method): - self.tmpdir = py.test.ensuretemp("%s_%s" % - (self.__class__.__name__, method.__name__)) - def makepyfile(self, **kwargs): - return self._makefile('.py', **kwargs) - def maketxtfile(self, **kwargs): - return self._makefile('.txt', **kwargs) - def _makefile(self, ext, **kwargs): - ret = None - for name, value in kwargs.iteritems(): - p = self.tmpdir.join(name).new(ext=ext) - source = py.code.Source(value) - p.write(str(py.code.Source(value)).lstrip()) - if ret is None: - ret = p - return ret - - def parseconfig(self, *args): - return py.test.config._reparse(list(args)) - -class InlineCollection(FileCreation): - """ helps to collect and run test functions inlining other test functions. """ - - def getmodulecol(self, source, configargs=(), withsession=False): - self.tmpdir.ensure("__init__.py") - kw = {"test_" + self.tmpdir.basename: py.code.Source(source).strip()} - path = self.makepyfile(**kw) - self.config = self.parseconfig(path, *configargs) - if withsession: - self.session = self.config.initsession() - return self.config.getfsnode(path) - - def getitems(self, source): - modulecol = self.getmodulecol(source) - return modulecol.collect() - - def getitem(self, source, funcname="test_func"): - modulecol = self.getmodulecol(source) - item = modulecol.join(funcname) - assert item is not None, "%r item not found in module:\n%s" %(funcname, source) - return item - - def runitem(self, func, funcname="test_func", **runnerargs): - item = self.getitem(func, funcname=funcname) - runner = self.getrunner() - return runner(item, **runnerargs) - -class InlineSession(InlineCollection): - def parse_and_run(self, *args): - config = self.parseconfig(*args) - session = config.initsession() - sorter = EventSorter(config, session) - session.main() - return sorter - -def popvalue(stringio): - value = stringio.getvalue().rstrip() - stringio.truncate(0) - return value - -def assert_stringio_contains_lines(stringio, tomatchlines): - stringio.seek(0) - l = stringio.readlines() - l = map(str.rstrip, l) - assert_lines_contain_lines(l, tomatchlines) diff --git a/py/test/testing/test_collect.py b/py/test/testing/test_collect.py index 73ffc60ca..9498a1fa3 100644 --- a/py/test/testing/test_collect.py +++ b/py/test/testing/test_collect.py @@ -1,220 +1,13 @@ -from __future__ import generators import py -from py.__.test import event, outcome -from py.__.test.testing import suptest -from py.__.test.conftesthandle import Conftest -from py.__.test.collect import SetupState -from test_config import getcolitems -from py.__.test.pycollect import DoctestFileContent -class DummyConfig: - def __init__(self): - self._conftest = Conftest() - self._setupstate = SetupState() - class dummyoption: - nomagic = False - self.option = dummyoption - def getvalue(self, name, fspath): - return self._conftest.rget(name, fspath) +class TestCollector: + def test_collect_versus_item(self): + from py.__.test.collect import Collector, Item + assert not issubclass(Collector, Item) + assert not issubclass(Item, Collector) -def setup_module(mod): - mod.tmpdir = py.test.ensuretemp(mod.__name__) - mod.dummyconfig = DummyConfig() - -def test_collect_versus_item(): - from py.__.test.collect import Collector, Item - assert not issubclass(Collector, Item) - assert not issubclass(Item, Collector) - -def test_ignored_certain_directories(): - tmp = py.test.ensuretemp("ignore_certain_directories") - tmp.ensure("_darcs", 'test_notfound.py') - tmp.ensure("CVS", 'test_notfound.py') - tmp.ensure("{arch}", 'test_notfound.py') - tmp.ensure(".whatever", 'test_notfound.py') - tmp.ensure(".bzr", 'test_notfound.py') - tmp.ensure("normal", 'test_found.py') - tmp.ensure('test_found.py') - - col = py.test.collect.Directory(tmp, config=dummyconfig) - items = col.collect() - names = [x.name for x in items] - assert len(items) == 2 - assert 'normal' in names - assert 'test_found.py' in names - -class TestCollect(suptest.InlineCollection): - def test_failing_import(self): - modcol = self.getmodulecol("import alksdjalskdjalkjals") - py.test.raises(ImportError, modcol.collect) - py.test.raises(ImportError, modcol.collect) - py.test.raises(ImportError, modcol.run) - - def test_syntax_error_in_module(self): - modcol = self.getmodulecol("this is a syntax error") - py.test.raises(SyntaxError, modcol.collect) - py.test.raises(SyntaxError, modcol.collect) - py.test.raises(SyntaxError, modcol.run) - - def test_listnames_and__getitembynames(self): - modcol = self.getmodulecol("pass") - names = modcol.listnames() - dircol = modcol._config.getfsnode(modcol._config.topdir) - x = dircol._getitembynames(names) - assert modcol.name == x.name - assert modcol.name == x.name - - def test_listnames_getitembynames_custom(self): - hello = self._makefile(".xxx", hello="world") - self.makepyfile(conftest=""" - import py - class CustomFile(py.test.collect.File): - pass - class MyDirectory(py.test.collect.Directory): - def collect(self): - return [CustomFile(self.fspath.join("hello.xxx"), parent=self)] - Directory = MyDirectory - """) - config = self.parseconfig(hello) - node = config.getfsnode(hello) - assert isinstance(node, py.test.collect.File) - assert node.name == "hello.xxx" - names = node.listnames()[1:] - dircol = config.getfsnode(config.topdir) - node = dircol._getitembynames(names) - assert isinstance(node, py.test.collect.File) - - def test_found_certain_testfiles(self): - p1 = self.makepyfile(test_found = "pass", found_test="pass") - col = py.test.collect.Directory(p1.dirpath(), config=dummyconfig) - items = col.collect() # Directory collect returns files sorted by name - assert len(items) == 2 - assert items[1].name == 'test_found.py' - assert items[0].name == 'found_test.py' - - def test_disabled_class(self): - modcol = self.getmodulecol(""" - class TestClass: - disabled = True - def test_method(self): - pass - """) - l = modcol.collect() - assert len(l) == 1 - modcol = l[0] - assert isinstance(modcol, py.test.collect.Class) - assert not modcol.collect() - - def test_disabled_module(self): - modcol = self.getmodulecol(""" - disabled = True - def setup_module(mod): - raise ValueError - """) - assert not modcol.collect() - assert not modcol.run() - - def test_generative_functions(self): - modcol = self.getmodulecol(""" - def func1(arg, arg2): - assert arg == arg2 - - def test_gen(): - yield func1, 17, 3*5 - yield func1, 42, 6*7 - """) - colitems = modcol.collect() - assert len(colitems) == 1 - gencol = colitems[0] - assert isinstance(gencol, py.test.collect.Generator) - gencolitems = gencol.collect() - assert len(gencolitems) == 2 - assert isinstance(gencolitems[0], py.test.collect.Function) - assert isinstance(gencolitems[1], py.test.collect.Function) - assert gencolitems[0].name == '[0]' - assert gencolitems[0].obj.func_name == 'func1' - - def test_generative_methods(self): - modcol = self.getmodulecol(""" - def func1(arg, arg2): - assert arg == arg2 - class TestGenMethods: - def test_gen(self): - yield func1, 17, 3*5 - yield func1, 42, 6*7 - """) - gencol = modcol.collect()[0].collect()[0].collect()[0] - assert isinstance(gencol, py.test.collect.Generator) - gencolitems = gencol.collect() - assert len(gencolitems) == 2 - assert isinstance(gencolitems[0], py.test.collect.Function) - assert isinstance(gencolitems[1], py.test.collect.Function) - assert gencolitems[0].name == '[0]' - assert gencolitems[0].obj.func_name == 'func1' - - def test_generative_functions_with_explicit_names(self): - modcol = self.getmodulecol(""" - def func1(arg, arg2): - assert arg == arg2 - - def test_gen(): - yield "seventeen", func1, 17, 3*5 - yield "fortytwo", func1, 42, 6*7 - """) - colitems = modcol.collect() - assert len(colitems) == 1 - gencol = colitems[0] - assert isinstance(gencol, py.test.collect.Generator) - gencolitems = gencol.collect() - assert len(gencolitems) == 2 - assert isinstance(gencolitems[0], py.test.collect.Function) - assert isinstance(gencolitems[1], py.test.collect.Function) - assert gencolitems[0].name == "['seventeen']" - assert gencolitems[0].obj.func_name == 'func1' - assert gencolitems[1].name == "['fortytwo']" - assert gencolitems[1].obj.func_name == 'func1' - - def test_generative_methods_with_explicit_names(self): - modcol = self.getmodulecol(""" - def func1(arg, arg2): - assert arg == arg2 - class TestGenMethods: - def test_gen(self): - yield "m1", func1, 17, 3*5 - yield "m2", func1, 42, 6*7 - """) - gencol = modcol.collect()[0].collect()[0].collect()[0] - assert isinstance(gencol, py.test.collect.Generator) - gencolitems = gencol.collect() - assert len(gencolitems) == 2 - assert isinstance(gencolitems[0], py.test.collect.Function) - assert isinstance(gencolitems[1], py.test.collect.Function) - assert gencolitems[0].name == "['m1']" - assert gencolitems[0].obj.func_name == 'func1' - assert gencolitems[1].name == "['m2']" - assert gencolitems[1].obj.func_name == 'func1' - - def test_module_assertion_setup(self): - modcol = self.getmodulecol("pass") - from py.__.magic import assertion - l = [] - py.magic.patch(assertion, "invoke", lambda: l.append(None)) - try: - modcol.setup() - finally: - py.magic.revert(assertion, "invoke") - x = l.pop() - assert x is None - py.magic.patch(assertion, "revoke", lambda: l.append(None)) - try: - modcol.teardown() - finally: - py.magic.revert(assertion, "revoke") - x = l.pop() - assert x is None - - def test_check_equality_and_cmp_basic(self): - modcol = self.getmodulecol(""" + def test_check_equality_and_cmp_basic(self, testdir): + modcol = testdir.getmodulecol(""" def test_pass(): pass def test_fail(): assert 0 """) @@ -244,304 +37,10 @@ class TestCollect(suptest.InlineCollection): assert [1,2,3] != fn assert modcol != fn - def test_directory_file_sorting(self): - p1 = self.makepyfile(test_one="hello") - p1.dirpath().mkdir("x") - p1.dirpath().mkdir("dir1") - self.makepyfile(test_two="hello") - p1.dirpath().mkdir("dir2") - config = self.parseconfig() - col = config.getfsnode(p1.dirpath()) - names = [x.name for x in col.collect()] - assert names == ["dir1", "dir2", "test_one.py", "test_two.py", "x"] - - def test_collector_deprecated_run_method(self): - modcol = self.getmodulecol("pass") - res1 = py.test.deprecated_call(modcol.run) - res2 = modcol.collect() - assert res1 == [x.name for x in res2] - - def test_allow_sane_sorting_for_decorators(self): - modcol = self.getmodulecol(""" - def dec(f): - g = lambda: f(2) - g.place_as = f - return g - - - def test_a(y): - pass - test_a = dec(test_a) - - def test_b(y): - pass - test_b = dec(test_b) - """) - colitems = modcol.collect() - assert len(colitems) == 2 - f1, f2 = colitems - assert cmp(f2, f1) > 0 - - -class TestCustomConftests(suptest.InlineCollection): - def test_extra_python_files_and_functions(self): - self.makepyfile(conftest=""" - import py - class MyFunction(py.test.collect.Function): - pass - class Directory(py.test.collect.Directory): - def consider_file(self, path, usefilters=True): - if path.check(fnmatch="check_*.py"): - return self.Module(path, parent=self) - return super(Directory, self).consider_file(path, usefilters=usefilters) - class myfuncmixin: - Function = MyFunction - def funcnamefilter(self, name): - return name.startswith('check_') - class Module(myfuncmixin, py.test.collect.Module): - def classnamefilter(self, name): - return name.startswith('CustomTestClass') - class Instance(myfuncmixin, py.test.collect.Instance): - pass - """) - checkfile = self.makepyfile(check_file=""" - def check_func(): - assert 42 == 42 - class CustomTestClass: - def check_method(self): - assert 23 == 23 - """) - # check that directory collects "check_" files - config = self.parseconfig() - col = config.getfsnode(checkfile.dirpath()) - colitems = col.collect() - assert len(colitems) == 1 - assert isinstance(colitems[0], py.test.collect.Module) - - # check that module collects "check_" functions and methods - config = self.parseconfig(checkfile) - col = config.getfsnode(checkfile) - assert isinstance(col, py.test.collect.Module) - colitems = col.collect() - assert len(colitems) == 2 - funccol = colitems[0] - assert isinstance(funccol, py.test.collect.Function) - assert funccol.name == "check_func" - clscol = colitems[1] - assert isinstance(clscol, py.test.collect.Class) - colitems = clscol.collect()[0].collect() - assert len(colitems) == 1 - assert colitems[0].name == "check_method" - - def test_non_python_files(self): - self.makepyfile(conftest=""" - import py - class CustomItem(py.test.collect.Item): - def run(self): - pass - class Directory(py.test.collect.Directory): - def consider_file(self, fspath, usefilters=True): - if fspath.ext == ".xxx": - return CustomItem(fspath.basename, parent=self) - """) - checkfile = self._makefile(ext="xxx", hello="world") - self.makepyfile(x="") - self.maketxtfile(x="") - config = self.parseconfig() - dircol = config.getfsnode(checkfile.dirpath()) - colitems = dircol.collect() - assert len(colitems) == 1 - assert colitems[0].name == "hello.xxx" - assert colitems[0].__class__.__name__ == "CustomItem" - - item = config.getfsnode(checkfile) - assert item.name == "hello.xxx" - assert item.__class__.__name__ == "CustomItem" - -def test_module_file_not_found(): - fn = tmpdir.join('nada','no') - col = py.test.collect.Module(fn, config=dummyconfig) - py.test.raises(py.error.ENOENT, col.collect) - - -def test_order_of_execution_generator_same_codeline(): - o = tmpdir.ensure('genorder1', dir=1) - o.join("test_order1.py").write(py.code.Source(""" - def test_generative_order_of_execution(): - test_list = [] - expected_list = range(6) - - def list_append(item): - test_list.append(item) - - def assert_order_of_execution(): - print 'expected order', expected_list - print 'but got ', test_list - assert test_list == expected_list - - for i in expected_list: - yield list_append, i - yield assert_order_of_execution - """)) - sorter = suptest.events_from_cmdline([o]) - passed, skipped, failed = sorter.countoutcomes() - assert passed == 7 - assert not skipped and not failed - -def test_order_of_execution_generator_different_codeline(): - o = tmpdir.ensure('genorder2', dir=2) - o.join("test_genorder2.py").write(py.code.Source(""" - def test_generative_tests_different_codeline(): - test_list = [] - expected_list = range(3) - - def list_append_2(): - test_list.append(2) - - def list_append_1(): - test_list.append(1) - - def list_append_0(): - test_list.append(0) - - def assert_order_of_execution(): - print 'expected order', expected_list - print 'but got ', test_list - assert test_list == expected_list - - yield list_append_0 - yield list_append_1 - yield list_append_2 - yield assert_order_of_execution - """)) - sorter = suptest.events_from_cmdline([o]) - passed, skipped, failed = sorter.countoutcomes() - assert passed == 4 - assert not skipped and not failed - -def test_function_equality(): - config = py.test.config._reparse([tmpdir]) - f1 = py.test.collect.Function(name="name", config=config, - args=(1,), callobj=isinstance) - f2 = py.test.collect.Function(name="name", config=config, - args=(1,), callobj=callable) - assert not f1 == f2 - assert f1 != f2 - f3 = py.test.collect.Function(name="name", config=config, - args=(1,2), callobj=callable) - assert not f3 == f2 - assert f3 != f2 - - assert not f3 == f1 - assert f3 != f1 - - f1_b = py.test.collect.Function(name="name", config=config, - args=(1,), callobj=isinstance) - assert f1 == f1_b - assert not f1 != f1_b - -class Testgenitems: - def setup_class(cls): - cls.classtemp = py.test.ensuretemp(cls.__name__) - - def setup_method(self, method): - self.tmp = self.classtemp.mkdir(method.func_name) - - def _genitems(self, tmp=None): - if tmp is None: - tmp = self.tmp - print "using tempdir", tmp - config = py.test.config._reparse([tmp]) - session = config.initsession() - l = suptest.eventappender(session) - items = list(session.genitems(getcolitems(config))) - return items, l - - def test_check_collect_hashes(self): - one = self.tmp.ensure("test_check_collect_hashes.py") - one.write(py.code.Source(""" - def test_1(): - pass - - def test_2(): - pass - """)) - one.copy(self.tmp.join("test_check_collect_hashes_2.py")) - items, events = self._genitems() - assert len(items) == 4 - for numi, i in enumerate(items): - for numj, j in enumerate(items): - if numj != numi: - assert hash(i) != hash(j) - assert i != j - - def test_root_conftest_syntax_error(self): - # do we want to unify behaviour with - # test_subdir_conftest_error? - self.tmp.ensure("conftest.py").write("raise SyntaxError\n") - py.test.raises(SyntaxError, self._genitems) - - def test_subdir_conftest_error(self): - self.tmp.ensure("sub", "conftest.py").write("raise SyntaxError\n") - items, events = self._genitems() - failures = [x for x in events - if isinstance(x, event.CollectionReport) - and x.failed] - assert len(failures) == 1 - ev = failures[0] - assert ev.outcome.longrepr.reprcrash.message.startswith("SyntaxError") - - def test_example_items1(self): - self.tmp.ensure("test_example.py").write(py.code.Source(''' - def testone(): - pass - - class TestX: - def testmethod_one(self): - pass - - class TestY(TestX): - pass - ''')) - items, events = self._genitems() - assert len(items) == 3 - assert items[0].name == 'testone' - assert items[1].name == 'testmethod_one' - assert items[2].name == 'testmethod_one' - - # let's also test getmodpath here - assert items[0].getmodpath() == "testone" - assert items[1].getmodpath() == "TestX.testmethod_one" - assert items[2].getmodpath() == "TestY.testmethod_one" - - s = items[0].getmodpath(stopatmodule=False) - assert s == "test_example_items1.test_example.testone" - print s - - def test_collect_doctest_files_with_test_prefix(self): - self.tmp.ensure("whatever.txt") - checkfile = self.tmp.ensure("test_something.txt") - checkfile.write(py.code.Source(""" - alskdjalsdk - >>> i = 5 - >>> i-1 - 4 - """)) - for x in (self.tmp, checkfile): - #print "checking that %s returns custom items" % (x,) - items, events = self._genitems(x) - assert len(items) == 1 - assert isinstance(items[0], DoctestFileContent) - -class TestCollector: - def setup_method(self, method): - self.tmpdir = py.test.ensuretemp("%s_%s" % - (self.__class__.__name__, method.__name__)) - - def test_totrail_and_back(self): - a = self.tmpdir.ensure("a", dir=1) - self.tmpdir.ensure("a", "__init__.py") - x = self.tmpdir.ensure("a", "trail.py") + def test_totrail_and_back(self, tmpdir): + a = tmpdir.ensure("a", dir=1) + tmpdir.ensure("a", "__init__.py") + x = tmpdir.ensure("a", "trail.py") config = py.test.config._reparse([x]) col = config.getfsnode(x) trail = col._totrail() @@ -551,8 +50,8 @@ class TestCollector: col2 = py.test.collect.Collector._fromtrail(trail, config) assert col2.listnames() == col.listnames() - def test_totrail_topdir_and_beyond(self): - config = py.test.config._reparse([self.tmpdir]) + def test_totrail_topdir_and_beyond(self, tmpdir): + config = py.test.config._reparse([tmpdir]) col = config.getfsnode(config.topdir) trail = col._totrail() assert len(trail) == 2 @@ -565,9 +64,124 @@ class TestCollector: py.test.raises(ValueError, "col3._totrail()") -class TestCollectorReprs(suptest.InlineCollection): - def test_repr_metainfo_basic_item(self): - modcol = self.getmodulecol("") + + def test_listnames_and__getitembynames(self, testdir): + modcol = testdir.getmodulecol("pass", withinit=True) + print modcol._config.pytestplugins.getplugins() + names = modcol.listnames() + print names + dircol = modcol._config.getfsnode(modcol._config.topdir) + x = dircol._getitembynames(names) + assert modcol.name == x.name + + def test_listnames_getitembynames_custom(self, testdir): + hello = testdir.makefile(".xxx", hello="world") + testdir.makepyfile(conftest=""" + import py + class CustomFile(py.test.collect.File): + pass + class MyDirectory(py.test.collect.Directory): + def collect(self): + return [CustomFile(self.fspath.join("hello.xxx"), parent=self)] + Directory = MyDirectory + """) + config = testdir.parseconfig(hello) + node = config.getfsnode(hello) + assert isinstance(node, py.test.collect.File) + assert node.name == "hello.xxx" + names = node.listnames()[1:] + dircol = config.getfsnode(config.topdir) + node = dircol._getitembynames(names) + assert isinstance(node, py.test.collect.File) + +class TestCollectFS: + def test_ignored_certain_directories(self, testdir): + tmpdir = testdir.tmpdir + tmpdir.ensure("_darcs", 'test_notfound.py') + tmpdir.ensure("CVS", 'test_notfound.py') + tmpdir.ensure("{arch}", 'test_notfound.py') + tmpdir.ensure(".whatever", 'test_notfound.py') + tmpdir.ensure(".bzr", 'test_notfound.py') + tmpdir.ensure("normal", 'test_found.py') + tmpdir.ensure('test_found.py') + + col = testdir.parseconfig(tmpdir).getfsnode(tmpdir) + items = col.collect() + names = [x.name for x in items] + assert len(items) == 2 + assert 'normal' in names + assert 'test_found.py' in names + + def test_found_certain_testfiles(self, testdir): + p1 = testdir.makepyfile(test_found = "pass", found_test="pass") + col = testdir.parseconfig(p1).getfsnode(p1.dirpath()) + items = col.collect() # Directory collect returns files sorted by name + assert len(items) == 2 + assert items[1].name == 'test_found.py' + assert items[0].name == 'found_test.py' + + def test_directory_file_sorting(self, testdir): + p1 = testdir.makepyfile(test_one="hello") + p1.dirpath().mkdir("x") + p1.dirpath().mkdir("dir1") + testdir.makepyfile(test_two="hello") + p1.dirpath().mkdir("dir2") + config = testdir.parseconfig() + col = config.getfsnode(p1.dirpath()) + names = [x.name for x in col.collect()] + assert names == ["dir1", "dir2", "test_one.py", "test_two.py", "x"] + + def test_collector_deprecated_run_method(self, testdir): + modcol = testdir.getmodulecol("pass") + res1 = py.test.deprecated_call(modcol.run) + res2 = modcol.collect() + assert res1 == [x.name for x in res2] + +class TestCollectPluginHooks: + def test_pytest_collect_file(self, testdir): + tmpdir = testdir.tmpdir + wascalled = [] + class Plugin: + def pytest_collect_file(self, path, parent): + wascalled.append(path) + config = testdir.Config() + config.pytestplugins.register(Plugin()) + config.parse([tmpdir]) + col = config.getfsnode(tmpdir) + testdir.makefile(".abc", "xyz") + res = col.collect() + assert len(wascalled) == 1 + assert wascalled[0].ext == '.abc' + +class TestCustomConftests: + def test_non_python_files(self, testdir): + testdir.makepyfile(conftest=""" + import py + class CustomItem(py.test.collect.Item): + def run(self): + pass + class Directory(py.test.collect.Directory): + def consider_file(self, fspath): + if fspath.ext == ".xxx": + return CustomItem(fspath.basename, parent=self) + """) + checkfile = testdir.makefile(ext="xxx", hello="world") + testdir.makepyfile(x="") + testdir.maketxtfile(x="") + config = testdir.parseconfig() + dircol = config.getfsnode(checkfile.dirpath()) + colitems = dircol.collect() + assert len(colitems) == 1 + assert colitems[0].name == "hello.xxx" + assert colitems[0].__class__.__name__ == "CustomItem" + + item = config.getfsnode(checkfile) + assert item.name == "hello.xxx" + assert item.__class__.__name__ == "CustomItem" + +class TestCollectorReprs: + def test_repr_metainfo_basic_item(self, testdir): + modcol = testdir.getmodulecol("") Item = py.test.collect.Item item = Item("virtual", parent=modcol) info = item.repr_metainfo() @@ -575,15 +189,15 @@ class TestCollectorReprs(suptest.InlineCollection): assert not info.lineno assert info.modpath == "Item" - def test_repr_metainfo_func(self): - item = self.getitem("def test_func(): pass") + def test_repr_metainfo_func(self, testdir): + item = testdir.getitem("def test_func(): pass") info = item.repr_metainfo() assert info.fspath == item.fspath assert info.lineno == 0 assert info.modpath == "test_func" - def test_repr_metainfo_class(self): - modcol = self.getmodulecol(""" + def test_repr_metainfo_class(self, testdir): + modcol = testdir.getmodulecol(""" # lineno 0 class TestClass: def test_hello(self): pass @@ -594,8 +208,8 @@ class TestCollectorReprs(suptest.InlineCollection): assert info.lineno == 1 assert info.modpath == "TestClass" - def test_repr_metainfo_generator(self): - modcol = self.getmodulecol(""" + def test_repr_metainfo_generator(self, testdir): + modcol = testdir.getmodulecol(""" # lineno 0 def test_gen(): def check(x): @@ -624,56 +238,3 @@ class TestCollectorReprs(suptest.InlineCollection): def test_method(self): pass """ - -from py.__.test.dsession.mypickle import ImmutablePickler -class PickleTransport: - def __init__(self): - self.p1 = ImmutablePickler(uneven=0) - self.p2 = ImmutablePickler(uneven=1) - - def p1_to_p2(self, obj): - return self.p2.loads(self.p1.dumps(obj)) - - def p2_to_p1(self, obj): - return self.p1.loads(self.p2.dumps(obj)) - -class TestPickling(suptest.InlineCollection): - def setup_method(self, method): - super(TestPickling, self).setup_method(method) - pt = PickleTransport() - self.p1_to_p2 = pt.p1_to_p2 - self.p2_to_p1 = pt.p2_to_p1 - - def unifyconfig(self, config): - p2config = self.p1_to_p2(config) - p2config._initafterpickle(config.topdir) - return p2config - - def test_pickle_config(self): - config1 = py.test.config._reparse([]) - p2config = self.unifyconfig(config1) - assert p2config.topdir == config1.topdir - config_back = self.p2_to_p1(p2config) - assert config_back is config1 - - def test_pickle_module(self): - modcol1 = self.getmodulecol("def test_one(): pass") - self.unifyconfig(modcol1._config) - - modcol2a = self.p1_to_p2(modcol1) - modcol2b = self.p1_to_p2(modcol1) - assert modcol2a is modcol2b - - modcol1_back = self.p2_to_p1(modcol2a) - assert modcol1_back - - def test_pickle_func(self): - modcol1 = self.getmodulecol("def test_one(): pass") - self.unifyconfig(modcol1._config) - item = modcol1.collect_by_name("test_one") - item2a = self.p1_to_p2(item) - assert item is not item2a # of course - assert item2a.name == item.name - modback = self.p2_to_p1(item2a.parent) - assert modback is modcol1 - diff --git a/py/test/testing/test_collect_pickle.py b/py/test/testing/test_collect_pickle.py new file mode 100644 index 000000000..46696a4bc --- /dev/null +++ b/py/test/testing/test_collect_pickle.py @@ -0,0 +1,52 @@ +import py + +def pytest_pyfuncarg_pickletransport(pyfuncitem): + return PickleTransport() + +class PickleTransport: + def __init__(self): + from py.__.test.dsession.mypickle import ImmutablePickler + self.p1 = ImmutablePickler(uneven=0) + self.p2 = ImmutablePickler(uneven=1) + + def p1_to_p2(self, obj): + return self.p2.loads(self.p1.dumps(obj)) + + def p2_to_p1(self, obj): + return self.p1.loads(self.p2.dumps(obj)) + + def unifyconfig(self, config): + p2config = self.p1_to_p2(config) + p2config._initafterpickle(config.topdir) + return p2config + +class TestPickling: + + def test_pickle_config(self, pickletransport): + config1 = py.test.config._reparse([]) + p2config = pickletransport.unifyconfig(config1) + assert p2config.topdir == config1.topdir + config_back = pickletransport.p2_to_p1(p2config) + assert config_back is config1 + + def test_pickle_module(self, testdir, pickletransport): + modcol1 = testdir.getmodulecol("def test_one(): pass") + pickletransport.unifyconfig(modcol1._config) + + modcol2a = pickletransport.p1_to_p2(modcol1) + modcol2b = pickletransport.p1_to_p2(modcol1) + assert modcol2a is modcol2b + + modcol1_back = pickletransport.p2_to_p1(modcol2a) + assert modcol1_back + + def test_pickle_func(self, testdir, pickletransport): + modcol1 = testdir.getmodulecol("def test_one(): pass") + pickletransport.unifyconfig(modcol1._config) + item = modcol1.collect_by_name("test_one") + item2a = pickletransport.p1_to_p2(item) + assert item is not item2a # of course + assert item2a.name == item.name + modback = pickletransport.p2_to_p1(item2a.parent) + assert modback is modcol1 + diff --git a/py/test/testing/test_config.py b/py/test/testing/test_config.py index ddc5e2eb4..9a7d0e255 100644 --- a/py/test/testing/test_config.py +++ b/py/test/testing/test_config.py @@ -1,302 +1,110 @@ -from __future__ import generators import py -from py.__.test.config import gettopdir -from py.__.test.testing import suptest -from py.__.test import event +pytest_plugins = 'pytest_iocapture' -def getcolitems(config): - return [config.getfsnode(arg) for arg in config.args] - -def test_tmpdir(): - d1 = py.test.ensuretemp('hello') - d2 = py.test.ensuretemp('hello') - assert d1 == d2 - assert d1.check(dir=1) - -def test_config_cmdline_options(): - o = py.test.ensuretemp('configoptions') - o.ensure("conftest.py").write(py.code.Source(""" - import py - def _callback(option, opt_str, value, parser, *args, **kwargs): - option.tdest = True - Option = py.test.config.Option - option = py.test.config.addoptions("testing group", - Option('-G', '--glong', action="store", default=42, - type="int", dest="gdest", help="g value."), - # XXX note: special case, option without a destination - Option('-T', '--tlong', action="callback", callback=_callback, - help='t value'), - ) - """)) - old = o.chdir() - try: - config = py.test.config._reparse(['-G', '17']) - finally: - old.chdir() - assert config.option.gdest == 17 - -def test_config_cmdline_options_only_lowercase(): - o = py.test.ensuretemp('test_config_cmdline_options_only_lowercase') - o = o.mkdir("onemore") # neccessary because collection of o.dirpath() - # could see our conftest.py - o.ensure("conftest.py").write(py.code.Source(""" - import py - Option = py.test.config.Option - options = py.test.config.addoptions("testing group", - Option('-g', '--glong', action="store", default=42, - type="int", dest="gdest", help="g value."), - ) - """)) - old = o.chdir() - try: - py.test.raises(ValueError, """ - py.test.config._reparse(['-g', '17']) - """) - finally: - old.chdir() - -def test_parsing_again_fails(): - dir = py.test.ensuretemp("parsing_again_fails") - config = py.test.config._reparse([str(dir)]) - py.test.raises(AssertionError, "config.parse([])") - -def test_config_getvalue_honours_conftest(): - o = py.test.ensuretemp('testconfigget') - o.ensure("conftest.py").write("x=1") - o.ensure("sub", "conftest.py").write("x=2 ; y = 3") - config = py.test.config._reparse([str(o)]) - assert config.getvalue("x") == 1 - assert config.getvalue("x", o.join('sub')) == 2 - py.test.raises(KeyError, "config.getvalue('y')") - config = py.test.config._reparse([str(o.join('sub'))]) - assert config.getvalue("x") == 2 - assert config.getvalue("y") == 3 - assert config.getvalue("x", o) == 1 - py.test.raises(KeyError, 'config.getvalue("y", o)') - - -def test_config_overwrite(): - o = py.test.ensuretemp('testconfigget') - o.ensure("conftest.py").write("x=1") - config = py.test.config._reparse([str(o)]) - assert config.getvalue('x') == 1 - config.option.x = 2 - assert config.getvalue('x') == 2 - config = py.test.config._reparse([str(o)]) - assert config.getvalue('x') == 1 - -def test_gettopdir(): - tmp = py.test.ensuretemp("topdir") - assert gettopdir([tmp]) == tmp - topdir =gettopdir([tmp.join("hello"), tmp.join("world")]) - assert topdir == tmp - somefile = tmp.ensure("somefile.py") - assert gettopdir([somefile]) == tmp - -def test_gettopdir_pypkg(): - tmp = py.test.ensuretemp("topdir2") - a = tmp.ensure('a', dir=1) - b = tmp.ensure('a', 'b', '__init__.py') - c = tmp.ensure('a', 'b', 'c.py') - Z = tmp.ensure('Z', dir=1) - assert gettopdir([c]) == a - assert gettopdir([c, Z]) == tmp - - -def test_config_initafterpickle_some(): - tmp = py.test.ensuretemp("test_config_initafterpickle_some") - tmp.ensure("__init__.py") - tmp.ensure("conftest.py").write("x=1 ; y=2") - hello = tmp.ensure("test_hello.py") - config = py.test.config._reparse([hello]) - config2 = py.test.config._reparse([tmp.dirpath()]) - config2._initialized = False # we have to do that from tests - config2._repr = config._makerepr() - config2._initafterpickle(topdir=tmp.dirpath()) - for col1, col2 in zip(getcolitems(config), getcolitems(config2)): - assert col1.fspath == col2.fspath - cols = getcolitems(config2) - assert len(cols) == 1 - col = cols[0] - assert col.name == 'test_hello.py' - assert col.parent.name == tmp.basename - assert col.parent.parent is None - -def test_config_make_and__mergerepr(): - tmp = py.test.ensuretemp("reprconfig1") - tmp.ensure("__init__.py") - tmp.ensure("conftest.py").write("x=1") - config = py.test.config._reparse([tmp]) - repr = config._makerepr() - config.option.verbose = 42 - repr2 = config._makerepr() - config = py.test.config._reparse([tmp.dirpath()]) - py.test.raises(KeyError, "config.getvalue('x')") - config._mergerepr(repr) - assert config.getvalue('x') == 1 - config._mergerepr(repr2) - assert config.option.verbose == 42 - - -def test_config_rconfig(): - tmp = py.test.ensuretemp("rconfigopt") - tmp.ensure("__init__.py") - tmp.ensure("conftest.py").write(py.code.Source(""" - import py - Option = py.test.config.Option - option = py.test.config.addoptions("testing group", - Option('-G', '--glong', action="store", default=42, - type="int", dest="gdest", help="g value.")) - """)) - config = py.test.config._reparse([tmp, "-G", "11"]) - assert config.option.gdest == 11 - repr = config._makerepr() - config = py.test.config._reparse([tmp.dirpath()]) - py.test.raises(AttributeError, "config.option.gdest") - config._mergerepr(repr) - option = config.addoptions("testing group", - config.Option('-G', '--glong', action="store", default=42, - type="int", dest="gdest", help="g value.")) - assert config.option.gdest == 11 - assert option.gdest == 11 - -class TestSessionAndOptions(suptest.FileCreation): - def exampletestfile(self): - return self.makepyfile(file_test=""" - def test_one(): - assert 42 == 43 - - class TestClass(object): - def test_method_one(self): - assert 42 == 43 - """) - - def test_session_eventlog(self): - eventlog = self.tmpdir.join("test_session_eventlog") - config = py.test.config._reparse([self.tmpdir, - '--eventlog=%s' % eventlog]) - session = config.initsession() - session.bus.notify(event.TestrunStart()) - s = eventlog.read() - assert s.find("TestrunStart") != -1 - - def test_session_resultlog(self): - from py.__.test.collect import Item - from py.__.test.runner import OutcomeRepr - - resultlog = self.tmpdir.join("test_session_resultlog") - config = py.test.config._reparse([self.tmpdir, - '--resultlog=%s' % resultlog]) - - session = config.initsession() - - item = Item("a", config=config) - outcome = OutcomeRepr('execute', '.', '') - rep_ev = event.ItemTestReport(item, passed=outcome) - - session.bus.notify(rep_ev) +class TestConfigCmdlineParsing: + @py.test.keywords(xfail="commit parser") + def test_config_addoption(self, stdcapture): + from py.__.test.config import Config + config = Config() + config.addoption("cat1", "--option1", action="store_true") + config.addoption("cat1", "--option2", action="store_true") + config.parse(["-h"]) + out, err = stdcapture.reset() + assert out.count("cat1") == 1 + assert out.find("option1") != -1 + assert out.find("option2") != -1 - s = resultlog.read() - assert s.find(". a") != -1 + def test_config_cmdline_options(self, testdir): + testdir.makepyfile(conftest=""" + import py + def _callback(option, opt_str, value, parser, *args, **kwargs): + option.tdest = True + Option = py.test.config.Option + option = py.test.config.addoptions("testing group", + Option('-G', '--glong', action="store", default=42, + type="int", dest="gdest", help="g value."), + # XXX note: special case, option without a destination + Option('-T', '--tlong', action="callback", callback=_callback, + help='t value'), + ) + """) + testdir.chdir() + config = py.test.config._reparse(['-G', '17']) + assert config.option.gdest == 17 - def test_tracedir_tracer(self): - tracedir = self.tmpdir.join("tracedir") - config = py.test.config._reparse([self.tmpdir, - '--tracedir=%s' % tracedir]) - assert config.gettracedir() == tracedir + def test_config_cmdline_options_only_lowercase(self, testdir): + testdir.makepyfile(conftest=""" + import py + Option = py.test.config.Option + options = py.test.config.addoptions("testing group", + Option('-g', '--glong', action="store", default=42, + type="int", dest="gdest", help="g value."), + ) + """) + old = testdir.chdir() + try: + py.test.raises(ValueError, """ + py.test.config._reparse(['-g', '17']) + """) + finally: + old.chdir() - trace = config.maketrace("trace1.log") # flush=True by default - trace("hello", "world") - class A: pass - trace(A()) - p = tracedir.join("trace1.log") - lines = p.readlines(cr=0) - assert lines[0].endswith("hello world") - assert lines[1].find("A") != -1 - trace.close() - - def test_trace_null(self): - config = py.test.config._reparse([self.tmpdir]) - assert config.gettracedir() is None - trace = config.maketrace("hello", flush=True) - trace("hello", "world") - trace.close() - - def test_implied_dsession(self): - for x in 'startserver runbrowser rest'.split(): - config = py.test.config._reparse([self.tmpdir, '--dist', '--%s' % x]) - assert config._getsessionname() == 'DSession' - - def test_implied_different_sessions(self): - config = py.test.config._reparse([self.tmpdir]) - assert config._getsessionname() == 'Session' - config = py.test.config._reparse([self.tmpdir, '--dist']) - assert config._getsessionname() == 'DSession' - config = py.test.config._reparse([self.tmpdir, '-n3']) - assert config._getsessionname() == 'DSession' - config = py.test.config._reparse([self.tmpdir, '--looponfailing']) - assert config._getsessionname() == 'LooponfailingSession' - config = py.test.config._reparse([self.tmpdir, '--exec=x']) - assert config._getsessionname() == 'DSession' - config = py.test.config._reparse([self.tmpdir, '--dist', '--exec=x']) - assert config._getsessionname() == 'DSession' - config = py.test.config._reparse([self.tmpdir, '-f', - '--dist', '--exec=x']) - assert config._getsessionname() == 'LooponfailingSession' - config = py.test.config._reparse([self.tmpdir, '-f', '-n3', - '--dist', '--exec=x', - '--collectonly']) - assert config._getsessionname() == 'Session' - - def test_sessionname_lookup_custom(self): - self.tmpdir.join("conftest.py").write(py.code.Source(""" - from py.__.test.session import Session - class MySession(Session): - pass - """)) - config = py.test.config._reparse(["--session=MySession", self.tmpdir]) - session = config.initsession() - assert session.__class__.__name__ == 'MySession' - - def test_initsession(self): - config = py.test.config._reparse([self.tmpdir]) - session = config.initsession() - assert session.config is config - - def test_boxed_option_default(self): - self.tmpdir.join("conftest.py").write("dist_hosts=[]") - tmpdir = self.tmpdir.ensure("subdir", dir=1) + def test_parsing_again_fails(self, tmpdir): config = py.test.config._reparse([tmpdir]) - config.initsession() - assert not config.option.boxed - config = py.test.config._reparse(['--dist', tmpdir]) - config.initsession() - assert not config.option.boxed + py.test.raises(AssertionError, "config.parse([])") - def test_boxed_option_from_conftest(self): - self.tmpdir.join("conftest.py").write("dist_hosts=[]") - tmpdir = self.tmpdir.ensure("subdir", dir=1) - tmpdir.join("conftest.py").write(py.code.Source(""" - dist_hosts = [] - dist_boxed = True - """)) - config = py.test.config._reparse(['--dist', tmpdir]) - config.initsession() - assert config.option.boxed + def test_conflict_options(self): + def check_conflict_option(opts): + print "testing if options conflict:", " ".join(opts) + config = py.test.config._reparse(opts) + py.test.raises((ValueError, SystemExit), """ + config.initsession() + """) + py.test.skip("check on conflict options") + conflict_options = ( + '--looponfailing --pdb', + '--dist --pdb', + '--exec=%s --pdb' % (py.std.sys.executable,), + ) + for spec in conflict_options: + opts = spec.split() + yield check_conflict_option, opts - def test_boxed_option_from_conftest(self): - tmpdir = self.tmpdir - tmpdir.join("conftest.py").write(py.code.Source(""" - dist_boxed = False - """)) - config = py.test.config._reparse([tmpdir, '--box']) - assert config.option.boxed - config.initsession() - assert config.option.boxed - def test_getvalue_pathlist(self): - tmpdir = self.tmpdir +class TestConfigAPI: + @py.test.keywords(issue="ensuretemp should call config.maketemp(basename)") + def test_tmpdir(self): + d1 = py.test.ensuretemp('hello') + d2 = py.test.ensuretemp('hello') + assert d1 == d2 + assert d1.check(dir=1) + + def test_config_getvalue_honours_conftest(self, testdir): + testdir.makepyfile(conftest="x=1") + testdir.mkdir("sub").join("conftest.py").write("x=2 ; y = 3") + config = testdir.parseconfig() + o = testdir.tmpdir + assert config.getvalue("x") == 1 + assert config.getvalue("x", o.join('sub')) == 2 + py.test.raises(KeyError, "config.getvalue('y')") + config = py.test.config._reparse([str(o.join('sub'))]) + assert config.getvalue("x") == 2 + assert config.getvalue("y") == 3 + assert config.getvalue("x", o) == 1 + py.test.raises(KeyError, 'config.getvalue("y", o)') + + def test_config_overwrite(self, testdir): + o = testdir.tmpdir + o.ensure("conftest.py").write("x=1") + config = py.test.config._reparse([str(o)]) + assert config.getvalue('x') == 1 + config.option.x = 2 + assert config.getvalue('x') == 2 + config = py.test.config._reparse([str(o)]) + assert config.getvalue('x') == 1 + + def test_getvalue_pathlist(self, tmpdir): somepath = tmpdir.join("x", "y", "z") p = tmpdir.join("conftest.py") p.write("pathlist = ['.', %r]" % str(somepath)) @@ -312,10 +120,109 @@ class TestSessionAndOptions(suptest.FileCreation): pl = config.getvalue_pathlist('mypathlist') assert pl == [py.path.local()] - def test_config_iocapturing(self): - config = py.test.config._reparse([self.tmpdir]) +class TestConfigApi_getcolitems: + def test_getcolitems_onedir(self, tmpdir): + config = py.test.config._reparse([tmpdir]) + colitems = config.getcolitems() + assert len(colitems) == 1 + col = colitems[0] + assert isinstance(col, py.test.collect.Directory) + for col in col.listchain(): + assert col._config is config + + def test_getcolitems_twodirs(self, tmpdir): + config = py.test.config._reparse([tmpdir, tmpdir]) + colitems = config.getcolitems() + assert len(colitems) == 2 + col1, col2 = colitems + assert col1.name == col2.name + assert col1.parent == col2.parent + + def test_getcolitems_curdir_and_subdir(self, tmpdir): + a = tmpdir.ensure("a", dir=1) + config = py.test.config._reparse([tmpdir, a]) + colitems = config.getcolitems() + assert len(colitems) == 2 + col1, col2 = colitems + assert col1.name == tmpdir.basename + assert col2.name == 'a' + for col in colitems: + for subcol in col.listchain(): + assert col._config is config + + def test__getcol_global_file(self, tmpdir): + x = tmpdir.ensure("x.py") + config = py.test.config._reparse([x]) + col = config.getfsnode(x) + assert isinstance(col, py.test.collect.Module) + assert col.name == 'x.py' + assert col.parent.name == tmpdir.basename + assert col.parent.parent is None + for col in col.listchain(): + assert col._config is config + + def test__getcol_global_dir(self, tmpdir): + x = tmpdir.ensure("a", dir=1) + config = py.test.config._reparse([x]) + col = config.getfsnode(x) + assert isinstance(col, py.test.collect.Directory) + print col.listchain() + assert col.name == 'a' + assert col.parent is None + assert col._config is config + + def test__getcol_pkgfile(self, tmpdir): + x = tmpdir.ensure("x.py") + tmpdir.ensure("__init__.py") + config = py.test.config._reparse([x]) + col = config.getfsnode(x) + assert isinstance(col, py.test.collect.Module) + assert col.name == 'x.py' + assert col.parent.name == x.dirpath().basename + assert col.parent.parent is None + for col in col.listchain(): + assert col._config is config + + + + +class TestOptionEffects: + def test_boxed_option_default(self, testdir): + testdir.makepyfile(conftest="dist_hosts=[]") + tmpdir = testdir.tmpdir.ensure("subdir", dir=1) + config = py.test.config._reparse([tmpdir]) + config.initsession() + assert not config.option.boxed + config = py.test.config._reparse(['--dist', tmpdir]) + config.initsession() + assert not config.option.boxed + + def test_is_not_boxed_by_default(self, testdir): + config = py.test.config._reparse([testdir.tmpdir]) + assert not config.option.boxed + + def test_boxed_option_from_conftest(self, testdir): + testdir.makepyfile(conftest="dist_hosts=[]") + tmpdir = testdir.tmpdir.ensure("subdir", dir=1) + tmpdir.join("conftest.py").write(py.code.Source(""" + dist_hosts = [] + dist_boxed = True + """)) + config = py.test.config._reparse(['--dist', tmpdir]) + config.initsession() + assert config.option.boxed + + def test_boxed_option_from_conftest(self, testdir): + testdir.makepyfile(conftest="dist_boxed=False") + config = py.test.config._reparse([testdir.tmpdir, '--box']) + assert config.option.boxed + config.initsession() + assert config.option.boxed + + def test_config_iocapturing(self, testdir): + config = testdir.parseconfig(testdir.tmpdir) assert config.getvalue("conf_iocapture") - tmpdir = self.tmpdir.ensure("sub-with-conftest", dir=1) + tmpdir = testdir.tmpdir.ensure("sub-with-conftest", dir=1) tmpdir.join("conftest.py").write(py.code.Source(""" conf_iocapture = "no" """)) @@ -334,130 +241,105 @@ class TestSessionAndOptions(suptest.FileCreation): capture = config._getcapture() assert isinstance(capture, cls) - def test_conflict_options(self): - def check_conflict_option(opts): - print "testing if options conflict:", " ".join(opts) - path = self.exampletestfile() - config = py.test.config._reparse(opts + [path]) - py.test.raises((ValueError, SystemExit), """ - config.initsession() - """) - py.test.skip("check on conflict options") - conflict_options = ( - '--looponfailing --pdb', - '--dist --pdb', - '--exec=%s --pdb' % (py.std.sys.executable,), - ) - for spec in conflict_options: - opts = spec.split() - yield check_conflict_option, opts - - def test_implied_options(self): - def check_implied_option(opts, expr): - path = self.exampletestfile() - config = py.test.config._reparse(opts + [path]) - session = config.initsession() - assert eval(expr, session.config.option.__dict__) - implied_options = { - '-v': 'verbose', - '-l': 'showlocals', - #'--runbrowser': 'startserver and runbrowser', XXX starts browser - } - for key, expr in implied_options.items(): - yield check_implied_option, [key], expr +class TestConfig_gettopdir: + def test_gettopdir(self, testdir): + from py.__.test.config import gettopdir + tmp = testdir.tmpdir + assert gettopdir([tmp]) == tmp + topdir = gettopdir([tmp.join("hello"), tmp.join("world")]) + assert topdir == tmp + somefile = tmp.ensure("somefile.py") + assert gettopdir([somefile]) == tmp - def test_default_session_options(self): - def runfiletest(opts): - sorter = suptest.events_from_cmdline(opts) - passed, skipped, failed = sorter.countoutcomes() - assert failed == 2 - assert skipped == passed == 0 - path = self.exampletestfile() - for opts in ([], ['-l'], ['-s'], ['--tb=no'], ['--tb=short'], - ['--tb=long'], ['--fulltrace'], ['--nomagic'], - ['--traceconfig'], ['-v'], ['-v', '-v']): - yield runfiletest, opts + [path] + def test_gettopdir_pypkg(self, testdir): + from py.__.test.config import gettopdir + tmp = testdir.tmpdir + a = tmp.ensure('a', dir=1) + b = tmp.ensure('a', 'b', '__init__.py') + c = tmp.ensure('a', 'b', 'c.py') + Z = tmp.ensure('Z', dir=1) + assert gettopdir([c]) == a + assert gettopdir([c, Z]) == tmp - def test_is_not_boxed_by_default(self): - path = self.exampletestfile() - config = py.test.config._reparse([path]) - assert not config.option.boxed +class TestConfigPickling: + @py.test.keywords(xfail=True, issue="config's pytestplugins/bus initialization") + def test_config_initafterpickle_plugin(self, testdir): + testdir.makepyfile(__init__="", conftest="x=1; y=2") + hello = testdir.makepyfile(hello="") + tmp = testdir.tmpdir + config = py.test.config._reparse([hello]) + config2 = py.test.config._reparse([tmp.dirpath()]) + config2._initialized = False # we have to do that from tests + config2._repr = config._makerepr() + config2._initafterpickle(topdir=tmp.dirpath()) + # we check that config "remote" config objects + # have correct plugin initialization + #XXX assert config2.pytestplugins.pm._plugins + #XXX assert config2.bus.isregistered(config2.pytestplugins.forward_event) + assert config2.bus == py._com.pyplugins + assert config2.pytestplugins.pm == py._com.pyplugins + def test_config_initafterpickle_some(self, testdir): + tmp = testdir.tmpdir + tmp.ensure("__init__.py") + tmp.ensure("conftest.py").write("x=1 ; y=2") + hello = tmp.ensure("test_hello.py") + config = py.test.config._reparse([hello]) + config2 = py.test.config._reparse([tmp.dirpath()]) + config2._initialized = False # we have to do that from tests + config2._repr = config._makerepr() + config2._initafterpickle(topdir=tmp.dirpath()) -class TestConfigColitems(suptest.FileCreation): - def setup_class(cls): - cls.tmproot = py.test.ensuretemp(cls.__name__) + for col1, col2 in zip(config.getcolitems(), config2.getcolitems()): + assert col1.fspath == col2.fspath + cols = config2.getcolitems() + assert len(cols) == 1 + col = cols[0] + assert col.name == 'test_hello.py' + assert col.parent.name == tmp.basename + assert col.parent.parent is None - def setup_method(self, method): - self.tmpdir = self.tmproot.mkdir(method.__name__) - - def test_getcolitems_onedir(self): - config = py.test.config._reparse([self.tmpdir]) - colitems = getcolitems(config) - assert len(colitems) == 1 - col = colitems[0] - assert isinstance(col, py.test.collect.Directory) - for col in col.listchain(): - assert col._config is config + def test_config_make_and__mergerepr(self, testdir): + tmp = testdir.tmpdir + tmp.ensure("__init__.py") + tmp.ensure("conftest.py").write("x=1") + config = py.test.config._reparse([tmp]) + repr = config._makerepr() + config.option.verbose = 42 + repr2 = config._makerepr() + config = py.test.config._reparse([tmp.dirpath()]) + py.test.raises(KeyError, "config.getvalue('x')") + config._mergerepr(repr) + assert config.getvalue('x') == 1 + config._mergerepr(repr2) + assert config.option.verbose == 42 + + def test_config_rconfig(self, testdir): + tmp = testdir.tmpdir + tmp.ensure("__init__.py") + tmp.ensure("conftest.py").write(py.code.Source(""" + import py + Option = py.test.config.Option + option = py.test.config.addoptions("testing group", + Option('-G', '--glong', action="store", default=42, + type="int", dest="gdest", help="g value.")) + """)) + config = py.test.config._reparse([tmp, "-G", "11"]) + assert config.option.gdest == 11 + repr = config._makerepr() + config = py.test.config._reparse([tmp.dirpath()]) + py.test.raises(AttributeError, "config.option.gdest") + config._mergerepr(repr) + option = config.addoptions("testing group", + config.Option('-G', '--glong', action="store", default=42, + type="int", dest="gdest", help="g value.")) + assert config.option.gdest == 11 + assert option.gdest == 11 - def test_getcolitems_twodirs(self): - config = py.test.config._reparse([self.tmpdir, self.tmpdir]) - colitems = getcolitems(config) - assert len(colitems) == 2 - col1, col2 = colitems - assert col1.name == col2.name - assert col1.parent == col2.parent - - def test_getcolitems_curdir_and_subdir(self): - a = self.tmpdir.ensure("a", dir=1) - config = py.test.config._reparse([self.tmpdir, a]) - colitems = getcolitems(config) - assert len(colitems) == 2 - col1, col2 = colitems - assert col1.name == self.tmpdir.basename - assert col2.name == 'a' - for col in colitems: - for subcol in col.listchain(): - assert col._config is config - - def test__getcol_global_file(self): - x = self.tmpdir.ensure("x.py") - config = py.test.config._reparse([x]) - col = config.getfsnode(x) - assert isinstance(col, py.test.collect.Module) - assert col.name == 'x.py' - assert col.parent.name == self.tmpdir.basename - assert col.parent.parent is None - for col in col.listchain(): - assert col._config is config - - def test__getcol_global_dir(self): - x = self.tmpdir.ensure("a", dir=1) - config = py.test.config._reparse([x]) - col = config.getfsnode(x) - assert isinstance(col, py.test.collect.Directory) - print col.listchain() - assert col.name == 'a' - assert col.parent is None - assert col._config is config - - def test__getcol_pkgfile(self): - x = self.tmpdir.ensure("x.py") - self.tmpdir.ensure("__init__.py") - config = py.test.config._reparse([x]) - col = config.getfsnode(x) - assert isinstance(col, py.test.collect.Module) - assert col.name == 'x.py' - assert col.parent.name == x.dirpath().basename - assert col.parent.parent is None - for col in col.listchain(): - assert col._config is config - - - def test_config_picklability(self): + def test_config_picklability(self, tmpdir): import cPickle - config = py.test.config._reparse([self.tmpdir]) + config = py.test.config._reparse([tmpdir]) s = cPickle.dumps(config) newconfig = cPickle.loads(s) assert not hasattr(newconfig, "topdir") @@ -466,11 +348,11 @@ class TestConfigColitems(suptest.FileCreation): newconfig._initafterpickle(config.topdir) assert newconfig.topdir == config.topdir assert newconfig._initialized - assert newconfig.args == [self.tmpdir] + assert newconfig.args == [tmpdir] - def test_config_and_collector_pickling_missing_initafter(self): + def test_config_and_collector_pickling_missing_initafter(self, tmpdir): from cPickle import Pickler, Unpickler - config = py.test.config._reparse([self.tmpdir]) + config = py.test.config._reparse([tmpdir]) col = config.getfsnode(config.topdir) io = py.std.cStringIO.StringIO() pickler = Pickler(io) @@ -482,10 +364,10 @@ class TestConfigColitems(suptest.FileCreation): # we don't call _initafterpickle ... so py.test.raises(ValueError, "unpickler.load()") - def test_config_and_collector_pickling(self): + def test_config_and_collector_pickling(self, tmpdir): from cPickle import Pickler, Unpickler - dir1 = self.tmpdir.ensure("somedir", dir=1) - config = py.test.config._reparse([self.tmpdir]) + dir1 = tmpdir.ensure("somedir", dir=1) + config = py.test.config._reparse([tmpdir]) col = config.getfsnode(config.topdir) col1 = col.join(dir1.basename) assert col1.parent is col @@ -498,7 +380,7 @@ class TestConfigColitems(suptest.FileCreation): io.seek(0) unpickler = Unpickler(io) newconfig = unpickler.load() - topdir = self.tmpdir.ensure("newtopdir", dir=1) + topdir = tmpdir.ensure("newtopdir", dir=1) newconfig._initafterpickle(topdir) topdir.ensure("somedir", dir=1) newcol = unpickler.load() @@ -513,3 +395,64 @@ class TestConfigColitems(suptest.FileCreation): assert newcol2.fspath.basename == dir1.basename assert newcol2.fspath.relto(topdir) +class TestSessionAndOptions: + def test_implied_dsession(self, testdir): + for x in 'startserver runbrowser rest'.split(): + config = testdir.parseconfig(testdir.tmpdir, '--dist', '--%s' % x) + assert config._getestdirname() == 'DSession' + + def test_implied_different_sessions(self, tmpdir): + config = py.test.config._reparse([tmpdir]) + assert config._getestdirname() == 'Session' + config = py.test.config._reparse([tmpdir, '--dist']) + assert config._getestdirname() == 'DSession' + config = py.test.config._reparse([tmpdir, '-n3']) + assert config._getestdirname() == 'DSession' + config = py.test.config._reparse([tmpdir, '--looponfailing']) + assert config._getestdirname() == 'LooponfailingSession' + config = py.test.config._reparse([tmpdir, '--exec=x']) + assert config._getestdirname() == 'DSession' + config = py.test.config._reparse([tmpdir, '--dist', '--exec=x']) + assert config._getestdirname() == 'DSession' + config = py.test.config._reparse([tmpdir, '-f', + '--dist', '--exec=x']) + assert config._getestdirname() == 'LooponfailingSession' + config = py.test.config._reparse([tmpdir, '-f', '-n3', + '--dist', '--exec=x', + '--collectonly']) + assert config._getestdirname() == 'Session' + + def test_sessionname_lookup_custom(self, testdir): + testdir.makepyfile(conftest=""" + from py.__.test.session import Session + class MySession(Session): + pass + """) + config = testdir.parseconfig("--session=MySession", testdir.tmpdir) + session = config.initsession() + assert session.__class__.__name__ == 'MySession' + + def test_initsession(self, tmpdir): + config = py.test.config._reparse([tmpdir]) + session = config.initsession() + assert session.config is config + + def test_default_session_options(self, testdir): + def runfiletest(opts): + sorter = testdir.inline_run(*opts) + passed, skipped, failed = sorter.countoutcomes() + assert failed == 2 + assert skipped == passed == 0 + path = testdir.makepyfile(""" + def test_f1(): assert 0 + def test_f2(): assert 0 + """) + + for opts in ([], ['-l'], ['-s'], ['--tb=no'], ['--tb=short'], + ['--tb=long'], ['--fulltrace'], ['--nomagic'], + ['--traceconfig'], ['-v'], ['-v', '-v']): + runfiletest(opts + [path]) + +def test_default_bus(): + assert py.test.config.bus is py._com.pyplugins + diff --git a/py/test/testing/test_conftesthandle.py b/py/test/testing/test_conftesthandle.py index 812b171f4..750500209 100644 --- a/py/test/testing/test_conftesthandle.py +++ b/py/test/testing/test_conftesthandle.py @@ -16,6 +16,15 @@ class TestConftestValueAccessGlobal: conftest.setinitial([self.basedir.join("adir")]) assert conftest.rget("a") == 1 + def test_onimport(self): + l = [] + conftest = Conftest(onimport=l.append) + conftest.setinitial([self.basedir.join("adir")]) + assert len(l) == 2 # default + the one + assert conftest.rget("a") == 1 + assert conftest.rget("b", self.basedir.join("adir", "b")) == 2 + assert len(l) == 3 + def test_immediate_initialiation_and_incremental_are_the_same(self): conftest = Conftest() snap0 = len(conftest._path2confmods) diff --git a/py/test/testing/test_deprecated_api.py b/py/test/testing/test_deprecated_api.py index 8de8b6a44..9f4ac9de0 100644 --- a/py/test/testing/test_deprecated_api.py +++ b/py/test/testing/test_deprecated_api.py @@ -1,18 +1,17 @@ import py -from py.__.test.testing import suptest -class TestCollectDeprecated(suptest.InlineCollection): - def test_directory_run_join_warnings(self): - p = self.makepyfile(test_one="") - config = self.parseconfig() +class TestCollectDeprecated: + def test_directory_run_join_warnings(self, testdir): + p = testdir.makepyfile(test_one="") + config = testdir.parseconfig(p) dirnode = config.getfsnode(p.dirpath()) py.test.deprecated_call(dirnode.run) # XXX for directories we still have join() #py.test.deprecated_call(dirnode.join, 'test_one') - def test_collect_with_deprecated_run_and_join(self): - self.makepyfile(conftest=""" + def test_collect_with_deprecated_run_and_join(self, testdir): + testdir.makepyfile(conftest=""" import py class MyInstance(py.test.collect.Instance): @@ -46,12 +45,12 @@ class TestCollectDeprecated(suptest.InlineCollection): return self.Module(self.fspath.join(name), parent=self) Directory = MyDirectory """) - p = self.makepyfile(somefile=""" + p = testdir.makepyfile(somefile=""" def check(): pass class Cls: def check2(self): pass """) - config = self.parseconfig() + config = testdir.parseconfig() dirnode = config.getfsnode(p.dirpath()) colitems = py.test.deprecated_call(dirnode.collect) assert len(colitems) == 1 @@ -69,8 +68,8 @@ class TestCollectDeprecated(suptest.InlineCollection): assert len(colitems) == 1 assert colitems[0].name == 'check2' - def test_collect_with_deprecated_join_but_no_run(self): - self.makepyfile(conftest=""" + def test_collect_with_deprecated_join_but_no_run(self, testdir): + testdir.makepyfile(conftest=""" import py class Module(py.test.collect.Module): @@ -83,7 +82,7 @@ class TestCollectDeprecated(suptest.InlineCollection): return self.Function(name, parent=self) assert name != "SomeClass", "join should not be called with this name" """) - col = self.getmodulecol(""" + col = testdir.getmodulecol(""" def somefunc(): pass def check_one(): pass class SomeClass: pass @@ -93,62 +92,62 @@ class TestCollectDeprecated(suptest.InlineCollection): funcitem = colitems[0] assert funcitem.name == "check_one" - def test_function_custom_run(self): - self.makepyfile(conftest=""" + def test_function_custom_run(self, testdir): + testdir.makepyfile(conftest=""" import py class MyFunction(py.test.collect.Function): def run(self): pass Function=MyFunction """) - modcol = self.getmodulecol("def test_func(): pass") + modcol = testdir.getmodulecol("def test_func(): pass") funcitem = modcol.collect()[0] assert funcitem.name == 'test_func' py.test.deprecated_call(funcitem.runtest) - def test_function_custom_execute(self): - self.makepyfile(conftest=""" + def test_function_custom_execute(self, testdir): + testdir.makepyfile(conftest=""" import py class MyFunction(py.test.collect.Function): def execute(self, obj, *args): pass Function=MyFunction """) - modcol = self.getmodulecol("def test_func(): pass") + modcol = testdir.getmodulecol("def test_func(): pass") funcitem = modcol.collect()[0] assert funcitem.name == 'test_func' py.test.deprecated_call(funcitem.runtest) - def test_function_deprecated_run_execute(self): - modcol = self.getmodulecol("def test_some(): pass") + def test_function_deprecated_run_execute(self, testdir): + modcol = testdir.getmodulecol("def test_some(): pass") funcitem = modcol.collect()[0] py.test.deprecated_call(funcitem.run) py.test.deprecated_call(funcitem.execute, funcitem.obj) - def test_function_deprecated_run_recursive(self): - self.makepyfile(conftest=""" + def test_function_deprecated_run_recursive(self, testdir): + testdir.makepyfile(conftest=""" import py class Module(py.test.collect.Module): def run(self): return super(Module, self).run() """) - modcol = self.getmodulecol("def test_some(): pass") + modcol = testdir.getmodulecol("def test_some(): pass") colitems = py.test.deprecated_call(modcol.collect) funcitem = colitems[0] - def test_conftest_subclasses_Module_with_non_pyfile(self): - self.makepyfile(conftest=""" + def test_conftest_subclasses_Module_with_non_pyfile(self, testdir): + testdir.makepyfile(conftest=""" import py class Module(py.test.collect.Module): def run(self): return [] class Directory(py.test.collect.Directory): - def consider_file(self, path, usefilters=True): + def consider_file(self, path): if path.basename == "testme.xxx": return Module(path, parent=self) - return super(Directory, self).consider_file(path, usefilters=usefilters) + return super(Directory, self).consider_file(path) """) - testme = self._makefile('xxx', testme="hello") - config = self.parseconfig(testme) + testme = testdir.makefile('xxx', testme="hello") + config = testdir.parseconfig(testme) col = config.getfsnode(testme) assert col.collect() == [] diff --git a/py/test/testing/test_doctest.py b/py/test/testing/test_doctest.py deleted file mode 100644 index a19694795..000000000 --- a/py/test/testing/test_doctest.py +++ /dev/null @@ -1,34 +0,0 @@ - -import py -from py.__.test.outcome import Failed -from py.__.test.testing.suptest import InlineCollection - -def setup_module(mod): - mod.tmp = py.test.ensuretemp(__name__) - -class TestDoctests(InlineCollection): - def test_simple_docteststring(self): - txtfile = self.maketxtfile(test_doc=""" - >>> i = 0 - >>> i + 1 - 1 - """) - config = self.parseconfig(txtfile) - col = config.getfsnode(txtfile) - testitem = col.join(txtfile.basename) - res = testitem.runtest() - assert res is None - - - def test_doctest_unexpected_exception(self): - py.test.skip("implement nice doctest repr for unexpected exceptions") - p = tmp.join("test_doctest_unexpected_exception") - p.write(py.code.Source(""" - >>> i = 0 - >>> x - 2 - """)) - testitem = py.test.collect.DoctestFile(p).join(p.basename) - excinfo = py.test.raises(Failed, "testitem.runtest()") - repr = testitem.repr_failure(excinfo, ("", "")) - assert repr.reprlocation diff --git a/py/test/testing/test_event.py b/py/test/testing/test_event.py deleted file mode 100644 index a58748d1d..000000000 --- a/py/test/testing/test_event.py +++ /dev/null @@ -1,61 +0,0 @@ -import py -from py.__.test.event import EventBus -from py.__.test import event -import suptest -from py.__.code.testing.test_excinfo import TWMock - -class TestEventBus: - def test_simple(self): - bus = EventBus() - l = [] - bus.subscribe(l.append) - bus.notify(1) - bus.notify(2) - bus.notify(3) - assert l == [1,2,3] - - def test_multi_sub(self): - bus = EventBus() - l1 = [] - l2 = [] - bus.subscribe(l1.append) - bus.subscribe(l2.append) - bus.notify(1) - bus.notify(2) - bus.notify(3) - assert l1 == [1,2,3] - assert l2 == [1,2,3] - - def test_remove(self): - bus = EventBus() - l = [] - bus.subscribe(l.append) - bus.notify(1) - bus.unsubscribe(l.append) - bus.notify(2) - assert l == [1] - - -class TestItemTestReport(suptest.InlineCollection): - def test_toterminal(self): - sorter = suptest.events_from_runsource(""" - def test_one(): - assert 42 == 43 - """) - reports = sorter.get(event.ItemTestReport) - ev = reports[0] - assert ev.failed - twmock = TWMock() - ev.toterminal(twmock) - assert twmock.lines - twmock = TWMock() - ev.outcome.longrepr = "hello" - ev.toterminal(twmock) - assert twmock.lines[0] == "hello" - assert not twmock.lines[1:] - - ##assert ev.repr_run.find("AssertionError") != -1 - filepath = ev.colitem.fspath - #filepath , modpath = ev.itemrepr_path - assert str(filepath).endswith(".py") - #assert modpath.endswith("file_test.test_one") diff --git a/py/test/testing/test_genitems.py b/py/test/testing/test_genitems.py new file mode 100644 index 000000000..bb39d4570 --- /dev/null +++ b/py/test/testing/test_genitems.py @@ -0,0 +1,123 @@ +import py + +class Test_genitems: + def test_check_collect_hashes(self, testdir): + p = testdir.makepyfile(""" + def test_1(): + pass + + def test_2(): + pass + """) + p.copy(p.dirpath(p.purebasename + "2" + ".py")) + items, events = testdir.inline_genitems(p.dirpath()) + assert len(items) == 4 + for numi, i in enumerate(items): + for numj, j in enumerate(items): + if numj != numi: + assert hash(i) != hash(j) + assert i != j + + def test_root_conftest_syntax_error(self, testdir): + # do we want to unify behaviour with + # test_subdir_conftest_error? + p = testdir.makepyfile(conftest="raise SyntaxError\n") + py.test.raises(SyntaxError, testdir.inline_genitems, p.dirpath()) + + def test_subdir_conftest_error(self, testdir): + tmp = testdir.tmpdir + tmp.ensure("sub", "conftest.py").write("raise SyntaxError\n") + items, events = testdir.inline_genitems(tmp) + collectionfailures = events.getfailedcollections() + assert len(collectionfailures) == 1 + ev = collectionfailures[0] + assert ev.longrepr.reprcrash.message.startswith("SyntaxError") + + def test_example_items1(self, testdir): + p = testdir.makepyfile(''' + def testone(): + pass + + class TestX: + def testmethod_one(self): + pass + + class TestY(TestX): + pass + ''') + items, events = testdir.inline_genitems(p) + assert len(items) == 3 + assert items[0].name == 'testone' + assert items[1].name == 'testmethod_one' + assert items[2].name == 'testmethod_one' + + # let's also test getmodpath here + assert items[0].getmodpath() == "testone" + assert items[1].getmodpath() == "TestX.testmethod_one" + assert items[2].getmodpath() == "TestY.testmethod_one" + + s = items[0].getmodpath(stopatmodule=False) + assert s.endswith("test_example_items1.testone") + print s + + +class TestKeywordSelection: + def test_select_simple(self, testdir): + file_test = testdir.makepyfile(""" + def test_one(): assert 0 + class TestClass(object): + def test_method_one(self): + assert 42 == 43 + """) + def check(keyword, name): + sorter = testdir.inline_run("-s", "-k", keyword, file_test) + passed, skipped, failed = sorter.listoutcomes() + assert len(failed) == 1 + assert failed[0].colitem.name == name + assert len(sorter.getnamed('deselected')) == 1 + + for keyword in ['test_one', 'est_on']: + #yield check, keyword, 'test_one' + check(keyword, 'test_one') + check('TestClass.test', 'test_method_one') + + def test_select_extra_keywords(self, testdir): + p = testdir.makepyfile(test_select=""" + def test_1(): + pass + class TestClass: + def test_2(self): + pass + """) + testdir.makepyfile(conftest=""" + import py + class Class(py.test.collect.Class): + def _keywords(self): + return ['xxx', self.name] + """) + for keyword in ('xxx', 'xxx test_2', 'TestClass', 'xxx -test_1', + 'TestClass test_2', 'xxx TestClass test_2',): + sorter = testdir.inline_run(p.dirpath(), '-s', '-k', keyword) + print "keyword", repr(keyword) + passed, skipped, failed = sorter.listoutcomes() + assert len(passed) == 1 + assert passed[0].colitem.name == "test_2" + dlist = sorter.getnamed("deselected") + assert len(dlist) == 1 + assert dlist[0].items[0].name == 'test_1' + + def test_select_starton(self, testdir): + threepass = testdir.makepyfile(test_threepass=""" + def test_one(): assert 1 + def test_two(): assert 1 + def test_three(): assert 1 + """) + sorter = testdir.inline_run("-k", "test_two:", threepass) + passed, skipped, failed = sorter.listoutcomes() + assert len(passed) == 2 + assert not failed + dlist = sorter.getnamed("deselected") + assert len(dlist) == 1 + item = dlist[0].items[0] + assert item.name == "test_one" + diff --git a/py/test/testing/test_parseopt.py b/py/test/testing/test_parseopt.py new file mode 100644 index 000000000..12e5e29e5 --- /dev/null +++ b/py/test/testing/test_parseopt.py @@ -0,0 +1,90 @@ +import py +from py.__.test import parseopt + +pytest_plugins = 'pytest_iocapture' + +class TestParser: + def test_init(self, stdcapture): + parser = parseopt.Parser(usage="xyz") + py.test.raises(SystemExit, 'parser.parse(["-h"])') + out, err = stdcapture.reset() + assert out.find("xyz") != -1 + + def test_group_add_and_get(self): + parser = parseopt.Parser() + group = parser.addgroup("hello", description="desc") + assert group.name == "hello" + assert group.description == "desc" + py.test.raises(ValueError, parser.addgroup, "hello") + group2 = parser.getgroup("hello") + assert group2 is group + py.test.raises(ValueError, parser.getgroup, 'something') + + def test_group_addoption(self): + group = parseopt.OptionGroup("hello") + group.addoption("--option1", action="store_true") + assert len(group.options) == 1 + assert isinstance(group.options[0], py.compat.optparse.Option) + + def test_group_shortopt_lowercase(self): + parser = parseopt.Parser() + group = parser.addgroup("hello") + py.test.raises(ValueError, """ + group.addoption("-x", action="store_true") + """) + assert len(group.options) == 0 + group._addoption("-x", action="store_true") + assert len(group.options) == 1 + + def test_parser_addoption(self): + parser = parseopt.Parser() + group = parser.getgroup("misc") + assert len(group.options) == 0 + group.addoption("--option1", action="store_true") + assert len(group.options) == 1 + + def test_parse(self): + parser = parseopt.Parser() + parser.addoption("--hello", dest="hello", action="store") + option, args = parser.parse(['--hello', 'world']) + assert option.hello == "world" + assert not args + + def test_parse(self): + parser = parseopt.Parser() + option, args = parser.parse([py.path.local()]) + assert args[0] == py.path.local() + + def test_parse_will_set_default(self): + parser = parseopt.Parser() + parser.addoption("--hello", dest="hello", default="x", action="store") + option, args = parser.parse([]) + assert option.hello == "x" + del option.hello + args = parser.parse_setoption([], option) + assert option.hello == "x" + + def test_parse_setoption(self): + parser = parseopt.Parser() + parser.addoption("--hello", dest="hello", action="store") + parser.addoption("--world", dest="world", default=42) + class A: pass + option = A() + args = parser.parse_setoption(['--hello', 'world'], option) + assert option.hello == "world" + assert option.world == 42 + assert not args + + def test_parse_defaultgetter(self): + def defaultget(option): + if option.type == "int": + option.default = 42 + elif option.type == "string": + option.default = "world" + parser = parseopt.Parser(processopt=defaultget) + parser.addoption("--this", dest="this", type="int", action="store") + parser.addoption("--hello", dest="hello", type="string", action="store") + parser.addoption("--no", dest="no", action="store_true") + option, args = parser.parse([]) + assert option.hello == "world" + assert option.this == 42 diff --git a/py/test/testing/test_pycollect.py b/py/test/testing/test_pycollect.py new file mode 100644 index 000000000..3d8fd946d --- /dev/null +++ b/py/test/testing/test_pycollect.py @@ -0,0 +1,387 @@ +import py + +class TestModule: + def test_module_file_not_found(self, testdir): + tmpdir = testdir.tmpdir + fn = tmpdir.join('nada','no') + col = py.test.collect.Module(fn, config=testdir.parseconfig(tmpdir)) + py.test.raises(py.error.ENOENT, col.collect) + + def test_failing_import(self, testdir): + modcol = testdir.getmodulecol("import alksdjalskdjalkjals") + py.test.raises(ImportError, modcol.collect) + py.test.raises(ImportError, modcol.collect) + py.test.raises(ImportError, modcol.run) + + def test_syntax_error_in_module(self, testdir): + modcol = testdir.getmodulecol("this is a syntax error") + py.test.raises(SyntaxError, modcol.collect) + py.test.raises(SyntaxError, modcol.collect) + py.test.raises(SyntaxError, modcol.run) + + def test_module_assertion_setup(self, testdir): + modcol = testdir.getmodulecol("pass") + from py.__.magic import assertion + l = [] + py.magic.patch(assertion, "invoke", lambda: l.append(None)) + try: + modcol.setup() + finally: + py.magic.revert(assertion, "invoke") + x = l.pop() + assert x is None + py.magic.patch(assertion, "revoke", lambda: l.append(None)) + try: + modcol.teardown() + finally: + py.magic.revert(assertion, "revoke") + x = l.pop() + assert x is None + + def test_disabled_module(self, testdir): + modcol = testdir.getmodulecol(""" + disabled = True + def setup_module(mod): + raise ValueError + """) + assert not modcol.collect() + assert not modcol.run() + + def test_module_participates_as_plugin(self, testdir): + modcol = testdir.getmodulecol("") + modcol.setup() + assert modcol._config.pytestplugins.isregistered(modcol.obj) + modcol.teardown() + assert not modcol._config.pytestplugins.isregistered(modcol.obj) + + def test_module_considers_pytestplugins_at_import(self, testdir): + modcol = testdir.getmodulecol("pytest_plugins='xasdlkj',") + py.test.raises(ImportError, "modcol.obj") + +class TestClass: + def test_disabled_class(self, testdir): + modcol = testdir.getmodulecol(""" + class TestClass: + disabled = True + def test_method(self): + pass + """) + l = modcol.collect() + assert len(l) == 1 + modcol = l[0] + assert isinstance(modcol, py.test.collect.Class) + assert not modcol.collect() + +class TestGenerator: + def test_generative_functions(self, testdir): + modcol = testdir.getmodulecol(""" + def func1(arg, arg2): + assert arg == arg2 + + def test_gen(): + yield func1, 17, 3*5 + yield func1, 42, 6*7 + """) + colitems = modcol.collect() + assert len(colitems) == 1 + gencol = colitems[0] + assert isinstance(gencol, py.test.collect.Generator) + gencolitems = gencol.collect() + assert len(gencolitems) == 2 + assert isinstance(gencolitems[0], py.test.collect.Function) + assert isinstance(gencolitems[1], py.test.collect.Function) + assert gencolitems[0].name == '[0]' + assert gencolitems[0].obj.func_name == 'func1' + + def test_generative_methods(self, testdir): + modcol = testdir.getmodulecol(""" + def func1(arg, arg2): + assert arg == arg2 + class TestGenMethods: + def test_gen(self): + yield func1, 17, 3*5 + yield func1, 42, 6*7 + """) + gencol = modcol.collect()[0].collect()[0].collect()[0] + assert isinstance(gencol, py.test.collect.Generator) + gencolitems = gencol.collect() + assert len(gencolitems) == 2 + assert isinstance(gencolitems[0], py.test.collect.Function) + assert isinstance(gencolitems[1], py.test.collect.Function) + assert gencolitems[0].name == '[0]' + assert gencolitems[0].obj.func_name == 'func1' + + def test_generative_functions_with_explicit_names(self, testdir): + modcol = testdir.getmodulecol(""" + def func1(arg, arg2): + assert arg == arg2 + + def test_gen(): + yield "seventeen", func1, 17, 3*5 + yield "fortytwo", func1, 42, 6*7 + """) + colitems = modcol.collect() + assert len(colitems) == 1 + gencol = colitems[0] + assert isinstance(gencol, py.test.collect.Generator) + gencolitems = gencol.collect() + assert len(gencolitems) == 2 + assert isinstance(gencolitems[0], py.test.collect.Function) + assert isinstance(gencolitems[1], py.test.collect.Function) + assert gencolitems[0].name == "['seventeen']" + assert gencolitems[0].obj.func_name == 'func1' + assert gencolitems[1].name == "['fortytwo']" + assert gencolitems[1].obj.func_name == 'func1' + + def test_generative_methods_with_explicit_names(self, testdir): + modcol = testdir.getmodulecol(""" + def func1(arg, arg2): + assert arg == arg2 + class TestGenMethods: + def test_gen(self): + yield "m1", func1, 17, 3*5 + yield "m2", func1, 42, 6*7 + """) + gencol = modcol.collect()[0].collect()[0].collect()[0] + assert isinstance(gencol, py.test.collect.Generator) + gencolitems = gencol.collect() + assert len(gencolitems) == 2 + assert isinstance(gencolitems[0], py.test.collect.Function) + assert isinstance(gencolitems[1], py.test.collect.Function) + assert gencolitems[0].name == "['m1']" + assert gencolitems[0].obj.func_name == 'func1' + assert gencolitems[1].name == "['m2']" + assert gencolitems[1].obj.func_name == 'func1' + + def test_order_of_execution_generator_same_codeline(self, testdir, tmpdir): + o = testdir.makepyfile(""" + def test_generative_order_of_execution(): + test_list = [] + expected_list = range(6) + + def list_append(item): + test_list.append(item) + + def assert_order_of_execution(): + print 'expected order', expected_list + print 'but got ', test_list + assert test_list == expected_list + + for i in expected_list: + yield list_append, i + yield assert_order_of_execution + """) + sorter = testdir.inline_run(o) + passed, skipped, failed = sorter.countoutcomes() + assert passed == 7 + assert not skipped and not failed + + def test_order_of_execution_generator_different_codeline(self, testdir): + o = testdir.makepyfile(""" + def test_generative_tests_different_codeline(): + test_list = [] + expected_list = range(3) + + def list_append_2(): + test_list.append(2) + + def list_append_1(): + test_list.append(1) + + def list_append_0(): + test_list.append(0) + + def assert_order_of_execution(): + print 'expected order', expected_list + print 'but got ', test_list + assert test_list == expected_list + + yield list_append_0 + yield list_append_1 + yield list_append_2 + yield assert_order_of_execution + """) + sorter = testdir.inline_run(o) # .events_from_cmdline([o]) + passed, skipped, failed = sorter.countoutcomes() + assert passed == 4 + assert not skipped and not failed + +class TestFunction: + def test_function_equality(self, tmpdir): + config = py.test.config._reparse([tmpdir]) + f1 = py.test.collect.Function(name="name", config=config, + args=(1,), callobj=isinstance) + f2 = py.test.collect.Function(name="name", config=config, + args=(1,), callobj=callable) + assert not f1 == f2 + assert f1 != f2 + f3 = py.test.collect.Function(name="name", config=config, + args=(1,2), callobj=callable) + assert not f3 == f2 + assert f3 != f2 + + assert not f3 == f1 + assert f3 != f1 + + f1_b = py.test.collect.Function(name="name", config=config, + args=(1,), callobj=isinstance) + assert f1 == f1_b + assert not f1 != f1_b + + def test_pyfuncarg_lookupfails(self, testdir): + item = testdir.getitem("def test_func(some, other): pass") + kw = py.test.raises(LookupError, "item.lookup_allargs()") + + def test_pyfuncarg_basic(self, testdir): + item = testdir.getitem("def test_func(some, other): pass") + class Provider: + def pytest_pyfuncarg_some(self, pyfuncitem): + return pyfuncitem.name + def pytest_pyfuncarg_other(self, pyfuncitem): + return 42 + item._config.pytestplugins.register(Provider()) + kw = item.lookup_allargs() + assert len(kw) == 2 + assert kw['some'] == "test_func" + assert kw['other'] == 42 + + def test_pyfuncarg_addfinalizer(self, testdir): + item = testdir.getitem("def test_func(some): pass") + l = [] + class Provider: + def pytest_pyfuncarg_some(self, pyfuncitem): + pyfuncitem.addfinalizer(lambda: l.append(42)) + return 3 + item._config.pytestplugins.register(Provider()) + kw = item.lookup_allargs() + assert len(kw) == 1 + assert kw['some'] == 3 + assert len(l) == 0 + item.teardown() + assert len(l) == 1 + assert l[0] == 42 + + def test_pyfuncarg_lookup_modulelevel(self, testdir): + modcol = testdir.getmodulecol(""" + def pytest_pyfuncarg_something(pyfuncitem): + return pyfuncitem.name + + class TestClass: + def test_method(self, something): + pass + def test_func(something): + pass + """) + item1, item2 = testdir.genitems([modcol]) + modcol.setup() + assert modcol._config.pytestplugins.isregistered(modcol.obj) + kwargs = item1.lookup_allargs() + assert kwargs['something'] == "test_method" + kwargs = item2.lookup_allargs() + assert kwargs['something'] == "test_func" + modcol.teardown() + assert not modcol._config.pytestplugins.isregistered(modcol.obj) + +class TestSorting: + def test_check_equality_and_cmp_basic(self, testdir): + modcol = testdir.getmodulecol(""" + def test_pass(): pass + def test_fail(): assert 0 + """) + fn1 = modcol.collect_by_name("test_pass") + assert isinstance(fn1, py.test.collect.Function) + fn2 = modcol.collect_by_name("test_pass") + assert isinstance(fn2, py.test.collect.Function) + + assert fn1 == fn2 + assert fn1 != modcol + assert cmp(fn1, fn2) == 0 + assert hash(fn1) == hash(fn2) + + fn3 = modcol.collect_by_name("test_fail") + assert isinstance(fn3, py.test.collect.Function) + assert not (fn1 == fn3) + assert fn1 != fn3 + assert cmp(fn1, fn3) == -1 + + assert cmp(fn1, 10) == -1 + assert cmp(fn2, 10) == -1 + assert cmp(fn3, 10) == -1 + for fn in fn1,fn2,fn3: + assert fn != 3 + assert fn != modcol + assert fn != [1,2,3] + assert [1,2,3] != fn + assert modcol != fn + + def test_allow_sane_sorting_for_decorators(self, testdir): + modcol = testdir.getmodulecol(""" + def dec(f): + g = lambda: f(2) + g.place_as = f + return g + + + def test_a(y): + pass + test_a = dec(test_a) + + def test_b(y): + pass + test_b = dec(test_b) + """) + colitems = modcol.collect() + assert len(colitems) == 2 + f1, f2 = colitems + assert cmp(f2, f1) > 0 + +class TestConftestCustomization: + def test_extra_python_files_and_functions(self, testdir): + testdir.makepyfile(conftest=""" + import py + class MyFunction(py.test.collect.Function): + pass + class Directory(py.test.collect.Directory): + def consider_file(self, path): + if path.check(fnmatch="check_*.py"): + return self.Module(path, parent=self) + return super(Directory, self).consider_file(path) + class myfuncmixin: + Function = MyFunction + def funcnamefilter(self, name): + return name.startswith('check_') + class Module(myfuncmixin, py.test.collect.Module): + def classnamefilter(self, name): + return name.startswith('CustomTestClass') + class Instance(myfuncmixin, py.test.collect.Instance): + pass + """) + checkfile = testdir.makepyfile(check_file=""" + def check_func(): + assert 42 == 42 + class CustomTestClass: + def check_method(self): + assert 23 == 23 + """) + # check that directory collects "check_" files + config = testdir.parseconfig() + col = config.getfsnode(checkfile.dirpath()) + colitems = col.collect() + assert len(colitems) == 1 + assert isinstance(colitems[0], py.test.collect.Module) + + # check that module collects "check_" functions and methods + config = testdir.parseconfig(checkfile) + col = config.getfsnode(checkfile) + assert isinstance(col, py.test.collect.Module) + colitems = col.collect() + assert len(colitems) == 2 + funccol = colitems[0] + assert isinstance(funccol, py.test.collect.Function) + assert funccol.name == "check_func" + clscol = colitems[1] + assert isinstance(clscol, py.test.collect.Class) + colitems = clscol.collect()[0].collect() + assert len(colitems) == 1 + assert colitems[0].name == "check_method" + diff --git a/py/test/testing/test_pytestplugin.py b/py/test/testing/test_pytestplugin.py new file mode 100644 index 000000000..2bd3ab7fe --- /dev/null +++ b/py/test/testing/test_pytestplugin.py @@ -0,0 +1,273 @@ +import py, os +from py.__.test.pytestplugin import PytestPlugins, canonical_names +from py.__.test.pytestplugin import registerplugin, importplugin + +class TestBootstrapping: + def test_consider_env_fails_to_import(self, monkeypatch): + plugins = PytestPlugins() + monkeypatch.setitem(os.environ, 'PYTEST_PLUGINS', 'nonexistingmodule') + py.test.raises(ImportError, "plugins.consider_env()") + + def test_consider_env_plugin_instantiation(self, testdir, monkeypatch): + plugins = PytestPlugins() + testdir.syspathinsert() + testdir.makepyfile(pytest_xy123="class Xy123Plugin: pass") + monkeypatch.setitem(os.environ, 'PYTEST_PLUGINS', 'xy123') + l1 = len(plugins.getplugins()) + plugins.consider_env() + l2 = len(plugins.getplugins()) + assert l2 == l1 + 1 + assert plugins.getplugin('pytest_xy123') + plugins.consider_env() + l3 = len(plugins.getplugins()) + assert l2 == l3 + + def test_pytestplugin_ENV_startup(self, testdir, monkeypatch): + testdir.makepyfile(pytest_x500="class X500Plugin: pass") + p = testdir.makepyfile(""" + import py + def test_hello(): + plugin = py.test.config.pytestplugins.getplugin('x500') + assert plugin is not None + """) + testdir.syspathinsert() + monkeypatch.setitem(os.environ, 'PYTEST_PLUGINS', 'pytest_x500') + result = testdir.runpytest(p) + assert result.ret == 0 + extra = result.stdout.fnmatch_lines(["*1 passed in*"]) + + def test_import_plugin_importname(self, testdir): + plugins = PytestPlugins() + py.test.raises(ImportError, 'plugins.import_plugin("x.y")') + py.test.raises(ImportError, 'plugins.import_plugin("pytest_x.y")') + + reset = testdir.syspathinsert() + pluginname = "pytest_hello" + testdir.makepyfile(**{pluginname: """ + class HelloPlugin: + pass + """}) + plugins.import_plugin("hello") + len1 = len(plugins.getplugins()) + plugins.import_plugin("pytest_hello") + len2 = len(plugins.getplugins()) + assert len1 == len2 + plugin1 = plugins.getplugin("pytest_hello") + assert plugin1.__class__.__name__ == 'HelloPlugin' + plugin2 = plugins.getplugin("hello") + assert plugin2 is plugin1 + + def test_consider_module(self, testdir): + plugins = PytestPlugins() + testdir.syspathinsert() + testdir.makepyfile(pytest_plug1="class Plug1Plugin: pass") + testdir.makepyfile(pytest_plug2="class Plug2Plugin: pass") + mod = py.std.new.module("temp") + mod.pytest_plugins = ["pytest_plug1", "pytest_plug2"] + plugins.consider_module(mod) + assert plugins.getplugin("plug1").__class__.__name__ == "Plug1Plugin" + assert plugins.getplugin("plug2").__class__.__name__ == "Plug2Plugin" + + def test_consider_module_import_module(self, testdir, EventRecorder): + mod = py.std.new.module("x") + mod.pytest_plugins = "pytest_a" + aplugin = testdir.makepyfile(pytest_a="""class APlugin: pass""") + plugins = PytestPlugins() + evrec = EventRecorder(plugins) + #syspath.prepend(aplugin.dirpath()) + py.std.sys.path.insert(0, str(aplugin.dirpath())) + plugins.consider_module(mod) + evlist = evrec.getnamed("plugin_registered") + assert len(evlist) == 1 + assert evlist[0].__class__.__name__ == "APlugin" + + # check that it is not registered twice + plugins.consider_module(mod) + evlist = evrec.getnamed("plugin_registered") + assert len(evlist) == 1 + + def test_consider_conftest(self, testdir): + pp = PytestPlugins() + mod = testdir.makepyfile("class ConftestPlugin: hello = 1").pyimport() + pp.consider_conftest(mod) + l = [x for x in pp.getplugins() if isinstance(x, mod.ConftestPlugin)] + assert len(l) == 1 + assert l[0].hello == 1 + + pp.consider_conftest(mod) + l = [x for x in pp.getplugins() if isinstance(x, mod.ConftestPlugin)] + assert len(l) == 1 + + def test_config_sets_conftesthandle_onimport(self, testdir): + config = testdir.parseconfig([]) + assert config._conftest._onimport == config.pytestplugins.consider_conftest + + def test_consider_conftest_deps(self, testdir): + mod = testdir.makepyfile("pytest_plugins='xyz'").pyimport() + pp = PytestPlugins() + py.test.raises(ImportError, "pp.consider_conftest(mod)") + + def test_registry(self): + pp = PytestPlugins() + a1, a2 = object(), object() + pp.register(a1) + assert pp.isregistered(a1) + pp.register(a2) + assert pp.isregistered(a2) + assert pp.getplugins() == [a1, a2] + pp.unregister(a1) + assert not pp.isregistered(a1) + pp.unregister(a2) + assert not pp.isregistered(a2) + + def test_canonical_names(self): + for name in 'xyz', 'pytest_xyz', 'pytest_Xyz', 'Xyz': + impname, clsname = canonical_names(name) + assert impname == "pytest_xyz" + assert clsname == "XyzPlugin" + + def test_registerplugin(self): + l = [] + registerfunc = l.append + registerplugin(registerfunc, py.io, "TerminalWriter") + assert len(l) == 1 + assert isinstance(l[0], py.io.TerminalWriter) + + def test_importplugin(self): + assert importplugin("py") == py + py.test.raises(ImportError, "importplugin('laksjd.qwe')") + mod = importplugin("pytest_terminal") + assert mod is py.__.test.plugin.pytest_terminal + + +class TestPytestPluginInteractions: + def test_do_option_conftestplugin(self, testdir): + from py.__.test.config import Config + p = testdir.makepyfile(""" + class ConftestPlugin: + def pytest_addoption(self, parser): + parser.addoption('--test123', action="store_true") + """) + config = Config() + config._conftest.importconftest(p) + print config.pytestplugins.getplugins() + config.parse([]) + assert not config.option.test123 + + def test_do_option_postinitialize(self, testdir): + from py.__.test.config import Config + config = Config() + config.parse([]) + config.pytestplugins.configure(config=config) + assert not hasattr(config.option, 'test123') + p = testdir.makepyfile(""" + class ConftestPlugin: + def pytest_addoption(self, parser): + parser.addoption('--test123', action="store_true", + default=True) + """) + config._conftest.importconftest(p) + assert config.option.test123 + + def test_configure(self, testdir): + config = testdir.parseconfig() + l = [] + events = [] + class A: + def pytest_configure(self, config): + l.append(self) + def pyevent_hello(self, obj): + events.append(obj) + + config.bus.register(A()) + assert len(l) == 0 + config.pytestplugins.configure(config=config) + assert len(l) == 1 + config.bus.register(A()) # this should lead to a configured() plugin + assert len(l) == 2 + assert l[0] != l[1] + + config.bus.notify("hello", 42) + assert len(events) == 2 + assert events == [42,42] + + config.pytestplugins.unconfigure(config=config) + config.bus.register(A()) + assert len(l) == 2 + + def test_MultiCall(self): + pp = PytestPlugins() + assert hasattr(pp, 'MultiCall') + + # lower level API + + def test_getfirst(self): + plugins = PytestPlugins() + class My1: + x = 1 + assert plugins.getfirst("x") is None + plugins.register(My1()) + assert plugins.getfirst("x") == 1 + + def test_call_each(self): + plugins = PytestPlugins() + class My: + def method(self, arg): + pass + plugins.register(My()) + py.test.raises(TypeError, 'plugins.call_each("method")') + l = plugins.call_each("method", arg=42) + assert l == [] + py.test.raises(TypeError, 'plugins.call_each("method", arg=42, s=13)') + + def test_call_firstresult(self): + plugins = PytestPlugins() + class My1: + def method(self): + pass + class My2: + def method(self): + return True + class My3: + def method(self): + return None + assert plugins.call_firstresult("method") is None + assert plugins.call_firstresult("methodnotexists") is None + plugins.register(My1()) + assert plugins.call_firstresult("method") is None + plugins.register(My2()) + assert plugins.call_firstresult("method") == True + plugins.register(My3()) + assert plugins.call_firstresult("method") == True + + def test_listattr(self): + plugins = PytestPlugins() + class My2: + x = 42 + plugins.register(My2()) + assert not plugins.listattr("hello") + assert plugins.listattr("x") == [42] + + @py.test.keywords(xfail="implement setupcall") + def test_call_setup_participants(self, testdir): + testdir.makepyfile( + conftest=""" + import py + def pytest_method(self, x): + return x+1 + pytest_plugin = "pytest_someplugin", + """ + ) + testdir.makepyfile(pytest_someplugin=""" + def pytest_method(self, x): + return x+1 + """) + modcol = testdir.getmodulecol(""" + def pytest_method(x): + return x+0 + """) + l = [] + call = modcol._config.pytestplugins.setupcall(modcol, "pytest_method", 1) + assert len(call.methods) == 3 + results = call.execute() + assert results == [1,2,2] diff --git a/py/test/testing/test_recording.py b/py/test/testing/test_recording.py index 784a15ce1..b6f1fe5e2 100644 --- a/py/test/testing/test_recording.py +++ b/py/test/testing/test_recording.py @@ -1,12 +1,9 @@ import py,sys py.test.skip("implementation missing: recording") -from py.__.test.testing import suptest -from py.__.test.acceptance_test import AcceptBase - -class TestRecordingAccept(AcceptBase): - def test_recording_and_back(self): - p = self.makepyfile(test_one=""" +class TestRecordingAccept: + def test_recording_and_back(self, testdir): + p = testdir.makepyfile(""" import py def test_fail(): assert x diff --git a/py/test/testing/test_resultlog.py b/py/test/testing/test_resultlog.py deleted file mode 100644 index d919452b2..000000000 --- a/py/test/testing/test_resultlog.py +++ /dev/null @@ -1,184 +0,0 @@ -import os, StringIO - -import py - -from py.__.test import resultlog -from py.__.test.collect import Node, Item, FSCollector -from py.__.test.event import EventBus -from py.__.test.event import ItemTestReport, CollectionReport -from py.__.test.event import InternalException -from py.__.test.runner import OutcomeRepr - - -class Fake(object): - def __init__(self, **kwds): - self.__dict__.update(kwds) - - -def test_generic_path(): - p1 = Node('a', config='dummy') - assert p1.fspath is None - p2 = Node('B', parent=p1) - p3 = Node('()', parent = p2) - item = Item('c', parent = p3) - - res = resultlog.generic_path(item) - assert res == 'a.B().c' - - p0 = FSCollector('proj/test', config='dummy') - p1 = FSCollector('proj/test/a', parent=p0) - p2 = Node('B', parent=p1) - p3 = Node('()', parent = p2) - p4 = Node('c', parent=p3) - item = Item('[1]', parent = p4) - - res = resultlog.generic_path(item) - assert res == 'test/a:B().c[1]' - - -def make_item(*names): - node = None - config = "dummy" - for name in names[:-1]: - if '/' in name: - node = FSCollector(name, parent=node, config=config) - else: - node = Node(name, parent=node, config=config) - if names[-1] is None: - return node - return Item(names[-1], parent=node) - -class TestResultLog(object): - - def test_create(self): - bus = EventBus() - logfile = object() - - reslog = resultlog.ResultLog(bus, logfile) - assert len(bus._subscribers) == 1 - assert reslog.logfile is logfile - - def test_write_log_entry(self): - reslog = resultlog.ResultLog(EventBus(), None) - - reslog.logfile = StringIO.StringIO() - reslog.write_log_entry('.', 'name', '') - entry = reslog.logfile.getvalue() - assert entry[-1] == '\n' - entry_lines = entry.splitlines() - assert len(entry_lines) == 1 - assert entry_lines[0] == '. name' - - reslog.logfile = StringIO.StringIO() - reslog.write_log_entry('s', 'name', 'Skipped') - entry = reslog.logfile.getvalue() - assert entry[-1] == '\n' - entry_lines = entry.splitlines() - assert len(entry_lines) == 2 - assert entry_lines[0] == 's name' - assert entry_lines[1] == ' Skipped' - - reslog.logfile = StringIO.StringIO() - reslog.write_log_entry('s', 'name', 'Skipped\n') - entry = reslog.logfile.getvalue() - assert entry[-1] == '\n' - entry_lines = entry.splitlines() - assert len(entry_lines) == 2 - assert entry_lines[0] == 's name' - assert entry_lines[1] == ' Skipped' - - reslog.logfile = StringIO.StringIO() - longrepr = ' tb1\n tb 2\nE tb3\nSome Error' - reslog.write_log_entry('F', 'name', longrepr) - entry = reslog.logfile.getvalue() - assert entry[-1] == '\n' - entry_lines = entry.splitlines() - assert len(entry_lines) == 5 - assert entry_lines[0] == 'F name' - assert entry_lines[1:] == [' '+line for line in longrepr.splitlines()] - - def test_log_outcome(self): - reslog = resultlog.ResultLog(EventBus(), StringIO.StringIO()) - - colitem = make_item('some', 'path', 'a', 'b') - - try: - raise ValueError - except ValueError: - the_repr = py.code.ExceptionInfo().getrepr() - - outcome=OutcomeRepr('execute', 'F', the_repr) - ev = Fake(colitem=colitem, outcome=outcome) - - reslog.log_outcome(ev) - - entry = reslog.logfile.getvalue() - entry_lines = entry.splitlines() - - assert entry_lines[0] == 'F some.path.a.b' - assert entry_lines[-1][0] == ' ' - assert 'ValueError' in entry - - def test_item_test_passed(self): - bus = EventBus() - reslog = resultlog.ResultLog(bus, StringIO.StringIO()) - - colitem = make_item('proj/test', 'proj/test/mod', 'a', 'b') - - outcome=OutcomeRepr('execute', '.', '') - rep_ev = ItemTestReport(colitem, passed=outcome) - - bus.notify(rep_ev) - - lines = reslog.logfile.getvalue().splitlines() - assert len(lines) == 1 - line = lines[0] - assert line.startswith(". ") - assert line[2:] == 'test/mod:a.b' - - def test_collection_report(self): - bus = EventBus() - reslog = resultlog.ResultLog(bus, None) - - reslog.logfile = StringIO.StringIO() - colitem = make_item('proj/test', 'proj/test/mod', 'A', None) - outcome=OutcomeRepr('execute', '', '') - rep_ev = CollectionReport(colitem, object(), passed=outcome) - - bus.notify(rep_ev) - - entry = reslog.logfile.getvalue() - assert not entry - - reslog.logfile = StringIO.StringIO() - outcome=OutcomeRepr('execute', 'F', 'Some Error') - rep_ev = CollectionReport(colitem, object(), failed=outcome) - - bus.notify(rep_ev) - - lines = reslog.logfile.getvalue().splitlines() - assert len(lines) == 2 - assert lines[0] == 'F test/mod:A' - - def test_internal_exception(self): - # they are produced for example by a teardown failing - # at the end of the run - bus = EventBus() - reslog = resultlog.ResultLog(bus, StringIO.StringIO()) - - try: - raise ValueError - except ValueError: - excinfo = py.code.ExceptionInfo() - - internal = InternalException(excinfo) - - bus.notify(internal) - - entry = reslog.logfile.getvalue() - entry_lines = entry.splitlines() - - assert entry_lines[0].startswith('! ') - assert os.path.basename(__file__)[:-1] in entry_lines[0] #.py/.pyc - assert entry_lines[-1][0] == ' ' - assert 'ValueError' in entry diff --git a/py/test/testing/test_runner_functional.py b/py/test/testing/test_runner_functional.py index e02e525af..5b1116954 100644 --- a/py/test/testing/test_runner_functional.py +++ b/py/test/testing/test_runner_functional.py @@ -1,34 +1,42 @@ import py -from py.__.test.testing.suptest import InlineCollection from py.__.test.runner import basic_run_report, forked_run_report, basic_collect_report from py.__.test.runner import RobustRun from py.__.code.excinfo import ReprExceptionInfo -class BaseTests(InlineCollection): - def test_passfunction(self): - ev = self.runitem(""" +class BaseTests: + def test_funcattr(self, testdir): + ev = testdir.runitem(""" + import py + @py.test.keywords(xfail="needs refactoring") + def test_func(): + raise Exit() + """) + assert ev.keywords['xfail'] == "needs refactoring" + + def test_passfunction(self, testdir): + ev = testdir.runitem(""" def test_func(): pass """) assert ev.passed assert not ev.failed - assert ev.outcome.shortrepr == "." - assert not ev.outcome.longrepr + assert ev.shortrepr == "." + assert not hasattr(ev, 'longrepr') - def test_failfunction(self): - ev = self.runitem(""" + def test_failfunction(self, testdir): + ev = testdir.runitem(""" def test_func(): assert 0 """) assert not ev.passed assert not ev.skipped assert ev.failed - assert ev.outcome.when == "execute" - assert isinstance(ev.outcome.longrepr, ReprExceptionInfo) - assert str(ev.outcome.shortrepr) == "F" + assert ev.when == "execute" + assert isinstance(ev.longrepr, ReprExceptionInfo) + assert str(ev.shortrepr) == "F" - def test_skipfunction(self): - ev = self.runitem(""" + def test_skipfunction(self, testdir): + ev = testdir.runitem(""" import py def test_func(): py.test.skip("hello") @@ -43,8 +51,8 @@ class BaseTests(InlineCollection): #assert ev.skipped.location.path #assert not ev.skipped.failurerepr - def test_skip_in_setup_function(self): - ev = self.runitem(""" + def test_skip_in_setup_function(self, testdir): + ev = testdir.runitem(""" import py def setup_function(func): py.test.skip("hello") @@ -59,8 +67,8 @@ class BaseTests(InlineCollection): #assert ev.skipped.location.lineno == 3 #assert ev.skipped.location.lineno == 3 - def test_failure_in_setup_function(self): - ev = self.runitem(""" + def test_failure_in_setup_function(self, testdir): + ev = testdir.runitem(""" import py def setup_function(func): raise ValueError(42) @@ -71,10 +79,10 @@ class BaseTests(InlineCollection): assert not ev.skipped assert not ev.passed assert ev.failed - assert ev.outcome.when == "setup" + assert ev.when == "setup" - def test_failure_in_teardown_function(self): - ev = self.runitem(""" + def test_failure_in_teardown_function(self, testdir): + ev = testdir.runitem(""" import py def teardown_function(func): raise ValueError(42) @@ -85,18 +93,18 @@ class BaseTests(InlineCollection): assert not ev.skipped assert not ev.passed assert ev.failed - #assert ev.outcome.when == "teardown" - #assert ev.outcome.where.lineno == 3 - #assert ev.outcome.entries + assert ev.when == "teardown" + assert ev.longrepr.reprcrash.lineno == 3 + assert ev.longrepr.reprtraceback.reprentries - def test_custom_failure_repr(self): - self.makepyfile(conftest=""" + def test_custom_failure_repr(self, testdir): + testdir.makepyfile(conftest=""" import py class Function(py.test.collect.Function): def repr_failure(self, excinfo, outerr): return "hello" """) - ev = self.runitem(""" + ev = testdir.runitem(""" import py def test_func(): assert 0 @@ -109,14 +117,14 @@ class BaseTests(InlineCollection): #assert ev.failed.where.path.basename == "test_func.py" #assert ev.failed.failurerepr == "hello" - def test_failure_in_setup_function_ignores_custom_failure_repr(self): - self.makepyfile(conftest=""" + def test_failure_in_setup_function_ignores_custom_failure_repr(self, testdir): + testdir.makepyfile(conftest=""" import py class Function(py.test.collect.Function): def repr_failure(self, excinfo): assert 0 """) - ev = self.runitem(""" + ev = testdir.runitem(""" import py def setup_function(func): raise ValueError(42) @@ -132,8 +140,8 @@ class BaseTests(InlineCollection): #assert ev.outcome.where.path.basename == "test_func.py" #assert instanace(ev.failed.failurerepr, PythonFailureRepr) - def test_capture_in_func(self): - ev = self.runitem(""" + def test_capture_in_func(self, testdir): + ev = testdir.runitem(""" import py def setup_function(func): print >>py.std.sys.stderr, "in setup" @@ -148,21 +156,21 @@ class BaseTests(InlineCollection): # assert out == ['in function\nin teardown\n'] # assert err == ['in setup\n'] - def test_systemexit_does_not_bail_out(self): + def test_systemexit_does_not_bail_out(self, testdir): try: - ev = self.runitem(""" + ev = testdir.runitem(""" def test_func(): raise SystemExit(42) """) except SystemExit: py.test.fail("runner did not catch SystemExit") assert ev.failed - assert ev.outcome.when == "execute" + assert ev.when == "execute" - def test_exit_propagates(self): + def test_exit_propagates(self, testdir): from py.__.test.outcome import Exit try: - self.runitem(""" + testdir.runitem(""" from py.__.test.outcome import Exit def test_func(): raise Exit() @@ -172,14 +180,15 @@ class BaseTests(InlineCollection): else: py.test.fail("did not raise") + class TestExecutionNonForked(BaseTests): def getrunner(self): return basic_run_report - def test_keyboardinterrupt_propagates(self): + def test_keyboardinterrupt_propagates(self, testdir): from py.__.test.outcome import Exit try: - self.runitem(""" + testdir.runitem(""" def test_func(): raise KeyboardInterrupt("fake") """) @@ -188,19 +197,19 @@ class TestExecutionNonForked(BaseTests): else: py.test.fail("did not raise") - def test_pdb_on_fail(self): + def test_pdb_on_fail(self, testdir): l = [] - ev = self.runitem(""" + ev = testdir.runitem(""" def test_func(): assert 0 """, pdb=l.append) assert ev.failed - assert ev.outcome.when == "execute" + assert ev.when == "execute" assert len(l) == 1 - def test_pdb_on_skip(self): + def test_pdb_on_skip(self, testdir): l = [] - ev = self.runitem(""" + ev = testdir.runitem(""" import py def test_func(): py.test.skip("hello") @@ -214,18 +223,18 @@ class TestExecutionForked(BaseTests): py.test.skip("no os.fork available") return forked_run_report - def test_suicide(self): - ev = self.runitem(""" + def test_suicide(self, testdir): + ev = testdir.runitem(""" def test_func(): import os os.kill(os.getpid(), 15) """) assert ev.failed - assert ev.outcome.when == "???" + assert ev.when == "???" -class TestCollectionEvent(InlineCollection): - def test_collect_result(self): - col = self.getmodulecol(""" +class TestCollectionEvent: + def test_collect_result(self, testdir): + col = testdir.getmodulecol(""" def test_func1(): pass class TestClass: @@ -240,8 +249,8 @@ class TestCollectionEvent(InlineCollection): assert res[0].name == "test_func1" assert res[1].name == "TestClass" - def test_skip_at_module_scope(self): - col = self.getmodulecol(""" + def test_skip_at_module_scope(self, testdir): + col = testdir.getmodulecol(""" import py py.test.skip("hello") def test_func(): @@ -253,9 +262,9 @@ class TestCollectionEvent(InlineCollection): assert ev.skipped -class TestRunnerRepr(InlineCollection): - def test_runner_repr(self): - item = self.getitem("def test_func(): pass") +class TestRunnerRepr: + def test_runner_repr(self, testdir): + item = testdir.getitem("def test_func(): pass") robustrun = RobustRun(item) r = repr(robustrun) assert r diff --git a/py/test/testing/test_session.py b/py/test/testing/test_session.py index 9fdbfaaa3..939a3fef2 100644 --- a/py/test/testing/test_session.py +++ b/py/test/testing/test_session.py @@ -1,91 +1,8 @@ import py -from py.__.test import event -from py.__.test.testing import suptest -def setup_module(mod): - mod.tmpdir = py.test.ensuretemp(mod.__name__) - -class TestKeywordSelection(suptest.InlineSession): - def test_select_simple(self): - file_test = self.makepyfile(file_test=""" - def test_one(): assert 0 - class TestClass(object): - def test_method_one(self): - assert 42 == 43 - """) - def check(keyword, name): - sorter = self.parse_and_run("-s", "-k", keyword, file_test) - passed, skipped, failed = sorter.listoutcomes() - assert len(failed) == 1 - assert failed[0].colitem.name == name - assert len(sorter.get(event.Deselected)) == 1 - - for keyword in ['test_one', 'est_on']: - yield check, keyword, 'test_one' - yield check, 'TestClass.test', 'test_method_one' - - def test_select_extra_keywords(self): - o = self.tmpdir - tfile = o.join('test_select.py').write(py.code.Source(""" - def test_1(): - pass - class TestClass: - def test_2(self): - pass - """)) - conftest = o.join('conftest.py').write(py.code.Source(""" - import py - class Class(py.test.collect.Class): - def _keywords(self): - return ['xxx', self.name] - """)) - for keyword in ('xxx', 'xxx test_2', 'TestClass', 'xxx -test_1', - 'TestClass test_2', 'xxx TestClass test_2',): - sorter = suptest.events_from_cmdline([o, '-s', '-k', keyword]) - print "keyword", repr(keyword) - passed, skipped, failed = sorter.listoutcomes() - assert len(passed) == 1 - assert passed[0].colitem.name == "test_2" - dlist = sorter.get(event.Deselected) - assert len(dlist) == 1 - assert dlist[0].items[0].name == 'test_1' - - def test_select_starton(self): - threepass = self.makepyfile(test_threepass=""" - def test_one(): assert 1 - def test_two(): assert 1 - def test_three(): assert 1 - """) - sorter = self.parse_and_run("-k", "test_two:", threepass) - passed, skipped, failed = sorter.listoutcomes() - assert len(passed) == 2 - assert not failed - dlist = sorter.get(event.Deselected) - assert len(dlist) == 1 - item = dlist[0].items[0] - assert item.name == "test_one" - -class SessionTests(suptest.InlineCollection): - def events_from_cmdline(self, *args): - paths = [p for p in args if isinstance(p, py.path.local)] - if not paths: - args = (self.tmpdir,) + args - config = self.parseconfig(*args) - self.session = config.initsession() - self.sorter = suptest.EventSorter(config, self.session) - self.session.main() - return self.sorter - - def events_from_runsource(self, source, *args): - p = self.makepyfile(test_source=source) - return self.events_from_cmdline(p, *args) - - def makepyfile(self, *args, **kw): - self.tmpdir.ensure('__init__.py') - return super(SessionTests, self).makepyfile(*args, **kw) - - def test_basic_testitem_events(self): - tfile = self.makepyfile(test_one=""" +class SessionTests: + def test_basic_testitem_events(self, testdir): + tfile = testdir.makepyfile(""" def test_one(): pass def test_one_one(): @@ -95,7 +12,7 @@ class SessionTests(suptest.InlineCollection): def test_two(someargs): pass """) - sorter = self.events_from_cmdline(tfile) + sorter = testdir.inline_run(tfile) passed, skipped, failed = sorter.listoutcomes() assert len(skipped) == 0 assert len(passed) == 1 @@ -103,15 +20,15 @@ class SessionTests(suptest.InlineCollection): assert failed[0].colitem.name == "test_one_one" assert failed[1].colitem.name == "test_other" assert failed[2].colitem.name == "test_two" - itemstarted = sorter.get(event.ItemStart) + itemstarted = sorter.getnamed("itemstart") assert len(itemstarted) == 4 - colstarted = sorter.get(event.CollectionStart) + colstarted = sorter.getnamed("collectionstart") assert len(colstarted) == 1 col = colstarted[0].collector assert isinstance(col, py.test.collect.Module) - def test_nested_import_error(self): - tfile = self.makepyfile(test_one=""" + def test_nested_import_error(self, testdir): + tfile = testdir.makepyfile(""" import import_fails def test_this(): assert import_fails.a == 1 @@ -119,45 +36,44 @@ class SessionTests(suptest.InlineCollection): import does_not_work a = 1 """) - sorter = self.events_from_cmdline() + sorter = testdir.inline_run(tfile) l = sorter.getfailedcollections() assert len(l) == 1 - out = l[0].outcome.longrepr.reprcrash.message + out = l[0].longrepr.reprcrash.message assert out.find('does_not_work') != -1 - def test_raises_output(self): - self.makepyfile(test_one=""" + def test_raises_output(self, testdir): + sorter = testdir.inline_runsource(""" import py def test_raises_doesnt(): py.test.raises(ValueError, int, "3") """) - sorter = self.events_from_cmdline() passed, skipped, failed = sorter.listoutcomes() assert len(failed) == 1 - out = failed[0].outcome.longrepr.reprcrash.message + out = failed[0].longrepr.reprcrash.message if not out.find("DID NOT RAISE") != -1: print out py.test.fail("incorrect raises() output") - def test_generator_yields_None(self): - sorter = self.events_from_runsource(""" + def test_generator_yields_None(self, testdir): + sorter = testdir.inline_runsource(""" def test_1(): yield None """) failures = sorter.getfailedcollections() - out = failures[0].outcome.longrepr.reprcrash.message + out = failures[0].longrepr.reprcrash.message i = out.find('TypeError') assert i != -1 - def test_syntax_error_module(self): - sorter = self.events_from_runsource("this is really not python") + def test_syntax_error_module(self, testdir): + sorter = testdir.inline_runsource("this is really not python") l = sorter.getfailedcollections() assert len(l) == 1 - out = l[0].outcome.longrepr.reprcrash.message + out = l[0].longrepr.reprcrash.message assert out.find(str('not python')) != -1 - def test_exit_first_problem(self): - sorter = self.events_from_runsource(""" + def test_exit_first_problem(self, testdir): + sorter = testdir.inline_runsource(""" def test_one(): assert 0 def test_two(): assert 0 """, '--exitfirst') @@ -165,8 +81,8 @@ class SessionTests(suptest.InlineCollection): assert failed == 1 assert passed == skipped == 0 - def test_broken_repr(self): - self.makepyfile(test_broken=""" + def test_broken_repr(self, testdir): + p = testdir.makepyfile(""" import py class BrokenRepr1: foo=0 @@ -190,18 +106,17 @@ class SessionTests(suptest.InlineCollection): t = BrokenRepr2() assert t.foo == 1 """) - sorter = self.events_from_cmdline() + sorter = testdir.inline_run(p) passed, skipped, failed = sorter.listoutcomes() assert len(failed) == 2 - out = failed[0].outcome.longrepr.reprcrash.message + out = failed[0].longrepr.reprcrash.message assert out.find("""[Exception("Ha Ha fooled you, I'm a broken repr().") raised in repr()]""") != -1 #' - out = failed[1].outcome.longrepr.reprcrash.message + out = failed[1].longrepr.reprcrash.message assert (out.find("[unknown exception raised in repr()]") != -1 or out.find("TypeError") != -1) - def test_skip_by_conftest_directory(self): - from py.__.test import outcome - self.makepyfile(conftest=""" + def test_skip_by_conftest_directory(self, testdir): + testdir.makepyfile(conftest=""" import py class Directory(py.test.collect.Directory): def collect(self): @@ -209,14 +124,14 @@ class SessionTests(suptest.InlineCollection): """, test_file=""" def test_one(): pass """) - sorter = self.events_from_cmdline() - skips = sorter.get(event.CollectionReport) + sorter = testdir.inline_run(testdir.tmpdir) + skips = sorter.getnamed("collectionreport") assert len(skips) == 1 assert skips[0].skipped class TestNewSession(SessionTests): - def test_pdb_run(self): - tfile = self.makepyfile(test_one=""" + def test_pdb_run(self, testdir): + tfile = testdir.makepyfile(""" def test_usepdb(): assert 0 """) @@ -225,7 +140,7 @@ class TestNewSession(SessionTests): l.append(args) py.magic.patch(py.__.test.custompdb, 'post_mortem', mypdb) try: - sorter = self.events_from_cmdline('--pdb') + sorter = testdir.inline_run('--pdb', tfile) finally: py.magic.revert(py.__.test.custompdb, 'post_mortem') rep = sorter.getreport("test_usepdb") @@ -234,8 +149,8 @@ class TestNewSession(SessionTests): tb = py.code.Traceback(l[0][0]) assert tb[-1].name == "test_usepdb" - def test_order_of_execution(self): - sorter = self.events_from_runsource(""" + def test_order_of_execution(self, testdir): + sorter = testdir.inline_runsource(""" l = [] def test_1(): l.append(1) @@ -259,8 +174,8 @@ class TestNewSession(SessionTests): assert passed == 7 # also test listnames() here ... - def test_collect_only_with_various_situations(self): - p = self.makepyfile( + def test_collect_only_with_various_situations(self, testdir): + p = testdir.makepyfile( test_one=""" def test_one(): raise ValueError() @@ -276,15 +191,16 @@ class TestNewSession(SessionTests): import py py.test.skip('xxx') """, - test_three="xxxdsadsadsadsa" + test_three="xxxdsadsadsadsa", + __init__="" ) - sorter = self.events_from_cmdline('--collectonly') + sorter = testdir.inline_run('--collectonly', p.dirpath()) - itemstarted = sorter.get(event.ItemStart) + itemstarted = sorter.getnamed("itemstart") assert len(itemstarted) == 3 - assert not sorter.get(event.ItemTestReport) - started = sorter.get(event.CollectionStart) - finished = sorter.get(event.CollectionReport) + assert not sorter.getnamed("itemtestreport") + started = sorter.getnamed("collectionstart") + finished = sorter.getnamed("collectionreport") assert len(started) == len(finished) assert len(started) == 8 colfail = [x for x in finished if x.failed] diff --git a/py/test/testing/test_setup_nested.py b/py/test/testing/test_setup_nested.py index 7ea0b1707..2e5b13faf 100644 --- a/py/test/testing/test_setup_nested.py +++ b/py/test/testing/test_setup_nested.py @@ -2,11 +2,8 @@ # test correct setup/teardowns at # module, class, and instance level -import py -import suptest - -def test_module_and_function_setup(): - sorter = suptest.events_from_runsource(""" +def test_module_and_function_setup(testdir): + sorter = testdir.inline_runsource(""" modlevel = [] def setup_module(module): assert not modlevel @@ -35,8 +32,8 @@ def test_module_and_function_setup(): rep = sorter.getreport("test_module") assert rep.passed -def test_class_setup(): - sorter = suptest.events_from_runsource(""" +def test_class_setup(testdir): + sorter = testdir.inline_runsource(""" class TestSimpleClassSetup: clslevel = [] def setup_class(cls): @@ -58,8 +55,8 @@ def test_class_setup(): """) sorter.assertoutcome(passed=1+2+1) -def test_method_setup(): - sorter = suptest.events_from_runsource(""" +def test_method_setup(testdir): + sorter = testdir.inline_runsource(""" class TestSetupMethod: def setup_method(self, meth): self.methsetup = meth @@ -74,8 +71,8 @@ def test_method_setup(): """) sorter.assertoutcome(passed=2) -def test_method_generator_setup(): - sorter = suptest.events_from_runsource(""" +def test_method_generator_setup(testdir): + sorter = testdir.inline_runsource(""" class TestSetupTeardownOnInstance: def setup_class(cls): cls.classsetup = True @@ -96,8 +93,8 @@ def test_method_generator_setup(): """) sorter.assertoutcome(passed=1, failed=1) -def test_func_generator_setup(): - sorter = suptest.events_from_runsource(""" +def test_func_generator_setup(testdir): + sorter = testdir.inline_runsource(""" import sys def setup_module(mod): @@ -124,8 +121,8 @@ def test_func_generator_setup(): rep = sorter.getreport("test_one") assert rep.passed -def test_method_setup_uses_fresh_instances(): - sorter = suptest.events_from_runsource(""" +def test_method_setup_uses_fresh_instances(testdir): + sorter = testdir.inline_runsource(""" class TestSelfState1: def __init__(self): self.hello = 42