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