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