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