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