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