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