""" 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) assert newnode is not None, (self, name, parent) self.__dict__.update(newnode.__dict__) #self.__init__(name=name, parent=parent) def __repr__(self): 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 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.join(name) if next is None: existingnames = cur.listdir() 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) 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 generate children through their listdir() and join() methods and thus form 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 run(self): """ deprecated: use listdir(). """ py.std.warnings.warn("deprecated: use listdir()", category=DeprecationWarning) return self.listdir() def multijoin(self, namelist): """ return a list of colitems for the given namelist. """ return [self.join(name) for name in namelist] def listdir(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. """ raise NotImplementedError("abstract") def join(self, name): """ return a child collector or item for the given name. If the return value is None there is no such child. """ raise NotImplementedError("abstract") def repr_failure(self, excinfo, outerr): return self._repr_failure_py(excinfo, outerr) 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 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 listdir(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): # not use self.Directory here as # dir/conftest.py shall be able to # define Directory(dir) already Directory = self._config.getvalue('Directory', p) res = Directory(p, parent=self) name2items[name] = res 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 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 execute(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