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