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