##// END OF EJS Templates
tests: avoid shellquoting bytes on Python 3...
Augie Fackler -
r39433:f57682dc default
parent child Browse files
Show More
@@ -1,3219 +1,3223 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 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 quoted = shellquote(cmd.strip()).replace(b'\\', b'\\\\')
1355 if isinstance(cmd, str):
1356 quoted = shellquote(cmd.strip())
1357 else:
1358 quoted = shellquote(cmd.strip().decode('utf8')).encode('utf8')
1359 quoted = quoted.replace(b'\\', b'\\\\')
1356 1360 if active:
1357 1361 script.append(
1358 1362 b'echo END %s %s >> "$HGCATAPULTSERVERPIPE"\n' % (
1359 1363 session, active[0]))
1360 1364 script.append(
1361 1365 b'echo START %s %s >> "$HGCATAPULTSERVERPIPE"\n' % (
1362 1366 session, quoted))
1363 1367 active[0:] = [quoted]
1364 1368
1365 1369 script = []
1366 1370
1367 1371 # After we run the shell script, we re-unify the script output
1368 1372 # with non-active parts of the source, with synchronization by our
1369 1373 # SALT line number markers. The after table contains the non-active
1370 1374 # components, ordered by line number.
1371 1375 after = {}
1372 1376
1373 1377 # Expected shell script output.
1374 1378 expected = {}
1375 1379
1376 1380 pos = prepos = -1
1377 1381
1378 1382 # True or False when in a true or false conditional section
1379 1383 skipping = None
1380 1384
1381 1385 # We keep track of whether or not we're in a Python block so we
1382 1386 # can generate the surrounding doctest magic.
1383 1387 inpython = False
1384 1388
1385 1389 if self._debug:
1386 1390 script.append(b'set -x\n')
1387 1391 if self._hgcommand != b'hg':
1388 1392 script.append(b'alias hg="%s"\n' % self._hgcommand)
1389 1393 if os.getenv('MSYSTEM'):
1390 1394 script.append(b'alias pwd="pwd -W"\n')
1391 1395
1392 1396 hgcatapult = os.getenv('HGCATAPULTSERVERPIPE')
1393 1397 if hgcatapult and hgcatapult != os.devnull:
1394 1398 # Kludge: use a while loop to keep the pipe from getting
1395 1399 # closed by our echo commands. The still-running file gets
1396 1400 # reaped at the end of the script, which causes the while
1397 1401 # loop to exit and closes the pipe. Sigh.
1398 1402 script.append(
1399 1403 b'rtendtracing() {\n'
1400 1404 b' echo END %(session)s %(name)s >> $HGCATAPULTSERVERPIPE\n'
1401 1405 b' rm -f "$TESTTMP/.still-running"\n'
1402 1406 b'}\n'
1403 1407 b'trap "rtendtracing" 0\n'
1404 1408 b'touch "$TESTTMP/.still-running"\n'
1405 1409 b'while [ -f "$TESTTMP/.still-running" ]; do sleep 1; done '
1406 1410 b'> $HGCATAPULTSERVERPIPE &\n'
1407 1411 b'HGCATAPULTSESSION=%(session)s ; export HGCATAPULTSESSION\n'
1408 1412 b'echo START %(session)s %(name)s >> $HGCATAPULTSERVERPIPE\n'
1409 1413 % {
1410 1414 'name': self.name,
1411 1415 'session': session,
1412 1416 }
1413 1417 )
1414 1418
1415 1419 if self._case:
1416 1420 casestr = b'#'.join(self._case)
1417 1421 if isinstance(self._case, str):
1418 1422 quoted = shellquote(casestr)
1419 1423 else:
1420 1424 quoted = shellquote(casestr.decode('utf8')).encode('utf8')
1421 1425 script.append(b'TESTCASE=%s\n' % quoted)
1422 1426 script.append(b'export TESTCASE\n')
1423 1427
1424 1428 n = 0
1425 1429 for n, l in enumerate(lines):
1426 1430 if not l.endswith(b'\n'):
1427 1431 l += b'\n'
1428 1432 if l.startswith(b'#require'):
1429 1433 lsplit = l.split()
1430 1434 if len(lsplit) < 2 or lsplit[0] != b'#require':
1431 1435 after.setdefault(pos, []).append(' !!! invalid #require\n')
1432 1436 if not skipping:
1433 1437 haveresult, message = self._hghave(lsplit[1:])
1434 1438 if not haveresult:
1435 1439 script = [b'echo "%s"\nexit 80\n' % message]
1436 1440 break
1437 1441 after.setdefault(pos, []).append(l)
1438 1442 elif l.startswith(b'#if'):
1439 1443 lsplit = l.split()
1440 1444 if len(lsplit) < 2 or lsplit[0] != b'#if':
1441 1445 after.setdefault(pos, []).append(' !!! invalid #if\n')
1442 1446 if skipping is not None:
1443 1447 after.setdefault(pos, []).append(' !!! nested #if\n')
1444 1448 skipping = not self._iftest(lsplit[1:])
1445 1449 after.setdefault(pos, []).append(l)
1446 1450 elif l.startswith(b'#else'):
1447 1451 if skipping is None:
1448 1452 after.setdefault(pos, []).append(' !!! missing #if\n')
1449 1453 skipping = not skipping
1450 1454 after.setdefault(pos, []).append(l)
1451 1455 elif l.startswith(b'#endif'):
1452 1456 if skipping is None:
1453 1457 after.setdefault(pos, []).append(' !!! missing #if\n')
1454 1458 skipping = None
1455 1459 after.setdefault(pos, []).append(l)
1456 1460 elif skipping:
1457 1461 after.setdefault(pos, []).append(l)
1458 1462 elif l.startswith(b' >>> '): # python inlines
1459 1463 after.setdefault(pos, []).append(l)
1460 1464 prepos = pos
1461 1465 pos = n
1462 1466 if not inpython:
1463 1467 # We've just entered a Python block. Add the header.
1464 1468 inpython = True
1465 1469 addsalt(prepos, False) # Make sure we report the exit code.
1466 1470 script.append(b'%s -m heredoctest <<EOF\n' % PYTHON)
1467 1471 addsalt(n, True)
1468 1472 script.append(l[2:])
1469 1473 elif l.startswith(b' ... '): # python inlines
1470 1474 after.setdefault(prepos, []).append(l)
1471 1475 script.append(l[2:])
1472 1476 elif l.startswith(b' $ '): # commands
1473 1477 if inpython:
1474 1478 script.append(b'EOF\n')
1475 1479 inpython = False
1476 1480 after.setdefault(pos, []).append(l)
1477 1481 prepos = pos
1478 1482 pos = n
1479 1483 addsalt(n, False)
1480 1484 rawcmd = l[4:]
1481 1485 cmd = rawcmd.split()
1482 1486 toggletrace(rawcmd)
1483 1487 if len(cmd) == 2 and cmd[0] == b'cd':
1484 1488 l = b' $ cd %s || exit 1\n' % cmd[1]
1485 1489 script.append(rawcmd)
1486 1490 elif l.startswith(b' > '): # continuations
1487 1491 after.setdefault(prepos, []).append(l)
1488 1492 script.append(l[4:])
1489 1493 elif l.startswith(b' '): # results
1490 1494 # Queue up a list of expected results.
1491 1495 expected.setdefault(pos, []).append(l[2:])
1492 1496 else:
1493 1497 if inpython:
1494 1498 script.append(b'EOF\n')
1495 1499 inpython = False
1496 1500 # Non-command/result. Queue up for merged output.
1497 1501 after.setdefault(pos, []).append(l)
1498 1502
1499 1503 if inpython:
1500 1504 script.append(b'EOF\n')
1501 1505 if skipping is not None:
1502 1506 after.setdefault(pos, []).append(' !!! missing #endif\n')
1503 1507 addsalt(n + 1, False)
1504 1508 return salt, script, after, expected
1505 1509
1506 1510 def _processoutput(self, exitcode, output, salt, after, expected):
1507 1511 # Merge the script output back into a unified test.
1508 1512 warnonly = 1 # 1: not yet; 2: yes; 3: for sure not
1509 1513 if exitcode != 0:
1510 1514 warnonly = 3
1511 1515
1512 1516 pos = -1
1513 1517 postout = []
1514 1518 for l in output:
1515 1519 lout, lcmd = l, None
1516 1520 if salt in l:
1517 1521 lout, lcmd = l.split(salt, 1)
1518 1522
1519 1523 while lout:
1520 1524 if not lout.endswith(b'\n'):
1521 1525 lout += b' (no-eol)\n'
1522 1526
1523 1527 # Find the expected output at the current position.
1524 1528 els = [None]
1525 1529 if expected.get(pos, None):
1526 1530 els = expected[pos]
1527 1531
1528 1532 optional = []
1529 1533 for i, el in enumerate(els):
1530 1534 r = False
1531 1535 if el:
1532 1536 r, exact = self.linematch(el, lout)
1533 1537 if isinstance(r, str):
1534 1538 if r == '-glob':
1535 1539 lout = ''.join(el.rsplit(' (glob)', 1))
1536 1540 r = '' # Warn only this line.
1537 1541 elif r == "retry":
1538 1542 postout.append(b' ' + el)
1539 1543 else:
1540 1544 log('\ninfo, unknown linematch result: %r\n' % r)
1541 1545 r = False
1542 1546 if r:
1543 1547 els.pop(i)
1544 1548 break
1545 1549 if el:
1546 1550 if el.endswith(b" (?)\n"):
1547 1551 optional.append(i)
1548 1552 else:
1549 1553 m = optline.match(el)
1550 1554 if m:
1551 1555 conditions = [
1552 1556 c for c in m.group(2).split(b' ')]
1553 1557
1554 1558 if not self._iftest(conditions):
1555 1559 optional.append(i)
1556 1560 if exact:
1557 1561 # Don't allow line to be matches against a later
1558 1562 # line in the output
1559 1563 els.pop(i)
1560 1564 break
1561 1565
1562 1566 if r:
1563 1567 if r == "retry":
1564 1568 continue
1565 1569 # clean up any optional leftovers
1566 1570 for i in optional:
1567 1571 postout.append(b' ' + els[i])
1568 1572 for i in reversed(optional):
1569 1573 del els[i]
1570 1574 postout.append(b' ' + el)
1571 1575 else:
1572 1576 if self.NEEDESCAPE(lout):
1573 1577 lout = TTest._stringescape(b'%s (esc)\n' %
1574 1578 lout.rstrip(b'\n'))
1575 1579 postout.append(b' ' + lout) # Let diff deal with it.
1576 1580 if r != '': # If line failed.
1577 1581 warnonly = 3 # for sure not
1578 1582 elif warnonly == 1: # Is "not yet" and line is warn only.
1579 1583 warnonly = 2 # Yes do warn.
1580 1584 break
1581 1585 else:
1582 1586 # clean up any optional leftovers
1583 1587 while expected.get(pos, None):
1584 1588 el = expected[pos].pop(0)
1585 1589 if el:
1586 1590 if not el.endswith(b" (?)\n"):
1587 1591 m = optline.match(el)
1588 1592 if m:
1589 1593 conditions = [c for c in m.group(2).split(b' ')]
1590 1594
1591 1595 if self._iftest(conditions):
1592 1596 # Don't append as optional line
1593 1597 continue
1594 1598 else:
1595 1599 continue
1596 1600 postout.append(b' ' + el)
1597 1601
1598 1602 if lcmd:
1599 1603 # Add on last return code.
1600 1604 ret = int(lcmd.split()[1])
1601 1605 if ret != 0:
1602 1606 postout.append(b' [%d]\n' % ret)
1603 1607 if pos in after:
1604 1608 # Merge in non-active test bits.
1605 1609 postout += after.pop(pos)
1606 1610 pos = int(lcmd.split()[0])
1607 1611
1608 1612 if pos in after:
1609 1613 postout += after.pop(pos)
1610 1614
1611 1615 if warnonly == 2:
1612 1616 exitcode = False # Set exitcode to warned.
1613 1617
1614 1618 return exitcode, postout
1615 1619
1616 1620 @staticmethod
1617 1621 def rematch(el, l):
1618 1622 try:
1619 1623 el = b'(?:' + el + b')'
1620 1624 # use \Z to ensure that the regex matches to the end of the string
1621 1625 if os.name == 'nt':
1622 1626 return re.match(el + br'\r?\n\Z', l)
1623 1627 return re.match(el + br'\n\Z', l)
1624 1628 except re.error:
1625 1629 # el is an invalid regex
1626 1630 return False
1627 1631
1628 1632 @staticmethod
1629 1633 def globmatch(el, l):
1630 1634 # The only supported special characters are * and ? plus / which also
1631 1635 # matches \ on windows. Escaping of these characters is supported.
1632 1636 if el + b'\n' == l:
1633 1637 if os.altsep:
1634 1638 # matching on "/" is not needed for this line
1635 1639 for pat in checkcodeglobpats:
1636 1640 if pat.match(el):
1637 1641 return True
1638 1642 return b'-glob'
1639 1643 return True
1640 1644 el = el.replace(b'$LOCALIP', b'*')
1641 1645 i, n = 0, len(el)
1642 1646 res = b''
1643 1647 while i < n:
1644 1648 c = el[i:i + 1]
1645 1649 i += 1
1646 1650 if c == b'\\' and i < n and el[i:i + 1] in b'*?\\/':
1647 1651 res += el[i - 1:i + 1]
1648 1652 i += 1
1649 1653 elif c == b'*':
1650 1654 res += b'.*'
1651 1655 elif c == b'?':
1652 1656 res += b'.'
1653 1657 elif c == b'/' and os.altsep:
1654 1658 res += b'[/\\\\]'
1655 1659 else:
1656 1660 res += re.escape(c)
1657 1661 return TTest.rematch(res, l)
1658 1662
1659 1663 def linematch(self, el, l):
1660 1664 if el == l: # perfect match (fast)
1661 1665 return True, True
1662 1666 retry = False
1663 1667 if el.endswith(b" (?)\n"):
1664 1668 retry = "retry"
1665 1669 el = el[:-5] + b"\n"
1666 1670 else:
1667 1671 m = optline.match(el)
1668 1672 if m:
1669 1673 conditions = [c for c in m.group(2).split(b' ')]
1670 1674
1671 1675 el = m.group(1) + b"\n"
1672 1676 if not self._iftest(conditions):
1673 1677 retry = "retry" # Not required by listed features
1674 1678
1675 1679 if el.endswith(b" (esc)\n"):
1676 1680 if PYTHON3:
1677 1681 el = el[:-7].decode('unicode_escape') + '\n'
1678 1682 el = el.encode('utf-8')
1679 1683 else:
1680 1684 el = el[:-7].decode('string-escape') + '\n'
1681 1685 if el == l or os.name == 'nt' and el[:-1] + b'\r\n' == l:
1682 1686 return True, True
1683 1687 if el.endswith(b" (re)\n"):
1684 1688 return (TTest.rematch(el[:-6], l) or retry), False
1685 1689 if el.endswith(b" (glob)\n"):
1686 1690 # ignore '(glob)' added to l by 'replacements'
1687 1691 if l.endswith(b" (glob)\n"):
1688 1692 l = l[:-8] + b"\n"
1689 1693 return (TTest.globmatch(el[:-8], l) or retry), False
1690 1694 if os.altsep:
1691 1695 _l = l.replace(b'\\', b'/')
1692 1696 if el == _l or os.name == 'nt' and el[:-1] + b'\r\n' == _l:
1693 1697 return True, True
1694 1698 return retry, True
1695 1699
1696 1700 @staticmethod
1697 1701 def parsehghaveoutput(lines):
1698 1702 '''Parse hghave log lines.
1699 1703
1700 1704 Return tuple of lists (missing, failed):
1701 1705 * the missing/unknown features
1702 1706 * the features for which existence check failed'''
1703 1707 missing = []
1704 1708 failed = []
1705 1709 for line in lines:
1706 1710 if line.startswith(TTest.SKIPPED_PREFIX):
1707 1711 line = line.splitlines()[0]
1708 1712 missing.append(line[len(TTest.SKIPPED_PREFIX):].decode('utf-8'))
1709 1713 elif line.startswith(TTest.FAILED_PREFIX):
1710 1714 line = line.splitlines()[0]
1711 1715 failed.append(line[len(TTest.FAILED_PREFIX):].decode('utf-8'))
1712 1716
1713 1717 return missing, failed
1714 1718
1715 1719 @staticmethod
1716 1720 def _escapef(m):
1717 1721 return TTest.ESCAPEMAP[m.group(0)]
1718 1722
1719 1723 @staticmethod
1720 1724 def _stringescape(s):
1721 1725 return TTest.ESCAPESUB(TTest._escapef, s)
1722 1726
1723 1727 iolock = threading.RLock()
1724 1728 firstlock = threading.RLock()
1725 1729 firsterror = False
1726 1730
1727 1731 class TestResult(unittest._TextTestResult):
1728 1732 """Holds results when executing via unittest."""
1729 1733 # Don't worry too much about accessing the non-public _TextTestResult.
1730 1734 # It is relatively common in Python testing tools.
1731 1735 def __init__(self, options, *args, **kwargs):
1732 1736 super(TestResult, self).__init__(*args, **kwargs)
1733 1737
1734 1738 self._options = options
1735 1739
1736 1740 # unittest.TestResult didn't have skipped until 2.7. We need to
1737 1741 # polyfill it.
1738 1742 self.skipped = []
1739 1743
1740 1744 # We have a custom "ignored" result that isn't present in any Python
1741 1745 # unittest implementation. It is very similar to skipped. It may make
1742 1746 # sense to map it into skip some day.
1743 1747 self.ignored = []
1744 1748
1745 1749 self.times = []
1746 1750 self._firststarttime = None
1747 1751 # Data stored for the benefit of generating xunit reports.
1748 1752 self.successes = []
1749 1753 self.faildata = {}
1750 1754
1751 1755 if options.color == 'auto':
1752 1756 self.color = pygmentspresent and self.stream.isatty()
1753 1757 elif options.color == 'never':
1754 1758 self.color = False
1755 1759 else: # 'always', for testing purposes
1756 1760 self.color = pygmentspresent
1757 1761
1758 1762 def onStart(self, test):
1759 1763 """ Can be overriden by custom TestResult
1760 1764 """
1761 1765
1762 1766 def onEnd(self):
1763 1767 """ Can be overriden by custom TestResult
1764 1768 """
1765 1769
1766 1770 def addFailure(self, test, reason):
1767 1771 self.failures.append((test, reason))
1768 1772
1769 1773 if self._options.first:
1770 1774 self.stop()
1771 1775 else:
1772 1776 with iolock:
1773 1777 if reason == "timed out":
1774 1778 self.stream.write('t')
1775 1779 else:
1776 1780 if not self._options.nodiff:
1777 1781 self.stream.write('\n')
1778 1782 # Exclude the '\n' from highlighting to lex correctly
1779 1783 formatted = 'ERROR: %s output changed\n' % test
1780 1784 self.stream.write(highlightmsg(formatted, self.color))
1781 1785 self.stream.write('!')
1782 1786
1783 1787 self.stream.flush()
1784 1788
1785 1789 def addSuccess(self, test):
1786 1790 with iolock:
1787 1791 super(TestResult, self).addSuccess(test)
1788 1792 self.successes.append(test)
1789 1793
1790 1794 def addError(self, test, err):
1791 1795 super(TestResult, self).addError(test, err)
1792 1796 if self._options.first:
1793 1797 self.stop()
1794 1798
1795 1799 # Polyfill.
1796 1800 def addSkip(self, test, reason):
1797 1801 self.skipped.append((test, reason))
1798 1802 with iolock:
1799 1803 if self.showAll:
1800 1804 self.stream.writeln('skipped %s' % reason)
1801 1805 else:
1802 1806 self.stream.write('s')
1803 1807 self.stream.flush()
1804 1808
1805 1809 def addIgnore(self, test, reason):
1806 1810 self.ignored.append((test, reason))
1807 1811 with iolock:
1808 1812 if self.showAll:
1809 1813 self.stream.writeln('ignored %s' % reason)
1810 1814 else:
1811 1815 if reason not in ('not retesting', "doesn't match keyword"):
1812 1816 self.stream.write('i')
1813 1817 else:
1814 1818 self.testsRun += 1
1815 1819 self.stream.flush()
1816 1820
1817 1821 def addOutputMismatch(self, test, ret, got, expected):
1818 1822 """Record a mismatch in test output for a particular test."""
1819 1823 if self.shouldStop or firsterror:
1820 1824 # don't print, some other test case already failed and
1821 1825 # printed, we're just stale and probably failed due to our
1822 1826 # temp dir getting cleaned up.
1823 1827 return
1824 1828
1825 1829 accepted = False
1826 1830 lines = []
1827 1831
1828 1832 with iolock:
1829 1833 if self._options.nodiff:
1830 1834 pass
1831 1835 elif self._options.view:
1832 1836 v = self._options.view
1833 1837 if PYTHON3:
1834 1838 v = _bytespath(v)
1835 1839 os.system(b"%s %s %s" %
1836 1840 (v, test.refpath, test.errpath))
1837 1841 else:
1838 1842 servefail, lines = getdiff(expected, got,
1839 1843 test.refpath, test.errpath)
1840 1844 self.stream.write('\n')
1841 1845 for line in lines:
1842 1846 line = highlightdiff(line, self.color)
1843 1847 if PYTHON3:
1844 1848 self.stream.flush()
1845 1849 self.stream.buffer.write(line)
1846 1850 self.stream.buffer.flush()
1847 1851 else:
1848 1852 self.stream.write(line)
1849 1853 self.stream.flush()
1850 1854
1851 1855 if servefail:
1852 1856 raise test.failureException(
1853 1857 'server failed to start (HGPORT=%s)' % test._startport)
1854 1858
1855 1859 # handle interactive prompt without releasing iolock
1856 1860 if self._options.interactive:
1857 1861 if test.readrefout() != expected:
1858 1862 self.stream.write(
1859 1863 'Reference output has changed (run again to prompt '
1860 1864 'changes)')
1861 1865 else:
1862 1866 self.stream.write('Accept this change? [n] ')
1863 1867 answer = sys.stdin.readline().strip()
1864 1868 if answer.lower() in ('y', 'yes'):
1865 1869 if test.path.endswith(b'.t'):
1866 1870 rename(test.errpath, test.path)
1867 1871 else:
1868 1872 rename(test.errpath, '%s.out' % test.path)
1869 1873 accepted = True
1870 1874 if not accepted:
1871 1875 self.faildata[test.name] = b''.join(lines)
1872 1876
1873 1877 return accepted
1874 1878
1875 1879 def startTest(self, test):
1876 1880 super(TestResult, self).startTest(test)
1877 1881
1878 1882 # os.times module computes the user time and system time spent by
1879 1883 # child's processes along with real elapsed time taken by a process.
1880 1884 # This module has one limitation. It can only work for Linux user
1881 1885 # and not for Windows.
1882 1886 test.started = os.times()
1883 1887 if self._firststarttime is None: # thread racy but irrelevant
1884 1888 self._firststarttime = test.started[4]
1885 1889
1886 1890 def stopTest(self, test, interrupted=False):
1887 1891 super(TestResult, self).stopTest(test)
1888 1892
1889 1893 test.stopped = os.times()
1890 1894
1891 1895 starttime = test.started
1892 1896 endtime = test.stopped
1893 1897 origin = self._firststarttime
1894 1898 self.times.append((test.name,
1895 1899 endtime[2] - starttime[2], # user space CPU time
1896 1900 endtime[3] - starttime[3], # sys space CPU time
1897 1901 endtime[4] - starttime[4], # real time
1898 1902 starttime[4] - origin, # start date in run context
1899 1903 endtime[4] - origin, # end date in run context
1900 1904 ))
1901 1905
1902 1906 if interrupted:
1903 1907 with iolock:
1904 1908 self.stream.writeln('INTERRUPTED: %s (after %d seconds)' % (
1905 1909 test.name, self.times[-1][3]))
1906 1910
1907 1911 def getTestResult():
1908 1912 """
1909 1913 Returns the relevant test result
1910 1914 """
1911 1915 if "CUSTOM_TEST_RESULT" in os.environ:
1912 1916 testresultmodule = __import__(os.environ["CUSTOM_TEST_RESULT"])
1913 1917 return testresultmodule.TestResult
1914 1918 else:
1915 1919 return TestResult
1916 1920
1917 1921 class TestSuite(unittest.TestSuite):
1918 1922 """Custom unittest TestSuite that knows how to execute Mercurial tests."""
1919 1923
1920 1924 def __init__(self, testdir, jobs=1, whitelist=None, blacklist=None,
1921 1925 retest=False, keywords=None, loop=False, runs_per_test=1,
1922 1926 loadtest=None, showchannels=False,
1923 1927 *args, **kwargs):
1924 1928 """Create a new instance that can run tests with a configuration.
1925 1929
1926 1930 testdir specifies the directory where tests are executed from. This
1927 1931 is typically the ``tests`` directory from Mercurial's source
1928 1932 repository.
1929 1933
1930 1934 jobs specifies the number of jobs to run concurrently. Each test
1931 1935 executes on its own thread. Tests actually spawn new processes, so
1932 1936 state mutation should not be an issue.
1933 1937
1934 1938 If there is only one job, it will use the main thread.
1935 1939
1936 1940 whitelist and blacklist denote tests that have been whitelisted and
1937 1941 blacklisted, respectively. These arguments don't belong in TestSuite.
1938 1942 Instead, whitelist and blacklist should be handled by the thing that
1939 1943 populates the TestSuite with tests. They are present to preserve
1940 1944 backwards compatible behavior which reports skipped tests as part
1941 1945 of the results.
1942 1946
1943 1947 retest denotes whether to retest failed tests. This arguably belongs
1944 1948 outside of TestSuite.
1945 1949
1946 1950 keywords denotes key words that will be used to filter which tests
1947 1951 to execute. This arguably belongs outside of TestSuite.
1948 1952
1949 1953 loop denotes whether to loop over tests forever.
1950 1954 """
1951 1955 super(TestSuite, self).__init__(*args, **kwargs)
1952 1956
1953 1957 self._jobs = jobs
1954 1958 self._whitelist = whitelist
1955 1959 self._blacklist = blacklist
1956 1960 self._retest = retest
1957 1961 self._keywords = keywords
1958 1962 self._loop = loop
1959 1963 self._runs_per_test = runs_per_test
1960 1964 self._loadtest = loadtest
1961 1965 self._showchannels = showchannels
1962 1966
1963 1967 def run(self, result):
1964 1968 # We have a number of filters that need to be applied. We do this
1965 1969 # here instead of inside Test because it makes the running logic for
1966 1970 # Test simpler.
1967 1971 tests = []
1968 1972 num_tests = [0]
1969 1973 for test in self._tests:
1970 1974 def get():
1971 1975 num_tests[0] += 1
1972 1976 if getattr(test, 'should_reload', False):
1973 1977 return self._loadtest(test, num_tests[0])
1974 1978 return test
1975 1979 if not os.path.exists(test.path):
1976 1980 result.addSkip(test, "Doesn't exist")
1977 1981 continue
1978 1982
1979 1983 if not (self._whitelist and test.bname in self._whitelist):
1980 1984 if self._blacklist and test.bname in self._blacklist:
1981 1985 result.addSkip(test, 'blacklisted')
1982 1986 continue
1983 1987
1984 1988 if self._retest and not os.path.exists(test.errpath):
1985 1989 result.addIgnore(test, 'not retesting')
1986 1990 continue
1987 1991
1988 1992 if self._keywords:
1989 1993 with open(test.path, 'rb') as f:
1990 1994 t = f.read().lower() + test.bname.lower()
1991 1995 ignored = False
1992 1996 for k in self._keywords.lower().split():
1993 1997 if k not in t:
1994 1998 result.addIgnore(test, "doesn't match keyword")
1995 1999 ignored = True
1996 2000 break
1997 2001
1998 2002 if ignored:
1999 2003 continue
2000 2004 for _ in xrange(self._runs_per_test):
2001 2005 tests.append(get())
2002 2006
2003 2007 runtests = list(tests)
2004 2008 done = queue.Queue()
2005 2009 running = 0
2006 2010
2007 2011 channels = [""] * self._jobs
2008 2012
2009 2013 def job(test, result):
2010 2014 for n, v in enumerate(channels):
2011 2015 if not v:
2012 2016 channel = n
2013 2017 break
2014 2018 else:
2015 2019 raise ValueError('Could not find output channel')
2016 2020 channels[channel] = "=" + test.name[5:].split(".")[0]
2017 2021 try:
2018 2022 test(result)
2019 2023 done.put(None)
2020 2024 except KeyboardInterrupt:
2021 2025 pass
2022 2026 except: # re-raises
2023 2027 done.put(('!', test, 'run-test raised an error, see traceback'))
2024 2028 raise
2025 2029 finally:
2026 2030 try:
2027 2031 channels[channel] = ''
2028 2032 except IndexError:
2029 2033 pass
2030 2034
2031 2035 def stat():
2032 2036 count = 0
2033 2037 while channels:
2034 2038 d = '\n%03s ' % count
2035 2039 for n, v in enumerate(channels):
2036 2040 if v:
2037 2041 d += v[0]
2038 2042 channels[n] = v[1:] or '.'
2039 2043 else:
2040 2044 d += ' '
2041 2045 d += ' '
2042 2046 with iolock:
2043 2047 sys.stdout.write(d + ' ')
2044 2048 sys.stdout.flush()
2045 2049 for x in xrange(10):
2046 2050 if channels:
2047 2051 time.sleep(.1)
2048 2052 count += 1
2049 2053
2050 2054 stoppedearly = False
2051 2055
2052 2056 if self._showchannels:
2053 2057 statthread = threading.Thread(target=stat, name="stat")
2054 2058 statthread.start()
2055 2059
2056 2060 try:
2057 2061 while tests or running:
2058 2062 if not done.empty() or running == self._jobs or not tests:
2059 2063 try:
2060 2064 done.get(True, 1)
2061 2065 running -= 1
2062 2066 if result and result.shouldStop:
2063 2067 stoppedearly = True
2064 2068 break
2065 2069 except queue.Empty:
2066 2070 continue
2067 2071 if tests and not running == self._jobs:
2068 2072 test = tests.pop(0)
2069 2073 if self._loop:
2070 2074 if getattr(test, 'should_reload', False):
2071 2075 num_tests[0] += 1
2072 2076 tests.append(
2073 2077 self._loadtest(test, num_tests[0]))
2074 2078 else:
2075 2079 tests.append(test)
2076 2080 if self._jobs == 1:
2077 2081 job(test, result)
2078 2082 else:
2079 2083 t = threading.Thread(target=job, name=test.name,
2080 2084 args=(test, result))
2081 2085 t.start()
2082 2086 running += 1
2083 2087
2084 2088 # If we stop early we still need to wait on started tests to
2085 2089 # finish. Otherwise, there is a race between the test completing
2086 2090 # and the test's cleanup code running. This could result in the
2087 2091 # test reporting incorrect.
2088 2092 if stoppedearly:
2089 2093 while running:
2090 2094 try:
2091 2095 done.get(True, 1)
2092 2096 running -= 1
2093 2097 except queue.Empty:
2094 2098 continue
2095 2099 except KeyboardInterrupt:
2096 2100 for test in runtests:
2097 2101 test.abort()
2098 2102
2099 2103 channels = []
2100 2104
2101 2105 return result
2102 2106
2103 2107 # Save the most recent 5 wall-clock runtimes of each test to a
2104 2108 # human-readable text file named .testtimes. Tests are sorted
2105 2109 # alphabetically, while times for each test are listed from oldest to
2106 2110 # newest.
2107 2111
2108 2112 def loadtimes(outputdir):
2109 2113 times = []
2110 2114 try:
2111 2115 with open(os.path.join(outputdir, b'.testtimes')) as fp:
2112 2116 for line in fp:
2113 2117 m = re.match('(.*?) ([0-9. ]+)', line)
2114 2118 times.append((m.group(1),
2115 2119 [float(t) for t in m.group(2).split()]))
2116 2120 except IOError as err:
2117 2121 if err.errno != errno.ENOENT:
2118 2122 raise
2119 2123 return times
2120 2124
2121 2125 def savetimes(outputdir, result):
2122 2126 saved = dict(loadtimes(outputdir))
2123 2127 maxruns = 5
2124 2128 skipped = set([str(t[0]) for t in result.skipped])
2125 2129 for tdata in result.times:
2126 2130 test, real = tdata[0], tdata[3]
2127 2131 if test not in skipped:
2128 2132 ts = saved.setdefault(test, [])
2129 2133 ts.append(real)
2130 2134 ts[:] = ts[-maxruns:]
2131 2135
2132 2136 fd, tmpname = tempfile.mkstemp(prefix=b'.testtimes',
2133 2137 dir=outputdir, text=True)
2134 2138 with os.fdopen(fd, 'w') as fp:
2135 2139 for name, ts in sorted(saved.items()):
2136 2140 fp.write('%s %s\n' % (name, ' '.join(['%.3f' % (t,) for t in ts])))
2137 2141 timepath = os.path.join(outputdir, b'.testtimes')
2138 2142 try:
2139 2143 os.unlink(timepath)
2140 2144 except OSError:
2141 2145 pass
2142 2146 try:
2143 2147 os.rename(tmpname, timepath)
2144 2148 except OSError:
2145 2149 pass
2146 2150
2147 2151 class TextTestRunner(unittest.TextTestRunner):
2148 2152 """Custom unittest test runner that uses appropriate settings."""
2149 2153
2150 2154 def __init__(self, runner, *args, **kwargs):
2151 2155 super(TextTestRunner, self).__init__(*args, **kwargs)
2152 2156
2153 2157 self._runner = runner
2154 2158
2155 2159 self._result = getTestResult()(self._runner.options, self.stream,
2156 2160 self.descriptions, self.verbosity)
2157 2161
2158 2162 def listtests(self, test):
2159 2163 test = sorted(test, key=lambda t: t.name)
2160 2164
2161 2165 self._result.onStart(test)
2162 2166
2163 2167 for t in test:
2164 2168 print(t.name)
2165 2169 self._result.addSuccess(t)
2166 2170
2167 2171 if self._runner.options.xunit:
2168 2172 with open(self._runner.options.xunit, "wb") as xuf:
2169 2173 self._writexunit(self._result, xuf)
2170 2174
2171 2175 if self._runner.options.json:
2172 2176 jsonpath = os.path.join(self._runner._outputdir, b'report.json')
2173 2177 with open(jsonpath, 'w') as fp:
2174 2178 self._writejson(self._result, fp)
2175 2179
2176 2180 return self._result
2177 2181
2178 2182 def run(self, test):
2179 2183 self._result.onStart(test)
2180 2184 test(self._result)
2181 2185
2182 2186 failed = len(self._result.failures)
2183 2187 skipped = len(self._result.skipped)
2184 2188 ignored = len(self._result.ignored)
2185 2189
2186 2190 with iolock:
2187 2191 self.stream.writeln('')
2188 2192
2189 2193 if not self._runner.options.noskips:
2190 2194 for test, msg in self._result.skipped:
2191 2195 formatted = 'Skipped %s: %s\n' % (test.name, msg)
2192 2196 msg = highlightmsg(formatted, self._result.color)
2193 2197 self.stream.write(msg)
2194 2198 for test, msg in self._result.failures:
2195 2199 formatted = 'Failed %s: %s\n' % (test.name, msg)
2196 2200 self.stream.write(highlightmsg(formatted, self._result.color))
2197 2201 for test, msg in self._result.errors:
2198 2202 self.stream.writeln('Errored %s: %s' % (test.name, msg))
2199 2203
2200 2204 if self._runner.options.xunit:
2201 2205 with open(self._runner.options.xunit, "wb") as xuf:
2202 2206 self._writexunit(self._result, xuf)
2203 2207
2204 2208 if self._runner.options.json:
2205 2209 jsonpath = os.path.join(self._runner._outputdir, b'report.json')
2206 2210 with open(jsonpath, 'w') as fp:
2207 2211 self._writejson(self._result, fp)
2208 2212
2209 2213 self._runner._checkhglib('Tested')
2210 2214
2211 2215 savetimes(self._runner._outputdir, self._result)
2212 2216
2213 2217 if failed and self._runner.options.known_good_rev:
2214 2218 self._bisecttests(t for t, m in self._result.failures)
2215 2219 self.stream.writeln(
2216 2220 '# Ran %d tests, %d skipped, %d failed.'
2217 2221 % (self._result.testsRun, skipped + ignored, failed))
2218 2222 if failed:
2219 2223 self.stream.writeln('python hash seed: %s' %
2220 2224 os.environ['PYTHONHASHSEED'])
2221 2225 if self._runner.options.time:
2222 2226 self.printtimes(self._result.times)
2223 2227
2224 2228 if self._runner.options.exceptions:
2225 2229 exceptions = aggregateexceptions(
2226 2230 os.path.join(self._runner._outputdir, b'exceptions'))
2227 2231
2228 2232 self.stream.writeln('Exceptions Report:')
2229 2233 self.stream.writeln('%d total from %d frames' %
2230 2234 (exceptions['total'],
2231 2235 len(exceptions['exceptioncounts'])))
2232 2236 combined = exceptions['combined']
2233 2237 for key in sorted(combined, key=combined.get, reverse=True):
2234 2238 frame, line, exc = key
2235 2239 totalcount, testcount, leastcount, leasttest = combined[key]
2236 2240
2237 2241 self.stream.writeln('%d (%d tests)\t%s: %s (%s - %d total)'
2238 2242 % (totalcount,
2239 2243 testcount,
2240 2244 frame, exc,
2241 2245 leasttest, leastcount))
2242 2246
2243 2247 self.stream.flush()
2244 2248
2245 2249 return self._result
2246 2250
2247 2251 def _bisecttests(self, tests):
2248 2252 bisectcmd = ['hg', 'bisect']
2249 2253 bisectrepo = self._runner.options.bisect_repo
2250 2254 if bisectrepo:
2251 2255 bisectcmd.extend(['-R', os.path.abspath(bisectrepo)])
2252 2256 def pread(args):
2253 2257 env = os.environ.copy()
2254 2258 env['HGPLAIN'] = '1'
2255 2259 p = subprocess.Popen(args, stderr=subprocess.STDOUT,
2256 2260 stdout=subprocess.PIPE, env=env)
2257 2261 data = p.stdout.read()
2258 2262 p.wait()
2259 2263 return data
2260 2264 for test in tests:
2261 2265 pread(bisectcmd + ['--reset']),
2262 2266 pread(bisectcmd + ['--bad', '.'])
2263 2267 pread(bisectcmd + ['--good', self._runner.options.known_good_rev])
2264 2268 # TODO: we probably need to forward more options
2265 2269 # that alter hg's behavior inside the tests.
2266 2270 opts = ''
2267 2271 withhg = self._runner.options.with_hg
2268 2272 if withhg:
2269 2273 opts += ' --with-hg=%s ' % shellquote(_strpath(withhg))
2270 2274 rtc = '%s %s %s %s' % (sys.executable, sys.argv[0], opts,
2271 2275 test)
2272 2276 data = pread(bisectcmd + ['--command', rtc])
2273 2277 m = re.search(
2274 2278 (br'\nThe first (?P<goodbad>bad|good) revision '
2275 2279 br'is:\nchangeset: +\d+:(?P<node>[a-f0-9]+)\n.*\n'
2276 2280 br'summary: +(?P<summary>[^\n]+)\n'),
2277 2281 data, (re.MULTILINE | re.DOTALL))
2278 2282 if m is None:
2279 2283 self.stream.writeln(
2280 2284 'Failed to identify failure point for %s' % test)
2281 2285 continue
2282 2286 dat = m.groupdict()
2283 2287 verb = 'broken' if dat['goodbad'] == b'bad' else 'fixed'
2284 2288 self.stream.writeln(
2285 2289 '%s %s by %s (%s)' % (
2286 2290 test, verb, dat['node'].decode('ascii'),
2287 2291 dat['summary'].decode('utf8', 'ignore')))
2288 2292
2289 2293 def printtimes(self, times):
2290 2294 # iolock held by run
2291 2295 self.stream.writeln('# Producing time report')
2292 2296 times.sort(key=lambda t: (t[3]))
2293 2297 cols = '%7.3f %7.3f %7.3f %7.3f %7.3f %s'
2294 2298 self.stream.writeln('%-7s %-7s %-7s %-7s %-7s %s' %
2295 2299 ('start', 'end', 'cuser', 'csys', 'real', 'Test'))
2296 2300 for tdata in times:
2297 2301 test = tdata[0]
2298 2302 cuser, csys, real, start, end = tdata[1:6]
2299 2303 self.stream.writeln(cols % (start, end, cuser, csys, real, test))
2300 2304
2301 2305 @staticmethod
2302 2306 def _writexunit(result, outf):
2303 2307 # See http://llg.cubic.org/docs/junit/ for a reference.
2304 2308 timesd = dict((t[0], t[3]) for t in result.times)
2305 2309 doc = minidom.Document()
2306 2310 s = doc.createElement('testsuite')
2307 2311 s.setAttribute('name', 'run-tests')
2308 2312 s.setAttribute('tests', str(result.testsRun))
2309 2313 s.setAttribute('errors', "0") # TODO
2310 2314 s.setAttribute('failures', str(len(result.failures)))
2311 2315 s.setAttribute('skipped', str(len(result.skipped) +
2312 2316 len(result.ignored)))
2313 2317 doc.appendChild(s)
2314 2318 for tc in result.successes:
2315 2319 t = doc.createElement('testcase')
2316 2320 t.setAttribute('name', tc.name)
2317 2321 tctime = timesd.get(tc.name)
2318 2322 if tctime is not None:
2319 2323 t.setAttribute('time', '%.3f' % tctime)
2320 2324 s.appendChild(t)
2321 2325 for tc, err in sorted(result.faildata.items()):
2322 2326 t = doc.createElement('testcase')
2323 2327 t.setAttribute('name', tc)
2324 2328 tctime = timesd.get(tc)
2325 2329 if tctime is not None:
2326 2330 t.setAttribute('time', '%.3f' % tctime)
2327 2331 # createCDATASection expects a unicode or it will
2328 2332 # convert using default conversion rules, which will
2329 2333 # fail if string isn't ASCII.
2330 2334 err = cdatasafe(err).decode('utf-8', 'replace')
2331 2335 cd = doc.createCDATASection(err)
2332 2336 # Use 'failure' here instead of 'error' to match errors = 0,
2333 2337 # failures = len(result.failures) in the testsuite element.
2334 2338 failelem = doc.createElement('failure')
2335 2339 failelem.setAttribute('message', 'output changed')
2336 2340 failelem.setAttribute('type', 'output-mismatch')
2337 2341 failelem.appendChild(cd)
2338 2342 t.appendChild(failelem)
2339 2343 s.appendChild(t)
2340 2344 for tc, message in result.skipped:
2341 2345 # According to the schema, 'skipped' has no attributes. So store
2342 2346 # the skip message as a text node instead.
2343 2347 t = doc.createElement('testcase')
2344 2348 t.setAttribute('name', tc.name)
2345 2349 binmessage = message.encode('utf-8')
2346 2350 message = cdatasafe(binmessage).decode('utf-8', 'replace')
2347 2351 cd = doc.createCDATASection(message)
2348 2352 skipelem = doc.createElement('skipped')
2349 2353 skipelem.appendChild(cd)
2350 2354 t.appendChild(skipelem)
2351 2355 s.appendChild(t)
2352 2356 outf.write(doc.toprettyxml(indent=' ', encoding='utf-8'))
2353 2357
2354 2358 @staticmethod
2355 2359 def _writejson(result, outf):
2356 2360 timesd = {}
2357 2361 for tdata in result.times:
2358 2362 test = tdata[0]
2359 2363 timesd[test] = tdata[1:]
2360 2364
2361 2365 outcome = {}
2362 2366 groups = [('success', ((tc, None)
2363 2367 for tc in result.successes)),
2364 2368 ('failure', result.failures),
2365 2369 ('skip', result.skipped)]
2366 2370 for res, testcases in groups:
2367 2371 for tc, __ in testcases:
2368 2372 if tc.name in timesd:
2369 2373 diff = result.faildata.get(tc.name, b'')
2370 2374 try:
2371 2375 diff = diff.decode('unicode_escape')
2372 2376 except UnicodeDecodeError as e:
2373 2377 diff = '%r decoding diff, sorry' % e
2374 2378 tres = {'result': res,
2375 2379 'time': ('%0.3f' % timesd[tc.name][2]),
2376 2380 'cuser': ('%0.3f' % timesd[tc.name][0]),
2377 2381 'csys': ('%0.3f' % timesd[tc.name][1]),
2378 2382 'start': ('%0.3f' % timesd[tc.name][3]),
2379 2383 'end': ('%0.3f' % timesd[tc.name][4]),
2380 2384 'diff': diff,
2381 2385 }
2382 2386 else:
2383 2387 # blacklisted test
2384 2388 tres = {'result': res}
2385 2389
2386 2390 outcome[tc.name] = tres
2387 2391 jsonout = json.dumps(outcome, sort_keys=True, indent=4,
2388 2392 separators=(',', ': '))
2389 2393 outf.writelines(("testreport =", jsonout))
2390 2394
2391 2395 def sorttests(testdescs, previoustimes, shuffle=False):
2392 2396 """Do an in-place sort of tests."""
2393 2397 if shuffle:
2394 2398 random.shuffle(testdescs)
2395 2399 return
2396 2400
2397 2401 if previoustimes:
2398 2402 def sortkey(f):
2399 2403 f = f['path']
2400 2404 if f in previoustimes:
2401 2405 # Use most recent time as estimate
2402 2406 return -previoustimes[f][-1]
2403 2407 else:
2404 2408 # Default to a rather arbitrary value of 1 second for new tests
2405 2409 return -1.0
2406 2410 else:
2407 2411 # keywords for slow tests
2408 2412 slow = {b'svn': 10,
2409 2413 b'cvs': 10,
2410 2414 b'hghave': 10,
2411 2415 b'largefiles-update': 10,
2412 2416 b'run-tests': 10,
2413 2417 b'corruption': 10,
2414 2418 b'race': 10,
2415 2419 b'i18n': 10,
2416 2420 b'check': 100,
2417 2421 b'gendoc': 100,
2418 2422 b'contrib-perf': 200,
2419 2423 }
2420 2424 perf = {}
2421 2425
2422 2426 def sortkey(f):
2423 2427 # run largest tests first, as they tend to take the longest
2424 2428 f = f['path']
2425 2429 try:
2426 2430 return perf[f]
2427 2431 except KeyError:
2428 2432 try:
2429 2433 val = -os.stat(f).st_size
2430 2434 except OSError as e:
2431 2435 if e.errno != errno.ENOENT:
2432 2436 raise
2433 2437 perf[f] = -1e9 # file does not exist, tell early
2434 2438 return -1e9
2435 2439 for kw, mul in slow.items():
2436 2440 if kw in f:
2437 2441 val *= mul
2438 2442 if f.endswith(b'.py'):
2439 2443 val /= 10.0
2440 2444 perf[f] = val / 1000.0
2441 2445 return perf[f]
2442 2446
2443 2447 testdescs.sort(key=sortkey)
2444 2448
2445 2449 class TestRunner(object):
2446 2450 """Holds context for executing tests.
2447 2451
2448 2452 Tests rely on a lot of state. This object holds it for them.
2449 2453 """
2450 2454
2451 2455 # Programs required to run tests.
2452 2456 REQUIREDTOOLS = [
2453 2457 b'diff',
2454 2458 b'grep',
2455 2459 b'unzip',
2456 2460 b'gunzip',
2457 2461 b'bunzip2',
2458 2462 b'sed',
2459 2463 ]
2460 2464
2461 2465 # Maps file extensions to test class.
2462 2466 TESTTYPES = [
2463 2467 (b'.py', PythonTest),
2464 2468 (b'.t', TTest),
2465 2469 ]
2466 2470
2467 2471 def __init__(self):
2468 2472 self.options = None
2469 2473 self._hgroot = None
2470 2474 self._testdir = None
2471 2475 self._outputdir = None
2472 2476 self._hgtmp = None
2473 2477 self._installdir = None
2474 2478 self._bindir = None
2475 2479 self._tmpbinddir = None
2476 2480 self._pythondir = None
2477 2481 self._coveragefile = None
2478 2482 self._createdfiles = []
2479 2483 self._hgcommand = None
2480 2484 self._hgpath = None
2481 2485 self._portoffset = 0
2482 2486 self._ports = {}
2483 2487
2484 2488 def run(self, args, parser=None):
2485 2489 """Run the test suite."""
2486 2490 oldmask = os.umask(0o22)
2487 2491 try:
2488 2492 parser = parser or getparser()
2489 2493 options = parseargs(args, parser)
2490 2494 tests = [_bytespath(a) for a in options.tests]
2491 2495 if options.test_list is not None:
2492 2496 for listfile in options.test_list:
2493 2497 with open(listfile, 'rb') as f:
2494 2498 tests.extend(t for t in f.read().splitlines() if t)
2495 2499 self.options = options
2496 2500
2497 2501 self._checktools()
2498 2502 testdescs = self.findtests(tests)
2499 2503 if options.profile_runner:
2500 2504 import statprof
2501 2505 statprof.start()
2502 2506 result = self._run(testdescs)
2503 2507 if options.profile_runner:
2504 2508 statprof.stop()
2505 2509 statprof.display()
2506 2510 return result
2507 2511
2508 2512 finally:
2509 2513 os.umask(oldmask)
2510 2514
2511 2515 def _run(self, testdescs):
2512 2516 self._testdir = osenvironb[b'TESTDIR'] = getattr(
2513 2517 os, 'getcwdb', os.getcwd)()
2514 2518 # assume all tests in same folder for now
2515 2519 if testdescs:
2516 2520 pathname = os.path.dirname(testdescs[0]['path'])
2517 2521 if pathname:
2518 2522 osenvironb[b'TESTDIR'] = os.path.join(osenvironb[b'TESTDIR'],
2519 2523 pathname)
2520 2524 if self.options.outputdir:
2521 2525 self._outputdir = canonpath(_bytespath(self.options.outputdir))
2522 2526 else:
2523 2527 self._outputdir = self._testdir
2524 2528 if testdescs and pathname:
2525 2529 self._outputdir = os.path.join(self._outputdir, pathname)
2526 2530 previoustimes = {}
2527 2531 if self.options.order_by_runtime:
2528 2532 previoustimes = dict(loadtimes(self._outputdir))
2529 2533 sorttests(testdescs, previoustimes, shuffle=self.options.random)
2530 2534
2531 2535 if 'PYTHONHASHSEED' not in os.environ:
2532 2536 # use a random python hash seed all the time
2533 2537 # we do the randomness ourself to know what seed is used
2534 2538 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
2535 2539
2536 2540 if self.options.tmpdir:
2537 2541 self.options.keep_tmpdir = True
2538 2542 tmpdir = _bytespath(self.options.tmpdir)
2539 2543 if os.path.exists(tmpdir):
2540 2544 # Meaning of tmpdir has changed since 1.3: we used to create
2541 2545 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
2542 2546 # tmpdir already exists.
2543 2547 print("error: temp dir %r already exists" % tmpdir)
2544 2548 return 1
2545 2549
2546 2550 os.makedirs(tmpdir)
2547 2551 else:
2548 2552 d = None
2549 2553 if os.name == 'nt':
2550 2554 # without this, we get the default temp dir location, but
2551 2555 # in all lowercase, which causes troubles with paths (issue3490)
2552 2556 d = osenvironb.get(b'TMP', None)
2553 2557 tmpdir = tempfile.mkdtemp(b'', b'hgtests.', d)
2554 2558
2555 2559 self._hgtmp = osenvironb[b'HGTMP'] = (
2556 2560 os.path.realpath(tmpdir))
2557 2561
2558 2562 if self.options.with_hg:
2559 2563 self._installdir = None
2560 2564 whg = self.options.with_hg
2561 2565 self._bindir = os.path.dirname(os.path.realpath(whg))
2562 2566 assert isinstance(self._bindir, bytes)
2563 2567 self._hgcommand = os.path.basename(whg)
2564 2568 self._tmpbindir = os.path.join(self._hgtmp, b'install', b'bin')
2565 2569 os.makedirs(self._tmpbindir)
2566 2570
2567 2571 normbin = os.path.normpath(os.path.abspath(whg))
2568 2572 normbin = normbin.replace(os.sep.encode('ascii'), b'/')
2569 2573
2570 2574 # Other Python scripts in the test harness need to
2571 2575 # `import mercurial`. If `hg` is a Python script, we assume
2572 2576 # the Mercurial modules are relative to its path and tell the tests
2573 2577 # to load Python modules from its directory.
2574 2578 with open(whg, 'rb') as fh:
2575 2579 initial = fh.read(1024)
2576 2580
2577 2581 if re.match(b'#!.*python', initial):
2578 2582 self._pythondir = self._bindir
2579 2583 # If it looks like our in-repo Rust binary, use the source root.
2580 2584 # This is a bit hacky. But rhg is still not supported outside the
2581 2585 # source directory. So until it is, do the simple thing.
2582 2586 elif re.search(b'/rust/target/[^/]+/hg', normbin):
2583 2587 self._pythondir = os.path.dirname(self._testdir)
2584 2588 # Fall back to the legacy behavior.
2585 2589 else:
2586 2590 self._pythondir = self._bindir
2587 2591
2588 2592 else:
2589 2593 self._installdir = os.path.join(self._hgtmp, b"install")
2590 2594 self._bindir = os.path.join(self._installdir, b"bin")
2591 2595 self._hgcommand = b'hg'
2592 2596 self._tmpbindir = self._bindir
2593 2597 self._pythondir = os.path.join(self._installdir, b"lib", b"python")
2594 2598
2595 2599 # set CHGHG, then replace "hg" command by "chg"
2596 2600 chgbindir = self._bindir
2597 2601 if self.options.chg or self.options.with_chg:
2598 2602 osenvironb[b'CHGHG'] = os.path.join(self._bindir, self._hgcommand)
2599 2603 else:
2600 2604 osenvironb.pop(b'CHGHG', None) # drop flag for hghave
2601 2605 if self.options.chg:
2602 2606 self._hgcommand = b'chg'
2603 2607 elif self.options.with_chg:
2604 2608 chgbindir = os.path.dirname(os.path.realpath(self.options.with_chg))
2605 2609 self._hgcommand = os.path.basename(self.options.with_chg)
2606 2610
2607 2611 osenvironb[b"BINDIR"] = self._bindir
2608 2612 osenvironb[b"PYTHON"] = PYTHON
2609 2613
2610 2614 if self.options.with_python3:
2611 2615 osenvironb[b'PYTHON3'] = self.options.with_python3
2612 2616
2613 2617 fileb = _bytespath(__file__)
2614 2618 runtestdir = os.path.abspath(os.path.dirname(fileb))
2615 2619 osenvironb[b'RUNTESTDIR'] = runtestdir
2616 2620 if PYTHON3:
2617 2621 sepb = _bytespath(os.pathsep)
2618 2622 else:
2619 2623 sepb = os.pathsep
2620 2624 path = [self._bindir, runtestdir] + osenvironb[b"PATH"].split(sepb)
2621 2625 if os.path.islink(__file__):
2622 2626 # test helper will likely be at the end of the symlink
2623 2627 realfile = os.path.realpath(fileb)
2624 2628 realdir = os.path.abspath(os.path.dirname(realfile))
2625 2629 path.insert(2, realdir)
2626 2630 if chgbindir != self._bindir:
2627 2631 path.insert(1, chgbindir)
2628 2632 if self._testdir != runtestdir:
2629 2633 path = [self._testdir] + path
2630 2634 if self._tmpbindir != self._bindir:
2631 2635 path = [self._tmpbindir] + path
2632 2636 osenvironb[b"PATH"] = sepb.join(path)
2633 2637
2634 2638 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
2635 2639 # can run .../tests/run-tests.py test-foo where test-foo
2636 2640 # adds an extension to HGRC. Also include run-test.py directory to
2637 2641 # import modules like heredoctest.
2638 2642 pypath = [self._pythondir, self._testdir, runtestdir]
2639 2643 # We have to augment PYTHONPATH, rather than simply replacing
2640 2644 # it, in case external libraries are only available via current
2641 2645 # PYTHONPATH. (In particular, the Subversion bindings on OS X
2642 2646 # are in /opt/subversion.)
2643 2647 oldpypath = osenvironb.get(IMPL_PATH)
2644 2648 if oldpypath:
2645 2649 pypath.append(oldpypath)
2646 2650 osenvironb[IMPL_PATH] = sepb.join(pypath)
2647 2651
2648 2652 if self.options.pure:
2649 2653 os.environ["HGTEST_RUN_TESTS_PURE"] = "--pure"
2650 2654 os.environ["HGMODULEPOLICY"] = "py"
2651 2655
2652 2656 if self.options.allow_slow_tests:
2653 2657 os.environ["HGTEST_SLOW"] = "slow"
2654 2658 elif 'HGTEST_SLOW' in os.environ:
2655 2659 del os.environ['HGTEST_SLOW']
2656 2660
2657 2661 self._coveragefile = os.path.join(self._testdir, b'.coverage')
2658 2662
2659 2663 if self.options.exceptions:
2660 2664 exceptionsdir = os.path.join(self._outputdir, b'exceptions')
2661 2665 try:
2662 2666 os.makedirs(exceptionsdir)
2663 2667 except OSError as e:
2664 2668 if e.errno != errno.EEXIST:
2665 2669 raise
2666 2670
2667 2671 # Remove all existing exception reports.
2668 2672 for f in os.listdir(exceptionsdir):
2669 2673 os.unlink(os.path.join(exceptionsdir, f))
2670 2674
2671 2675 osenvironb[b'HGEXCEPTIONSDIR'] = exceptionsdir
2672 2676 logexceptions = os.path.join(self._testdir, b'logexceptions.py')
2673 2677 self.options.extra_config_opt.append(
2674 2678 'extensions.logexceptions=%s' % logexceptions.decode('utf-8'))
2675 2679
2676 2680 vlog("# Using TESTDIR", self._testdir)
2677 2681 vlog("# Using RUNTESTDIR", osenvironb[b'RUNTESTDIR'])
2678 2682 vlog("# Using HGTMP", self._hgtmp)
2679 2683 vlog("# Using PATH", os.environ["PATH"])
2680 2684 vlog("# Using", IMPL_PATH, osenvironb[IMPL_PATH])
2681 2685 vlog("# Writing to directory", self._outputdir)
2682 2686
2683 2687 try:
2684 2688 return self._runtests(testdescs) or 0
2685 2689 finally:
2686 2690 time.sleep(.1)
2687 2691 self._cleanup()
2688 2692
2689 2693 def findtests(self, args):
2690 2694 """Finds possible test files from arguments.
2691 2695
2692 2696 If you wish to inject custom tests into the test harness, this would
2693 2697 be a good function to monkeypatch or override in a derived class.
2694 2698 """
2695 2699 if not args:
2696 2700 if self.options.changed:
2697 2701 proc = Popen4('hg st --rev "%s" -man0 .' %
2698 2702 self.options.changed, None, 0)
2699 2703 stdout, stderr = proc.communicate()
2700 2704 args = stdout.strip(b'\0').split(b'\0')
2701 2705 else:
2702 2706 args = os.listdir(b'.')
2703 2707
2704 2708 expanded_args = []
2705 2709 for arg in args:
2706 2710 if os.path.isdir(arg):
2707 2711 if not arg.endswith(b'/'):
2708 2712 arg += b'/'
2709 2713 expanded_args.extend([arg + a for a in os.listdir(arg)])
2710 2714 else:
2711 2715 expanded_args.append(arg)
2712 2716 args = expanded_args
2713 2717
2714 2718 testcasepattern = re.compile(br'([\w-]+\.t|py)(#([a-zA-Z0-9_\-\.#]+))')
2715 2719 tests = []
2716 2720 for t in args:
2717 2721 case = []
2718 2722
2719 2723 if not (os.path.basename(t).startswith(b'test-')
2720 2724 and (t.endswith(b'.py') or t.endswith(b'.t'))):
2721 2725
2722 2726 m = testcasepattern.match(t)
2723 2727 if m is not None:
2724 2728 t, _, casestr = m.groups()
2725 2729 if casestr:
2726 2730 case = casestr.split(b'#')
2727 2731 else:
2728 2732 continue
2729 2733
2730 2734 if t.endswith(b'.t'):
2731 2735 # .t file may contain multiple test cases
2732 2736 casedimensions = parsettestcases(t)
2733 2737 if casedimensions:
2734 2738 cases = []
2735 2739 def addcases(case, casedimensions):
2736 2740 if not casedimensions:
2737 2741 cases.append(case)
2738 2742 else:
2739 2743 for c in casedimensions[0]:
2740 2744 addcases(case + [c], casedimensions[1:])
2741 2745 addcases([], casedimensions)
2742 2746 if case and case in cases:
2743 2747 cases = [case]
2744 2748 elif case:
2745 2749 # Ignore invalid cases
2746 2750 cases = []
2747 2751 else:
2748 2752 pass
2749 2753 tests += [{'path': t, 'case': c} for c in sorted(cases)]
2750 2754 else:
2751 2755 tests.append({'path': t})
2752 2756 else:
2753 2757 tests.append({'path': t})
2754 2758 return tests
2755 2759
2756 2760 def _runtests(self, testdescs):
2757 2761 def _reloadtest(test, i):
2758 2762 # convert a test back to its description dict
2759 2763 desc = {'path': test.path}
2760 2764 case = getattr(test, '_case', [])
2761 2765 if case:
2762 2766 desc['case'] = case
2763 2767 return self._gettest(desc, i)
2764 2768
2765 2769 try:
2766 2770 if self.options.restart:
2767 2771 orig = list(testdescs)
2768 2772 while testdescs:
2769 2773 desc = testdescs[0]
2770 2774 # desc['path'] is a relative path
2771 2775 if 'case' in desc:
2772 2776 casestr = b'#'.join(desc['case'])
2773 2777 errpath = b'%s#%s.err' % (desc['path'], casestr)
2774 2778 else:
2775 2779 errpath = b'%s.err' % desc['path']
2776 2780 errpath = os.path.join(self._outputdir, errpath)
2777 2781 if os.path.exists(errpath):
2778 2782 break
2779 2783 testdescs.pop(0)
2780 2784 if not testdescs:
2781 2785 print("running all tests")
2782 2786 testdescs = orig
2783 2787
2784 2788 tests = [self._gettest(d, i) for i, d in enumerate(testdescs)]
2785 2789
2786 2790 failed = False
2787 2791 kws = self.options.keywords
2788 2792 if kws is not None and PYTHON3:
2789 2793 kws = kws.encode('utf-8')
2790 2794
2791 2795 suite = TestSuite(self._testdir,
2792 2796 jobs=self.options.jobs,
2793 2797 whitelist=self.options.whitelisted,
2794 2798 blacklist=self.options.blacklist,
2795 2799 retest=self.options.retest,
2796 2800 keywords=kws,
2797 2801 loop=self.options.loop,
2798 2802 runs_per_test=self.options.runs_per_test,
2799 2803 showchannels=self.options.showchannels,
2800 2804 tests=tests, loadtest=_reloadtest)
2801 2805 verbosity = 1
2802 2806 if self.options.list_tests:
2803 2807 verbosity = 0
2804 2808 elif self.options.verbose:
2805 2809 verbosity = 2
2806 2810 runner = TextTestRunner(self, verbosity=verbosity)
2807 2811
2808 2812 if self.options.list_tests:
2809 2813 result = runner.listtests(suite)
2810 2814 else:
2811 2815 if self._installdir:
2812 2816 self._installhg()
2813 2817 self._checkhglib("Testing")
2814 2818 else:
2815 2819 self._usecorrectpython()
2816 2820 if self.options.chg:
2817 2821 assert self._installdir
2818 2822 self._installchg()
2819 2823
2820 2824 result = runner.run(suite)
2821 2825
2822 2826 if result.failures:
2823 2827 failed = True
2824 2828
2825 2829 result.onEnd()
2826 2830
2827 2831 if self.options.anycoverage:
2828 2832 self._outputcoverage()
2829 2833 except KeyboardInterrupt:
2830 2834 failed = True
2831 2835 print("\ninterrupted!")
2832 2836
2833 2837 if failed:
2834 2838 return 1
2835 2839
2836 2840 def _getport(self, count):
2837 2841 port = self._ports.get(count) # do we have a cached entry?
2838 2842 if port is None:
2839 2843 portneeded = 3
2840 2844 # above 100 tries we just give up and let test reports failure
2841 2845 for tries in xrange(100):
2842 2846 allfree = True
2843 2847 port = self.options.port + self._portoffset
2844 2848 for idx in xrange(portneeded):
2845 2849 if not checkportisavailable(port + idx):
2846 2850 allfree = False
2847 2851 break
2848 2852 self._portoffset += portneeded
2849 2853 if allfree:
2850 2854 break
2851 2855 self._ports[count] = port
2852 2856 return port
2853 2857
2854 2858 def _gettest(self, testdesc, count):
2855 2859 """Obtain a Test by looking at its filename.
2856 2860
2857 2861 Returns a Test instance. The Test may not be runnable if it doesn't
2858 2862 map to a known type.
2859 2863 """
2860 2864 path = testdesc['path']
2861 2865 lctest = path.lower()
2862 2866 testcls = Test
2863 2867
2864 2868 for ext, cls in self.TESTTYPES:
2865 2869 if lctest.endswith(ext):
2866 2870 testcls = cls
2867 2871 break
2868 2872
2869 2873 refpath = os.path.join(self._testdir, path)
2870 2874 tmpdir = os.path.join(self._hgtmp, b'child%d' % count)
2871 2875
2872 2876 # extra keyword parameters. 'case' is used by .t tests
2873 2877 kwds = dict((k, testdesc[k]) for k in ['case'] if k in testdesc)
2874 2878
2875 2879 t = testcls(refpath, self._outputdir, tmpdir,
2876 2880 keeptmpdir=self.options.keep_tmpdir,
2877 2881 debug=self.options.debug,
2878 2882 first=self.options.first,
2879 2883 timeout=self.options.timeout,
2880 2884 startport=self._getport(count),
2881 2885 extraconfigopts=self.options.extra_config_opt,
2882 2886 py3kwarnings=self.options.py3k_warnings,
2883 2887 shell=self.options.shell,
2884 2888 hgcommand=self._hgcommand,
2885 2889 usechg=bool(self.options.with_chg or self.options.chg),
2886 2890 useipv6=useipv6, **kwds)
2887 2891 t.should_reload = True
2888 2892 return t
2889 2893
2890 2894 def _cleanup(self):
2891 2895 """Clean up state from this test invocation."""
2892 2896 if self.options.keep_tmpdir:
2893 2897 return
2894 2898
2895 2899 vlog("# Cleaning up HGTMP", self._hgtmp)
2896 2900 shutil.rmtree(self._hgtmp, True)
2897 2901 for f in self._createdfiles:
2898 2902 try:
2899 2903 os.remove(f)
2900 2904 except OSError:
2901 2905 pass
2902 2906
2903 2907 def _usecorrectpython(self):
2904 2908 """Configure the environment to use the appropriate Python in tests."""
2905 2909 # Tests must use the same interpreter as us or bad things will happen.
2906 2910 pyexename = sys.platform == 'win32' and b'python.exe' or b'python'
2907 2911 if getattr(os, 'symlink', None):
2908 2912 vlog("# Making python executable in test path a symlink to '%s'" %
2909 2913 sys.executable)
2910 2914 mypython = os.path.join(self._tmpbindir, pyexename)
2911 2915 try:
2912 2916 if os.readlink(mypython) == sys.executable:
2913 2917 return
2914 2918 os.unlink(mypython)
2915 2919 except OSError as err:
2916 2920 if err.errno != errno.ENOENT:
2917 2921 raise
2918 2922 if self._findprogram(pyexename) != sys.executable:
2919 2923 try:
2920 2924 os.symlink(sys.executable, mypython)
2921 2925 self._createdfiles.append(mypython)
2922 2926 except OSError as err:
2923 2927 # child processes may race, which is harmless
2924 2928 if err.errno != errno.EEXIST:
2925 2929 raise
2926 2930 else:
2927 2931 exedir, exename = os.path.split(sys.executable)
2928 2932 vlog("# Modifying search path to find %s as %s in '%s'" %
2929 2933 (exename, pyexename, exedir))
2930 2934 path = os.environ['PATH'].split(os.pathsep)
2931 2935 while exedir in path:
2932 2936 path.remove(exedir)
2933 2937 os.environ['PATH'] = os.pathsep.join([exedir] + path)
2934 2938 if not self._findprogram(pyexename):
2935 2939 print("WARNING: Cannot find %s in search path" % pyexename)
2936 2940
2937 2941 def _installhg(self):
2938 2942 """Install hg into the test environment.
2939 2943
2940 2944 This will also configure hg with the appropriate testing settings.
2941 2945 """
2942 2946 vlog("# Performing temporary installation of HG")
2943 2947 installerrs = os.path.join(self._hgtmp, b"install.err")
2944 2948 compiler = ''
2945 2949 if self.options.compiler:
2946 2950 compiler = '--compiler ' + self.options.compiler
2947 2951 if self.options.pure:
2948 2952 pure = b"--pure"
2949 2953 else:
2950 2954 pure = b""
2951 2955
2952 2956 # Run installer in hg root
2953 2957 script = os.path.realpath(sys.argv[0])
2954 2958 exe = sys.executable
2955 2959 if PYTHON3:
2956 2960 compiler = _bytespath(compiler)
2957 2961 script = _bytespath(script)
2958 2962 exe = _bytespath(exe)
2959 2963 hgroot = os.path.dirname(os.path.dirname(script))
2960 2964 self._hgroot = hgroot
2961 2965 os.chdir(hgroot)
2962 2966 nohome = b'--home=""'
2963 2967 if os.name == 'nt':
2964 2968 # The --home="" trick works only on OS where os.sep == '/'
2965 2969 # because of a distutils convert_path() fast-path. Avoid it at
2966 2970 # least on Windows for now, deal with .pydistutils.cfg bugs
2967 2971 # when they happen.
2968 2972 nohome = b''
2969 2973 cmd = (b'%(exe)s setup.py %(pure)s clean --all'
2970 2974 b' build %(compiler)s --build-base="%(base)s"'
2971 2975 b' install --force --prefix="%(prefix)s"'
2972 2976 b' --install-lib="%(libdir)s"'
2973 2977 b' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
2974 2978 % {b'exe': exe, b'pure': pure,
2975 2979 b'compiler': compiler,
2976 2980 b'base': os.path.join(self._hgtmp, b"build"),
2977 2981 b'prefix': self._installdir, b'libdir': self._pythondir,
2978 2982 b'bindir': self._bindir,
2979 2983 b'nohome': nohome, b'logfile': installerrs})
2980 2984
2981 2985 # setuptools requires install directories to exist.
2982 2986 def makedirs(p):
2983 2987 try:
2984 2988 os.makedirs(p)
2985 2989 except OSError as e:
2986 2990 if e.errno != errno.EEXIST:
2987 2991 raise
2988 2992 makedirs(self._pythondir)
2989 2993 makedirs(self._bindir)
2990 2994
2991 2995 vlog("# Running", cmd)
2992 2996 if os.system(cmd) == 0:
2993 2997 if not self.options.verbose:
2994 2998 try:
2995 2999 os.remove(installerrs)
2996 3000 except OSError as e:
2997 3001 if e.errno != errno.ENOENT:
2998 3002 raise
2999 3003 else:
3000 3004 with open(installerrs, 'rb') as f:
3001 3005 for line in f:
3002 3006 if PYTHON3:
3003 3007 sys.stdout.buffer.write(line)
3004 3008 else:
3005 3009 sys.stdout.write(line)
3006 3010 sys.exit(1)
3007 3011 os.chdir(self._testdir)
3008 3012
3009 3013 self._usecorrectpython()
3010 3014
3011 3015 if self.options.py3k_warnings and not self.options.anycoverage:
3012 3016 vlog("# Updating hg command to enable Py3k Warnings switch")
3013 3017 with open(os.path.join(self._bindir, 'hg'), 'rb') as f:
3014 3018 lines = [line.rstrip() for line in f]
3015 3019 lines[0] += ' -3'
3016 3020 with open(os.path.join(self._bindir, 'hg'), 'wb') as f:
3017 3021 for line in lines:
3018 3022 f.write(line + '\n')
3019 3023
3020 3024 hgbat = os.path.join(self._bindir, b'hg.bat')
3021 3025 if os.path.isfile(hgbat):
3022 3026 # hg.bat expects to be put in bin/scripts while run-tests.py
3023 3027 # installation layout put it in bin/ directly. Fix it
3024 3028 with open(hgbat, 'rb') as f:
3025 3029 data = f.read()
3026 3030 if b'"%~dp0..\python" "%~dp0hg" %*' in data:
3027 3031 data = data.replace(b'"%~dp0..\python" "%~dp0hg" %*',
3028 3032 b'"%~dp0python" "%~dp0hg" %*')
3029 3033 with open(hgbat, 'wb') as f:
3030 3034 f.write(data)
3031 3035 else:
3032 3036 print('WARNING: cannot fix hg.bat reference to python.exe')
3033 3037
3034 3038 if self.options.anycoverage:
3035 3039 custom = os.path.join(self._testdir, 'sitecustomize.py')
3036 3040 target = os.path.join(self._pythondir, 'sitecustomize.py')
3037 3041 vlog('# Installing coverage trigger to %s' % target)
3038 3042 shutil.copyfile(custom, target)
3039 3043 rc = os.path.join(self._testdir, '.coveragerc')
3040 3044 vlog('# Installing coverage rc to %s' % rc)
3041 3045 os.environ['COVERAGE_PROCESS_START'] = rc
3042 3046 covdir = os.path.join(self._installdir, '..', 'coverage')
3043 3047 try:
3044 3048 os.mkdir(covdir)
3045 3049 except OSError as e:
3046 3050 if e.errno != errno.EEXIST:
3047 3051 raise
3048 3052
3049 3053 os.environ['COVERAGE_DIR'] = covdir
3050 3054
3051 3055 def _checkhglib(self, verb):
3052 3056 """Ensure that the 'mercurial' package imported by python is
3053 3057 the one we expect it to be. If not, print a warning to stderr."""
3054 3058 if ((self._bindir == self._pythondir) and
3055 3059 (self._bindir != self._tmpbindir)):
3056 3060 # The pythondir has been inferred from --with-hg flag.
3057 3061 # We cannot expect anything sensible here.
3058 3062 return
3059 3063 expecthg = os.path.join(self._pythondir, b'mercurial')
3060 3064 actualhg = self._gethgpath()
3061 3065 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
3062 3066 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
3063 3067 ' (expected %s)\n'
3064 3068 % (verb, actualhg, expecthg))
3065 3069 def _gethgpath(self):
3066 3070 """Return the path to the mercurial package that is actually found by
3067 3071 the current Python interpreter."""
3068 3072 if self._hgpath is not None:
3069 3073 return self._hgpath
3070 3074
3071 3075 cmd = b'%s -c "import mercurial; print (mercurial.__path__[0])"'
3072 3076 cmd = cmd % PYTHON
3073 3077 if PYTHON3:
3074 3078 cmd = _strpath(cmd)
3075 3079 pipe = os.popen(cmd)
3076 3080 try:
3077 3081 self._hgpath = _bytespath(pipe.read().strip())
3078 3082 finally:
3079 3083 pipe.close()
3080 3084
3081 3085 return self._hgpath
3082 3086
3083 3087 def _installchg(self):
3084 3088 """Install chg into the test environment"""
3085 3089 vlog('# Performing temporary installation of CHG')
3086 3090 assert os.path.dirname(self._bindir) == self._installdir
3087 3091 assert self._hgroot, 'must be called after _installhg()'
3088 3092 cmd = (b'"%(make)s" clean install PREFIX="%(prefix)s"'
3089 3093 % {b'make': 'make', # TODO: switch by option or environment?
3090 3094 b'prefix': self._installdir})
3091 3095 cwd = os.path.join(self._hgroot, b'contrib', b'chg')
3092 3096 vlog("# Running", cmd)
3093 3097 proc = subprocess.Popen(cmd, shell=True, cwd=cwd,
3094 3098 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
3095 3099 stderr=subprocess.STDOUT)
3096 3100 out, _err = proc.communicate()
3097 3101 if proc.returncode != 0:
3098 3102 if PYTHON3:
3099 3103 sys.stdout.buffer.write(out)
3100 3104 else:
3101 3105 sys.stdout.write(out)
3102 3106 sys.exit(1)
3103 3107
3104 3108 def _outputcoverage(self):
3105 3109 """Produce code coverage output."""
3106 3110 import coverage
3107 3111 coverage = coverage.coverage
3108 3112
3109 3113 vlog('# Producing coverage report')
3110 3114 # chdir is the easiest way to get short, relative paths in the
3111 3115 # output.
3112 3116 os.chdir(self._hgroot)
3113 3117 covdir = os.path.join(self._installdir, '..', 'coverage')
3114 3118 cov = coverage(data_file=os.path.join(covdir, 'cov'))
3115 3119
3116 3120 # Map install directory paths back to source directory.
3117 3121 cov.config.paths['srcdir'] = ['.', self._pythondir]
3118 3122
3119 3123 cov.combine()
3120 3124
3121 3125 omit = [os.path.join(x, '*') for x in [self._bindir, self._testdir]]
3122 3126 cov.report(ignore_errors=True, omit=omit)
3123 3127
3124 3128 if self.options.htmlcov:
3125 3129 htmldir = os.path.join(self._outputdir, 'htmlcov')
3126 3130 cov.html_report(directory=htmldir, omit=omit)
3127 3131 if self.options.annotate:
3128 3132 adir = os.path.join(self._outputdir, 'annotated')
3129 3133 if not os.path.isdir(adir):
3130 3134 os.mkdir(adir)
3131 3135 cov.annotate(directory=adir, omit=omit)
3132 3136
3133 3137 def _findprogram(self, program):
3134 3138 """Search PATH for a executable program"""
3135 3139 dpb = _bytespath(os.defpath)
3136 3140 sepb = _bytespath(os.pathsep)
3137 3141 for p in osenvironb.get(b'PATH', dpb).split(sepb):
3138 3142 name = os.path.join(p, program)
3139 3143 if os.name == 'nt' or os.access(name, os.X_OK):
3140 3144 return name
3141 3145 return None
3142 3146
3143 3147 def _checktools(self):
3144 3148 """Ensure tools required to run tests are present."""
3145 3149 for p in self.REQUIREDTOOLS:
3146 3150 if os.name == 'nt' and not p.endswith('.exe'):
3147 3151 p += '.exe'
3148 3152 found = self._findprogram(p)
3149 3153 if found:
3150 3154 vlog("# Found prerequisite", p, "at", found)
3151 3155 else:
3152 3156 print("WARNING: Did not find prerequisite tool: %s " %
3153 3157 p.decode("utf-8"))
3154 3158
3155 3159 def aggregateexceptions(path):
3156 3160 exceptioncounts = collections.Counter()
3157 3161 testsbyfailure = collections.defaultdict(set)
3158 3162 failuresbytest = collections.defaultdict(set)
3159 3163
3160 3164 for f in os.listdir(path):
3161 3165 with open(os.path.join(path, f), 'rb') as fh:
3162 3166 data = fh.read().split(b'\0')
3163 3167 if len(data) != 5:
3164 3168 continue
3165 3169
3166 3170 exc, mainframe, hgframe, hgline, testname = data
3167 3171 exc = exc.decode('utf-8')
3168 3172 mainframe = mainframe.decode('utf-8')
3169 3173 hgframe = hgframe.decode('utf-8')
3170 3174 hgline = hgline.decode('utf-8')
3171 3175 testname = testname.decode('utf-8')
3172 3176
3173 3177 key = (hgframe, hgline, exc)
3174 3178 exceptioncounts[key] += 1
3175 3179 testsbyfailure[key].add(testname)
3176 3180 failuresbytest[testname].add(key)
3177 3181
3178 3182 # Find test having fewest failures for each failure.
3179 3183 leastfailing = {}
3180 3184 for key, tests in testsbyfailure.items():
3181 3185 fewesttest = None
3182 3186 fewestcount = 99999999
3183 3187 for test in sorted(tests):
3184 3188 if len(failuresbytest[test]) < fewestcount:
3185 3189 fewesttest = test
3186 3190 fewestcount = len(failuresbytest[test])
3187 3191
3188 3192 leastfailing[key] = (fewestcount, fewesttest)
3189 3193
3190 3194 # Create a combined counter so we can sort by total occurrences and
3191 3195 # impacted tests.
3192 3196 combined = {}
3193 3197 for key in exceptioncounts:
3194 3198 combined[key] = (exceptioncounts[key],
3195 3199 len(testsbyfailure[key]),
3196 3200 leastfailing[key][0],
3197 3201 leastfailing[key][1])
3198 3202
3199 3203 return {
3200 3204 'exceptioncounts': exceptioncounts,
3201 3205 'total': sum(exceptioncounts.values()),
3202 3206 'combined': combined,
3203 3207 'leastfailing': leastfailing,
3204 3208 'byfailure': testsbyfailure,
3205 3209 'bytest': failuresbytest,
3206 3210 }
3207 3211
3208 3212 if __name__ == '__main__':
3209 3213 runner = TestRunner()
3210 3214
3211 3215 try:
3212 3216 import msvcrt
3213 3217 msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
3214 3218 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
3215 3219 msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
3216 3220 except ImportError:
3217 3221 pass
3218 3222
3219 3223 sys.exit(runner.run(sys.argv[1:]))
General Comments 0
You need to be logged in to leave comments. Login now