""" 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 Directory CustomCollector # provided via conftest's CustomItem # provided via conftest's CustomItem # provided via conftest's Directory ... """ 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 SetupState(object): """ shared state for setting up/tearing down test items or collectors. """ def __init__(self): self.stack = [] def teardown_all(self): while self.stack: col = self.stack.pop() col.teardown() def teardown_exact(self, item): if self.stack and self.stack[-1] == item: col = self.stack.pop() col.teardown() def prepare(self, colitem): """ setup objects along the collector chain to the test-method Teardown any unneccessary previously setup objects.""" needed_collectors = colitem.listchain() while self.stack: if self.stack == needed_collectors[:len(self.stack)]: break col = self.stack.pop() col.teardown() for col in needed_collectors[len(self.stack):]: #print "setting up", col col.setup() self.stack.append(col) class ReprMetaInfo(object): def __init__(self, fspath=None, lineno=None, modpath=None): self.fspath = fspath self.lineno = lineno self.modpath = modpath def verboseline(self, basedir=None): params = self.__dict__.copy() if self.fspath: if basedir is not None: params['fspath'] = getrelpath(basedir, self.fspath) if self.lineno is not None: params['lineno'] = self.lineno + 1 if self.fspath and self.lineno and self.modpath: line = "%(fspath)s:%(lineno)s: %(modpath)s" elif self.fspath and self.modpath: line = "%(fspath)s: %(modpath)s" elif self.fspath and self.lineno: line = "%(fspath)s:%(lineno)s" else: line = "[nometainfo]" return line % params class Node(object): """ base class for Nodes in the collection tree. Collector nodes have children and Item nodes are terminal. All nodes of the collection tree carry a _config attribute for these reasons: - to access custom Collection Nodes from a project (defined in conftest's) - to pickle themselves relatively to the "topdir" - configuration/options for setup/teardown stdout/stderr capturing and execution of test items """ ReprMetaInfo = ReprMetaInfo # XXX we keep global SetupState here because # pycollect's Generators participate # in setup/teardown procedures during collect. _setupstate = SetupState() def __init__(self, name, parent=None, config=None): self.name = name self.parent = parent if config is None: config = getattr(parent, '_config') self._config = config self.fspath = getattr(parent, 'fspath', None) # # note to myself: Pickling is uh. # def __getstate__(self): return (self.name, self.parent) def __setstate__(self, (name, parent)): newnode = parent.join(name) if newnode is None: raise AssertionError(self, name, parent, parent.__dict__) self.__dict__.update(newnode.__dict__) #self.__init__(name=name, parent=parent) 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)) # methods for ordering nodes def __eq__(self, other): if not isinstance(other, Node): return False return self.name == other.name and self.parent == other.parent def __ne__(self, other): return not self == other def __hash__(self): return hash((self.name, self.parent)) def __cmp__(self, other): if not isinstance(other, Node): return -1 s1 = self._getsortvalue() s2 = other._getsortvalue() return cmp(s1, s2) def setup(self): pass def teardown(self): pass def _memoizedcall(self, attrname, function): exattrname = "_ex_" + attrname failure = getattr(self, exattrname, None) if failure is not None: raise failure[0], failure[1], failure[2] if hasattr(self, attrname): return getattr(self, attrname) try: res = function() except (KeyboardInterrupt, SystemExit): raise except: failure = py.std.sys.exc_info() setattr(self, exattrname, failure) raise setattr(self, attrname, res) return res def listchain(self): """ return list of all parent collectors up to self. """ 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): cur = self for name in namelist: if name: next = cur.collect_by_name(name) if next is None: existingnames = [x.name for x in self._memocollect()] msg = ("Collector %r does not have name %r " "existing names are: %s" % (cur, name, existingnames)) raise AssertionError(msg) cur = next return cur def _keywords(self): return [self.name] def _skipbykeyword(self, keywordexpr): """ return True if they given keyword expression means to skip this collector/item. """ if not keywordexpr: return chain = self.listchain() for key in filter(None, keywordexpr.split()): eor = key[:1] == '-' if eor: key = key[1:] if not (eor ^ self._matchonekeyword(key, chain)): return True def _matchonekeyword(self, key, chain): elems = key.split(".") # XXX O(n^2), anyone cares? chain = [item._keywords() for item in chain if item._keywords()] for start, _ in enumerate(chain): if start + len(elems) > len(chain): return False for num, elem in enumerate(elems): for keyword in chain[num + start]: ok = False if elem in keyword: ok = True break if not ok: break if num == len(elems) - 1 and ok: return True return False def _getsortvalue(self): return self.name def _prunetraceback(self, traceback): return traceback def _totrail(self): """ provide a trail relative to the topdir, which can be used to reconstruct the collector (possibly on a different host starting from a different topdir). """ chain = self.listchain() topdir = self._config.topdir relpath = chain[0].fspath.relto(topdir) if not relpath: if chain[0].fspath == topdir: relpath = "." else: raise ValueError("%r not relative to topdir %s" %(chain[0].fspath, topdir)) return relpath, tuple([x.name for x in chain[1:]]) def _fromtrail(trail, config): relpath, names = trail fspath = config.topdir.join(relpath) col = config.getfsnode(fspath) return col._getitembynames(names) _fromtrail = staticmethod(_fromtrail) def _repr_failure_py(self, excinfo, outerr): excinfo.traceback = self._prunetraceback(excinfo.traceback) repr = excinfo.getrepr(funcargs=True, showlocals=self._config.option.showlocals, style=self._config.option.tbstyle) for secname, content in zip(["out", "err"], outerr): if content: repr.addsection("Captured std%s" % secname, content.rstrip()) return repr repr_failure = _repr_failure_py shortfailurerepr = "F" class Collector(Node): """ Collector instances create children through collect() and thus iteratively build a tree. attributes:: parent: attribute pointing to the parent collector (or None if this is the root collector) name: basename of this collector object """ Directory = configproperty('Directory') Module = configproperty('Module') DoctestFile = configproperty('DoctestFile') def collect(self): """ returns a list of children (items and collectors) for this collection node. """ raise NotImplementedError("abstract") def collect_by_name(self, name): """ return a child matching the given name, else None. """ for colitem in self._memocollect(): if colitem.name == name: return colitem def repr_failure(self, excinfo, outerr): """ represent a failure. """ return self._repr_failure_py(excinfo, outerr) def _memocollect(self): """ internal helper method to cache results of calling collect(). """ return self._memoizedcall('_collected', self.collect) # ********************************************************************** # DEPRECATED METHODS # ********************************************************************** def _deprecated_collect(self): # avoid recursion: # collect -> _deprecated_collect -> custom run() -> # super().run() -> collect attrname = '_depcollectentered' if hasattr(self, attrname): return setattr(self, attrname, True) method = getattr(self.__class__, 'run', None) if method is not None and method != Collector.run: warnoldcollect() names = self.run() return filter(None, [self.join(name) for name in names]) def run(self): """ DEPRECATED: 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. """ warnoldcollect() return [colitem.name for colitem in self._memocollect()] def join(self, name): """ DEPRECATED: return a child collector or item for the given name. If the return value is None there is no such child. """ warnoldcollect() return self.collect_by_name(name) def multijoin(self, namelist): """ DEPRECATED: return a list of child items matching the given namelist. """ warnoldcollect() return [self.join(name) for name in namelist] class FSCollector(Collector): def __init__(self, fspath, parent=None, config=None): fspath = py.path.local(fspath) super(FSCollector, self).__init__(fspath.basename, parent, config=config) self.fspath = fspath def __getstate__(self): if self.parent is None: # the root node needs to pickle more context info topdir = self._config.topdir relpath = self.fspath.relto(topdir) if not relpath: if self.fspath == topdir: relpath = "." else: raise ValueError("%r not relative to topdir %s" %(self.fspath, topdir)) return (self.name, self._config, relpath) else: return (self.name, self.parent) def __setstate__(self, picklestate): if len(picklestate) == 3: # root node name, config, relpath = picklestate if not config._initialized: raise ValueError("incomplete unpickling of " "config object, need call to _initafterpickle()?") fspath = config.topdir.join(relpath) fsnode = config.getfsnode(fspath) self.__dict__.update(fsnode.__dict__) else: name, parent = picklestate self.__init__(parent.fspath.join(name), parent=parent) class File(FSCollector): """ base class for collecting tests from a file. """ class Directory(FSCollector): def recfilter(self, path): if path.check(dir=1, dotfile=0): return path.basename not in ('CVS', '_darcs', '{arch}') def collect(self): l = self._deprecated_collect() if l is not None: return l l = [] for path in self.fspath.listdir(sort=True): res = self.consider(path, usefilters=True) if res is not None: l.append(res) return l def consider(self, path, usefilters=True): if path.check(file=1): return self.consider_file(path, usefilters=usefilters) elif path.check(dir=1): return self.consider_dir(path, usefilters=usefilters) def consider_file(self, path, usefilters=True): ext = path.ext pb = path.purebasename if not usefilters or pb.startswith("test_") or pb.endswith("_test"): if ext == ".py": return self.Module(path, parent=self) elif ext == ".txt": return self.DoctestFile(path, parent=self) def consider_dir(self, path, usefilters=True): if not usefilters or self.recfilter(path): # not use self.Directory here as # dir/conftest.py shall be able to # define Directory(dir) already Directory = self._config.getvalue('Directory', path) return Directory(path, parent=self) def collect_by_name(self, name): """ get a child with the given name. """ res = super(Directory, self).collect_by_name(name) if res is None: p = self.fspath.join(name) res = self.consider(p, usefilters=False) return res from py.__.test.runner import basic_run_report, forked_run_report class Item(Node): """ a basic test item. """ def _getrunner(self): if self._config.option.boxed: return forked_run_report return basic_run_report def _deprecated_testexecution(self): if self.__class__.run != Item.run: warnoldtestrun() self.run() return True elif self.__class__.execute != Item.execute: warnoldtestrun() self.execute(self.obj, *self._args) return True def run(self): warnoldtestrun() return self.execute(self.obj, *self._args) def execute(self, obj, *args): warnoldtestrun() return obj(*args) def repr_metainfo(self): try: return self.ReprMetaInfo(self.fspath, modpath=self.__class__.__name__) except AttributeError: code = py.code.Code(self.execute) return self.ReprMetaInfo(code.path, code.firstlineno) def runtest(self): """ execute this test item.""" def getrelpath(curdir, dest): try: base = curdir.common(dest) if not base: # can be the case on windows return dest curdir2base = curdir.relto(base) reldest = dest.relto(base) if curdir2base: n = curdir2base.count(curdir.sep) + 1 else: n = 0 l = ['..'] * n if reldest: l.append(reldest) target = dest.sep.join(l) return target except AttributeError: return dest def depwarn(msg): py.std.warnings.warn(msg, DeprecationWarning) def warnoldcollect(): return depwarn("implement collector.collect() instead of collector.run() and collector.join()") def warnoldtestrun(): return depwarn("implement item.runtest() instead of item.run() and item.execute()")