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