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