1   
  2   
  3   
  4   
  5   
  6   
  7   
  8   
  9   
 10   
 11   
 12   
 13   
 14   
 15   
 16   
 17   
 18   
 19   
 20   
 21   
 22   
 23   
 24   
 25   
 26   
 27   
 28   
 29   
 30  """Utilities for unit test running from the command line or within the relax testing frame work. 
 31   
 32  Unit tests in the relax frame work are stored in a directory structure 
 33  rooted at <relax-root-directory>/test_suite/unit_tests. The directory 
 34  unit tests contains a directory structure that mirrors the relax directory 
 35  structure and which ideally contains one unit test file/module for each 
 36  file/module in the relax framework. The default convention is that the unit 
 37  test module for a relax module called <relax-module> is called 
 38  test_<relax-module> (stored in test_<relax-module>.py). The unit test module 
 39  test_<relax-module> should then contain a class called Test_<relax-module> 
 40  which is a child of TestCase and contains methods whose names start with 
 41  'test' and take no arguments other than self. 
 42   
 43  A concrete example: for class <relax-root-directory>/maths-fns/chi2.py FIXME:***complete*** 
 44   
 45   
 46  The framework can discover sets of unit tests from the file system and add 
 47  them to TestSuites either from the command line or programmatically from 
 48  inside another program. It also has the ability to search for a  root unit 
 49  test and system directory from a position anywhere inside the unit test 
 50  hierarchy. 
 51   
 52  TODO: Examine PEP 338 and runpy.run_module(modulename): Executing Modules as Scripts for a later 
 53  version of relax that is dependant on python 2.5. 
 54  TODO: Split out runner part from search part. 
 55  """ 
 56   
 57  from copy import copy 
 58  import os, re, string, sys, unittest, traceback 
 59  from optparse import OptionParser 
 60  from textwrap import dedent 
 61   
 62   
 63  try: 
 64      from test_suite.relax_test_loader import RelaxTestLoader as TestLoader 
 65  except ImportError: 
 66      from unittest import TestLoader 
 67   
 68   
 69   
 70   
 71   
 72  PY_FILE_EXTENSION='.py' 
 73   
 74   
 75   
 76   
 77   
 78   
 80      """Get the path of the directory the program started from. 
 81   
 82      The startup path is the first path in sys.path (the internal PYTHONPATH) by convention. If the 
 83      first element of sys.path is an empty trying the current working directory is used instead. 
 84   
 85      @return:    A file system path for the current operating system. 
 86      @rtype:     str 
 87      """ 
 88   
 89      startup_path = sys.path[0] 
 90      if startup_path == '': 
 91          startup_path = os.getcwd() 
 92      return startup_path 
  93   
 94   
 96      """Import the python module named by module_path. 
 97   
 98      @param module_path: A module path in python dot separated format.  Note: this currently doesn't 
 99                          support relative module paths as defined by pep328 and python 2.5. 
100      @type module_path:  str 
101      @return:            The module path as a list of module instances or None if the module path 
102                          cannot be found in the python path. 
103      @rtype:             list of class module instances or None 
104      """ 
105   
106      module = None 
107      result = None 
108   
109       
110      module = __import__(module_path) 
111       
112       
113       
114   
115      if module != None: 
116          result = [module] 
117          components = module_path.split('.') 
118          for component in components[1:]: 
119              module = getattr(module, component) 
120              result.append(module) 
121      return result 
 122   
