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