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