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