| Tor Norbye | 3a2425a | 2013-11-04 10:16:08 -0800 | [diff] [blame^] | 1 | import fnmatch |
| 2 | import os.path |
| 3 | import re |
| 4 | import sys |
| 5 | import unittest |
| 6 | |
| 7 | |
| 8 | |
| 9 | |
| 10 | try: |
| 11 | __setFalse = False |
| 12 | except: |
| 13 | import __builtin__ |
| 14 | setattr(__builtin__, 'True', 1) |
| 15 | setattr(__builtin__, 'False', 0) |
| 16 | |
| 17 | |
| 18 | |
| 19 | |
| 20 | #======================================================================================================================= |
| 21 | # Jython? |
| 22 | #======================================================================================================================= |
| 23 | try: |
| 24 | import org.python.core.PyDictionary #@UnresolvedImport @UnusedImport -- just to check if it could be valid |
| 25 | def DictContains(d, key): |
| 26 | return d.has_key(key) |
| 27 | except: |
| 28 | try: |
| 29 | #Py3k does not have has_key anymore, and older versions don't have __contains__ |
| 30 | DictContains = dict.__contains__ |
| 31 | except: |
| 32 | DictContains = dict.has_key |
| 33 | |
| 34 | try: |
| 35 | xrange |
| 36 | except: |
| 37 | #Python 3k does not have it |
| 38 | xrange = range |
| 39 | |
| 40 | try: |
| 41 | enumerate |
| 42 | except: |
| 43 | def enumerate(lst): |
| 44 | ret = [] |
| 45 | i=0 |
| 46 | for element in lst: |
| 47 | ret.append((i, element)) |
| 48 | i+=1 |
| 49 | return ret |
| 50 | |
| 51 | |
| 52 | |
| 53 | #======================================================================================================================= |
| 54 | # getopt code copied since gnu_getopt is not available on jython 2.1 |
| 55 | #======================================================================================================================= |
| 56 | class GetoptError(Exception): |
| 57 | opt = '' |
| 58 | msg = '' |
| 59 | def __init__(self, msg, opt=''): |
| 60 | self.msg = msg |
| 61 | self.opt = opt |
| 62 | Exception.__init__(self, msg, opt) |
| 63 | |
| 64 | def __str__(self): |
| 65 | return self.msg |
| 66 | |
| 67 | |
| 68 | def gnu_getopt(args, shortopts, longopts=[]): |
| 69 | """getopt(args, options[, long_options]) -> opts, args |
| 70 | |
| 71 | This function works like getopt(), except that GNU style scanning |
| 72 | mode is used by default. This means that option and non-option |
| 73 | arguments may be intermixed. The getopt() function stops |
| 74 | processing options as soon as a non-option argument is |
| 75 | encountered. |
| 76 | |
| 77 | If the first character of the option string is `+', or if the |
| 78 | environment variable POSIXLY_CORRECT is set, then option |
| 79 | processing stops as soon as a non-option argument is encountered. |
| 80 | """ |
| 81 | |
| 82 | opts = [] |
| 83 | prog_args = [] |
| 84 | if isinstance(longopts, ''.__class__): |
| 85 | longopts = [longopts] |
| 86 | else: |
| 87 | longopts = list(longopts) |
| 88 | |
| 89 | # Allow options after non-option arguments? |
| 90 | if shortopts.startswith('+'): |
| 91 | shortopts = shortopts[1:] |
| 92 | all_options_first = True |
| 93 | elif os.environ.get("POSIXLY_CORRECT"): |
| 94 | all_options_first = True |
| 95 | else: |
| 96 | all_options_first = False |
| 97 | |
| 98 | while args: |
| 99 | if args[0] == '--': |
| 100 | prog_args += args[1:] |
| 101 | break |
| 102 | |
| 103 | if args[0][:2] == '--': |
| 104 | opts, args = do_longs(opts, args[0][2:], longopts, args[1:]) |
| 105 | elif args[0][:1] == '-': |
| 106 | opts, args = do_shorts(opts, args[0][1:], shortopts, args[1:]) |
| 107 | else: |
| 108 | if all_options_first: |
| 109 | prog_args += args |
| 110 | break |
| 111 | else: |
| 112 | prog_args.append(args[0]) |
| 113 | args = args[1:] |
| 114 | |
| 115 | return opts, prog_args |
| 116 | |
| 117 | def do_longs(opts, opt, longopts, args): |
| 118 | try: |
| 119 | i = opt.index('=') |
| 120 | except ValueError: |
| 121 | optarg = None |
| 122 | else: |
| 123 | opt, optarg = opt[:i], opt[i + 1:] |
| 124 | |
| 125 | has_arg, opt = long_has_args(opt, longopts) |
| 126 | if has_arg: |
| 127 | if optarg is None: |
| 128 | if not args: |
| 129 | raise GetoptError('option --%s requires argument' % opt, opt) |
| 130 | optarg, args = args[0], args[1:] |
| 131 | elif optarg: |
| 132 | raise GetoptError('option --%s must not have an argument' % opt, opt) |
| 133 | opts.append(('--' + opt, optarg or '')) |
| 134 | return opts, args |
| 135 | |
| 136 | # Return: |
| 137 | # has_arg? |
| 138 | # full option name |
| 139 | def long_has_args(opt, longopts): |
| 140 | possibilities = [o for o in longopts if o.startswith(opt)] |
| 141 | if not possibilities: |
| 142 | raise GetoptError('option --%s not recognized' % opt, opt) |
| 143 | # Is there an exact match? |
| 144 | if opt in possibilities: |
| 145 | return False, opt |
| 146 | elif opt + '=' in possibilities: |
| 147 | return True, opt |
| 148 | # No exact match, so better be unique. |
| 149 | if len(possibilities) > 1: |
| 150 | # XXX since possibilities contains all valid continuations, might be |
| 151 | # nice to work them into the error msg |
| 152 | raise GetoptError('option --%s not a unique prefix' % opt, opt) |
| 153 | assert len(possibilities) == 1 |
| 154 | unique_match = possibilities[0] |
| 155 | has_arg = unique_match.endswith('=') |
| 156 | if has_arg: |
| 157 | unique_match = unique_match[:-1] |
| 158 | return has_arg, unique_match |
| 159 | |
| 160 | def do_shorts(opts, optstring, shortopts, args): |
| 161 | while optstring != '': |
| 162 | opt, optstring = optstring[0], optstring[1:] |
| 163 | if short_has_arg(opt, shortopts): |
| 164 | if optstring == '': |
| 165 | if not args: |
| 166 | raise GetoptError('option -%s requires argument' % opt, |
| 167 | opt) |
| 168 | optstring, args = args[0], args[1:] |
| 169 | optarg, optstring = optstring, '' |
| 170 | else: |
| 171 | optarg = '' |
| 172 | opts.append(('-' + opt, optarg)) |
| 173 | return opts, args |
| 174 | |
| 175 | def short_has_arg(opt, shortopts): |
| 176 | for i in range(len(shortopts)): |
| 177 | if opt == shortopts[i] != ':': |
| 178 | return shortopts.startswith(':', i + 1) |
| 179 | raise GetoptError('option -%s not recognized' % opt, opt) |
| 180 | |
| 181 | |
| 182 | #======================================================================================================================= |
| 183 | # End getopt code |
| 184 | #======================================================================================================================= |
| 185 | |
| 186 | |
| 187 | |
| 188 | |
| 189 | |
| 190 | |
| 191 | |
| 192 | |
| 193 | |
| 194 | |
| 195 | #======================================================================================================================= |
| 196 | # parse_cmdline |
| 197 | #======================================================================================================================= |
| 198 | def parse_cmdline(): |
| 199 | """ parses command line and returns test directories, verbosity, test filter and test suites |
| 200 | usage: |
| 201 | runfiles.py -v|--verbosity <level> -f|--filter <regex> -t|--tests <Test.test1,Test2> dirs|files |
| 202 | """ |
| 203 | verbosity = 2 |
| 204 | test_filter = None |
| 205 | tests = None |
| 206 | |
| 207 | optlist, dirs = gnu_getopt(sys.argv[1:], "v:f:t:", ["verbosity=", "filter=", "tests="]) |
| 208 | for opt, value in optlist: |
| 209 | if opt in ("-v", "--verbosity"): |
| 210 | verbosity = value |
| 211 | |
| 212 | elif opt in ("-f", "--filter"): |
| 213 | test_filter = value.split(',') |
| 214 | |
| 215 | elif opt in ("-t", "--tests"): |
| 216 | tests = value.split(',') |
| 217 | |
| 218 | if type([]) != type(dirs): |
| 219 | dirs = [dirs] |
| 220 | |
| 221 | ret_dirs = [] |
| 222 | for d in dirs: |
| 223 | if '|' in d: |
| 224 | #paths may come from the ide separated by | |
| 225 | ret_dirs.extend(d.split('|')) |
| 226 | else: |
| 227 | ret_dirs.append(d) |
| 228 | |
| 229 | return ret_dirs, int(verbosity), test_filter, tests |
| 230 | |
| 231 | |
| 232 | #======================================================================================================================= |
| 233 | # PydevTestRunner |
| 234 | #======================================================================================================================= |
| 235 | class PydevTestRunner: |
| 236 | """ finds and runs a file or directory of files as a unit test """ |
| 237 | |
| 238 | __py_extensions = ["*.py", "*.pyw"] |
| 239 | __exclude_files = ["__init__.*"] |
| 240 | |
| 241 | def __init__(self, test_dir, test_filter=None, verbosity=2, tests=None): |
| 242 | self.test_dir = test_dir |
| 243 | self.__adjust_path() |
| 244 | self.test_filter = self.__setup_test_filter(test_filter) |
| 245 | self.verbosity = verbosity |
| 246 | self.tests = tests |
| 247 | |
| 248 | |
| 249 | def __adjust_path(self): |
| 250 | """ add the current file or directory to the python path """ |
| 251 | path_to_append = None |
| 252 | for n in xrange(len(self.test_dir)): |
| 253 | dir_name = self.__unixify(self.test_dir[n]) |
| 254 | if os.path.isdir(dir_name): |
| 255 | if not dir_name.endswith("/"): |
| 256 | self.test_dir[n] = dir_name + "/" |
| 257 | path_to_append = os.path.normpath(dir_name) |
| 258 | elif os.path.isfile(dir_name): |
| 259 | path_to_append = os.path.dirname(dir_name) |
| 260 | else: |
| 261 | msg = ("unknown type. \n%s\nshould be file or a directory.\n" % (dir_name)) |
| 262 | raise RuntimeError(msg) |
| 263 | if path_to_append is not None: |
| 264 | #Add it as the last one (so, first things are resolved against the default dirs and |
| 265 | #if none resolves, then we try a relative import). |
| 266 | sys.path.append(path_to_append) |
| 267 | return |
| 268 | |
| 269 | def __setup_test_filter(self, test_filter): |
| 270 | """ turn a filter string into a list of filter regexes """ |
| 271 | if test_filter is None or len(test_filter) == 0: |
| 272 | return None |
| 273 | return [re.compile("test%s" % f) for f in test_filter] |
| 274 | |
| 275 | def __is_valid_py_file(self, fname): |
| 276 | """ tests that a particular file contains the proper file extension |
| 277 | and is not in the list of files to exclude """ |
| 278 | is_valid_fname = 0 |
| 279 | for invalid_fname in self.__class__.__exclude_files: |
| 280 | is_valid_fname += int(not fnmatch.fnmatch(fname, invalid_fname)) |
| 281 | if_valid_ext = 0 |
| 282 | for ext in self.__class__.__py_extensions: |
| 283 | if_valid_ext += int(fnmatch.fnmatch(fname, ext)) |
| 284 | return is_valid_fname > 0 and if_valid_ext > 0 |
| 285 | |
| 286 | def __unixify(self, s): |
| 287 | """ stupid windows. converts the backslash to forwardslash for consistency """ |
| 288 | return os.path.normpath(s).replace(os.sep, "/") |
| 289 | |
| 290 | def __importify(self, s, dir=False): |
| 291 | """ turns directory separators into dots and removes the ".py*" extension |
| 292 | so the string can be used as import statement """ |
| 293 | if not dir: |
| 294 | dirname, fname = os.path.split(s) |
| 295 | |
| 296 | if fname.count('.') > 1: |
| 297 | #if there's a file named xxx.xx.py, it is not a valid module, so, let's not load it... |
| 298 | return |
| 299 | |
| 300 | imp_stmt_pieces = [dirname.replace("\\", "/").replace("/", "."), os.path.splitext(fname)[0]] |
| 301 | |
| 302 | if len(imp_stmt_pieces[0]) == 0: |
| 303 | imp_stmt_pieces = imp_stmt_pieces[1:] |
| 304 | |
| 305 | return ".".join(imp_stmt_pieces) |
| 306 | |
| 307 | else: #handle dir |
| 308 | return s.replace("\\", "/").replace("/", ".") |
| 309 | |
| 310 | def __add_files(self, pyfiles, root, files): |
| 311 | """ if files match, appends them to pyfiles. used by os.path.walk fcn """ |
| 312 | for fname in files: |
| 313 | if self.__is_valid_py_file(fname): |
| 314 | name_without_base_dir = self.__unixify(os.path.join(root, fname)) |
| 315 | pyfiles.append(name_without_base_dir) |
| 316 | return |
| 317 | |
| 318 | |
| 319 | def find_import_files(self): |
| 320 | """ return a list of files to import """ |
| 321 | pyfiles = [] |
| 322 | |
| 323 | for base_dir in self.test_dir: |
| 324 | if os.path.isdir(base_dir): |
| 325 | if hasattr(os, 'walk'): |
| 326 | for root, dirs, files in os.walk(base_dir): |
| 327 | self.__add_files(pyfiles, root, files) |
| 328 | else: |
| 329 | # jython2.1 is too old for os.walk! |
| 330 | os.path.walk(base_dir, self.__add_files, pyfiles) |
| 331 | |
| 332 | elif os.path.isfile(base_dir): |
| 333 | pyfiles.append(base_dir) |
| 334 | |
| 335 | return pyfiles |
| 336 | |
| 337 | def __get_module_from_str(self, modname, print_exception): |
| 338 | """ Import the module in the given import path. |
| 339 | * Returns the "final" module, so importing "coilib40.subject.visu" |
| 340 | returns the "visu" module, not the "coilib40" as returned by __import__ """ |
| 341 | try: |
| 342 | mod = __import__(modname) |
| 343 | for part in modname.split('.')[1:]: |
| 344 | mod = getattr(mod, part) |
| 345 | return mod |
| 346 | except: |
| 347 | if print_exception: |
| 348 | import traceback;traceback.print_exc() |
| 349 | sys.stderr.write('ERROR: Module: %s could not be imported.\n' % (modname,)) |
| 350 | return None |
| 351 | |
| 352 | def find_modules_from_files(self, pyfiles): |
| 353 | """ returns a lisst of modules given a list of files """ |
| 354 | #let's make sure that the paths we want are in the pythonpath... |
| 355 | imports = [self.__importify(s) for s in pyfiles] |
| 356 | |
| 357 | system_paths = [] |
| 358 | for s in sys.path: |
| 359 | system_paths.append(self.__importify(s, True)) |
| 360 | |
| 361 | |
| 362 | ret = [] |
| 363 | for imp in imports: |
| 364 | if imp is None: |
| 365 | continue #can happen if a file is not a valid module |
| 366 | choices = [] |
| 367 | for s in system_paths: |
| 368 | if imp.startswith(s): |
| 369 | add = imp[len(s) + 1:] |
| 370 | if add: |
| 371 | choices.append(add) |
| 372 | #sys.stdout.write(' ' + add + ' ') |
| 373 | |
| 374 | if not choices: |
| 375 | sys.stdout.write('PYTHONPATH not found for file: %s\n' % imp) |
| 376 | else: |
| 377 | for i, import_str in enumerate(choices): |
| 378 | mod = self.__get_module_from_str(import_str, print_exception=i == len(choices) - 1) |
| 379 | if mod is not None: |
| 380 | ret.append(mod) |
| 381 | break |
| 382 | |
| 383 | |
| 384 | return ret |
| 385 | |
| 386 | def find_tests_from_modules(self, modules): |
| 387 | """ returns the unittests given a list of modules """ |
| 388 | loader = unittest.TestLoader() |
| 389 | |
| 390 | ret = [] |
| 391 | if self.tests: |
| 392 | accepted_classes = {} |
| 393 | accepted_methods = {} |
| 394 | |
| 395 | for t in self.tests: |
| 396 | splitted = t.split('.') |
| 397 | if len(splitted) == 1: |
| 398 | accepted_classes[t] = t |
| 399 | |
| 400 | elif len(splitted) == 2: |
| 401 | accepted_methods[t] = t |
| 402 | |
| 403 | #=========================================================================================================== |
| 404 | # GetTestCaseNames |
| 405 | #=========================================================================================================== |
| 406 | class GetTestCaseNames: |
| 407 | """Yes, we need a class for that (cannot use outer context on jython 2.1)""" |
| 408 | |
| 409 | def __init__(self, accepted_classes, accepted_methods): |
| 410 | self.accepted_classes = accepted_classes |
| 411 | self.accepted_methods = accepted_methods |
| 412 | |
| 413 | def __call__(self, testCaseClass): |
| 414 | """Return a sorted sequence of method names found within testCaseClass""" |
| 415 | testFnNames = [] |
| 416 | className = testCaseClass.__name__ |
| 417 | |
| 418 | if DictContains(self.accepted_classes, className): |
| 419 | for attrname in dir(testCaseClass): |
| 420 | #If a class is chosen, we select all the 'test' methods' |
| 421 | if attrname.startswith('test') and hasattr(getattr(testCaseClass, attrname), '__call__'): |
| 422 | testFnNames.append(attrname) |
| 423 | |
| 424 | else: |
| 425 | for attrname in dir(testCaseClass): |
| 426 | #If we have the class+method name, we must do a full check and have an exact match. |
| 427 | if DictContains(self.accepted_methods, className + '.' + attrname): |
| 428 | if hasattr(getattr(testCaseClass, attrname), '__call__'): |
| 429 | testFnNames.append(attrname) |
| 430 | |
| 431 | #sorted() is not available in jython 2.1 |
| 432 | testFnNames.sort() |
| 433 | return testFnNames |
| 434 | |
| 435 | |
| 436 | loader.getTestCaseNames = GetTestCaseNames(accepted_classes, accepted_methods) |
| 437 | |
| 438 | |
| 439 | ret.extend([loader.loadTestsFromModule(m) for m in modules]) |
| 440 | |
| 441 | return ret |
| 442 | |
| 443 | |
| 444 | def filter_tests(self, test_objs): |
| 445 | """ based on a filter name, only return those tests that have |
| 446 | the test case names that match """ |
| 447 | test_suite = [] |
| 448 | for test_obj in test_objs: |
| 449 | |
| 450 | if isinstance(test_obj, unittest.TestSuite): |
| 451 | if test_obj._tests: |
| 452 | test_obj._tests = self.filter_tests(test_obj._tests) |
| 453 | if test_obj._tests: |
| 454 | test_suite.append(test_obj) |
| 455 | |
| 456 | elif isinstance(test_obj, unittest.TestCase): |
| 457 | test_cases = [] |
| 458 | for tc in test_objs: |
| 459 | try: |
| 460 | testMethodName = tc._TestCase__testMethodName |
| 461 | except AttributeError: |
| 462 | #changed in python 2.5 |
| 463 | testMethodName = tc._testMethodName |
| 464 | |
| 465 | if self.__match(self.test_filter, testMethodName) and self.__match_tests(self.tests, tc, testMethodName): |
| 466 | test_cases.append(tc) |
| 467 | return test_cases |
| 468 | return test_suite |
| 469 | |
| 470 | |
| 471 | def __match_tests(self, tests, test_case, test_method_name): |
| 472 | if not tests: |
| 473 | return 1 |
| 474 | |
| 475 | for t in tests: |
| 476 | class_and_method = t.split('.') |
| 477 | if len(class_and_method) == 1: |
| 478 | #only class name |
| 479 | if class_and_method[0] == test_case.__class__.__name__: |
| 480 | return 1 |
| 481 | |
| 482 | elif len(class_and_method) == 2: |
| 483 | if class_and_method[0] == test_case.__class__.__name__ and class_and_method[1] == test_method_name: |
| 484 | return 1 |
| 485 | |
| 486 | return 0 |
| 487 | |
| 488 | |
| 489 | |
| 490 | |
| 491 | def __match(self, filter_list, name): |
| 492 | """ returns whether a test name matches the test filter """ |
| 493 | if filter_list is None: |
| 494 | return 1 |
| 495 | for f in filter_list: |
| 496 | if re.match(f, name): |
| 497 | return 1 |
| 498 | return 0 |
| 499 | |
| 500 | |
| 501 | def run_tests(self): |
| 502 | """ runs all tests """ |
| 503 | sys.stdout.write("Finding files...\n") |
| 504 | files = self.find_import_files() |
| 505 | sys.stdout.write('%s %s\n' % (self.test_dir, '... done')) |
| 506 | sys.stdout.write("Importing test modules ... ") |
| 507 | modules = self.find_modules_from_files(files) |
| 508 | sys.stdout.write("done.\n") |
| 509 | all_tests = self.find_tests_from_modules(modules) |
| 510 | if self.test_filter or self.tests: |
| 511 | |
| 512 | if self.test_filter: |
| 513 | sys.stdout.write('Test Filter: %s' % ([p.pattern for p in self.test_filter],)) |
| 514 | |
| 515 | if self.tests: |
| 516 | sys.stdout.write('Tests to run: %s' % (self.tests,)) |
| 517 | |
| 518 | all_tests = self.filter_tests(all_tests) |
| 519 | |
| 520 | sys.stdout.write('\n') |
| 521 | runner = unittest.TextTestRunner(stream=sys.stdout, descriptions=1, verbosity=verbosity) |
| 522 | runner.run(unittest.TestSuite(all_tests)) |
| 523 | return |
| 524 | |
| 525 | #======================================================================================================================= |
| 526 | # main |
| 527 | #======================================================================================================================= |
| 528 | if __name__ == '__main__': |
| 529 | dirs, verbosity, test_filter, tests = parse_cmdline() |
| 530 | PydevTestRunner(dirs, test_filter, verbosity, tests).run_tests() |