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