##// END OF EJS Templates
run-tests: make --local set --with-chg if --chg is used...
Jun Wu -
r29583:6ce870db default
parent child Browse files
Show More
@@ -1,2566 +1,2569 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 # 10) parallel, pure, tests that call run-tests:
39 39 # ./run-tests.py --pure `grep -l run-tests.py *.t`
40 40 #
41 41 # (You could use any subset of the tests: test-s* happens to match
42 42 # enough that it's worth doing parallel runs, few enough that it
43 43 # completes fairly quickly, includes both shell and Python scripts, and
44 44 # includes some scripts that run daemon processes.)
45 45
46 46 from __future__ import absolute_import, print_function
47 47
48 48 import difflib
49 49 import distutils.version as version
50 50 import errno
51 51 import json
52 52 import optparse
53 53 import os
54 54 import random
55 55 import re
56 56 import shutil
57 57 import signal
58 58 import socket
59 59 import subprocess
60 60 import sys
61 61 import tempfile
62 62 import threading
63 63 import time
64 64 import unittest
65 65 import xml.dom.minidom as minidom
66 66
67 67 try:
68 68 import Queue as queue
69 69 except ImportError:
70 70 import queue
71 71
72 72 if os.environ.get('RTUNICODEPEDANTRY', False):
73 73 try:
74 74 reload(sys)
75 75 sys.setdefaultencoding("undefined")
76 76 except NameError:
77 77 pass
78 78
79 79 osenvironb = getattr(os, 'environb', os.environ)
80 80 processlock = threading.Lock()
81 81
82 82 if sys.version_info > (3, 5, 0):
83 83 PYTHON3 = True
84 84 xrange = range # we use xrange in one place, and we'd rather not use range
85 85 def _bytespath(p):
86 86 return p.encode('utf-8')
87 87
88 88 def _strpath(p):
89 89 return p.decode('utf-8')
90 90
91 91 elif sys.version_info >= (3, 0, 0):
92 92 print('%s is only supported on Python 3.5+ and 2.6-2.7, not %s' %
93 93 (sys.argv[0], '.'.join(str(v) for v in sys.version_info[:3])))
94 94 sys.exit(70) # EX_SOFTWARE from `man 3 sysexit`
95 95 else:
96 96 PYTHON3 = False
97 97
98 98 # In python 2.x, path operations are generally done using
99 99 # bytestrings by default, so we don't have to do any extra
100 100 # fiddling there. We define the wrapper functions anyway just to
101 101 # help keep code consistent between platforms.
102 102 def _bytespath(p):
103 103 return p
104 104
105 105 _strpath = _bytespath
106 106
107 107 # For Windows support
108 108 wifexited = getattr(os, "WIFEXITED", lambda x: False)
109 109
110 110 def checkportisavailable(port):
111 111 """return true if a port seems free to bind on localhost"""
112 112 try:
113 113 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
114 114 s.bind(('localhost', port))
115 115 s.close()
116 116 return True
117 117 except socket.error as exc:
118 118 if not exc.errno == errno.EADDRINUSE:
119 119 raise
120 120 return False
121 121
122 122 closefds = os.name == 'posix'
123 123 def Popen4(cmd, wd, timeout, env=None):
124 124 processlock.acquire()
125 125 p = subprocess.Popen(cmd, shell=True, bufsize=-1, cwd=wd, env=env,
126 126 close_fds=closefds,
127 127 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
128 128 stderr=subprocess.STDOUT)
129 129 processlock.release()
130 130
131 131 p.fromchild = p.stdout
132 132 p.tochild = p.stdin
133 133 p.childerr = p.stderr
134 134
135 135 p.timeout = False
136 136 if timeout:
137 137 def t():
138 138 start = time.time()
139 139 while time.time() - start < timeout and p.returncode is None:
140 140 time.sleep(.1)
141 141 p.timeout = True
142 142 if p.returncode is None:
143 143 terminate(p)
144 144 threading.Thread(target=t).start()
145 145
146 146 return p
147 147
148 148 PYTHON = _bytespath(sys.executable.replace('\\', '/'))
149 149 IMPL_PATH = b'PYTHONPATH'
150 150 if 'java' in sys.platform:
151 151 IMPL_PATH = b'JYTHONPATH'
152 152
153 153 defaults = {
154 154 'jobs': ('HGTEST_JOBS', 1),
155 155 'timeout': ('HGTEST_TIMEOUT', 180),
156 156 'slowtimeout': ('HGTEST_SLOWTIMEOUT', 500),
157 157 'port': ('HGTEST_PORT', 20059),
158 158 'shell': ('HGTEST_SHELL', 'sh'),
159 159 }
160 160
161 161 def canonpath(path):
162 162 return os.path.realpath(os.path.expanduser(path))
163 163
164 164 def parselistfiles(files, listtype, warn=True):
165 165 entries = dict()
166 166 for filename in files:
167 167 try:
168 168 path = os.path.expanduser(os.path.expandvars(filename))
169 169 f = open(path, "rb")
170 170 except IOError as err:
171 171 if err.errno != errno.ENOENT:
172 172 raise
173 173 if warn:
174 174 print("warning: no such %s file: %s" % (listtype, filename))
175 175 continue
176 176
177 177 for line in f.readlines():
178 178 line = line.split(b'#', 1)[0].strip()
179 179 if line:
180 180 entries[line] = filename
181 181
182 182 f.close()
183 183 return entries
184 184
185 185 def getparser():
186 186 """Obtain the OptionParser used by the CLI."""
187 187 parser = optparse.OptionParser("%prog [options] [tests]")
188 188
189 189 # keep these sorted
190 190 parser.add_option("--blacklist", action="append",
191 191 help="skip tests listed in the specified blacklist file")
192 192 parser.add_option("--whitelist", action="append",
193 193 help="always run tests listed in the specified whitelist file")
194 194 parser.add_option("--changed", type="string",
195 195 help="run tests that are changed in parent rev or working directory")
196 196 parser.add_option("-C", "--annotate", action="store_true",
197 197 help="output files annotated with coverage")
198 198 parser.add_option("-c", "--cover", action="store_true",
199 199 help="print a test coverage report")
200 200 parser.add_option("-d", "--debug", action="store_true",
201 201 help="debug mode: write output of test scripts to console"
202 202 " rather than capturing and diffing it (disables timeout)")
203 203 parser.add_option("-f", "--first", action="store_true",
204 204 help="exit on the first test failure")
205 205 parser.add_option("-H", "--htmlcov", action="store_true",
206 206 help="create an HTML report of the coverage of the files")
207 207 parser.add_option("-i", "--interactive", action="store_true",
208 208 help="prompt to accept changed output")
209 209 parser.add_option("-j", "--jobs", type="int",
210 210 help="number of jobs to run in parallel"
211 211 " (default: $%s or %d)" % defaults['jobs'])
212 212 parser.add_option("--keep-tmpdir", action="store_true",
213 213 help="keep temporary directory after running tests")
214 214 parser.add_option("-k", "--keywords",
215 215 help="run tests matching keywords")
216 216 parser.add_option("-l", "--local", action="store_true",
217 help="shortcut for --with-hg=<testdir>/../hg")
217 help="shortcut for --with-hg=<testdir>/../hg, "
218 "and --with-chg=<testdir>/../contrib/chg/chg if --chg is set")
218 219 parser.add_option("--loop", action="store_true",
219 220 help="loop tests repeatedly")
220 221 parser.add_option("--runs-per-test", type="int", dest="runs_per_test",
221 222 help="run each test N times (default=1)", default=1)
222 223 parser.add_option("-n", "--nodiff", action="store_true",
223 224 help="skip showing test changes")
224 225 parser.add_option("-p", "--port", type="int",
225 226 help="port on which servers should listen"
226 227 " (default: $%s or %d)" % defaults['port'])
227 228 parser.add_option("--compiler", type="string",
228 229 help="compiler to build with")
229 230 parser.add_option("--pure", action="store_true",
230 231 help="use pure Python code instead of C extensions")
231 232 parser.add_option("-R", "--restart", action="store_true",
232 233 help="restart at last error")
233 234 parser.add_option("-r", "--retest", action="store_true",
234 235 help="retest failed tests")
235 236 parser.add_option("-S", "--noskips", action="store_true",
236 237 help="don't report skip tests verbosely")
237 238 parser.add_option("--shell", type="string",
238 239 help="shell to use (default: $%s or %s)" % defaults['shell'])
239 240 parser.add_option("-t", "--timeout", type="int",
240 241 help="kill errant tests after TIMEOUT seconds"
241 242 " (default: $%s or %d)" % defaults['timeout'])
242 243 parser.add_option("--slowtimeout", type="int",
243 244 help="kill errant slow tests after SLOWTIMEOUT seconds"
244 245 " (default: $%s or %d)" % defaults['slowtimeout'])
245 246 parser.add_option("--time", action="store_true",
246 247 help="time how long each test takes")
247 248 parser.add_option("--json", action="store_true",
248 249 help="store test result data in 'report.json' file")
249 250 parser.add_option("--tmpdir", type="string",
250 251 help="run tests in the given temporary directory"
251 252 " (implies --keep-tmpdir)")
252 253 parser.add_option("-v", "--verbose", action="store_true",
253 254 help="output verbose messages")
254 255 parser.add_option("--xunit", type="string",
255 256 help="record xunit results at specified path")
256 257 parser.add_option("--view", type="string",
257 258 help="external diff viewer")
258 259 parser.add_option("--with-hg", type="string",
259 260 metavar="HG",
260 261 help="test using specified hg script rather than a "
261 262 "temporary installation")
262 263 parser.add_option("--chg", action="store_true",
263 264 help="install and use chg wrapper in place of hg")
264 265 parser.add_option("--with-chg", metavar="CHG",
265 266 help="use specified chg wrapper in place of hg")
266 267 parser.add_option("-3", "--py3k-warnings", action="store_true",
267 268 help="enable Py3k warnings on Python 2.6+")
268 269 # This option should be deleted once test-check-py3-compat.t and other
269 270 # Python 3 tests run with Python 3.
270 271 parser.add_option("--with-python3", metavar="PYTHON3",
271 272 help="Python 3 interpreter (if running under Python 2)"
272 273 " (TEMPORARY)")
273 274 parser.add_option('--extra-config-opt', action="append",
274 275 help='set the given config opt in the test hgrc')
275 276 parser.add_option('--random', action="store_true",
276 277 help='run tests in random order')
277 278 parser.add_option('--profile-runner', action='store_true',
278 279 help='run statprof on run-tests')
279 280 parser.add_option('--allow-slow-tests', action='store_true',
280 281 help='allow extremely slow tests')
281 282 parser.add_option('--showchannels', action='store_true',
282 283 help='show scheduling channels')
283 284 parser.add_option('--known-good-rev', type="string",
284 285 metavar="known_good_rev",
285 286 help=("Automatically bisect any failures using this "
286 287 "revision as a known-good revision."))
287 288
288 289 for option, (envvar, default) in defaults.items():
289 290 defaults[option] = type(default)(os.environ.get(envvar, default))
290 291 parser.set_defaults(**defaults)
291 292
292 293 return parser
293 294
294 295 def parseargs(args, parser):
295 296 """Parse arguments with our OptionParser and validate results."""
296 297 (options, args) = parser.parse_args(args)
297 298
298 299 # jython is always pure
299 300 if 'java' in sys.platform or '__pypy__' in sys.modules:
300 301 options.pure = True
301 302
302 303 if options.with_hg:
303 304 options.with_hg = canonpath(_bytespath(options.with_hg))
304 305 if not (os.path.isfile(options.with_hg) and
305 306 os.access(options.with_hg, os.X_OK)):
306 307 parser.error('--with-hg must specify an executable hg script')
307 308 if not os.path.basename(options.with_hg) == b'hg':
308 309 sys.stderr.write('warning: --with-hg should specify an hg script\n')
309 310 if options.local:
310 311 testdir = os.path.dirname(_bytespath(canonpath(sys.argv[0])))
311 312 reporootdir = os.path.dirname(testdir)
312 313 pathandattrs = [(b'hg', 'with_hg')]
314 if options.chg:
315 pathandattrs.append((b'contrib/chg/chg', 'with_chg'))
313 316 for relpath, attr in pathandattrs:
314 317 binpath = os.path.join(reporootdir, relpath)
315 318 if os.name != 'nt' and not os.access(binpath, os.X_OK):
316 319 parser.error('--local specified, but %r not found or '
317 320 'not executable' % binpath)
318 321 setattr(options, attr, binpath)
319 322
320 323 if (options.chg or options.with_chg) and os.name == 'nt':
321 324 parser.error('chg does not work on %s' % os.name)
322 325 if options.with_chg:
323 326 options.chg = False # no installation to temporary location
324 327 options.with_chg = canonpath(_bytespath(options.with_chg))
325 328 if not (os.path.isfile(options.with_chg) and
326 329 os.access(options.with_chg, os.X_OK)):
327 330 parser.error('--with-chg must specify a chg executable')
328 331 if options.chg and options.with_hg:
329 332 # chg shares installation location with hg
330 333 parser.error('--chg does not work when --with-hg is specified '
331 334 '(use --with-chg instead)')
332 335
333 336 options.anycoverage = options.cover or options.annotate or options.htmlcov
334 337 if options.anycoverage:
335 338 try:
336 339 import coverage
337 340 covver = version.StrictVersion(coverage.__version__).version
338 341 if covver < (3, 3):
339 342 parser.error('coverage options require coverage 3.3 or later')
340 343 except ImportError:
341 344 parser.error('coverage options now require the coverage package')
342 345
343 346 if options.anycoverage and options.local:
344 347 # this needs some path mangling somewhere, I guess
345 348 parser.error("sorry, coverage options do not work when --local "
346 349 "is specified")
347 350
348 351 if options.anycoverage and options.with_hg:
349 352 parser.error("sorry, coverage options do not work when --with-hg "
350 353 "is specified")
351 354
352 355 global verbose
353 356 if options.verbose:
354 357 verbose = ''
355 358
356 359 if options.tmpdir:
357 360 options.tmpdir = canonpath(options.tmpdir)
358 361
359 362 if options.jobs < 1:
360 363 parser.error('--jobs must be positive')
361 364 if options.interactive and options.debug:
362 365 parser.error("-i/--interactive and -d/--debug are incompatible")
363 366 if options.debug:
364 367 if options.timeout != defaults['timeout']:
365 368 sys.stderr.write(
366 369 'warning: --timeout option ignored with --debug\n')
367 370 if options.slowtimeout != defaults['slowtimeout']:
368 371 sys.stderr.write(
369 372 'warning: --slowtimeout option ignored with --debug\n')
370 373 options.timeout = 0
371 374 options.slowtimeout = 0
372 375 if options.py3k_warnings:
373 376 if PYTHON3:
374 377 parser.error(
375 378 '--py3k-warnings can only be used on Python 2.6 and 2.7')
376 379 if options.with_python3:
377 380 if PYTHON3:
378 381 parser.error('--with-python3 cannot be used when executing with '
379 382 'Python 3')
380 383
381 384 options.with_python3 = canonpath(options.with_python3)
382 385 # Verify Python3 executable is acceptable.
383 386 proc = subprocess.Popen([options.with_python3, b'--version'],
384 387 stdout=subprocess.PIPE,
385 388 stderr=subprocess.STDOUT)
386 389 out, _err = proc.communicate()
387 390 ret = proc.wait()
388 391 if ret != 0:
389 392 parser.error('could not determine version of python 3')
390 393 if not out.startswith('Python '):
391 394 parser.error('unexpected output from python3 --version: %s' %
392 395 out)
393 396 vers = version.LooseVersion(out[len('Python '):])
394 397 if vers < version.LooseVersion('3.5.0'):
395 398 parser.error('--with-python3 version must be 3.5.0 or greater; '
396 399 'got %s' % out)
397 400
398 401 if options.blacklist:
399 402 options.blacklist = parselistfiles(options.blacklist, 'blacklist')
400 403 if options.whitelist:
401 404 options.whitelisted = parselistfiles(options.whitelist, 'whitelist')
402 405 else:
403 406 options.whitelisted = {}
404 407
405 408 if options.showchannels:
406 409 options.nodiff = True
407 410
408 411 return (options, args)
409 412
410 413 def rename(src, dst):
411 414 """Like os.rename(), trade atomicity and opened files friendliness
412 415 for existing destination support.
413 416 """
414 417 shutil.copy(src, dst)
415 418 os.remove(src)
416 419
417 420 _unified_diff = difflib.unified_diff
418 421 if PYTHON3:
419 422 import functools
420 423 _unified_diff = functools.partial(difflib.diff_bytes, difflib.unified_diff)
421 424
422 425 def getdiff(expected, output, ref, err):
423 426 servefail = False
424 427 lines = []
425 428 for line in _unified_diff(expected, output, ref, err):
426 429 if line.startswith(b'+++') or line.startswith(b'---'):
427 430 line = line.replace(b'\\', b'/')
428 431 if line.endswith(b' \n'):
429 432 line = line[:-2] + b'\n'
430 433 lines.append(line)
431 434 if not servefail and line.startswith(
432 435 b'+ abort: child process failed to start'):
433 436 servefail = True
434 437
435 438 return servefail, lines
436 439
437 440 verbose = False
438 441 def vlog(*msg):
439 442 """Log only when in verbose mode."""
440 443 if verbose is False:
441 444 return
442 445
443 446 return log(*msg)
444 447
445 448 # Bytes that break XML even in a CDATA block: control characters 0-31
446 449 # sans \t, \n and \r
447 450 CDATA_EVIL = re.compile(br"[\000-\010\013\014\016-\037]")
448 451
449 452 def cdatasafe(data):
450 453 """Make a string safe to include in a CDATA block.
451 454
452 455 Certain control characters are illegal in a CDATA block, and
453 456 there's no way to include a ]]> in a CDATA either. This function
454 457 replaces illegal bytes with ? and adds a space between the ]] so
455 458 that it won't break the CDATA block.
456 459 """
457 460 return CDATA_EVIL.sub(b'?', data).replace(b']]>', b'] ]>')
458 461
459 462 def log(*msg):
460 463 """Log something to stdout.
461 464
462 465 Arguments are strings to print.
463 466 """
464 467 with iolock:
465 468 if verbose:
466 469 print(verbose, end=' ')
467 470 for m in msg:
468 471 print(m, end=' ')
469 472 print()
470 473 sys.stdout.flush()
471 474
472 475 def terminate(proc):
473 476 """Terminate subprocess (with fallback for Python versions < 2.6)"""
474 477 vlog('# Terminating process %d' % proc.pid)
475 478 try:
476 479 getattr(proc, 'terminate', lambda : os.kill(proc.pid, signal.SIGTERM))()
477 480 except OSError:
478 481 pass
479 482
480 483 def killdaemons(pidfile):
481 484 import killdaemons as killmod
482 485 return killmod.killdaemons(pidfile, tryhard=False, remove=True,
483 486 logfn=vlog)
484 487
485 488 class Test(unittest.TestCase):
486 489 """Encapsulates a single, runnable test.
487 490
488 491 While this class conforms to the unittest.TestCase API, it differs in that
489 492 instances need to be instantiated manually. (Typically, unittest.TestCase
490 493 classes are instantiated automatically by scanning modules.)
491 494 """
492 495
493 496 # Status code reserved for skipped tests (used by hghave).
494 497 SKIPPED_STATUS = 80
495 498
496 499 def __init__(self, path, tmpdir, keeptmpdir=False,
497 500 debug=False,
498 501 timeout=defaults['timeout'],
499 502 startport=defaults['port'], extraconfigopts=None,
500 503 py3kwarnings=False, shell=None, hgcommand=None,
501 504 slowtimeout=defaults['slowtimeout'], usechg=False):
502 505 """Create a test from parameters.
503 506
504 507 path is the full path to the file defining the test.
505 508
506 509 tmpdir is the main temporary directory to use for this test.
507 510
508 511 keeptmpdir determines whether to keep the test's temporary directory
509 512 after execution. It defaults to removal (False).
510 513
511 514 debug mode will make the test execute verbosely, with unfiltered
512 515 output.
513 516
514 517 timeout controls the maximum run time of the test. It is ignored when
515 518 debug is True. See slowtimeout for tests with #require slow.
516 519
517 520 slowtimeout overrides timeout if the test has #require slow.
518 521
519 522 startport controls the starting port number to use for this test. Each
520 523 test will reserve 3 port numbers for execution. It is the caller's
521 524 responsibility to allocate a non-overlapping port range to Test
522 525 instances.
523 526
524 527 extraconfigopts is an iterable of extra hgrc config options. Values
525 528 must have the form "key=value" (something understood by hgrc). Values
526 529 of the form "foo.key=value" will result in "[foo] key=value".
527 530
528 531 py3kwarnings enables Py3k warnings.
529 532
530 533 shell is the shell to execute tests in.
531 534 """
532 535 self.path = path
533 536 self.bname = os.path.basename(path)
534 537 self.name = _strpath(self.bname)
535 538 self._testdir = os.path.dirname(path)
536 539 self.errpath = os.path.join(self._testdir, b'%s.err' % self.bname)
537 540
538 541 self._threadtmp = tmpdir
539 542 self._keeptmpdir = keeptmpdir
540 543 self._debug = debug
541 544 self._timeout = timeout
542 545 self._slowtimeout = slowtimeout
543 546 self._startport = startport
544 547 self._extraconfigopts = extraconfigopts or []
545 548 self._py3kwarnings = py3kwarnings
546 549 self._shell = _bytespath(shell)
547 550 self._hgcommand = hgcommand or b'hg'
548 551 self._usechg = usechg
549 552
550 553 self._aborted = False
551 554 self._daemonpids = []
552 555 self._finished = None
553 556 self._ret = None
554 557 self._out = None
555 558 self._skipped = None
556 559 self._testtmp = None
557 560 self._chgsockdir = None
558 561
559 562 # If we're not in --debug mode and reference output file exists,
560 563 # check test output against it.
561 564 if debug:
562 565 self._refout = None # to match "out is None"
563 566 elif os.path.exists(self.refpath):
564 567 f = open(self.refpath, 'rb')
565 568 self._refout = f.read().splitlines(True)
566 569 f.close()
567 570 else:
568 571 self._refout = []
569 572
570 573 # needed to get base class __repr__ running
571 574 @property
572 575 def _testMethodName(self):
573 576 return self.name
574 577
575 578 def __str__(self):
576 579 return self.name
577 580
578 581 def shortDescription(self):
579 582 return self.name
580 583
581 584 def setUp(self):
582 585 """Tasks to perform before run()."""
583 586 self._finished = False
584 587 self._ret = None
585 588 self._out = None
586 589 self._skipped = None
587 590
588 591 try:
589 592 os.mkdir(self._threadtmp)
590 593 except OSError as e:
591 594 if e.errno != errno.EEXIST:
592 595 raise
593 596
594 597 name = os.path.basename(self.path)
595 598 self._testtmp = os.path.join(self._threadtmp, name)
596 599 os.mkdir(self._testtmp)
597 600
598 601 # Remove any previous output files.
599 602 if os.path.exists(self.errpath):
600 603 try:
601 604 os.remove(self.errpath)
602 605 except OSError as e:
603 606 # We might have raced another test to clean up a .err
604 607 # file, so ignore ENOENT when removing a previous .err
605 608 # file.
606 609 if e.errno != errno.ENOENT:
607 610 raise
608 611
609 612 if self._usechg:
610 613 self._chgsockdir = os.path.join(self._threadtmp,
611 614 b'%s.chgsock' % name)
612 615 os.mkdir(self._chgsockdir)
613 616
614 617 def run(self, result):
615 618 """Run this test and report results against a TestResult instance."""
616 619 # This function is extremely similar to unittest.TestCase.run(). Once
617 620 # we require Python 2.7 (or at least its version of unittest), this
618 621 # function can largely go away.
619 622 self._result = result
620 623 result.startTest(self)
621 624 try:
622 625 try:
623 626 self.setUp()
624 627 except (KeyboardInterrupt, SystemExit):
625 628 self._aborted = True
626 629 raise
627 630 except Exception:
628 631 result.addError(self, sys.exc_info())
629 632 return
630 633
631 634 success = False
632 635 try:
633 636 self.runTest()
634 637 except KeyboardInterrupt:
635 638 self._aborted = True
636 639 raise
637 640 except SkipTest as e:
638 641 result.addSkip(self, str(e))
639 642 # The base class will have already counted this as a
640 643 # test we "ran", but we want to exclude skipped tests
641 644 # from those we count towards those run.
642 645 result.testsRun -= 1
643 646 except IgnoreTest as e:
644 647 result.addIgnore(self, str(e))
645 648 # As with skips, ignores also should be excluded from
646 649 # the number of tests executed.
647 650 result.testsRun -= 1
648 651 except WarnTest as e:
649 652 result.addWarn(self, str(e))
650 653 except ReportedTest as e:
651 654 pass
652 655 except self.failureException as e:
653 656 # This differs from unittest in that we don't capture
654 657 # the stack trace. This is for historical reasons and
655 658 # this decision could be revisited in the future,
656 659 # especially for PythonTest instances.
657 660 if result.addFailure(self, str(e)):
658 661 success = True
659 662 except Exception:
660 663 result.addError(self, sys.exc_info())
661 664 else:
662 665 success = True
663 666
664 667 try:
665 668 self.tearDown()
666 669 except (KeyboardInterrupt, SystemExit):
667 670 self._aborted = True
668 671 raise
669 672 except Exception:
670 673 result.addError(self, sys.exc_info())
671 674 success = False
672 675
673 676 if success:
674 677 result.addSuccess(self)
675 678 finally:
676 679 result.stopTest(self, interrupted=self._aborted)
677 680
678 681 def runTest(self):
679 682 """Run this test instance.
680 683
681 684 This will return a tuple describing the result of the test.
682 685 """
683 686 env = self._getenv()
684 687 self._daemonpids.append(env['DAEMON_PIDS'])
685 688 self._createhgrc(env['HGRCPATH'])
686 689
687 690 vlog('# Test', self.name)
688 691
689 692 ret, out = self._run(env)
690 693 self._finished = True
691 694 self._ret = ret
692 695 self._out = out
693 696
694 697 def describe(ret):
695 698 if ret < 0:
696 699 return 'killed by signal: %d' % -ret
697 700 return 'returned error code %d' % ret
698 701
699 702 self._skipped = False
700 703
701 704 if ret == self.SKIPPED_STATUS:
702 705 if out is None: # Debug mode, nothing to parse.
703 706 missing = ['unknown']
704 707 failed = None
705 708 else:
706 709 missing, failed = TTest.parsehghaveoutput(out)
707 710
708 711 if not missing:
709 712 missing = ['skipped']
710 713
711 714 if failed:
712 715 self.fail('hg have failed checking for %s' % failed[-1])
713 716 else:
714 717 self._skipped = True
715 718 raise SkipTest(missing[-1])
716 719 elif ret == 'timeout':
717 720 self.fail('timed out')
718 721 elif ret is False:
719 722 raise WarnTest('no result code from test')
720 723 elif out != self._refout:
721 724 # Diff generation may rely on written .err file.
722 725 if (ret != 0 or out != self._refout) and not self._skipped \
723 726 and not self._debug:
724 727 f = open(self.errpath, 'wb')
725 728 for line in out:
726 729 f.write(line)
727 730 f.close()
728 731
729 732 # The result object handles diff calculation for us.
730 733 if self._result.addOutputMismatch(self, ret, out, self._refout):
731 734 # change was accepted, skip failing
732 735 return
733 736
734 737 if ret:
735 738 msg = 'output changed and ' + describe(ret)
736 739 else:
737 740 msg = 'output changed'
738 741
739 742 self.fail(msg)
740 743 elif ret:
741 744 self.fail(describe(ret))
742 745
743 746 def tearDown(self):
744 747 """Tasks to perform after run()."""
745 748 for entry in self._daemonpids:
746 749 killdaemons(entry)
747 750 self._daemonpids = []
748 751
749 752 if self._keeptmpdir:
750 753 log('\nKeeping testtmp dir: %s\nKeeping threadtmp dir: %s' %
751 754 (self._testtmp.decode('utf-8'),
752 755 self._threadtmp.decode('utf-8')))
753 756 else:
754 757 shutil.rmtree(self._testtmp, True)
755 758 shutil.rmtree(self._threadtmp, True)
756 759
757 760 if self._usechg:
758 761 # chgservers will stop automatically after they find the socket
759 762 # files are deleted
760 763 shutil.rmtree(self._chgsockdir, True)
761 764
762 765 if (self._ret != 0 or self._out != self._refout) and not self._skipped \
763 766 and not self._debug and self._out:
764 767 f = open(self.errpath, 'wb')
765 768 for line in self._out:
766 769 f.write(line)
767 770 f.close()
768 771
769 772 vlog("# Ret was:", self._ret, '(%s)' % self.name)
770 773
771 774 def _run(self, env):
772 775 # This should be implemented in child classes to run tests.
773 776 raise SkipTest('unknown test type')
774 777
775 778 def abort(self):
776 779 """Terminate execution of this test."""
777 780 self._aborted = True
778 781
779 782 def _portmap(self, i):
780 783 offset = b'' if i == 0 else b'%d' % i
781 784 return (br':%d\b' % (self._startport + i), b':$HGPORT%s' % offset)
782 785
783 786 def _getreplacements(self):
784 787 """Obtain a mapping of text replacements to apply to test output.
785 788
786 789 Test output needs to be normalized so it can be compared to expected
787 790 output. This function defines how some of that normalization will
788 791 occur.
789 792 """
790 793 r = [
791 794 # This list should be parallel to defineport in _getenv
792 795 self._portmap(0),
793 796 self._portmap(1),
794 797 self._portmap(2),
795 798 (br'(?m)^(saved backup bundle to .*\.hg)( \(glob\))?$',
796 799 br'\1 (glob)'),
797 800 ]
798 801 r.append((self._escapepath(self._testtmp), b'$TESTTMP'))
799 802
800 803 return r
801 804
802 805 def _escapepath(self, p):
803 806 if os.name == 'nt':
804 807 return (
805 808 (b''.join(c.isalpha() and b'[%s%s]' % (c.lower(), c.upper()) or
806 809 c in b'/\\' and br'[/\\]' or c.isdigit() and c or b'\\' + c
807 810 for c in p))
808 811 )
809 812 else:
810 813 return re.escape(p)
811 814
812 815 def _getenv(self):
813 816 """Obtain environment variables to use during test execution."""
814 817 def defineport(i):
815 818 offset = '' if i == 0 else '%s' % i
816 819 env["HGPORT%s" % offset] = '%s' % (self._startport + i)
817 820 env = os.environ.copy()
818 821 env['TESTTMP'] = self._testtmp
819 822 env['HOME'] = self._testtmp
820 823 # This number should match portneeded in _getport
821 824 for port in xrange(3):
822 825 # This list should be parallel to _portmap in _getreplacements
823 826 defineport(port)
824 827 env["HGRCPATH"] = os.path.join(self._threadtmp, b'.hgrc')
825 828 env["DAEMON_PIDS"] = os.path.join(self._threadtmp, b'daemon.pids')
826 829 env["HGEDITOR"] = ('"' + sys.executable + '"'
827 830 + ' -c "import sys; sys.exit(0)"')
828 831 env["HGMERGE"] = "internal:merge"
829 832 env["HGUSER"] = "test"
830 833 env["HGENCODING"] = "ascii"
831 834 env["HGENCODINGMODE"] = "strict"
832 835
833 836 # Reset some environment variables to well-known values so that
834 837 # the tests produce repeatable output.
835 838 env['LANG'] = env['LC_ALL'] = env['LANGUAGE'] = 'C'
836 839 env['TZ'] = 'GMT'
837 840 env["EMAIL"] = "Foo Bar <foo.bar@example.com>"
838 841 env['COLUMNS'] = '80'
839 842 env['TERM'] = 'xterm'
840 843
841 844 for k in ('HG HGPROF CDPATH GREP_OPTIONS http_proxy no_proxy ' +
842 845 'NO_PROXY').split():
843 846 if k in env:
844 847 del env[k]
845 848
846 849 # unset env related to hooks
847 850 for k in env.keys():
848 851 if k.startswith('HG_'):
849 852 del env[k]
850 853
851 854 if self._usechg:
852 855 env['CHGSOCKNAME'] = os.path.join(self._chgsockdir, b'server')
853 856
854 857 return env
855 858
856 859 def _createhgrc(self, path):
857 860 """Create an hgrc file for this test."""
858 861 hgrc = open(path, 'wb')
859 862 hgrc.write(b'[ui]\n')
860 863 hgrc.write(b'slash = True\n')
861 864 hgrc.write(b'interactive = False\n')
862 865 hgrc.write(b'mergemarkers = detailed\n')
863 866 hgrc.write(b'promptecho = True\n')
864 867 hgrc.write(b'[defaults]\n')
865 868 hgrc.write(b'backout = -d "0 0"\n')
866 869 hgrc.write(b'commit = -d "0 0"\n')
867 870 hgrc.write(b'shelve = --date "0 0"\n')
868 871 hgrc.write(b'tag = -d "0 0"\n')
869 872 hgrc.write(b'[devel]\n')
870 873 hgrc.write(b'all-warnings = true\n')
871 874 hgrc.write(b'[largefiles]\n')
872 875 hgrc.write(b'usercache = %s\n' %
873 876 (os.path.join(self._testtmp, b'.cache/largefiles')))
874 877
875 878 for opt in self._extraconfigopts:
876 879 section, key = opt.split('.', 1)
877 880 assert '=' in key, ('extra config opt %s must '
878 881 'have an = for assignment' % opt)
879 882 hgrc.write(b'[%s]\n%s\n' % (section, key))
880 883 hgrc.close()
881 884
882 885 def fail(self, msg):
883 886 # unittest differentiates between errored and failed.
884 887 # Failed is denoted by AssertionError (by default at least).
885 888 raise AssertionError(msg)
886 889
887 890 def _runcommand(self, cmd, env, normalizenewlines=False):
888 891 """Run command in a sub-process, capturing the output (stdout and
889 892 stderr).
890 893
891 894 Return a tuple (exitcode, output). output is None in debug mode.
892 895 """
893 896 if self._debug:
894 897 proc = subprocess.Popen(cmd, shell=True, cwd=self._testtmp,
895 898 env=env)
896 899 ret = proc.wait()
897 900 return (ret, None)
898 901
899 902 proc = Popen4(cmd, self._testtmp, self._timeout, env)
900 903 def cleanup():
901 904 terminate(proc)
902 905 ret = proc.wait()
903 906 if ret == 0:
904 907 ret = signal.SIGTERM << 8
905 908 killdaemons(env['DAEMON_PIDS'])
906 909 return ret
907 910
908 911 output = ''
909 912 proc.tochild.close()
910 913
911 914 try:
912 915 output = proc.fromchild.read()
913 916 except KeyboardInterrupt:
914 917 vlog('# Handling keyboard interrupt')
915 918 cleanup()
916 919 raise
917 920
918 921 ret = proc.wait()
919 922 if wifexited(ret):
920 923 ret = os.WEXITSTATUS(ret)
921 924
922 925 if proc.timeout:
923 926 ret = 'timeout'
924 927
925 928 if ret:
926 929 killdaemons(env['DAEMON_PIDS'])
927 930
928 931 for s, r in self._getreplacements():
929 932 output = re.sub(s, r, output)
930 933
931 934 if normalizenewlines:
932 935 output = output.replace('\r\n', '\n')
933 936
934 937 return ret, output.splitlines(True)
935 938
936 939 class PythonTest(Test):
937 940 """A Python-based test."""
938 941
939 942 @property
940 943 def refpath(self):
941 944 return os.path.join(self._testdir, b'%s.out' % self.bname)
942 945
943 946 def _run(self, env):
944 947 py3kswitch = self._py3kwarnings and b' -3' or b''
945 948 cmd = b'%s%s "%s"' % (PYTHON, py3kswitch, self.path)
946 949 vlog("# Running", cmd)
947 950 normalizenewlines = os.name == 'nt'
948 951 result = self._runcommand(cmd, env,
949 952 normalizenewlines=normalizenewlines)
950 953 if self._aborted:
951 954 raise KeyboardInterrupt()
952 955
953 956 return result
954 957
955 958 # Some glob patterns apply only in some circumstances, so the script
956 959 # might want to remove (glob) annotations that otherwise should be
957 960 # retained.
958 961 checkcodeglobpats = [
959 962 # On Windows it looks like \ doesn't require a (glob), but we know
960 963 # better.
961 964 re.compile(br'^pushing to \$TESTTMP/.*[^)]$'),
962 965 re.compile(br'^moving \S+/.*[^)]$'),
963 966 re.compile(br'^pulling from \$TESTTMP/.*[^)]$'),
964 967 # Not all platforms have 127.0.0.1 as loopback (though most do),
965 968 # so we always glob that too.
966 969 re.compile(br'.*127.0.0.1.*$'),
967 970 ]
968 971
969 972 bchr = chr
970 973 if PYTHON3:
971 974 bchr = lambda x: bytes([x])
972 975
973 976 class TTest(Test):
974 977 """A "t test" is a test backed by a .t file."""
975 978
976 979 SKIPPED_PREFIX = b'skipped: '
977 980 FAILED_PREFIX = b'hghave check failed: '
978 981 NEEDESCAPE = re.compile(br'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
979 982
980 983 ESCAPESUB = re.compile(br'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
981 984 ESCAPEMAP = dict((bchr(i), br'\x%02x' % i) for i in range(256))
982 985 ESCAPEMAP.update({b'\\': b'\\\\', b'\r': br'\r'})
983 986
984 987 @property
985 988 def refpath(self):
986 989 return os.path.join(self._testdir, self.bname)
987 990
988 991 def _run(self, env):
989 992 f = open(self.path, 'rb')
990 993 lines = f.readlines()
991 994 f.close()
992 995
993 996 salt, script, after, expected = self._parsetest(lines)
994 997
995 998 # Write out the generated script.
996 999 fname = b'%s.sh' % self._testtmp
997 1000 f = open(fname, 'wb')
998 1001 for l in script:
999 1002 f.write(l)
1000 1003 f.close()
1001 1004
1002 1005 cmd = b'%s "%s"' % (self._shell, fname)
1003 1006 vlog("# Running", cmd)
1004 1007
1005 1008 exitcode, output = self._runcommand(cmd, env)
1006 1009
1007 1010 if self._aborted:
1008 1011 raise KeyboardInterrupt()
1009 1012
1010 1013 # Do not merge output if skipped. Return hghave message instead.
1011 1014 # Similarly, with --debug, output is None.
1012 1015 if exitcode == self.SKIPPED_STATUS or output is None:
1013 1016 return exitcode, output
1014 1017
1015 1018 return self._processoutput(exitcode, output, salt, after, expected)
1016 1019
1017 1020 def _hghave(self, reqs):
1018 1021 # TODO do something smarter when all other uses of hghave are gone.
1019 1022 runtestdir = os.path.abspath(os.path.dirname(_bytespath(__file__)))
1020 1023 tdir = runtestdir.replace(b'\\', b'/')
1021 1024 proc = Popen4(b'%s -c "%s/hghave %s"' %
1022 1025 (self._shell, tdir, b' '.join(reqs)),
1023 1026 self._testtmp, 0, self._getenv())
1024 1027 stdout, stderr = proc.communicate()
1025 1028 ret = proc.wait()
1026 1029 if wifexited(ret):
1027 1030 ret = os.WEXITSTATUS(ret)
1028 1031 if ret == 2:
1029 1032 print(stdout.decode('utf-8'))
1030 1033 sys.exit(1)
1031 1034
1032 1035 if ret != 0:
1033 1036 return False, stdout
1034 1037
1035 1038 if 'slow' in reqs:
1036 1039 self._timeout = self._slowtimeout
1037 1040 return True, None
1038 1041
1039 1042 def _parsetest(self, lines):
1040 1043 # We generate a shell script which outputs unique markers to line
1041 1044 # up script results with our source. These markers include input
1042 1045 # line number and the last return code.
1043 1046 salt = b"SALT%d" % time.time()
1044 1047 def addsalt(line, inpython):
1045 1048 if inpython:
1046 1049 script.append(b'%s %d 0\n' % (salt, line))
1047 1050 else:
1048 1051 script.append(b'echo %s %d $?\n' % (salt, line))
1049 1052
1050 1053 script = []
1051 1054
1052 1055 # After we run the shell script, we re-unify the script output
1053 1056 # with non-active parts of the source, with synchronization by our
1054 1057 # SALT line number markers. The after table contains the non-active
1055 1058 # components, ordered by line number.
1056 1059 after = {}
1057 1060
1058 1061 # Expected shell script output.
1059 1062 expected = {}
1060 1063
1061 1064 pos = prepos = -1
1062 1065
1063 1066 # True or False when in a true or false conditional section
1064 1067 skipping = None
1065 1068
1066 1069 # We keep track of whether or not we're in a Python block so we
1067 1070 # can generate the surrounding doctest magic.
1068 1071 inpython = False
1069 1072
1070 1073 if self._debug:
1071 1074 script.append(b'set -x\n')
1072 1075 if self._hgcommand != b'hg':
1073 1076 script.append(b'alias hg="%s"\n' % self._hgcommand)
1074 1077 if os.getenv('MSYSTEM'):
1075 1078 script.append(b'alias pwd="pwd -W"\n')
1076 1079
1077 1080 n = 0
1078 1081 for n, l in enumerate(lines):
1079 1082 if not l.endswith(b'\n'):
1080 1083 l += b'\n'
1081 1084 if l.startswith(b'#require'):
1082 1085 lsplit = l.split()
1083 1086 if len(lsplit) < 2 or lsplit[0] != b'#require':
1084 1087 after.setdefault(pos, []).append(' !!! invalid #require\n')
1085 1088 haveresult, message = self._hghave(lsplit[1:])
1086 1089 if not haveresult:
1087 1090 script = [b'echo "%s"\nexit 80\n' % message]
1088 1091 break
1089 1092 after.setdefault(pos, []).append(l)
1090 1093 elif l.startswith(b'#if'):
1091 1094 lsplit = l.split()
1092 1095 if len(lsplit) < 2 or lsplit[0] != b'#if':
1093 1096 after.setdefault(pos, []).append(' !!! invalid #if\n')
1094 1097 if skipping is not None:
1095 1098 after.setdefault(pos, []).append(' !!! nested #if\n')
1096 1099 skipping = not self._hghave(lsplit[1:])[0]
1097 1100 after.setdefault(pos, []).append(l)
1098 1101 elif l.startswith(b'#else'):
1099 1102 if skipping is None:
1100 1103 after.setdefault(pos, []).append(' !!! missing #if\n')
1101 1104 skipping = not skipping
1102 1105 after.setdefault(pos, []).append(l)
1103 1106 elif l.startswith(b'#endif'):
1104 1107 if skipping is None:
1105 1108 after.setdefault(pos, []).append(' !!! missing #if\n')
1106 1109 skipping = None
1107 1110 after.setdefault(pos, []).append(l)
1108 1111 elif skipping:
1109 1112 after.setdefault(pos, []).append(l)
1110 1113 elif l.startswith(b' >>> '): # python inlines
1111 1114 after.setdefault(pos, []).append(l)
1112 1115 prepos = pos
1113 1116 pos = n
1114 1117 if not inpython:
1115 1118 # We've just entered a Python block. Add the header.
1116 1119 inpython = True
1117 1120 addsalt(prepos, False) # Make sure we report the exit code.
1118 1121 script.append(b'%s -m heredoctest <<EOF\n' % PYTHON)
1119 1122 addsalt(n, True)
1120 1123 script.append(l[2:])
1121 1124 elif l.startswith(b' ... '): # python inlines
1122 1125 after.setdefault(prepos, []).append(l)
1123 1126 script.append(l[2:])
1124 1127 elif l.startswith(b' $ '): # commands
1125 1128 if inpython:
1126 1129 script.append(b'EOF\n')
1127 1130 inpython = False
1128 1131 after.setdefault(pos, []).append(l)
1129 1132 prepos = pos
1130 1133 pos = n
1131 1134 addsalt(n, False)
1132 1135 cmd = l[4:].split()
1133 1136 if len(cmd) == 2 and cmd[0] == b'cd':
1134 1137 l = b' $ cd %s || exit 1\n' % cmd[1]
1135 1138 script.append(l[4:])
1136 1139 elif l.startswith(b' > '): # continuations
1137 1140 after.setdefault(prepos, []).append(l)
1138 1141 script.append(l[4:])
1139 1142 elif l.startswith(b' '): # results
1140 1143 # Queue up a list of expected results.
1141 1144 expected.setdefault(pos, []).append(l[2:])
1142 1145 else:
1143 1146 if inpython:
1144 1147 script.append(b'EOF\n')
1145 1148 inpython = False
1146 1149 # Non-command/result. Queue up for merged output.
1147 1150 after.setdefault(pos, []).append(l)
1148 1151
1149 1152 if inpython:
1150 1153 script.append(b'EOF\n')
1151 1154 if skipping is not None:
1152 1155 after.setdefault(pos, []).append(' !!! missing #endif\n')
1153 1156 addsalt(n + 1, False)
1154 1157
1155 1158 return salt, script, after, expected
1156 1159
1157 1160 def _processoutput(self, exitcode, output, salt, after, expected):
1158 1161 # Merge the script output back into a unified test.
1159 1162 warnonly = 1 # 1: not yet; 2: yes; 3: for sure not
1160 1163 if exitcode != 0:
1161 1164 warnonly = 3
1162 1165
1163 1166 pos = -1
1164 1167 postout = []
1165 1168 for l in output:
1166 1169 lout, lcmd = l, None
1167 1170 if salt in l:
1168 1171 lout, lcmd = l.split(salt, 1)
1169 1172
1170 1173 while lout:
1171 1174 if not lout.endswith(b'\n'):
1172 1175 lout += b' (no-eol)\n'
1173 1176
1174 1177 # Find the expected output at the current position.
1175 1178 els = [None]
1176 1179 if expected.get(pos, None):
1177 1180 els = expected[pos]
1178 1181
1179 1182 i = 0
1180 1183 optional = []
1181 1184 while i < len(els):
1182 1185 el = els[i]
1183 1186
1184 1187 r = TTest.linematch(el, lout)
1185 1188 if isinstance(r, str):
1186 1189 if r == '+glob':
1187 1190 lout = el[:-1] + ' (glob)\n'
1188 1191 r = '' # Warn only this line.
1189 1192 elif r == '-glob':
1190 1193 lout = ''.join(el.rsplit(' (glob)', 1))
1191 1194 r = '' # Warn only this line.
1192 1195 elif r == "retry":
1193 1196 postout.append(b' ' + el)
1194 1197 els.pop(i)
1195 1198 break
1196 1199 else:
1197 1200 log('\ninfo, unknown linematch result: %r\n' % r)
1198 1201 r = False
1199 1202 if r:
1200 1203 els.pop(i)
1201 1204 break
1202 1205 if el and el.endswith(b" (?)\n"):
1203 1206 optional.append(i)
1204 1207 i += 1
1205 1208
1206 1209 if r:
1207 1210 if r == "retry":
1208 1211 continue
1209 1212 # clean up any optional leftovers
1210 1213 for i in optional:
1211 1214 postout.append(b' ' + els[i])
1212 1215 for i in reversed(optional):
1213 1216 del els[i]
1214 1217 postout.append(b' ' + el)
1215 1218 else:
1216 1219 if self.NEEDESCAPE(lout):
1217 1220 lout = TTest._stringescape(b'%s (esc)\n' %
1218 1221 lout.rstrip(b'\n'))
1219 1222 postout.append(b' ' + lout) # Let diff deal with it.
1220 1223 if r != '': # If line failed.
1221 1224 warnonly = 3 # for sure not
1222 1225 elif warnonly == 1: # Is "not yet" and line is warn only.
1223 1226 warnonly = 2 # Yes do warn.
1224 1227 break
1225 1228 else:
1226 1229 # clean up any optional leftovers
1227 1230 while expected.get(pos, None):
1228 1231 el = expected[pos].pop(0)
1229 1232 if el and not el.endswith(b" (?)\n"):
1230 1233 break
1231 1234 postout.append(b' ' + el)
1232 1235
1233 1236 if lcmd:
1234 1237 # Add on last return code.
1235 1238 ret = int(lcmd.split()[1])
1236 1239 if ret != 0:
1237 1240 postout.append(b' [%d]\n' % ret)
1238 1241 if pos in after:
1239 1242 # Merge in non-active test bits.
1240 1243 postout += after.pop(pos)
1241 1244 pos = int(lcmd.split()[0])
1242 1245
1243 1246 if pos in after:
1244 1247 postout += after.pop(pos)
1245 1248
1246 1249 if warnonly == 2:
1247 1250 exitcode = False # Set exitcode to warned.
1248 1251
1249 1252 return exitcode, postout
1250 1253
1251 1254 @staticmethod
1252 1255 def rematch(el, l):
1253 1256 try:
1254 1257 # use \Z to ensure that the regex matches to the end of the string
1255 1258 if os.name == 'nt':
1256 1259 return re.match(el + br'\r?\n\Z', l)
1257 1260 return re.match(el + br'\n\Z', l)
1258 1261 except re.error:
1259 1262 # el is an invalid regex
1260 1263 return False
1261 1264
1262 1265 @staticmethod
1263 1266 def globmatch(el, l):
1264 1267 # The only supported special characters are * and ? plus / which also
1265 1268 # matches \ on windows. Escaping of these characters is supported.
1266 1269 if el + b'\n' == l:
1267 1270 if os.altsep:
1268 1271 # matching on "/" is not needed for this line
1269 1272 for pat in checkcodeglobpats:
1270 1273 if pat.match(el):
1271 1274 return True
1272 1275 return b'-glob'
1273 1276 return True
1274 1277 el = el.replace(b'127.0.0.1', b'*')
1275 1278 i, n = 0, len(el)
1276 1279 res = b''
1277 1280 while i < n:
1278 1281 c = el[i:i + 1]
1279 1282 i += 1
1280 1283 if c == b'\\' and i < n and el[i:i + 1] in b'*?\\/':
1281 1284 res += el[i - 1:i + 1]
1282 1285 i += 1
1283 1286 elif c == b'*':
1284 1287 res += b'.*'
1285 1288 elif c == b'?':
1286 1289 res += b'.'
1287 1290 elif c == b'/' and os.altsep:
1288 1291 res += b'[/\\\\]'
1289 1292 else:
1290 1293 res += re.escape(c)
1291 1294 return TTest.rematch(res, l)
1292 1295
1293 1296 @staticmethod
1294 1297 def linematch(el, l):
1295 1298 retry = False
1296 1299 if el == l: # perfect match (fast)
1297 1300 return True
1298 1301 if el:
1299 1302 if el.endswith(b" (?)\n"):
1300 1303 retry = "retry"
1301 1304 el = el[:-5] + b"\n"
1302 1305 if el.endswith(b" (esc)\n"):
1303 1306 if PYTHON3:
1304 1307 el = el[:-7].decode('unicode_escape') + '\n'
1305 1308 el = el.encode('utf-8')
1306 1309 else:
1307 1310 el = el[:-7].decode('string-escape') + '\n'
1308 1311 if el == l or os.name == 'nt' and el[:-1] + b'\r\n' == l:
1309 1312 return True
1310 1313 if el.endswith(b" (re)\n"):
1311 1314 return TTest.rematch(el[:-6], l) or retry
1312 1315 if el.endswith(b" (glob)\n"):
1313 1316 # ignore '(glob)' added to l by 'replacements'
1314 1317 if l.endswith(b" (glob)\n"):
1315 1318 l = l[:-8] + b"\n"
1316 1319 return TTest.globmatch(el[:-8], l)
1317 1320 if os.altsep and l.replace(b'\\', b'/') == el:
1318 1321 return b'+glob'
1319 1322 return retry
1320 1323
1321 1324 @staticmethod
1322 1325 def parsehghaveoutput(lines):
1323 1326 '''Parse hghave log lines.
1324 1327
1325 1328 Return tuple of lists (missing, failed):
1326 1329 * the missing/unknown features
1327 1330 * the features for which existence check failed'''
1328 1331 missing = []
1329 1332 failed = []
1330 1333 for line in lines:
1331 1334 if line.startswith(TTest.SKIPPED_PREFIX):
1332 1335 line = line.splitlines()[0]
1333 1336 missing.append(line[len(TTest.SKIPPED_PREFIX):].decode('utf-8'))
1334 1337 elif line.startswith(TTest.FAILED_PREFIX):
1335 1338 line = line.splitlines()[0]
1336 1339 failed.append(line[len(TTest.FAILED_PREFIX):].decode('utf-8'))
1337 1340
1338 1341 return missing, failed
1339 1342
1340 1343 @staticmethod
1341 1344 def _escapef(m):
1342 1345 return TTest.ESCAPEMAP[m.group(0)]
1343 1346
1344 1347 @staticmethod
1345 1348 def _stringescape(s):
1346 1349 return TTest.ESCAPESUB(TTest._escapef, s)
1347 1350
1348 1351 iolock = threading.RLock()
1349 1352
1350 1353 class SkipTest(Exception):
1351 1354 """Raised to indicate that a test is to be skipped."""
1352 1355
1353 1356 class IgnoreTest(Exception):
1354 1357 """Raised to indicate that a test is to be ignored."""
1355 1358
1356 1359 class WarnTest(Exception):
1357 1360 """Raised to indicate that a test warned."""
1358 1361
1359 1362 class ReportedTest(Exception):
1360 1363 """Raised to indicate that a test already reported."""
1361 1364
1362 1365 class TestResult(unittest._TextTestResult):
1363 1366 """Holds results when executing via unittest."""
1364 1367 # Don't worry too much about accessing the non-public _TextTestResult.
1365 1368 # It is relatively common in Python testing tools.
1366 1369 def __init__(self, options, *args, **kwargs):
1367 1370 super(TestResult, self).__init__(*args, **kwargs)
1368 1371
1369 1372 self._options = options
1370 1373
1371 1374 # unittest.TestResult didn't have skipped until 2.7. We need to
1372 1375 # polyfill it.
1373 1376 self.skipped = []
1374 1377
1375 1378 # We have a custom "ignored" result that isn't present in any Python
1376 1379 # unittest implementation. It is very similar to skipped. It may make
1377 1380 # sense to map it into skip some day.
1378 1381 self.ignored = []
1379 1382
1380 1383 # We have a custom "warned" result that isn't present in any Python
1381 1384 # unittest implementation. It is very similar to failed. It may make
1382 1385 # sense to map it into fail some day.
1383 1386 self.warned = []
1384 1387
1385 1388 self.times = []
1386 1389 self._firststarttime = None
1387 1390 # Data stored for the benefit of generating xunit reports.
1388 1391 self.successes = []
1389 1392 self.faildata = {}
1390 1393
1391 1394 def addFailure(self, test, reason):
1392 1395 self.failures.append((test, reason))
1393 1396
1394 1397 if self._options.first:
1395 1398 self.stop()
1396 1399 else:
1397 1400 with iolock:
1398 1401 if reason == "timed out":
1399 1402 self.stream.write('t')
1400 1403 else:
1401 1404 if not self._options.nodiff:
1402 1405 self.stream.write('\nERROR: %s output changed\n' % test)
1403 1406 self.stream.write('!')
1404 1407
1405 1408 self.stream.flush()
1406 1409
1407 1410 def addSuccess(self, test):
1408 1411 with iolock:
1409 1412 super(TestResult, self).addSuccess(test)
1410 1413 self.successes.append(test)
1411 1414
1412 1415 def addError(self, test, err):
1413 1416 super(TestResult, self).addError(test, err)
1414 1417 if self._options.first:
1415 1418 self.stop()
1416 1419
1417 1420 # Polyfill.
1418 1421 def addSkip(self, test, reason):
1419 1422 self.skipped.append((test, reason))
1420 1423 with iolock:
1421 1424 if self.showAll:
1422 1425 self.stream.writeln('skipped %s' % reason)
1423 1426 else:
1424 1427 self.stream.write('s')
1425 1428 self.stream.flush()
1426 1429
1427 1430 def addIgnore(self, test, reason):
1428 1431 self.ignored.append((test, reason))
1429 1432 with iolock:
1430 1433 if self.showAll:
1431 1434 self.stream.writeln('ignored %s' % reason)
1432 1435 else:
1433 1436 if reason not in ('not retesting', "doesn't match keyword"):
1434 1437 self.stream.write('i')
1435 1438 else:
1436 1439 self.testsRun += 1
1437 1440 self.stream.flush()
1438 1441
1439 1442 def addWarn(self, test, reason):
1440 1443 self.warned.append((test, reason))
1441 1444
1442 1445 if self._options.first:
1443 1446 self.stop()
1444 1447
1445 1448 with iolock:
1446 1449 if self.showAll:
1447 1450 self.stream.writeln('warned %s' % reason)
1448 1451 else:
1449 1452 self.stream.write('~')
1450 1453 self.stream.flush()
1451 1454
1452 1455 def addOutputMismatch(self, test, ret, got, expected):
1453 1456 """Record a mismatch in test output for a particular test."""
1454 1457 if self.shouldStop:
1455 1458 # don't print, some other test case already failed and
1456 1459 # printed, we're just stale and probably failed due to our
1457 1460 # temp dir getting cleaned up.
1458 1461 return
1459 1462
1460 1463 accepted = False
1461 1464 lines = []
1462 1465
1463 1466 with iolock:
1464 1467 if self._options.nodiff:
1465 1468 pass
1466 1469 elif self._options.view:
1467 1470 v = self._options.view
1468 1471 if PYTHON3:
1469 1472 v = _bytespath(v)
1470 1473 os.system(b"%s %s %s" %
1471 1474 (v, test.refpath, test.errpath))
1472 1475 else:
1473 1476 servefail, lines = getdiff(expected, got,
1474 1477 test.refpath, test.errpath)
1475 1478 if servefail:
1476 1479 self.addFailure(
1477 1480 test,
1478 1481 'server failed to start (HGPORT=%s)' % test._startport)
1479 1482 raise ReportedTest('server failed to start')
1480 1483 else:
1481 1484 self.stream.write('\n')
1482 1485 for line in lines:
1483 1486 if PYTHON3:
1484 1487 self.stream.flush()
1485 1488 self.stream.buffer.write(line)
1486 1489 self.stream.buffer.flush()
1487 1490 else:
1488 1491 self.stream.write(line)
1489 1492 self.stream.flush()
1490 1493
1491 1494 # handle interactive prompt without releasing iolock
1492 1495 if self._options.interactive:
1493 1496 self.stream.write('Accept this change? [n] ')
1494 1497 answer = sys.stdin.readline().strip()
1495 1498 if answer.lower() in ('y', 'yes'):
1496 1499 if test.name.endswith('.t'):
1497 1500 rename(test.errpath, test.path)
1498 1501 else:
1499 1502 rename(test.errpath, '%s.out' % test.path)
1500 1503 accepted = True
1501 1504 if not accepted:
1502 1505 self.faildata[test.name] = b''.join(lines)
1503 1506
1504 1507 return accepted
1505 1508
1506 1509 def startTest(self, test):
1507 1510 super(TestResult, self).startTest(test)
1508 1511
1509 1512 # os.times module computes the user time and system time spent by
1510 1513 # child's processes along with real elapsed time taken by a process.
1511 1514 # This module has one limitation. It can only work for Linux user
1512 1515 # and not for Windows.
1513 1516 test.started = os.times()
1514 1517 if self._firststarttime is None: # thread racy but irrelevant
1515 1518 self._firststarttime = test.started[4]
1516 1519
1517 1520 def stopTest(self, test, interrupted=False):
1518 1521 super(TestResult, self).stopTest(test)
1519 1522
1520 1523 test.stopped = os.times()
1521 1524
1522 1525 starttime = test.started
1523 1526 endtime = test.stopped
1524 1527 origin = self._firststarttime
1525 1528 self.times.append((test.name,
1526 1529 endtime[2] - starttime[2], # user space CPU time
1527 1530 endtime[3] - starttime[3], # sys space CPU time
1528 1531 endtime[4] - starttime[4], # real time
1529 1532 starttime[4] - origin, # start date in run context
1530 1533 endtime[4] - origin, # end date in run context
1531 1534 ))
1532 1535
1533 1536 if interrupted:
1534 1537 with iolock:
1535 1538 self.stream.writeln('INTERRUPTED: %s (after %d seconds)' % (
1536 1539 test.name, self.times[-1][3]))
1537 1540
1538 1541 class TestSuite(unittest.TestSuite):
1539 1542 """Custom unittest TestSuite that knows how to execute Mercurial tests."""
1540 1543
1541 1544 def __init__(self, testdir, jobs=1, whitelist=None, blacklist=None,
1542 1545 retest=False, keywords=None, loop=False, runs_per_test=1,
1543 1546 loadtest=None, showchannels=False,
1544 1547 *args, **kwargs):
1545 1548 """Create a new instance that can run tests with a configuration.
1546 1549
1547 1550 testdir specifies the directory where tests are executed from. This
1548 1551 is typically the ``tests`` directory from Mercurial's source
1549 1552 repository.
1550 1553
1551 1554 jobs specifies the number of jobs to run concurrently. Each test
1552 1555 executes on its own thread. Tests actually spawn new processes, so
1553 1556 state mutation should not be an issue.
1554 1557
1555 1558 If there is only one job, it will use the main thread.
1556 1559
1557 1560 whitelist and blacklist denote tests that have been whitelisted and
1558 1561 blacklisted, respectively. These arguments don't belong in TestSuite.
1559 1562 Instead, whitelist and blacklist should be handled by the thing that
1560 1563 populates the TestSuite with tests. They are present to preserve
1561 1564 backwards compatible behavior which reports skipped tests as part
1562 1565 of the results.
1563 1566
1564 1567 retest denotes whether to retest failed tests. This arguably belongs
1565 1568 outside of TestSuite.
1566 1569
1567 1570 keywords denotes key words that will be used to filter which tests
1568 1571 to execute. This arguably belongs outside of TestSuite.
1569 1572
1570 1573 loop denotes whether to loop over tests forever.
1571 1574 """
1572 1575 super(TestSuite, self).__init__(*args, **kwargs)
1573 1576
1574 1577 self._jobs = jobs
1575 1578 self._whitelist = whitelist
1576 1579 self._blacklist = blacklist
1577 1580 self._retest = retest
1578 1581 self._keywords = keywords
1579 1582 self._loop = loop
1580 1583 self._runs_per_test = runs_per_test
1581 1584 self._loadtest = loadtest
1582 1585 self._showchannels = showchannels
1583 1586
1584 1587 def run(self, result):
1585 1588 # We have a number of filters that need to be applied. We do this
1586 1589 # here instead of inside Test because it makes the running logic for
1587 1590 # Test simpler.
1588 1591 tests = []
1589 1592 num_tests = [0]
1590 1593 for test in self._tests:
1591 1594 def get():
1592 1595 num_tests[0] += 1
1593 1596 if getattr(test, 'should_reload', False):
1594 1597 return self._loadtest(test.path, num_tests[0])
1595 1598 return test
1596 1599 if not os.path.exists(test.path):
1597 1600 result.addSkip(test, "Doesn't exist")
1598 1601 continue
1599 1602
1600 1603 if not (self._whitelist and test.name in self._whitelist):
1601 1604 if self._blacklist and test.bname in self._blacklist:
1602 1605 result.addSkip(test, 'blacklisted')
1603 1606 continue
1604 1607
1605 1608 if self._retest and not os.path.exists(test.errpath):
1606 1609 result.addIgnore(test, 'not retesting')
1607 1610 continue
1608 1611
1609 1612 if self._keywords:
1610 1613 f = open(test.path, 'rb')
1611 1614 t = f.read().lower() + test.bname.lower()
1612 1615 f.close()
1613 1616 ignored = False
1614 1617 for k in self._keywords.lower().split():
1615 1618 if k not in t:
1616 1619 result.addIgnore(test, "doesn't match keyword")
1617 1620 ignored = True
1618 1621 break
1619 1622
1620 1623 if ignored:
1621 1624 continue
1622 1625 for _ in xrange(self._runs_per_test):
1623 1626 tests.append(get())
1624 1627
1625 1628 runtests = list(tests)
1626 1629 done = queue.Queue()
1627 1630 running = 0
1628 1631
1629 1632 channels = [""] * self._jobs
1630 1633
1631 1634 def job(test, result):
1632 1635 for n, v in enumerate(channels):
1633 1636 if not v:
1634 1637 channel = n
1635 1638 break
1636 1639 channels[channel] = "=" + test.name[5:].split(".")[0]
1637 1640 try:
1638 1641 test(result)
1639 1642 done.put(None)
1640 1643 except KeyboardInterrupt:
1641 1644 pass
1642 1645 except: # re-raises
1643 1646 done.put(('!', test, 'run-test raised an error, see traceback'))
1644 1647 raise
1645 1648 try:
1646 1649 channels[channel] = ''
1647 1650 except IndexError:
1648 1651 pass
1649 1652
1650 1653 def stat():
1651 1654 count = 0
1652 1655 while channels:
1653 1656 d = '\n%03s ' % count
1654 1657 for n, v in enumerate(channels):
1655 1658 if v:
1656 1659 d += v[0]
1657 1660 channels[n] = v[1:] or '.'
1658 1661 else:
1659 1662 d += ' '
1660 1663 d += ' '
1661 1664 with iolock:
1662 1665 sys.stdout.write(d + ' ')
1663 1666 sys.stdout.flush()
1664 1667 for x in xrange(10):
1665 1668 if channels:
1666 1669 time.sleep(.1)
1667 1670 count += 1
1668 1671
1669 1672 stoppedearly = False
1670 1673
1671 1674 if self._showchannels:
1672 1675 statthread = threading.Thread(target=stat, name="stat")
1673 1676 statthread.start()
1674 1677
1675 1678 try:
1676 1679 while tests or running:
1677 1680 if not done.empty() or running == self._jobs or not tests:
1678 1681 try:
1679 1682 done.get(True, 1)
1680 1683 running -= 1
1681 1684 if result and result.shouldStop:
1682 1685 stoppedearly = True
1683 1686 break
1684 1687 except queue.Empty:
1685 1688 continue
1686 1689 if tests and not running == self._jobs:
1687 1690 test = tests.pop(0)
1688 1691 if self._loop:
1689 1692 if getattr(test, 'should_reload', False):
1690 1693 num_tests[0] += 1
1691 1694 tests.append(
1692 1695 self._loadtest(test.name, num_tests[0]))
1693 1696 else:
1694 1697 tests.append(test)
1695 1698 if self._jobs == 1:
1696 1699 job(test, result)
1697 1700 else:
1698 1701 t = threading.Thread(target=job, name=test.name,
1699 1702 args=(test, result))
1700 1703 t.start()
1701 1704 running += 1
1702 1705
1703 1706 # If we stop early we still need to wait on started tests to
1704 1707 # finish. Otherwise, there is a race between the test completing
1705 1708 # and the test's cleanup code running. This could result in the
1706 1709 # test reporting incorrect.
1707 1710 if stoppedearly:
1708 1711 while running:
1709 1712 try:
1710 1713 done.get(True, 1)
1711 1714 running -= 1
1712 1715 except queue.Empty:
1713 1716 continue
1714 1717 except KeyboardInterrupt:
1715 1718 for test in runtests:
1716 1719 test.abort()
1717 1720
1718 1721 channels = []
1719 1722
1720 1723 return result
1721 1724
1722 1725 # Save the most recent 5 wall-clock runtimes of each test to a
1723 1726 # human-readable text file named .testtimes. Tests are sorted
1724 1727 # alphabetically, while times for each test are listed from oldest to
1725 1728 # newest.
1726 1729
1727 1730 def loadtimes(testdir):
1728 1731 times = []
1729 1732 try:
1730 1733 with open(os.path.join(testdir, b'.testtimes-')) as fp:
1731 1734 for line in fp:
1732 1735 ts = line.split()
1733 1736 times.append((ts[0], [float(t) for t in ts[1:]]))
1734 1737 except IOError as err:
1735 1738 if err.errno != errno.ENOENT:
1736 1739 raise
1737 1740 return times
1738 1741
1739 1742 def savetimes(testdir, result):
1740 1743 saved = dict(loadtimes(testdir))
1741 1744 maxruns = 5
1742 1745 skipped = set([str(t[0]) for t in result.skipped])
1743 1746 for tdata in result.times:
1744 1747 test, real = tdata[0], tdata[3]
1745 1748 if test not in skipped:
1746 1749 ts = saved.setdefault(test, [])
1747 1750 ts.append(real)
1748 1751 ts[:] = ts[-maxruns:]
1749 1752
1750 1753 fd, tmpname = tempfile.mkstemp(prefix=b'.testtimes',
1751 1754 dir=testdir, text=True)
1752 1755 with os.fdopen(fd, 'w') as fp:
1753 1756 for name, ts in sorted(saved.items()):
1754 1757 fp.write('%s %s\n' % (name, ' '.join(['%.3f' % (t,) for t in ts])))
1755 1758 timepath = os.path.join(testdir, b'.testtimes')
1756 1759 try:
1757 1760 os.unlink(timepath)
1758 1761 except OSError:
1759 1762 pass
1760 1763 try:
1761 1764 os.rename(tmpname, timepath)
1762 1765 except OSError:
1763 1766 pass
1764 1767
1765 1768 class TextTestRunner(unittest.TextTestRunner):
1766 1769 """Custom unittest test runner that uses appropriate settings."""
1767 1770
1768 1771 def __init__(self, runner, *args, **kwargs):
1769 1772 super(TextTestRunner, self).__init__(*args, **kwargs)
1770 1773
1771 1774 self._runner = runner
1772 1775
1773 1776 def run(self, test):
1774 1777 result = TestResult(self._runner.options, self.stream,
1775 1778 self.descriptions, self.verbosity)
1776 1779
1777 1780 test(result)
1778 1781
1779 1782 failed = len(result.failures)
1780 1783 warned = len(result.warned)
1781 1784 skipped = len(result.skipped)
1782 1785 ignored = len(result.ignored)
1783 1786
1784 1787 with iolock:
1785 1788 self.stream.writeln('')
1786 1789
1787 1790 if not self._runner.options.noskips:
1788 1791 for test, msg in result.skipped:
1789 1792 self.stream.writeln('Skipped %s: %s' % (test.name, msg))
1790 1793 for test, msg in result.warned:
1791 1794 self.stream.writeln('Warned %s: %s' % (test.name, msg))
1792 1795 for test, msg in result.failures:
1793 1796 self.stream.writeln('Failed %s: %s' % (test.name, msg))
1794 1797 for test, msg in result.errors:
1795 1798 self.stream.writeln('Errored %s: %s' % (test.name, msg))
1796 1799
1797 1800 if self._runner.options.xunit:
1798 1801 with open(self._runner.options.xunit, 'wb') as xuf:
1799 1802 timesd = dict((t[0], t[3]) for t in result.times)
1800 1803 doc = minidom.Document()
1801 1804 s = doc.createElement('testsuite')
1802 1805 s.setAttribute('name', 'run-tests')
1803 1806 s.setAttribute('tests', str(result.testsRun))
1804 1807 s.setAttribute('errors', "0") # TODO
1805 1808 s.setAttribute('failures', str(failed))
1806 1809 s.setAttribute('skipped', str(skipped + ignored))
1807 1810 doc.appendChild(s)
1808 1811 for tc in result.successes:
1809 1812 t = doc.createElement('testcase')
1810 1813 t.setAttribute('name', tc.name)
1811 1814 t.setAttribute('time', '%.3f' % timesd[tc.name])
1812 1815 s.appendChild(t)
1813 1816 for tc, err in sorted(result.faildata.items()):
1814 1817 t = doc.createElement('testcase')
1815 1818 t.setAttribute('name', tc)
1816 1819 t.setAttribute('time', '%.3f' % timesd[tc])
1817 1820 # createCDATASection expects a unicode or it will
1818 1821 # convert using default conversion rules, which will
1819 1822 # fail if string isn't ASCII.
1820 1823 err = cdatasafe(err).decode('utf-8', 'replace')
1821 1824 cd = doc.createCDATASection(err)
1822 1825 t.appendChild(cd)
1823 1826 s.appendChild(t)
1824 1827 xuf.write(doc.toprettyxml(indent=' ', encoding='utf-8'))
1825 1828
1826 1829 if self._runner.options.json:
1827 1830 jsonpath = os.path.join(self._runner._testdir, b'report.json')
1828 1831 with open(jsonpath, 'w') as fp:
1829 1832 timesd = {}
1830 1833 for tdata in result.times:
1831 1834 test = tdata[0]
1832 1835 timesd[test] = tdata[1:]
1833 1836
1834 1837 outcome = {}
1835 1838 groups = [('success', ((tc, None)
1836 1839 for tc in result.successes)),
1837 1840 ('failure', result.failures),
1838 1841 ('skip', result.skipped)]
1839 1842 for res, testcases in groups:
1840 1843 for tc, __ in testcases:
1841 1844 if tc.name in timesd:
1842 1845 diff = result.faildata.get(tc.name, b'')
1843 1846 tres = {'result': res,
1844 1847 'time': ('%0.3f' % timesd[tc.name][2]),
1845 1848 'cuser': ('%0.3f' % timesd[tc.name][0]),
1846 1849 'csys': ('%0.3f' % timesd[tc.name][1]),
1847 1850 'start': ('%0.3f' % timesd[tc.name][3]),
1848 1851 'end': ('%0.3f' % timesd[tc.name][4]),
1849 1852 'diff': diff.decode('unicode_escape'),
1850 1853 }
1851 1854 else:
1852 1855 # blacklisted test
1853 1856 tres = {'result': res}
1854 1857
1855 1858 outcome[tc.name] = tres
1856 1859 jsonout = json.dumps(outcome, sort_keys=True, indent=4,
1857 1860 separators=(',', ': '))
1858 1861 fp.writelines(("testreport =", jsonout))
1859 1862
1860 1863 self._runner._checkhglib('Tested')
1861 1864
1862 1865 savetimes(self._runner._testdir, result)
1863 1866
1864 1867 if failed and self._runner.options.known_good_rev:
1865 1868 def nooutput(args):
1866 1869 p = subprocess.Popen(args, stderr=subprocess.STDOUT,
1867 1870 stdout=subprocess.PIPE)
1868 1871 p.stdout.read()
1869 1872 p.wait()
1870 1873 for test, msg in result.failures:
1871 1874 nooutput(['hg', 'bisect', '--reset']),
1872 1875 nooutput(['hg', 'bisect', '--bad', '.'])
1873 1876 nooutput(['hg', 'bisect', '--good',
1874 1877 self._runner.options.known_good_rev])
1875 1878 # TODO: we probably need to forward some options
1876 1879 # that alter hg's behavior inside the tests.
1877 1880 rtc = '%s %s %s' % (sys.executable, sys.argv[0], test)
1878 1881 sub = subprocess.Popen(['hg', 'bisect', '--command', rtc],
1879 1882 stderr=subprocess.STDOUT,
1880 1883 stdout=subprocess.PIPE)
1881 1884 data = sub.stdout.read()
1882 1885 sub.wait()
1883 1886 m = re.search(
1884 1887 (r'\nThe first (?P<goodbad>bad|good) revision '
1885 1888 r'is:\nchangeset: +\d+:(?P<node>[a-f0-9]+)\n.*\n'
1886 1889 r'summary: +(?P<summary>[^\n]+)\n'),
1887 1890 data, (re.MULTILINE | re.DOTALL))
1888 1891 if m is None:
1889 1892 self.stream.writeln(
1890 1893 'Failed to identify failure point for %s' % test)
1891 1894 continue
1892 1895 dat = m.groupdict()
1893 1896 verb = 'broken' if dat['goodbad'] == 'bad' else 'fixed'
1894 1897 self.stream.writeln(
1895 1898 '%s %s by %s (%s)' % (
1896 1899 test, verb, dat['node'], dat['summary']))
1897 1900 self.stream.writeln(
1898 1901 '# Ran %d tests, %d skipped, %d warned, %d failed.'
1899 1902 % (result.testsRun,
1900 1903 skipped + ignored, warned, failed))
1901 1904 if failed:
1902 1905 self.stream.writeln('python hash seed: %s' %
1903 1906 os.environ['PYTHONHASHSEED'])
1904 1907 if self._runner.options.time:
1905 1908 self.printtimes(result.times)
1906 1909
1907 1910 return result
1908 1911
1909 1912 def printtimes(self, times):
1910 1913 # iolock held by run
1911 1914 self.stream.writeln('# Producing time report')
1912 1915 times.sort(key=lambda t: (t[3]))
1913 1916 cols = '%7.3f %7.3f %7.3f %7.3f %7.3f %s'
1914 1917 self.stream.writeln('%-7s %-7s %-7s %-7s %-7s %s' %
1915 1918 ('start', 'end', 'cuser', 'csys', 'real', 'Test'))
1916 1919 for tdata in times:
1917 1920 test = tdata[0]
1918 1921 cuser, csys, real, start, end = tdata[1:6]
1919 1922 self.stream.writeln(cols % (start, end, cuser, csys, real, test))
1920 1923
1921 1924 class TestRunner(object):
1922 1925 """Holds context for executing tests.
1923 1926
1924 1927 Tests rely on a lot of state. This object holds it for them.
1925 1928 """
1926 1929
1927 1930 # Programs required to run tests.
1928 1931 REQUIREDTOOLS = [
1929 1932 os.path.basename(_bytespath(sys.executable)),
1930 1933 b'diff',
1931 1934 b'grep',
1932 1935 b'unzip',
1933 1936 b'gunzip',
1934 1937 b'bunzip2',
1935 1938 b'sed',
1936 1939 ]
1937 1940
1938 1941 # Maps file extensions to test class.
1939 1942 TESTTYPES = [
1940 1943 (b'.py', PythonTest),
1941 1944 (b'.t', TTest),
1942 1945 ]
1943 1946
1944 1947 def __init__(self):
1945 1948 self.options = None
1946 1949 self._hgroot = None
1947 1950 self._testdir = None
1948 1951 self._hgtmp = None
1949 1952 self._installdir = None
1950 1953 self._bindir = None
1951 1954 self._tmpbinddir = None
1952 1955 self._pythondir = None
1953 1956 self._coveragefile = None
1954 1957 self._createdfiles = []
1955 1958 self._hgcommand = None
1956 1959 self._hgpath = None
1957 1960 self._portoffset = 0
1958 1961 self._ports = {}
1959 1962
1960 1963 def run(self, args, parser=None):
1961 1964 """Run the test suite."""
1962 1965 oldmask = os.umask(0o22)
1963 1966 try:
1964 1967 parser = parser or getparser()
1965 1968 options, args = parseargs(args, parser)
1966 1969 # positional arguments are paths to test files to run, so
1967 1970 # we make sure they're all bytestrings
1968 1971 args = [_bytespath(a) for a in args]
1969 1972 self.options = options
1970 1973
1971 1974 self._checktools()
1972 1975 tests = self.findtests(args)
1973 1976 if options.profile_runner:
1974 1977 import statprof
1975 1978 statprof.start()
1976 1979 result = self._run(tests)
1977 1980 if options.profile_runner:
1978 1981 statprof.stop()
1979 1982 statprof.display()
1980 1983 return result
1981 1984
1982 1985 finally:
1983 1986 os.umask(oldmask)
1984 1987
1985 1988 def _run(self, tests):
1986 1989 if self.options.random:
1987 1990 random.shuffle(tests)
1988 1991 else:
1989 1992 # keywords for slow tests
1990 1993 slow = {b'svn': 10,
1991 1994 b'cvs': 10,
1992 1995 b'hghave': 10,
1993 1996 b'largefiles-update': 10,
1994 1997 b'run-tests': 10,
1995 1998 b'corruption': 10,
1996 1999 b'race': 10,
1997 2000 b'i18n': 10,
1998 2001 b'check': 100,
1999 2002 b'gendoc': 100,
2000 2003 b'contrib-perf': 200,
2001 2004 }
2002 2005 perf = {}
2003 2006 def sortkey(f):
2004 2007 # run largest tests first, as they tend to take the longest
2005 2008 try:
2006 2009 return perf[f]
2007 2010 except KeyError:
2008 2011 try:
2009 2012 val = -os.stat(f).st_size
2010 2013 except OSError as e:
2011 2014 if e.errno != errno.ENOENT:
2012 2015 raise
2013 2016 perf[f] = -1e9 # file does not exist, tell early
2014 2017 return -1e9
2015 2018 for kw, mul in slow.items():
2016 2019 if kw in f:
2017 2020 val *= mul
2018 2021 if f.endswith(b'.py'):
2019 2022 val /= 10.0
2020 2023 perf[f] = val / 1000.0
2021 2024 return perf[f]
2022 2025 tests.sort(key=sortkey)
2023 2026
2024 2027 self._testdir = osenvironb[b'TESTDIR'] = getattr(
2025 2028 os, 'getcwdb', os.getcwd)()
2026 2029
2027 2030 if 'PYTHONHASHSEED' not in os.environ:
2028 2031 # use a random python hash seed all the time
2029 2032 # we do the randomness ourself to know what seed is used
2030 2033 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
2031 2034
2032 2035 if self.options.tmpdir:
2033 2036 self.options.keep_tmpdir = True
2034 2037 tmpdir = _bytespath(self.options.tmpdir)
2035 2038 if os.path.exists(tmpdir):
2036 2039 # Meaning of tmpdir has changed since 1.3: we used to create
2037 2040 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
2038 2041 # tmpdir already exists.
2039 2042 print("error: temp dir %r already exists" % tmpdir)
2040 2043 return 1
2041 2044
2042 2045 # Automatically removing tmpdir sounds convenient, but could
2043 2046 # really annoy anyone in the habit of using "--tmpdir=/tmp"
2044 2047 # or "--tmpdir=$HOME".
2045 2048 #vlog("# Removing temp dir", tmpdir)
2046 2049 #shutil.rmtree(tmpdir)
2047 2050 os.makedirs(tmpdir)
2048 2051 else:
2049 2052 d = None
2050 2053 if os.name == 'nt':
2051 2054 # without this, we get the default temp dir location, but
2052 2055 # in all lowercase, which causes troubles with paths (issue3490)
2053 2056 d = osenvironb.get(b'TMP', None)
2054 2057 tmpdir = tempfile.mkdtemp(b'', b'hgtests.', d)
2055 2058
2056 2059 self._hgtmp = osenvironb[b'HGTMP'] = (
2057 2060 os.path.realpath(tmpdir))
2058 2061
2059 2062 if self.options.with_hg:
2060 2063 self._installdir = None
2061 2064 whg = self.options.with_hg
2062 2065 self._bindir = os.path.dirname(os.path.realpath(whg))
2063 2066 assert isinstance(self._bindir, bytes)
2064 2067 self._hgcommand = os.path.basename(whg)
2065 2068 self._tmpbindir = os.path.join(self._hgtmp, b'install', b'bin')
2066 2069 os.makedirs(self._tmpbindir)
2067 2070
2068 2071 # This looks redundant with how Python initializes sys.path from
2069 2072 # the location of the script being executed. Needed because the
2070 2073 # "hg" specified by --with-hg is not the only Python script
2071 2074 # executed in the test suite that needs to import 'mercurial'
2072 2075 # ... which means it's not really redundant at all.
2073 2076 self._pythondir = self._bindir
2074 2077 else:
2075 2078 self._installdir = os.path.join(self._hgtmp, b"install")
2076 2079 self._bindir = os.path.join(self._installdir, b"bin")
2077 2080 self._hgcommand = b'hg'
2078 2081 self._tmpbindir = self._bindir
2079 2082 self._pythondir = os.path.join(self._installdir, b"lib", b"python")
2080 2083
2081 2084 # set CHGHG, then replace "hg" command by "chg"
2082 2085 chgbindir = self._bindir
2083 2086 if self.options.chg or self.options.with_chg:
2084 2087 osenvironb[b'CHGHG'] = os.path.join(self._bindir, self._hgcommand)
2085 2088 else:
2086 2089 osenvironb.pop(b'CHGHG', None) # drop flag for hghave
2087 2090 if self.options.chg:
2088 2091 self._hgcommand = b'chg'
2089 2092 elif self.options.with_chg:
2090 2093 chgbindir = os.path.dirname(os.path.realpath(self.options.with_chg))
2091 2094 self._hgcommand = os.path.basename(self.options.with_chg)
2092 2095
2093 2096 osenvironb[b"BINDIR"] = self._bindir
2094 2097 osenvironb[b"PYTHON"] = PYTHON
2095 2098
2096 2099 if self.options.with_python3:
2097 2100 osenvironb[b'PYTHON3'] = self.options.with_python3
2098 2101
2099 2102 fileb = _bytespath(__file__)
2100 2103 runtestdir = os.path.abspath(os.path.dirname(fileb))
2101 2104 osenvironb[b'RUNTESTDIR'] = runtestdir
2102 2105 if PYTHON3:
2103 2106 sepb = _bytespath(os.pathsep)
2104 2107 else:
2105 2108 sepb = os.pathsep
2106 2109 path = [self._bindir, runtestdir] + osenvironb[b"PATH"].split(sepb)
2107 2110 if os.path.islink(__file__):
2108 2111 # test helper will likely be at the end of the symlink
2109 2112 realfile = os.path.realpath(fileb)
2110 2113 realdir = os.path.abspath(os.path.dirname(realfile))
2111 2114 path.insert(2, realdir)
2112 2115 if chgbindir != self._bindir:
2113 2116 path.insert(1, chgbindir)
2114 2117 if self._testdir != runtestdir:
2115 2118 path = [self._testdir] + path
2116 2119 if self._tmpbindir != self._bindir:
2117 2120 path = [self._tmpbindir] + path
2118 2121 osenvironb[b"PATH"] = sepb.join(path)
2119 2122
2120 2123 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
2121 2124 # can run .../tests/run-tests.py test-foo where test-foo
2122 2125 # adds an extension to HGRC. Also include run-test.py directory to
2123 2126 # import modules like heredoctest.
2124 2127 pypath = [self._pythondir, self._testdir, runtestdir]
2125 2128 # We have to augment PYTHONPATH, rather than simply replacing
2126 2129 # it, in case external libraries are only available via current
2127 2130 # PYTHONPATH. (In particular, the Subversion bindings on OS X
2128 2131 # are in /opt/subversion.)
2129 2132 oldpypath = osenvironb.get(IMPL_PATH)
2130 2133 if oldpypath:
2131 2134 pypath.append(oldpypath)
2132 2135 osenvironb[IMPL_PATH] = sepb.join(pypath)
2133 2136
2134 2137 if self.options.pure:
2135 2138 os.environ["HGTEST_RUN_TESTS_PURE"] = "--pure"
2136 2139 os.environ["HGMODULEPOLICY"] = "py"
2137 2140
2138 2141 if self.options.allow_slow_tests:
2139 2142 os.environ["HGTEST_SLOW"] = "slow"
2140 2143 elif 'HGTEST_SLOW' in os.environ:
2141 2144 del os.environ['HGTEST_SLOW']
2142 2145
2143 2146 self._coveragefile = os.path.join(self._testdir, b'.coverage')
2144 2147
2145 2148 vlog("# Using TESTDIR", self._testdir)
2146 2149 vlog("# Using RUNTESTDIR", osenvironb[b'RUNTESTDIR'])
2147 2150 vlog("# Using HGTMP", self._hgtmp)
2148 2151 vlog("# Using PATH", os.environ["PATH"])
2149 2152 vlog("# Using", IMPL_PATH, osenvironb[IMPL_PATH])
2150 2153
2151 2154 try:
2152 2155 return self._runtests(tests) or 0
2153 2156 finally:
2154 2157 time.sleep(.1)
2155 2158 self._cleanup()
2156 2159
2157 2160 def findtests(self, args):
2158 2161 """Finds possible test files from arguments.
2159 2162
2160 2163 If you wish to inject custom tests into the test harness, this would
2161 2164 be a good function to monkeypatch or override in a derived class.
2162 2165 """
2163 2166 if not args:
2164 2167 if self.options.changed:
2165 2168 proc = Popen4('hg st --rev "%s" -man0 .' %
2166 2169 self.options.changed, None, 0)
2167 2170 stdout, stderr = proc.communicate()
2168 2171 args = stdout.strip(b'\0').split(b'\0')
2169 2172 else:
2170 2173 args = os.listdir(b'.')
2171 2174
2172 2175 return [t for t in args
2173 2176 if os.path.basename(t).startswith(b'test-')
2174 2177 and (t.endswith(b'.py') or t.endswith(b'.t'))]
2175 2178
2176 2179 def _runtests(self, tests):
2177 2180 try:
2178 2181 if self._installdir:
2179 2182 self._installhg()
2180 2183 self._checkhglib("Testing")
2181 2184 else:
2182 2185 self._usecorrectpython()
2183 2186 if self.options.chg:
2184 2187 assert self._installdir
2185 2188 self._installchg()
2186 2189
2187 2190 if self.options.restart:
2188 2191 orig = list(tests)
2189 2192 while tests:
2190 2193 if os.path.exists(tests[0] + ".err"):
2191 2194 break
2192 2195 tests.pop(0)
2193 2196 if not tests:
2194 2197 print("running all tests")
2195 2198 tests = orig
2196 2199
2197 2200 tests = [self._gettest(t, i) for i, t in enumerate(tests)]
2198 2201
2199 2202 failed = False
2200 2203 warned = False
2201 2204 kws = self.options.keywords
2202 2205 if kws is not None and PYTHON3:
2203 2206 kws = kws.encode('utf-8')
2204 2207
2205 2208 suite = TestSuite(self._testdir,
2206 2209 jobs=self.options.jobs,
2207 2210 whitelist=self.options.whitelisted,
2208 2211 blacklist=self.options.blacklist,
2209 2212 retest=self.options.retest,
2210 2213 keywords=kws,
2211 2214 loop=self.options.loop,
2212 2215 runs_per_test=self.options.runs_per_test,
2213 2216 showchannels=self.options.showchannels,
2214 2217 tests=tests, loadtest=self._gettest)
2215 2218 verbosity = 1
2216 2219 if self.options.verbose:
2217 2220 verbosity = 2
2218 2221 runner = TextTestRunner(self, verbosity=verbosity)
2219 2222 result = runner.run(suite)
2220 2223
2221 2224 if result.failures:
2222 2225 failed = True
2223 2226 if result.warned:
2224 2227 warned = True
2225 2228
2226 2229 if self.options.anycoverage:
2227 2230 self._outputcoverage()
2228 2231 except KeyboardInterrupt:
2229 2232 failed = True
2230 2233 print("\ninterrupted!")
2231 2234
2232 2235 if failed:
2233 2236 return 1
2234 2237 if warned:
2235 2238 return 80
2236 2239
2237 2240 def _getport(self, count):
2238 2241 port = self._ports.get(count) # do we have a cached entry?
2239 2242 if port is None:
2240 2243 portneeded = 3
2241 2244 # above 100 tries we just give up and let test reports failure
2242 2245 for tries in xrange(100):
2243 2246 allfree = True
2244 2247 port = self.options.port + self._portoffset
2245 2248 for idx in xrange(portneeded):
2246 2249 if not checkportisavailable(port + idx):
2247 2250 allfree = False
2248 2251 break
2249 2252 self._portoffset += portneeded
2250 2253 if allfree:
2251 2254 break
2252 2255 self._ports[count] = port
2253 2256 return port
2254 2257
2255 2258 def _gettest(self, test, count):
2256 2259 """Obtain a Test by looking at its filename.
2257 2260
2258 2261 Returns a Test instance. The Test may not be runnable if it doesn't
2259 2262 map to a known type.
2260 2263 """
2261 2264 lctest = test.lower()
2262 2265 testcls = Test
2263 2266
2264 2267 for ext, cls in self.TESTTYPES:
2265 2268 if lctest.endswith(ext):
2266 2269 testcls = cls
2267 2270 break
2268 2271
2269 2272 refpath = os.path.join(self._testdir, test)
2270 2273 tmpdir = os.path.join(self._hgtmp, b'child%d' % count)
2271 2274
2272 2275 t = testcls(refpath, tmpdir,
2273 2276 keeptmpdir=self.options.keep_tmpdir,
2274 2277 debug=self.options.debug,
2275 2278 timeout=self.options.timeout,
2276 2279 startport=self._getport(count),
2277 2280 extraconfigopts=self.options.extra_config_opt,
2278 2281 py3kwarnings=self.options.py3k_warnings,
2279 2282 shell=self.options.shell,
2280 2283 hgcommand=self._hgcommand,
2281 2284 usechg=bool(self.options.with_chg or self.options.chg))
2282 2285 t.should_reload = True
2283 2286 return t
2284 2287
2285 2288 def _cleanup(self):
2286 2289 """Clean up state from this test invocation."""
2287 2290 if self.options.keep_tmpdir:
2288 2291 return
2289 2292
2290 2293 vlog("# Cleaning up HGTMP", self._hgtmp)
2291 2294 shutil.rmtree(self._hgtmp, True)
2292 2295 for f in self._createdfiles:
2293 2296 try:
2294 2297 os.remove(f)
2295 2298 except OSError:
2296 2299 pass
2297 2300
2298 2301 def _usecorrectpython(self):
2299 2302 """Configure the environment to use the appropriate Python in tests."""
2300 2303 # Tests must use the same interpreter as us or bad things will happen.
2301 2304 pyexename = sys.platform == 'win32' and b'python.exe' or b'python'
2302 2305 if getattr(os, 'symlink', None):
2303 2306 vlog("# Making python executable in test path a symlink to '%s'" %
2304 2307 sys.executable)
2305 2308 mypython = os.path.join(self._tmpbindir, pyexename)
2306 2309 try:
2307 2310 if os.readlink(mypython) == sys.executable:
2308 2311 return
2309 2312 os.unlink(mypython)
2310 2313 except OSError as err:
2311 2314 if err.errno != errno.ENOENT:
2312 2315 raise
2313 2316 if self._findprogram(pyexename) != sys.executable:
2314 2317 try:
2315 2318 os.symlink(sys.executable, mypython)
2316 2319 self._createdfiles.append(mypython)
2317 2320 except OSError as err:
2318 2321 # child processes may race, which is harmless
2319 2322 if err.errno != errno.EEXIST:
2320 2323 raise
2321 2324 else:
2322 2325 exedir, exename = os.path.split(sys.executable)
2323 2326 vlog("# Modifying search path to find %s as %s in '%s'" %
2324 2327 (exename, pyexename, exedir))
2325 2328 path = os.environ['PATH'].split(os.pathsep)
2326 2329 while exedir in path:
2327 2330 path.remove(exedir)
2328 2331 os.environ['PATH'] = os.pathsep.join([exedir] + path)
2329 2332 if not self._findprogram(pyexename):
2330 2333 print("WARNING: Cannot find %s in search path" % pyexename)
2331 2334
2332 2335 def _installhg(self):
2333 2336 """Install hg into the test environment.
2334 2337
2335 2338 This will also configure hg with the appropriate testing settings.
2336 2339 """
2337 2340 vlog("# Performing temporary installation of HG")
2338 2341 installerrs = os.path.join(self._hgtmp, b"install.err")
2339 2342 compiler = ''
2340 2343 if self.options.compiler:
2341 2344 compiler = '--compiler ' + self.options.compiler
2342 2345 if self.options.pure:
2343 2346 pure = b"--pure"
2344 2347 else:
2345 2348 pure = b""
2346 2349
2347 2350 # Run installer in hg root
2348 2351 script = os.path.realpath(sys.argv[0])
2349 2352 exe = sys.executable
2350 2353 if PYTHON3:
2351 2354 compiler = _bytespath(compiler)
2352 2355 script = _bytespath(script)
2353 2356 exe = _bytespath(exe)
2354 2357 hgroot = os.path.dirname(os.path.dirname(script))
2355 2358 self._hgroot = hgroot
2356 2359 os.chdir(hgroot)
2357 2360 nohome = b'--home=""'
2358 2361 if os.name == 'nt':
2359 2362 # The --home="" trick works only on OS where os.sep == '/'
2360 2363 # because of a distutils convert_path() fast-path. Avoid it at
2361 2364 # least on Windows for now, deal with .pydistutils.cfg bugs
2362 2365 # when they happen.
2363 2366 nohome = b''
2364 2367 cmd = (b'%(exe)s setup.py %(pure)s clean --all'
2365 2368 b' build %(compiler)s --build-base="%(base)s"'
2366 2369 b' install --force --prefix="%(prefix)s"'
2367 2370 b' --install-lib="%(libdir)s"'
2368 2371 b' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
2369 2372 % {b'exe': exe, b'pure': pure,
2370 2373 b'compiler': compiler,
2371 2374 b'base': os.path.join(self._hgtmp, b"build"),
2372 2375 b'prefix': self._installdir, b'libdir': self._pythondir,
2373 2376 b'bindir': self._bindir,
2374 2377 b'nohome': nohome, b'logfile': installerrs})
2375 2378
2376 2379 # setuptools requires install directories to exist.
2377 2380 def makedirs(p):
2378 2381 try:
2379 2382 os.makedirs(p)
2380 2383 except OSError as e:
2381 2384 if e.errno != errno.EEXIST:
2382 2385 raise
2383 2386 makedirs(self._pythondir)
2384 2387 makedirs(self._bindir)
2385 2388
2386 2389 vlog("# Running", cmd)
2387 2390 if os.system(cmd) == 0:
2388 2391 if not self.options.verbose:
2389 2392 try:
2390 2393 os.remove(installerrs)
2391 2394 except OSError as e:
2392 2395 if e.errno != errno.ENOENT:
2393 2396 raise
2394 2397 else:
2395 2398 f = open(installerrs, 'rb')
2396 2399 for line in f:
2397 2400 if PYTHON3:
2398 2401 sys.stdout.buffer.write(line)
2399 2402 else:
2400 2403 sys.stdout.write(line)
2401 2404 f.close()
2402 2405 sys.exit(1)
2403 2406 os.chdir(self._testdir)
2404 2407
2405 2408 self._usecorrectpython()
2406 2409
2407 2410 if self.options.py3k_warnings and not self.options.anycoverage:
2408 2411 vlog("# Updating hg command to enable Py3k Warnings switch")
2409 2412 f = open(os.path.join(self._bindir, 'hg'), 'rb')
2410 2413 lines = [line.rstrip() for line in f]
2411 2414 lines[0] += ' -3'
2412 2415 f.close()
2413 2416 f = open(os.path.join(self._bindir, 'hg'), 'wb')
2414 2417 for line in lines:
2415 2418 f.write(line + '\n')
2416 2419 f.close()
2417 2420
2418 2421 hgbat = os.path.join(self._bindir, b'hg.bat')
2419 2422 if os.path.isfile(hgbat):
2420 2423 # hg.bat expects to be put in bin/scripts while run-tests.py
2421 2424 # installation layout put it in bin/ directly. Fix it
2422 2425 f = open(hgbat, 'rb')
2423 2426 data = f.read()
2424 2427 f.close()
2425 2428 if b'"%~dp0..\python" "%~dp0hg" %*' in data:
2426 2429 data = data.replace(b'"%~dp0..\python" "%~dp0hg" %*',
2427 2430 b'"%~dp0python" "%~dp0hg" %*')
2428 2431 f = open(hgbat, 'wb')
2429 2432 f.write(data)
2430 2433 f.close()
2431 2434 else:
2432 2435 print('WARNING: cannot fix hg.bat reference to python.exe')
2433 2436
2434 2437 if self.options.anycoverage:
2435 2438 custom = os.path.join(self._testdir, 'sitecustomize.py')
2436 2439 target = os.path.join(self._pythondir, 'sitecustomize.py')
2437 2440 vlog('# Installing coverage trigger to %s' % target)
2438 2441 shutil.copyfile(custom, target)
2439 2442 rc = os.path.join(self._testdir, '.coveragerc')
2440 2443 vlog('# Installing coverage rc to %s' % rc)
2441 2444 os.environ['COVERAGE_PROCESS_START'] = rc
2442 2445 covdir = os.path.join(self._installdir, '..', 'coverage')
2443 2446 try:
2444 2447 os.mkdir(covdir)
2445 2448 except OSError as e:
2446 2449 if e.errno != errno.EEXIST:
2447 2450 raise
2448 2451
2449 2452 os.environ['COVERAGE_DIR'] = covdir
2450 2453
2451 2454 def _checkhglib(self, verb):
2452 2455 """Ensure that the 'mercurial' package imported by python is
2453 2456 the one we expect it to be. If not, print a warning to stderr."""
2454 2457 if ((self._bindir == self._pythondir) and
2455 2458 (self._bindir != self._tmpbindir)):
2456 2459 # The pythondir has been inferred from --with-hg flag.
2457 2460 # We cannot expect anything sensible here.
2458 2461 return
2459 2462 expecthg = os.path.join(self._pythondir, b'mercurial')
2460 2463 actualhg = self._gethgpath()
2461 2464 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
2462 2465 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
2463 2466 ' (expected %s)\n'
2464 2467 % (verb, actualhg, expecthg))
2465 2468 def _gethgpath(self):
2466 2469 """Return the path to the mercurial package that is actually found by
2467 2470 the current Python interpreter."""
2468 2471 if self._hgpath is not None:
2469 2472 return self._hgpath
2470 2473
2471 2474 cmd = b'%s -c "import mercurial; print (mercurial.__path__[0])"'
2472 2475 cmd = cmd % PYTHON
2473 2476 if PYTHON3:
2474 2477 cmd = _strpath(cmd)
2475 2478 pipe = os.popen(cmd)
2476 2479 try:
2477 2480 self._hgpath = _bytespath(pipe.read().strip())
2478 2481 finally:
2479 2482 pipe.close()
2480 2483
2481 2484 return self._hgpath
2482 2485
2483 2486 def _installchg(self):
2484 2487 """Install chg into the test environment"""
2485 2488 vlog('# Performing temporary installation of CHG')
2486 2489 assert os.path.dirname(self._bindir) == self._installdir
2487 2490 assert self._hgroot, 'must be called after _installhg()'
2488 2491 cmd = (b'"%(make)s" clean install PREFIX="%(prefix)s"'
2489 2492 % {b'make': 'make', # TODO: switch by option or environment?
2490 2493 b'prefix': self._installdir})
2491 2494 cwd = os.path.join(self._hgroot, b'contrib', b'chg')
2492 2495 vlog("# Running", cmd)
2493 2496 proc = subprocess.Popen(cmd, shell=True, cwd=cwd,
2494 2497 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
2495 2498 stderr=subprocess.STDOUT)
2496 2499 out, _err = proc.communicate()
2497 2500 if proc.returncode != 0:
2498 2501 if PYTHON3:
2499 2502 sys.stdout.buffer.write(out)
2500 2503 else:
2501 2504 sys.stdout.write(out)
2502 2505 sys.exit(1)
2503 2506
2504 2507 def _outputcoverage(self):
2505 2508 """Produce code coverage output."""
2506 2509 import coverage
2507 2510 coverage = coverage.coverage
2508 2511
2509 2512 vlog('# Producing coverage report')
2510 2513 # chdir is the easiest way to get short, relative paths in the
2511 2514 # output.
2512 2515 os.chdir(self._hgroot)
2513 2516 covdir = os.path.join(self._installdir, '..', 'coverage')
2514 2517 cov = coverage(data_file=os.path.join(covdir, 'cov'))
2515 2518
2516 2519 # Map install directory paths back to source directory.
2517 2520 cov.config.paths['srcdir'] = ['.', self._pythondir]
2518 2521
2519 2522 cov.combine()
2520 2523
2521 2524 omit = [os.path.join(x, '*') for x in [self._bindir, self._testdir]]
2522 2525 cov.report(ignore_errors=True, omit=omit)
2523 2526
2524 2527 if self.options.htmlcov:
2525 2528 htmldir = os.path.join(self._testdir, 'htmlcov')
2526 2529 cov.html_report(directory=htmldir, omit=omit)
2527 2530 if self.options.annotate:
2528 2531 adir = os.path.join(self._testdir, 'annotated')
2529 2532 if not os.path.isdir(adir):
2530 2533 os.mkdir(adir)
2531 2534 cov.annotate(directory=adir, omit=omit)
2532 2535
2533 2536 def _findprogram(self, program):
2534 2537 """Search PATH for a executable program"""
2535 2538 dpb = _bytespath(os.defpath)
2536 2539 sepb = _bytespath(os.pathsep)
2537 2540 for p in osenvironb.get(b'PATH', dpb).split(sepb):
2538 2541 name = os.path.join(p, program)
2539 2542 if os.name == 'nt' or os.access(name, os.X_OK):
2540 2543 return name
2541 2544 return None
2542 2545
2543 2546 def _checktools(self):
2544 2547 """Ensure tools required to run tests are present."""
2545 2548 for p in self.REQUIREDTOOLS:
2546 2549 if os.name == 'nt' and not p.endswith('.exe'):
2547 2550 p += '.exe'
2548 2551 found = self._findprogram(p)
2549 2552 if found:
2550 2553 vlog("# Found prerequisite", p, "at", found)
2551 2554 else:
2552 2555 print("WARNING: Did not find prerequisite tool: %s " %
2553 2556 p.decode("utf-8"))
2554 2557
2555 2558 if __name__ == '__main__':
2556 2559 runner = TestRunner()
2557 2560
2558 2561 try:
2559 2562 import msvcrt
2560 2563 msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
2561 2564 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
2562 2565 msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
2563 2566 except ImportError:
2564 2567 pass
2565 2568
2566 2569 sys.exit(runner.run(sys.argv[1:]))
General Comments 0
You need to be logged in to leave comments. Login now