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