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