123   
125      """Find the relative path of a module to one of a set of root paths using a list of package paths and a module name. 
126   
127      As the module may match more than one path the first path that can contain it is chosen. 
128   
129      @param package_path:    Path of a python packages leading to module_name. 
130      @type  package_path:    str 
131      @param module_name:     The name of the module to load. 
132      @type module_name:      str 
133      @keyword root_paths:    A set of paths to search for the module in.  If None is passed the list 
134                              is initialized from the internal PYTHONPATH sys.path.  Elements which 
135                              are empty strings are replace with the current working directory 
136                              sys.getcwd(). 
137      @type root_paths:       list of str 
138      @return:                A relative module path to one of the rootPaths which is separated by 
139                              '.'s if the modulePath is a subpath of one of the root paths, otherwise 
140                              None. 
141      @rtype:                 str or None 
142      """ 
143   
144      relative_path = None 
145      if root_paths == None: 
146          root_paths = sys.path 
147      for root_path in root_paths: 
148          root_path = segment_path(os.path.abspath(root_path)) 
149   
150           
151          if not isinstance(package_path, list): 
152              package_path = segment_path(os.path.abspath(package_path)) 
153   
154          common_prefix = get_common_prefix(root_path, package_path) 
155          if common_prefix == root_path: 
156              relative_path = package_path[len(common_prefix):] 
157              break 
158   
159      if relative_path != None: 
160          relative_path = '.'.join(relative_path) 
161   
162          if relative_path != '': 
163              relative_path = '.'.join((relative_path, module_name)) 
164          else: 
165              relative_path = module_name 
166   
167   
168   
169      return relative_path 
 170   
171   
173      """Get the common prefix between two paths. 
174   
175      @param path1:   The first path to be compared. 
176      @type path1:    list of str 
177      @param path2:   The second path to be compared. 
178      @type path2:    list of str 
179      @return:        The common path shared between the two paths starting from the root directory as 
180                      a list of segments.  If there is no common path an empty list is returned. 
181      @rtype:         list of str 
182      """ 
183   
184      result_path = [] 
185      for elem1, elem2 in map(None, path1, path2): 
186          if elem1 == None or elem2 == None: 
187              break 
188   
189          if elem1 == elem2: 
190            result_path.append(elem1) 
191      return result_path 
 192   
193   
195      """Segment a path into a list of components (drives, files, directories etc). 
196   
197      @param path:        The path to segment. 
198      @type path:         str 
199      @param normalise:   Whether to normalise the path before starting. 
200      @type normalise:    bool 
201      @return:            A list of path segments. 
202      @rtype:             list of str 
203      """ 
204   
205      if normalise: 
206          path = os.path.normpath(path) 
207   
208      result  = [] 
209      (head, tail) = os.path.split(path) 
210      if head =='' or tail == '': 
211          result.append(head+tail) 
212      else: 
213          while head != '' and tail != '': 
214              result.append(tail) 
215              head, tail = os.path.split(head) 
216          result.append(head+tail) 
217          result.reverse() 
218      return result 
 219   
220   
222      """Join a list of path segments (drives, files, directories etc) into a path. 
223   
224      @param segments:    The path segments to join into a path. 
225      @type segments:     a list of path segments 
226      @return:            The path containing the joined path segments. 
227      @rtype:             str 
228      """ 
229   
230      if len(segments) == 0: 
231          result = '' 
232      else: 
233          segments_copy = segments[:] 
234   
235          segments_copy.reverse() 
236   
237          result = segments_copy.pop() 
238          while len(segments_copy) > 0: 
239              result = os.path.join(result, segments_copy.pop()) 
240   
241      return result 
 242   
243   
244   
246      """TestCase class for nicely handling import errors.""" 
247   
248 -    def __init__(self, module_name, message): 
 249          """Set up the import error class. 
250   
251          @param module_name: The module which could not be imported. 
252          @type module_name:  str 
253          @param message:     The formatted traceback message (e.g. from traceback.format_exc()). 
254          @type message:      str 
255          """ 
256   
257           
258          super(ImportErrorTestCase, self).__init__('testImportError') 
259   
260           
261          self.module_name = module_name 
262          self.message = message 
 263   
264   
266          """Unit test module import.""" 
267   
268           
269          print("\nImport of the %s module.\n" % self.module_name) 
270   
271           
272          self.fail(self.message) 
  273   
274   
276      """Load a testCase from the file system using a package path, file name and class name. 
277   
278      @param package_path:    Full system path of the module file. 
279      @type package_path:     str 
280      @param module_name:     Name of the module to load the class from. 
281      @type module_name:      str 
282      @param class_name:      Name of the class to load. 
283      @type class_name:       str 
284      @return:                The suite of test cases. 
285      @rtype:                 TestSuite instance 
286      """ 
287   
288       
289      module = get_module_relative_path(package_path, module_name) 
290   
291       
292      try: 
293          packages = import_module(module) 
294      except: 
295           
296          suite = unittest.TestSuite() 
297   
298           
299          suite.addTest(ImportErrorTestCase(module, traceback.format_exc())) 
300   
301           
302          return suite 
303   
304       
305      if not packages: 
306          return 
307   
308       
309      if not hasattr(packages[-1], class_name): 
310          return 
311   
312       
313      clazz = getattr(packages[-1], class_name) 
314   
315       
316      return TestLoader().loadTestsFromTestCase(clazz) 
 317   
