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