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