318   
319   
321      """Find and load unit test classes as a hierarchy of TestSuites and TestCases. 
322   
323      The class provides functions for running or returning the resulting TestSuite and requires a 
324      root directory to start searching from. 
325   
326      TestCases are identified by the class name matching a pattern (pattern_string). 
327      """ 
328   
329      suite = unittest.TestSuite() 
330      """The root test suite to which testSuites and cases are added.""" 
331   
332 -    def __init__(self, root_path=None, pattern_list=[]): 
 333          """Initialise the unit test finder. 
334   
335          @keyword root_path:     The path to starts searching for unit tests from, all sub 
336                                  directories and files are searched. 
337          @type root_path:        str 
338          @keyword pattern_list:  A list of regular expression patterns which identify a file as one 
339                                  containing a unit test TestCase. 
340          @type pattern_list:     list of str 
341          """ 
342   
343          self.root_path = root_path 
344          if self.root_path == None: 
345              self.root_path = get_startup_path() 
346          self.patterns=[] 
347          for pattern in pattern_list: 
348              self.patterns.append(re.compile(pattern)) 
349          self.paths_scanned = False 
 350   
351   
353          """Scan directories and paths for unit test classes and load them into TestSuites.""" 
354   
355           
356          self.suite = unittest.TestSuite() 
357   
358           
359          for (dir_path, dir_names, file_names) in os.walk(self.root_path): 
360               
361              for file_name in file_names: 
362                   
363                  module_found = False 
364                  for pattern in self.patterns: 
365                      if pattern.match(file_name): 
366                          module_found = True 
367                          break 
368   
369                   
370                  if not module_found: 
371                      continue 
372   
373                   
374                  module_name = os.path.splitext(file_name)[0] 
375                  class_name = string.upper(module_name[0]) + module_name[1:] 
376   
377                   
378                  test_case = load_test_case(dir_path, module_name, class_name) 
379                  if test_case != None: 
380                      self.suite.addTest(test_case) 
  381   
