From 95de17b6525b5dcae237889e64c54c9942177331 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Sun, 17 Jan 2010 23:23:02 +0100 Subject: [PATCH] refine tests and refine code to deal with new xdist semantics. --HG-- branch : trunk --- py/_plugin/pytest_pytester.py | 2 +- py/_plugin/pytest_restdoc.py | 11 +-- py/_test/collect.py | 51 +++++----- py/_test/config.py | 30 +++--- py/_test/session.py | 2 + testing/plugin/test_pytest_terminal.py | 53 +++++----- testing/plugin/test_pytest_unittest.py | 2 +- testing/test_config.py | 129 +++++++++++++++++++++++++ testing/test_session.py | 5 - 9 files changed, 206 insertions(+), 79 deletions(-) diff --git a/py/_plugin/pytest_pytester.py b/py/_plugin/pytest_pytester.py index 51ced5544..1644a83d5 100644 --- a/py/_plugin/pytest_pytester.py +++ b/py/_plugin/pytest_pytester.py @@ -219,7 +219,7 @@ class TmpTestdir: if not args: args = [self.tmpdir] from py._test import config - oldconfig = py.test.config + oldconfig = config.config_per_process # py.test.config try: c = config.config_per_process = py.test.config = pytestConfig() c.basetemp = oldconfig.mktemp("reparse", numbered=True) diff --git a/py/_plugin/pytest_restdoc.py b/py/_plugin/pytest_restdoc.py index f9388bd5e..6f815550d 100644 --- a/py/_plugin/pytest_restdoc.py +++ b/py/_plugin/pytest_restdoc.py @@ -30,16 +30,13 @@ def getproject(path): return Project(parent) class ReSTFile(py.test.collect.File): - def __init__(self, fspath, parent, project=None): + def __init__(self, fspath, parent, project): 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), + ReSTSyntaxTest("ReSTSyntax", parent=self, project=self.project), LinkCheckerMaker("checklinks", parent=self), DoctestText("doctest", parent=self), ] @@ -63,8 +60,8 @@ def deindent(s, sep='\n'): return sep.join(lines) class ReSTSyntaxTest(py.test.collect.Item): - def __init__(self, project, *args, **kwargs): - super(ReSTSyntaxTest, self).__init__(*args, **kwargs) + def __init__(self, name, parent, project): + super(ReSTSyntaxTest, self).__init__(name=name, parent=parent) self.project = project def reportinfo(self): diff --git a/py/_test/collect.py b/py/_test/collect.py index a4f76b44d..27b18ea4b 100644 --- a/py/_test/collect.py +++ b/py/_test/collect.py @@ -1,6 +1,5 @@ """ -base test collection objects. Collectors and test Items form a tree -that is usually built iteratively. +test collection nodes, forming a tree, Items are leafs. """ import py @@ -33,9 +32,9 @@ class Node(object): self.fspath = getattr(parent, 'fspath', None) self.ihook = HookProxy(self) - def _checkcollectable(self): - if not hasattr(self, 'fspath'): - self.parent._memocollect() # to reraise exception + def _reraiseunpicklingproblem(self): + if hasattr(self, '_unpickle_exc'): + py.builtin._reraise(*self._unpickle_exc) # # note to myself: Pickling is uh. @@ -46,23 +45,25 @@ class Node(object): name, parent = nameparent try: colitems = parent._memocollect() - except KeyboardInterrupt: - raise - except Exception: - # seems our parent can't collect us - # so let's be somewhat operable - # _checkcollectable() is to tell outsiders about the fact - self.name = name - self.parent = parent - self.config = parent.config - #self._obj = "could not unpickle" - else: for colitem in colitems: if colitem.name == name: # we are a copy that will not be returned # by our parent self.__dict__ = colitem.__dict__ break + else: + raise ValueError("item %r not found in parent collection %r" %( + name, [x.name for x in colitems])) + except KeyboardInterrupt: + raise + except Exception: + # our parent can't collect us but we want unpickling to + # otherwise continue - self._reraiseunpicklingproblem() will + # reraise the problem + self._unpickle_exc = py.std.sys.exc_info() + self.name = name + self.parent = parent + self.config = parent.config def __repr__(self): if getattr(self.config.option, 'debug', False): @@ -268,15 +269,12 @@ class FSCollector(Collector): self.fspath = fspath def __getstate__(self): - if isinstance(self.parent, RootCollector): - relpath = self.parent._getrelpath(self.fspath) - return (relpath, self.parent) - else: - return (self.name, self.parent) - - def __setstate__(self, picklestate): - name, parent = picklestate - self.__init__(parent.fspath.join(name), parent=parent) + # RootCollector.getbynames() inserts a directory which we need + # to throw out here for proper re-instantiation + if isinstance(self.parent.parent, RootCollector): + assert self.parent.fspath == self.parent.parent.fspath, self.parent + return (self.name, self.parent.parent) # shortcut + return super(Collector, self).__getstate__() class File(FSCollector): """ base class for collecting tests from a file. """ @@ -382,6 +380,9 @@ class RootCollector(Directory): def __init__(self, config): Directory.__init__(self, config.topdir, parent=None, config=config) self.name = None + + def __repr__(self): + return "" %(self.fspath,) def getbynames(self, names): current = self.consider(self.config.topdir) diff --git a/py/_test/config.py b/py/_test/config.py index 831d17b83..46945a207 100644 --- a/py/_test/config.py +++ b/py/_test/config.py @@ -15,9 +15,9 @@ def ensuretemp(string, dir=1): return py.test.config.ensuretemp(string, dir=dir) class CmdOptions(object): - """ pure container instance for holding cmdline options - as attributes. - """ + """ holds cmdline options as attributes.""" + def __init__(self, **kwargs): + self.__dict__.update(kwargs) def __repr__(self): return "" %(self.__dict__,) @@ -31,8 +31,8 @@ class Config(object): basetemp = None _sessionclass = None - def __init__(self, topdir=None): - self.option = CmdOptions() + def __init__(self, topdir=None, option=None): + self.option = option or CmdOptions() self.topdir = topdir self._parser = parseopt.Parser( usage="usage: %prog [options] [file_or_dir] [file_or_dir] [...]", @@ -47,9 +47,9 @@ class Config(object): self.pluginmanager.consider_conftest(conftestmodule) def _getmatchingplugins(self, fspath): - conftests = self._conftest._conftestpath2mod.values() + allconftests = self._conftest._conftestpath2mod.values() plugins = [x for x in self.pluginmanager.getplugins() - if x not in conftests] + if x not in allconftests] plugins += self._conftest.getconftestmodules(fspath) return plugins @@ -114,20 +114,20 @@ class Config(object): for path in self.args: path = py.path.local(path) l.append(path.relto(self.topdir)) - return l, vars(self.option) + return l, self.option.__dict__ def __setstate__(self, repr): # we have to set py.test.config because loading # of conftest files may use it (deprecated) # mainly by py.test.config.addoptions() - py.test.config = self - # next line will registers default plugins - self.__init__(topdir=py.path.local()) - self._rootcol = RootCollector(config=self) + global config_per_process + py.test.config = config_per_process = self args, cmdlineopts = repr + cmdlineopts = CmdOptions(**cmdlineopts) + # next line will registers default plugins + self.__init__(topdir=py.path.local(), option=cmdlineopts) + self._rootcol = RootCollector(config=self) args = [str(self.topdir.join(x)) for x in args] - self.option = CmdOptions() - self.option.__dict__.update(cmdlineopts) self._preparse(args) self._setargs(args) @@ -177,7 +177,7 @@ class Config(object): def _getcollectclass(self, name, path): try: - cls = self.getvalue(name, path) + cls = self._conftest.rget(name, path) except KeyError: return getattr(py.test.collect, name) else: diff --git a/py/_test/session.py b/py/_test/session.py index 6eb1c9cbb..f155ffea2 100644 --- a/py/_test/session.py +++ b/py/_test/session.py @@ -24,6 +24,8 @@ class Session(object): def genitems(self, colitems, keywordexpr=None): """ yield Items from iterating over the given colitems. """ + if colitems: + colitems = list(colitems) while colitems: next = colitems.pop(0) if isinstance(next, (tuple, list)): diff --git a/testing/plugin/test_pytest_terminal.py b/testing/plugin/test_pytest_terminal.py index e60679292..ff86d685e 100644 --- a/testing/plugin/test_pytest_terminal.py +++ b/testing/plugin/test_pytest_terminal.py @@ -33,21 +33,16 @@ class Option: def pytest_generate_tests(metafunc): if "option" in metafunc.funcargnames: - metafunc.addcall( - id="default", - funcargs={'option': Option(verbose=False)} - ) - metafunc.addcall( - id="verbose", - funcargs={'option': Option(verbose=True)} - ) - if metafunc.config.pluginmanager.hasplugin("xdist"): - nodist = getattr(metafunc.function, 'nodist', False) - if not nodist: - metafunc.addcall( - id="verbose-dist", - funcargs={'option': Option(dist='each', verbose=True)} - ) + metafunc.addcall(id="default", param=Option(verbose=False)) + metafunc.addcall(id="verbose", param=Option(verbose=True)) + if not getattr(metafunc.function, 'nodist', False): + metafunc.addcall(id="verbose-dist", + param=Option(dist='each', verbose=True)) + +def pytest_funcarg__option(request): + if request.param.dist: + request.config.pluginmanager.skipifmissing("xdist") + return request.param class TestTerminal: def test_pass_skip_fail(self, testdir, option): @@ -255,12 +250,19 @@ class TestTerminal: ]) def test_keyboard_interrupt_dist(self, testdir, option): + # xxx could be refined to check for return code p = testdir.makepyfile(""" - raise KeyboardInterrupt + def test_sleep(): + import time + time.sleep(10) """) - result = testdir.runpytest(*option._getcmdargs()) - assert result.ret == 2 - result.stdout.fnmatch_lines(['*KEYBOARD INTERRUPT*']) + child = testdir.spawn_pytest(" ".join(option._getcmdargs())) + child.expect(".*test session starts.*") + child.kill(2) # keyboard interrupt + child.expect(".*KeyboardInterrupt.*") + #child.expect(".*seconds.*") + child.close() + #assert ret == 2 @py.test.mark.nodist def test_keyboard_interrupt(self, testdir, option): @@ -593,9 +595,10 @@ def test_terminalreporter_reportopt_conftestsetting(testdir): assert result.stdout.fnmatch_lines([ "*1 passed*" ]) - def test_trace_reporting(self, testdir): - result = testdir.runpytest("--trace") - assert result.stdout.fnmatch_lines([ - "*active plugins*" - ]) - assert result.ret == 0 + +def test_trace_reporting(testdir): + result = testdir.runpytest("--traceconfig") + assert result.stdout.fnmatch_lines([ + "*active plugins*" + ]) + assert result.ret == 0 diff --git a/testing/plugin/test_pytest_unittest.py b/testing/plugin/test_pytest_unittest.py index a43cb0326..e99221efd 100644 --- a/testing/plugin/test_pytest_unittest.py +++ b/testing/plugin/test_pytest_unittest.py @@ -51,7 +51,7 @@ def test_new_instances(testdir): reprec.assertoutcome(passed=2) def test_teardown(testdir): - testpath = testdir.makepyfile(test_three=""" + testpath = testdir.makepyfile(""" import unittest pytest_plugins = "pytest_unittest" # XXX class MyTestCase(unittest.TestCase): diff --git a/testing/test_config.py b/testing/test_config.py index efa6c8bed..13a8ed7b0 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -246,3 +246,132 @@ def test_preparse_ordering(testdir, monkeypatch): plugin = config.pluginmanager.getplugin("mytestplugin") assert plugin.x == 42 + +import pickle +class TestConfigPickling: + def pytest_funcarg__testdir(self, request): + oldconfig = py.test.config + print("setting py.test.config to None") + py.test.config = None + def resetglobals(): + py.builtin.print_("setting py.test.config to", oldconfig) + py.test.config = oldconfig + request.addfinalizer(resetglobals) + return request.getfuncargvalue("testdir") + + def test_config_getstate_setstate(self, testdir): + from py._test.config import Config + testdir.makepyfile(__init__="", conftest="x=1; y=2") + hello = testdir.makepyfile(hello="") + tmp = testdir.tmpdir + testdir.chdir() + config1 = testdir.parseconfig(hello) + config2 = Config() + config2.__setstate__(config1.__getstate__()) + assert config2.topdir == py.path.local() + config2_relpaths = [py.path.local(x).relto(config2.topdir) + for x in config2.args] + config1_relpaths = [py.path.local(x).relto(config1.topdir) + for x in config1.args] + + assert config2_relpaths == config1_relpaths + for name, value in config1.option.__dict__.items(): + assert getattr(config2.option, name) == value + assert config2.getvalue("x") == 1 + + def test_config_pickling_customoption(self, testdir): + testdir.makeconftest(""" + def pytest_addoption(parser): + group = parser.getgroup("testing group") + group.addoption('-G', '--glong', action="store", default=42, + type="int", dest="gdest", help="g value.") + """) + config = testdir.parseconfig("-G", "11") + assert config.option.gdest == 11 + repr = config.__getstate__() + + config = testdir.Config() + py.test.raises(AttributeError, "config.option.gdest") + + config2 = testdir.Config() + config2.__setstate__(repr) + assert config2.option.gdest == 11 + + def test_config_pickling_and_conftest_deprecated(self, testdir): + tmp = testdir.tmpdir.ensure("w1", "w2", dir=1) + tmp.ensure("__init__.py") + tmp.join("conftest.py").write(py.code.Source(""" + def pytest_addoption(parser): + group = parser.getgroup("testing group") + group.addoption('-G', '--glong', action="store", default=42, + type="int", dest="gdest", help="g value.") + """)) + config = testdir.parseconfig(tmp, "-G", "11") + assert config.option.gdest == 11 + repr = config.__getstate__() + + config = testdir.Config() + py.test.raises(AttributeError, "config.option.gdest") + + config2 = testdir.Config() + config2.__setstate__(repr) + assert config2.option.gdest == 11 + + option = config2.addoptions("testing group", + config2.Option('-G', '--glong', action="store", default=42, + type="int", dest="gdest", help="g value.")) + assert option.gdest == 11 + + def test_config_picklability(self, testdir): + config = testdir.parseconfig() + s = pickle.dumps(config) + newconfig = pickle.loads(s) + assert hasattr(newconfig, "topdir") + assert newconfig.topdir == py.path.local() + + def test_collector_implicit_config_pickling(self, testdir): + tmpdir = testdir.tmpdir + testdir.chdir() + testdir.makepyfile(hello="def test_x(): pass") + config = testdir.parseconfig(tmpdir) + col = config.getnode(config.topdir) + io = py.io.BytesIO() + pickler = pickle.Pickler(io) + pickler.dump(col) + io.seek(0) + unpickler = pickle.Unpickler(io) + col2 = unpickler.load() + assert col2.name == col.name + assert col2.listnames() == col.listnames() + + def test_config_and_collector_pickling(self, testdir): + tmpdir = testdir.tmpdir + dir1 = tmpdir.ensure("sourcedir", "somedir", dir=1) + config = testdir.parseconfig() + assert config.topdir == tmpdir + col = config.getnode(dir1.dirpath()) + col1 = config.getnode(dir1) + assert col1.parent == col + io = py.io.BytesIO() + pickler = pickle.Pickler(io) + pickler.dump(col) + pickler.dump(col1) + pickler.dump(col) + io.seek(0) + unpickler = pickle.Unpickler(io) + newtopdir = tmpdir.ensure("newtopdir", dir=1) + newtopdir.mkdir("sourcedir").mkdir("somedir") + old = newtopdir.chdir() + try: + newcol = unpickler.load() + newcol2 = unpickler.load() + newcol3 = unpickler.load() + assert newcol2.config is newcol.config + assert newcol2.parent == newcol + assert newcol2.config.topdir.realpath() == newtopdir.realpath() + newsourcedir = newtopdir.join("sourcedir") + assert newcol.fspath.realpath() == newsourcedir.realpath() + assert newcol2.fspath.basename == dir1.basename + assert newcol2.fspath.relto(newcol2.config.topdir) + finally: + old.chdir() diff --git a/testing/test_session.py b/testing/test_session.py index 330784193..b6e0936e2 100644 --- a/testing/test_session.py +++ b/testing/test_session.py @@ -193,8 +193,3 @@ class TestNewSession(SessionTests): colfail = [x for x in finished if x.failed] assert len(colfail) == 1 -class TestNewSessionDSession(SessionTests): - def parseconfig(self, *args): - args = ('-n1',) + args - return SessionTests.parseconfig(self, *args) -