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