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