diff --git a/CHANGELOG b/CHANGELOG index 71870b484..115b02a72 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -6,10 +6,10 @@ Changes between 1.3.4 and 2.0.0dev0 - new python invcation: pytest.main(args, plugins) to load some custom plugins early. - try harder to run unittest test suites in a more compatible manner - by deferring setup/teardown semantics to the unittest package. + by deferring setup/teardown semantics to the unittest package. - introduce a new way to set config options via ini-style files, by default setup.cfg and tox.ini files are searched. The old - ways (certain environment variables, dynamic conftest.py reading + ways (certain environment variables, dynamic conftest.py reading is removed). - add a new "-q" option which decreases verbosity and prints a more nose/unittest-style "dot" output. @@ -26,7 +26,8 @@ Changes between 1.3.4 and 2.0.0dev0 output on assertion failures for comparisons and other cases (Floris Bruynooghe) - nose-plugin: pass through type-signature failures in setup/teardown functions instead of not calling them (Ed Singleton) -- major refactoring of internal collection handling +- remove py.test.collect.Directory (follows from a major refactoring + and simplification of the collection process) - majorly reduce py.test core code, shift function/python testing to own plugin - fix issue88 (finding custom test nodes from command line arg) - refine 'tmpdir' creation, will now create basenames better associated diff --git a/pytest/__init__.py b/pytest/__init__.py index bef0c9c24..2ac1a43b8 100644 --- a/pytest/__init__.py +++ b/pytest/__init__.py @@ -5,10 +5,10 @@ see http://pytest.org for documentation and details (c) Holger Krekel and others, 2004-2010 """ -__version__ = '2.0.0.dev18' +__version__ = '2.0.0.dev19' __all__ = ['config', 'cmdline'] from pytest import main as cmdline UsageError = cmdline.UsageError -main = cmdline.main +main = cmdline.main \ No newline at end of file diff --git a/pytest/hookspec.py b/pytest/hookspec.py index a5213a937..cd778a522 100644 --- a/pytest/hookspec.py +++ b/pytest/hookspec.py @@ -126,7 +126,7 @@ def pytest_runtest_protocol(item): """ pytest_runtest_protocol.firstresult = True -def pytest_runtest_logstart(nodeid, location, fspath): +def pytest_runtest_logstart(nodeid, location): """ signal the start of a test run. """ def pytest_runtest_setup(item): diff --git a/pytest/main.py b/pytest/main.py index 9f4825cd3..26de3f59d 100644 --- a/pytest/main.py +++ b/pytest/main.py @@ -67,7 +67,10 @@ class PluginManager(object): self._hints = [] self.trace = TagTracer().get("pluginmanage") if os.environ.get('PYTEST_DEBUG'): - self.trace.root.setwriter(sys.stderr.write) + err = sys.stderr + if hasattr(os, 'dup'): + err = py.io.dupfile(err) + self.trace.root.setwriter(err.write) self.hook = HookRelay([hookspec], pm=self) self.register(self) if load: @@ -370,6 +373,7 @@ class HookCaller: self.hookrelay = hookrelay self.name = name self.firstresult = firstresult + self.trace = self.hookrelay.trace def __repr__(self): return "" %(self.name,) @@ -380,10 +384,15 @@ class HookCaller: return mc.execute() def pcall(self, plugins, **kwargs): - self.hookrelay.trace(self.name, kwargs) + self.trace(self.name, kwargs) + self.trace.root.indent += 1 methods = self.hookrelay._pm.listattr(self.name, plugins=plugins) mc = MultiCall(methods, kwargs, firstresult=self.firstresult) - return mc.execute() + res = mc.execute() + if res: + self.trace(res) + self.trace.root.indent -= 1 + return res _preinit = [PluginManager(load=True)] # triggers default plugin importing diff --git a/pytest/plugin/capture.py b/pytest/plugin/capture.py index ff29723b6..38a9d431a 100644 --- a/pytest/plugin/capture.py +++ b/pytest/plugin/capture.py @@ -133,7 +133,11 @@ class CaptureManager: def pytest_make_collect_report(self, __multicall__, collector): method = self._getmethod(collector.config, collector.fspath) - self.resumecapture(method) + try: + self.resumecapture(method) + except ValueError: + return # recursive collect, XXX refactor capturing + # to allow for more lightweight recursive capturing try: rep = __multicall__.execute() finally: diff --git a/pytest/plugin/config.py b/pytest/plugin/config.py index 77d5fc1f6..365c73e81 100644 --- a/pytest/plugin/config.py +++ b/pytest/plugin/config.py @@ -255,7 +255,7 @@ class Config(object): ) #: a pluginmanager instance self.pluginmanager = pluginmanager or PluginManager(load=True) - self.trace = self.pluginmanager.trace.get("config") + self.trace = self.pluginmanager.trace.root.get("config") self._conftest = Conftest(onimport=self._onimportconftest) self.hook = self.pluginmanager.hook diff --git a/pytest/plugin/doctest.py b/pytest/plugin/doctest.py index a58c5d41b..43033d6b9 100644 --- a/pytest/plugin/doctest.py +++ b/pytest/plugin/doctest.py @@ -17,7 +17,7 @@ def pytest_addoption(parser): def pytest_collect_file(path, parent): config = parent.config if path.ext == ".py": - if config.getvalue("doctestmodules"): + if config.option.doctestmodules: return DoctestModule(path, parent) elif path.check(fnmatch=config.getvalue("doctestglob")): return DoctestTextfile(path, parent) diff --git a/pytest/plugin/junitxml.py b/pytest/plugin/junitxml.py index bea439a25..d50056c1e 100644 --- a/pytest/plugin/junitxml.py +++ b/pytest/plugin/junitxml.py @@ -3,6 +3,7 @@ """ import py +import os import time def pytest_addoption(parser): @@ -36,7 +37,9 @@ class LogXML(object): self._durations = {} def _opentestcase(self, report): - names = report.nodenames + names = report.nodeid.split("::") + names[0] = names[0].replace(os.sep, '.') + names = tuple(names) d = {'time': self._durations.pop(names, "0")} names = [x.replace(".py", "") for x in names if x != "()"] classnames = names[:-1] diff --git a/pytest/plugin/pytester.py b/pytest/plugin/pytester.py index 8162aa5cf..7a67ea000 100644 --- a/pytest/plugin/pytester.py +++ b/pytest/plugin/pytester.py @@ -105,6 +105,7 @@ class HookRecorder: return l def contains(self, entries): + __tracebackhide__ = True from py.builtin import print_ i = 0 entries = list(entries) @@ -123,8 +124,7 @@ class HookRecorder: break print_("NONAMEMATCH", name, "with", call) else: - raise AssertionError("could not find %r in %r" %( - name, self.calls[i:])) + py.test.fail("could not find %r check %r" % (name, check)) def popcall(self, name): for i, call in enumerate(self.calls): @@ -278,7 +278,16 @@ class TmpTestdir: Collection = Collection def getnode(self, config, arg): collection = Collection(config) - return collection.getbyid(collection._normalizearg(arg))[0] + assert '::' not in str(arg) + p = py.path.local(arg) + x = collection.fspath.bestrelpath(p) + return collection.perform_collect([x], genitems=False)[0] + + def getpathnode(self, path): + config = self.parseconfig(path) + collection = Collection(config) + x = collection.fspath.bestrelpath(path) + return collection.perform_collect([x], genitems=False)[0] def genitems(self, colitems): collection = colitems[0].collection @@ -291,8 +300,9 @@ class TmpTestdir: #config = self.parseconfig(*args) config = self.parseconfigure(*args) rec = self.getreportrecorder(config) - items = Collection(config).perform_collect() - return items, rec + collection = Collection(config) + collection.perform_collect() + return collection.items, rec def runitem(self, source): # used from runner functional tests @@ -469,11 +479,12 @@ class TmpTestdir: p = py.path.local.make_numbered_dir(prefix="runpytest-", keep=None, rootdir=self.tmpdir) args = ('--basetemp=%s' % p, ) + args - for x in args: - if '--confcutdir' in str(x): - break - else: - args = ('--confcutdir=.',) + args + #for x in args: + # if '--confcutdir' in str(x): + # break + #else: + # pass + # args = ('--confcutdir=.',) + args plugins = [x for x in self.plugins if isinstance(x, str)] if plugins: args = ('-p', plugins[0]) + args @@ -530,7 +541,7 @@ class ReportRecorder(object): """ return a testreport whose dotted import path matches """ l = [] for rep in self.getreports(names=names): - if not inamepart or inamepart in rep.nodenames: + if not inamepart or inamepart in rep.nodeid.split("::"): l.append(rep) if not l: raise ValueError("could not find test report matching %r: no test reports at all!" % @@ -616,6 +627,8 @@ class LineMatcher: raise ValueError("line %r not found in output" % line) def fnmatch_lines(self, lines2): + def show(arg1, arg2): + py.builtin.print_(arg1, arg2, file=py.std.sys.stderr) lines2 = self._getlines(lines2) lines1 = self.lines[:] nextline = None @@ -626,17 +639,17 @@ class LineMatcher: while lines1: nextline = lines1.pop(0) if line == nextline: - print_("exact match:", repr(line)) + show("exact match:", repr(line)) break elif fnmatch(nextline, line): - print_("fnmatch:", repr(line)) - print_(" with:", repr(nextline)) + show("fnmatch:", repr(line)) + show(" with:", repr(nextline)) break else: if not nomatchprinted: - print_("nomatch:", repr(line)) + show("nomatch:", repr(line)) nomatchprinted = True - print_(" and:", repr(nextline)) + show(" and:", repr(nextline)) extralines.append(nextline) else: - assert line == nextline + py.test.fail("remains unmatched: %r, see stderr" % (line,)) diff --git a/pytest/plugin/python.py b/pytest/plugin/python.py index 7c510b29d..c0bc4c557 100644 --- a/pytest/plugin/python.py +++ b/pytest/plugin/python.py @@ -48,11 +48,10 @@ def pytest_pyfunc_call(__multicall__, pyfuncitem): def pytest_collect_file(path, parent): ext = path.ext pb = path.purebasename - if pb.startswith("test_") or pb.endswith("_test") or \ - path in parent.collection._argfspaths: - if ext == ".py": - return parent.ihook.pytest_pycollect_makemodule( - path=path, parent=parent) + if ext == ".py" and (pb.startswith("test_") or pb.endswith("_test") or + parent.collection.isinitpath(path)): + return parent.ihook.pytest_pycollect_makemodule( + path=path, parent=parent) def pytest_pycollect_makemodule(path, parent): return Module(path, parent) @@ -713,11 +712,13 @@ class FuncargRequest: def showfuncargs(config): from pytest.plugin.session import Collection collection = Collection(config) - firstid = collection._normalizearg(config.args[0]) - colitem = collection.getbyid(firstid)[0] + collection.perform_collect() + if collection.items: + plugins = getplugins(collection.items[0]) + else: + plugins = getplugins(collection) curdir = py.path.local() tw = py.io.TerminalWriter() - plugins = getplugins(colitem, withpy=True) verbose = config.getvalue("verbose") for plugin in plugins: available = [] diff --git a/pytest/plugin/runner.py b/pytest/plugin/runner.py index 45df6cd9d..dd118b20a 100644 --- a/pytest/plugin/runner.py +++ b/pytest/plugin/runner.py @@ -29,30 +29,12 @@ def pytest_sessionfinish(session, exitstatus): session.exitstatus = 1 class NodeInfo: - def __init__(self, nodeid, nodenames, fspath, location): - self.nodeid = nodeid - self.nodenames = nodenames - self.fspath = fspath + def __init__(self, location): self.location = location -def getitemnodeinfo(item): - try: - return item._nodeinfo - except AttributeError: - location = item.reportinfo() - location = (str(location[0]), location[1], str(location[2])) - nodenames = tuple(item.listnames()) - nodeid = item.collection.getid(item) - fspath = item.fspath - item._nodeinfo = n = NodeInfo(nodeid, nodenames, fspath, location) - return n - def pytest_runtest_protocol(item): - nodeinfo = getitemnodeinfo(item) item.ihook.pytest_runtest_logstart( - nodeid=nodeinfo.nodeid, - location=nodeinfo.location, - fspath=str(item.fspath), + nodeid=item.nodeid, location=item.location, ) runtestprotocol(item) return True @@ -142,16 +124,18 @@ class BaseReport(object): failed = property(lambda x: x.outcome == "failed") skipped = property(lambda x: x.outcome == "skipped") + @property + def fspath(self): + return self.nodeid.split("::")[0] def pytest_runtest_makereport(item, call): - nodeinfo = getitemnodeinfo(item) when = call.when keywords = dict([(x,1) for x in item.keywords]) - excinfo = call.excinfo if not call.excinfo: outcome = "passed" longrepr = None else: + excinfo = call.excinfo if not isinstance(excinfo, py.code.ExceptionInfo): outcome = "failed" longrepr = excinfo @@ -164,25 +148,18 @@ def pytest_runtest_makereport(item, call): longrepr = item.repr_failure(excinfo) else: # exception in setup or teardown longrepr = item._repr_failure_py(excinfo) - return TestReport(nodeinfo.nodeid, nodeinfo.nodenames, - nodeinfo.fspath, nodeinfo.location, + return TestReport(item.nodeid, item.location, keywords, outcome, longrepr, when) class TestReport(BaseReport): """ Basic test report object (also used for setup and teardown calls if they fail). """ - def __init__(self, nodeid, nodenames, fspath, location, + def __init__(self, nodeid, location, keywords, outcome, longrepr, when): #: normalized collection node id self.nodeid = nodeid - #: list of names indicating position in collection tree. - self.nodenames = nodenames - - #: the collected path of the file containing the test. - self.fspath = fspath # where the test was collected - #: a (filesystempath, lineno, domaininfo) tuple indicating the #: actual location of a test item - it might be different from the #: collected one e.g. if a method is inherited from a different module. @@ -212,39 +189,27 @@ class TeardownErrorReport(BaseReport): self.longrepr = longrepr def pytest_make_collect_report(collector): - result = excinfo = None - try: - result = collector._memocollect() - except KeyboardInterrupt: - raise - except: - excinfo = py.code.ExceptionInfo() - nodenames = tuple(collector.listnames()) - nodeid = collector.collection.getid(collector) - fspath = str(collector.fspath) + call = CallInfo(collector._memocollect, "memocollect") reason = longrepr = None - if not excinfo: + if not call.excinfo: outcome = "passed" else: - if excinfo.errisinstance(py.test.skip.Exception): + if call.excinfo.errisinstance(py.test.skip.Exception): outcome = "skipped" - reason = str(excinfo.value) - longrepr = collector._repr_failure_py(excinfo, "line") + reason = str(call.excinfo.value) + longrepr = collector._repr_failure_py(call.excinfo, "line") else: outcome = "failed" - errorinfo = collector.repr_failure(excinfo) + errorinfo = collector.repr_failure(call.excinfo) if not hasattr(errorinfo, "toterminal"): errorinfo = CollectErrorRepr(errorinfo) longrepr = errorinfo - return CollectReport(nodenames, nodeid, fspath, - outcome, longrepr, result, reason) + return CollectReport(collector.nodeid, outcome, longrepr, + getattr(call, 'result', None), reason) class CollectReport(BaseReport): - def __init__(self, nodenames, nodeid, fspath, outcome, - longrepr, result, reason): - self.nodenames = nodenames + def __init__(self, nodeid, outcome, longrepr, result, reason): self.nodeid = nodeid - self.fspath = fspath self.outcome = outcome self.longrepr = longrepr self.result = result or [] @@ -255,7 +220,8 @@ class CollectReport(BaseReport): return (self.fspath, None, self.fspath) def __repr__(self): - return "" % (self.nodeid, self.outcome) + return "" % ( + self.nodeid, len(self.result), self.outcome) class CollectErrorRepr(TerminalRepr): def __init__(self, msg): diff --git a/pytest/plugin/session.py b/pytest/plugin/session.py index a5c5bead6..ec3f94d57 100644 --- a/pytest/plugin/session.py +++ b/pytest/plugin/session.py @@ -14,11 +14,15 @@ EXIT_OK = 0 EXIT_TESTSFAILED = 1 EXIT_INTERRUPTED = 2 EXIT_INTERNALERROR = 3 -EXIT_NOHOSTS = 4 def pytest_addoption(parser): parser.addini("norecursedirs", "directory patterns to avoid for recursion", type="args", default=('.*', 'CVS', '_darcs', '{arch}')) + #parser.addini("dirpatterns", + # "patterns specifying possible locations of test files", + # type="linelist", default=["**/test_*.txt", + # "**/test_*.py", "**/*_test.py"] + #) group = parser.getgroup("general", "running and selection options") group._addoption('-x', '--exitfirst', action="store_true", default=False, dest="exitfirst", @@ -44,12 +48,11 @@ def pytest_addoption(parser): def pytest_namespace(): - return dict(collect=dict(Item=Item, Collector=Collector, - File=File, Directory=Directory)) + return dict(collect=dict(Item=Item, Collector=Collector, File=File)) def pytest_configure(config): py.test.config = config # compatibiltiy - if config.getvalue("exitfirst"): + if config.option.exitfirst: config.option.maxfail = 1 def pytest_cmdline_main(config): @@ -84,8 +87,10 @@ def pytest_cmdline_main(config): def pytest_collection(session): collection = session.collection assert not hasattr(collection, 'items') + + collection.perform_collect() hook = session.config.hook - collection.items = items = collection.perform_collect() + items = collection.items hook.pytest_collection_modifyitems(config=session.config, items=items) hook.pytest_collection_finish(collection=collection) return True @@ -108,18 +113,6 @@ def pytest_ignore_collect(path, config): ignore_paths.extend([py.path.local(x) for x in excludeopt]) return path in ignore_paths -def pytest_collect_directory(path, parent): - # check if cmdline specified this dir or a subdir directly - for arg in parent.collection._argfspaths: - if path == arg or arg.relto(path): - break - else: - patterns = parent.config.getini("norecursedirs") - for pat in patterns or []: - if path.check(fnmatch=pat): - return - return Directory(path, parent=parent) - class Session(object): class Interrupted(KeyboardInterrupt): """ signals an interrupted test run. """ @@ -145,160 +138,17 @@ class Session(object): self._testsfailed) pytest_collectreport = pytest_runtest_logreport -class Collection: - def __init__(self, config): - self.config = config - self.topdir = gettopdir(self.config.args) - self._argfspaths = [py.path.local(decodearg(x)[0]) - for x in self.config.args] - x = pytest.collect.Directory(fspath=self.topdir, - config=config, collection=self) - self._topcollector = x.consider_dir(self.topdir) - self._topcollector.parent = None - - def _normalizearg(self, arg): - return "::".join(self._parsearg(arg)) - - def _parsearg(self, arg, base=None): - """ return normalized name list for a command line specified id - which might be of the form x/y/z::name1::name2 - and should result into the form x::y::z::name1::name2 - """ - if base is None: - base = py.path.local() - parts = str(arg).split("::") - path = base.join(parts[0], abs=True) - if not path.check(): - raise pytest.UsageError("file not found: %s" %(path,)) - topdir = self.topdir - if path != topdir and not path.relto(topdir): - raise pytest.UsageError("path %r is not relative to %r" % - (str(path), str(topdir))) - topparts = path.relto(topdir).split(path.sep) - return topparts + parts[1:] - - def getid(self, node): - """ return id for node, relative to topdir. """ - path = node.fspath - chain = [x for x in node.listchain() if x.fspath == path] - chain = chain[1:] - names = [x.name for x in chain if x.name != "()"] - relpath = path.relto(self.topdir) - if not relpath: - assert path == self.topdir - path = '' - else: - path = relpath - if os.sep != "/": - path = str(path).replace(os.sep, "/") - names.insert(0, path) - return "::".join(names) - - def getbyid(self, id): - """ return one or more nodes matching the id. """ - names = [x for x in id.split("::") if x] - if names and '/' in names[0]: - names[:1] = names[0].split("/") - return list(self.matchnodes([self._topcollector], names)) - - def perform_collect(self): - items = [] - for arg in self.config.args: - names = self._parsearg(arg) - try: - for node in self.matchnodes([self._topcollector], names): - items.extend(self.genitems(node)) - except NoMatch: - raise pytest.UsageError("can't collect: %s" % (arg,)) - return items - - def matchnodes(self, matching, names): - if not matching: - return - if not names: - for x in matching: - yield x - return - name = names[0] - names = names[1:] - for node in matching: - if isinstance(node, pytest.collect.Item): - if not name: - yield node - continue - assert isinstance(node, pytest.collect.Collector) - node.ihook.pytest_collectstart(collector=node) - rep = node.ihook.pytest_make_collect_report(collector=node) - #print "matching", rep.result, "against name", name - if rep.passed: - if not name: - for x in rep.result: - yield x - else: - matched = False - for x in rep.result: - try: - if x.name == name or x.fspath.basename == name: - for x in self.matchnodes([x], names): - yield x - matched = True - elif x.name == "()": # XXX special Instance() case - for x in self.matchnodes([x], [name] + names): - yield x - matched = True - except NoMatch: - pass - if not matched: - node.ihook.pytest_collectreport(report=rep) - raise NoMatch(name) - node.ihook.pytest_collectreport(report=rep) - - def genitems(self, node): - if isinstance(node, pytest.collect.Item): - node.ihook.pytest_itemcollected(item=node) - yield node - else: - assert isinstance(node, pytest.collect.Collector) - node.ihook.pytest_collectstart(collector=node) - rep = node.ihook.pytest_make_collect_report(collector=node) - if rep.passed: - for subnode in rep.result: - for x in self.genitems(subnode): - yield x - node.ihook.pytest_collectreport(report=rep) - class NoMatch(Exception): """ raised if matching cannot locate a matching names. """ -def gettopdir(args): - """ return the top directory for the given paths. - if the common base dir resides in a python package - parent directory of the root package is returned. - """ - fsargs = [py.path.local(decodearg(arg)[0]) for arg in args] - p = fsargs and fsargs[0] or None - for x in fsargs[1:]: - p = p.common(x) - assert p, "cannot determine common basedir of %s" %(fsargs,) - pkgdir = p.pypkgpath() - if pkgdir is None: - if p.check(file=1): - p = p.dirpath() - return p - else: - return pkgdir.dirpath() - -def decodearg(arg): - arg = str(arg) - return arg.split("::") - class HookProxy: - def __init__(self, node): - self.node = node + def __init__(self, fspath, config): + self.fspath = fspath + self.config = config def __getattr__(self, name): - hookmethod = getattr(self.node.config.hook, name) + hookmethod = getattr(self.config.hook, name) def call_matching_hooks(**kwargs): - plugins = self.node.config._getmatchingplugins(self.node.fspath) + plugins = self.config._getmatchingplugins(self.fspath) return hookmethod.pcall(plugins, **kwargs) return call_matching_hooks @@ -329,7 +179,7 @@ class Node(object): #: the file where this item is contained/collected from. self.fspath = getattr(parent, 'fspath', None) - self.ihook = HookProxy(self) + self.ihook = self.collection.gethookproxy(self.fspath) self.keywords = {self.name: True} Module = compatproperty("Module") @@ -339,14 +189,19 @@ class Node(object): Item = compatproperty("Item") def __repr__(self): - if getattr(self.config.option, 'debug', False): - return "<%s %r %0x>" %(self.__class__.__name__, - getattr(self, 'name', None), id(self)) - else: - return "<%s %r>" %(self.__class__.__name__, - getattr(self, 'name', None)) + return "<%s %r>" %(self.__class__.__name__, getattr(self, 'name', None)) # methods for ordering nodes + @property + def nodeid(self): + try: + return self._nodeid + except AttributeError: + self._nodeid = x = self._makeid() + return x + + def _makeid(self): + return self.parent.nodeid + "::" + self.name def __eq__(self, other): if not isinstance(other, Node): @@ -447,7 +302,7 @@ class Collector(Node): def _memocollect(self): """ internal helper method to cache results of calling collect(). """ - return self._memoizedcall('_collected', self.collect) + return self._memoizedcall('_collected', lambda: list(self.collect())) def _prunetraceback(self, excinfo): if hasattr(self, 'fspath'): @@ -460,55 +315,177 @@ class Collector(Node): class FSCollector(Collector): def __init__(self, fspath, parent=None, config=None, collection=None): - fspath = py.path.local(fspath) - super(FSCollector, self).__init__(fspath.basename, - parent, config, collection) + fspath = py.path.local(fspath) # xxx only for test_resultlog.py? + name = parent and fspath.relto(parent.fspath) or fspath.basename + super(FSCollector, self).__init__(name, parent, config, collection) self.fspath = fspath + def _makeid(self): + if self == self.collection: + return "." + relpath = self.collection.fspath.bestrelpath(self.fspath) + if os.sep != "/": + relpath = str(path).replace(os.sep, "/") + return relpath + class File(FSCollector): """ base class for collecting tests from a file. """ -class Directory(FSCollector): - def collect(self): - l = [] - for path in self.fspath.listdir(sort=True): - res = self.consider(path) - if res is not None: - if isinstance(res, (list, tuple)): - l.extend(res) - else: - l.append(res) - return l - - def consider(self, path): - if self.ihook.pytest_ignore_collect(path=path, config=self.config): - return - if path.check(file=1): - res = self.consider_file(path) - elif path.check(dir=1): - res = self.consider_dir(path) - else: - res = None - if isinstance(res, list): - # throw out identical results - l = [] - for x in res: - if x not in l: - assert x.parent == self, (x.parent, self) - assert x.fspath == path, (x.fspath, path) - l.append(x) - res = l - return res - - def consider_file(self, path): - return self.ihook.pytest_collect_file(path=path, parent=self) - - def consider_dir(self, path): - return self.ihook.pytest_collect_directory(path=path, parent=self) - class Item(Node): """ a basic test invocation item. Note that for a single function there might be multiple test invocation items. """ def reportinfo(self): return self.fspath, None, "" + + @property + def location(self): + try: + return self._location + except AttributeError: + location = self.reportinfo() + location = (str(location[0]), location[1], str(location[2])) + self._location = location + return location + +class Collection(FSCollector): + def __init__(self, config): + super(Collection, self).__init__(py.path.local(), parent=None, + config=config, collection=self) + self.trace = config.trace.root.get("collection") + self._norecursepatterns = config.getini("norecursedirs") + + def isinitpath(self, path): + return path in self._initialpaths + + def gethookproxy(self, fspath): + return HookProxy(fspath, self.config) + + def perform_collect(self, args=None, genitems=True): + if args is None: + args = self.config.args + self.trace("perform_collect", self, args) + self.trace.root.indent += 1 + self._notfound = [] + self._initialpaths = set() + self._initialargs = args + for arg in args: + parts = self._parsearg(arg) + self._initialpaths.add(parts[0]) + self.ihook.pytest_collectstart(collector=self) + rep = self.ihook.pytest_make_collect_report(collector=self) + self.ihook.pytest_collectreport(report=rep) + self.trace.root.indent -= 1 + if self._notfound: + for arg, exc in self._notfound: + line = "no name %r in any of %r" % (exc.args[1], exc.args[0]) + raise pytest.UsageError("not found: %s\n%s" %(arg, line)) + if not genitems: + return rep.result + else: + self.items = items = [] + if rep.passed: + for node in rep.result: + self.items.extend(self.genitems(node)) + return items + + def collect(self): + for arg in self._initialargs: + self.trace("processing arg", arg) + self.trace.root.indent += 1 + try: + for x in self._collect(arg): + yield x + except NoMatch: + # we are inside a make_report hook so + # we cannot directly pass through the exception + self._notfound.append((arg, sys.exc_info()[1])) + self.trace.root.indent -= 1 + break + self.trace.root.indent -= 1 + + def _collect(self, arg): + names = self._parsearg(arg) + path = names.pop(0) + if path.check(dir=1): + assert not names, "invalid arg %r" %(arg,) + for path in path.visit(rec=self._recurse, bf=True, sort=True): + for x in self._collectfile(path): + yield x + else: + assert path.check(file=1) + for x in self.matchnodes(self._collectfile(path), names): + yield x + + def _collectfile(self, path): + ihook = self.gethookproxy(path) + if ihook.pytest_ignore_collect(path=path, config=self.config): + return () + return ihook.pytest_collect_file(path=path, parent=self) + + def _recurse(self, path): + ihook = self.gethookproxy(path) + if ihook.pytest_ignore_collect(path=path, config=self.config): + return + for pat in self._norecursepatterns: + if path.check(fnmatch=pat): + return False + ihook.pytest_collect_directory(path=path, parent=self) + return True + + def _parsearg(self, arg): + """ return (fspath, names) tuple after checking the file exists. """ + parts = str(arg).split("::") + path = self.fspath.join(parts[0], abs=True) + if not path.check(): + raise pytest.UsageError("file not found: %s" %(path,)) + parts[0] = path + return parts + + def matchnodes(self, matching, names): + self.trace("matchnodes", matching, names) + self.trace.root.indent += 1 + nodes = self._matchnodes(matching, names) + num = len(nodes) + self.trace("matchnodes finished -> ", num, "nodes") + self.trace.root.indent -= 1 + if num == 0: + raise NoMatch(matching, names[:1]) + return nodes + + def _matchnodes(self, matching, names): + if not matching or not names: + return matching + name = names[0] + assert name + nextnames = names[1:] + resultnodes = [] + for node in matching: + if isinstance(node, pytest.collect.Item): + resultnodes.append(node) + continue + assert isinstance(node, pytest.collect.Collector) + node.ihook.pytest_collectstart(collector=node) + rep = node.ihook.pytest_make_collect_report(collector=node) + if rep.passed: + for x in rep.result: + if x.name == name: + resultnodes.extend(self.matchnodes([x], nextnames)) + node.ihook.pytest_collectreport(report=rep) + return resultnodes + + def genitems(self, node): + self.trace("genitems", node) + if isinstance(node, pytest.collect.Item): + node.ihook.pytest_itemcollected(item=node) + yield node + else: + assert isinstance(node, pytest.collect.Collector) + node.ihook.pytest_collectstart(collector=node) + rep = node.ihook.pytest_make_collect_report(collector=node) + if rep.passed: + for subnode in rep.result: + for x in self.genitems(subnode): + yield x + node.ihook.pytest_collectreport(report=rep) + diff --git a/pytest/plugin/terminal.py b/pytest/plugin/terminal.py index 1ccfd23b7..ffc6070cc 100644 --- a/pytest/plugin/terminal.py +++ b/pytest/plugin/terminal.py @@ -115,10 +115,10 @@ class TerminalReporter: def write_fspath_result(self, fspath, res): if fspath != self.currentfspath: self.currentfspath = fspath - fspath = self.curdir.bestrelpath(fspath) + #fspath = self.curdir.bestrelpath(fspath) self._tw.line() - relpath = self.curdir.bestrelpath(fspath) - self._tw.write(relpath + " ") + #relpath = self.curdir.bestrelpath(fspath) + self._tw.write(fspath + " ") self._tw.write(res) def write_ensure_prefix(self, prefix, extra="", **kwargs): @@ -163,14 +163,15 @@ class TerminalReporter: def pytest__teardown_final_logerror(self, report): self.stats.setdefault("error", []).append(report) - def pytest_runtest_logstart(self, nodeid, location, fspath): + def pytest_runtest_logstart(self, nodeid, location): # ensure that the path is printed before the # 1st test of a module starts running + fspath = nodeid.split("::")[0] if self.showlongtestinfo: line = self._locationline(fspath, *location) self.write_ensure_prefix(line, "") elif self.showfspath: - self.write_fspath_result(py.path.local(fspath), "") + self.write_fspath_result(fspath, "") def pytest_runtest_logreport(self, report): rep = report diff --git a/setup.py b/setup.py index 984623dda..5a69ffce7 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def main(): name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.0.0.dev18', + version='2.0.0.dev19', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], @@ -66,4 +66,4 @@ def make_entry_points(): return {'console_scripts': l} if __name__ == '__main__': - main() + main() \ No newline at end of file diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index c24eb1c17..20474ed6a 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -76,14 +76,13 @@ class TestGeneralUsage: p1 = testdir.makepyfile("") p2 = testdir.makefile(".pyc", "123") result = testdir.runpytest(p1, p2) - assert result.ret != 0 + assert result.ret result.stderr.fnmatch_lines([ - "*ERROR: can't collect:*%s" %(p2.basename,) + "*ERROR: not found:*%s" %(p2.basename,) ]) - @py.test.mark.xfail def test_early_skip(self, testdir): testdir.mkdir("xyz") testdir.makeconftest(""" @@ -97,7 +96,6 @@ class TestGeneralUsage: "*1 skip*" ]) - def test_issue88_initial_file_multinodes(self, testdir): testdir.makeconftest(""" import py @@ -145,7 +143,7 @@ class TestGeneralUsage: print (py.__file__) print (py.__path__) os.chdir(os.path.dirname(os.getcwd())) - print (py.log.Producer) + print (py.log) """)) result = testdir.runpython(p, prepend=False) assert not result.ret @@ -210,6 +208,27 @@ class TestGeneralUsage: res = testdir.runpytest(p) assert res.ret == 0 res.stdout.fnmatch_lines(["*1 skipped*"]) + + def test_direct_addressing_selects(self, testdir): + p = testdir.makepyfile(""" + def pytest_generate_tests(metafunc): + metafunc.addcall({'i': 1}, id="1") + metafunc.addcall({'i': 2}, id="2") + def test_func(i): + pass + """) + res = testdir.runpytest(p.basename + "::" + "test_func[1]") + assert res.ret == 0 + res.stdout.fnmatch_lines(["*1 passed*"]) + + def test_direct_addressing_notfound(self, testdir): + p = testdir.makepyfile(""" + def test_func(): + pass + """) + res = testdir.runpytest(p.basename + "::" + "test_notfound") + assert res.ret + res.stderr.fnmatch_lines(["*ERROR*not found*"]) class TestInvocationVariants: def test_earlyinit(self, testdir): diff --git a/testing/plugin/test_junitxml.py b/testing/plugin/test_junitxml.py index fe4479aae..d6ae6982c 100644 --- a/testing/plugin/test_junitxml.py +++ b/testing/plugin/test_junitxml.py @@ -9,11 +9,13 @@ def runandparse(testdir, *args): return result, xmldoc def assert_attr(node, **kwargs): + __tracebackhide__ = True for name, expected in kwargs.items(): anode = node.getAttributeNode(name) assert anode, "node %r has no attribute %r" %(node, name) val = anode.value - assert val == str(expected) + if val != str(expected): + py.test.fail("%r != %r" %(str(val), str(expected))) class TestPython: def test_summing_simple(self, testdir): @@ -50,7 +52,7 @@ class TestPython: assert_attr(node, errors=1, tests=0) tnode = node.getElementsByTagName("testcase")[0] assert_attr(tnode, - classname="test_setup_error.test_setup_error", + classname="test_setup_error", name="test_function") fnode = tnode.getElementsByTagName("error")[0] assert_attr(fnode, message="test setup failure") @@ -68,9 +70,21 @@ class TestPython: assert_attr(node, failures=1) tnode = node.getElementsByTagName("testcase")[0] assert_attr(tnode, - classname="test_classname_instance.test_classname_instance.TestClass", + classname="test_classname_instance.TestClass", name="test_method") + def test_classname_nested_dir(self, testdir): + p = testdir.tmpdir.ensure("sub", "test_hello.py") + p.write("def test_func(): 0/0") + result, dom = runandparse(testdir) + assert result.ret + node = dom.getElementsByTagName("testsuite")[0] + assert_attr(node, failures=1) + tnode = node.getElementsByTagName("testcase")[0] + assert_attr(tnode, + classname="sub.test_hello", + name="test_func") + def test_internal_error(self, testdir): testdir.makeconftest("def pytest_runtest_protocol(): 0 / 0") testdir.makepyfile("def test_function(): pass") @@ -92,7 +106,7 @@ class TestPython: assert_attr(node, failures=1, tests=1) tnode = node.getElementsByTagName("testcase")[0] assert_attr(tnode, - classname="test_failure_function.test_failure_function", + classname="test_failure_function", name="test_fail") fnode = tnode.getElementsByTagName("failure")[0] assert_attr(fnode, message="test failure") @@ -112,11 +126,11 @@ class TestPython: assert_attr(node, failures=2, tests=2) tnode = node.getElementsByTagName("testcase")[0] assert_attr(tnode, - classname="test_failure_escape.test_failure_escape", + classname="test_failure_escape", name="test_func[<]") tnode = node.getElementsByTagName("testcase")[1] assert_attr(tnode, - classname="test_failure_escape.test_failure_escape", + classname="test_failure_escape", name="test_func[&]") def test_junit_prefixing(self, testdir): @@ -133,11 +147,11 @@ class TestPython: assert_attr(node, failures=1, tests=2) tnode = node.getElementsByTagName("testcase")[0] assert_attr(tnode, - classname="xyz.test_junit_prefixing.test_junit_prefixing", + classname="xyz.test_junit_prefixing", name="test_func") tnode = node.getElementsByTagName("testcase")[1] assert_attr(tnode, - classname="xyz.test_junit_prefixing.test_junit_prefixing." + classname="xyz.test_junit_prefixing." "TestHello", name="test_hello") @@ -153,7 +167,7 @@ class TestPython: assert_attr(node, skips=1, tests=0) tnode = node.getElementsByTagName("testcase")[0] assert_attr(tnode, - classname="test_xfailure_function.test_xfailure_function", + classname="test_xfailure_function", name="test_xfail") fnode = tnode.getElementsByTagName("skipped")[0] assert_attr(fnode, message="expected test failure") @@ -172,7 +186,7 @@ class TestPython: assert_attr(node, skips=1, tests=0) tnode = node.getElementsByTagName("testcase")[0] assert_attr(tnode, - classname="test_xfailure_xpass.test_xfailure_xpass", + classname="test_xfailure_xpass", name="test_xpass") fnode = tnode.getElementsByTagName("skipped")[0] assert_attr(fnode, message="xfail-marked test passes unexpectedly") diff --git a/testing/plugin/test_python.py b/testing/plugin/test_python.py index 2e116706a..ac8f7135a 100644 --- a/testing/plugin/test_python.py +++ b/testing/plugin/test_python.py @@ -235,7 +235,7 @@ class TestFunction: param = 1 funcargs = {} id = "world" - collection = object() + collection = testdir.Collection(config) f5 = py.test.collect.Function(name="name", config=config, callspec=callspec1, callobj=isinstance, collection=collection) f5b = py.test.collect.Function(name="name", config=config, @@ -395,8 +395,8 @@ def test_generate_tests_only_done_in_subdir(testdir): def test_modulecol_roundtrip(testdir): modcol = testdir.getmodulecol("pass", withinit=True) - trail = modcol.collection.getid(modcol) - newcol = modcol.collection.getbyid(trail)[0] + trail = modcol.nodeid + newcol = modcol.collection.perform_collect([trail], genitems=0)[0] assert modcol.name == newcol.name @@ -1058,8 +1058,7 @@ class TestReportInfo: """) item = testdir.getitem("def test_func(): pass") runner = item.config.pluginmanager.getplugin("runner") - nodeinfo = runner.getitemnodeinfo(item) - assert nodeinfo.location == ("ABCDE", 42, "custom") + assert item.location == ("ABCDE", 42, "custom") def test_func_reportinfo(self, testdir): item = testdir.getitem("def test_func(): pass") diff --git a/testing/plugin/test_runner.py b/testing/plugin/test_runner.py index ddf6dba72..2c595b7dd 100644 --- a/testing/plugin/test_runner.py +++ b/testing/plugin/test_runner.py @@ -257,9 +257,9 @@ class TestCollectionReports: assert not rep.skipped assert rep.passed locinfo = rep.location - assert locinfo[0] == col.fspath + assert locinfo[0] == col.fspath.basename assert not locinfo[1] - assert locinfo[2] == col.fspath + assert locinfo[2] == col.fspath.basename res = rep.result assert len(res) == 2 assert res[0].name == "test_func1" diff --git a/testing/plugin/test_session.py b/testing/plugin/test_session.py index 85a5a7d4b..4597ba7e4 100644 --- a/testing/plugin/test_session.py +++ b/testing/plugin/test_session.py @@ -17,13 +17,14 @@ class SessionTests: assert len(skipped) == 0 assert len(passed) == 1 assert len(failed) == 3 - assert failed[0].nodenames[-1] == "test_one_one" - assert failed[1].nodenames[-1] == "test_other" - assert failed[2].nodenames[-1] == "test_two" + end = lambda x: x.nodeid.split("::")[-1] + assert end(failed[0]) == "test_one_one" + assert end(failed[1]) == "test_other" + assert end(failed[2]) == "test_two" itemstarted = reprec.getcalls("pytest_itemcollected") assert len(itemstarted) == 4 colstarted = reprec.getcalls("pytest_collectstart") - assert len(colstarted) == 1 + 1 # XXX ExtraTopCollector + assert len(colstarted) == 1 + 1 col = colstarted[1].collector assert isinstance(col, py.test.collect.Module) @@ -186,7 +187,7 @@ class TestNewSession(SessionTests): started = reprec.getcalls("pytest_collectstart") finished = reprec.getreports("pytest_collectreport") assert len(started) == len(finished) - assert len(started) == 8 + 1 # XXX extra TopCollector + assert len(started) == 8 # XXX extra TopCollector colfail = [x for x in finished if x.failed] colskipped = [x for x in finished if x.skipped] assert len(colfail) == 1 diff --git a/testing/plugin/test_terminal.py b/testing/plugin/test_terminal.py index 897863787..610942e5b 100644 --- a/testing/plugin/test_terminal.py +++ b/testing/plugin/test_terminal.py @@ -1,14 +1,9 @@ """ terminal reporting of the full testing process. """ -import py +import pytest,py import sys -# =============================================================================== -# plugin tests -# -# =============================================================================== - from pytest.plugin.terminal import TerminalReporter, \ CollectonlyReporter, repr_pythonversion, getreportopt from pytest.plugin import runner @@ -95,9 +90,8 @@ class TestTerminal: item = testdir.getitem("def test_func(): pass") tr = TerminalReporter(item.config, file=linecomp.stringio) item.config.pluginmanager.register(tr) - nodeid = item.collection.getid(item) location = item.reportinfo() - tr.config.hook.pytest_runtest_logstart(nodeid=nodeid, + tr.config.hook.pytest_runtest_logstart(nodeid=item.nodeid, location=location, fspath=str(item.fspath)) linecomp.assert_contains_lines([ "*test_show_runtest_logstart.py*" @@ -424,6 +418,7 @@ class TestTerminalFunctional: "*test_verbose_reporting.py:10: test_gen*FAIL*", ]) assert result.ret == 1 + pytest.xfail("repair xdist") pytestconfig.pluginmanager.skipifmissing("xdist") result = testdir.runpytest(p1, '-v', '-n 1') result.stdout.fnmatch_lines([ diff --git a/testing/test_collect.py b/testing/test_collect.py deleted file mode 100644 index 4803f5b3d..000000000 --- a/testing/test_collect.py +++ /dev/null @@ -1,288 +0,0 @@ -import py - -class TestCollector: - def test_collect_versus_item(self): - from pytest.collect import Collector, Item - assert not issubclass(Collector, Item) - assert not issubclass(Item, Collector) - - def test_compat_attributes(self, testdir, recwarn): - modcol = testdir.getmodulecol(""" - def test_pass(): pass - def test_fail(): assert 0 - """) - recwarn.clear() - assert modcol.Module == py.test.collect.Module - recwarn.pop(DeprecationWarning) - assert modcol.Class == py.test.collect.Class - recwarn.pop(DeprecationWarning) - assert modcol.Item == py.test.collect.Item - recwarn.pop(DeprecationWarning) - assert modcol.File == py.test.collect.File - recwarn.pop(DeprecationWarning) - assert modcol.Function == py.test.collect.Function - recwarn.pop(DeprecationWarning) - - def test_check_equality(self, testdir): - modcol = testdir.getmodulecol(""" - def test_pass(): pass - def test_fail(): assert 0 - """) - fn1 = testdir.collect_by_name(modcol, "test_pass") - assert isinstance(fn1, py.test.collect.Function) - fn2 = testdir.collect_by_name(modcol, "test_pass") - assert isinstance(fn2, py.test.collect.Function) - - assert fn1 == fn2 - assert fn1 != modcol - if py.std.sys.version_info < (3, 0): - assert cmp(fn1, fn2) == 0 - assert hash(fn1) == hash(fn2) - - fn3 = testdir.collect_by_name(modcol, "test_fail") - assert isinstance(fn3, py.test.collect.Function) - assert not (fn1 == fn3) - assert fn1 != fn3 - - 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_getparent(self, testdir): - modcol = testdir.getmodulecol(""" - class TestClass: - def test_foo(): - pass - """) - cls = testdir.collect_by_name(modcol, "TestClass") - fn = testdir.collect_by_name( - testdir.collect_by_name(cls, "()"), "test_foo") - - parent = fn.getparent(py.test.collect.Module) - assert parent is modcol - - parent = fn.getparent(py.test.collect.Function) - assert parent is fn - - parent = fn.getparent(py.test.collect.Class) - assert parent is cls - - - def test_getcustomfile_roundtrip(self, testdir): - hello = testdir.makefile(".xxx", hello="world") - testdir.makepyfile(conftest=""" - import py - class CustomFile(py.test.collect.File): - pass - def pytest_collect_file(path, parent): - if path.ext == ".xxx": - return CustomFile(path, parent=parent) - """) - config = testdir.parseconfig(hello) - node = testdir.getnode(config, hello) - assert isinstance(node, py.test.collect.File) - assert node.name == "hello.xxx" - id = node.collection.getid(node) - nodes = node.collection.getbyid(id) - assert len(nodes) == 1 - assert isinstance(nodes[0], 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') - - result = testdir.runpytest("--collectonly") - s = result.stdout.str() - assert "test_notfound" not in s - assert "test_found" in s - - def test_custom_norecursedirs(self, testdir): - testdir.makeini(""" - [pytest] - norecursedirs = mydir xyz* - """) - tmpdir = testdir.tmpdir - tmpdir.ensure("mydir", "test_hello.py").write("def test_1(): pass") - tmpdir.ensure("xyz123", "test_2.py").write("def test_2(): 0/0") - tmpdir.ensure("xy", "test_ok.py").write("def test_3(): pass") - rec = testdir.inline_run() - rec.assertoutcome(passed=1) - rec = testdir.inline_run("xyz123/test_2.py") - rec.assertoutcome(failed=1) - - def test_found_certain_testfiles(self, testdir): - p1 = testdir.makepyfile(test_found = "pass", found_test="pass") - col = testdir.getnode(testdir.parseconfig(p1), 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 = testdir.getnode(config, p1.dirpath()) - names = [x.name for x in col.collect()] - assert names == ["dir1", "dir2", "test_one.py", "test_two.py", "x"] - -class TestCollectPluginHookRelay: - 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.pluginmanager.register(Plugin()) - config.parse([tmpdir]) - col = testdir.getnode(config, tmpdir) - testdir.makefile(".abc", "xyz") - res = col.collect() - assert len(wascalled) == 1 - assert wascalled[0].ext == '.abc' - - def test_pytest_collect_directory(self, testdir): - tmpdir = testdir.tmpdir - wascalled = [] - class Plugin: - def pytest_collect_directory(self, path, parent): - wascalled.append(path.basename) - return py.test.collect.Directory(path, parent) - testdir.plugins.append(Plugin()) - testdir.mkdir("hello") - testdir.mkdir("world") - reprec = testdir.inline_run() - assert "hello" in wascalled - assert "world" in wascalled - # make sure the directories do not get double-appended - colreports = reprec.getreports("pytest_collectreport") - names = [rep.nodenames[-1] for rep in colreports] - assert names.count("hello") == 1 - -class TestPrunetraceback: - def test_collection_error(self, testdir): - p = testdir.makepyfile(""" - import not_exists - """) - result = testdir.runpytest(p) - assert "__import__" not in result.stdout.str(), "too long traceback" - result.stdout.fnmatch_lines([ - "*ERROR collecting*", - "*mport*not_exists*" - ]) - - def test_custom_repr_failure(self, testdir): - p = testdir.makepyfile(""" - import not_exists - """) - testdir.makeconftest(""" - import py - def pytest_collect_file(path, parent): - return MyFile(path, parent) - class MyError(Exception): - pass - class MyFile(py.test.collect.File): - def collect(self): - raise MyError() - def repr_failure(self, excinfo): - if excinfo.errisinstance(MyError): - return "hello world" - return py.test.collect.File.repr_failure(self, excinfo) - """) - - result = testdir.runpytest(p) - result.stdout.fnmatch_lines([ - "*ERROR collecting*", - "*hello world*", - ]) - - @py.test.mark.xfail(reason="other mechanism for adding to reporting needed") - def test_collect_report_postprocessing(self, testdir): - p = testdir.makepyfile(""" - import not_exists - """) - testdir.makeconftest(""" - import py - def pytest_make_collect_report(__multicall__): - rep = __multicall__.execute() - rep.headerlines += ["header1"] - return rep - """) - result = testdir.runpytest(p) - result.stdout.fnmatch_lines([ - "*ERROR collecting*", - "*header1*", - ]) - - -class TestCustomConftests: - def test_ignore_collect_path(self, testdir): - testdir.makeconftest(""" - def pytest_ignore_collect(path, config): - return path.basename.startswith("x") or \ - path.basename == "test_one.py" - """) - testdir.mkdir("xy123").ensure("test_hello.py").write( - "syntax error" - ) - testdir.makepyfile("def test_hello(): pass") - testdir.makepyfile(test_one="syntax error") - result = testdir.runpytest() - assert result.ret == 0 - result.stdout.fnmatch_lines(["*1 passed*"]) - - def test_collectignore_exclude_on_option(self, testdir): - testdir.makeconftest(""" - collect_ignore = ['hello', 'test_world.py'] - def pytest_addoption(parser): - parser.addoption("--XX", action="store_true", default=False) - def pytest_configure(config): - if config.getvalue("XX"): - collect_ignore[:] = [] - """) - testdir.mkdir("hello") - testdir.makepyfile(test_world="#") - reprec = testdir.inline_run(testdir.tmpdir) - names = [rep.nodenames[-1] - for rep in reprec.getreports("pytest_collectreport")] - assert 'hello' not in names - assert 'test_world.py' not in names - reprec = testdir.inline_run(testdir.tmpdir, "--XX") - names = [rep.nodenames[-1] - for rep in reprec.getreports("pytest_collectreport")] - assert 'hello' in names - assert 'test_world.py' in names - - def test_pytest_fs_collect_hooks_are_seen(self, testdir): - conf = testdir.makeconftest(""" - import py - class MyDirectory(py.test.collect.Directory): - pass - class MyModule(py.test.collect.Module): - pass - def pytest_collect_directory(path, parent): - return MyDirectory(path, parent) - def pytest_collect_file(path, parent): - return MyModule(path, parent) - """) - sub = testdir.mkdir("sub") - p = testdir.makepyfile("def test_x(): pass") - result = testdir.runpytest("--collectonly") - result.stdout.fnmatch_lines([ - "*MyDirectory*", - "*MyModule*", - "*test_x*" - ]) diff --git a/testing/test_collection.py b/testing/test_collection.py index 231617993..9aa0fb96d 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -1,6 +1,283 @@ import py -from pytest.plugin.session import Collection, gettopdir +from pytest.plugin.session import Collection + +class TestCollector: + def test_collect_versus_item(self): + from pytest.collect import Collector, Item + assert not issubclass(Collector, Item) + assert not issubclass(Item, Collector) + + def test_compat_attributes(self, testdir, recwarn): + modcol = testdir.getmodulecol(""" + def test_pass(): pass + def test_fail(): assert 0 + """) + recwarn.clear() + assert modcol.Module == py.test.collect.Module + recwarn.pop(DeprecationWarning) + assert modcol.Class == py.test.collect.Class + recwarn.pop(DeprecationWarning) + assert modcol.Item == py.test.collect.Item + recwarn.pop(DeprecationWarning) + assert modcol.File == py.test.collect.File + recwarn.pop(DeprecationWarning) + assert modcol.Function == py.test.collect.Function + recwarn.pop(DeprecationWarning) + + def test_check_equality(self, testdir): + modcol = testdir.getmodulecol(""" + def test_pass(): pass + def test_fail(): assert 0 + """) + fn1 = testdir.collect_by_name(modcol, "test_pass") + assert isinstance(fn1, py.test.collect.Function) + fn2 = testdir.collect_by_name(modcol, "test_pass") + assert isinstance(fn2, py.test.collect.Function) + + assert fn1 == fn2 + assert fn1 != modcol + if py.std.sys.version_info < (3, 0): + assert cmp(fn1, fn2) == 0 + assert hash(fn1) == hash(fn2) + + fn3 = testdir.collect_by_name(modcol, "test_fail") + assert isinstance(fn3, py.test.collect.Function) + assert not (fn1 == fn3) + assert fn1 != fn3 + + 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_getparent(self, testdir): + modcol = testdir.getmodulecol(""" + class TestClass: + def test_foo(): + pass + """) + cls = testdir.collect_by_name(modcol, "TestClass") + fn = testdir.collect_by_name( + testdir.collect_by_name(cls, "()"), "test_foo") + + parent = fn.getparent(py.test.collect.Module) + assert parent is modcol + + parent = fn.getparent(py.test.collect.Function) + assert parent is fn + + parent = fn.getparent(py.test.collect.Class) + assert parent is cls + + + def test_getcustomfile_roundtrip(self, testdir): + hello = testdir.makefile(".xxx", hello="world") + testdir.makepyfile(conftest=""" + import py + class CustomFile(py.test.collect.File): + pass + def pytest_collect_file(path, parent): + if path.ext == ".xxx": + return CustomFile(path, parent=parent) + """) + node = testdir.getpathnode(hello) + assert isinstance(node, py.test.collect.File) + assert node.name == "hello.xxx" + nodes = node.collection.perform_collect([node.nodeid], genitems=False) + assert len(nodes) == 1 + assert isinstance(nodes[0], 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') + + result = testdir.runpytest("--collectonly") + s = result.stdout.str() + assert "test_notfound" not in s + assert "test_found" in s + + def test_custom_norecursedirs(self, testdir): + testdir.makeini(""" + [pytest] + norecursedirs = mydir xyz* + """) + tmpdir = testdir.tmpdir + tmpdir.ensure("mydir", "test_hello.py").write("def test_1(): pass") + tmpdir.ensure("xyz123", "test_2.py").write("def test_2(): 0/0") + tmpdir.ensure("xy", "test_ok.py").write("def test_3(): pass") + rec = testdir.inline_run() + rec.assertoutcome(passed=1) + rec = testdir.inline_run("xyz123/test_2.py") + rec.assertoutcome(failed=1) + +class TestCollectPluginHookRelay: + def test_pytest_collect_file(self, testdir): + wascalled = [] + class Plugin: + def pytest_collect_file(self, path, parent): + wascalled.append(path) + testdir.makefile(".abc", "xyz") + testdir.pytestmain([testdir.tmpdir], plugins=[Plugin()]) + assert len(wascalled) == 1 + assert wascalled[0].ext == '.abc' + + def test_pytest_collect_directory(self, testdir): + wascalled = [] + class Plugin: + def pytest_collect_directory(self, path, parent): + wascalled.append(path.basename) + testdir.mkdir("hello") + testdir.mkdir("world") + testdir.pytestmain(testdir.tmpdir, plugins=[Plugin()]) + assert "hello" in wascalled + assert "world" in wascalled + +class TestPrunetraceback: + def test_collection_error(self, testdir): + p = testdir.makepyfile(""" + import not_exists + """) + result = testdir.runpytest(p) + assert "__import__" not in result.stdout.str(), "too long traceback" + result.stdout.fnmatch_lines([ + "*ERROR collecting*", + "*mport*not_exists*" + ]) + + def test_custom_repr_failure(self, testdir): + p = testdir.makepyfile(""" + import not_exists + """) + testdir.makeconftest(""" + import py + def pytest_collect_file(path, parent): + return MyFile(path, parent) + class MyError(Exception): + pass + class MyFile(py.test.collect.File): + def collect(self): + raise MyError() + def repr_failure(self, excinfo): + if excinfo.errisinstance(MyError): + return "hello world" + return py.test.collect.File.repr_failure(self, excinfo) + """) + + result = testdir.runpytest(p) + result.stdout.fnmatch_lines([ + "*ERROR collecting*", + "*hello world*", + ]) + + @py.test.mark.xfail(reason="other mechanism for adding to reporting needed") + def test_collect_report_postprocessing(self, testdir): + p = testdir.makepyfile(""" + import not_exists + """) + testdir.makeconftest(""" + import py + def pytest_make_collect_report(__multicall__): + rep = __multicall__.execute() + rep.headerlines += ["header1"] + return rep + """) + result = testdir.runpytest(p) + result.stdout.fnmatch_lines([ + "*ERROR collecting*", + "*header1*", + ]) + + +class TestCustomConftests: + def test_ignore_collect_path(self, testdir): + testdir.makeconftest(""" + def pytest_ignore_collect(path, config): + return path.basename.startswith("x") or \ + path.basename == "test_one.py" + """) + testdir.mkdir("xy123").ensure("test_hello.py").write( + "syntax error" + ) + testdir.makepyfile("def test_hello(): pass") + testdir.makepyfile(test_one="syntax error") + result = testdir.runpytest() + assert result.ret == 0 + result.stdout.fnmatch_lines(["*1 passed*"]) + + def test_collectignore_exclude_on_option(self, testdir): + testdir.makeconftest(""" + collect_ignore = ['hello', 'test_world.py'] + def pytest_addoption(parser): + parser.addoption("--XX", action="store_true", default=False) + def pytest_configure(config): + if config.getvalue("XX"): + collect_ignore[:] = [] + """) + testdir.mkdir("hello") + testdir.makepyfile(test_world="def test_hello(): pass") + result = testdir.runpytest() + assert result.ret == 0 + assert "passed" not in result.stdout.str() + result = testdir.runpytest("--XX") + assert result.ret == 0 + assert "passed" in result.stdout.str() + + def test_pytest_fs_collect_hooks_are_seen(self, testdir): + conf = testdir.makeconftest(""" + import py + class MyModule(py.test.collect.Module): + pass + def pytest_collect_file(path, parent): + if path.ext == ".py": + return MyModule(path, parent) + """) + sub = testdir.mkdir("sub") + p = testdir.makepyfile("def test_x(): pass") + result = testdir.runpytest("--collectonly") + result.stdout.fnmatch_lines([ + "*MyModule*", + "*test_x*" + ]) + + def test_pytest_collect_file_from_sister_dir(self, testdir): + sub1 = testdir.mkpydir("sub1") + sub2 = testdir.mkpydir("sub2") + conf1 = testdir.makeconftest(""" + import py + class MyModule1(py.test.collect.Module): + pass + def pytest_collect_file(path, parent): + if path.ext == ".py": + return MyModule1(path, parent) + """) + conf1.move(sub1.join(conf1.basename)) + conf2 = testdir.makeconftest(""" + import py + class MyModule2(py.test.collect.Module): + pass + def pytest_collect_file(path, parent): + if path.ext == ".py": + return MyModule2(path, parent) + """) + conf2.move(sub2.join(conf2.basename)) + p = testdir.makepyfile("def test_x(): pass") + p.copy(sub1.join(p.basename)) + p.copy(sub2.join(p.basename)) + result = testdir.runpytest("--collectonly") + result.stdout.fnmatch_lines([ + "*MyModule1*", + "*MyModule2*", + "*test_x*" + ]) class TestCollection: def test_parsearg(self, testdir): @@ -13,16 +290,15 @@ class TestCollection: subdir.chdir() config = testdir.parseconfig(p.basename) rcol = Collection(config=config) - assert rcol.topdir == testdir.tmpdir + assert rcol.fspath == subdir parts = rcol._parsearg(p.basename) - assert parts[0] == "sub" - assert parts[1] == p.basename - assert len(parts) == 2 + + assert parts[0] == target + assert len(parts) == 1 parts = rcol._parsearg(p.basename + "::test_func") - assert parts[0] == "sub" - assert parts[1] == p.basename - assert parts[2] == "test_func" - assert len(parts) == 3 + assert parts[0] == target + assert parts[1] == "test_func" + assert len(parts) == 2 def test_collect_topdir(self, testdir): p = testdir.makepyfile("def test_func(): pass") @@ -30,14 +306,14 @@ class TestCollection: config = testdir.parseconfig(id) topdir = testdir.tmpdir rcol = Collection(config) - assert topdir == rcol.topdir - hookrec = testdir.getreportrecorder(config) - items = rcol.perform_collect() - assert len(items) == 1 - root = items[0].listchain()[0] - root_id = rcol.getid(root) - root2 = rcol.getbyid(root_id)[0] - assert root2.fspath == root.fspath + assert topdir == rcol.fspath + rootid = rcol.nodeid + #root2 = rcol.perform_collect([rcol.nodeid], genitems=False)[0] + #assert root2 == rcol, rootid + colitems = rcol.perform_collect([rcol.nodeid], genitems=False) + assert len(colitems) == 1 + assert colitems[0].fspath == p + def test_collect_protocol_single_function(self, testdir): p = testdir.makepyfile("def test_func(): pass") @@ -45,13 +321,14 @@ class TestCollection: config = testdir.parseconfig(id) topdir = testdir.tmpdir rcol = Collection(config) - assert topdir == rcol.topdir + assert topdir == rcol.fspath hookrec = testdir.getreportrecorder(config) - items = rcol.perform_collect() + rcol.perform_collect() + items = rcol.items assert len(items) == 1 item = items[0] assert item.name == "test_func" - newid = rcol.getid(item) + newid = item.nodeid assert newid == id py.std.pprint.pprint(hookrec.hookrecorder.calls) hookrec.hookrecorder.contains([ @@ -60,8 +337,8 @@ class TestCollection: ("pytest_collectstart", "collector.fspath == p"), ("pytest_make_collect_report", "collector.fspath == p"), ("pytest_pycollect_makeitem", "name == 'test_func'"), - ("pytest_collectreport", "report.fspath == p"), - ("pytest_collectreport", "report.fspath == topdir") + ("pytest_collectreport", "report.nodeid.startswith(p.basename)"), + ("pytest_collectreport", "report.nodeid == '.'") ]) def test_collect_protocol_method(self, testdir): @@ -70,19 +347,19 @@ class TestCollection: def test_method(self): pass """) - normid = p.basename + "::TestClass::test_method" + normid = p.basename + "::TestClass::()::test_method" for id in [p.basename, p.basename + "::TestClass", p.basename + "::TestClass::()", - p.basename + "::TestClass::()::test_method", normid, ]: config = testdir.parseconfig(id) rcol = Collection(config=config) - nodes = rcol.perform_collect() - assert len(nodes) == 1 - assert nodes[0].name == "test_method" - newid = rcol.getid(nodes[0]) + rcol.perform_collect() + items = rcol.items + assert len(items) == 1 + assert items[0].name == "test_method" + newid = items[0].nodeid assert newid == normid def test_collect_custom_nodes_multi_id(self, testdir): @@ -104,20 +381,21 @@ class TestCollection: config = testdir.parseconfig(id) rcol = Collection(config) hookrec = testdir.getreportrecorder(config) - items = rcol.perform_collect() + rcol.perform_collect() + items = rcol.items py.std.pprint.pprint(hookrec.hookrecorder.calls) assert len(items) == 2 hookrec.hookrecorder.contains([ ("pytest_collectstart", - "collector.fspath == collector.collection.topdir"), + "collector.fspath == collector.collection.fspath"), ("pytest_collectstart", "collector.__class__.__name__ == 'SpecialFile'"), ("pytest_collectstart", "collector.__class__.__name__ == 'Module'"), ("pytest_pycollect_makeitem", "name == 'test_func'"), - ("pytest_collectreport", "report.fspath == p"), - ("pytest_collectreport", - "report.fspath == %r" % str(rcol.topdir)), + ("pytest_collectreport", "report.nodeid.startswith(p.basename)"), + #("pytest_collectreport", + # "report.fspath == %r" % str(rcol.fspath)), ]) def test_collect_subdir_event_ordering(self, testdir): @@ -128,134 +406,87 @@ class TestCollection: config = testdir.parseconfig() rcol = Collection(config) hookrec = testdir.getreportrecorder(config) - items = rcol.perform_collect() + rcol.perform_collect() + items = rcol.items assert len(items) == 1 py.std.pprint.pprint(hookrec.hookrecorder.calls) hookrec.hookrecorder.contains([ - ("pytest_collectstart", "collector.fspath == aaa"), ("pytest_collectstart", "collector.fspath == test_aaa"), ("pytest_pycollect_makeitem", "name == 'test_func'"), - ("pytest_collectreport", "report.fspath == test_aaa"), - ("pytest_collectreport", "report.fspath == aaa"), + ("pytest_collectreport", + "report.nodeid.startswith('aaa/test_aaa.py')"), ]) def test_collect_two_commandline_args(self, testdir): p = testdir.makepyfile("def test_func(): pass") aaa = testdir.mkpydir("aaa") bbb = testdir.mkpydir("bbb") - p.copy(aaa.join("test_aaa.py")) - p.move(bbb.join("test_bbb.py")) + test_aaa = aaa.join("test_aaa.py") + p.copy(test_aaa) + test_bbb = bbb.join("test_bbb.py") + p.move(test_bbb) id = "." config = testdir.parseconfig(id) rcol = Collection(config) hookrec = testdir.getreportrecorder(config) - items = rcol.perform_collect() + rcol.perform_collect() + items = rcol.items assert len(items) == 2 py.std.pprint.pprint(hookrec.hookrecorder.calls) hookrec.hookrecorder.contains([ - ("pytest_collectstart", "collector.fspath == aaa"), + ("pytest_collectstart", "collector.fspath == test_aaa"), ("pytest_pycollect_makeitem", "name == 'test_func'"), - ("pytest_collectreport", "report.fspath == aaa"), - ("pytest_collectstart", "collector.fspath == bbb"), + ("pytest_collectreport", "report.nodeid == 'aaa/test_aaa.py'"), + ("pytest_collectstart", "collector.fspath == test_bbb"), ("pytest_pycollect_makeitem", "name == 'test_func'"), - ("pytest_collectreport", "report.fspath == bbb"), + ("pytest_collectreport", "report.nodeid == 'bbb/test_bbb.py'"), ]) def test_serialization_byid(self, testdir): p = testdir.makepyfile("def test_func(): pass") config = testdir.parseconfig() rcol = Collection(config) - items = rcol.perform_collect() + rcol.perform_collect() + items = rcol.items assert len(items) == 1 item, = items - id = rcol.getid(item) newcol = Collection(config) - item2, = newcol.getbyid(id) + item2, = newcol.perform_collect([item.nodeid], genitems=False) assert item2.name == item.name assert item2.fspath == item.fspath - item2b, = newcol.getbyid(id) - assert item2b is item2 - -class Test_gettopdir: - def test_gettopdir(self, testdir): - 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_gettopdir_pypkg(self, testdir): - 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 - assert gettopdir(["%s::xyc" % c]) == a - assert gettopdir(["%s::xyc::abc" % c]) == a - assert gettopdir(["%s::xyc" % c, "%s::abc" % Z]) == tmp + item2b, = newcol.perform_collect([item.nodeid], genitems=False) + assert item2b == item2 def getargnode(collection, arg): - return collection.getbyid(collection._normalizearg(str(arg)))[0] + argpath = arg.relto(collection.fspath) + return collection.perform_collect([argpath], genitems=False)[0] class Test_getinitialnodes: - def test_onedir(self, testdir): - config = testdir.reparseconfig([testdir.tmpdir]) - c = Collection(config) - col = getargnode(c, testdir.tmpdir) - assert isinstance(col, py.test.collect.Directory) - for col in col.listchain(): - assert col.config is config - t2 = getargnode(c, testdir.tmpdir) - assert col == t2 - - def test_curdir_and_subdir(self, testdir, tmpdir): - a = tmpdir.ensure("a", dir=1) - config = testdir.reparseconfig([tmpdir, a]) - c = Collection(config) - - col1 = getargnode(c, tmpdir) - col2 = getargnode(c, a) - assert col1.name == tmpdir.basename - assert col2.name == 'a' - for col in (col1, col2): - for subcol in col.listchain(): - assert col.config is config - def test_global_file(self, testdir, tmpdir): x = tmpdir.ensure("x.py") config = testdir.reparseconfig([x]) - col = getargnode(Collection(config), x) + col = testdir.getnode(config, x) assert isinstance(col, py.test.collect.Module) assert col.name == 'x.py' - assert col.parent.name == tmpdir.basename + assert col.parent.name == testdir.tmpdir.basename assert col.parent.parent is None for col in col.listchain(): assert col.config is config - def test_global_dir(self, testdir, tmpdir): - x = tmpdir.ensure("a", dir=1) + def test_pkgfile(self, testdir): + testdir.chdir() + tmpdir = testdir.tmpdir + subdir = tmpdir.join("subdir") + x = subdir.ensure("x.py") + subdir.ensure("__init__.py") config = testdir.reparseconfig([x]) - col = getargnode(Collection(config), 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_pkgfile(self, testdir, tmpdir): - tmpdir = tmpdir.join("subdir") - x = tmpdir.ensure("x.py") - tmpdir.ensure("__init__.py") - config = testdir.reparseconfig([x]) - col = getargnode(Collection(config), x) + col = testdir.getnode(config, x) assert isinstance(col, py.test.collect.Module) - assert col.name == 'x.py' - assert col.parent.name == x.dirpath().basename - assert col.parent.parent.parent is None + print col.obj + print col.listchain() + assert col.name == 'subdir/x.py' + assert col.parent.parent is None for col in col.listchain(): assert col.config is config diff --git a/testing/test_config.py b/testing/test_config.py index 62221fc5c..391238419 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -81,7 +81,7 @@ class TestConfigAPI: config.trace.root.setwriter(l.append) config.trace("hello") assert len(l) == 1 - assert l[0] == "[pytest:config] hello\n" + assert l[0] == "[pytest] hello\n" def test_config_getvalue_honours_conftest(self, testdir): testdir.makepyfile(conftest="x=1") diff --git a/tox.ini b/tox.ini index 04e18188b..1844826c7 100644 --- a/tox.ini +++ b/tox.ini @@ -52,4 +52,5 @@ commands= [pytest] minversion=2.0 plugins=pytester -#addargs=-rf +addargs=-rfx +rsyncdirs=pytest testing