382   
383   
385      """Class to run a particular unit test or a directory of unit tests.""" 
386   
387       
388      system_path_pattern = ['test_suite' + os.sep + 'unit_tests', os.pardir + os.sep + os.pardir] 
389      """@ivar:   A search template for the directory in which relax is installed.  The directory which relax is installed in is viewed as the the 'PYTHONPATH' of the classes to be tested. It must be unique and defined relative to the test suite. For the current setup in relax this is (\'test_suite\', /'..\'). The first string is a directory structure to match the second string is a relative path from that directory to the system directory. The search is started from the value of root_path in the file system. 
390         @type:  list of str 
391      """ 
392   
393      unit_test_path_pattern = ['test_suite' + os.sep + 'unit_tests', os.curdir] 
394      """@ivar:   A search template for the directory from which all unit module directories descend. For the current setup in relax this is (\'unit_tests\', \'.\'). The search is started from the value of root_path in the file system. 
395         @type:   list of str 
396      """ 
397   
398      test_case_patterns = ['test_.*\.py$'] 
399      """@ivar:   A list of regex patterns against which files will be 
400                  tested to see if they are expected to contain unit tests. If 
401                  the file has the correct pattern the module contained inside the 
402                  file will be searched for testCases e.g in the case of test_float.py 
403                  the module to be searched for would be test_float.Test_float. 
404         @type:   list of str 
405      """ 
406   
407 -    def __init__(self, root_path=os.curdir, test_module=None, search_for_root_path=True, search_for_unit_test_path=True, verbose=False): 
 408          """Initialise the unit test runner. 
409   
410          @keyword root_path:                 Root path to start searching for modules to unit test from.  If the string contains '.' the search starts from the current working directory.  Default current working directory. 
411          @type root_path:                    str 
412          @keyword test_module:               The name of a module to unit test. If the variable is None a search for all unit tests using <test-pattern> will start from <root_path>, if the variable is '.' a search for all unit tests will commence from the current working directory, otherwise it will be used as a module path from the current root_path or CHECKME: ****module_directory_path****. The module name can be in the directory path format used by the current operating system or a unix style path with /'s including a final .py extension or a dotted moudle name. 
413          @type test_module:                  str 
414          @keyword search_for_root_path:      Whether to carry out a search from the root_directory using self.system_path_pattern to find the directory self.system_directory if no search is carried out self.system_directory is set to None and it is the responsibility of code creating the class to set it before self.run is called. 
415          @type search_for_root_path:         bool 
416          @keyword search_for_unit_test_path: Whether to carry out a search from the root_directory using self.unit_test_path_patter to find the directory self.unit_test_directory if no search is carried out self.unit_test_directory is set to None and it is the responsibility of code creating the class to set it before self.run is called. 
417          @type search_for_unit_test_path:    bool 
418          @keyword verbose:                   Produce verbose output during testing e.g. directories searched root directories etc. 
419          @type verbose:                      bool 
420          """ 
421   
422           
423          if root_path == os.curdir: 
424              root_path = os.getcwd() 
425   
426           
427          self.root_path = root_path 
428   
429           
430          if ((search_for_root_path) == True or (search_for_unit_test_path == True)) and verbose: 
431              print('searching for paths') 
432              print('-------------------') 
433              print('') 
434   
435           
436          if search_for_root_path: 
437              self.system_directory = self.get_first_instance_path(root_path, self.system_path_pattern[0], self.system_path_pattern[1]) 
438   
439              if self.system_directory == None: 
440                  raise Exception("can't find system directory start from %s" % root_path) 
441              else: 
442                  if verbose: 
443                      print(('search for system directory found:    %s' % self.system_directory)) 
444          else: 
445              self.system_directory = None 
446   
447          if search_for_unit_test_path: 
448              self.unit_test_directory = self.get_first_instance_path(root_path, self.unit_test_path_pattern[0], self.unit_test_path_pattern[1]) 
449              if self.unit_test_directory == None: 
450                  raise Exception("can't find unit test directory start from %s" % root_path) 
451              else: 
452                  if verbose: 
453                      print(('search for unit test directory found: %s' % self.unit_test_directory)) 
454          else: 
455              self.unit_test_directory = None 
456   
457           
458          if test_module == None: 
459              test_module = self.root_path 
460          elif test_module == os.curdir: 
461              test_module =  os.getcwd() 
462          elif test_module == self.TEST_SUITE_ROOT: 
463              test_module = self.unit_test_directory 
464   
465          self.test_module = test_module 
466   
467           
468          self.verbose = verbose 
 469   
470   
472          """Get the minimal path searching up the file system to target_directory. 
473   
474          The algorithm is that we repeatedly chop the end off path and see if the tail of the path matches target_path If it doesn't match we search in the resulting directory by appending target_path and seeing if it exists in the file system. Finally once the required directory structure has been found the offset_path is appended to the found path and the resulting path normalised. 
475   
476          Note the algorithm understands .. and . 
477   
478   
479          @param path:            A directory path to search up. 
480          @type path:             str 
481          @param target_path:     A directory to find in the path or below one of the elements in the path. 
482          @type target_path:      str 
483          @keyword offset_path:   A relative path offset to add to the path that has been found to give the result directory. 
484          @type offset_path:      str 
485          @return:                The path that has been found or None if the path cannot be found by walking up and analysing the current directory structure. 
486          @rtype:                 str 
487          """ 
488   
489          seg_path = segment_path(os.path.normpath(path)) 
490          seg_target_directory = segment_path(target_path) 
491          seg_target_directory_len = len(seg_target_directory) 
492   
493          found_seg_path = None 
494          while len(seg_path) > 0 and found_seg_path == None: 
495             if seg_path[-seg_target_directory_len:] == seg_target_directory[-seg_target_directory_len:]: 
496                 found_seg_path = seg_path 
497                 break 
498             else: 
499                 extended_seg_path = copy(seg_path) 
500                 extended_seg_path.extend(seg_target_directory) 
501                 if os.path.exists(os.path.join(*extended_seg_path)): 
502                     found_seg_path = extended_seg_path 
503                     break 
504   
505             seg_path.pop() 
506   
507          result = None 
508          if found_seg_path != None and len(found_seg_path) != 0: 
509              seg_offset_path = segment_path(offset_path) 
510              found_seg_path.extend(seg_offset_path) 
511              result = os.path.normpath(join_path_segments(found_seg_path)) 
512   
513          return result 
 514   
