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