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