##// END OF EJS Templates
run-tests.py: correctly handle list options with parallel tasks
Augie Fackler -
r14192:38e387a6 default
parent child Browse files
Show More
@@ -1,1159 +1,1162 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 of the
8 8 # GNU General Public License version 2 or any later version.
9 9
10 10 # Modifying this script is tricky because it has many modes:
11 11 # - serial (default) vs parallel (-jN, N > 1)
12 12 # - no coverage (default) vs coverage (-c, -C, -s)
13 13 # - temp install (default) vs specific hg script (--with-hg, --local)
14 14 # - tests are a mix of shell scripts and Python scripts
15 15 #
16 16 # If you change this script, it is recommended that you ensure you
17 17 # haven't broken it by running it in various modes with a representative
18 18 # sample of test scripts. For example:
19 19 #
20 20 # 1) serial, no coverage, temp install:
21 21 # ./run-tests.py test-s*
22 22 # 2) serial, no coverage, local hg:
23 23 # ./run-tests.py --local test-s*
24 24 # 3) serial, coverage, temp install:
25 25 # ./run-tests.py -c test-s*
26 26 # 4) serial, coverage, local hg:
27 27 # ./run-tests.py -c --local test-s* # unsupported
28 28 # 5) parallel, no coverage, temp install:
29 29 # ./run-tests.py -j2 test-s*
30 30 # 6) parallel, no coverage, local hg:
31 31 # ./run-tests.py -j2 --local test-s*
32 32 # 7) parallel, coverage, temp install:
33 33 # ./run-tests.py -j2 -c test-s* # currently broken
34 34 # 8) parallel, coverage, local install:
35 35 # ./run-tests.py -j2 -c --local test-s* # unsupported (and broken)
36 36 # 9) parallel, custom tmp dir:
37 37 # ./run-tests.py -j2 --tmpdir /tmp/myhgtests
38 38 #
39 39 # (You could use any subset of the tests: test-s* happens to match
40 40 # enough that it's worth doing parallel runs, few enough that it
41 41 # completes fairly quickly, includes both shell and Python scripts, and
42 42 # includes some scripts that run daemon processes.)
43 43
44 44 from distutils import version
45 45 import difflib
46 46 import errno
47 47 import optparse
48 48 import os
49 49 import shutil
50 50 import subprocess
51 51 import signal
52 52 import sys
53 53 import tempfile
54 54 import time
55 55 import re
56 56 import threading
57 57
58 58 processlock = threading.Lock()
59 59
60 60 closefds = os.name == 'posix'
61 61 def Popen4(cmd, wd, timeout):
62 62 processlock.acquire()
63 63 orig = os.getcwd()
64 64 os.chdir(wd)
65 65 p = subprocess.Popen(cmd, shell=True, bufsize=-1,
66 66 close_fds=closefds,
67 67 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
68 68 stderr=subprocess.STDOUT)
69 69 os.chdir(orig)
70 70 processlock.release()
71 71
72 72 p.fromchild = p.stdout
73 73 p.tochild = p.stdin
74 74 p.childerr = p.stderr
75 75
76 76 if timeout:
77 77 p.timeout = False
78 78 def t():
79 79 start = time.time()
80 80 while time.time() - start < timeout and p.returncode is None:
81 81 time.sleep(1)
82 82 p.timeout = True
83 83 if p.returncode is None:
84 84 try:
85 85 p.terminate()
86 86 except OSError:
87 87 pass
88 88 threading.Thread(target=t).start()
89 89
90 90 return p
91 91
92 92 # reserved exit code to skip test (used by hghave)
93 93 SKIPPED_STATUS = 80
94 94 SKIPPED_PREFIX = 'skipped: '
95 95 FAILED_PREFIX = 'hghave check failed: '
96 96 PYTHON = sys.executable
97 97 IMPL_PATH = 'PYTHONPATH'
98 98 if 'java' in sys.platform:
99 99 IMPL_PATH = 'JYTHONPATH'
100 100
101 101 requiredtools = ["python", "diff", "grep", "unzip", "gunzip", "bunzip2", "sed"]
102 102
103 103 defaults = {
104 104 'jobs': ('HGTEST_JOBS', 1),
105 105 'timeout': ('HGTEST_TIMEOUT', 180),
106 106 'port': ('HGTEST_PORT', 20059),
107 107 }
108 108
109 109 def parseargs():
110 110 parser = optparse.OptionParser("%prog [options] [tests]")
111 111
112 112 # keep these sorted
113 113 parser.add_option("--blacklist", action="append",
114 114 help="skip tests listed in the specified blacklist file")
115 115 parser.add_option("-C", "--annotate", action="store_true",
116 116 help="output files annotated with coverage")
117 117 parser.add_option("--child", type="int",
118 118 help="run as child process, summary to given fd")
119 119 parser.add_option("-c", "--cover", action="store_true",
120 120 help="print a test coverage report")
121 121 parser.add_option("-d", "--debug", action="store_true",
122 122 help="debug mode: write output of test scripts to console"
123 123 " rather than capturing and diff'ing it (disables timeout)")
124 124 parser.add_option("-f", "--first", action="store_true",
125 125 help="exit on the first test failure")
126 126 parser.add_option("--inotify", action="store_true",
127 127 help="enable inotify extension when running tests")
128 128 parser.add_option("-i", "--interactive", action="store_true",
129 129 help="prompt to accept changed output")
130 130 parser.add_option("-j", "--jobs", type="int",
131 131 help="number of jobs to run in parallel"
132 132 " (default: $%s or %d)" % defaults['jobs'])
133 133 parser.add_option("--keep-tmpdir", action="store_true",
134 134 help="keep temporary directory after running tests")
135 135 parser.add_option("-k", "--keywords",
136 136 help="run tests matching keywords")
137 137 parser.add_option("-l", "--local", action="store_true",
138 138 help="shortcut for --with-hg=<testdir>/../hg")
139 139 parser.add_option("-n", "--nodiff", action="store_true",
140 140 help="skip showing test changes")
141 141 parser.add_option("-p", "--port", type="int",
142 142 help="port on which servers should listen"
143 143 " (default: $%s or %d)" % defaults['port'])
144 144 parser.add_option("--pure", action="store_true",
145 145 help="use pure Python code instead of C extensions")
146 146 parser.add_option("-R", "--restart", action="store_true",
147 147 help="restart at last error")
148 148 parser.add_option("-r", "--retest", action="store_true",
149 149 help="retest failed tests")
150 150 parser.add_option("-S", "--noskips", action="store_true",
151 151 help="don't report skip tests verbosely")
152 152 parser.add_option("-t", "--timeout", type="int",
153 153 help="kill errant tests after TIMEOUT seconds"
154 154 " (default: $%s or %d)" % defaults['timeout'])
155 155 parser.add_option("--tmpdir", type="string",
156 156 help="run tests in the given temporary directory"
157 157 " (implies --keep-tmpdir)")
158 158 parser.add_option("-v", "--verbose", action="store_true",
159 159 help="output verbose messages")
160 160 parser.add_option("--view", type="string",
161 161 help="external diff viewer")
162 162 parser.add_option("--with-hg", type="string",
163 163 metavar="HG",
164 164 help="test using specified hg script rather than a "
165 165 "temporary installation")
166 166 parser.add_option("-3", "--py3k-warnings", action="store_true",
167 167 help="enable Py3k warnings on Python 2.6+")
168 168 parser.add_option('--extra-config-opt', action="append",
169 169 help='set the given config opt in the test hgrc')
170 170
171 171 for option, default in defaults.items():
172 172 defaults[option] = int(os.environ.get(*default))
173 173 parser.set_defaults(**defaults)
174 174 (options, args) = parser.parse_args()
175 175
176 176 # jython is always pure
177 177 if 'java' in sys.platform or '__pypy__' in sys.modules:
178 178 options.pure = True
179 179
180 180 if options.with_hg:
181 181 if not (os.path.isfile(options.with_hg) and
182 182 os.access(options.with_hg, os.X_OK)):
183 183 parser.error('--with-hg must specify an executable hg script')
184 184 if not os.path.basename(options.with_hg) == 'hg':
185 185 sys.stderr.write('warning: --with-hg should specify an hg script')
186 186 if options.local:
187 187 testdir = os.path.dirname(os.path.realpath(sys.argv[0]))
188 188 hgbin = os.path.join(os.path.dirname(testdir), 'hg')
189 189 if not os.access(hgbin, os.X_OK):
190 190 parser.error('--local specified, but %r not found or not executable'
191 191 % hgbin)
192 192 options.with_hg = hgbin
193 193
194 194 options.anycoverage = options.cover or options.annotate
195 195 if options.anycoverage:
196 196 try:
197 197 import coverage
198 198 covver = version.StrictVersion(coverage.__version__).version
199 199 if covver < (3, 3):
200 200 parser.error('coverage options require coverage 3.3 or later')
201 201 except ImportError:
202 202 parser.error('coverage options now require the coverage package')
203 203
204 204 if options.anycoverage and options.local:
205 205 # this needs some path mangling somewhere, I guess
206 206 parser.error("sorry, coverage options do not work when --local "
207 207 "is specified")
208 208
209 209 global vlog
210 210 if options.verbose:
211 211 if options.jobs > 1 or options.child is not None:
212 212 pid = "[%d]" % os.getpid()
213 213 else:
214 214 pid = None
215 215 def vlog(*msg):
216 216 iolock.acquire()
217 217 if pid:
218 218 print pid,
219 219 for m in msg:
220 220 print m,
221 221 print
222 222 sys.stdout.flush()
223 223 iolock.release()
224 224 else:
225 225 vlog = lambda *msg: None
226 226
227 227 if options.tmpdir:
228 228 options.tmpdir = os.path.expanduser(options.tmpdir)
229 229
230 230 if options.jobs < 1:
231 231 parser.error('--jobs must be positive')
232 232 if options.interactive and options.jobs > 1:
233 233 print '(--interactive overrides --jobs)'
234 234 options.jobs = 1
235 235 if options.interactive and options.debug:
236 236 parser.error("-i/--interactive and -d/--debug are incompatible")
237 237 if options.debug:
238 238 if options.timeout != defaults['timeout']:
239 239 sys.stderr.write(
240 240 'warning: --timeout option ignored with --debug\n')
241 241 options.timeout = 0
242 242 if options.py3k_warnings:
243 243 if sys.version_info[:2] < (2, 6) or sys.version_info[:2] >= (3, 0):
244 244 parser.error('--py3k-warnings can only be used on Python 2.6+')
245 245 if options.blacklist:
246 246 blacklist = dict()
247 247 for filename in options.blacklist:
248 248 try:
249 249 path = os.path.expanduser(os.path.expandvars(filename))
250 250 f = open(path, "r")
251 251 except IOError, err:
252 252 if err.errno != errno.ENOENT:
253 253 raise
254 254 print "warning: no such blacklist file: %s" % filename
255 255 continue
256 256
257 257 for line in f.readlines():
258 258 line = line.split('#', 1)[0].strip()
259 259 if line:
260 260 blacklist[line] = filename
261 261
262 262 f.close()
263 263
264 264 options.blacklist = blacklist
265 265
266 266 return (options, args)
267 267
268 268 def rename(src, dst):
269 269 """Like os.rename(), trade atomicity and opened files friendliness
270 270 for existing destination support.
271 271 """
272 272 shutil.copy(src, dst)
273 273 os.remove(src)
274 274
275 275 def splitnewlines(text):
276 276 '''like str.splitlines, but only split on newlines.
277 277 keep line endings.'''
278 278 i = 0
279 279 lines = []
280 280 while True:
281 281 n = text.find('\n', i)
282 282 if n == -1:
283 283 last = text[i:]
284 284 if last:
285 285 lines.append(last)
286 286 return lines
287 287 lines.append(text[i:n + 1])
288 288 i = n + 1
289 289
290 290 def parsehghaveoutput(lines):
291 291 '''Parse hghave log lines.
292 292 Return tuple of lists (missing, failed):
293 293 * the missing/unknown features
294 294 * the features for which existence check failed'''
295 295 missing = []
296 296 failed = []
297 297 for line in lines:
298 298 if line.startswith(SKIPPED_PREFIX):
299 299 line = line.splitlines()[0]
300 300 missing.append(line[len(SKIPPED_PREFIX):])
301 301 elif line.startswith(FAILED_PREFIX):
302 302 line = line.splitlines()[0]
303 303 failed.append(line[len(FAILED_PREFIX):])
304 304
305 305 return missing, failed
306 306
307 307 def showdiff(expected, output, ref, err):
308 308 print
309 309 for line in difflib.unified_diff(expected, output, ref, err):
310 310 sys.stdout.write(line)
311 311
312 312 def findprogram(program):
313 313 """Search PATH for a executable program"""
314 314 for p in os.environ.get('PATH', os.defpath).split(os.pathsep):
315 315 name = os.path.join(p, program)
316 316 if os.access(name, os.X_OK):
317 317 return name
318 318 return None
319 319
320 320 def checktools():
321 321 # Before we go any further, check for pre-requisite tools
322 322 # stuff from coreutils (cat, rm, etc) are not tested
323 323 for p in requiredtools:
324 324 if os.name == 'nt':
325 325 p += '.exe'
326 326 found = findprogram(p)
327 327 if found:
328 328 vlog("# Found prerequisite", p, "at", found)
329 329 else:
330 330 print "WARNING: Did not find prerequisite tool: "+p
331 331
332 332 def killdaemons():
333 333 # Kill off any leftover daemon processes
334 334 try:
335 335 fp = open(DAEMON_PIDS)
336 336 for line in fp:
337 337 try:
338 338 pid = int(line)
339 339 except ValueError:
340 340 continue
341 341 try:
342 342 os.kill(pid, 0)
343 343 vlog('# Killing daemon process %d' % pid)
344 344 os.kill(pid, signal.SIGTERM)
345 345 time.sleep(0.25)
346 346 os.kill(pid, 0)
347 347 vlog('# Daemon process %d is stuck - really killing it' % pid)
348 348 os.kill(pid, signal.SIGKILL)
349 349 except OSError, err:
350 350 if err.errno != errno.ESRCH:
351 351 raise
352 352 fp.close()
353 353 os.unlink(DAEMON_PIDS)
354 354 except IOError:
355 355 pass
356 356
357 357 def cleanup(options):
358 358 if not options.keep_tmpdir:
359 359 vlog("# Cleaning up HGTMP", HGTMP)
360 360 shutil.rmtree(HGTMP, True)
361 361
362 362 def usecorrectpython():
363 363 # some tests run python interpreter. they must use same
364 364 # interpreter we use or bad things will happen.
365 365 exedir, exename = os.path.split(sys.executable)
366 366 if exename == 'python':
367 367 path = findprogram('python')
368 368 if os.path.dirname(path) == exedir:
369 369 return
370 370 vlog('# Making python executable in test path use correct Python')
371 371 mypython = os.path.join(BINDIR, 'python')
372 372 try:
373 373 os.symlink(sys.executable, mypython)
374 374 except AttributeError:
375 375 # windows fallback
376 376 shutil.copyfile(sys.executable, mypython)
377 377 shutil.copymode(sys.executable, mypython)
378 378
379 379 def installhg(options):
380 380 vlog("# Performing temporary installation of HG")
381 381 installerrs = os.path.join("tests", "install.err")
382 382 pure = options.pure and "--pure" or ""
383 383
384 384 # Run installer in hg root
385 385 script = os.path.realpath(sys.argv[0])
386 386 hgroot = os.path.dirname(os.path.dirname(script))
387 387 os.chdir(hgroot)
388 388 nohome = '--home=""'
389 389 if os.name == 'nt':
390 390 # The --home="" trick works only on OS where os.sep == '/'
391 391 # because of a distutils convert_path() fast-path. Avoid it at
392 392 # least on Windows for now, deal with .pydistutils.cfg bugs
393 393 # when they happen.
394 394 nohome = ''
395 395 cmd = ('%s setup.py %s clean --all'
396 396 ' build --build-base="%s"'
397 397 ' install --force --prefix="%s" --install-lib="%s"'
398 398 ' --install-scripts="%s" %s >%s 2>&1'
399 399 % (sys.executable, pure, os.path.join(HGTMP, "build"),
400 400 INST, PYTHONDIR, BINDIR, nohome, installerrs))
401 401 vlog("# Running", cmd)
402 402 if os.system(cmd) == 0:
403 403 if not options.verbose:
404 404 os.remove(installerrs)
405 405 else:
406 406 f = open(installerrs)
407 407 for line in f:
408 408 print line,
409 409 f.close()
410 410 sys.exit(1)
411 411 os.chdir(TESTDIR)
412 412
413 413 usecorrectpython()
414 414
415 415 vlog("# Installing dummy diffstat")
416 416 f = open(os.path.join(BINDIR, 'diffstat'), 'w')
417 417 f.write('#!' + sys.executable + '\n'
418 418 'import sys\n'
419 419 'files = 0\n'
420 420 'for line in sys.stdin:\n'
421 421 ' if line.startswith("diff "):\n'
422 422 ' files += 1\n'
423 423 'sys.stdout.write("files patched: %d\\n" % files)\n')
424 424 f.close()
425 425 os.chmod(os.path.join(BINDIR, 'diffstat'), 0700)
426 426
427 427 if options.py3k_warnings and not options.anycoverage:
428 428 vlog("# Updating hg command to enable Py3k Warnings switch")
429 429 f = open(os.path.join(BINDIR, 'hg'), 'r')
430 430 lines = [line.rstrip() for line in f]
431 431 lines[0] += ' -3'
432 432 f.close()
433 433 f = open(os.path.join(BINDIR, 'hg'), 'w')
434 434 for line in lines:
435 435 f.write(line + '\n')
436 436 f.close()
437 437
438 438 if options.anycoverage:
439 439 custom = os.path.join(TESTDIR, 'sitecustomize.py')
440 440 target = os.path.join(PYTHONDIR, 'sitecustomize.py')
441 441 vlog('# Installing coverage trigger to %s' % target)
442 442 shutil.copyfile(custom, target)
443 443 rc = os.path.join(TESTDIR, '.coveragerc')
444 444 vlog('# Installing coverage rc to %s' % rc)
445 445 os.environ['COVERAGE_PROCESS_START'] = rc
446 446 fn = os.path.join(INST, '..', '.coverage')
447 447 os.environ['COVERAGE_FILE'] = fn
448 448
449 449 def outputcoverage(options):
450 450
451 451 vlog('# Producing coverage report')
452 452 os.chdir(PYTHONDIR)
453 453
454 454 def covrun(*args):
455 455 cmd = 'coverage %s' % ' '.join(args)
456 456 vlog('# Running: %s' % cmd)
457 457 os.system(cmd)
458 458
459 459 if options.child:
460 460 return
461 461
462 462 covrun('-c')
463 463 omit = ','.join([BINDIR, TESTDIR])
464 464 covrun('-i', '-r', '"--omit=%s"' % omit) # report
465 465 if options.annotate:
466 466 adir = os.path.join(TESTDIR, 'annotated')
467 467 if not os.path.isdir(adir):
468 468 os.mkdir(adir)
469 469 covrun('-i', '-a', '"--directory=%s"' % adir, '"--omit=%s"' % omit)
470 470
471 471 def pytest(test, wd, options, replacements):
472 472 py3kswitch = options.py3k_warnings and ' -3' or ''
473 473 cmd = '%s%s "%s"' % (PYTHON, py3kswitch, test)
474 474 vlog("# Running", cmd)
475 475 return run(cmd, wd, options, replacements)
476 476
477 477 def shtest(test, wd, options, replacements):
478 478 cmd = '"%s"' % test
479 479 vlog("# Running", cmd)
480 480 return run(cmd, wd, options, replacements)
481 481
482 482 needescape = re.compile(r'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
483 483 escapesub = re.compile(r'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
484 484 escapemap = dict((chr(i), r'\x%02x' % i) for i in range(256))
485 485 escapemap.update({'\\': '\\\\', '\r': r'\r'})
486 486 def escapef(m):
487 487 return escapemap[m.group(0)]
488 488 def stringescape(s):
489 489 return escapesub(escapef, s)
490 490
491 491 def tsttest(test, wd, options, replacements):
492 492 t = open(test)
493 493 out = []
494 494 script = []
495 495 salt = "SALT" + str(time.time())
496 496
497 497 pos = prepos = -1
498 498 after = {}
499 499 expected = {}
500 500 for n, l in enumerate(t):
501 501 if not l.endswith('\n'):
502 502 l += '\n'
503 503 if l.startswith(' $ '): # commands
504 504 after.setdefault(pos, []).append(l)
505 505 prepos = pos
506 506 pos = n
507 507 script.append('echo %s %s $?\n' % (salt, n))
508 508 script.append(l[4:])
509 509 elif l.startswith(' > '): # continuations
510 510 after.setdefault(prepos, []).append(l)
511 511 script.append(l[4:])
512 512 elif l.startswith(' '): # results
513 513 # queue up a list of expected results
514 514 expected.setdefault(pos, []).append(l[2:])
515 515 else:
516 516 # non-command/result - queue up for merged output
517 517 after.setdefault(pos, []).append(l)
518 518
519 519 t.close()
520 520
521 521 script.append('echo %s %s $?\n' % (salt, n + 1))
522 522
523 523 fd, name = tempfile.mkstemp(suffix='hg-tst')
524 524
525 525 try:
526 526 for l in script:
527 527 os.write(fd, l)
528 528 os.close(fd)
529 529
530 530 cmd = '/bin/sh "%s"' % name
531 531 vlog("# Running", cmd)
532 532 exitcode, output = run(cmd, wd, options, replacements)
533 533 # do not merge output if skipped, return hghave message instead
534 534 # similarly, with --debug, output is None
535 535 if exitcode == SKIPPED_STATUS or output is None:
536 536 return exitcode, output
537 537 finally:
538 538 os.remove(name)
539 539
540 540 def rematch(el, l):
541 541 try:
542 542 # ensure that the regex matches to the end of the string
543 543 return re.match(el + r'\Z', l)
544 544 except re.error:
545 545 # el is an invalid regex
546 546 return False
547 547
548 548 def globmatch(el, l):
549 549 # The only supported special characters are * and ?. Escaping is
550 550 # supported.
551 551 i, n = 0, len(el)
552 552 res = ''
553 553 while i < n:
554 554 c = el[i]
555 555 i += 1
556 556 if c == '\\' and el[i] in '*?\\':
557 557 res += el[i - 1:i + 1]
558 558 i += 1
559 559 elif c == '*':
560 560 res += '.*'
561 561 elif c == '?':
562 562 res += '.'
563 563 else:
564 564 res += re.escape(c)
565 565 return rematch(res, l)
566 566
567 567 pos = -1
568 568 postout = []
569 569 ret = 0
570 570 for n, l in enumerate(output):
571 571 lout, lcmd = l, None
572 572 if salt in l:
573 573 lout, lcmd = l.split(salt, 1)
574 574
575 575 if lout:
576 576 if lcmd:
577 577 lout += ' (no-eol)\n'
578 578
579 579 el = None
580 580 if pos in expected and expected[pos]:
581 581 el = expected[pos].pop(0)
582 582
583 583 if el == lout: # perfect match (fast)
584 584 postout.append(" " + lout)
585 585 elif (el and
586 586 (el.endswith(" (re)\n") and rematch(el[:-6] + '\n', lout) or
587 587 el.endswith(" (glob)\n") and globmatch(el[:-8] + '\n', lout)
588 588 or el.endswith(" (esc)\n") and
589 589 el.decode('string-escape') == l)):
590 590 postout.append(" " + el) # fallback regex/glob/esc match
591 591 else:
592 592 if needescape(lout):
593 593 lout = stringescape(lout.rstrip('\n')) + " (esc)\n"
594 594 postout.append(" " + lout) # let diff deal with it
595 595
596 596 if lcmd:
597 597 # add on last return code
598 598 ret = int(lcmd.split()[1])
599 599 if ret != 0:
600 600 postout.append(" [%s]\n" % ret)
601 601 if pos in after:
602 602 postout += after.pop(pos)
603 603 pos = int(lcmd.split()[0])
604 604
605 605 if pos in after:
606 606 postout += after.pop(pos)
607 607
608 608 return exitcode, postout
609 609
610 610 wifexited = getattr(os, "WIFEXITED", lambda x: False)
611 611 def run(cmd, wd, options, replacements):
612 612 """Run command in a sub-process, capturing the output (stdout and stderr).
613 613 Return a tuple (exitcode, output). output is None in debug mode."""
614 614 # TODO: Use subprocess.Popen if we're running on Python 2.4
615 615 if options.debug:
616 616 proc = subprocess.Popen(cmd, shell=True)
617 617 ret = proc.wait()
618 618 return (ret, None)
619 619
620 620 if os.name == 'nt' or sys.platform.startswith('java'):
621 621 tochild, fromchild = os.popen4(cmd)
622 622 tochild.close()
623 623 output = fromchild.read()
624 624 ret = fromchild.close()
625 625 if ret is None:
626 626 ret = 0
627 627 else:
628 628 proc = Popen4(cmd, wd, options.timeout)
629 629 def cleanup():
630 630 try:
631 631 proc.terminate()
632 632 except OSError:
633 633 pass
634 634 ret = proc.wait()
635 635 if ret == 0:
636 636 ret = signal.SIGTERM << 8
637 637 killdaemons()
638 638 return ret
639 639
640 640 output = ''
641 641 proc.tochild.close()
642 642
643 643 try:
644 644 output = proc.fromchild.read()
645 645 except KeyboardInterrupt:
646 646 vlog('# Handling keyboard interrupt')
647 647 cleanup()
648 648 raise
649 649
650 650 ret = proc.wait()
651 651 if wifexited(ret):
652 652 ret = os.WEXITSTATUS(ret)
653 653
654 654 if proc.timeout:
655 655 ret = 'timeout'
656 656
657 657 if ret:
658 658 killdaemons()
659 659
660 660 for s, r in replacements:
661 661 output = re.sub(s, r, output)
662 662 return ret, splitnewlines(output)
663 663
664 664 def runone(options, test):
665 665 '''tristate output:
666 666 None -> skipped
667 667 True -> passed
668 668 False -> failed'''
669 669
670 670 global results, resultslock, iolock
671 671
672 672 testpath = os.path.join(TESTDIR, test)
673 673
674 674 def result(l, e):
675 675 resultslock.acquire()
676 676 results[l].append(e)
677 677 resultslock.release()
678 678
679 679 def skip(msg):
680 680 if not options.verbose:
681 681 result('s', (test, msg))
682 682 else:
683 683 iolock.acquire()
684 684 print "\nSkipping %s: %s" % (testpath, msg)
685 685 iolock.release()
686 686 return None
687 687
688 688 def fail(msg, ret):
689 689 if not options.nodiff:
690 690 iolock.acquire()
691 691 print "\nERROR: %s %s" % (testpath, msg)
692 692 iolock.release()
693 693 if (not ret and options.interactive
694 694 and os.path.exists(testpath + ".err")):
695 695 iolock.acquire()
696 696 print "Accept this change? [n] ",
697 697 answer = sys.stdin.readline().strip()
698 698 iolock.release()
699 699 if answer.lower() in "y yes".split():
700 700 if test.endswith(".t"):
701 701 rename(testpath + ".err", testpath)
702 702 else:
703 703 rename(testpath + ".err", testpath + ".out")
704 704 return
705 705 result('f', (test, msg))
706 706
707 707 def success():
708 708 result('p', test)
709 709
710 710 def ignore(msg):
711 711 result('i', (test, msg))
712 712
713 713 if (test.startswith("test-") and '~' not in test and
714 714 ('.' not in test or test.endswith('.py') or
715 715 test.endswith('.bat') or test.endswith('.t'))):
716 716 if not os.path.exists(test):
717 717 skip("doesn't exist")
718 718 return None
719 719 else:
720 720 return None # not a supported test, don't record
721 721
722 722 if options.blacklist:
723 723 filename = options.blacklist.get(test)
724 724 if filename is not None:
725 725 skip("blacklisted")
726 726 return None
727 727
728 728 if options.retest and not os.path.exists(test + ".err"):
729 729 ignore("not retesting")
730 730 return None
731 731
732 732 if options.keywords:
733 733 fp = open(test)
734 734 t = fp.read().lower() + test.lower()
735 735 fp.close()
736 736 for k in options.keywords.lower().split():
737 737 if k in t:
738 738 break
739 739 else:
740 740 ignore("doesn't match keyword")
741 741 return None
742 742
743 743 vlog("# Test", test)
744 744
745 745 # create a fresh hgrc
746 746 hgrc = open(HGRCPATH, 'w+')
747 747 hgrc.write('[ui]\n')
748 748 hgrc.write('slash = True\n')
749 749 hgrc.write('[defaults]\n')
750 750 hgrc.write('backout = -d "0 0"\n')
751 751 hgrc.write('commit = -d "0 0"\n')
752 752 hgrc.write('tag = -d "0 0"\n')
753 753 if options.inotify:
754 754 hgrc.write('[extensions]\n')
755 755 hgrc.write('inotify=\n')
756 756 hgrc.write('[inotify]\n')
757 757 hgrc.write('pidfile=%s\n' % DAEMON_PIDS)
758 758 hgrc.write('appendpid=True\n')
759 759 if options.extra_config_opt:
760 760 for opt in options.extra_config_opt:
761 761 section, key = opt.split('.', 1)
762 762 assert '=' in key, ('extra config opt %s must '
763 763 'have an = for assignment' % opt)
764 764 hgrc.write('[%s]\n%s\n' % (section, key))
765 765 hgrc.close()
766 766
767 767 ref = os.path.join(TESTDIR, test+".out")
768 768 err = os.path.join(TESTDIR, test+".err")
769 769 if os.path.exists(err):
770 770 os.remove(err) # Remove any previous output files
771 771 try:
772 772 tf = open(testpath)
773 773 firstline = tf.readline().rstrip()
774 774 tf.close()
775 775 except:
776 776 firstline = ''
777 777 lctest = test.lower()
778 778
779 779 if lctest.endswith('.py') or firstline == '#!/usr/bin/env python':
780 780 runner = pytest
781 781 elif lctest.endswith('.t'):
782 782 runner = tsttest
783 783 ref = testpath
784 784 else:
785 785 # do not try to run non-executable programs
786 786 if not os.access(testpath, os.X_OK):
787 787 return skip("not executable")
788 788 runner = shtest
789 789
790 790 # Make a tmp subdirectory to work in
791 791 testtmp = os.environ["TESTTMP"] = os.environ["HOME"] = \
792 792 os.path.join(HGTMP, test)
793 793
794 794 os.mkdir(testtmp)
795 795 ret, out = runner(testpath, testtmp, options, [
796 796 (re.escape(testtmp), '$TESTTMP'),
797 797 (r':%s\b' % options.port, ':$HGPORT'),
798 798 (r':%s\b' % (options.port + 1), ':$HGPORT1'),
799 799 (r':%s\b' % (options.port + 2), ':$HGPORT2'),
800 800 ])
801 801 vlog("# Ret was:", ret)
802 802
803 803 mark = '.'
804 804
805 805 skipped = (ret == SKIPPED_STATUS)
806 806
807 807 # If we're not in --debug mode and reference output file exists,
808 808 # check test output against it.
809 809 if options.debug:
810 810 refout = None # to match "out is None"
811 811 elif os.path.exists(ref):
812 812 f = open(ref, "r")
813 813 refout = splitnewlines(f.read())
814 814 f.close()
815 815 else:
816 816 refout = []
817 817
818 818 if (ret != 0 or out != refout) and not skipped and not options.debug:
819 819 # Save errors to a file for diagnosis
820 820 f = open(err, "wb")
821 821 for line in out:
822 822 f.write(line)
823 823 f.close()
824 824
825 825 if skipped:
826 826 mark = 's'
827 827 if out is None: # debug mode: nothing to parse
828 828 missing = ['unknown']
829 829 failed = None
830 830 else:
831 831 missing, failed = parsehghaveoutput(out)
832 832 if not missing:
833 833 missing = ['irrelevant']
834 834 if failed:
835 835 fail("hghave failed checking for %s" % failed[-1], ret)
836 836 skipped = False
837 837 else:
838 838 skip(missing[-1])
839 839 elif ret == 'timeout':
840 840 mark = 't'
841 841 fail("timed out", ret)
842 842 elif out != refout:
843 843 mark = '!'
844 844 if not options.nodiff:
845 845 iolock.acquire()
846 846 if options.view:
847 847 os.system("%s %s %s" % (options.view, ref, err))
848 848 else:
849 849 showdiff(refout, out, ref, err)
850 850 iolock.release()
851 851 if ret:
852 852 fail("output changed and returned error code %d" % ret, ret)
853 853 else:
854 854 fail("output changed", ret)
855 855 ret = 1
856 856 elif ret:
857 857 mark = '!'
858 858 fail("returned error code %d" % ret, ret)
859 859 else:
860 860 success()
861 861
862 862 if not options.verbose:
863 863 iolock.acquire()
864 864 sys.stdout.write(mark)
865 865 sys.stdout.flush()
866 866 iolock.release()
867 867
868 868 killdaemons()
869 869
870 870 if not options.keep_tmpdir:
871 871 shutil.rmtree(testtmp, True)
872 872 if skipped:
873 873 return None
874 874 return ret == 0
875 875
876 876 _hgpath = None
877 877
878 878 def _gethgpath():
879 879 """Return the path to the mercurial package that is actually found by
880 880 the current Python interpreter."""
881 881 global _hgpath
882 882 if _hgpath is not None:
883 883 return _hgpath
884 884
885 885 cmd = '%s -c "import mercurial; print mercurial.__path__[0]"'
886 886 pipe = os.popen(cmd % PYTHON)
887 887 try:
888 888 _hgpath = pipe.read().strip()
889 889 finally:
890 890 pipe.close()
891 891 return _hgpath
892 892
893 893 def _checkhglib(verb):
894 894 """Ensure that the 'mercurial' package imported by python is
895 895 the one we expect it to be. If not, print a warning to stderr."""
896 896 expecthg = os.path.join(PYTHONDIR, 'mercurial')
897 897 actualhg = _gethgpath()
898 898 if actualhg != expecthg:
899 899 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
900 900 ' (expected %s)\n'
901 901 % (verb, actualhg, expecthg))
902 902
903 903 def runchildren(options, tests):
904 904 if INST:
905 905 installhg(options)
906 906 _checkhglib("Testing")
907 907
908 908 optcopy = dict(options.__dict__)
909 909 optcopy['jobs'] = 1
910 910 del optcopy['blacklist']
911 911 if optcopy['with_hg'] is None:
912 912 optcopy['with_hg'] = os.path.join(BINDIR, "hg")
913 913 optcopy.pop('anycoverage', None)
914 914
915 915 opts = []
916 916 for opt, value in optcopy.iteritems():
917 917 name = '--' + opt.replace('_', '-')
918 918 if value is True:
919 919 opts.append(name)
920 elif isinstance(value, list):
921 for v in value:
922 opts.append(name + '=' + str(v))
920 923 elif value is not None:
921 924 opts.append(name + '=' + str(value))
922 925
923 926 tests.reverse()
924 927 jobs = [[] for j in xrange(options.jobs)]
925 928 while tests:
926 929 for job in jobs:
927 930 if not tests:
928 931 break
929 932 job.append(tests.pop())
930 933 fps = {}
931 934
932 935 for j, job in enumerate(jobs):
933 936 if not job:
934 937 continue
935 938 rfd, wfd = os.pipe()
936 939 childopts = ['--child=%d' % wfd, '--port=%d' % (options.port + j * 3)]
937 940 childtmp = os.path.join(HGTMP, 'child%d' % j)
938 941 childopts += ['--tmpdir', childtmp]
939 942 cmdline = [PYTHON, sys.argv[0]] + opts + childopts + job
940 943 vlog(' '.join(cmdline))
941 944 fps[os.spawnvp(os.P_NOWAIT, cmdline[0], cmdline)] = os.fdopen(rfd, 'r')
942 945 os.close(wfd)
943 946 signal.signal(signal.SIGINT, signal.SIG_IGN)
944 947 failures = 0
945 948 tested, skipped, failed = 0, 0, 0
946 949 skips = []
947 950 fails = []
948 951 while fps:
949 952 pid, status = os.wait()
950 953 fp = fps.pop(pid)
951 954 l = fp.read().splitlines()
952 955 try:
953 956 test, skip, fail = map(int, l[:3])
954 957 except ValueError:
955 958 test, skip, fail = 0, 0, 0
956 959 split = -fail or len(l)
957 960 for s in l[3:split]:
958 961 skips.append(s.split(" ", 1))
959 962 for s in l[split:]:
960 963 fails.append(s.split(" ", 1))
961 964 tested += test
962 965 skipped += skip
963 966 failed += fail
964 967 vlog('pid %d exited, status %d' % (pid, status))
965 968 failures |= status
966 969 print
967 970 if not options.noskips:
968 971 for s in skips:
969 972 print "Skipped %s: %s" % (s[0], s[1])
970 973 for s in fails:
971 974 print "Failed %s: %s" % (s[0], s[1])
972 975
973 976 _checkhglib("Tested")
974 977 print "# Ran %d tests, %d skipped, %d failed." % (
975 978 tested, skipped, failed)
976 979
977 980 if options.anycoverage:
978 981 outputcoverage(options)
979 982 sys.exit(failures != 0)
980 983
981 984 results = dict(p=[], f=[], s=[], i=[])
982 985 resultslock = threading.Lock()
983 986 iolock = threading.Lock()
984 987
985 988 def runqueue(options, tests, results):
986 989 for test in tests:
987 990 ret = runone(options, test)
988 991 if options.first and ret is not None and not ret:
989 992 break
990 993
991 994 def runtests(options, tests):
992 995 global DAEMON_PIDS, HGRCPATH
993 996 DAEMON_PIDS = os.environ["DAEMON_PIDS"] = os.path.join(HGTMP, 'daemon.pids')
994 997 HGRCPATH = os.environ["HGRCPATH"] = os.path.join(HGTMP, '.hgrc')
995 998
996 999 try:
997 1000 if INST:
998 1001 installhg(options)
999 1002 _checkhglib("Testing")
1000 1003
1001 1004 if options.restart:
1002 1005 orig = list(tests)
1003 1006 while tests:
1004 1007 if os.path.exists(tests[0] + ".err"):
1005 1008 break
1006 1009 tests.pop(0)
1007 1010 if not tests:
1008 1011 print "running all tests"
1009 1012 tests = orig
1010 1013
1011 1014 runqueue(options, tests, results)
1012 1015
1013 1016 failed = len(results['f'])
1014 1017 tested = len(results['p']) + failed
1015 1018 skipped = len(results['s'])
1016 1019 ignored = len(results['i'])
1017 1020
1018 1021 if options.child:
1019 1022 fp = os.fdopen(options.child, 'w')
1020 1023 fp.write('%d\n%d\n%d\n' % (tested, skipped, failed))
1021 1024 for s in results['s']:
1022 1025 fp.write("%s %s\n" % s)
1023 1026 for s in results['f']:
1024 1027 fp.write("%s %s\n" % s)
1025 1028 fp.close()
1026 1029 else:
1027 1030 print
1028 1031 for s in results['s']:
1029 1032 print "Skipped %s: %s" % s
1030 1033 for s in results['f']:
1031 1034 print "Failed %s: %s" % s
1032 1035 _checkhglib("Tested")
1033 1036 print "# Ran %d tests, %d skipped, %d failed." % (
1034 1037 tested, skipped + ignored, failed)
1035 1038
1036 1039 if options.anycoverage:
1037 1040 outputcoverage(options)
1038 1041 except KeyboardInterrupt:
1039 1042 failed = True
1040 1043 print "\ninterrupted!"
1041 1044
1042 1045 if failed:
1043 1046 sys.exit(1)
1044 1047
1045 1048 def main():
1046 1049 (options, args) = parseargs()
1047 1050 if not options.child:
1048 1051 os.umask(022)
1049 1052
1050 1053 checktools()
1051 1054
1052 1055 if len(args) == 0:
1053 1056 args = os.listdir(".")
1054 1057 args.sort()
1055 1058
1056 1059 tests = args
1057 1060
1058 1061 # Reset some environment variables to well-known values so that
1059 1062 # the tests produce repeatable output.
1060 1063 os.environ['LANG'] = os.environ['LC_ALL'] = os.environ['LANGUAGE'] = 'C'
1061 1064 os.environ['TZ'] = 'GMT'
1062 1065 os.environ["EMAIL"] = "Foo Bar <foo.bar@example.com>"
1063 1066 os.environ['CDPATH'] = ''
1064 1067 os.environ['COLUMNS'] = '80'
1065 1068 os.environ['GREP_OPTIONS'] = ''
1066 1069 os.environ['http_proxy'] = ''
1067 1070
1068 1071 # unset env related to hooks
1069 1072 for k in os.environ.keys():
1070 1073 if k.startswith('HG_'):
1071 1074 # can't remove on solaris
1072 1075 os.environ[k] = ''
1073 1076 del os.environ[k]
1074 1077
1075 1078 global TESTDIR, HGTMP, INST, BINDIR, PYTHONDIR, COVERAGE_FILE
1076 1079 TESTDIR = os.environ["TESTDIR"] = os.getcwd()
1077 1080 if options.tmpdir:
1078 1081 options.keep_tmpdir = True
1079 1082 tmpdir = options.tmpdir
1080 1083 if os.path.exists(tmpdir):
1081 1084 # Meaning of tmpdir has changed since 1.3: we used to create
1082 1085 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
1083 1086 # tmpdir already exists.
1084 1087 sys.exit("error: temp dir %r already exists" % tmpdir)
1085 1088
1086 1089 # Automatically removing tmpdir sounds convenient, but could
1087 1090 # really annoy anyone in the habit of using "--tmpdir=/tmp"
1088 1091 # or "--tmpdir=$HOME".
1089 1092 #vlog("# Removing temp dir", tmpdir)
1090 1093 #shutil.rmtree(tmpdir)
1091 1094 os.makedirs(tmpdir)
1092 1095 else:
1093 1096 tmpdir = tempfile.mkdtemp('', 'hgtests.')
1094 1097 HGTMP = os.environ['HGTMP'] = os.path.realpath(tmpdir)
1095 1098 DAEMON_PIDS = None
1096 1099 HGRCPATH = None
1097 1100
1098 1101 os.environ["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"'
1099 1102 os.environ["HGMERGE"] = "internal:merge"
1100 1103 os.environ["HGUSER"] = "test"
1101 1104 os.environ["HGENCODING"] = "ascii"
1102 1105 os.environ["HGENCODINGMODE"] = "strict"
1103 1106 os.environ["HGPORT"] = str(options.port)
1104 1107 os.environ["HGPORT1"] = str(options.port + 1)
1105 1108 os.environ["HGPORT2"] = str(options.port + 2)
1106 1109
1107 1110 if options.with_hg:
1108 1111 INST = None
1109 1112 BINDIR = os.path.dirname(os.path.realpath(options.with_hg))
1110 1113
1111 1114 # This looks redundant with how Python initializes sys.path from
1112 1115 # the location of the script being executed. Needed because the
1113 1116 # "hg" specified by --with-hg is not the only Python script
1114 1117 # executed in the test suite that needs to import 'mercurial'
1115 1118 # ... which means it's not really redundant at all.
1116 1119 PYTHONDIR = BINDIR
1117 1120 else:
1118 1121 INST = os.path.join(HGTMP, "install")
1119 1122 BINDIR = os.environ["BINDIR"] = os.path.join(INST, "bin")
1120 1123 PYTHONDIR = os.path.join(INST, "lib", "python")
1121 1124
1122 1125 os.environ["BINDIR"] = BINDIR
1123 1126 os.environ["PYTHON"] = PYTHON
1124 1127
1125 1128 if not options.child:
1126 1129 path = [BINDIR] + os.environ["PATH"].split(os.pathsep)
1127 1130 os.environ["PATH"] = os.pathsep.join(path)
1128 1131
1129 1132 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
1130 1133 # can run .../tests/run-tests.py test-foo where test-foo
1131 1134 # adds an extension to HGRC
1132 1135 pypath = [PYTHONDIR, TESTDIR]
1133 1136 # We have to augment PYTHONPATH, rather than simply replacing
1134 1137 # it, in case external libraries are only available via current
1135 1138 # PYTHONPATH. (In particular, the Subversion bindings on OS X
1136 1139 # are in /opt/subversion.)
1137 1140 oldpypath = os.environ.get(IMPL_PATH)
1138 1141 if oldpypath:
1139 1142 pypath.append(oldpypath)
1140 1143 os.environ[IMPL_PATH] = os.pathsep.join(pypath)
1141 1144
1142 1145 COVERAGE_FILE = os.path.join(TESTDIR, ".coverage")
1143 1146
1144 1147 vlog("# Using TESTDIR", TESTDIR)
1145 1148 vlog("# Using HGTMP", HGTMP)
1146 1149 vlog("# Using PATH", os.environ["PATH"])
1147 1150 vlog("# Using", IMPL_PATH, os.environ[IMPL_PATH])
1148 1151
1149 1152 try:
1150 1153 if len(tests) > 1 and options.jobs > 1:
1151 1154 runchildren(options, tests)
1152 1155 else:
1153 1156 runtests(options, tests)
1154 1157 finally:
1155 1158 time.sleep(1)
1156 1159 cleanup(options)
1157 1160
1158 1161 if __name__ == '__main__':
1159 1162 main()
General Comments 0
You need to be logged in to leave comments. Login now