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