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