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