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