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