blob: 4a25469c1fec921850514f4a960d2879bd4361f9 [file] [log] [blame]
Tor Norbye3a2425a2013-11-04 10:16:08 -08001import fnmatch
2import os.path
3import re
4import sys
5import unittest
6
7
8
9
10try:
11 __setFalse = False
12except:
13 import __builtin__
14 setattr(__builtin__, 'True', 1)
15 setattr(__builtin__, 'False', 0)
16
17
18
19
20#=======================================================================================================================
21# Jython?
22#=======================================================================================================================
23try:
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)
27except:
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
34try:
35 xrange
36except:
37 #Python 3k does not have it
38 xrange = range
39
40try:
41 enumerate
42except:
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#=======================================================================================================================
56class 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
68def 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
117def 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
139def 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
160def 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
175def 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#=======================================================================================================================
198def 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#=======================================================================================================================
235class 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#=======================================================================================================================
528if __name__ == '__main__':
529 dirs, verbosity, test_filter, tests = parse_cmdline()
530 PydevTestRunner(dirs, test_filter, verbosity, tests).run_tests()