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