From 5c8df1d4cac111871bae8fcc0e7c44ab72aca2b2 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Sun, 9 Aug 2009 23:46:27 +0200 Subject: [PATCH 1/8] turn some tests from skipped to xfail strike dead code, small refinements to xfail exception reporting --HG-- branch : 1.0.x --- py/code/excinfo.py | 3 +-- py/code/testing/test_excinfo.py | 6 +++++- py/code/testing/test_source.py | 4 ++-- py/test/plugin/pytest_xfail.py | 6 ++++-- py/test/pluginmanager.py | 3 --- py/test/testing/test_pluginmanager.py | 11 ++++++++++- 6 files changed, 22 insertions(+), 11 deletions(-) diff --git a/py/code/excinfo.py b/py/code/excinfo.py index 52fe04b51..4245f7fe5 100644 --- a/py/code/excinfo.py +++ b/py/code/excinfo.py @@ -39,8 +39,7 @@ class ExceptionInfo(object): """ lines = py.std.traceback.format_exception_only(self.type, self.value) text = ''.join(lines) - if text.endswith('\n'): - text = text[:-1] + text = text.rstrip() if tryshort: if text.startswith(self._striptext): text = text[len(self._striptext):] diff --git a/py/code/testing/test_excinfo.py b/py/code/testing/test_excinfo.py index 2d1b43661..4d831c580 100644 --- a/py/code/testing/test_excinfo.py +++ b/py/code/testing/test_excinfo.py @@ -200,6 +200,11 @@ def test_tbentry_reinterpret(): def test_excinfo_exconly(): excinfo = py.test.raises(ValueError, h) assert excinfo.exconly().startswith('ValueError') + excinfo = py.test.raises(ValueError, + "raise ValueError('hello\\nworld')") + msg = excinfo.exconly(tryshort=True) + assert msg.startswith('ValueError') + assert msg.endswith("world") def test_excinfo_repr(): excinfo = py.test.raises(ValueError, h) @@ -242,7 +247,6 @@ def test_entrysource_Queue_example(): assert s.startswith("def get") def test_codepath_Queue_example(): - py.test.skip("try harder to get at the paths of code objects.") import Queue try: Queue.Queue().get(timeout=0.001) diff --git a/py/code/testing/test_source.py b/py/code/testing/test_source.py index 641b26fd4..ffb00200c 100644 --- a/py/code/testing/test_source.py +++ b/py/code/testing/test_source.py @@ -173,8 +173,8 @@ class TestSourceParsingAndCompiling: assert len(source) == 6 assert source.getstatementrange(2) == (1, 4) + @py.test.mark.xfail def test_getstatementrange_bug2(self): - py.test.skip("fix me (issue19)") source = Source("""\ assert ( 33 @@ -300,8 +300,8 @@ def test_deindent(): lines = deindent(source.splitlines()) assert lines == ['', 'def f():', ' def g():', ' pass', ' '] +@py.test.mark.xfail def test_source_of_class_at_eof_without_newline(): - py.test.skip("CPython's inspect.getsource is buggy") # this test fails because the implicit inspect.getsource(A) below # does not return the "x = 1" last line. tmpdir = py.test.ensuretemp("source_write_read") diff --git a/py/test/plugin/pytest_xfail.py b/py/test/plugin/pytest_xfail.py index be85e8721..8e6a3b73c 100644 --- a/py/test/plugin/pytest_xfail.py +++ b/py/test/plugin/pytest_xfail.py @@ -19,8 +19,6 @@ when it fails. Instead terminal reporting will list it in the import py -pytest_plugins = ['keyword'] - def pytest_runtest_makereport(__call__, item, call): if call.when != "call": return @@ -53,6 +51,9 @@ def pytest_terminal_summary(terminalreporter): modpath = rep.item.getmodpath(includemodule=True) pos = "%s %s:%d: " %(modpath, entry.path, entry.lineno) reason = rep.longrepr.reprcrash.message + i = reason.find("\n") + if i != -1: + reason = reason[:i] tr._tw.line("%s %s" %(pos, reason)) xpassed = terminalreporter.stats.get("xpassed") @@ -89,3 +90,4 @@ def test_xfail(testdir): "*test_that*", ]) assert result.ret == 1 + diff --git a/py/test/pluginmanager.py b/py/test/pluginmanager.py index d9c6860a2..c9e946b6e 100644 --- a/py/test/pluginmanager.py +++ b/py/test/pluginmanager.py @@ -200,9 +200,6 @@ class PluginManager(object): config.hook.pytest_unconfigure(config=config) config.pluginmanager.unregister(self) -class Ext: - """ namespace for extension objects. """ - # # XXX old code to automatically load classes # diff --git a/py/test/testing/test_pluginmanager.py b/py/test/testing/test_pluginmanager.py index a4ea7f27d..8365652f9 100644 --- a/py/test/testing/test_pluginmanager.py +++ b/py/test/testing/test_pluginmanager.py @@ -185,7 +185,7 @@ class TestPytestPluginInteractions: assert hello == "world" """) result = testdir.runpytest(p) - assert result.stdout.fnmatch_lines([ + result.stdout.fnmatch_lines([ "*1 passed*" ]) @@ -247,3 +247,12 @@ def test_collectattr(): assert list(methods) == ['pytest_hello', 'pytest_world'] methods = py.builtin.sorted(collectattr(B())) assert list(methods) == ['pytest_hello', 'pytest_world'] + +@py.test.mark.xfail +def test_namespace_has_default_and_env_plugins(testdir): + p = testdir.makepyfile(""" + import py + py.test.mark + """) + result = testdir.runpython(p) + assert result.ret == 0 From a01e4769ccf95d193d1b617f2e5d138658b52bd2 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Sun, 9 Aug 2009 23:51:25 +0200 Subject: [PATCH 2/8] simplify internal plugin dispatching code, rename parts of the py._com plugin helpers --HG-- branch : 1.0.x --- CHANGELOG | 11 +- py/__init__.py | 2 +- py/_com.py | 143 ++++++++++-------------- py/execnet/gateway.py | 4 +- py/execnet/gwmanage.py | 3 +- py/misc/testing/test_com.py | 77 +++++-------- py/test/plugin/pytest__pytest.py | 2 +- py/test/plugin/pytest_capture.py | 6 +- py/test/plugin/pytest_default.py | 2 +- py/test/plugin/pytest_execnetcleanup.py | 2 +- py/test/plugin/pytest_hooklog.py | 25 ++++- py/test/plugin/pytest_xfail.py | 2 +- py/test/pluginmanager.py | 21 ++-- py/test/pycollect.py | 4 +- py/test/testing/test_collect.py | 2 +- py/test/testing/test_pluginmanager.py | 4 - 16 files changed, 137 insertions(+), 173 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index c4120e896..939214f1b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,18 +1,21 @@ Changes between 1.0.0 and 1.0.1 ===================================== -* various unicode fixes: capturing and prints of unicode strings now - work within tests, they are encoded as "utf8" by default, terminalwriting +* unicode fixes: capturing and unicode writes to sys.stdout + (through e.g a print statement) now work within tests, + they are encoded as "utf8" by default, also terminalwriting was adapted and somewhat unified between windows and linux * fix issue #27: better reporting on non-collectable items given on commandline (e.g. pyc files) -* "Test" prefixed classes with an __init__ method are *not* collected by default anymore +* "Test" prefixed classes are *not* collected by default anymore if they + have an __init__ method * terser reporting of collection error tracebacks -* renaming of arguments to some special rather internal hooks +* streamlined internal plugin arch code, renamed of internal methods + and argnames (related to py/_com.py multicall/plugin) Changes between 1.0.0b9 and 1.0.0 ===================================== diff --git a/py/__init__.py b/py/__init__.py index ecabde8b4..e94966656 100644 --- a/py/__init__.py +++ b/py/__init__.py @@ -52,7 +52,7 @@ initpkg(__name__, '_com.Registry' : ('./_com.py', 'Registry'), '_com.MultiCall' : ('./_com.py', 'MultiCall'), '_com.comregistry' : ('./_com.py', 'comregistry'), - '_com.Hooks' : ('./_com.py', 'Hooks'), + '_com.HookRelay' : ('./_com.py', 'HookRelay'), # py lib cmdline tools 'cmdline.pytest' : ('./cmdline/pytest.py', 'main',), diff --git a/py/_com.py b/py/_com.py index 6ba7b47fb..9085916c8 100644 --- a/py/_com.py +++ b/py/_com.py @@ -5,77 +5,50 @@ py lib plugins and plugin call management import py class MultiCall: - """ Manage a specific call into many python functions/methods. + """ execute a call into multiple python functions/methods. """ - Simple example: - MultiCall([list1.append, list2.append], 42).execute() - """ - - def __init__(self, methods, *args, **kwargs): + def __init__(self, methods, kwargs, firstresult=False): self.methods = methods[:] - self.args = args self.kwargs = kwargs self.results = [] + self.firstresult = firstresult def __repr__(self): - args = [] - if self.args: - args.append("posargs=%r" %(self.args,)) - kw = self.kwargs - args.append(", ".join(["%s=%r" % x for x in self.kwargs.items()])) - args = " ".join(args) - status = "results: %r, rmethods: %r" % (self.results, self.methods) - return "" %(args, status) + status = "%d results, %d meths" % (len(self.results), len(self.methods)) + return "" %(status, self.kwargs) - def execute(self, firstresult=False): + def execute(self): while self.methods: - currentmethod = self.methods.pop() - res = self.execute_method(currentmethod) - if hasattr(self, '_ex1'): - self.results = [res] - break + method = self.methods.pop() + res = self._call1(method) if res is not None: self.results.append(res) - if firstresult: - break - if not firstresult: + if self.firstresult: + break + if not self.firstresult: return self.results if self.results: - return self.results[-1] + return self.results[-1] - def execute_method(self, currentmethod): - self.currentmethod = currentmethod - # provide call introspection if "__call__" is the first positional argument - if hasattr(currentmethod, 'im_self'): - varnames = currentmethod.im_func.func_code.co_varnames - needscall = varnames[1:2] == ('__call__',) - else: - try: - varnames = currentmethod.func_code.co_varnames - except AttributeError: - # builtin function - varnames = () - needscall = varnames[:1] == ('__call__',) - if needscall: - return currentmethod(self, *self.args, **self.kwargs) - else: - #try: - return currentmethod(*self.args, **self.kwargs) - #except TypeError: - # print currentmethod.__module__, currentmethod.__name__, self.args, self.kwargs - # raise - - def exclude_other_results(self): - self._ex1 = True + def _call1(self, method): + kwargs = self.kwargs + if '__call__' in varnames(method): + kwargs = kwargs.copy() + kwargs['__call__'] = self + return method(**kwargs) +def varnames(rawcode): + rawcode = getattr(rawcode, 'im_func', rawcode) + rawcode = getattr(rawcode, 'func_code', rawcode) + try: + return rawcode.co_varnames + except AttributeError: + return () class Registry: """ - Manage Plugins: Load plugins and manage calls to plugins. + Manage Plugins: register/unregister call calls to plugins. """ - logfile = None - MultiCall = MultiCall - def __init__(self, plugins=None): if plugins is None: plugins = [] @@ -83,6 +56,7 @@ class Registry: def register(self, plugin): assert not isinstance(plugin, str) + assert not plugin in self._plugins self._plugins.append(plugin) def unregister(self, plugin): @@ -107,45 +81,44 @@ class Registry: l.reverse() return l -class Hooks: - def __init__(self, hookspecs, registry=None): +class HookRelay: + def __init__(self, hookspecs, registry): self._hookspecs = hookspecs - if registry is None: - registry = py._com.comregistry - self.registry = registry + self._registry = registry for name, method in vars(hookspecs).items(): if name[:1] != "_": - firstresult = getattr(method, 'firstresult', False) - mm = HookCall(registry, name, firstresult=firstresult) - setattr(self, name, mm) - def __repr__(self): - return "" %(self._hookspecs, self.registry) + setattr(self, name, self._makecall(name)) -class HookCall: - def __init__(self, registry, name, firstresult, extralookup=None): - self.registry = registry + def _makecall(self, name, extralookup=None): + hookspecmethod = getattr(self._hookspecs, name) + firstresult = getattr(hookspecmethod, 'firstresult', False) + return HookCaller(self, name, firstresult=firstresult, + extralookup=extralookup) + + def _getmethods(self, name, extralookup=()): + return self._registry.listattr(name, extra=extralookup) + + def _performcall(self, name, multicall): + return multicall.execute() + + def __repr__(self): + return "" %(self._hookspecs, self._registry) + +class HookCaller: + def __init__(self, hookrelay, name, firstresult, extralookup=()): + self.hookrelay = hookrelay self.name = name self.firstresult = firstresult self.extralookup = extralookup and [extralookup] or () - def clone(self, extralookup): - return HookCall(self.registry, self.name, self.firstresult, extralookup) - def __repr__(self): - mode = self.firstresult and "firstresult" or "each" - return "" %(self.name, mode, self.registry) + return "" %( + self.name, self.firstresult, self.hookrelay) - def __call__(self, *args, **kwargs): - if args: - raise TypeError("only keyword arguments allowed " - "for api call to %r" % self.name) - attr = self.registry.listattr(self.name, extra=self.extralookup) - mc = MultiCall(attr, **kwargs) - # XXX this should be doable from a hook impl: - if self.registry.logfile: - self.registry.logfile.write("%s(**%s) # firstresult=%s\n" % - (self.name, kwargs, self.firstresult)) - self.registry.logfile.flush() - return mc.execute(firstresult=self.firstresult) - -comregistry = Registry() + def __call__(self, **kwargs): + methods = self.hookrelay._getmethods(self.name, + extralookup=self.extralookup) + mc = MultiCall(methods, kwargs, firstresult=self.firstresult) + return self.hookrelay._performcall(self.name, mc) + +comregistry = Registry([]) diff --git a/py/execnet/gateway.py b/py/execnet/gateway.py index 2ea35ae91..751380029 100644 --- a/py/execnet/gateway.py +++ b/py/execnet/gateway.py @@ -88,8 +88,8 @@ class Gateway(object): self._channelfactory = ChannelFactory(self, _startcount) self._cleanup.register(self) if _startcount == 1: # only import 'py' on the "client" side - from py._com import Hooks - self.hook = Hooks(ExecnetAPI) + import py + self.hook = py._com.HookRelay(ExecnetAPI, py._com.comregistry) else: self.hook = ExecnetAPI() diff --git a/py/execnet/gwmanage.py b/py/execnet/gwmanage.py index 9cd749268..6003ea491 100644 --- a/py/execnet/gwmanage.py +++ b/py/execnet/gwmanage.py @@ -21,7 +21,8 @@ class GatewayManager: if not spec.chdir and not spec.popen: spec.chdir = defaultchdir self.specs.append(spec) - self.hook = py._com.Hooks(py.execnet._HookSpecs) + self.hook = py._com.HookRelay( + py.execnet._HookSpecs, py._com.comregistry) def makegateways(self): assert not self.gateways diff --git a/py/misc/testing/test_com.py b/py/misc/testing/test_com.py index 5734420fe..9bccdc9c0 100644 --- a/py/misc/testing/test_com.py +++ b/py/misc/testing/test_com.py @@ -1,15 +1,12 @@ import py import os -from py._com import Registry, MultiCall -from py._com import Hooks - -pytest_plugins = "xfail" +from py._com import Registry, MultiCall, HookRelay class TestMultiCall: def test_uses_copy_of_methods(self): l = [lambda: 42] - mc = MultiCall(l) + mc = MultiCall(l, {}) repr(mc) l[:] = [] res = mc.execute() @@ -18,22 +15,19 @@ 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) + multicall = MultiCall([p1.m, p2.m], {'x': 23}) assert "23" in repr(multicall) reslist = multicall.execute() assert len(reslist) == 2 @@ -43,62 +37,44 @@ class TestMultiCall: def test_keyword_args(self): def f(x): return x + 1 - multicall = MultiCall([f], x=23) - assert "x=23" in repr(multicall) + multicall = MultiCall([f], dict(x=23)) + assert "'x': 23" in repr(multicall) reslist = multicall.execute() assert reslist == [24] - assert "24" in repr(multicall) + assert "1 results" in repr(multicall) def test_optionalcallarg(self): class P1: def m(self, x): return x - call = MultiCall([P1().m], 23) + call = MultiCall([P1().m], dict(x=23)) assert "23" in repr(call) assert call.execute() == [23] - assert call.execute(firstresult=True) == 23 + call = MultiCall([P1().m], dict(x=23), firstresult=True) def test_call_subexecute(self): def m(__call__): - subresult = __call__.execute(firstresult=True) + subresult = __call__.execute() return subresult + 1 def n(): return 1 - call = MultiCall([n, m]) - res = call.execute(firstresult=True) - assert res == 2 - - def test_call_exclude_other_results(self): - def m(__call__): - __call__.exclude_other_results() - return 10 - - def n(): - return 1 - - call = MultiCall([n, n, m, n]) + call = MultiCall([n, m], {}, firstresult=True) res = call.execute() - assert res == [10] - # doesn't really make sense for firstresult-mode - because - # we might not have had a chance to run at all. - #res = call.execute(firstresult=True) - #assert res == 10 + assert res == 2 def test_call_none_is_no_result(self): def m1(): return 1 def m2(): return None - mc = MultiCall([m1, m2]) - res = mc.execute(firstresult=True) + res = MultiCall([m1, m2], {}, firstresult=True).execute() assert res == 1 + res = MultiCall([m1, m2], {}).execute() + assert res == [1] class TestRegistry: - def test_MultiCall(self): - plugins = Registry() - assert hasattr(plugins, "MultiCall") def test_register(self): registry = Registry() @@ -142,14 +118,14 @@ class TestRegistry: def test_api_and_defaults(): assert isinstance(py._com.comregistry, Registry) -class TestHooks: +class TestHookRelay: def test_happypath(self): registry = Registry() class Api: def hello(self, arg): pass - mcm = Hooks(hookspecs=Api, registry=registry) + mcm = HookRelay(hookspecs=Api, registry=registry) assert hasattr(mcm, 'hello') assert repr(mcm.hello).find("hello") != -1 class Plugin: @@ -160,23 +136,21 @@ class TestHooks: assert l == [4] assert not hasattr(mcm, 'world') - def test_needskeywordargs(self): + def test_only_kwargs(self): registry = Registry() class Api: def hello(self, arg): pass - mcm = Hooks(hookspecs=Api, registry=registry) - excinfo = py.test.raises(TypeError, "mcm.hello(3)") - assert str(excinfo.value).find("only keyword arguments") != -1 - assert str(excinfo.value).find("hello(self, arg)") + mcm = HookRelay(hookspecs=Api, registry=registry) + py.test.raises(TypeError, "mcm.hello(3)") - def test_firstresult(self): + def test_firstresult_definition(self): registry = Registry() class Api: def hello(self, arg): pass hello.firstresult = True - mcm = Hooks(hookspecs=Api, registry=registry) + mcm = HookRelay(hookspecs=Api, registry=registry) class Plugin: def hello(self, arg): return arg + 1 @@ -186,15 +160,16 @@ class TestHooks: def test_default_plugins(self): class Api: pass - mcm = Hooks(hookspecs=Api) - assert mcm.registry == py._com.comregistry + mcm = HookRelay(hookspecs=Api, registry=py._com.comregistry) + assert mcm._registry == py._com.comregistry def test_hooks_extra_plugins(self): registry = Registry() class Api: def hello(self, arg): pass - hook_hello = Hooks(hookspecs=Api, registry=registry).hello + hookrelay = HookRelay(hookspecs=Api, registry=registry) + hook_hello = hookrelay.hello class Plugin: def hello(self, arg): return arg + 1 @@ -202,7 +177,7 @@ class TestHooks: class Plugin2: def hello(self, arg): return arg + 2 - newhook = hook_hello.clone(extralookup=Plugin2()) + newhook = hookrelay._makecall("hello", extralookup=Plugin2()) l = newhook(arg=3) assert l == [5, 4] l2 = hook_hello(arg=3) diff --git a/py/test/plugin/pytest__pytest.py b/py/test/plugin/pytest__pytest.py index 3cca095f3..756d3cae4 100644 --- a/py/test/plugin/pytest__pytest.py +++ b/py/test/plugin/pytest__pytest.py @@ -47,7 +47,7 @@ class HookRecorder: recorder = RecordCalls() self._recorders[hookspecs] = recorder self._comregistry.register(recorder) - self.hook = py._com.Hooks(hookspecs, registry=self._comregistry) + self.hook = py._com.HookRelay(hookspecs, registry=self._comregistry) def finish_recording(self): for recorder in self._recorders.values(): diff --git a/py/test/plugin/pytest_capture.py b/py/test/plugin/pytest_capture.py index 3acb8a5a3..228b4617b 100644 --- a/py/test/plugin/pytest_capture.py +++ b/py/test/plugin/pytest_capture.py @@ -185,7 +185,7 @@ class CaptureManager: method = self._getmethod(collector.config, collector.fspath) self.resumecapture(method) try: - rep = __call__.execute(firstresult=True) + rep = __call__.execute() finally: outerr = self.suspendcapture() addouterr(rep, outerr) @@ -208,7 +208,7 @@ class CaptureManager: method = self._getmethod(session.config, None) self.resumecapture(method) try: - rep = __call__.execute(firstresult=True) + rep = __call__.execute() finally: outerr = self.suspendcapture() if rep: @@ -221,7 +221,7 @@ class CaptureManager: def pytest_runtest_makereport(self, __call__, item, call): self.deactivate_funcargs() - rep = __call__.execute(firstresult=True) + rep = __call__.execute() outerr = self.suspendcapture() outerr = (item.outerr[0] + outerr[0], item.outerr[1] + outerr[1]) if not rep.passed: diff --git a/py/test/plugin/pytest_default.py b/py/test/plugin/pytest_default.py index b27ef4d68..cc6ff7e0a 100644 --- a/py/test/plugin/pytest_default.py +++ b/py/test/plugin/pytest_default.py @@ -3,7 +3,7 @@ import py def pytest_pyfunc_call(__call__, pyfuncitem): - if not __call__.execute(firstresult=True): + if not __call__.execute(): testfunction = pyfuncitem.obj if pyfuncitem._isyieldedfunction(): testfunction(*pyfuncitem._args) diff --git a/py/test/plugin/pytest_execnetcleanup.py b/py/test/plugin/pytest_execnetcleanup.py index 7a5f65aeb..52c3fdd89 100644 --- a/py/test/plugin/pytest_execnetcleanup.py +++ b/py/test/plugin/pytest_execnetcleanup.py @@ -35,7 +35,7 @@ class Execnetcleanup: def pytest_pyfunc_call(self, __call__, pyfuncitem): if self._gateways is not None: gateways = self._gateways[:] - res = __call__.execute(firstresult=True) + res = __call__.execute() while len(self._gateways) > len(gateways): self._gateways[-1].exit() return res diff --git a/py/test/plugin/pytest_hooklog.py b/py/test/plugin/pytest_hooklog.py index c00b39151..96ed854ab 100644 --- a/py/test/plugin/pytest_hooklog.py +++ b/py/test/plugin/pytest_hooklog.py @@ -8,14 +8,27 @@ def pytest_addoption(parser): def pytest_configure(config): hooklog = config.getvalue("hooklog") if hooklog: - assert not config.pluginmanager.comregistry.logfile - config.pluginmanager.comregistry.logfile = open(hooklog, 'w') + config._hooklogfile = open(hooklog, 'w', 0) + config._hooklog_oldperformcall = config.hook._performcall + config.hook._performcall = (lambda name, multicall: + logged_call(name=name, multicall=multicall, config=config)) + +def logged_call(name, multicall, config): + f = config._hooklogfile + f.write("%s(**%s)\n" % (name, multicall.kwargs)) + try: + res = config._hooklog_oldperformcall(name=name, multicall=multicall) + except: + f.write("-> exception") + raise + f.write("-> %r" % (res,)) + return res def pytest_unconfigure(config): - f = config.pluginmanager.comregistry.logfile - if f: - f.close() - config.pluginmanager.comregistry.logfile = None + try: + del config.hook.__dict__['_performcall'] + except KeyError: + pass # =============================================================================== # plugin tests diff --git a/py/test/plugin/pytest_xfail.py b/py/test/plugin/pytest_xfail.py index 8e6a3b73c..56aac847c 100644 --- a/py/test/plugin/pytest_xfail.py +++ b/py/test/plugin/pytest_xfail.py @@ -24,7 +24,7 @@ def pytest_runtest_makereport(__call__, item, call): return if hasattr(item, 'obj') and hasattr(item.obj, 'func_dict'): if 'xfail' in item.obj.func_dict: - res = __call__.execute(firstresult=True) + res = __call__.execute() if call.excinfo: res.skipped = True res.failed = res.passed = False diff --git a/py/test/pluginmanager.py b/py/test/pluginmanager.py index c9e946b6e..621fd5a99 100644 --- a/py/test/pluginmanager.py +++ b/py/test/pluginmanager.py @@ -16,10 +16,9 @@ class PluginManager(object): if comregistry is None: comregistry = py._com.Registry() self.comregistry = comregistry - self.MultiCall = self.comregistry.MultiCall self.impname2plugin = {} - self.hook = py._com.Hooks( + self.hook = py._com.HookRelay( hookspecs=hookspec, registry=self.comregistry) @@ -166,20 +165,24 @@ class PluginManager(object): return self.hook.pytest_internalerror(excrepr=excrepr) def do_addoption(self, parser): - methods = self.comregistry.listattr("pytest_addoption", reverse=True) - mc = py._com.MultiCall(methods, parser=parser) + mname = "pytest_addoption" + methods = self.comregistry.listattr(mname, reverse=True) + mc = py._com.MultiCall(methods, {'parser': parser}) mc.execute() def pytest_plugin_registered(self, plugin): if hasattr(self, '_config'): - self.call_plugin(plugin, "pytest_addoption", parser=self._config._parser) - self.call_plugin(plugin, "pytest_configure", config=self._config) + self.call_plugin(plugin, "pytest_addoption", + {'parser': self._config._parser}) + self.call_plugin(plugin, "pytest_configure", + {'config': self._config}) #dic = self.call_plugin(plugin, "pytest_namespace") #self._updateext(dic) - def call_plugin(self, plugin, methname, **kwargs): - return self.MultiCall(self.listattr(methname, plugins=[plugin]), - **kwargs).execute(firstresult=True) + def call_plugin(self, plugin, methname, kwargs): + return py._com.MultiCall( + methods=self.listattr(methname, plugins=[plugin]), + kwargs=kwargs, firstresult=True).execute() def _updateext(self, dic): if dic: diff --git a/py/test/pycollect.py b/py/test/pycollect.py index 9e7bd49df..4df59b1ee 100644 --- a/py/test/pycollect.py +++ b/py/test/pycollect.py @@ -155,8 +155,8 @@ class PyCollectorMixin(PyobjMixin, py.test.collect.Collector): cls = clscol and clscol.obj or None metafunc = funcargs.Metafunc(funcobj, config=self.config, cls=cls, module=module) - gentesthook = self.config.hook.pytest_generate_tests.clone( - extralookup=module) + gentesthook = self.config.hook._makecall( + "pytest_generate_tests", extralookup=module) gentesthook(metafunc=metafunc) if not metafunc._calls: return self.Function(name, parent=self) diff --git a/py/test/testing/test_collect.py b/py/test/testing/test_collect.py index a43a4ccd3..c37c638b7 100644 --- a/py/test/testing/test_collect.py +++ b/py/test/testing/test_collect.py @@ -145,7 +145,7 @@ class TestCollectFS: names = [x.name for x in col.collect()] assert names == ["dir1", "dir2", "test_one.py", "test_two.py", "x"] -class TestCollectPluginHooks: +class TestCollectPluginHookRelay: def test_pytest_collect_file(self, testdir): tmpdir = testdir.tmpdir wascalled = [] diff --git a/py/test/testing/test_pluginmanager.py b/py/test/testing/test_pluginmanager.py index 8365652f9..3e96e8683 100644 --- a/py/test/testing/test_pluginmanager.py +++ b/py/test/testing/test_pluginmanager.py @@ -222,10 +222,6 @@ class TestPytestPluginInteractions: config.pluginmanager.register(A()) assert len(l) == 2 - def test_MultiCall(self): - pp = PluginManager() - assert hasattr(pp, 'MultiCall') - # lower level API def test_listattr(self): From b552f6eb461e2dcfbcfc4b08bf4ac5eafdad02db Mon Sep 17 00:00:00 2001 From: holger krekel Date: Mon, 10 Aug 2009 11:27:13 +0200 Subject: [PATCH 3/8] * add pytest_nose plugin * have unittest functions always receive a fresh instance --HG-- branch : 1.0.x --- CHANGELOG | 4 ++ doc/test/plugin/nose.txt | 64 ++++++++++++++++++++ makepluginlist.py | 2 +- py/test/collect.py | 7 ++- py/test/plugin/conftest.py | 11 +++- py/test/plugin/pytest_nose.py | 97 ++++++++++++++++++++++++++++++ py/test/plugin/pytest_terminal.py | 13 ++-- py/test/plugin/pytest_unittest.py | 16 ++++- py/test/plugin/test_pytest_nose.py | 87 +++++++++++++++++++++++++++ 9 files changed, 286 insertions(+), 15 deletions(-) create mode 100644 doc/test/plugin/nose.txt create mode 100644 py/test/plugin/pytest_nose.py create mode 100644 py/test/plugin/test_pytest_nose.py diff --git a/CHANGELOG b/CHANGELOG index 939214f1b..9310172e5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,10 @@ Changes between 1.0.0 and 1.0.1 ===================================== +* added a 'pytest_nose' plugin which handles nose.SkipTest, + nose-style function/method/generator setup/teardown and + tries to report functions correctly. + * unicode fixes: capturing and unicode writes to sys.stdout (through e.g a print statement) now work within tests, they are encoded as "utf8" by default, also terminalwriting diff --git a/doc/test/plugin/nose.txt b/doc/test/plugin/nose.txt new file mode 100644 index 000000000..1f5775a11 --- /dev/null +++ b/doc/test/plugin/nose.txt @@ -0,0 +1,64 @@ + +pytest_nose plugin +================== + +nose-compatibility plugin: allow to run nose test suites natively. + +.. contents:: + :local: + +This is an experimental plugin for allowing to run tests written +in the 'nosetests' style with py.test. +nosetests is a popular clone +of py.test and thus shares some philosophy. This plugin is an +attempt to understand and neutralize differences. It allows to +run nosetests' own test suite and a number of other test suites +without problems. + +Usage +------------- + +If you type:: + + py.test -p nose + +where you would type ``nosetests``, you can run your nose style tests. +You might also try to run without the nose plugin to see where your test +suite is incompatible to the default py.test. + +To avoid the need for specifying a command line option you can set an environment +variable:: + + PYTEST_PLUGINS=nose + +or create a ``conftest.py`` file in your test directory or below:: + + # conftest.py + pytest_plugins = "nose", + +If you find issues or have suggestions you may run:: + + py.test -p nose --pastebin=all + +to create a URL of a test run session and send it with comments to the issue +tracker or mailing list. + +Known issues +------------------ + +- nose-style doctests are not collected and executed correctly, + also fixtures don't work. + +Start improving this plugin in 30 seconds +========================================= + + +Do you find the above documentation or the plugin itself lacking? + +1. Download `pytest_nose.py`_ plugin source code +2. put it somewhere as ``pytest_nose.py`` into your import path +3. a subsequent ``py.test`` run will use your local version + +Further information: extend_ documentation, other plugins_ or contact_. + +.. include:: links.txt diff --git a/makepluginlist.py b/makepluginlist.py index d220238a9..c4bf6f2ba 100644 --- a/makepluginlist.py +++ b/makepluginlist.py @@ -7,7 +7,7 @@ plugins = [ ('Plugins related to Python test functions and programs', 'xfail figleaf monkeypatch capture recwarn',), ('Plugins for other testing styles and languages', - 'unittest doctest oejskit restdoc'), + 'oejskit unittest nose doctest restdoc'), ('Plugins for generic reporting and failure logging', 'pastebin resultlog terminal',), ('internal plugins / core functionality', diff --git a/py/test/collect.py b/py/test/collect.py index 8a2e9467b..722f8e0c8 100644 --- a/py/test/collect.py +++ b/py/test/collect.py @@ -395,7 +395,12 @@ class Directory(FSCollector): def _ignore(self, path): ignore_paths = self.config.getconftest_pathlist("collect_ignore", path=path) - return ignore_paths and path in ignore_paths + return ignore_paths and path in ignore_paths + # XXX more refined would be: + if ignore_paths: + for p in ignore_paths: + if path == p or path.relto(p): + return True def consider(self, path): if self._ignore(path): diff --git a/py/test/plugin/conftest.py b/py/test/plugin/conftest.py index a4542a643..93d239708 100644 --- a/py/test/plugin/conftest.py +++ b/py/test/plugin/conftest.py @@ -15,10 +15,15 @@ def pytest_funcarg__testdir(request): # testdir.plugins.append(obj.testplugin) # break #else: - basename = request.module.__name__.split(".")[-1] - if basename.startswith("pytest_"): + modname = request.module.__name__.split(".")[-1] + if modname.startswith("pytest_"): testdir.plugins.append(vars(request.module)) - testdir.plugins.append(basename) + testdir.plugins.append(modname) + #elif modname.startswith("test_pytest"): + # pname = modname[5:] + # assert pname not in testdir.plugins + # testdir.plugins.append(pname) + # #testdir.plugins.append(vars(request.module)) else: pass # raise ValueError("need better support code") return testdir diff --git a/py/test/plugin/pytest_nose.py b/py/test/plugin/pytest_nose.py new file mode 100644 index 000000000..c77b01f08 --- /dev/null +++ b/py/test/plugin/pytest_nose.py @@ -0,0 +1,97 @@ +"""nose-compatibility plugin: allow to run nose test suites natively. + +This is an experimental plugin for allowing to run tests written +in the 'nosetests' style with py.test. +nosetests is a popular clone +of py.test and thus shares some philosophy. This plugin is an +attempt to understand and neutralize differences. It allows to +run nosetests' own test suite and a number of other test suites +without problems. + +Usage +------------- + +If you type:: + + py.test -p nose + +where you would type ``nosetests``, you can run your nose style tests. +You might also try to run without the nose plugin to see where your test +suite is incompatible to the default py.test. + +To avoid the need for specifying a command line option you can set an environment +variable:: + + PYTEST_PLUGINS=nose + +or create a ``conftest.py`` file in your test directory or below:: + + # conftest.py + pytest_plugins = "nose", + +If you find issues or have suggestions you may run:: + + py.test -p nose --pastebin=all + +to create a URL of a test run session and send it with comments to the issue +tracker or mailing list. + +Known issues +------------------ + +- nose-style doctests are not collected and executed correctly, + also fixtures don't work. + +""" +import py +import inspect +import sys + +def pytest_runtest_makereport(__call__, item, call): + SkipTest = getattr(sys.modules.get('nose', None), 'SkipTest', None) + if SkipTest: + if call.excinfo and call.excinfo.errisinstance(SkipTest): + # let's substitute the excinfo with a py.test.skip one + call2 = call.__class__(lambda: py.test.skip(str(call.excinfo.value)), call.when) + call.excinfo = call2.excinfo + +def pytest_report_iteminfo(item): + # nose 0.11.1 uses decorators for "raises" and other helpers. + # for reporting progress by filename we fish for the filename + if isinstance(item, py.test.collect.Function): + obj = item.obj + if hasattr(obj, 'compat_co_firstlineno'): + fn = sys.modules[obj.__module__].__file__ + if fn.endswith(".pyc"): + fn = fn[:-1] + #assert 0 + #fn = inspect.getsourcefile(obj) or inspect.getfile(obj) + lineno = obj.compat_co_firstlineno + return py.path.local(fn), lineno, obj.__module__ + +def pytest_runtest_setup(item): + if isinstance(item, (py.test.collect.Function)): + if isinstance(item.parent, py.test.collect.Generator): + gen = item.parent + if not hasattr(gen, '_nosegensetup'): + call_optional(gen.obj, 'setup') + if isinstance(gen.parent, py.test.collect.Instance): + call_optional(gen.parent.obj, 'setup') + gen._nosegensetup = True + call_optional(item.obj, 'setup') + +def pytest_runtest_teardown(item): + if isinstance(item, py.test.collect.Function): + call_optional(item.obj, 'teardown') + #if hasattr(item.parent, '_nosegensetup'): + # #call_optional(item._nosegensetup, 'teardown') + # del item.parent._nosegensetup + +def pytest_make_collect_report(collector): + if isinstance(collector, py.test.collect.Generator): + call_optional(collector.obj, 'setup') + +def call_optional(obj, name): + method = getattr(obj, name, None) + if method: + method() diff --git a/py/test/plugin/pytest_terminal.py b/py/test/plugin/pytest_terminal.py index 158204b1e..c2f83c124 100644 --- a/py/test/plugin/pytest_terminal.py +++ b/py/test/plugin/pytest_terminal.py @@ -241,15 +241,10 @@ class TerminalReporter: if self.config.option.traceconfig: plugins = [] for plugin in self.config.pluginmanager.comregistry: - name = plugin.__class__.__name__ - if name.endswith("Plugin"): - name = name[:-6] - #if name == "Conftest": - # XXX get filename - plugins.append(name) - else: - plugins.append(str(plugin)) - + name = getattr(plugin, '__name__', None) + if name is None: + name = plugin.__class__.__name__ + plugins.append(name) plugins = ", ".join(plugins) self.write_line("active plugins: %s" %(plugins,)) for i, testarg in py.builtin.enumerate(self.config.args): diff --git a/py/test/plugin/pytest_unittest.py b/py/test/plugin/pytest_unittest.py index 6e17a1c77..1340cae99 100644 --- a/py/test/plugin/pytest_unittest.py +++ b/py/test/plugin/pytest_unittest.py @@ -55,6 +55,9 @@ class UnitTestFunction(py.test.collect.Function): if obj is not _dummy: self._obj = obj self._sort_value = sort_value + if hasattr(self.parent, 'newinstance'): + self.parent.newinstance() + self.obj = self._getobj() def runtest(self): target = self.obj @@ -87,7 +90,6 @@ def test_simple_unittest(testdir): def test_setup(testdir): testpath = testdir.makepyfile(test_two=""" import unittest - pytest_plugins = "pytest_unittest" # XXX class MyTestCase(unittest.TestCase): def setUp(self): self.foo = 1 @@ -98,6 +100,18 @@ def test_setup(testdir): rep = reprec.matchreport("test_setUp") assert rep.passed +def test_new_instances(testdir): + testpath = testdir.makepyfile(""" + import unittest + class MyTestCase(unittest.TestCase): + def test_func1(self): + self.x = 2 + def test_func2(self): + assert not hasattr(self, 'x') + """) + reprec = testdir.inline_run(testpath) + reprec.assertoutcome(passed=2) + def test_teardown(testdir): testpath = testdir.makepyfile(test_three=""" import unittest diff --git a/py/test/plugin/test_pytest_nose.py b/py/test/plugin/test_pytest_nose.py new file mode 100644 index 000000000..4950095dd --- /dev/null +++ b/py/test/plugin/test_pytest_nose.py @@ -0,0 +1,87 @@ +import py +py.test.importorskip("nose") + +def test_nose_setup(testdir): + p = testdir.makepyfile(""" + l = [] + + def test_hello(): + assert l == [1] + def test_world(): + assert l == [1,2] + test_hello.setup = lambda: l.append(1) + test_hello.teardown = lambda: l.append(2) + """) + result = testdir.runpytest(p, '-p', 'nose') + result.stdout.fnmatch_lines([ + "*2 passed*" + ]) + +def test_nose_test_generator_fixtures(testdir): + p = testdir.makepyfile(""" + # taken from nose-0.11.1 unit_tests/test_generator_fixtures.py + from nose.tools import eq_ + called = [] + + def outer_setup(): + called.append('outer_setup') + + def outer_teardown(): + called.append('outer_teardown') + + def inner_setup(): + called.append('inner_setup') + + def inner_teardown(): + called.append('inner_teardown') + + def test_gen(): + called[:] = [] + for i in range(0, 5): + yield check, i + + def check(i): + expect = ['outer_setup'] + for x in range(0, i): + expect.append('inner_setup') + expect.append('inner_teardown') + expect.append('inner_setup') + eq_(called, expect) + + + test_gen.setup = outer_setup + test_gen.teardown = outer_teardown + check.setup = inner_setup + check.teardown = inner_teardown + + class TestClass(object): + def setup(self): + print "setup called in", self + self.called = ['setup'] + + def teardown(self): + print "teardown called in", self + eq_(self.called, ['setup']) + self.called.append('teardown') + + def test(self): + print "test called in", self + for i in range(0, 5): + yield self.check, i + + def check(self, i): + print "check called in", self + expect = ['setup'] + #for x in range(0, i): + # expect.append('setup') + # expect.append('teardown') + #expect.append('setup') + eq_(self.called, expect) + + """) + result = testdir.runpytest(p, '-p', 'nose') + result.stdout.fnmatch_lines([ + "*10 passed*" + ]) + + From 37976be5292ba771a165249b77405d8e157dbc5c Mon Sep 17 00:00:00 2001 From: holger krekel Date: Tue, 11 Aug 2009 19:00:41 +0200 Subject: [PATCH 4/8] [mq]: flexcom --HG-- branch : 1.0.x --- CHANGELOG | 4 +- doc/test/extend.txt | 14 +++---- py/_com.py | 36 +++++++++-------- py/misc/testing/test_com.py | 51 +++++++++++++++---------- py/test/dist/dsession.py | 4 +- py/test/plugin/pytest_capture.py | 12 +++--- py/test/plugin/pytest_default.py | 4 +- py/test/plugin/pytest_execnetcleanup.py | 4 +- py/test/plugin/pytest_nose.py | 2 +- py/test/plugin/pytest_pastebin.py | 4 +- py/test/plugin/pytest_terminal.py | 4 +- py/test/plugin/pytest_xfail.py | 4 +- py/test/pluginmanager.py | 15 ++++---- 13 files changed, 83 insertions(+), 75 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 9310172e5..ed1b2746e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -18,8 +18,8 @@ Changes between 1.0.0 and 1.0.1 * terser reporting of collection error tracebacks -* streamlined internal plugin arch code, renamed of internal methods - and argnames (related to py/_com.py multicall/plugin) +* simplified multicall mechanism and plugin architecture, + renamed some internal methods and argnames Changes between 1.0.0b9 and 1.0.0 ===================================== diff --git a/doc/test/extend.txt b/doc/test/extend.txt index bdd87b4a2..b5c9c2a5b 100644 --- a/doc/test/extend.txt +++ b/doc/test/extend.txt @@ -90,13 +90,13 @@ Available py.test hooks ==================================== py.test calls hooks functions to implement its `test collection`_, running and -reporting process. Upon loading of a plugin py.test performs -strict checking on contained hook functions. Function and argument names -need to match exactly one of `hook definition specification`_. It thus -provides useful error reporting on mistyped hook or argument names -and minimizes version incompatibilites. Below you find some introductory -information on particular hooks. It's sensible to look at existing -plugins so see example usages and start off with your own plugin. +reporting process. When py.test loads a plugin it validates that all hook functions +conform to the `hook definition specification`_. The hook function name and its +argument names need to match exactly but it is allowed for an implementation +to accept *less* parameters. You'll get useful errors on mistyped hook or +argument names. Read on for some introductory information on particular +hooks. It's sensible to look at existing plugins so see example usages +and start off with your own plugin. .. _`hook definition specification`: plugin/hookspec.html diff --git a/py/_com.py b/py/_com.py index 9085916c8..43976ad43 100644 --- a/py/_com.py +++ b/py/_com.py @@ -9,7 +9,8 @@ class MultiCall: def __init__(self, methods, kwargs, firstresult=False): self.methods = methods[:] - self.kwargs = kwargs + self.kwargs = kwargs.copy() + self.kwargs['__multicall__'] = self self.results = [] self.firstresult = firstresult @@ -20,28 +21,30 @@ class MultiCall: def execute(self): while self.methods: method = self.methods.pop() - res = self._call1(method) + kwargs = self.getkwargs(method) + res = method(**kwargs) if res is not None: self.results.append(res) if self.firstresult: - break + return res if not self.firstresult: return self.results - if self.results: - return self.results[-1] - def _call1(self, method): - kwargs = self.kwargs - if '__call__' in varnames(method): - kwargs = kwargs.copy() - kwargs['__call__'] = self - return method(**kwargs) + def getkwargs(self, method): + kwargs = {} + for argname in varnames(method): + try: + kwargs[argname] = self.kwargs[argname] + except KeyError: + pass # might be optional param + return kwargs def varnames(rawcode): + ismethod = hasattr(rawcode, 'im_self') rawcode = getattr(rawcode, 'im_func', rawcode) rawcode = getattr(rawcode, 'func_code', rawcode) try: - return rawcode.co_varnames + return rawcode.co_varnames[ismethod:] except AttributeError: return () @@ -101,9 +104,6 @@ class HookRelay: def _performcall(self, name, multicall): return multicall.execute() - def __repr__(self): - return "" %(self._hookspecs, self._registry) - class HookCaller: def __init__(self, hookrelay, name, firstresult, extralookup=()): self.hookrelay = hookrelay @@ -112,12 +112,10 @@ class HookCaller: self.extralookup = extralookup and [extralookup] or () def __repr__(self): - return "" %( - self.name, self.firstresult, self.hookrelay) + return "" %(self.name,) def __call__(self, **kwargs): - methods = self.hookrelay._getmethods(self.name, - extralookup=self.extralookup) + methods = self.hookrelay._getmethods(self.name, self.extralookup) mc = MultiCall(methods, kwargs, firstresult=self.firstresult) return self.hookrelay._performcall(self.name, mc) diff --git a/py/misc/testing/test_com.py b/py/misc/testing/test_com.py index 9bccdc9c0..e4d9ec2a7 100644 --- a/py/misc/testing/test_com.py +++ b/py/misc/testing/test_com.py @@ -1,8 +1,18 @@ import py import os -from py._com import Registry, MultiCall, HookRelay +from py.__._com import Registry, MultiCall, HookRelay, varnames +def test_varnames(): + def f(x): + pass + class A: + def f(self, y): + pass + assert varnames(f) == ("x",) + assert varnames(A.f) == ('y',) + assert varnames(A().f) == ('y',) + class TestMultiCall: def test_uses_copy_of_methods(self): l = [lambda: 42] @@ -14,15 +24,15 @@ class TestMultiCall: def test_call_passing(self): class P1: - def m(self, __call__, x): - assert len(__call__.results) == 1 - assert not __call__.methods + def m(self, __multicall__, x): + assert len(__multicall__.results) == 1 + assert not __multicall__.methods return 17 class P2: - def m(self, __call__, x): - assert __call__.results == [] - assert __call__.methods + def m(self, __multicall__, x): + assert __multicall__.results == [] + assert __multicall__.methods return 23 p1 = P1() @@ -37,24 +47,23 @@ class TestMultiCall: def test_keyword_args(self): def f(x): return x + 1 - multicall = MultiCall([f], dict(x=23)) + class A: + def f(self, x, y): + return x + y + multicall = MultiCall([f, A().f], dict(x=23, y=24)) assert "'x': 23" in repr(multicall) + assert "'y': 24" in repr(multicall) reslist = multicall.execute() - assert reslist == [24] - assert "1 results" in repr(multicall) + assert reslist == [24+23, 24] + assert "2 results" in repr(multicall) + + def test_keywords_call_error(self): + multicall = MultiCall([lambda x: x], {}) + py.test.raises(TypeError, "multicall.execute()") - def test_optionalcallarg(self): - class P1: - def m(self, x): - return x - call = MultiCall([P1().m], dict(x=23)) - assert "23" in repr(call) - assert call.execute() == [23] - call = MultiCall([P1().m], dict(x=23), firstresult=True) - def test_call_subexecute(self): - def m(__call__): - subresult = __call__.execute() + def m(__multicall__): + subresult = __multicall__.execute() return subresult + 1 def n(): diff --git a/py/test/dist/dsession.py b/py/test/dist/dsession.py index 4c000f161..f68ccd4b2 100644 --- a/py/test/dist/dsession.py +++ b/py/test/dist/dsession.py @@ -77,8 +77,8 @@ class DSession(Session): self.item2nodes = {} super(DSession, self).__init__(config=config) - #def pytest_configure(self, __call__, config): - # __call__.execute() + #def pytest_configure(self, __multicall__, config): + # __multicall__.execute() # try: # config.getxspecs() # except config.Error: diff --git a/py/test/plugin/pytest_capture.py b/py/test/plugin/pytest_capture.py index 228b4617b..a391ba828 100644 --- a/py/test/plugin/pytest_capture.py +++ b/py/test/plugin/pytest_capture.py @@ -181,11 +181,11 @@ class CaptureManager: capfuncarg._finalize() del self._capturing_funcargs - def pytest_make_collect_report(self, __call__, collector): + def pytest_make_collect_report(self, __multicall__, collector): method = self._getmethod(collector.config, collector.fspath) self.resumecapture(method) try: - rep = __call__.execute() + rep = __multicall__.execute() finally: outerr = self.suspendcapture() addouterr(rep, outerr) @@ -204,11 +204,11 @@ class CaptureManager: def pytest_runtest_teardown(self, item): self.resumecapture_item(item) - def pytest__teardown_final(self, __call__, session): + def pytest__teardown_final(self, __multicall__, session): method = self._getmethod(session.config, None) self.resumecapture(method) try: - rep = __call__.execute() + rep = __multicall__.execute() finally: outerr = self.suspendcapture() if rep: @@ -219,9 +219,9 @@ class CaptureManager: if hasattr(self, '_capturing'): self.suspendcapture() - def pytest_runtest_makereport(self, __call__, item, call): + def pytest_runtest_makereport(self, __multicall__, item, call): self.deactivate_funcargs() - rep = __call__.execute() + rep = __multicall__.execute() outerr = self.suspendcapture() outerr = (item.outerr[0] + outerr[0], item.outerr[1] + outerr[1]) if not rep.passed: diff --git a/py/test/plugin/pytest_default.py b/py/test/plugin/pytest_default.py index cc6ff7e0a..684d1b020 100644 --- a/py/test/plugin/pytest_default.py +++ b/py/test/plugin/pytest_default.py @@ -2,8 +2,8 @@ import py -def pytest_pyfunc_call(__call__, pyfuncitem): - if not __call__.execute(): +def pytest_pyfunc_call(__multicall__, pyfuncitem): + if not __multicall__.execute(): testfunction = pyfuncitem.obj if pyfuncitem._isyieldedfunction(): testfunction(*pyfuncitem._args) diff --git a/py/test/plugin/pytest_execnetcleanup.py b/py/test/plugin/pytest_execnetcleanup.py index 52c3fdd89..2c5941430 100644 --- a/py/test/plugin/pytest_execnetcleanup.py +++ b/py/test/plugin/pytest_execnetcleanup.py @@ -32,10 +32,10 @@ class Execnetcleanup: #for gw in l: # gw.join() - def pytest_pyfunc_call(self, __call__, pyfuncitem): + def pytest_pyfunc_call(self, __multicall__, pyfuncitem): if self._gateways is not None: gateways = self._gateways[:] - res = __call__.execute() + res = __multicall__.execute() while len(self._gateways) > len(gateways): self._gateways[-1].exit() return res diff --git a/py/test/plugin/pytest_nose.py b/py/test/plugin/pytest_nose.py index c77b01f08..e78f7598d 100644 --- a/py/test/plugin/pytest_nose.py +++ b/py/test/plugin/pytest_nose.py @@ -47,7 +47,7 @@ import py import inspect import sys -def pytest_runtest_makereport(__call__, item, call): +def pytest_runtest_makereport(__multicall__, item, call): SkipTest = getattr(sys.modules.get('nose', None), 'SkipTest', None) if SkipTest: if call.excinfo and call.excinfo.errisinstance(SkipTest): diff --git a/py/test/plugin/pytest_pastebin.py b/py/test/plugin/pytest_pastebin.py index 3d2cbc2af..6ad8567fd 100644 --- a/py/test/plugin/pytest_pastebin.py +++ b/py/test/plugin/pytest_pastebin.py @@ -33,9 +33,9 @@ def pytest_addoption(parser): type="choice", choices=['failed', 'all'], help="send failed|all info to Pocoo pastebin service.") -def pytest_configure(__call__, config): +def pytest_configure(__multicall__, config): import tempfile - __call__.execute() + __multicall__.execute() if config.option.pastebin == "all": config._pastebinfile = tempfile.TemporaryFile() tr = config.pluginmanager.impname2plugin['terminalreporter'] diff --git a/py/test/plugin/pytest_terminal.py b/py/test/plugin/pytest_terminal.py index c2f83c124..80f7020ea 100644 --- a/py/test/plugin/pytest_terminal.py +++ b/py/test/plugin/pytest_terminal.py @@ -250,8 +250,8 @@ class TerminalReporter: for i, testarg in py.builtin.enumerate(self.config.args): self.write_line("test object %d: %s" %(i+1, testarg)) - def pytest_sessionfinish(self, __call__, session, exitstatus): - __call__.execute() + def pytest_sessionfinish(self, exitstatus, __multicall__): + __multicall__.execute() self._tw.line("") if exitstatus in (0, 1, 2): self.summary_errors() diff --git a/py/test/plugin/pytest_xfail.py b/py/test/plugin/pytest_xfail.py index 56aac847c..68361b570 100644 --- a/py/test/plugin/pytest_xfail.py +++ b/py/test/plugin/pytest_xfail.py @@ -19,12 +19,12 @@ when it fails. Instead terminal reporting will list it in the import py -def pytest_runtest_makereport(__call__, item, call): +def pytest_runtest_makereport(__multicall__, item, call): if call.when != "call": return if hasattr(item, 'obj') and hasattr(item.obj, 'func_dict'): if 'xfail' in item.obj.func_dict: - res = __call__.execute() + res = __multicall__.execute() if call.excinfo: res.skipped = True res.failed = res.passed = False diff --git a/py/test/pluginmanager.py b/py/test/pluginmanager.py index 621fd5a99..c8868c552 100644 --- a/py/test/pluginmanager.py +++ b/py/test/pluginmanager.py @@ -134,15 +134,16 @@ class PluginManager(object): fail = True else: method_args = getargs(method) - if '__call__' in method_args: - method_args.remove('__call__') + if '__multicall__' in method_args: + method_args.remove('__multicall__') hook = hooks[name] hookargs = getargs(hook) - for arg, hookarg in zip(method_args, hookargs): - if arg != hookarg: - Print("argument mismatch: %r != %r" %(arg, hookarg)) - Print("actual : %s" %(formatdef(method))) - Print("required:", formatdef(hook)) + for arg in method_args: + if arg not in hookargs: + Print("argument %r not available" %(arg, )) + Print("actual definition: %s" %(formatdef(method))) + Print("available hook arguments: %s" % + ", ".join(hookargs)) fail = True break #if not fail: From f0181c35a5386561f1a9fd23f35a79ede4c2e380 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Tue, 11 Aug 2009 19:02:32 +0200 Subject: [PATCH 5/8] adding a 5 LOC plugin for capturing and ignoring the output of test function calls --HG-- branch : 1.0.x --- contrib/pytest_ignoreout.py | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 contrib/pytest_ignoreout.py diff --git a/contrib/pytest_ignoreout.py b/contrib/pytest_ignoreout.py new file mode 100644 index 000000000..b065593aa --- /dev/null +++ b/contrib/pytest_ignoreout.py @@ -0,0 +1,8 @@ +import py + +def pytest_runtest_call(item, __multicall__): + cap = py.io.StdCapture() + try: + return __multicall__.execute() + finally: + outerr = cap.reset() From 7b906ca763e2bed278cc4cb830e2d4b7e43b9fa3 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Thu, 13 Aug 2009 20:10:12 +0200 Subject: [PATCH 6/8] [mq]: monkey --HG-- branch : 1.0.x --- CHANGELOG | 2 ++ py/test/plugin/pytest_monkeypatch.py | 30 +++++++++++++++++++++++---- py/test/testing/test_pluginmanager.py | 4 ++-- 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index ed1b2746e..fa06cb3a8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -16,6 +16,8 @@ Changes between 1.0.0 and 1.0.1 * "Test" prefixed classes are *not* collected by default anymore if they have an __init__ method +* monkeypatch setenv() now accepts a "prepend" parameter + * terser reporting of collection error tracebacks * simplified multicall mechanism and plugin architecture, diff --git a/py/test/plugin/pytest_monkeypatch.py b/py/test/plugin/pytest_monkeypatch.py index 56fb61b0e..80984b6f0 100644 --- a/py/test/plugin/pytest_monkeypatch.py +++ b/py/test/plugin/pytest_monkeypatch.py @@ -4,7 +4,7 @@ safely patch object attributes, dicts and environment variables. Usage ---------------- -Use the `monkeypatch funcarg`_ to safely patch the environment +Use the `monkeypatch funcarg`_ to safely patch environment variables, object attributes or dictionaries. For example, if you want to set the environment variable ``ENV1`` and patch the ``os.path.abspath`` function to return a particular value during a test @@ -20,7 +20,16 @@ function execution you can write it down like this: The function argument will do the modifications and memorize the old state. After the test function finished execution all modifications will be reverted. See the `monkeypatch blog post`_ -for an extensive discussion. +for an extensive discussion. + +To add to a possibly existing environment parameter you +can use this example: + +.. sourcecode:: python + + def test_mypath_finding(monkeypatch): + monkeypatch.setenv('PATH', 'x/y', prepend=":") + # x/y will be at the beginning of $PATH .. _`monkeypatch blog post`: http://tetamap.wordpress.com/2009/03/03/monkeypatching-in-unit-tests-done-right/ """ @@ -57,8 +66,11 @@ class MonkeyPatch: self._setitem.insert(0, (dictionary, name, dictionary.get(name, notset))) dictionary[name] = value - def setenv(self, name, value): - self.setitem(os.environ, name, str(value)) + def setenv(self, name, value, prepend=None): + value = str(value) + if prepend and name in os.environ: + value = value + prepend + os.environ[name] + self.setitem(os.environ, name, value) def finalize(self): for obj, name, value in self._setattr: @@ -111,6 +123,16 @@ def test_setenv(): monkeypatch.finalize() assert 'XYZ123' not in os.environ +def test_setenv_prepend(): + import os + monkeypatch = MonkeyPatch() + monkeypatch.setenv('XYZ123', 2, prepend="-") + assert os.environ['XYZ123'] == "2" + monkeypatch.setenv('XYZ123', 3, prepend="-") + assert os.environ['XYZ123'] == "3-2" + monkeypatch.finalize() + assert 'XYZ123' not in os.environ + def test_monkeypatch_plugin(testdir): reprec = testdir.inline_runsource(""" pytest_plugins = 'pytest_monkeypatch', diff --git a/py/test/testing/test_pluginmanager.py b/py/test/testing/test_pluginmanager.py index 3e96e8683..fa19fa4f0 100644 --- a/py/test/testing/test_pluginmanager.py +++ b/py/test/testing/test_pluginmanager.py @@ -4,7 +4,7 @@ from py.__.test.pluginmanager import PluginManager, canonical_importname, collec class TestBootstrapping: def test_consider_env_fails_to_import(self, monkeypatch): pluginmanager = PluginManager() - monkeypatch.setitem(os.environ, 'PYTEST_PLUGINS', 'nonexistingmodule') + monkeypatch.setenv('PYTEST_PLUGINS', 'nonexisting', prepend=",") py.test.raises(ImportError, "pluginmanager.consider_env()") def test_preparse_args(self): @@ -50,7 +50,7 @@ class TestBootstrapping: plugin = py.test.config.pluginmanager.getplugin('x500') assert plugin is not None """) - monkeypatch.setitem(os.environ, 'PYTEST_PLUGINS', 'pytest_x500') + monkeypatch.setenv('PYTEST_PLUGINS', 'pytest_x500', prepend=",") result = testdir.runpytest(p) assert result.ret == 0 extra = result.stdout.fnmatch_lines(["*1 passed in*"]) From d702f4da149d9ac9d00f509e214bb2390949fab5 Mon Sep 17 00:00:00 2001 From: Benjamin Peterson Date: Fri, 14 Aug 2009 09:16:40 -0500 Subject: [PATCH 7/8] add a --version option to print the pylib version --HG-- branch : 1.0.x --- CHANGELOG | 6 ++++-- py/test/plugin/pytest_default.py | 8 ++++++++ py/test/testing/acceptance_test.py | 8 ++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index fa06cb3a8..9c5547029 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -11,14 +11,16 @@ Changes between 1.0.0 and 1.0.1 was adapted and somewhat unified between windows and linux * fix issue #27: better reporting on non-collectable items given on commandline - (e.g. pyc files) + (e.g. pyc files) + +* fix issue #33: added --version flag (thanks Benjamin Peterson) * "Test" prefixed classes are *not* collected by default anymore if they have an __init__ method * monkeypatch setenv() now accepts a "prepend" parameter -* terser reporting of collection error tracebacks +* improved reporting of collection error tracebacks * simplified multicall mechanism and plugin architecture, renamed some internal methods and argnames diff --git a/py/test/plugin/pytest_default.py b/py/test/plugin/pytest_default.py index 684d1b020..047ea82ca 100644 --- a/py/test/plugin/pytest_default.py +++ b/py/test/plugin/pytest_default.py @@ -1,5 +1,6 @@ """ default hooks and general py.test options. """ +import sys import py def pytest_pyfunc_call(__multicall__, pyfuncitem): @@ -90,10 +91,17 @@ def pytest_addoption(parser): help="shortcut for '--dist=load --tx=NUM*popen'") group.addoption('--rsyncdir', action="append", default=[], metavar="dir1", help="add directory for rsyncing to remote tx nodes.") + group.addoption('--version', action="store_true", + help="display version information") def pytest_configure(config): fixoptions(config) setsession(config) + if config.option.version: + p = py.path.local(py.__file__).dirpath() + print "This is py.test version %s, imported from %s" % ( + py.__version__, p) + sys.exit(0) #xxxloadplugins(config) def fixoptions(config): diff --git a/py/test/testing/acceptance_test.py b/py/test/testing/acceptance_test.py index c96c65185..ae1c42f74 100644 --- a/py/test/testing/acceptance_test.py +++ b/py/test/testing/acceptance_test.py @@ -3,6 +3,14 @@ import py EXPECTTIMEOUT=10.0 class TestGeneralUsage: + def test_version(self, testdir): + assert py.version == py.__version__ + result = testdir.runpytest("--version") + assert result.ret == 0 + p = py.path.local(py.__file__).dirpath() + assert result.stderr.fnmatch_lines([ + '*py.test*%s*, imported from: %s*' % (py.version, p) + ]) def test_config_error(self, testdir): testdir.makeconftest(""" def pytest_configure(config): From d2f497084e4e2071bb3bf8bd7bd65de65fa1e121 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Fri, 14 Aug 2009 18:01:16 +0200 Subject: [PATCH 8/8] fixing svn status on incomplete files issue #32 --HG-- branch : 1.0.x --- CHANGELOG | 2 ++ py/path/svn/testing/test_wccommand.py | 6 ++++++ py/path/svn/wccommand.py | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 9c5547029..46506e19c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -15,6 +15,8 @@ Changes between 1.0.0 and 1.0.1 * fix issue #33: added --version flag (thanks Benjamin Peterson) +* fix issue #32: adding support for "incomplete" paths to wcpath.status() + * "Test" prefixed classes are *not* collected by default anymore if they have an __init__ method diff --git a/py/path/svn/testing/test_wccommand.py b/py/path/svn/testing/test_wccommand.py index 8067f80f5..5fe58a656 100644 --- a/py/path/svn/testing/test_wccommand.py +++ b/py/path/svn/testing/test_wccommand.py @@ -225,6 +225,12 @@ class TestWCSvnCommandPath(CommonSvnTests): ''' XMLWCStatus.fromstring(xml, self.root) + def test_status_wrong_xml(self): + # testing for XML without author - this used to raise an exception + xml = u'\n\n\n' + st = XMLWCStatus.fromstring(xml, self.root) + assert len(st.incomplete) == 1 + def test_diff(self): p = self.root / 'anotherfile' out = p.diff(rev=2) diff --git a/py/path/svn/wccommand.py b/py/path/svn/wccommand.py index ae6245160..51394b69b 100644 --- a/py/path/svn/wccommand.py +++ b/py/path/svn/wccommand.py @@ -671,6 +671,10 @@ class XMLWCStatus(WCStatus): wcpath = rootwcpath.join(path, abs=1) rootstatus.ignored.append(wcpath) continue + elif itemstatus == 'incomplete': + wcpath = rootwcpath.join(path, abs=1) + rootstatus.incomplete.append(wcpath) + continue rev = statusel.getAttribute('revision') if itemstatus == 'added' or itemstatus == 'none':