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