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