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