""" Collect test items at filesystem and python module levels. Collectors and test items form a tree. The difference between a collector and a test item as seen from the session is smalll. Collectors usually return a list of child collectors/items whereas items usually return None indicating a successful test run. The is a schematic example of a tree of collectors and test items:: Directory Module Class Instance Function Generator ... Function Generator Function Directory ... """ from __future__ import generators import py def configproperty(name): def fget(self): #print "retrieving %r property from %s" %(name, self.fspath) return self.config.getvalue(name, self.fspath) return property(fget) class Collector(object): """ Collector instances are iteratively generated (through their run() and join() methods) and form a tree. attributes:: parent: attribute pointing to the parent collector (or None if it is the root collector) name: basename of this collector object """ def __init__(self, name, parent=None): self.name = name self.parent = parent self.config = getattr(parent, 'config', py.test.config) self.fspath = getattr(parent, 'fspath', None) Module = configproperty('Module') DoctestFile = configproperty('DoctestFile') Directory = configproperty('Directory') Class = configproperty('Class') Instance = configproperty('Instance') Function = configproperty('Function') Generator = configproperty('Generator') _stickyfailure = None class Outcome: def __init__(self, msg=None, excinfo=None): self.msg = msg self.excinfo = excinfo def __repr__(self): if self.msg: return self.msg return "<%s instance>" %(self.__class__.__name__,) __str__ = __repr__ class Passed(Outcome): pass class Failed(Outcome): pass class ExceptionFailure(Failed): def __init__(self, expr, expected, msg=None, excinfo=None): Collector.Failed.__init__(self, msg=msg, excinfo=excinfo) self.expr = expr self.expected = expected class Skipped(Outcome): pass def __repr__(self): return "<%s %r>" %(self.__class__.__name__, self.name) def __eq__(self, other): try: return self.name == other.name and self.parent == other.parent except AttributeError: return False def __hash__(self): return hash((self.name, self.parent)) def __ne__(self, other): return not self == other def __cmp__(self, other): s1 = self.getsortvalue() s2 = other.getsortvalue() #print "cmp", s1, s2 return cmp(s1, s2) def run(self): """ returns a list of names available from this collector. You can return an empty list. Callers of this method must take care to catch exceptions properly. The session object guards its calls to ``colitem.run()`` in its ``session.runtraced(colitem)`` method, including catching of stdout. """ raise NotImplementedError("abstract") def join(self, name): """ return a child item for the given name. Usually the session feeds the join method with each name obtained from ``colitem.run()``. If the return value is None it means the ``colitem`` was not able to resolve with the given name. """ def obj(): def fget(self): try: return self._obj except AttributeError: self._obj = obj = self._getobj() return obj def fset(self, value): self._obj = value return property(fget, fset, None, "underlying object") obj = obj() def _getobj(self): return getattr(self.parent.obj, self.name) def multijoin(self, namelist): """ return a list of colitems for the given namelist. """ return [self.join(name) for name in namelist] def getpathlineno(self): return self.fspath, py.std.sys.maxint def setup(self): pass def teardown(self): pass def listchain(self): """ return list of all parent collectors up to ourself. """ l = [self] while 1: x = l[-1] if x.parent is not None: l.append(x.parent) else: l.reverse() return l def listnames(self): return [x.name for x in self.listchain()] def getitembynames(self, namelist): if isinstance(namelist, str): namelist = namelist.split("/") cur = self for name in namelist: if name: next = cur.join(name) assert next is not None, (cur, name, namelist) cur = next return cur def haskeyword(self, keyword): return keyword in self.name def getmodpath(self): """ return dotted module path (relative to the containing). """ inmodule = False newl = [] for x in self.listchain(): if not inmodule and not isinstance(x, Module): continue if not inmodule: inmodule = True continue if newl and x.name[:1] in '([': newl[-1] += x.name else: newl.append(x.name) return ".".join(newl) def skipbykeyword(self, keyword): """ raise Skipped() exception if the given keyword matches for this collector. """ if not keyword: return chain = self.listchain() for key in filter(None, keyword.split()): eor = key[:1] == '-' if eor: key = key[1:] if not (eor ^ self._matchonekeyword(key, chain)): py.test.skip("test not selected by keyword %r" %(keyword,)) def _matchonekeyword(self, key, chain): for subitem in chain: if subitem.haskeyword(key): return True return False def tryiter(self, yieldtype=None, reporterror=None, keyword=None): """ yield stop item instances from flattening the collector. XXX deprecated: this way of iteration is not safe in all cases. """ if yieldtype is None: yieldtype = py.test.Item if isinstance(self, yieldtype): try: self.skipbykeyword(keyword) yield self except py.test.Item.Skipped: if reporterror is not None: excinfo = py.code.ExceptionInfo() reporterror((excinfo, self)) else: if not isinstance(self, py.test.Item): try: if reporterror is not None: reporterror((None, self)) for x in self.run(): for y in self.join(x).tryiter(yieldtype, reporterror, keyword): yield y except KeyboardInterrupt: raise except: if reporterror is not None: excinfo = py.code.ExceptionInfo() reporterror((excinfo, self)) def getsortvalue(self): return self.name captured_out = captured_err = None def startcapture(self): return None # by default collectors don't capture output def finishcapture(self): return None # by default collectors don't capture output def getouterr(self): return self.captured_out, self.captured_err def get_collector_trail(self): """ Shortcut """ return self.config.get_collector_trail(self) class FSCollector(Collector): def __init__(self, fspath, parent=None): fspath = py.path.local(fspath) super(FSCollector, self).__init__(fspath.basename, parent) self.fspath = fspath class Directory(FSCollector): def filefilter(self, path): if path.check(file=1): b = path.purebasename ext = path.ext return (b.startswith('test_') or b.endswith('_test')) and ext in ('.txt', '.py') def recfilter(self, path): if path.check(dir=1, dotfile=0): return path.basename not in ('CVS', '_darcs', '{arch}') def run(self): files = [] dirs = [] for p in self.fspath.listdir(): if self.filefilter(p): files.append(p.basename) elif self.recfilter(p): dirs.append(p.basename) files.sort() dirs.sort() return files + dirs def join(self, name): name2items = self.__dict__.setdefault('_name2items', {}) try: res = name2items[name] except KeyError: p = self.fspath.join(name) res = None if p.check(file=1): if p.ext == '.py': res = self.Module(p, parent=self) elif p.ext == '.txt': res = self.DoctestFile(p, parent=self) elif p.check(dir=1): Directory = py.test.config.getvalue('Directory', p) res = Directory(p, parent=self) name2items[name] = res return res class PyCollectorMixin(object): def funcnamefilter(self, name): return name.startswith('test') def classnamefilter(self, name): return name.startswith('Test') def _buildname2items(self): # NB. we avoid random getattrs and peek in the __dict__ instead d = {} dicts = [getattr(self.obj, '__dict__', {})] for basecls in py.std.inspect.getmro(self.obj.__class__): dicts.append(basecls.__dict__) seen = {} for dic in dicts: for name, obj in dic.items(): if name in seen: continue seen[name] = True res = self.makeitem(name, obj) if res is not None: d[name] = res return d def makeitem(self, name, obj, usefilters=True): if (not usefilters or self.classnamefilter(name)) and \ py.std.inspect.isclass(obj): return self.Class(name, parent=self) elif (not usefilters or self.funcnamefilter(name)) and callable(obj): if obj.func_code.co_flags & 32: # generator function return self.Generator(name, parent=self) else: return self.Function(name, parent=self) def _prepare(self): if not hasattr(self, '_name2items'): ex = getattr(self, '_name2items_exception', None) if ex is not None: raise ex[0], ex[1], ex[2] try: self._name2items = self._buildname2items() except (SystemExit, KeyboardInterrupt): raise except: self._name2items_exception = py.std.sys.exc_info() raise def run(self): self._prepare() itemlist = self._name2items.values() itemlist.sort() return [x.name for x in itemlist] def join(self, name): self._prepare() return self._name2items.get(name, None) class Module(FSCollector, PyCollectorMixin): def run(self): if getattr(self.obj, 'disabled', 0): return [] return PyCollectorMixin.run(self) def join(self, name): res = super(Module, self).join(name) if res is None: attr = getattr(self.obj, name, None) if attr is not None: res = self.makeitem(name, attr, usefilters=False) return res def startcapture(self): if not self.config.option.nocapture: assert not hasattr(self, '_capture') self._capture = py.io.StdCaptureFD() def finishcapture(self): if hasattr(self, '_capture'): capture = self._capture del self._capture self.captured_out, self.captured_err = capture.reset() def __repr__(self): return "<%s %r>" % (self.__class__.__name__, self.name) def obj(self): try: return self._obj except AttributeError: failure = getattr(self, '_stickyfailure', None) if failure is not None: raise failure[0], failure[1], failure[2] try: self._obj = obj = self.fspath.pyimport() except KeyboardInterrupt: raise except: self._stickyfailure = py.std.sys.exc_info() raise return obj obj = property(obj, None, None, "module object") def setup(self): if hasattr(self.obj, 'setup_module'): self.obj.setup_module(self.obj) def teardown(self): if hasattr(self.obj, 'teardown_module'): self.obj.teardown_module(self.obj) class Class(PyCollectorMixin, Collector): def run(self): if getattr(self.obj, 'disabled', 0): return [] return ["()"] def join(self, name): assert name == '()' return self.Instance(name, self) def setup(self): setup_class = getattr(self.obj, 'setup_class', None) if setup_class is not None: setup_class = getattr(setup_class, 'im_func', setup_class) setup_class(self.obj) def teardown(self): teardown_class = getattr(self.obj, 'teardown_class', None) if teardown_class is not None: teardown_class = getattr(teardown_class, 'im_func', teardown_class) teardown_class(self.obj) def getsortvalue(self): # try to locate the class in the source - not nice, but probably # the most useful "solution" that we have try: file = (py.std.inspect.getsourcefile(self.obj) or py.std.inspect.getfile(self.obj)) if not file: raise IOError lines, lineno = py.std.inspect.findsource(self.obj) return py.path.local(file), lineno except IOError: pass # fall back... for x in self.tryiter((py.test.collect.Generator, py.test.Item)): return x.getsortvalue() class Instance(PyCollectorMixin, Collector): def _getobj(self): return self.parent.obj() def Function(self): return getattr(self.obj, 'Function', Collector.Function.__get__(self)) # XXX for python 2.2 Function = property(Function) class Generator(PyCollectorMixin, Collector): def run(self): self._prepare() itemlist = self._name2items return [itemlist["[%d]" % num].name for num in xrange(len(itemlist))] def _buildname2items(self): d = {} # slightly hackish to invoke setup-states on # collection ... self.Function.state.prepare(self) for i, x in py.builtin.enumerate(self.obj()): call, args = self.getcallargs(x) if not callable(call): raise TypeError("yielded test %r not callable" %(call,)) name = "[%d]" % i d[name] = self.Function(name, self, args, obj=call, sort_value = i) return d def getcallargs(self, obj): if isinstance(obj, (tuple, list)): call, args = obj[0], obj[1:] else: call, args = obj, () return call, args def getpathlineno(self): code = py.code.Code(self.obj) return code.path, code.firstlineno def getsortvalue(self): return self.getpathlineno() class DoctestFile(PyCollectorMixin, FSCollector): def run(self): return [self.fspath.basename] def join(self, name): from py.__.test.doctest import DoctestText if name == self.fspath.basename: item = DoctestText(self.fspath.basename, parent=self) item._content = self.fspath.read() return item