414 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			414 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Python
		
	
	
	
| """
 | |
| 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
 |