515   
517          """Determine the possible paths of the test_module. 
518   
519          It is assumed that the test_module can be either a path or a python module or package name including dots. 
520   
521          The following heuristics are used: 
522   
523              1. If the test_module=None add the value '.'. 
524              2. If the test_module ends with a PY_FILE_EXTENSION append test_module with the PY_FILE_EXTENSION removed. 
525              3. Add the module_name with .'s converted to /'s and any elements of the form PY_FILE_EXTENSION removed. 
526              4. Repeat 2 and 3 with the last element of the path repeated with the first letter capitalised. 
527   
528          Note: we can't deal with module methods... 
529   
530   
531          @return:    A set of possible module names in python '.' separated format. 
532          @rtype:     str 
533          """ 
534   
535          result = set() 
536   
537           
538          if test_module == None: 
539              result.add(os.curdir) 
540          else: 
541               
542              mpath = [] 
543              test_module_segments = segment_path(test_module) 
544              for elem in test_module_segments: 
545                  if elem.endswith(PY_FILE_EXTENSION): 
546                      mpath.append(os.path.splitext(elem)[0]) 
547                  else: 
548                      mpath.append(elem) 
549   
550              result.add(tuple(mpath)) 
551   
552              mpath = copy(mpath) 
553              mpath.append(mpath[-1].capitalize()) 
554              result.add(tuple(mpath)) 
555   
556              module_path_elems = test_module.split('.') 
557   
558              module_norm_path = [] 
559              for elem in module_path_elems: 
560                  if elem != PY_FILE_EXTENSION[1:]: 
561                      module_norm_path.append(elem) 
562   
563               
564               
565              elems_ok = True 
566              for elem in module_norm_path: 
567                  if len(segment_path(elem)) > 1: 
568                      elems_ok = False 
569                      break 
570   
571              if elems_ok: 
572                  result.add(tuple(module_norm_path)) 
573   
574                  mpath = copy(module_norm_path) 
575                  mpath.append(module_norm_path[-1].capitalize()) 
576                  result.add(tuple(mpath)) 
577   
578          return result 
 579   
