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