##// END OF EJS Templates
tests: tidy up reporting of skipped tests...
Matt Mackall -
r5470:8374f3f0 default
parent child Browse files
Show More
@@ -1,558 +1,578 b''
1 1 #!/usr/bin/env python
2 2 #
3 3 # run-tests.py - Run a set of tests on Mercurial
4 4 #
5 5 # Copyright 2006 Matt Mackall <mpm@selenic.com>
6 6 #
7 7 # This software may be used and distributed according to the terms
8 8 # of the GNU General Public License, incorporated herein by reference.
9 9
10 10 import difflib
11 11 import errno
12 12 import optparse
13 13 import os
14 14 import popen2
15 15 import re
16 16 import shutil
17 17 import signal
18 18 import sys
19 19 import tempfile
20 20 import time
21 21
22 22 # hghave reserved exit code to skip test
23 23 SKIPPED_STATUS = 80
24 24
25 25 required_tools = ["python", "diff", "grep", "unzip", "gunzip", "bunzip2", "sed"]
26 26
27 27 parser = optparse.OptionParser("%prog [options] [tests]")
28 28 parser.add_option("-C", "--annotate", action="store_true",
29 29 help="output files annotated with coverage")
30 30 parser.add_option("--child", type="int",
31 31 help="run as child process, summary to given fd")
32 32 parser.add_option("-c", "--cover", action="store_true",
33 33 help="print a test coverage report")
34 34 parser.add_option("-f", "--first", action="store_true",
35 35 help="exit on the first test failure")
36 36 parser.add_option("-i", "--interactive", action="store_true",
37 37 help="prompt to accept changed output")
38 38 parser.add_option("-j", "--jobs", type="int",
39 39 help="number of jobs to run in parallel")
40 40 parser.add_option("-R", "--restart", action="store_true",
41 41 help="restart at last error")
42 42 parser.add_option("-p", "--port", type="int",
43 43 help="port on which servers should listen")
44 44 parser.add_option("-r", "--retest", action="store_true",
45 45 help="retest failed tests")
46 46 parser.add_option("-s", "--cover_stdlib", action="store_true",
47 47 help="print a test coverage report inc. standard libraries")
48 48 parser.add_option("-t", "--timeout", type="int",
49 49 help="kill errant tests after TIMEOUT seconds")
50 50 parser.add_option("--tmpdir", type="string",
51 51 help="run tests in the given temporary directory")
52 52 parser.add_option("-v", "--verbose", action="store_true",
53 53 help="output verbose messages")
54 54 parser.add_option("--with-hg", type="string",
55 55 help="test existing install at given location")
56 56
57 57 parser.set_defaults(jobs=1, port=20059, timeout=180)
58 58 (options, args) = parser.parse_args()
59 59 verbose = options.verbose
60 60 coverage = options.cover or options.cover_stdlib or options.annotate
61 61 python = sys.executable
62 62
63 63 if options.jobs < 1:
64 64 print >> sys.stderr, 'ERROR: -j/--jobs must be positive'
65 65 sys.exit(1)
66 66 if options.interactive and options.jobs > 1:
67 67 print >> sys.stderr, 'ERROR: cannot mix -interactive and --jobs > 1'
68 68 sys.exit(1)
69 69
70 70 def vlog(*msg):
71 71 if verbose:
72 72 for m in msg:
73 73 print m,
74 74 print
75 75
76 76 def splitnewlines(text):
77 77 '''like str.splitlines, but only split on newlines.
78 78 keep line endings.'''
79 79 i = 0
80 80 lines = []
81 81 while True:
82 82 n = text.find('\n', i)
83 83 if n == -1:
84 84 last = text[i:]
85 85 if last:
86 86 lines.append(last)
87 87 return lines
88 88 lines.append(text[i:n+1])
89 89 i = n + 1
90 90
91 91 def extract_missing_features(lines):
92 92 '''Extract missing/unknown features log lines as a list'''
93 93 missing = []
94 94 for line in lines:
95 95 if not line.startswith('hghave: '):
96 96 continue
97 97 line = line.splitlines()[0]
98 98 missing.append(line[8:])
99 99
100 100 return missing
101 101
102 102 def show_diff(expected, output):
103 103 for line in difflib.unified_diff(expected, output,
104 104 "Expected output", "Test output"):
105 105 sys.stdout.write(line)
106 106
107 107 def find_program(program):
108 108 """Search PATH for a executable program"""
109 109 for p in os.environ.get('PATH', os.defpath).split(os.pathsep):
110 110 name = os.path.join(p, program)
111 111 if os.access(name, os.X_OK):
112 112 return name
113 113 return None
114 114
115 115 def check_required_tools():
116 116 # Before we go any further, check for pre-requisite tools
117 117 # stuff from coreutils (cat, rm, etc) are not tested
118 118 for p in required_tools:
119 119 if os.name == 'nt':
120 120 p += '.exe'
121 121 found = find_program(p)
122 122 if found:
123 123 vlog("# Found prerequisite", p, "at", found)
124 124 else:
125 125 print "WARNING: Did not find prerequisite tool: "+p
126 126
127 127 def cleanup_exit():
128 128 if verbose:
129 129 print "# Cleaning up HGTMP", HGTMP
130 130 shutil.rmtree(HGTMP, True)
131 131
132 132 def use_correct_python():
133 133 # some tests run python interpreter. they must use same
134 134 # interpreter we use or bad things will happen.
135 135 exedir, exename = os.path.split(sys.executable)
136 136 if exename == 'python':
137 137 path = find_program('python')
138 138 if os.path.dirname(path) == exedir:
139 139 return
140 140 vlog('# Making python executable in test path use correct Python')
141 141 my_python = os.path.join(BINDIR, 'python')
142 142 try:
143 143 os.symlink(sys.executable, my_python)
144 144 except AttributeError:
145 145 # windows fallback
146 146 shutil.copyfile(sys.executable, my_python)
147 147 shutil.copymode(sys.executable, my_python)
148 148
149 149 def install_hg():
150 150 global python
151 151 vlog("# Performing temporary installation of HG")
152 152 installerrs = os.path.join("tests", "install.err")
153 153
154 154 # Run installer in hg root
155 155 os.chdir(os.path.join(os.path.dirname(sys.argv[0]), '..'))
156 156 cmd = ('%s setup.py clean --all'
157 157 ' install --force --home="%s" --install-lib="%s"'
158 158 ' --install-scripts="%s" >%s 2>&1'
159 159 % (sys.executable, INST, PYTHONDIR, BINDIR, installerrs))
160 160 vlog("# Running", cmd)
161 161 if os.system(cmd) == 0:
162 162 if not verbose:
163 163 os.remove(installerrs)
164 164 else:
165 165 f = open(installerrs)
166 166 for line in f:
167 167 print line,
168 168 f.close()
169 169 sys.exit(1)
170 170 os.chdir(TESTDIR)
171 171
172 172 os.environ["PATH"] = "%s%s%s" % (BINDIR, os.pathsep, os.environ["PATH"])
173 173
174 174 pydir = os.pathsep.join([PYTHONDIR, TESTDIR])
175 175 pythonpath = os.environ.get("PYTHONPATH")
176 176 if pythonpath:
177 177 pythonpath = pydir + os.pathsep + pythonpath
178 178 else:
179 179 pythonpath = pydir
180 180 os.environ["PYTHONPATH"] = pythonpath
181 181
182 182 use_correct_python()
183 183
184 184 if coverage:
185 185 vlog("# Installing coverage wrapper")
186 186 os.environ['COVERAGE_FILE'] = COVERAGE_FILE
187 187 if os.path.exists(COVERAGE_FILE):
188 188 os.unlink(COVERAGE_FILE)
189 189 # Create a wrapper script to invoke hg via coverage.py
190 190 os.rename(os.path.join(BINDIR, "hg"), os.path.join(BINDIR, "_hg.py"))
191 191 f = open(os.path.join(BINDIR, 'hg'), 'w')
192 192 f.write('#!' + sys.executable + '\n')
193 193 f.write('import sys, os; os.execv(sys.executable, [sys.executable, '
194 194 '"%s", "-x", "%s"] + sys.argv[1:])\n' %
195 195 (os.path.join(TESTDIR, 'coverage.py'),
196 196 os.path.join(BINDIR, '_hg.py')))
197 197 f.close()
198 198 os.chmod(os.path.join(BINDIR, 'hg'), 0700)
199 199 python = '"%s" "%s" -x' % (sys.executable,
200 200 os.path.join(TESTDIR,'coverage.py'))
201 201
202 202 def output_coverage():
203 203 vlog("# Producing coverage report")
204 204 omit = [BINDIR, TESTDIR, PYTHONDIR]
205 205 if not options.cover_stdlib:
206 206 # Exclude as system paths (ignoring empty strings seen on win)
207 207 omit += [x for x in sys.path if x != '']
208 208 omit = ','.join(omit)
209 209 os.chdir(PYTHONDIR)
210 210 cmd = '"%s" "%s" -i -r "--omit=%s"' % (
211 211 sys.executable, os.path.join(TESTDIR, 'coverage.py'), omit)
212 212 vlog("# Running: "+cmd)
213 213 os.system(cmd)
214 214 if options.annotate:
215 215 adir = os.path.join(TESTDIR, 'annotated')
216 216 if not os.path.isdir(adir):
217 217 os.mkdir(adir)
218 218 cmd = '"%s" "%s" -i -a "--directory=%s" "--omit=%s"' % (
219 219 sys.executable, os.path.join(TESTDIR, 'coverage.py'),
220 220 adir, omit)
221 221 vlog("# Running: "+cmd)
222 222 os.system(cmd)
223 223
224 224 class Timeout(Exception):
225 225 pass
226 226
227 227 def alarmed(signum, frame):
228 228 raise Timeout
229 229
230 230 def run(cmd):
231 231 """Run command in a sub-process, capturing the output (stdout and stderr).
232 232 Return the exist code, and output."""
233 233 # TODO: Use subprocess.Popen if we're running on Python 2.4
234 234 if os.name == 'nt':
235 235 tochild, fromchild = os.popen4(cmd)
236 236 tochild.close()
237 237 output = fromchild.read()
238 238 ret = fromchild.close()
239 239 if ret == None:
240 240 ret = 0
241 241 else:
242 242 proc = popen2.Popen4(cmd)
243 243 try:
244 244 output = ''
245 245 proc.tochild.close()
246 246 output = proc.fromchild.read()
247 247 ret = proc.wait()
248 248 if os.WIFEXITED(ret):
249 249 ret = os.WEXITSTATUS(ret)
250 250 except Timeout:
251 251 vlog('# Process %d timed out - killing it' % proc.pid)
252 252 os.kill(proc.pid, signal.SIGTERM)
253 253 ret = proc.wait()
254 254 if ret == 0:
255 255 ret = signal.SIGTERM << 8
256 256 output += ("\n### Abort: timeout after %d seconds.\n"
257 257 % options.timeout)
258 258 return ret, splitnewlines(output)
259 259
260 def run_one(test):
260 def run_one(test, skips):
261 261 '''tristate output:
262 262 None -> skipped
263 263 True -> passed
264 264 False -> failed'''
265 265
266 def skip(msg):
267 if not verbose:
268 skips.append((test, msg))
269 sys.stdout.write('s')
270 sys.stdout.flush()
271 else:
272 print "\nSkipping %s: %s" % (test, msg)
273 return None
274
266 275 vlog("# Test", test)
267 if not verbose:
268 sys.stdout.write('.')
269 sys.stdout.flush()
270 276
271 277 # create a fresh hgrc
272 278 hgrc = file(HGRCPATH, 'w+')
273 279 hgrc.write('[ui]\n')
274 280 hgrc.write('slash = True\n')
275 281 hgrc.close()
276 282
277 283 err = os.path.join(TESTDIR, test+".err")
278 284 ref = os.path.join(TESTDIR, test+".out")
279 285 testpath = os.path.join(TESTDIR, test)
280 286
281 287 if os.path.exists(err):
282 288 os.remove(err) # Remove any previous output files
283 289
284 290 # Make a tmp subdirectory to work in
285 291 tmpd = os.path.join(HGTMP, test)
286 292 os.mkdir(tmpd)
287 293 os.chdir(tmpd)
288 294
289 295 try:
290 296 tf = open(testpath)
291 297 firstline = tf.readline().rstrip()
292 298 tf.close()
293 299 except:
294 300 firstline = ''
295 301 lctest = test.lower()
296 302
297 303 if lctest.endswith('.py') or firstline == '#!/usr/bin/env python':
298 304 cmd = '%s "%s"' % (python, testpath)
299 305 elif lctest.endswith('.bat'):
300 306 # do not run batch scripts on non-windows
301 307 if os.name != 'nt':
302 print '\nSkipping %s: batch script' % test
303 return None
308 return skip("batch script")
304 309 # To reliably get the error code from batch files on WinXP,
305 310 # the "cmd /c call" prefix is needed. Grrr
306 311 cmd = 'cmd /c call "%s"' % testpath
307 312 else:
308 313 # do not run shell scripts on windows
309 314 if os.name == 'nt':
310 print '\nSkipping %s: shell script' % test
311 return None
315 return skip("shell script")
312 316 # do not try to run non-executable programs
313 317 if not os.access(testpath, os.X_OK):
314 print '\nSkipping %s: not executable' % test
315 return None
318 return skip("not executable")
316 319 cmd = '"%s"' % testpath
317 320
318 321 if options.timeout > 0:
319 322 signal.alarm(options.timeout)
320 323
321 324 vlog("# Running", cmd)
322 325 ret, out = run(cmd)
323 326 vlog("# Ret was:", ret)
324 327
325 328 if options.timeout > 0:
326 329 signal.alarm(0)
327 330
328 331 skipped = (ret == SKIPPED_STATUS)
329 332 diffret = 0
330 333 # If reference output file exists, check test output against it
331 334 if os.path.exists(ref):
332 335 f = open(ref, "r")
333 336 ref_out = splitnewlines(f.read())
334 337 f.close()
335 338 else:
336 339 ref_out = []
337 340 if not skipped and out != ref_out:
338 341 diffret = 1
339 342 print "\nERROR: %s output changed" % (test)
340 343 show_diff(ref_out, out)
341 344 if skipped:
342 345 missing = extract_missing_features(out)
343 346 if not missing:
344 347 missing = ['irrelevant']
345 print '\nSkipping %s: %s' % (test, missing[-1])
348 skip(missing[-1])
346 349 elif ret:
347 350 print "\nERROR: %s failed with error code %d" % (test, ret)
348 351 elif diffret:
349 352 ret = diffret
350 353
354 if not verbose:
355 sys.stdout.write('.')
356 sys.stdout.flush()
357
351 358 if ret != 0 and not skipped:
352 359 # Save errors to a file for diagnosis
353 360 f = open(err, "wb")
354 361 for line in out:
355 362 f.write(line)
356 363 f.close()
357 364
358 365 # Kill off any leftover daemon processes
359 366 try:
360 367 fp = file(DAEMON_PIDS)
361 368 for line in fp:
362 369 try:
363 370 pid = int(line)
364 371 except ValueError:
365 372 continue
366 373 try:
367 374 os.kill(pid, 0)
368 375 vlog('# Killing daemon process %d' % pid)
369 376 os.kill(pid, signal.SIGTERM)
370 377 time.sleep(0.25)
371 378 os.kill(pid, 0)
372 379 vlog('# Daemon process %d is stuck - really killing it' % pid)
373 380 os.kill(pid, signal.SIGKILL)
374 381 except OSError, err:
375 382 if err.errno != errno.ESRCH:
376 383 raise
377 384 fp.close()
378 385 os.unlink(DAEMON_PIDS)
379 386 except IOError:
380 387 pass
381 388
382 389 os.chdir(TESTDIR)
383 390 shutil.rmtree(tmpd, True)
384 391 if skipped:
385 392 return None
386 393 return ret == 0
387 394
388 395 if not options.child:
389 396 os.umask(022)
390 397
391 398 check_required_tools()
392 399
393 400 # Reset some environment variables to well-known values so that
394 401 # the tests produce repeatable output.
395 402 os.environ['LANG'] = os.environ['LC_ALL'] = 'C'
396 403 os.environ['TZ'] = 'GMT'
397 404
398 405 TESTDIR = os.environ["TESTDIR"] = os.getcwd()
399 406 HGTMP = os.environ['HGTMP'] = tempfile.mkdtemp('', 'hgtests.', options.tmpdir)
400 407 DAEMON_PIDS = None
401 408 HGRCPATH = None
402 409
403 410 os.environ["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"'
404 411 os.environ["HGMERGE"] = ('python "%s" -L my -L other'
405 412 % os.path.join(TESTDIR, os.path.pardir,
406 413 'contrib', 'simplemerge'))
407 414 os.environ["HGUSER"] = "test"
408 415 os.environ["HGENCODING"] = "ascii"
409 416 os.environ["HGENCODINGMODE"] = "strict"
410 417 os.environ["HGPORT"] = str(options.port)
411 418 os.environ["HGPORT1"] = str(options.port + 1)
412 419 os.environ["HGPORT2"] = str(options.port + 2)
413 420
414 421 if options.with_hg:
415 422 INST = options.with_hg
416 423 else:
417 424 INST = os.path.join(HGTMP, "install")
418 425 BINDIR = os.path.join(INST, "bin")
419 426 PYTHONDIR = os.path.join(INST, "lib", "python")
420 427 COVERAGE_FILE = os.path.join(TESTDIR, ".coverage")
421 428
422 429 def run_children(tests):
423 430 if not options.with_hg:
424 431 install_hg()
425 432
426 433 optcopy = dict(options.__dict__)
427 434 optcopy['jobs'] = 1
428 435 optcopy['with_hg'] = INST
429 436 opts = []
430 437 for opt, value in optcopy.iteritems():
431 438 name = '--' + opt.replace('_', '-')
432 439 if value is True:
433 440 opts.append(name)
434 441 elif value is not None:
435 442 opts.append(name + '=' + str(value))
436 443
437 444 tests.reverse()
438 445 jobs = [[] for j in xrange(options.jobs)]
439 446 while tests:
440 447 for j in xrange(options.jobs):
441 448 if not tests: break
442 449 jobs[j].append(tests.pop())
443 450 fps = {}
444 451 for j in xrange(len(jobs)):
445 452 job = jobs[j]
446 453 if not job:
447 454 continue
448 455 rfd, wfd = os.pipe()
449 456 childopts = ['--child=%d' % wfd, '--port=%d' % (options.port + j * 3)]
450 457 cmdline = [python, sys.argv[0]] + opts + childopts + job
451 458 vlog(' '.join(cmdline))
452 459 fps[os.spawnvp(os.P_NOWAIT, cmdline[0], cmdline)] = os.fdopen(rfd, 'r')
453 460 os.close(wfd)
454 461 failures = 0
455 462 tested, skipped, failed = 0, 0, 0
463 skips = []
456 464 while fps:
457 465 pid, status = os.wait()
458 466 fp = fps.pop(pid)
459 test, skip, fail = map(int, fp.read().splitlines())
467 l = fp.read().splitlines()
468 test, skip, fail = map(int, l[:3])
469 for s in l[3:]:
470 skips.append(s.split(" ", 1))
460 471 tested += test
461 472 skipped += skip
462 473 failed += fail
463 474 vlog('pid %d exited, status %d' % (pid, status))
464 475 failures |= status
465 print "\n# Ran %d tests, %d skipped, %d failed." % (
476 print
477 for s in skips:
478 print "Skipped %s: %s" % (s[0], s[1])
479 print "# Ran %d tests, %d skipped, %d failed." % (
466 480 tested, skipped, failed)
467 481 sys.exit(failures != 0)
468 482
469 483 def run_tests(tests):
470 484 global DAEMON_PIDS, HGRCPATH
471 485 DAEMON_PIDS = os.environ["DAEMON_PIDS"] = os.path.join(HGTMP, 'daemon.pids')
472 486 HGRCPATH = os.environ["HGRCPATH"] = os.path.join(HGTMP, '.hgrc')
473 487
474 488 try:
475 489 if not options.with_hg:
476 490 install_hg()
477 491
478 492 if options.timeout > 0:
479 493 try:
480 494 signal.signal(signal.SIGALRM, alarmed)
481 495 vlog('# Running tests with %d-second timeout' %
482 496 options.timeout)
483 497 except AttributeError:
484 498 print 'WARNING: cannot run tests with timeouts'
485 499 options.timeout = 0
486 500
487 501 tested = 0
488 502 failed = 0
489 503 skipped = 0
490 504
491 505 if options.restart:
492 506 orig = list(tests)
493 507 while tests:
494 508 if os.path.exists(tests[0] + ".err"):
495 509 break
496 510 tests.pop(0)
497 511 if not tests:
498 512 print "running all tests"
499 513 tests = orig
500 514
515 skips = []
501 516 for test in tests:
502 517 if options.retest and not os.path.exists(test + ".err"):
503 518 skipped += 1
504 519 continue
505 ret = run_one(test)
520 ret = run_one(test, skips)
506 521 if ret is None:
507 522 skipped += 1
508 523 elif not ret:
509 524 if options.interactive:
510 525 print "Accept this change? [n] ",
511 526 answer = sys.stdin.readline().strip()
512 527 if answer.lower() in "y yes".split():
513 528 os.rename(test + ".err", test + ".out")
514 529 tested += 1
515 530 continue
516 531 failed += 1
517 532 if options.first:
518 533 break
519 534 tested += 1
520 535
521 536 if options.child:
522 537 fp = os.fdopen(options.child, 'w')
523 538 fp.write('%d\n%d\n%d\n' % (tested, skipped, failed))
539 for s in skips:
540 fp.write("%s %s\n" % s)
524 541 fp.close()
525 542 else:
526 print "\n# Ran %d tests, %d skipped, %d failed." % (
543 print
544 for s in skips:
545 print "Skipped %s: %s" % s
546 print "# Ran %d tests, %d skipped, %d failed." % (
527 547 tested, skipped, failed)
528 548
529 549 if coverage:
530 550 output_coverage()
531 551 except KeyboardInterrupt:
532 552 failed = True
533 553 print "\ninterrupted!"
534 554
535 555 if failed:
536 556 sys.exit(1)
537 557
538 558 if len(args) == 0:
539 559 args = os.listdir(".")
540 560 args.sort()
541 561
542 562 tests = []
543 563 for test in args:
544 564 if (test.startswith("test-") and '~' not in test and
545 565 ('.' not in test or test.endswith('.py') or
546 566 test.endswith('.bat'))):
547 567 tests.append(test)
548 568
549 569 vlog("# Using TESTDIR", TESTDIR)
550 570 vlog("# Using HGTMP", HGTMP)
551 571
552 572 try:
553 573 if len(tests) > 1 and options.jobs > 1:
554 574 run_children(tests)
555 575 else:
556 576 run_tests(tests)
557 577 finally:
558 578 cleanup_exit()
General Comments 0
You need to be logged in to leave comments. Login now