580   
581 -    def run(self, runner=None): 
 582          """Run a unit test or set of unit tests. 
583   
584          @keyword runner:    A unit test runner such as TextTestRunner.  None indicates use of the default unit test runner.  For an example of how to write a test runner see the python documentation for TextTestRunner in the python source. 
585          @type runner:       Unit test runner instance (TextTestRunner, BaseGUITestRunner subclass, etc.) 
586          @return:            A string indicating success or failure of the unit tests run. 
587          @rtype:             str 
588          """ 
589   
590          msg = "Either set self.%s to a %s directory or set search_for_%s_path in self.__init__ to True" 
591          if self.unit_test_directory ==  None: 
592              raise Exception(msg % ('unit_test_directory', 'unit test', 'unit_test')) 
593          if self.system_directory == None: 
594              raise Exception(msg % ('system_directory', 'system', 'root')) 
595   
596           
597          if self.verbose: 
598              print('testing units...') 
599              print('----------------') 
600              print('') 
601   
602          module_paths = self.paths_from_test_module(self.test_module) 
603          if self.verbose: 
604              print(('root path:          ', self.root_path)) 
605              print(('system directory:   ', self.system_directory)) 
606              print(('unit test directory:', self.unit_test_directory)) 
607              print('') 
608              for i, elem in enumerate(module_paths): 
609                  print(('module path %d:  %s'  % (i, elem))) 
610              print('') 
611   
612           
613          sys.path.pop(0) 
614          sys.path.insert(0, self.system_directory) 
615   
616          tests = None 
617   
618           
619          for module_path in module_paths: 
620              module_string = os.path.join(*module_path) 
621   
622              if os.path.isdir(module_string): 
623                   
624                  finder = Test_finder(module_string, self.test_case_patterns) 
625                  finder.scan_paths() 
626                  tests = finder.suite 
627                  break 
628   
629           
630          if tests == None: 
631              for module_path in module_paths: 
632                  print(module_path) 
633                  path_len = len(module_path) 
634                  if path_len <= 1: 
635                      continue 
636                  elif path_len == 2: 
637                      print(('trying to load 2: ',  module_path[0], module_path[1])) 
638                      tests = load_test_case('', module_path[0], module_path[1]) 
639                  else: 
640                      print(('trying to load 3: ', os.path.join(*module_path[:-2]), module_path[-2], module_path[-1])) 
641                      tests = load_test_case(os.path.join(*module_path[:-2]), module_path[-2], module_path[-1]) 
642                  if tests != None: 
643                      break 
644   
645          if runner == None: 
646              runner = unittest.TextTestRunner() 
647   
648          if self.verbose: 
649              print('results') 
650              print('-------') 
651              print('') 
652   
653           
654          if tests != None and tests.countTestCases() != 0: 
655               
656              results = runner.run(tests) 
657              result_string = results.wasSuccessful() 
658   
659          elif tests == None: 
660              results = None 
661              result_string = 'Error: no test directories found for input module: %s' % self.test_module 
662              print(result_string) 
663          else: 
664              results = None 
665              result_string = 'note: no tests found for input module: %s' % self.test_module 
666              print(result_string) 
667   
668           
669          return result_string 
  670   
671   
672   
673   
674  if __name__ == '__main__': 
675       
676      parser = OptionParser() 
677      parser.add_option("-v", "--verbose", dest="verbose", help="verbose test ouput", default=False, action='store_true') 
678      parser.add_option("-u", "--system", dest="system_directory", help="path to relax top directory which contains test_suite", default=None) 
679      parser.add_option("-s", "--utest", dest="unit_test_directory", help="default unit test directory", default=None) 
680   
681       
682      usage = """ 
683      %%prog [options] [<file-or-dir>...] 
684   
685        a program to find and run subsets of the relax unit test suite using pyunit. 
686        (details of how to write pyunit tests can be found in your python distributions 
687        library reference) 
688   
689   
690      arguments: 
691        <file-or-dir> = <file-path> | <dir-path> is a list which can contain 
692                        inter-mixed directories and files 
693   
694        <file-path>  =  a file containing a test case class files of the same 
695                        name with the first letter capitalised 
696   
697                        e.g. maths_fns/test_chi2.py will be assumed to contain 
698                        a test case class called Test_chi2 
699   
700        <dir-path>   =  a path which will be recursivley searched for <file-path>s 
701                        which end in "*.py". 
702        """ 
703      parser.set_usage(dedent(usage)) 
704   
705       
706      (options, args) = parser.parse_args() 
707   
708       
709      search_system = True 
710      search_unit = True 
711   
712       
713      if options.system_directory != None: 
714          if not os.path.exists(options.system_directory): 
715              print("The path to the system directory doeesn't exist") 
716              print(("provided path: %s" % options.system_directory)) 
717              print("exiting...") 
718              sys.exit(0) 
719          search_system = False 
720   
721       
722      if options.unit_test_directory != None: 
723          if not os.path.exists(options.unit_test_directory): 
724              print("The path to the system directory doeesn't exist") 
725              print(("provided path: %s" % options.unit_test_directory)) 
726              print("exiting...") 
727              sys.exit(0) 
728          search_unit = False 
729   
730       
731      if len(args) < 1: 
732          args = [None] 
733   
734       
735      for arg in args: 
736           
737          runner = Unit_test_runner(test_module=arg, verbose=options.verbose, search_for_unit_test_path=search_unit, search_for_root_path=search_system) 
738   
739           
740          if not search_system: 
741              runner.system_directory = options.system_directory 
742   
743           
744          if not search_unit: 
745              runner.unit_test_directory = options.unit_test_directory 
746   
747           
748          runner.run() 
749