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