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