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