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