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