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