##// END OF EJS Templates
tests: setup dummyssh as the default ssh...
Valentin Gatien-Baron -
r48731:a28a7dcb default
parent child Browse files
Show More
@@ -1,4067 +1,4069 b''
1 1 #!/usr/bin/env python3
2 2 #
3 3 # run-tests.py - Run a set of tests on Mercurial
4 4 #
5 5 # Copyright 2006 Olivia Mackall <olivia@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 argparse
49 49 import collections
50 50 import contextlib
51 51 import difflib
52 52 import distutils.version as version
53 53 import errno
54 54 import json
55 55 import multiprocessing
56 56 import os
57 57 import platform
58 58 import random
59 59 import re
60 60 import shutil
61 61 import signal
62 62 import socket
63 63 import subprocess
64 64 import sys
65 65 import sysconfig
66 66 import tempfile
67 67 import threading
68 68 import time
69 69 import unittest
70 70 import uuid
71 71 import xml.dom.minidom as minidom
72 72
73 73 WINDOWS = os.name == r'nt'
74 74
75 75 try:
76 76 import Queue as queue
77 77 except ImportError:
78 78 import queue
79 79
80 80 try:
81 81 import shlex
82 82
83 83 shellquote = shlex.quote
84 84 except (ImportError, AttributeError):
85 85 import pipes
86 86
87 87 shellquote = pipes.quote
88 88
89 89
90 90 processlock = threading.Lock()
91 91
92 92 pygmentspresent = False
93 93 try: # is pygments installed
94 94 import pygments
95 95 import pygments.lexers as lexers
96 96 import pygments.lexer as lexer
97 97 import pygments.formatters as formatters
98 98 import pygments.token as token
99 99 import pygments.style as style
100 100
101 101 if WINDOWS:
102 102 hgpath = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
103 103 sys.path.append(hgpath)
104 104 try:
105 105 from mercurial import win32 # pytype: disable=import-error
106 106
107 107 # Don't check the result code because it fails on heptapod, but
108 108 # something is able to convert to color anyway.
109 109 win32.enablevtmode()
110 110 finally:
111 111 sys.path = sys.path[:-1]
112 112
113 113 pygmentspresent = True
114 114 difflexer = lexers.DiffLexer()
115 115 terminal256formatter = formatters.Terminal256Formatter()
116 116 except ImportError:
117 117 pass
118 118
119 119 if pygmentspresent:
120 120
121 121 class TestRunnerStyle(style.Style):
122 122 default_style = ""
123 123 skipped = token.string_to_tokentype("Token.Generic.Skipped")
124 124 failed = token.string_to_tokentype("Token.Generic.Failed")
125 125 skippedname = token.string_to_tokentype("Token.Generic.SName")
126 126 failedname = token.string_to_tokentype("Token.Generic.FName")
127 127 styles = {
128 128 skipped: '#e5e5e5',
129 129 skippedname: '#00ffff',
130 130 failed: '#7f0000',
131 131 failedname: '#ff0000',
132 132 }
133 133
134 134 class TestRunnerLexer(lexer.RegexLexer):
135 135 testpattern = r'[\w-]+\.(t|py)(#[a-zA-Z0-9_\-\.]+)?'
136 136 tokens = {
137 137 'root': [
138 138 (r'^Skipped', token.Generic.Skipped, 'skipped'),
139 139 (r'^Failed ', token.Generic.Failed, 'failed'),
140 140 (r'^ERROR: ', token.Generic.Failed, 'failed'),
141 141 ],
142 142 'skipped': [
143 143 (testpattern, token.Generic.SName),
144 144 (r':.*', token.Generic.Skipped),
145 145 ],
146 146 'failed': [
147 147 (testpattern, token.Generic.FName),
148 148 (r'(:| ).*', token.Generic.Failed),
149 149 ],
150 150 }
151 151
152 152 runnerformatter = formatters.Terminal256Formatter(style=TestRunnerStyle)
153 153 runnerlexer = TestRunnerLexer()
154 154
155 155 origenviron = os.environ.copy()
156 156
157 157
158 158 if sys.version_info > (3, 5, 0):
159 159 PYTHON3 = True
160 160 xrange = range # we use xrange in one place, and we'd rather not use range
161 161
162 162 def _sys2bytes(p):
163 163 if p is None:
164 164 return p
165 165 return p.encode('utf-8')
166 166
167 167 def _bytes2sys(p):
168 168 if p is None:
169 169 return p
170 170 return p.decode('utf-8')
171 171
172 172 osenvironb = getattr(os, 'environb', None)
173 173 if osenvironb is None:
174 174 # Windows lacks os.environb, for instance. A proxy over the real thing
175 175 # instead of a copy allows the environment to be updated via bytes on
176 176 # all platforms.
177 177 class environbytes(object):
178 178 def __init__(self, strenv):
179 179 self.__len__ = strenv.__len__
180 180 self.clear = strenv.clear
181 181 self._strenv = strenv
182 182
183 183 def __getitem__(self, k):
184 184 v = self._strenv.__getitem__(_bytes2sys(k))
185 185 return _sys2bytes(v)
186 186
187 187 def __setitem__(self, k, v):
188 188 self._strenv.__setitem__(_bytes2sys(k), _bytes2sys(v))
189 189
190 190 def __delitem__(self, k):
191 191 self._strenv.__delitem__(_bytes2sys(k))
192 192
193 193 def __contains__(self, k):
194 194 return self._strenv.__contains__(_bytes2sys(k))
195 195
196 196 def __iter__(self):
197 197 return iter([_sys2bytes(k) for k in iter(self._strenv)])
198 198
199 199 def get(self, k, default=None):
200 200 v = self._strenv.get(_bytes2sys(k), _bytes2sys(default))
201 201 return _sys2bytes(v)
202 202
203 203 def pop(self, k, default=None):
204 204 v = self._strenv.pop(_bytes2sys(k), _bytes2sys(default))
205 205 return _sys2bytes(v)
206 206
207 207 osenvironb = environbytes(os.environ)
208 208
209 209 getcwdb = getattr(os, 'getcwdb')
210 210 if not getcwdb or WINDOWS:
211 211 getcwdb = lambda: _sys2bytes(os.getcwd())
212 212
213 213 elif sys.version_info >= (3, 0, 0):
214 214 print(
215 215 '%s is only supported on Python 3.5+ and 2.7, not %s'
216 216 % (sys.argv[0], '.'.join(str(v) for v in sys.version_info[:3]))
217 217 )
218 218 sys.exit(70) # EX_SOFTWARE from `man 3 sysexit`
219 219 else:
220 220 PYTHON3 = False
221 221
222 222 # In python 2.x, path operations are generally done using
223 223 # bytestrings by default, so we don't have to do any extra
224 224 # fiddling there. We define the wrapper functions anyway just to
225 225 # help keep code consistent between platforms.
226 226 def _sys2bytes(p):
227 227 return p
228 228
229 229 _bytes2sys = _sys2bytes
230 230 osenvironb = os.environ
231 231 getcwdb = os.getcwd
232 232
233 233 if WINDOWS:
234 234 _getcwdb = getcwdb
235 235
236 236 def getcwdb():
237 237 cwd = _getcwdb()
238 238 if re.match(b'^[a-z]:', cwd):
239 239 # os.getcwd() is inconsistent on the capitalization of the drive
240 240 # letter, so adjust it. see https://bugs.python.org/issue40368
241 241 cwd = cwd[0:1].upper() + cwd[1:]
242 242 return cwd
243 243
244 244
245 245 # For Windows support
246 246 wifexited = getattr(os, "WIFEXITED", lambda x: False)
247 247
248 248 # Whether to use IPv6
249 249 def checksocketfamily(name, port=20058):
250 250 """return true if we can listen on localhost using family=name
251 251
252 252 name should be either 'AF_INET', or 'AF_INET6'.
253 253 port being used is okay - EADDRINUSE is considered as successful.
254 254 """
255 255 family = getattr(socket, name, None)
256 256 if family is None:
257 257 return False
258 258 try:
259 259 s = socket.socket(family, socket.SOCK_STREAM)
260 260 s.bind(('localhost', port))
261 261 s.close()
262 262 return True
263 263 except socket.error as exc:
264 264 if exc.errno == errno.EADDRINUSE:
265 265 return True
266 266 elif exc.errno in (errno.EADDRNOTAVAIL, errno.EPROTONOSUPPORT):
267 267 return False
268 268 else:
269 269 raise
270 270 else:
271 271 return False
272 272
273 273
274 274 # useipv6 will be set by parseargs
275 275 useipv6 = None
276 276
277 277
278 278 def checkportisavailable(port):
279 279 """return true if a port seems free to bind on localhost"""
280 280 if useipv6:
281 281 family = socket.AF_INET6
282 282 else:
283 283 family = socket.AF_INET
284 284 try:
285 285 with contextlib.closing(socket.socket(family, socket.SOCK_STREAM)) as s:
286 286 s.bind(('localhost', port))
287 287 return True
288 288 except socket.error as exc:
289 289 if WINDOWS and exc.errno == errno.WSAEACCES:
290 290 return False
291 291 elif PYTHON3:
292 292 # TODO: make a proper exception handler after dropping py2. This
293 293 # works because socket.error is an alias for OSError on py3,
294 294 # which is also the baseclass of PermissionError.
295 295 if isinstance(exc, PermissionError):
296 296 return False
297 297 if exc.errno not in (
298 298 errno.EADDRINUSE,
299 299 errno.EADDRNOTAVAIL,
300 300 errno.EPROTONOSUPPORT,
301 301 ):
302 302 raise
303 303 return False
304 304
305 305
306 306 closefds = os.name == 'posix'
307 307
308 308
309 309 def Popen4(cmd, wd, timeout, env=None):
310 310 processlock.acquire()
311 311 p = subprocess.Popen(
312 312 _bytes2sys(cmd),
313 313 shell=True,
314 314 bufsize=-1,
315 315 cwd=_bytes2sys(wd),
316 316 env=env,
317 317 close_fds=closefds,
318 318 stdin=subprocess.PIPE,
319 319 stdout=subprocess.PIPE,
320 320 stderr=subprocess.STDOUT,
321 321 )
322 322 processlock.release()
323 323
324 324 p.fromchild = p.stdout
325 325 p.tochild = p.stdin
326 326 p.childerr = p.stderr
327 327
328 328 p.timeout = False
329 329 if timeout:
330 330
331 331 def t():
332 332 start = time.time()
333 333 while time.time() - start < timeout and p.returncode is None:
334 334 time.sleep(0.1)
335 335 p.timeout = True
336 336 vlog('# Timout reached for process %d' % p.pid)
337 337 if p.returncode is None:
338 338 terminate(p)
339 339
340 340 threading.Thread(target=t).start()
341 341
342 342 return p
343 343
344 344
345 345 if sys.executable:
346 346 sysexecutable = sys.executable
347 347 elif os.environ.get('PYTHONEXECUTABLE'):
348 348 sysexecutable = os.environ['PYTHONEXECUTABLE']
349 349 elif os.environ.get('PYTHON'):
350 350 sysexecutable = os.environ['PYTHON']
351 351 else:
352 352 raise AssertionError('Could not find Python interpreter')
353 353
354 354 PYTHON = _sys2bytes(sysexecutable.replace('\\', '/'))
355 355 IMPL_PATH = b'PYTHONPATH'
356 356 if 'java' in sys.platform:
357 357 IMPL_PATH = b'JYTHONPATH'
358 358
359 359 default_defaults = {
360 360 'jobs': ('HGTEST_JOBS', multiprocessing.cpu_count()),
361 361 'timeout': ('HGTEST_TIMEOUT', 360),
362 362 'slowtimeout': ('HGTEST_SLOWTIMEOUT', 1500),
363 363 'port': ('HGTEST_PORT', 20059),
364 364 'shell': ('HGTEST_SHELL', 'sh'),
365 365 }
366 366
367 367 defaults = default_defaults.copy()
368 368
369 369
370 370 def canonpath(path):
371 371 return os.path.realpath(os.path.expanduser(path))
372 372
373 373
374 374 def which(exe):
375 375 if PYTHON3:
376 376 # shutil.which only accept bytes from 3.8
377 377 cmd = _bytes2sys(exe)
378 378 real_exec = shutil.which(cmd)
379 379 return _sys2bytes(real_exec)
380 380 else:
381 381 # let us do the os work
382 382 for p in osenvironb[b'PATH'].split(os.pathsep):
383 383 f = os.path.join(p, exe)
384 384 if os.path.isfile(f):
385 385 return f
386 386 return None
387 387
388 388
389 389 def parselistfiles(files, listtype, warn=True):
390 390 entries = dict()
391 391 for filename in files:
392 392 try:
393 393 path = os.path.expanduser(os.path.expandvars(filename))
394 394 f = open(path, "rb")
395 395 except IOError as err:
396 396 if err.errno != errno.ENOENT:
397 397 raise
398 398 if warn:
399 399 print("warning: no such %s file: %s" % (listtype, filename))
400 400 continue
401 401
402 402 for line in f.readlines():
403 403 line = line.split(b'#', 1)[0].strip()
404 404 if line:
405 405 # Ensure path entries are compatible with os.path.relpath()
406 406 entries[os.path.normpath(line)] = filename
407 407
408 408 f.close()
409 409 return entries
410 410
411 411
412 412 def parsettestcases(path):
413 413 """read a .t test file, return a set of test case names
414 414
415 415 If path does not exist, return an empty set.
416 416 """
417 417 cases = []
418 418 try:
419 419 with open(path, 'rb') as f:
420 420 for l in f:
421 421 if l.startswith(b'#testcases '):
422 422 cases.append(sorted(l[11:].split()))
423 423 except IOError as ex:
424 424 if ex.errno != errno.ENOENT:
425 425 raise
426 426 return cases
427 427
428 428
429 429 def getparser():
430 430 """Obtain the OptionParser used by the CLI."""
431 431 parser = argparse.ArgumentParser(usage='%(prog)s [options] [tests]')
432 432
433 433 selection = parser.add_argument_group('Test Selection')
434 434 selection.add_argument(
435 435 '--allow-slow-tests',
436 436 action='store_true',
437 437 help='allow extremely slow tests',
438 438 )
439 439 selection.add_argument(
440 440 "--blacklist",
441 441 action="append",
442 442 help="skip tests listed in the specified blacklist file",
443 443 )
444 444 selection.add_argument(
445 445 "--changed",
446 446 help="run tests that are changed in parent rev or working directory",
447 447 )
448 448 selection.add_argument(
449 449 "-k", "--keywords", help="run tests matching keywords"
450 450 )
451 451 selection.add_argument(
452 452 "-r", "--retest", action="store_true", help="retest failed tests"
453 453 )
454 454 selection.add_argument(
455 455 "--test-list",
456 456 action="append",
457 457 help="read tests to run from the specified file",
458 458 )
459 459 selection.add_argument(
460 460 "--whitelist",
461 461 action="append",
462 462 help="always run tests listed in the specified whitelist file",
463 463 )
464 464 selection.add_argument(
465 465 'tests', metavar='TESTS', nargs='*', help='Tests to run'
466 466 )
467 467
468 468 harness = parser.add_argument_group('Test Harness Behavior')
469 469 harness.add_argument(
470 470 '--bisect-repo',
471 471 metavar='bisect_repo',
472 472 help=(
473 473 "Path of a repo to bisect. Use together with " "--known-good-rev"
474 474 ),
475 475 )
476 476 harness.add_argument(
477 477 "-d",
478 478 "--debug",
479 479 action="store_true",
480 480 help="debug mode: write output of test scripts to console"
481 481 " rather than capturing and diffing it (disables timeout)",
482 482 )
483 483 harness.add_argument(
484 484 "-f",
485 485 "--first",
486 486 action="store_true",
487 487 help="exit on the first test failure",
488 488 )
489 489 harness.add_argument(
490 490 "-i",
491 491 "--interactive",
492 492 action="store_true",
493 493 help="prompt to accept changed output",
494 494 )
495 495 harness.add_argument(
496 496 "-j",
497 497 "--jobs",
498 498 type=int,
499 499 help="number of jobs to run in parallel"
500 500 " (default: $%s or %d)" % defaults['jobs'],
501 501 )
502 502 harness.add_argument(
503 503 "--keep-tmpdir",
504 504 action="store_true",
505 505 help="keep temporary directory after running tests",
506 506 )
507 507 harness.add_argument(
508 508 '--known-good-rev',
509 509 metavar="known_good_rev",
510 510 help=(
511 511 "Automatically bisect any failures using this "
512 512 "revision as a known-good revision."
513 513 ),
514 514 )
515 515 harness.add_argument(
516 516 "--list-tests",
517 517 action="store_true",
518 518 help="list tests instead of running them",
519 519 )
520 520 harness.add_argument(
521 521 "--loop", action="store_true", help="loop tests repeatedly"
522 522 )
523 523 harness.add_argument(
524 524 '--random', action="store_true", help='run tests in random order'
525 525 )
526 526 harness.add_argument(
527 527 '--order-by-runtime',
528 528 action="store_true",
529 529 help='run slowest tests first, according to .testtimes',
530 530 )
531 531 harness.add_argument(
532 532 "-p",
533 533 "--port",
534 534 type=int,
535 535 help="port on which servers should listen"
536 536 " (default: $%s or %d)" % defaults['port'],
537 537 )
538 538 harness.add_argument(
539 539 '--profile-runner',
540 540 action='store_true',
541 541 help='run statprof on run-tests',
542 542 )
543 543 harness.add_argument(
544 544 "-R", "--restart", action="store_true", help="restart at last error"
545 545 )
546 546 harness.add_argument(
547 547 "--runs-per-test",
548 548 type=int,
549 549 dest="runs_per_test",
550 550 help="run each test N times (default=1)",
551 551 default=1,
552 552 )
553 553 harness.add_argument(
554 554 "--shell", help="shell to use (default: $%s or %s)" % defaults['shell']
555 555 )
556 556 harness.add_argument(
557 557 '--showchannels', action='store_true', help='show scheduling channels'
558 558 )
559 559 harness.add_argument(
560 560 "--slowtimeout",
561 561 type=int,
562 562 help="kill errant slow tests after SLOWTIMEOUT seconds"
563 563 " (default: $%s or %d)" % defaults['slowtimeout'],
564 564 )
565 565 harness.add_argument(
566 566 "-t",
567 567 "--timeout",
568 568 type=int,
569 569 help="kill errant tests after TIMEOUT seconds"
570 570 " (default: $%s or %d)" % defaults['timeout'],
571 571 )
572 572 harness.add_argument(
573 573 "--tmpdir",
574 574 help="run tests in the given temporary directory"
575 575 " (implies --keep-tmpdir)",
576 576 )
577 577 harness.add_argument(
578 578 "-v", "--verbose", action="store_true", help="output verbose messages"
579 579 )
580 580
581 581 hgconf = parser.add_argument_group('Mercurial Configuration')
582 582 hgconf.add_argument(
583 583 "--chg",
584 584 action="store_true",
585 585 help="install and use chg wrapper in place of hg",
586 586 )
587 587 hgconf.add_argument(
588 588 "--chg-debug",
589 589 action="store_true",
590 590 help="show chg debug logs",
591 591 )
592 592 hgconf.add_argument(
593 593 "--rhg",
594 594 action="store_true",
595 595 help="install and use rhg Rust implementation in place of hg",
596 596 )
597 597 hgconf.add_argument(
598 598 "--pyoxidized",
599 599 action="store_true",
600 600 help="build the hg binary using pyoxidizer",
601 601 )
602 602 hgconf.add_argument("--compiler", help="compiler to build with")
603 603 hgconf.add_argument(
604 604 '--extra-config-opt',
605 605 action="append",
606 606 default=[],
607 607 help='set the given config opt in the test hgrc',
608 608 )
609 609 hgconf.add_argument(
610 610 "-l",
611 611 "--local",
612 612 action="store_true",
613 613 help="shortcut for --with-hg=<testdir>/../hg, "
614 614 "--with-rhg=<testdir>/../rust/target/release/rhg if --rhg is set, "
615 615 "and --with-chg=<testdir>/../contrib/chg/chg if --chg is set",
616 616 )
617 617 hgconf.add_argument(
618 618 "--ipv6",
619 619 action="store_true",
620 620 help="prefer IPv6 to IPv4 for network related tests",
621 621 )
622 622 hgconf.add_argument(
623 623 "--pure",
624 624 action="store_true",
625 625 help="use pure Python code instead of C extensions",
626 626 )
627 627 hgconf.add_argument(
628 628 "--rust",
629 629 action="store_true",
630 630 help="use Rust code alongside C extensions",
631 631 )
632 632 hgconf.add_argument(
633 633 "--no-rust",
634 634 action="store_true",
635 635 help="do not use Rust code even if compiled",
636 636 )
637 637 hgconf.add_argument(
638 638 "--with-chg",
639 639 metavar="CHG",
640 640 help="use specified chg wrapper in place of hg",
641 641 )
642 642 hgconf.add_argument(
643 643 "--with-rhg",
644 644 metavar="RHG",
645 645 help="use specified rhg Rust implementation in place of hg",
646 646 )
647 647 hgconf.add_argument(
648 648 "--with-hg",
649 649 metavar="HG",
650 650 help="test using specified hg script rather than a "
651 651 "temporary installation",
652 652 )
653 653
654 654 reporting = parser.add_argument_group('Results Reporting')
655 655 reporting.add_argument(
656 656 "-C",
657 657 "--annotate",
658 658 action="store_true",
659 659 help="output files annotated with coverage",
660 660 )
661 661 reporting.add_argument(
662 662 "--color",
663 663 choices=["always", "auto", "never"],
664 664 default=os.environ.get('HGRUNTESTSCOLOR', 'auto'),
665 665 help="colorisation: always|auto|never (default: auto)",
666 666 )
667 667 reporting.add_argument(
668 668 "-c",
669 669 "--cover",
670 670 action="store_true",
671 671 help="print a test coverage report",
672 672 )
673 673 reporting.add_argument(
674 674 '--exceptions',
675 675 action='store_true',
676 676 help='log all exceptions and generate an exception report',
677 677 )
678 678 reporting.add_argument(
679 679 "-H",
680 680 "--htmlcov",
681 681 action="store_true",
682 682 help="create an HTML report of the coverage of the files",
683 683 )
684 684 reporting.add_argument(
685 685 "--json",
686 686 action="store_true",
687 687 help="store test result data in 'report.json' file",
688 688 )
689 689 reporting.add_argument(
690 690 "--outputdir",
691 691 help="directory to write error logs to (default=test directory)",
692 692 )
693 693 reporting.add_argument(
694 694 "-n", "--nodiff", action="store_true", help="skip showing test changes"
695 695 )
696 696 reporting.add_argument(
697 697 "-S",
698 698 "--noskips",
699 699 action="store_true",
700 700 help="don't report skip tests verbosely",
701 701 )
702 702 reporting.add_argument(
703 703 "--time", action="store_true", help="time how long each test takes"
704 704 )
705 705 reporting.add_argument("--view", help="external diff viewer")
706 706 reporting.add_argument(
707 707 "--xunit", help="record xunit results at specified path"
708 708 )
709 709
710 710 for option, (envvar, default) in defaults.items():
711 711 defaults[option] = type(default)(os.environ.get(envvar, default))
712 712 parser.set_defaults(**defaults)
713 713
714 714 return parser
715 715
716 716
717 717 def parseargs(args, parser):
718 718 """Parse arguments with our OptionParser and validate results."""
719 719 options = parser.parse_args(args)
720 720
721 721 # jython is always pure
722 722 if 'java' in sys.platform or '__pypy__' in sys.modules:
723 723 options.pure = True
724 724
725 725 if platform.python_implementation() != 'CPython' and options.rust:
726 726 parser.error('Rust extensions are only available with CPython')
727 727
728 728 if options.pure and options.rust:
729 729 parser.error('--rust cannot be used with --pure')
730 730
731 731 if options.rust and options.no_rust:
732 732 parser.error('--rust cannot be used with --no-rust')
733 733
734 734 if options.local:
735 735 if options.with_hg or options.with_rhg or options.with_chg:
736 736 parser.error(
737 737 '--local cannot be used with --with-hg or --with-rhg or --with-chg'
738 738 )
739 739 if options.pyoxidized:
740 740 parser.error('--pyoxidized does not work with --local (yet)')
741 741 testdir = os.path.dirname(_sys2bytes(canonpath(sys.argv[0])))
742 742 reporootdir = os.path.dirname(testdir)
743 743 pathandattrs = [(b'hg', 'with_hg')]
744 744 if options.chg:
745 745 pathandattrs.append((b'contrib/chg/chg', 'with_chg'))
746 746 if options.rhg:
747 747 pathandattrs.append((b'rust/target/release/rhg', 'with_rhg'))
748 748 for relpath, attr in pathandattrs:
749 749 binpath = os.path.join(reporootdir, relpath)
750 750 if not (WINDOWS or os.access(binpath, os.X_OK)):
751 751 parser.error(
752 752 '--local specified, but %r not found or '
753 753 'not executable' % binpath
754 754 )
755 755 setattr(options, attr, _bytes2sys(binpath))
756 756
757 757 if options.with_hg:
758 758 options.with_hg = canonpath(_sys2bytes(options.with_hg))
759 759 if not (
760 760 os.path.isfile(options.with_hg)
761 761 and os.access(options.with_hg, os.X_OK)
762 762 ):
763 763 parser.error('--with-hg must specify an executable hg script')
764 764 if os.path.basename(options.with_hg) not in [b'hg', b'hg.exe']:
765 765 msg = 'warning: --with-hg should specify an hg script, not: %s\n'
766 766 msg %= _bytes2sys(os.path.basename(options.with_hg))
767 767 sys.stderr.write(msg)
768 768 sys.stderr.flush()
769 769
770 770 if (options.chg or options.with_chg) and WINDOWS:
771 771 parser.error('chg does not work on %s' % os.name)
772 772 if (options.rhg or options.with_rhg) and WINDOWS:
773 773 parser.error('rhg does not work on %s' % os.name)
774 774 if options.pyoxidized and not WINDOWS:
775 775 parser.error('--pyoxidized is currently Windows only')
776 776 if options.with_chg:
777 777 options.chg = False # no installation to temporary location
778 778 options.with_chg = canonpath(_sys2bytes(options.with_chg))
779 779 if not (
780 780 os.path.isfile(options.with_chg)
781 781 and os.access(options.with_chg, os.X_OK)
782 782 ):
783 783 parser.error('--with-chg must specify a chg executable')
784 784 if options.with_rhg:
785 785 options.rhg = False # no installation to temporary location
786 786 options.with_rhg = canonpath(_sys2bytes(options.with_rhg))
787 787 if not (
788 788 os.path.isfile(options.with_rhg)
789 789 and os.access(options.with_rhg, os.X_OK)
790 790 ):
791 791 parser.error('--with-rhg must specify a rhg executable')
792 792 if options.chg and options.with_hg:
793 793 # chg shares installation location with hg
794 794 parser.error(
795 795 '--chg does not work when --with-hg is specified '
796 796 '(use --with-chg instead)'
797 797 )
798 798 if options.rhg and options.with_hg:
799 799 # rhg shares installation location with hg
800 800 parser.error(
801 801 '--rhg does not work when --with-hg is specified '
802 802 '(use --with-rhg instead)'
803 803 )
804 804 if options.rhg and options.chg:
805 805 parser.error('--rhg and --chg do not work together')
806 806
807 807 if options.color == 'always' and not pygmentspresent:
808 808 sys.stderr.write(
809 809 'warning: --color=always ignored because '
810 810 'pygments is not installed\n'
811 811 )
812 812
813 813 if options.bisect_repo and not options.known_good_rev:
814 814 parser.error("--bisect-repo cannot be used without --known-good-rev")
815 815
816 816 global useipv6
817 817 if options.ipv6:
818 818 useipv6 = checksocketfamily('AF_INET6')
819 819 else:
820 820 # only use IPv6 if IPv4 is unavailable and IPv6 is available
821 821 useipv6 = (not checksocketfamily('AF_INET')) and checksocketfamily(
822 822 'AF_INET6'
823 823 )
824 824
825 825 options.anycoverage = options.cover or options.annotate or options.htmlcov
826 826 if options.anycoverage:
827 827 try:
828 828 import coverage
829 829
830 830 covver = version.StrictVersion(coverage.__version__).version
831 831 if covver < (3, 3):
832 832 parser.error('coverage options require coverage 3.3 or later')
833 833 except ImportError:
834 834 parser.error('coverage options now require the coverage package')
835 835
836 836 if options.anycoverage and options.local:
837 837 # this needs some path mangling somewhere, I guess
838 838 parser.error(
839 839 "sorry, coverage options do not work when --local " "is specified"
840 840 )
841 841
842 842 if options.anycoverage and options.with_hg:
843 843 parser.error(
844 844 "sorry, coverage options do not work when --with-hg " "is specified"
845 845 )
846 846
847 847 global verbose
848 848 if options.verbose:
849 849 verbose = ''
850 850
851 851 if options.tmpdir:
852 852 options.tmpdir = canonpath(options.tmpdir)
853 853
854 854 if options.jobs < 1:
855 855 parser.error('--jobs must be positive')
856 856 if options.interactive and options.debug:
857 857 parser.error("-i/--interactive and -d/--debug are incompatible")
858 858 if options.debug:
859 859 if options.timeout != defaults['timeout']:
860 860 sys.stderr.write('warning: --timeout option ignored with --debug\n')
861 861 if options.slowtimeout != defaults['slowtimeout']:
862 862 sys.stderr.write(
863 863 'warning: --slowtimeout option ignored with --debug\n'
864 864 )
865 865 options.timeout = 0
866 866 options.slowtimeout = 0
867 867
868 868 if options.blacklist:
869 869 options.blacklist = parselistfiles(options.blacklist, 'blacklist')
870 870 if options.whitelist:
871 871 options.whitelisted = parselistfiles(options.whitelist, 'whitelist')
872 872 else:
873 873 options.whitelisted = {}
874 874
875 875 if options.showchannels:
876 876 options.nodiff = True
877 877
878 878 return options
879 879
880 880
881 881 def rename(src, dst):
882 882 """Like os.rename(), trade atomicity and opened files friendliness
883 883 for existing destination support.
884 884 """
885 885 shutil.copy(src, dst)
886 886 os.remove(src)
887 887
888 888
889 889 def makecleanable(path):
890 890 """Try to fix directory permission recursively so that the entire tree
891 891 can be deleted"""
892 892 for dirpath, dirnames, _filenames in os.walk(path, topdown=True):
893 893 for d in dirnames:
894 894 p = os.path.join(dirpath, d)
895 895 try:
896 896 os.chmod(p, os.stat(p).st_mode & 0o777 | 0o700) # chmod u+rwx
897 897 except OSError:
898 898 pass
899 899
900 900
901 901 _unified_diff = difflib.unified_diff
902 902 if PYTHON3:
903 903 import functools
904 904
905 905 _unified_diff = functools.partial(difflib.diff_bytes, difflib.unified_diff)
906 906
907 907
908 908 def getdiff(expected, output, ref, err):
909 909 servefail = False
910 910 lines = []
911 911 for line in _unified_diff(expected, output, ref, err):
912 912 if line.startswith(b'+++') or line.startswith(b'---'):
913 913 line = line.replace(b'\\', b'/')
914 914 if line.endswith(b' \n'):
915 915 line = line[:-2] + b'\n'
916 916 lines.append(line)
917 917 if not servefail and line.startswith(
918 918 b'+ abort: child process failed to start'
919 919 ):
920 920 servefail = True
921 921
922 922 return servefail, lines
923 923
924 924
925 925 verbose = False
926 926
927 927
928 928 def vlog(*msg):
929 929 """Log only when in verbose mode."""
930 930 if verbose is False:
931 931 return
932 932
933 933 return log(*msg)
934 934
935 935
936 936 # Bytes that break XML even in a CDATA block: control characters 0-31
937 937 # sans \t, \n and \r
938 938 CDATA_EVIL = re.compile(br"[\000-\010\013\014\016-\037]")
939 939
940 940 # Match feature conditionalized output lines in the form, capturing the feature
941 941 # list in group 2, and the preceeding line output in group 1:
942 942 #
943 943 # output..output (feature !)\n
944 944 optline = re.compile(br'(.*) \((.+?) !\)\n$')
945 945
946 946
947 947 def cdatasafe(data):
948 948 """Make a string safe to include in a CDATA block.
949 949
950 950 Certain control characters are illegal in a CDATA block, and
951 951 there's no way to include a ]]> in a CDATA either. This function
952 952 replaces illegal bytes with ? and adds a space between the ]] so
953 953 that it won't break the CDATA block.
954 954 """
955 955 return CDATA_EVIL.sub(b'?', data).replace(b']]>', b'] ]>')
956 956
957 957
958 958 def log(*msg):
959 959 """Log something to stdout.
960 960
961 961 Arguments are strings to print.
962 962 """
963 963 with iolock:
964 964 if verbose:
965 965 print(verbose, end=' ')
966 966 for m in msg:
967 967 print(m, end=' ')
968 968 print()
969 969 sys.stdout.flush()
970 970
971 971
972 972 def highlightdiff(line, color):
973 973 if not color:
974 974 return line
975 975 assert pygmentspresent
976 976 return pygments.highlight(
977 977 line.decode('latin1'), difflexer, terminal256formatter
978 978 ).encode('latin1')
979 979
980 980
981 981 def highlightmsg(msg, color):
982 982 if not color:
983 983 return msg
984 984 assert pygmentspresent
985 985 return pygments.highlight(msg, runnerlexer, runnerformatter)
986 986
987 987
988 988 def terminate(proc):
989 989 """Terminate subprocess"""
990 990 vlog('# Terminating process %d' % proc.pid)
991 991 try:
992 992 proc.terminate()
993 993 except OSError:
994 994 pass
995 995
996 996
997 997 def killdaemons(pidfile):
998 998 import killdaemons as killmod
999 999
1000 1000 return killmod.killdaemons(pidfile, tryhard=False, remove=True, logfn=vlog)
1001 1001
1002 1002
1003 1003 class Test(unittest.TestCase):
1004 1004 """Encapsulates a single, runnable test.
1005 1005
1006 1006 While this class conforms to the unittest.TestCase API, it differs in that
1007 1007 instances need to be instantiated manually. (Typically, unittest.TestCase
1008 1008 classes are instantiated automatically by scanning modules.)
1009 1009 """
1010 1010
1011 1011 # Status code reserved for skipped tests (used by hghave).
1012 1012 SKIPPED_STATUS = 80
1013 1013
1014 1014 def __init__(
1015 1015 self,
1016 1016 path,
1017 1017 outputdir,
1018 1018 tmpdir,
1019 1019 keeptmpdir=False,
1020 1020 debug=False,
1021 1021 first=False,
1022 1022 timeout=None,
1023 1023 startport=None,
1024 1024 extraconfigopts=None,
1025 1025 shell=None,
1026 1026 hgcommand=None,
1027 1027 slowtimeout=None,
1028 1028 usechg=False,
1029 1029 chgdebug=False,
1030 1030 useipv6=False,
1031 1031 ):
1032 1032 """Create a test from parameters.
1033 1033
1034 1034 path is the full path to the file defining the test.
1035 1035
1036 1036 tmpdir is the main temporary directory to use for this test.
1037 1037
1038 1038 keeptmpdir determines whether to keep the test's temporary directory
1039 1039 after execution. It defaults to removal (False).
1040 1040
1041 1041 debug mode will make the test execute verbosely, with unfiltered
1042 1042 output.
1043 1043
1044 1044 timeout controls the maximum run time of the test. It is ignored when
1045 1045 debug is True. See slowtimeout for tests with #require slow.
1046 1046
1047 1047 slowtimeout overrides timeout if the test has #require slow.
1048 1048
1049 1049 startport controls the starting port number to use for this test. Each
1050 1050 test will reserve 3 port numbers for execution. It is the caller's
1051 1051 responsibility to allocate a non-overlapping port range to Test
1052 1052 instances.
1053 1053
1054 1054 extraconfigopts is an iterable of extra hgrc config options. Values
1055 1055 must have the form "key=value" (something understood by hgrc). Values
1056 1056 of the form "foo.key=value" will result in "[foo] key=value".
1057 1057
1058 1058 shell is the shell to execute tests in.
1059 1059 """
1060 1060 if timeout is None:
1061 1061 timeout = defaults['timeout']
1062 1062 if startport is None:
1063 1063 startport = defaults['port']
1064 1064 if slowtimeout is None:
1065 1065 slowtimeout = defaults['slowtimeout']
1066 1066 self.path = path
1067 1067 self.relpath = os.path.relpath(path)
1068 1068 self.bname = os.path.basename(path)
1069 1069 self.name = _bytes2sys(self.bname)
1070 1070 self._testdir = os.path.dirname(path)
1071 1071 self._outputdir = outputdir
1072 1072 self._tmpname = os.path.basename(path)
1073 1073 self.errpath = os.path.join(self._outputdir, b'%s.err' % self.bname)
1074 1074
1075 1075 self._threadtmp = tmpdir
1076 1076 self._keeptmpdir = keeptmpdir
1077 1077 self._debug = debug
1078 1078 self._first = first
1079 1079 self._timeout = timeout
1080 1080 self._slowtimeout = slowtimeout
1081 1081 self._startport = startport
1082 1082 self._extraconfigopts = extraconfigopts or []
1083 1083 self._shell = _sys2bytes(shell)
1084 1084 self._hgcommand = hgcommand or b'hg'
1085 1085 self._usechg = usechg
1086 1086 self._chgdebug = chgdebug
1087 1087 self._useipv6 = useipv6
1088 1088
1089 1089 self._aborted = False
1090 1090 self._daemonpids = []
1091 1091 self._finished = None
1092 1092 self._ret = None
1093 1093 self._out = None
1094 1094 self._skipped = None
1095 1095 self._testtmp = None
1096 1096 self._chgsockdir = None
1097 1097
1098 1098 self._refout = self.readrefout()
1099 1099
1100 1100 def readrefout(self):
1101 1101 """read reference output"""
1102 1102 # If we're not in --debug mode and reference output file exists,
1103 1103 # check test output against it.
1104 1104 if self._debug:
1105 1105 return None # to match "out is None"
1106 1106 elif os.path.exists(self.refpath):
1107 1107 with open(self.refpath, 'rb') as f:
1108 1108 return f.read().splitlines(True)
1109 1109 else:
1110 1110 return []
1111 1111
1112 1112 # needed to get base class __repr__ running
1113 1113 @property
1114 1114 def _testMethodName(self):
1115 1115 return self.name
1116 1116
1117 1117 def __str__(self):
1118 1118 return self.name
1119 1119
1120 1120 def shortDescription(self):
1121 1121 return self.name
1122 1122
1123 1123 def setUp(self):
1124 1124 """Tasks to perform before run()."""
1125 1125 self._finished = False
1126 1126 self._ret = None
1127 1127 self._out = None
1128 1128 self._skipped = None
1129 1129
1130 1130 try:
1131 1131 os.mkdir(self._threadtmp)
1132 1132 except OSError as e:
1133 1133 if e.errno != errno.EEXIST:
1134 1134 raise
1135 1135
1136 1136 name = self._tmpname
1137 1137 self._testtmp = os.path.join(self._threadtmp, name)
1138 1138 os.mkdir(self._testtmp)
1139 1139
1140 1140 # Remove any previous output files.
1141 1141 if os.path.exists(self.errpath):
1142 1142 try:
1143 1143 os.remove(self.errpath)
1144 1144 except OSError as e:
1145 1145 # We might have raced another test to clean up a .err
1146 1146 # file, so ignore ENOENT when removing a previous .err
1147 1147 # file.
1148 1148 if e.errno != errno.ENOENT:
1149 1149 raise
1150 1150
1151 1151 if self._usechg:
1152 1152 self._chgsockdir = os.path.join(
1153 1153 self._threadtmp, b'%s.chgsock' % name
1154 1154 )
1155 1155 os.mkdir(self._chgsockdir)
1156 1156
1157 1157 def run(self, result):
1158 1158 """Run this test and report results against a TestResult instance."""
1159 1159 # This function is extremely similar to unittest.TestCase.run(). Once
1160 1160 # we require Python 2.7 (or at least its version of unittest), this
1161 1161 # function can largely go away.
1162 1162 self._result = result
1163 1163 result.startTest(self)
1164 1164 try:
1165 1165 try:
1166 1166 self.setUp()
1167 1167 except (KeyboardInterrupt, SystemExit):
1168 1168 self._aborted = True
1169 1169 raise
1170 1170 except Exception:
1171 1171 result.addError(self, sys.exc_info())
1172 1172 return
1173 1173
1174 1174 success = False
1175 1175 try:
1176 1176 self.runTest()
1177 1177 except KeyboardInterrupt:
1178 1178 self._aborted = True
1179 1179 raise
1180 1180 except unittest.SkipTest as e:
1181 1181 result.addSkip(self, str(e))
1182 1182 # The base class will have already counted this as a
1183 1183 # test we "ran", but we want to exclude skipped tests
1184 1184 # from those we count towards those run.
1185 1185 result.testsRun -= 1
1186 1186 except self.failureException as e:
1187 1187 # This differs from unittest in that we don't capture
1188 1188 # the stack trace. This is for historical reasons and
1189 1189 # this decision could be revisited in the future,
1190 1190 # especially for PythonTest instances.
1191 1191 if result.addFailure(self, str(e)):
1192 1192 success = True
1193 1193 except Exception:
1194 1194 result.addError(self, sys.exc_info())
1195 1195 else:
1196 1196 success = True
1197 1197
1198 1198 try:
1199 1199 self.tearDown()
1200 1200 except (KeyboardInterrupt, SystemExit):
1201 1201 self._aborted = True
1202 1202 raise
1203 1203 except Exception:
1204 1204 result.addError(self, sys.exc_info())
1205 1205 success = False
1206 1206
1207 1207 if success:
1208 1208 result.addSuccess(self)
1209 1209 finally:
1210 1210 result.stopTest(self, interrupted=self._aborted)
1211 1211
1212 1212 def runTest(self):
1213 1213 """Run this test instance.
1214 1214
1215 1215 This will return a tuple describing the result of the test.
1216 1216 """
1217 1217 env = self._getenv()
1218 1218 self._genrestoreenv(env)
1219 1219 self._daemonpids.append(env['DAEMON_PIDS'])
1220 1220 self._createhgrc(env['HGRCPATH'])
1221 1221
1222 1222 vlog('# Test', self.name)
1223 1223
1224 1224 ret, out = self._run(env)
1225 1225 self._finished = True
1226 1226 self._ret = ret
1227 1227 self._out = out
1228 1228
1229 1229 def describe(ret):
1230 1230 if ret < 0:
1231 1231 return 'killed by signal: %d' % -ret
1232 1232 return 'returned error code %d' % ret
1233 1233
1234 1234 self._skipped = False
1235 1235
1236 1236 if ret == self.SKIPPED_STATUS:
1237 1237 if out is None: # Debug mode, nothing to parse.
1238 1238 missing = ['unknown']
1239 1239 failed = None
1240 1240 else:
1241 1241 missing, failed = TTest.parsehghaveoutput(out)
1242 1242
1243 1243 if not missing:
1244 1244 missing = ['skipped']
1245 1245
1246 1246 if failed:
1247 1247 self.fail('hg have failed checking for %s' % failed[-1])
1248 1248 else:
1249 1249 self._skipped = True
1250 1250 raise unittest.SkipTest(missing[-1])
1251 1251 elif ret == 'timeout':
1252 1252 self.fail('timed out')
1253 1253 elif ret is False:
1254 1254 self.fail('no result code from test')
1255 1255 elif out != self._refout:
1256 1256 # Diff generation may rely on written .err file.
1257 1257 if (
1258 1258 (ret != 0 or out != self._refout)
1259 1259 and not self._skipped
1260 1260 and not self._debug
1261 1261 ):
1262 1262 with open(self.errpath, 'wb') as f:
1263 1263 for line in out:
1264 1264 f.write(line)
1265 1265
1266 1266 # The result object handles diff calculation for us.
1267 1267 with firstlock:
1268 1268 if self._result.addOutputMismatch(self, ret, out, self._refout):
1269 1269 # change was accepted, skip failing
1270 1270 return
1271 1271 if self._first:
1272 1272 global firsterror
1273 1273 firsterror = True
1274 1274
1275 1275 if ret:
1276 1276 msg = 'output changed and ' + describe(ret)
1277 1277 else:
1278 1278 msg = 'output changed'
1279 1279
1280 1280 self.fail(msg)
1281 1281 elif ret:
1282 1282 self.fail(describe(ret))
1283 1283
1284 1284 def tearDown(self):
1285 1285 """Tasks to perform after run()."""
1286 1286 for entry in self._daemonpids:
1287 1287 killdaemons(entry)
1288 1288 self._daemonpids = []
1289 1289
1290 1290 if self._keeptmpdir:
1291 1291 log(
1292 1292 '\nKeeping testtmp dir: %s\nKeeping threadtmp dir: %s'
1293 1293 % (
1294 1294 _bytes2sys(self._testtmp),
1295 1295 _bytes2sys(self._threadtmp),
1296 1296 )
1297 1297 )
1298 1298 else:
1299 1299 try:
1300 1300 shutil.rmtree(self._testtmp)
1301 1301 except OSError:
1302 1302 # unreadable directory may be left in $TESTTMP; fix permission
1303 1303 # and try again
1304 1304 makecleanable(self._testtmp)
1305 1305 shutil.rmtree(self._testtmp, True)
1306 1306 shutil.rmtree(self._threadtmp, True)
1307 1307
1308 1308 if self._usechg:
1309 1309 # chgservers will stop automatically after they find the socket
1310 1310 # files are deleted
1311 1311 shutil.rmtree(self._chgsockdir, True)
1312 1312
1313 1313 if (
1314 1314 (self._ret != 0 or self._out != self._refout)
1315 1315 and not self._skipped
1316 1316 and not self._debug
1317 1317 and self._out
1318 1318 ):
1319 1319 with open(self.errpath, 'wb') as f:
1320 1320 for line in self._out:
1321 1321 f.write(line)
1322 1322
1323 1323 vlog("# Ret was:", self._ret, '(%s)' % self.name)
1324 1324
1325 1325 def _run(self, env):
1326 1326 # This should be implemented in child classes to run tests.
1327 1327 raise unittest.SkipTest('unknown test type')
1328 1328
1329 1329 def abort(self):
1330 1330 """Terminate execution of this test."""
1331 1331 self._aborted = True
1332 1332
1333 1333 def _portmap(self, i):
1334 1334 offset = b'' if i == 0 else b'%d' % i
1335 1335 return (br':%d\b' % (self._startport + i), b':$HGPORT%s' % offset)
1336 1336
1337 1337 def _getreplacements(self):
1338 1338 """Obtain a mapping of text replacements to apply to test output.
1339 1339
1340 1340 Test output needs to be normalized so it can be compared to expected
1341 1341 output. This function defines how some of that normalization will
1342 1342 occur.
1343 1343 """
1344 1344 r = [
1345 1345 # This list should be parallel to defineport in _getenv
1346 1346 self._portmap(0),
1347 1347 self._portmap(1),
1348 1348 self._portmap(2),
1349 1349 (br'([^0-9])%s' % re.escape(self._localip()), br'\1$LOCALIP'),
1350 1350 (br'\bHG_TXNID=TXN:[a-f0-9]{40}\b', br'HG_TXNID=TXN:$ID$'),
1351 1351 ]
1352 1352 r.append((self._escapepath(self._testtmp), b'$TESTTMP'))
1353 1353 if WINDOWS:
1354 1354 # JSON output escapes backslashes in Windows paths, so also catch a
1355 1355 # double-escape.
1356 1356 replaced = self._testtmp.replace(b'\\', br'\\')
1357 1357 r.append((self._escapepath(replaced), b'$STR_REPR_TESTTMP'))
1358 1358
1359 1359 replacementfile = os.path.join(self._testdir, b'common-pattern.py')
1360 1360
1361 1361 if os.path.exists(replacementfile):
1362 1362 data = {}
1363 1363 with open(replacementfile, mode='rb') as source:
1364 1364 # the intermediate 'compile' step help with debugging
1365 1365 code = compile(source.read(), replacementfile, 'exec')
1366 1366 exec(code, data)
1367 1367 for value in data.get('substitutions', ()):
1368 1368 if len(value) != 2:
1369 1369 msg = 'malformatted substitution in %s: %r'
1370 1370 msg %= (replacementfile, value)
1371 1371 raise ValueError(msg)
1372 1372 r.append(value)
1373 1373 return r
1374 1374
1375 1375 def _escapepath(self, p):
1376 1376 if WINDOWS:
1377 1377 return b''.join(
1378 1378 c.isalpha()
1379 1379 and b'[%s%s]' % (c.lower(), c.upper())
1380 1380 or c in b'/\\'
1381 1381 and br'[/\\]'
1382 1382 or c.isdigit()
1383 1383 and c
1384 1384 or b'\\' + c
1385 1385 for c in [p[i : i + 1] for i in range(len(p))]
1386 1386 )
1387 1387 else:
1388 1388 return re.escape(p)
1389 1389
1390 1390 def _localip(self):
1391 1391 if self._useipv6:
1392 1392 return b'::1'
1393 1393 else:
1394 1394 return b'127.0.0.1'
1395 1395
1396 1396 def _genrestoreenv(self, testenv):
1397 1397 """Generate a script that can be used by tests to restore the original
1398 1398 environment."""
1399 1399 # Put the restoreenv script inside self._threadtmp
1400 1400 scriptpath = os.path.join(self._threadtmp, b'restoreenv.sh')
1401 1401 testenv['HGTEST_RESTOREENV'] = _bytes2sys(scriptpath)
1402 1402
1403 1403 # Only restore environment variable names that the shell allows
1404 1404 # us to export.
1405 1405 name_regex = re.compile('^[a-zA-Z][a-zA-Z0-9_]*$')
1406 1406
1407 1407 # Do not restore these variables; otherwise tests would fail.
1408 1408 reqnames = {'PYTHON', 'TESTDIR', 'TESTTMP'}
1409 1409
1410 1410 with open(scriptpath, 'w') as envf:
1411 1411 for name, value in origenviron.items():
1412 1412 if not name_regex.match(name):
1413 1413 # Skip environment variables with unusual names not
1414 1414 # allowed by most shells.
1415 1415 continue
1416 1416 if name in reqnames:
1417 1417 continue
1418 1418 envf.write('%s=%s\n' % (name, shellquote(value)))
1419 1419
1420 1420 for name in testenv:
1421 1421 if name in origenviron or name in reqnames:
1422 1422 continue
1423 1423 envf.write('unset %s\n' % (name,))
1424 1424
1425 1425 def _getenv(self):
1426 1426 """Obtain environment variables to use during test execution."""
1427 1427
1428 1428 def defineport(i):
1429 1429 offset = '' if i == 0 else '%s' % i
1430 1430 env["HGPORT%s" % offset] = '%s' % (self._startport + i)
1431 1431
1432 1432 env = os.environ.copy()
1433 1433 env['PYTHONUSERBASE'] = sysconfig.get_config_var('userbase') or ''
1434 1434 env['HGEMITWARNINGS'] = '1'
1435 1435 env['TESTTMP'] = _bytes2sys(self._testtmp)
1436 1436 uid_file = os.path.join(_bytes2sys(self._testtmp), 'UID')
1437 1437 env['HGTEST_UUIDFILE'] = uid_file
1438 1438 env['TESTNAME'] = self.name
1439 1439 env['HOME'] = _bytes2sys(self._testtmp)
1440 1440 if WINDOWS:
1441 1441 env['REALUSERPROFILE'] = env['USERPROFILE']
1442 1442 # py3.8+ ignores HOME: https://bugs.python.org/issue36264
1443 1443 env['USERPROFILE'] = env['HOME']
1444 1444 formated_timeout = _bytes2sys(b"%d" % default_defaults['timeout'][1])
1445 1445 env['HGTEST_TIMEOUT_DEFAULT'] = formated_timeout
1446 1446 env['HGTEST_TIMEOUT'] = _bytes2sys(b"%d" % self._timeout)
1447 1447 # This number should match portneeded in _getport
1448 1448 for port in xrange(3):
1449 1449 # This list should be parallel to _portmap in _getreplacements
1450 1450 defineport(port)
1451 1451 env["HGRCPATH"] = _bytes2sys(os.path.join(self._threadtmp, b'.hgrc'))
1452 1452 env["DAEMON_PIDS"] = _bytes2sys(
1453 1453 os.path.join(self._threadtmp, b'daemon.pids')
1454 1454 )
1455 1455 env["HGEDITOR"] = (
1456 1456 '"' + sysexecutable + '"' + ' -c "import sys; sys.exit(0)"'
1457 1457 )
1458 1458 env["HGUSER"] = "test"
1459 1459 env["HGENCODING"] = "ascii"
1460 1460 env["HGENCODINGMODE"] = "strict"
1461 1461 env["HGHOSTNAME"] = "test-hostname"
1462 1462 env['HGIPV6'] = str(int(self._useipv6))
1463 1463 # See contrib/catapipe.py for how to use this functionality.
1464 1464 if 'HGTESTCATAPULTSERVERPIPE' not in env:
1465 1465 # If we don't have HGTESTCATAPULTSERVERPIPE explicitly set, pull the
1466 1466 # non-test one in as a default, otherwise set to devnull
1467 1467 env['HGTESTCATAPULTSERVERPIPE'] = env.get(
1468 1468 'HGCATAPULTSERVERPIPE', os.devnull
1469 1469 )
1470 1470
1471 1471 extraextensions = []
1472 1472 for opt in self._extraconfigopts:
1473 1473 section, key = opt.split('.', 1)
1474 1474 if section != 'extensions':
1475 1475 continue
1476 1476 name = key.split('=', 1)[0]
1477 1477 extraextensions.append(name)
1478 1478
1479 1479 if extraextensions:
1480 1480 env['HGTESTEXTRAEXTENSIONS'] = ' '.join(extraextensions)
1481 1481
1482 1482 # LOCALIP could be ::1 or 127.0.0.1. Useful for tests that require raw
1483 1483 # IP addresses.
1484 1484 env['LOCALIP'] = _bytes2sys(self._localip())
1485 1485
1486 1486 # This has the same effect as Py_LegacyWindowsStdioFlag in exewrapper.c,
1487 1487 # but this is needed for testing python instances like dummyssh,
1488 1488 # dummysmtpd.py, and dumbhttp.py.
1489 1489 if PYTHON3 and WINDOWS:
1490 1490 env['PYTHONLEGACYWINDOWSSTDIO'] = '1'
1491 1491
1492 1492 # Modified HOME in test environment can confuse Rust tools. So set
1493 1493 # CARGO_HOME and RUSTUP_HOME automatically if a Rust toolchain is
1494 1494 # present and these variables aren't already defined.
1495 1495 cargo_home_path = os.path.expanduser('~/.cargo')
1496 1496 rustup_home_path = os.path.expanduser('~/.rustup')
1497 1497
1498 1498 if os.path.exists(cargo_home_path) and b'CARGO_HOME' not in osenvironb:
1499 1499 env['CARGO_HOME'] = cargo_home_path
1500 1500 if (
1501 1501 os.path.exists(rustup_home_path)
1502 1502 and b'RUSTUP_HOME' not in osenvironb
1503 1503 ):
1504 1504 env['RUSTUP_HOME'] = rustup_home_path
1505 1505
1506 1506 # Reset some environment variables to well-known values so that
1507 1507 # the tests produce repeatable output.
1508 1508 env['LANG'] = env['LC_ALL'] = env['LANGUAGE'] = 'C'
1509 1509 env['TZ'] = 'GMT'
1510 1510 env["EMAIL"] = "Foo Bar <foo.bar@example.com>"
1511 1511 env['COLUMNS'] = '80'
1512 1512 env['TERM'] = 'xterm'
1513 1513
1514 1514 dropped = [
1515 1515 'CDPATH',
1516 1516 'CHGDEBUG',
1517 1517 'EDITOR',
1518 1518 'GREP_OPTIONS',
1519 1519 'HG',
1520 1520 'HGMERGE',
1521 1521 'HGPLAIN',
1522 1522 'HGPLAINEXCEPT',
1523 1523 'HGPROF',
1524 1524 'http_proxy',
1525 1525 'no_proxy',
1526 1526 'NO_PROXY',
1527 1527 'PAGER',
1528 1528 'VISUAL',
1529 1529 ]
1530 1530
1531 1531 for k in dropped:
1532 1532 if k in env:
1533 1533 del env[k]
1534 1534
1535 1535 # unset env related to hooks
1536 1536 for k in list(env):
1537 1537 if k.startswith('HG_'):
1538 1538 del env[k]
1539 1539
1540 1540 if self._usechg:
1541 1541 env['CHGSOCKNAME'] = os.path.join(self._chgsockdir, b'server')
1542 1542 if self._chgdebug:
1543 1543 env['CHGDEBUG'] = 'true'
1544 1544
1545 1545 return env
1546 1546
1547 1547 def _createhgrc(self, path):
1548 1548 """Create an hgrc file for this test."""
1549 1549 with open(path, 'wb') as hgrc:
1550 1550 hgrc.write(b'[ui]\n')
1551 1551 hgrc.write(b'slash = True\n')
1552 1552 hgrc.write(b'interactive = False\n')
1553 1553 hgrc.write(b'detailed-exit-code = True\n')
1554 1554 hgrc.write(b'merge = internal:merge\n')
1555 1555 hgrc.write(b'mergemarkers = detailed\n')
1556 1556 hgrc.write(b'promptecho = True\n')
1557 dummyssh = os.path.join(self._testdir, b'dummyssh')
1558 hgrc.write(b'ssh = "%s" "%s"\n' % (PYTHON, dummyssh))
1557 1559 hgrc.write(b'timeout.warn=15\n')
1558 1560 hgrc.write(b'[chgserver]\n')
1559 1561 hgrc.write(b'idletimeout=60\n')
1560 1562 hgrc.write(b'[defaults]\n')
1561 1563 hgrc.write(b'[devel]\n')
1562 1564 hgrc.write(b'all-warnings = true\n')
1563 1565 hgrc.write(b'default-date = 0 0\n')
1564 1566 hgrc.write(b'[largefiles]\n')
1565 1567 hgrc.write(
1566 1568 b'usercache = %s\n'
1567 1569 % (os.path.join(self._testtmp, b'.cache/largefiles'))
1568 1570 )
1569 1571 hgrc.write(b'[lfs]\n')
1570 1572 hgrc.write(
1571 1573 b'usercache = %s\n'
1572 1574 % (os.path.join(self._testtmp, b'.cache/lfs'))
1573 1575 )
1574 1576 hgrc.write(b'[web]\n')
1575 1577 hgrc.write(b'address = localhost\n')
1576 1578 hgrc.write(b'ipv6 = %r\n' % self._useipv6)
1577 1579 hgrc.write(b'server-header = testing stub value\n')
1578 1580
1579 1581 for opt in self._extraconfigopts:
1580 1582 section, key = _sys2bytes(opt).split(b'.', 1)
1581 1583 assert b'=' in key, (
1582 1584 'extra config opt %s must ' 'have an = for assignment' % opt
1583 1585 )
1584 1586 hgrc.write(b'[%s]\n%s\n' % (section, key))
1585 1587
1586 1588 def fail(self, msg):
1587 1589 # unittest differentiates between errored and failed.
1588 1590 # Failed is denoted by AssertionError (by default at least).
1589 1591 raise AssertionError(msg)
1590 1592
1591 1593 def _runcommand(self, cmd, env, normalizenewlines=False):
1592 1594 """Run command in a sub-process, capturing the output (stdout and
1593 1595 stderr).
1594 1596
1595 1597 Return a tuple (exitcode, output). output is None in debug mode.
1596 1598 """
1597 1599 if self._debug:
1598 1600 proc = subprocess.Popen(
1599 1601 _bytes2sys(cmd),
1600 1602 shell=True,
1601 1603 close_fds=closefds,
1602 1604 cwd=_bytes2sys(self._testtmp),
1603 1605 env=env,
1604 1606 )
1605 1607 ret = proc.wait()
1606 1608 return (ret, None)
1607 1609
1608 1610 proc = Popen4(cmd, self._testtmp, self._timeout, env)
1609 1611
1610 1612 def cleanup():
1611 1613 terminate(proc)
1612 1614 ret = proc.wait()
1613 1615 if ret == 0:
1614 1616 ret = signal.SIGTERM << 8
1615 1617 killdaemons(env['DAEMON_PIDS'])
1616 1618 return ret
1617 1619
1618 1620 proc.tochild.close()
1619 1621
1620 1622 try:
1621 1623 output = proc.fromchild.read()
1622 1624 except KeyboardInterrupt:
1623 1625 vlog('# Handling keyboard interrupt')
1624 1626 cleanup()
1625 1627 raise
1626 1628
1627 1629 ret = proc.wait()
1628 1630 if wifexited(ret):
1629 1631 ret = os.WEXITSTATUS(ret)
1630 1632
1631 1633 if proc.timeout:
1632 1634 ret = 'timeout'
1633 1635
1634 1636 if ret:
1635 1637 killdaemons(env['DAEMON_PIDS'])
1636 1638
1637 1639 for s, r in self._getreplacements():
1638 1640 output = re.sub(s, r, output)
1639 1641
1640 1642 if normalizenewlines:
1641 1643 output = output.replace(b'\r\n', b'\n')
1642 1644
1643 1645 return ret, output.splitlines(True)
1644 1646
1645 1647
1646 1648 class PythonTest(Test):
1647 1649 """A Python-based test."""
1648 1650
1649 1651 @property
1650 1652 def refpath(self):
1651 1653 return os.path.join(self._testdir, b'%s.out' % self.bname)
1652 1654
1653 1655 def _run(self, env):
1654 1656 # Quote the python(3) executable for Windows
1655 1657 cmd = b'"%s" "%s"' % (PYTHON, self.path)
1656 1658 vlog("# Running", cmd.decode("utf-8"))
1657 1659 result = self._runcommand(cmd, env, normalizenewlines=WINDOWS)
1658 1660 if self._aborted:
1659 1661 raise KeyboardInterrupt()
1660 1662
1661 1663 return result
1662 1664
1663 1665
1664 1666 # Some glob patterns apply only in some circumstances, so the script
1665 1667 # might want to remove (glob) annotations that otherwise should be
1666 1668 # retained.
1667 1669 checkcodeglobpats = [
1668 1670 # On Windows it looks like \ doesn't require a (glob), but we know
1669 1671 # better.
1670 1672 re.compile(br'^pushing to \$TESTTMP/.*[^)]$'),
1671 1673 re.compile(br'^moving \S+/.*[^)]$'),
1672 1674 re.compile(br'^pulling from \$TESTTMP/.*[^)]$'),
1673 1675 # Not all platforms have 127.0.0.1 as loopback (though most do),
1674 1676 # so we always glob that too.
1675 1677 re.compile(br'.*\$LOCALIP.*$'),
1676 1678 ]
1677 1679
1678 1680 bchr = chr
1679 1681 if PYTHON3:
1680 1682 bchr = lambda x: bytes([x])
1681 1683
1682 1684 WARN_UNDEFINED = 1
1683 1685 WARN_YES = 2
1684 1686 WARN_NO = 3
1685 1687
1686 1688 MARK_OPTIONAL = b" (?)\n"
1687 1689
1688 1690
1689 1691 def isoptional(line):
1690 1692 return line.endswith(MARK_OPTIONAL)
1691 1693
1692 1694
1693 1695 class TTest(Test):
1694 1696 """A "t test" is a test backed by a .t file."""
1695 1697
1696 1698 SKIPPED_PREFIX = b'skipped: '
1697 1699 FAILED_PREFIX = b'hghave check failed: '
1698 1700 NEEDESCAPE = re.compile(br'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
1699 1701
1700 1702 ESCAPESUB = re.compile(br'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
1701 1703 ESCAPEMAP = {bchr(i): br'\x%02x' % i for i in range(256)}
1702 1704 ESCAPEMAP.update({b'\\': b'\\\\', b'\r': br'\r'})
1703 1705
1704 1706 def __init__(self, path, *args, **kwds):
1705 1707 # accept an extra "case" parameter
1706 1708 case = kwds.pop('case', [])
1707 1709 self._case = case
1708 1710 self._allcases = {x for y in parsettestcases(path) for x in y}
1709 1711 super(TTest, self).__init__(path, *args, **kwds)
1710 1712 if case:
1711 1713 casepath = b'#'.join(case)
1712 1714 self.name = '%s#%s' % (self.name, _bytes2sys(casepath))
1713 1715 self.errpath = b'%s#%s.err' % (self.errpath[:-4], casepath)
1714 1716 self._tmpname += b'-%s' % casepath.replace(b'#', b'-')
1715 1717 self._have = {}
1716 1718
1717 1719 @property
1718 1720 def refpath(self):
1719 1721 return os.path.join(self._testdir, self.bname)
1720 1722
1721 1723 def _run(self, env):
1722 1724 with open(self.path, 'rb') as f:
1723 1725 lines = f.readlines()
1724 1726
1725 1727 # .t file is both reference output and the test input, keep reference
1726 1728 # output updated with the the test input. This avoids some race
1727 1729 # conditions where the reference output does not match the actual test.
1728 1730 if self._refout is not None:
1729 1731 self._refout = lines
1730 1732
1731 1733 salt, script, after, expected = self._parsetest(lines)
1732 1734
1733 1735 # Write out the generated script.
1734 1736 fname = b'%s.sh' % self._testtmp
1735 1737 with open(fname, 'wb') as f:
1736 1738 for l in script:
1737 1739 f.write(l)
1738 1740
1739 1741 cmd = b'%s "%s"' % (self._shell, fname)
1740 1742 vlog("# Running", cmd.decode("utf-8"))
1741 1743
1742 1744 exitcode, output = self._runcommand(cmd, env)
1743 1745
1744 1746 if self._aborted:
1745 1747 raise KeyboardInterrupt()
1746 1748
1747 1749 # Do not merge output if skipped. Return hghave message instead.
1748 1750 # Similarly, with --debug, output is None.
1749 1751 if exitcode == self.SKIPPED_STATUS or output is None:
1750 1752 return exitcode, output
1751 1753
1752 1754 return self._processoutput(exitcode, output, salt, after, expected)
1753 1755
1754 1756 def _hghave(self, reqs):
1755 1757 allreqs = b' '.join(reqs)
1756 1758
1757 1759 self._detectslow(reqs)
1758 1760
1759 1761 if allreqs in self._have:
1760 1762 return self._have.get(allreqs)
1761 1763
1762 1764 # TODO do something smarter when all other uses of hghave are gone.
1763 1765 runtestdir = osenvironb[b'RUNTESTDIR']
1764 1766 tdir = runtestdir.replace(b'\\', b'/')
1765 1767 proc = Popen4(
1766 1768 b'%s -c "%s/hghave %s"' % (self._shell, tdir, allreqs),
1767 1769 self._testtmp,
1768 1770 0,
1769 1771 self._getenv(),
1770 1772 )
1771 1773 stdout, stderr = proc.communicate()
1772 1774 ret = proc.wait()
1773 1775 if wifexited(ret):
1774 1776 ret = os.WEXITSTATUS(ret)
1775 1777 if ret == 2:
1776 1778 print(stdout.decode('utf-8'))
1777 1779 sys.exit(1)
1778 1780
1779 1781 if ret != 0:
1780 1782 self._have[allreqs] = (False, stdout)
1781 1783 return False, stdout
1782 1784
1783 1785 self._have[allreqs] = (True, None)
1784 1786 return True, None
1785 1787
1786 1788 def _detectslow(self, reqs):
1787 1789 """update the timeout of slow test when appropriate"""
1788 1790 if b'slow' in reqs:
1789 1791 self._timeout = self._slowtimeout
1790 1792
1791 1793 def _iftest(self, args):
1792 1794 # implements "#if"
1793 1795 reqs = []
1794 1796 for arg in args:
1795 1797 if arg.startswith(b'no-') and arg[3:] in self._allcases:
1796 1798 if arg[3:] in self._case:
1797 1799 return False
1798 1800 elif arg in self._allcases:
1799 1801 if arg not in self._case:
1800 1802 return False
1801 1803 else:
1802 1804 reqs.append(arg)
1803 1805 self._detectslow(reqs)
1804 1806 return self._hghave(reqs)[0]
1805 1807
1806 1808 def _parsetest(self, lines):
1807 1809 # We generate a shell script which outputs unique markers to line
1808 1810 # up script results with our source. These markers include input
1809 1811 # line number and the last return code.
1810 1812 salt = b"SALT%d" % time.time()
1811 1813
1812 1814 def addsalt(line, inpython):
1813 1815 if inpython:
1814 1816 script.append(b'%s %d 0\n' % (salt, line))
1815 1817 else:
1816 1818 script.append(b'echo %s %d $?\n' % (salt, line))
1817 1819
1818 1820 activetrace = []
1819 1821 session = str(uuid.uuid4())
1820 1822 if PYTHON3:
1821 1823 session = session.encode('ascii')
1822 1824 hgcatapult = os.getenv('HGTESTCATAPULTSERVERPIPE') or os.getenv(
1823 1825 'HGCATAPULTSERVERPIPE'
1824 1826 )
1825 1827
1826 1828 def toggletrace(cmd=None):
1827 1829 if not hgcatapult or hgcatapult == os.devnull:
1828 1830 return
1829 1831
1830 1832 if activetrace:
1831 1833 script.append(
1832 1834 b'echo END %s %s >> "$HGTESTCATAPULTSERVERPIPE"\n'
1833 1835 % (session, activetrace[0])
1834 1836 )
1835 1837 if cmd is None:
1836 1838 return
1837 1839
1838 1840 if isinstance(cmd, str):
1839 1841 quoted = shellquote(cmd.strip())
1840 1842 else:
1841 1843 quoted = shellquote(cmd.strip().decode('utf8')).encode('utf8')
1842 1844 quoted = quoted.replace(b'\\', b'\\\\')
1843 1845 script.append(
1844 1846 b'echo START %s %s >> "$HGTESTCATAPULTSERVERPIPE"\n'
1845 1847 % (session, quoted)
1846 1848 )
1847 1849 activetrace[0:] = [quoted]
1848 1850
1849 1851 script = []
1850 1852
1851 1853 # After we run the shell script, we re-unify the script output
1852 1854 # with non-active parts of the source, with synchronization by our
1853 1855 # SALT line number markers. The after table contains the non-active
1854 1856 # components, ordered by line number.
1855 1857 after = {}
1856 1858
1857 1859 # Expected shell script output.
1858 1860 expected = {}
1859 1861
1860 1862 pos = prepos = -1
1861 1863
1862 1864 # True or False when in a true or false conditional section
1863 1865 skipping = None
1864 1866
1865 1867 # We keep track of whether or not we're in a Python block so we
1866 1868 # can generate the surrounding doctest magic.
1867 1869 inpython = False
1868 1870
1869 1871 if self._debug:
1870 1872 script.append(b'set -x\n')
1871 1873 if os.getenv('MSYSTEM'):
1872 1874 script.append(b'alias pwd="pwd -W"\n')
1873 1875
1874 1876 if hgcatapult and hgcatapult != os.devnull:
1875 1877 if PYTHON3:
1876 1878 hgcatapult = hgcatapult.encode('utf8')
1877 1879 cataname = self.name.encode('utf8')
1878 1880 else:
1879 1881 cataname = self.name
1880 1882
1881 1883 # Kludge: use a while loop to keep the pipe from getting
1882 1884 # closed by our echo commands. The still-running file gets
1883 1885 # reaped at the end of the script, which causes the while
1884 1886 # loop to exit and closes the pipe. Sigh.
1885 1887 script.append(
1886 1888 b'rtendtracing() {\n'
1887 1889 b' echo END %(session)s %(name)s >> %(catapult)s\n'
1888 1890 b' rm -f "$TESTTMP/.still-running"\n'
1889 1891 b'}\n'
1890 1892 b'trap "rtendtracing" 0\n'
1891 1893 b'touch "$TESTTMP/.still-running"\n'
1892 1894 b'while [ -f "$TESTTMP/.still-running" ]; do sleep 1; done '
1893 1895 b'> %(catapult)s &\n'
1894 1896 b'HGCATAPULTSESSION=%(session)s ; export HGCATAPULTSESSION\n'
1895 1897 b'echo START %(session)s %(name)s >> %(catapult)s\n'
1896 1898 % {
1897 1899 b'name': cataname,
1898 1900 b'session': session,
1899 1901 b'catapult': hgcatapult,
1900 1902 }
1901 1903 )
1902 1904
1903 1905 if self._case:
1904 1906 casestr = b'#'.join(self._case)
1905 1907 if isinstance(casestr, str):
1906 1908 quoted = shellquote(casestr)
1907 1909 else:
1908 1910 quoted = shellquote(casestr.decode('utf8')).encode('utf8')
1909 1911 script.append(b'TESTCASE=%s\n' % quoted)
1910 1912 script.append(b'export TESTCASE\n')
1911 1913
1912 1914 n = 0
1913 1915 for n, l in enumerate(lines):
1914 1916 if not l.endswith(b'\n'):
1915 1917 l += b'\n'
1916 1918 if l.startswith(b'#require'):
1917 1919 lsplit = l.split()
1918 1920 if len(lsplit) < 2 or lsplit[0] != b'#require':
1919 1921 after.setdefault(pos, []).append(
1920 1922 b' !!! invalid #require\n'
1921 1923 )
1922 1924 if not skipping:
1923 1925 haveresult, message = self._hghave(lsplit[1:])
1924 1926 if not haveresult:
1925 1927 script = [b'echo "%s"\nexit 80\n' % message]
1926 1928 break
1927 1929 after.setdefault(pos, []).append(l)
1928 1930 elif l.startswith(b'#if'):
1929 1931 lsplit = l.split()
1930 1932 if len(lsplit) < 2 or lsplit[0] != b'#if':
1931 1933 after.setdefault(pos, []).append(b' !!! invalid #if\n')
1932 1934 if skipping is not None:
1933 1935 after.setdefault(pos, []).append(b' !!! nested #if\n')
1934 1936 skipping = not self._iftest(lsplit[1:])
1935 1937 after.setdefault(pos, []).append(l)
1936 1938 elif l.startswith(b'#else'):
1937 1939 if skipping is None:
1938 1940 after.setdefault(pos, []).append(b' !!! missing #if\n')
1939 1941 skipping = not skipping
1940 1942 after.setdefault(pos, []).append(l)
1941 1943 elif l.startswith(b'#endif'):
1942 1944 if skipping is None:
1943 1945 after.setdefault(pos, []).append(b' !!! missing #if\n')
1944 1946 skipping = None
1945 1947 after.setdefault(pos, []).append(l)
1946 1948 elif skipping:
1947 1949 after.setdefault(pos, []).append(l)
1948 1950 elif l.startswith(b' >>> '): # python inlines
1949 1951 after.setdefault(pos, []).append(l)
1950 1952 prepos = pos
1951 1953 pos = n
1952 1954 if not inpython:
1953 1955 # We've just entered a Python block. Add the header.
1954 1956 inpython = True
1955 1957 addsalt(prepos, False) # Make sure we report the exit code.
1956 1958 script.append(b'"%s" -m heredoctest <<EOF\n' % PYTHON)
1957 1959 addsalt(n, True)
1958 1960 script.append(l[2:])
1959 1961 elif l.startswith(b' ... '): # python inlines
1960 1962 after.setdefault(prepos, []).append(l)
1961 1963 script.append(l[2:])
1962 1964 elif l.startswith(b' $ '): # commands
1963 1965 if inpython:
1964 1966 script.append(b'EOF\n')
1965 1967 inpython = False
1966 1968 after.setdefault(pos, []).append(l)
1967 1969 prepos = pos
1968 1970 pos = n
1969 1971 addsalt(n, False)
1970 1972 rawcmd = l[4:]
1971 1973 cmd = rawcmd.split()
1972 1974 toggletrace(rawcmd)
1973 1975 if len(cmd) == 2 and cmd[0] == b'cd':
1974 1976 rawcmd = b'cd %s || exit 1\n' % cmd[1]
1975 1977 script.append(rawcmd)
1976 1978 elif l.startswith(b' > '): # continuations
1977 1979 after.setdefault(prepos, []).append(l)
1978 1980 script.append(l[4:])
1979 1981 elif l.startswith(b' '): # results
1980 1982 # Queue up a list of expected results.
1981 1983 expected.setdefault(pos, []).append(l[2:])
1982 1984 else:
1983 1985 if inpython:
1984 1986 script.append(b'EOF\n')
1985 1987 inpython = False
1986 1988 # Non-command/result. Queue up for merged output.
1987 1989 after.setdefault(pos, []).append(l)
1988 1990
1989 1991 if inpython:
1990 1992 script.append(b'EOF\n')
1991 1993 if skipping is not None:
1992 1994 after.setdefault(pos, []).append(b' !!! missing #endif\n')
1993 1995 addsalt(n + 1, False)
1994 1996 # Need to end any current per-command trace
1995 1997 if activetrace:
1996 1998 toggletrace()
1997 1999 return salt, script, after, expected
1998 2000
1999 2001 def _processoutput(self, exitcode, output, salt, after, expected):
2000 2002 # Merge the script output back into a unified test.
2001 2003 warnonly = WARN_UNDEFINED # 1: not yet; 2: yes; 3: for sure not
2002 2004 if exitcode != 0:
2003 2005 warnonly = WARN_NO
2004 2006
2005 2007 pos = -1
2006 2008 postout = []
2007 2009 for out_rawline in output:
2008 2010 out_line, cmd_line = out_rawline, None
2009 2011 if salt in out_rawline:
2010 2012 out_line, cmd_line = out_rawline.split(salt, 1)
2011 2013
2012 2014 pos, postout, warnonly = self._process_out_line(
2013 2015 out_line, pos, postout, expected, warnonly
2014 2016 )
2015 2017 pos, postout = self._process_cmd_line(cmd_line, pos, postout, after)
2016 2018
2017 2019 if pos in after:
2018 2020 postout += after.pop(pos)
2019 2021
2020 2022 if warnonly == WARN_YES:
2021 2023 exitcode = False # Set exitcode to warned.
2022 2024
2023 2025 return exitcode, postout
2024 2026
2025 2027 def _process_out_line(self, out_line, pos, postout, expected, warnonly):
2026 2028 while out_line:
2027 2029 if not out_line.endswith(b'\n'):
2028 2030 out_line += b' (no-eol)\n'
2029 2031
2030 2032 # Find the expected output at the current position.
2031 2033 els = [None]
2032 2034 if expected.get(pos, None):
2033 2035 els = expected[pos]
2034 2036
2035 2037 optional = []
2036 2038 for i, el in enumerate(els):
2037 2039 r = False
2038 2040 if el:
2039 2041 r, exact = self.linematch(el, out_line)
2040 2042 if isinstance(r, str):
2041 2043 if r == '-glob':
2042 2044 out_line = ''.join(el.rsplit(' (glob)', 1))
2043 2045 r = '' # Warn only this line.
2044 2046 elif r == "retry":
2045 2047 postout.append(b' ' + el)
2046 2048 else:
2047 2049 log('\ninfo, unknown linematch result: %r\n' % r)
2048 2050 r = False
2049 2051 if r:
2050 2052 els.pop(i)
2051 2053 break
2052 2054 if el:
2053 2055 if isoptional(el):
2054 2056 optional.append(i)
2055 2057 else:
2056 2058 m = optline.match(el)
2057 2059 if m:
2058 2060 conditions = [c for c in m.group(2).split(b' ')]
2059 2061
2060 2062 if not self._iftest(conditions):
2061 2063 optional.append(i)
2062 2064 if exact:
2063 2065 # Don't allow line to be matches against a later
2064 2066 # line in the output
2065 2067 els.pop(i)
2066 2068 break
2067 2069
2068 2070 if r:
2069 2071 if r == "retry":
2070 2072 continue
2071 2073 # clean up any optional leftovers
2072 2074 for i in optional:
2073 2075 postout.append(b' ' + els[i])
2074 2076 for i in reversed(optional):
2075 2077 del els[i]
2076 2078 postout.append(b' ' + el)
2077 2079 else:
2078 2080 if self.NEEDESCAPE(out_line):
2079 2081 out_line = TTest._stringescape(
2080 2082 b'%s (esc)\n' % out_line.rstrip(b'\n')
2081 2083 )
2082 2084 postout.append(b' ' + out_line) # Let diff deal with it.
2083 2085 if r != '': # If line failed.
2084 2086 warnonly = WARN_NO
2085 2087 elif warnonly == WARN_UNDEFINED:
2086 2088 warnonly = WARN_YES
2087 2089 break
2088 2090 else:
2089 2091 # clean up any optional leftovers
2090 2092 while expected.get(pos, None):
2091 2093 el = expected[pos].pop(0)
2092 2094 if el:
2093 2095 if not isoptional(el):
2094 2096 m = optline.match(el)
2095 2097 if m:
2096 2098 conditions = [c for c in m.group(2).split(b' ')]
2097 2099
2098 2100 if self._iftest(conditions):
2099 2101 # Don't append as optional line
2100 2102 continue
2101 2103 else:
2102 2104 continue
2103 2105 postout.append(b' ' + el)
2104 2106 return pos, postout, warnonly
2105 2107
2106 2108 def _process_cmd_line(self, cmd_line, pos, postout, after):
2107 2109 """process a "command" part of a line from unified test output"""
2108 2110 if cmd_line:
2109 2111 # Add on last return code.
2110 2112 ret = int(cmd_line.split()[1])
2111 2113 if ret != 0:
2112 2114 postout.append(b' [%d]\n' % ret)
2113 2115 if pos in after:
2114 2116 # Merge in non-active test bits.
2115 2117 postout += after.pop(pos)
2116 2118 pos = int(cmd_line.split()[0])
2117 2119 return pos, postout
2118 2120
2119 2121 @staticmethod
2120 2122 def rematch(el, l):
2121 2123 try:
2122 2124 # parse any flags at the beginning of the regex. Only 'i' is
2123 2125 # supported right now, but this should be easy to extend.
2124 2126 flags, el = re.match(br'^(\(\?i\))?(.*)', el).groups()[0:2]
2125 2127 flags = flags or b''
2126 2128 el = flags + b'(?:' + el + b')'
2127 2129 # use \Z to ensure that the regex matches to the end of the string
2128 2130 if WINDOWS:
2129 2131 return re.match(el + br'\r?\n\Z', l)
2130 2132 return re.match(el + br'\n\Z', l)
2131 2133 except re.error:
2132 2134 # el is an invalid regex
2133 2135 return False
2134 2136
2135 2137 @staticmethod
2136 2138 def globmatch(el, l):
2137 2139 # The only supported special characters are * and ? plus / which also
2138 2140 # matches \ on windows. Escaping of these characters is supported.
2139 2141 if el + b'\n' == l:
2140 2142 if os.altsep:
2141 2143 # matching on "/" is not needed for this line
2142 2144 for pat in checkcodeglobpats:
2143 2145 if pat.match(el):
2144 2146 return True
2145 2147 return b'-glob'
2146 2148 return True
2147 2149 el = el.replace(b'$LOCALIP', b'*')
2148 2150 i, n = 0, len(el)
2149 2151 res = b''
2150 2152 while i < n:
2151 2153 c = el[i : i + 1]
2152 2154 i += 1
2153 2155 if c == b'\\' and i < n and el[i : i + 1] in b'*?\\/':
2154 2156 res += el[i - 1 : i + 1]
2155 2157 i += 1
2156 2158 elif c == b'*':
2157 2159 res += b'.*'
2158 2160 elif c == b'?':
2159 2161 res += b'.'
2160 2162 elif c == b'/' and os.altsep:
2161 2163 res += b'[/\\\\]'
2162 2164 else:
2163 2165 res += re.escape(c)
2164 2166 return TTest.rematch(res, l)
2165 2167
2166 2168 def linematch(self, el, l):
2167 2169 if el == l: # perfect match (fast)
2168 2170 return True, True
2169 2171 retry = False
2170 2172 if isoptional(el):
2171 2173 retry = "retry"
2172 2174 el = el[: -len(MARK_OPTIONAL)] + b"\n"
2173 2175 else:
2174 2176 m = optline.match(el)
2175 2177 if m:
2176 2178 conditions = [c for c in m.group(2).split(b' ')]
2177 2179
2178 2180 el = m.group(1) + b"\n"
2179 2181 if not self._iftest(conditions):
2180 2182 # listed feature missing, should not match
2181 2183 return "retry", False
2182 2184
2183 2185 if el.endswith(b" (esc)\n"):
2184 2186 if PYTHON3:
2185 2187 el = el[:-7].decode('unicode_escape') + '\n'
2186 2188 el = el.encode('latin-1')
2187 2189 else:
2188 2190 el = el[:-7].decode('string-escape') + '\n'
2189 2191 if el == l or WINDOWS and el[:-1] + b'\r\n' == l:
2190 2192 return True, True
2191 2193 if el.endswith(b" (re)\n"):
2192 2194 return (TTest.rematch(el[:-6], l) or retry), False
2193 2195 if el.endswith(b" (glob)\n"):
2194 2196 # ignore '(glob)' added to l by 'replacements'
2195 2197 if l.endswith(b" (glob)\n"):
2196 2198 l = l[:-8] + b"\n"
2197 2199 return (TTest.globmatch(el[:-8], l) or retry), False
2198 2200 if os.altsep:
2199 2201 _l = l.replace(b'\\', b'/')
2200 2202 if el == _l or WINDOWS and el[:-1] + b'\r\n' == _l:
2201 2203 return True, True
2202 2204 return retry, True
2203 2205
2204 2206 @staticmethod
2205 2207 def parsehghaveoutput(lines):
2206 2208 """Parse hghave log lines.
2207 2209
2208 2210 Return tuple of lists (missing, failed):
2209 2211 * the missing/unknown features
2210 2212 * the features for which existence check failed"""
2211 2213 missing = []
2212 2214 failed = []
2213 2215 for line in lines:
2214 2216 if line.startswith(TTest.SKIPPED_PREFIX):
2215 2217 line = line.splitlines()[0]
2216 2218 missing.append(_bytes2sys(line[len(TTest.SKIPPED_PREFIX) :]))
2217 2219 elif line.startswith(TTest.FAILED_PREFIX):
2218 2220 line = line.splitlines()[0]
2219 2221 failed.append(_bytes2sys(line[len(TTest.FAILED_PREFIX) :]))
2220 2222
2221 2223 return missing, failed
2222 2224
2223 2225 @staticmethod
2224 2226 def _escapef(m):
2225 2227 return TTest.ESCAPEMAP[m.group(0)]
2226 2228
2227 2229 @staticmethod
2228 2230 def _stringescape(s):
2229 2231 return TTest.ESCAPESUB(TTest._escapef, s)
2230 2232
2231 2233
2232 2234 iolock = threading.RLock()
2233 2235 firstlock = threading.RLock()
2234 2236 firsterror = False
2235 2237
2236 2238
2237 2239 class TestResult(unittest._TextTestResult):
2238 2240 """Holds results when executing via unittest."""
2239 2241
2240 2242 # Don't worry too much about accessing the non-public _TextTestResult.
2241 2243 # It is relatively common in Python testing tools.
2242 2244 def __init__(self, options, *args, **kwargs):
2243 2245 super(TestResult, self).__init__(*args, **kwargs)
2244 2246
2245 2247 self._options = options
2246 2248
2247 2249 # unittest.TestResult didn't have skipped until 2.7. We need to
2248 2250 # polyfill it.
2249 2251 self.skipped = []
2250 2252
2251 2253 # We have a custom "ignored" result that isn't present in any Python
2252 2254 # unittest implementation. It is very similar to skipped. It may make
2253 2255 # sense to map it into skip some day.
2254 2256 self.ignored = []
2255 2257
2256 2258 self.times = []
2257 2259 self._firststarttime = None
2258 2260 # Data stored for the benefit of generating xunit reports.
2259 2261 self.successes = []
2260 2262 self.faildata = {}
2261 2263
2262 2264 if options.color == 'auto':
2263 2265 isatty = self.stream.isatty()
2264 2266 # For some reason, redirecting stdout on Windows disables the ANSI
2265 2267 # color processing of stderr, which is what is used to print the
2266 2268 # output. Therefore, both must be tty on Windows to enable color.
2267 2269 if WINDOWS:
2268 2270 isatty = isatty and sys.stdout.isatty()
2269 2271 self.color = pygmentspresent and isatty
2270 2272 elif options.color == 'never':
2271 2273 self.color = False
2272 2274 else: # 'always', for testing purposes
2273 2275 self.color = pygmentspresent
2274 2276
2275 2277 def onStart(self, test):
2276 2278 """Can be overriden by custom TestResult"""
2277 2279
2278 2280 def onEnd(self):
2279 2281 """Can be overriden by custom TestResult"""
2280 2282
2281 2283 def addFailure(self, test, reason):
2282 2284 self.failures.append((test, reason))
2283 2285
2284 2286 if self._options.first:
2285 2287 self.stop()
2286 2288 else:
2287 2289 with iolock:
2288 2290 if reason == "timed out":
2289 2291 self.stream.write('t')
2290 2292 else:
2291 2293 if not self._options.nodiff:
2292 2294 self.stream.write('\n')
2293 2295 # Exclude the '\n' from highlighting to lex correctly
2294 2296 formatted = 'ERROR: %s output changed\n' % test
2295 2297 self.stream.write(highlightmsg(formatted, self.color))
2296 2298 self.stream.write('!')
2297 2299
2298 2300 self.stream.flush()
2299 2301
2300 2302 def addSuccess(self, test):
2301 2303 with iolock:
2302 2304 super(TestResult, self).addSuccess(test)
2303 2305 self.successes.append(test)
2304 2306
2305 2307 def addError(self, test, err):
2306 2308 super(TestResult, self).addError(test, err)
2307 2309 if self._options.first:
2308 2310 self.stop()
2309 2311
2310 2312 # Polyfill.
2311 2313 def addSkip(self, test, reason):
2312 2314 self.skipped.append((test, reason))
2313 2315 with iolock:
2314 2316 if self.showAll:
2315 2317 self.stream.writeln('skipped %s' % reason)
2316 2318 else:
2317 2319 self.stream.write('s')
2318 2320 self.stream.flush()
2319 2321
2320 2322 def addIgnore(self, test, reason):
2321 2323 self.ignored.append((test, reason))
2322 2324 with iolock:
2323 2325 if self.showAll:
2324 2326 self.stream.writeln('ignored %s' % reason)
2325 2327 else:
2326 2328 if reason not in ('not retesting', "doesn't match keyword"):
2327 2329 self.stream.write('i')
2328 2330 else:
2329 2331 self.testsRun += 1
2330 2332 self.stream.flush()
2331 2333
2332 2334 def addOutputMismatch(self, test, ret, got, expected):
2333 2335 """Record a mismatch in test output for a particular test."""
2334 2336 if self.shouldStop or firsterror:
2335 2337 # don't print, some other test case already failed and
2336 2338 # printed, we're just stale and probably failed due to our
2337 2339 # temp dir getting cleaned up.
2338 2340 return
2339 2341
2340 2342 accepted = False
2341 2343 lines = []
2342 2344
2343 2345 with iolock:
2344 2346 if self._options.nodiff:
2345 2347 pass
2346 2348 elif self._options.view:
2347 2349 v = self._options.view
2348 2350 subprocess.call(
2349 2351 r'"%s" "%s" "%s"'
2350 2352 % (v, _bytes2sys(test.refpath), _bytes2sys(test.errpath)),
2351 2353 shell=True,
2352 2354 )
2353 2355 else:
2354 2356 servefail, lines = getdiff(
2355 2357 expected, got, test.refpath, test.errpath
2356 2358 )
2357 2359 self.stream.write('\n')
2358 2360 for line in lines:
2359 2361 line = highlightdiff(line, self.color)
2360 2362 if PYTHON3:
2361 2363 self.stream.flush()
2362 2364 self.stream.buffer.write(line)
2363 2365 self.stream.buffer.flush()
2364 2366 else:
2365 2367 self.stream.write(line)
2366 2368 self.stream.flush()
2367 2369
2368 2370 if servefail:
2369 2371 raise test.failureException(
2370 2372 'server failed to start (HGPORT=%s)' % test._startport
2371 2373 )
2372 2374
2373 2375 # handle interactive prompt without releasing iolock
2374 2376 if self._options.interactive:
2375 2377 if test.readrefout() != expected:
2376 2378 self.stream.write(
2377 2379 'Reference output has changed (run again to prompt '
2378 2380 'changes)'
2379 2381 )
2380 2382 else:
2381 2383 self.stream.write('Accept this change? [y/N] ')
2382 2384 self.stream.flush()
2383 2385 answer = sys.stdin.readline().strip()
2384 2386 if answer.lower() in ('y', 'yes'):
2385 2387 if test.path.endswith(b'.t'):
2386 2388 rename(test.errpath, test.path)
2387 2389 else:
2388 2390 rename(test.errpath, b'%s.out' % test.path)
2389 2391 accepted = True
2390 2392 if not accepted:
2391 2393 self.faildata[test.name] = b''.join(lines)
2392 2394
2393 2395 return accepted
2394 2396
2395 2397 def startTest(self, test):
2396 2398 super(TestResult, self).startTest(test)
2397 2399
2398 2400 # os.times module computes the user time and system time spent by
2399 2401 # child's processes along with real elapsed time taken by a process.
2400 2402 # This module has one limitation. It can only work for Linux user
2401 2403 # and not for Windows. Hence why we fall back to another function
2402 2404 # for wall time calculations.
2403 2405 test.started_times = os.times()
2404 2406 # TODO use a monotonic clock once support for Python 2.7 is dropped.
2405 2407 test.started_time = time.time()
2406 2408 if self._firststarttime is None: # thread racy but irrelevant
2407 2409 self._firststarttime = test.started_time
2408 2410
2409 2411 def stopTest(self, test, interrupted=False):
2410 2412 super(TestResult, self).stopTest(test)
2411 2413
2412 2414 test.stopped_times = os.times()
2413 2415 stopped_time = time.time()
2414 2416
2415 2417 starttime = test.started_times
2416 2418 endtime = test.stopped_times
2417 2419 origin = self._firststarttime
2418 2420 self.times.append(
2419 2421 (
2420 2422 test.name,
2421 2423 endtime[2] - starttime[2], # user space CPU time
2422 2424 endtime[3] - starttime[3], # sys space CPU time
2423 2425 stopped_time - test.started_time, # real time
2424 2426 test.started_time - origin, # start date in run context
2425 2427 stopped_time - origin, # end date in run context
2426 2428 )
2427 2429 )
2428 2430
2429 2431 if interrupted:
2430 2432 with iolock:
2431 2433 self.stream.writeln(
2432 2434 'INTERRUPTED: %s (after %d seconds)'
2433 2435 % (test.name, self.times[-1][3])
2434 2436 )
2435 2437
2436 2438
2437 2439 def getTestResult():
2438 2440 """
2439 2441 Returns the relevant test result
2440 2442 """
2441 2443 if "CUSTOM_TEST_RESULT" in os.environ:
2442 2444 testresultmodule = __import__(os.environ["CUSTOM_TEST_RESULT"])
2443 2445 return testresultmodule.TestResult
2444 2446 else:
2445 2447 return TestResult
2446 2448
2447 2449
2448 2450 class TestSuite(unittest.TestSuite):
2449 2451 """Custom unittest TestSuite that knows how to execute Mercurial tests."""
2450 2452
2451 2453 def __init__(
2452 2454 self,
2453 2455 testdir,
2454 2456 jobs=1,
2455 2457 whitelist=None,
2456 2458 blacklist=None,
2457 2459 keywords=None,
2458 2460 loop=False,
2459 2461 runs_per_test=1,
2460 2462 loadtest=None,
2461 2463 showchannels=False,
2462 2464 *args,
2463 2465 **kwargs
2464 2466 ):
2465 2467 """Create a new instance that can run tests with a configuration.
2466 2468
2467 2469 testdir specifies the directory where tests are executed from. This
2468 2470 is typically the ``tests`` directory from Mercurial's source
2469 2471 repository.
2470 2472
2471 2473 jobs specifies the number of jobs to run concurrently. Each test
2472 2474 executes on its own thread. Tests actually spawn new processes, so
2473 2475 state mutation should not be an issue.
2474 2476
2475 2477 If there is only one job, it will use the main thread.
2476 2478
2477 2479 whitelist and blacklist denote tests that have been whitelisted and
2478 2480 blacklisted, respectively. These arguments don't belong in TestSuite.
2479 2481 Instead, whitelist and blacklist should be handled by the thing that
2480 2482 populates the TestSuite with tests. They are present to preserve
2481 2483 backwards compatible behavior which reports skipped tests as part
2482 2484 of the results.
2483 2485
2484 2486 keywords denotes key words that will be used to filter which tests
2485 2487 to execute. This arguably belongs outside of TestSuite.
2486 2488
2487 2489 loop denotes whether to loop over tests forever.
2488 2490 """
2489 2491 super(TestSuite, self).__init__(*args, **kwargs)
2490 2492
2491 2493 self._jobs = jobs
2492 2494 self._whitelist = whitelist
2493 2495 self._blacklist = blacklist
2494 2496 self._keywords = keywords
2495 2497 self._loop = loop
2496 2498 self._runs_per_test = runs_per_test
2497 2499 self._loadtest = loadtest
2498 2500 self._showchannels = showchannels
2499 2501
2500 2502 def run(self, result):
2501 2503 # We have a number of filters that need to be applied. We do this
2502 2504 # here instead of inside Test because it makes the running logic for
2503 2505 # Test simpler.
2504 2506 tests = []
2505 2507 num_tests = [0]
2506 2508 for test in self._tests:
2507 2509
2508 2510 def get():
2509 2511 num_tests[0] += 1
2510 2512 if getattr(test, 'should_reload', False):
2511 2513 return self._loadtest(test, num_tests[0])
2512 2514 return test
2513 2515
2514 2516 if not os.path.exists(test.path):
2515 2517 result.addSkip(test, "Doesn't exist")
2516 2518 continue
2517 2519
2518 2520 is_whitelisted = self._whitelist and (
2519 2521 test.relpath in self._whitelist or test.bname in self._whitelist
2520 2522 )
2521 2523 if not is_whitelisted:
2522 2524 is_blacklisted = self._blacklist and (
2523 2525 test.relpath in self._blacklist
2524 2526 or test.bname in self._blacklist
2525 2527 )
2526 2528 if is_blacklisted:
2527 2529 result.addSkip(test, 'blacklisted')
2528 2530 continue
2529 2531 if self._keywords:
2530 2532 with open(test.path, 'rb') as f:
2531 2533 t = f.read().lower() + test.bname.lower()
2532 2534 ignored = False
2533 2535 for k in self._keywords.lower().split():
2534 2536 if k not in t:
2535 2537 result.addIgnore(test, "doesn't match keyword")
2536 2538 ignored = True
2537 2539 break
2538 2540
2539 2541 if ignored:
2540 2542 continue
2541 2543 for _ in xrange(self._runs_per_test):
2542 2544 tests.append(get())
2543 2545
2544 2546 runtests = list(tests)
2545 2547 done = queue.Queue()
2546 2548 running = 0
2547 2549
2548 2550 channels = [""] * self._jobs
2549 2551
2550 2552 def job(test, result):
2551 2553 for n, v in enumerate(channels):
2552 2554 if not v:
2553 2555 channel = n
2554 2556 break
2555 2557 else:
2556 2558 raise ValueError('Could not find output channel')
2557 2559 channels[channel] = "=" + test.name[5:].split(".")[0]
2558 2560 try:
2559 2561 test(result)
2560 2562 done.put(None)
2561 2563 except KeyboardInterrupt:
2562 2564 pass
2563 2565 except: # re-raises
2564 2566 done.put(('!', test, 'run-test raised an error, see traceback'))
2565 2567 raise
2566 2568 finally:
2567 2569 try:
2568 2570 channels[channel] = ''
2569 2571 except IndexError:
2570 2572 pass
2571 2573
2572 2574 def stat():
2573 2575 count = 0
2574 2576 while channels:
2575 2577 d = '\n%03s ' % count
2576 2578 for n, v in enumerate(channels):
2577 2579 if v:
2578 2580 d += v[0]
2579 2581 channels[n] = v[1:] or '.'
2580 2582 else:
2581 2583 d += ' '
2582 2584 d += ' '
2583 2585 with iolock:
2584 2586 sys.stdout.write(d + ' ')
2585 2587 sys.stdout.flush()
2586 2588 for x in xrange(10):
2587 2589 if channels:
2588 2590 time.sleep(0.1)
2589 2591 count += 1
2590 2592
2591 2593 stoppedearly = False
2592 2594
2593 2595 if self._showchannels:
2594 2596 statthread = threading.Thread(target=stat, name="stat")
2595 2597 statthread.start()
2596 2598
2597 2599 try:
2598 2600 while tests or running:
2599 2601 if not done.empty() or running == self._jobs or not tests:
2600 2602 try:
2601 2603 done.get(True, 1)
2602 2604 running -= 1
2603 2605 if result and result.shouldStop:
2604 2606 stoppedearly = True
2605 2607 break
2606 2608 except queue.Empty:
2607 2609 continue
2608 2610 if tests and not running == self._jobs:
2609 2611 test = tests.pop(0)
2610 2612 if self._loop:
2611 2613 if getattr(test, 'should_reload', False):
2612 2614 num_tests[0] += 1
2613 2615 tests.append(self._loadtest(test, num_tests[0]))
2614 2616 else:
2615 2617 tests.append(test)
2616 2618 if self._jobs == 1:
2617 2619 job(test, result)
2618 2620 else:
2619 2621 t = threading.Thread(
2620 2622 target=job, name=test.name, args=(test, result)
2621 2623 )
2622 2624 t.start()
2623 2625 running += 1
2624 2626
2625 2627 # If we stop early we still need to wait on started tests to
2626 2628 # finish. Otherwise, there is a race between the test completing
2627 2629 # and the test's cleanup code running. This could result in the
2628 2630 # test reporting incorrect.
2629 2631 if stoppedearly:
2630 2632 while running:
2631 2633 try:
2632 2634 done.get(True, 1)
2633 2635 running -= 1
2634 2636 except queue.Empty:
2635 2637 continue
2636 2638 except KeyboardInterrupt:
2637 2639 for test in runtests:
2638 2640 test.abort()
2639 2641
2640 2642 channels = []
2641 2643
2642 2644 return result
2643 2645
2644 2646
2645 2647 # Save the most recent 5 wall-clock runtimes of each test to a
2646 2648 # human-readable text file named .testtimes. Tests are sorted
2647 2649 # alphabetically, while times for each test are listed from oldest to
2648 2650 # newest.
2649 2651
2650 2652
2651 2653 def loadtimes(outputdir):
2652 2654 times = []
2653 2655 try:
2654 2656 with open(os.path.join(outputdir, b'.testtimes')) as fp:
2655 2657 for line in fp:
2656 2658 m = re.match('(.*?) ([0-9. ]+)', line)
2657 2659 times.append(
2658 2660 (m.group(1), [float(t) for t in m.group(2).split()])
2659 2661 )
2660 2662 except IOError as err:
2661 2663 if err.errno != errno.ENOENT:
2662 2664 raise
2663 2665 return times
2664 2666
2665 2667
2666 2668 def savetimes(outputdir, result):
2667 2669 saved = dict(loadtimes(outputdir))
2668 2670 maxruns = 5
2669 2671 skipped = {str(t[0]) for t in result.skipped}
2670 2672 for tdata in result.times:
2671 2673 test, real = tdata[0], tdata[3]
2672 2674 if test not in skipped:
2673 2675 ts = saved.setdefault(test, [])
2674 2676 ts.append(real)
2675 2677 ts[:] = ts[-maxruns:]
2676 2678
2677 2679 fd, tmpname = tempfile.mkstemp(
2678 2680 prefix=b'.testtimes', dir=outputdir, text=True
2679 2681 )
2680 2682 with os.fdopen(fd, 'w') as fp:
2681 2683 for name, ts in sorted(saved.items()):
2682 2684 fp.write('%s %s\n' % (name, ' '.join(['%.3f' % (t,) for t in ts])))
2683 2685 timepath = os.path.join(outputdir, b'.testtimes')
2684 2686 try:
2685 2687 os.unlink(timepath)
2686 2688 except OSError:
2687 2689 pass
2688 2690 try:
2689 2691 os.rename(tmpname, timepath)
2690 2692 except OSError:
2691 2693 pass
2692 2694
2693 2695
2694 2696 class TextTestRunner(unittest.TextTestRunner):
2695 2697 """Custom unittest test runner that uses appropriate settings."""
2696 2698
2697 2699 def __init__(self, runner, *args, **kwargs):
2698 2700 super(TextTestRunner, self).__init__(*args, **kwargs)
2699 2701
2700 2702 self._runner = runner
2701 2703
2702 2704 self._result = getTestResult()(
2703 2705 self._runner.options, self.stream, self.descriptions, self.verbosity
2704 2706 )
2705 2707
2706 2708 def listtests(self, test):
2707 2709 test = sorted(test, key=lambda t: t.name)
2708 2710
2709 2711 self._result.onStart(test)
2710 2712
2711 2713 for t in test:
2712 2714 print(t.name)
2713 2715 self._result.addSuccess(t)
2714 2716
2715 2717 if self._runner.options.xunit:
2716 2718 with open(self._runner.options.xunit, "wb") as xuf:
2717 2719 self._writexunit(self._result, xuf)
2718 2720
2719 2721 if self._runner.options.json:
2720 2722 jsonpath = os.path.join(self._runner._outputdir, b'report.json')
2721 2723 with open(jsonpath, 'w') as fp:
2722 2724 self._writejson(self._result, fp)
2723 2725
2724 2726 return self._result
2725 2727
2726 2728 def run(self, test):
2727 2729 self._result.onStart(test)
2728 2730 test(self._result)
2729 2731
2730 2732 failed = len(self._result.failures)
2731 2733 skipped = len(self._result.skipped)
2732 2734 ignored = len(self._result.ignored)
2733 2735
2734 2736 with iolock:
2735 2737 self.stream.writeln('')
2736 2738
2737 2739 if not self._runner.options.noskips:
2738 2740 for test, msg in sorted(
2739 2741 self._result.skipped, key=lambda s: s[0].name
2740 2742 ):
2741 2743 formatted = 'Skipped %s: %s\n' % (test.name, msg)
2742 2744 msg = highlightmsg(formatted, self._result.color)
2743 2745 self.stream.write(msg)
2744 2746 for test, msg in sorted(
2745 2747 self._result.failures, key=lambda f: f[0].name
2746 2748 ):
2747 2749 formatted = 'Failed %s: %s\n' % (test.name, msg)
2748 2750 self.stream.write(highlightmsg(formatted, self._result.color))
2749 2751 for test, msg in sorted(
2750 2752 self._result.errors, key=lambda e: e[0].name
2751 2753 ):
2752 2754 self.stream.writeln('Errored %s: %s' % (test.name, msg))
2753 2755
2754 2756 if self._runner.options.xunit:
2755 2757 with open(self._runner.options.xunit, "wb") as xuf:
2756 2758 self._writexunit(self._result, xuf)
2757 2759
2758 2760 if self._runner.options.json:
2759 2761 jsonpath = os.path.join(self._runner._outputdir, b'report.json')
2760 2762 with open(jsonpath, 'w') as fp:
2761 2763 self._writejson(self._result, fp)
2762 2764
2763 2765 self._runner._checkhglib('Tested')
2764 2766
2765 2767 savetimes(self._runner._outputdir, self._result)
2766 2768
2767 2769 if failed and self._runner.options.known_good_rev:
2768 2770 self._bisecttests(t for t, m in self._result.failures)
2769 2771 self.stream.writeln(
2770 2772 '# Ran %d tests, %d skipped, %d failed.'
2771 2773 % (self._result.testsRun, skipped + ignored, failed)
2772 2774 )
2773 2775 if failed:
2774 2776 self.stream.writeln(
2775 2777 'python hash seed: %s' % os.environ['PYTHONHASHSEED']
2776 2778 )
2777 2779 if self._runner.options.time:
2778 2780 self.printtimes(self._result.times)
2779 2781
2780 2782 if self._runner.options.exceptions:
2781 2783 exceptions = aggregateexceptions(
2782 2784 os.path.join(self._runner._outputdir, b'exceptions')
2783 2785 )
2784 2786
2785 2787 self.stream.writeln('Exceptions Report:')
2786 2788 self.stream.writeln(
2787 2789 '%d total from %d frames'
2788 2790 % (exceptions['total'], len(exceptions['exceptioncounts']))
2789 2791 )
2790 2792 combined = exceptions['combined']
2791 2793 for key in sorted(combined, key=combined.get, reverse=True):
2792 2794 frame, line, exc = key
2793 2795 totalcount, testcount, leastcount, leasttest = combined[key]
2794 2796
2795 2797 self.stream.writeln(
2796 2798 '%d (%d tests)\t%s: %s (%s - %d total)'
2797 2799 % (
2798 2800 totalcount,
2799 2801 testcount,
2800 2802 frame,
2801 2803 exc,
2802 2804 leasttest,
2803 2805 leastcount,
2804 2806 )
2805 2807 )
2806 2808
2807 2809 self.stream.flush()
2808 2810
2809 2811 return self._result
2810 2812
2811 2813 def _bisecttests(self, tests):
2812 2814 bisectcmd = ['hg', 'bisect']
2813 2815 bisectrepo = self._runner.options.bisect_repo
2814 2816 if bisectrepo:
2815 2817 bisectcmd.extend(['-R', os.path.abspath(bisectrepo)])
2816 2818
2817 2819 def pread(args):
2818 2820 env = os.environ.copy()
2819 2821 env['HGPLAIN'] = '1'
2820 2822 p = subprocess.Popen(
2821 2823 args, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, env=env
2822 2824 )
2823 2825 data = p.stdout.read()
2824 2826 p.wait()
2825 2827 return data
2826 2828
2827 2829 for test in tests:
2828 2830 pread(bisectcmd + ['--reset']),
2829 2831 pread(bisectcmd + ['--bad', '.'])
2830 2832 pread(bisectcmd + ['--good', self._runner.options.known_good_rev])
2831 2833 # TODO: we probably need to forward more options
2832 2834 # that alter hg's behavior inside the tests.
2833 2835 opts = ''
2834 2836 withhg = self._runner.options.with_hg
2835 2837 if withhg:
2836 2838 opts += ' --with-hg=%s ' % shellquote(_bytes2sys(withhg))
2837 2839 rtc = '%s %s %s %s' % (sysexecutable, sys.argv[0], opts, test)
2838 2840 data = pread(bisectcmd + ['--command', rtc])
2839 2841 m = re.search(
2840 2842 (
2841 2843 br'\nThe first (?P<goodbad>bad|good) revision '
2842 2844 br'is:\nchangeset: +\d+:(?P<node>[a-f0-9]+)\n.*\n'
2843 2845 br'summary: +(?P<summary>[^\n]+)\n'
2844 2846 ),
2845 2847 data,
2846 2848 (re.MULTILINE | re.DOTALL),
2847 2849 )
2848 2850 if m is None:
2849 2851 self.stream.writeln(
2850 2852 'Failed to identify failure point for %s' % test
2851 2853 )
2852 2854 continue
2853 2855 dat = m.groupdict()
2854 2856 verb = 'broken' if dat['goodbad'] == b'bad' else 'fixed'
2855 2857 self.stream.writeln(
2856 2858 '%s %s by %s (%s)'
2857 2859 % (
2858 2860 test,
2859 2861 verb,
2860 2862 dat['node'].decode('ascii'),
2861 2863 dat['summary'].decode('utf8', 'ignore'),
2862 2864 )
2863 2865 )
2864 2866
2865 2867 def printtimes(self, times):
2866 2868 # iolock held by run
2867 2869 self.stream.writeln('# Producing time report')
2868 2870 times.sort(key=lambda t: (t[3]))
2869 2871 cols = '%7.3f %7.3f %7.3f %7.3f %7.3f %s'
2870 2872 self.stream.writeln(
2871 2873 '%-7s %-7s %-7s %-7s %-7s %s'
2872 2874 % ('start', 'end', 'cuser', 'csys', 'real', 'Test')
2873 2875 )
2874 2876 for tdata in times:
2875 2877 test = tdata[0]
2876 2878 cuser, csys, real, start, end = tdata[1:6]
2877 2879 self.stream.writeln(cols % (start, end, cuser, csys, real, test))
2878 2880
2879 2881 @staticmethod
2880 2882 def _writexunit(result, outf):
2881 2883 # See http://llg.cubic.org/docs/junit/ for a reference.
2882 2884 timesd = {t[0]: t[3] for t in result.times}
2883 2885 doc = minidom.Document()
2884 2886 s = doc.createElement('testsuite')
2885 2887 s.setAttribute('errors', "0") # TODO
2886 2888 s.setAttribute('failures', str(len(result.failures)))
2887 2889 s.setAttribute('name', 'run-tests')
2888 2890 s.setAttribute(
2889 2891 'skipped', str(len(result.skipped) + len(result.ignored))
2890 2892 )
2891 2893 s.setAttribute('tests', str(result.testsRun))
2892 2894 doc.appendChild(s)
2893 2895 for tc in result.successes:
2894 2896 t = doc.createElement('testcase')
2895 2897 t.setAttribute('name', tc.name)
2896 2898 tctime = timesd.get(tc.name)
2897 2899 if tctime is not None:
2898 2900 t.setAttribute('time', '%.3f' % tctime)
2899 2901 s.appendChild(t)
2900 2902 for tc, err in sorted(result.faildata.items()):
2901 2903 t = doc.createElement('testcase')
2902 2904 t.setAttribute('name', tc)
2903 2905 tctime = timesd.get(tc)
2904 2906 if tctime is not None:
2905 2907 t.setAttribute('time', '%.3f' % tctime)
2906 2908 # createCDATASection expects a unicode or it will
2907 2909 # convert using default conversion rules, which will
2908 2910 # fail if string isn't ASCII.
2909 2911 err = cdatasafe(err).decode('utf-8', 'replace')
2910 2912 cd = doc.createCDATASection(err)
2911 2913 # Use 'failure' here instead of 'error' to match errors = 0,
2912 2914 # failures = len(result.failures) in the testsuite element.
2913 2915 failelem = doc.createElement('failure')
2914 2916 failelem.setAttribute('message', 'output changed')
2915 2917 failelem.setAttribute('type', 'output-mismatch')
2916 2918 failelem.appendChild(cd)
2917 2919 t.appendChild(failelem)
2918 2920 s.appendChild(t)
2919 2921 for tc, message in result.skipped:
2920 2922 # According to the schema, 'skipped' has no attributes. So store
2921 2923 # the skip message as a text node instead.
2922 2924 t = doc.createElement('testcase')
2923 2925 t.setAttribute('name', tc.name)
2924 2926 binmessage = message.encode('utf-8')
2925 2927 message = cdatasafe(binmessage).decode('utf-8', 'replace')
2926 2928 cd = doc.createCDATASection(message)
2927 2929 skipelem = doc.createElement('skipped')
2928 2930 skipelem.appendChild(cd)
2929 2931 t.appendChild(skipelem)
2930 2932 s.appendChild(t)
2931 2933 outf.write(doc.toprettyxml(indent=' ', encoding='utf-8'))
2932 2934
2933 2935 @staticmethod
2934 2936 def _writejson(result, outf):
2935 2937 timesd = {}
2936 2938 for tdata in result.times:
2937 2939 test = tdata[0]
2938 2940 timesd[test] = tdata[1:]
2939 2941
2940 2942 outcome = {}
2941 2943 groups = [
2942 2944 ('success', ((tc, None) for tc in result.successes)),
2943 2945 ('failure', result.failures),
2944 2946 ('skip', result.skipped),
2945 2947 ]
2946 2948 for res, testcases in groups:
2947 2949 for tc, __ in testcases:
2948 2950 if tc.name in timesd:
2949 2951 diff = result.faildata.get(tc.name, b'')
2950 2952 try:
2951 2953 diff = diff.decode('unicode_escape')
2952 2954 except UnicodeDecodeError as e:
2953 2955 diff = '%r decoding diff, sorry' % e
2954 2956 tres = {
2955 2957 'result': res,
2956 2958 'time': ('%0.3f' % timesd[tc.name][2]),
2957 2959 'cuser': ('%0.3f' % timesd[tc.name][0]),
2958 2960 'csys': ('%0.3f' % timesd[tc.name][1]),
2959 2961 'start': ('%0.3f' % timesd[tc.name][3]),
2960 2962 'end': ('%0.3f' % timesd[tc.name][4]),
2961 2963 'diff': diff,
2962 2964 }
2963 2965 else:
2964 2966 # blacklisted test
2965 2967 tres = {'result': res}
2966 2968
2967 2969 outcome[tc.name] = tres
2968 2970 jsonout = json.dumps(
2969 2971 outcome, sort_keys=True, indent=4, separators=(',', ': ')
2970 2972 )
2971 2973 outf.writelines(("testreport =", jsonout))
2972 2974
2973 2975
2974 2976 def sorttests(testdescs, previoustimes, shuffle=False):
2975 2977 """Do an in-place sort of tests."""
2976 2978 if shuffle:
2977 2979 random.shuffle(testdescs)
2978 2980 return
2979 2981
2980 2982 if previoustimes:
2981 2983
2982 2984 def sortkey(f):
2983 2985 f = f['path']
2984 2986 if f in previoustimes:
2985 2987 # Use most recent time as estimate
2986 2988 return -(previoustimes[f][-1])
2987 2989 else:
2988 2990 # Default to a rather arbitrary value of 1 second for new tests
2989 2991 return -1.0
2990 2992
2991 2993 else:
2992 2994 # keywords for slow tests
2993 2995 slow = {
2994 2996 b'svn': 10,
2995 2997 b'cvs': 10,
2996 2998 b'hghave': 10,
2997 2999 b'largefiles-update': 10,
2998 3000 b'run-tests': 10,
2999 3001 b'corruption': 10,
3000 3002 b'race': 10,
3001 3003 b'i18n': 10,
3002 3004 b'check': 100,
3003 3005 b'gendoc': 100,
3004 3006 b'contrib-perf': 200,
3005 3007 b'merge-combination': 100,
3006 3008 }
3007 3009 perf = {}
3008 3010
3009 3011 def sortkey(f):
3010 3012 # run largest tests first, as they tend to take the longest
3011 3013 f = f['path']
3012 3014 try:
3013 3015 return perf[f]
3014 3016 except KeyError:
3015 3017 try:
3016 3018 val = -os.stat(f).st_size
3017 3019 except OSError as e:
3018 3020 if e.errno != errno.ENOENT:
3019 3021 raise
3020 3022 perf[f] = -1e9 # file does not exist, tell early
3021 3023 return -1e9
3022 3024 for kw, mul in slow.items():
3023 3025 if kw in f:
3024 3026 val *= mul
3025 3027 if f.endswith(b'.py'):
3026 3028 val /= 10.0
3027 3029 perf[f] = val / 1000.0
3028 3030 return perf[f]
3029 3031
3030 3032 testdescs.sort(key=sortkey)
3031 3033
3032 3034
3033 3035 class TestRunner(object):
3034 3036 """Holds context for executing tests.
3035 3037
3036 3038 Tests rely on a lot of state. This object holds it for them.
3037 3039 """
3038 3040
3039 3041 # Programs required to run tests.
3040 3042 REQUIREDTOOLS = [
3041 3043 b'diff',
3042 3044 b'grep',
3043 3045 b'unzip',
3044 3046 b'gunzip',
3045 3047 b'bunzip2',
3046 3048 b'sed',
3047 3049 ]
3048 3050
3049 3051 # Maps file extensions to test class.
3050 3052 TESTTYPES = [
3051 3053 (b'.py', PythonTest),
3052 3054 (b'.t', TTest),
3053 3055 ]
3054 3056
3055 3057 def __init__(self):
3056 3058 self.options = None
3057 3059 self._hgroot = None
3058 3060 self._testdir = None
3059 3061 self._outputdir = None
3060 3062 self._hgtmp = None
3061 3063 self._installdir = None
3062 3064 self._bindir = None
3063 3065 # a place for run-tests.py to generate executable it needs
3064 3066 self._custom_bin_dir = None
3065 3067 self._pythondir = None
3066 3068 # True if we had to infer the pythondir from --with-hg
3067 3069 self._pythondir_inferred = False
3068 3070 self._coveragefile = None
3069 3071 self._createdfiles = []
3070 3072 self._hgcommand = None
3071 3073 self._hgpath = None
3072 3074 self._portoffset = 0
3073 3075 self._ports = {}
3074 3076
3075 3077 def run(self, args, parser=None):
3076 3078 """Run the test suite."""
3077 3079 oldmask = os.umask(0o22)
3078 3080 try:
3079 3081 parser = parser or getparser()
3080 3082 options = parseargs(args, parser)
3081 3083 tests = [_sys2bytes(a) for a in options.tests]
3082 3084 if options.test_list is not None:
3083 3085 for listfile in options.test_list:
3084 3086 with open(listfile, 'rb') as f:
3085 3087 tests.extend(t for t in f.read().splitlines() if t)
3086 3088 self.options = options
3087 3089
3088 3090 self._checktools()
3089 3091 testdescs = self.findtests(tests)
3090 3092 if options.profile_runner:
3091 3093 import statprof
3092 3094
3093 3095 statprof.start()
3094 3096 result = self._run(testdescs)
3095 3097 if options.profile_runner:
3096 3098 statprof.stop()
3097 3099 statprof.display()
3098 3100 return result
3099 3101
3100 3102 finally:
3101 3103 os.umask(oldmask)
3102 3104
3103 3105 def _run(self, testdescs):
3104 3106 testdir = getcwdb()
3105 3107 # assume all tests in same folder for now
3106 3108 if testdescs:
3107 3109 pathname = os.path.dirname(testdescs[0]['path'])
3108 3110 if pathname:
3109 3111 testdir = os.path.join(testdir, pathname)
3110 3112 self._testdir = osenvironb[b'TESTDIR'] = testdir
3111 3113 if self.options.outputdir:
3112 3114 self._outputdir = canonpath(_sys2bytes(self.options.outputdir))
3113 3115 else:
3114 3116 self._outputdir = getcwdb()
3115 3117 if testdescs and pathname:
3116 3118 self._outputdir = os.path.join(self._outputdir, pathname)
3117 3119 previoustimes = {}
3118 3120 if self.options.order_by_runtime:
3119 3121 previoustimes = dict(loadtimes(self._outputdir))
3120 3122 sorttests(testdescs, previoustimes, shuffle=self.options.random)
3121 3123
3122 3124 if 'PYTHONHASHSEED' not in os.environ:
3123 3125 # use a random python hash seed all the time
3124 3126 # we do the randomness ourself to know what seed is used
3125 3127 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
3126 3128
3127 3129 # Rayon (Rust crate for multi-threading) will use all logical CPU cores
3128 3130 # by default, causing thrashing on high-cpu-count systems.
3129 3131 # Setting its limit to 3 during tests should still let us uncover
3130 3132 # multi-threading bugs while keeping the thrashing reasonable.
3131 3133 os.environ.setdefault("RAYON_NUM_THREADS", "3")
3132 3134
3133 3135 if self.options.tmpdir:
3134 3136 self.options.keep_tmpdir = True
3135 3137 tmpdir = _sys2bytes(self.options.tmpdir)
3136 3138 if os.path.exists(tmpdir):
3137 3139 # Meaning of tmpdir has changed since 1.3: we used to create
3138 3140 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
3139 3141 # tmpdir already exists.
3140 3142 print("error: temp dir %r already exists" % tmpdir)
3141 3143 return 1
3142 3144
3143 3145 os.makedirs(tmpdir)
3144 3146 else:
3145 3147 d = None
3146 3148 if WINDOWS:
3147 3149 # without this, we get the default temp dir location, but
3148 3150 # in all lowercase, which causes troubles with paths (issue3490)
3149 3151 d = osenvironb.get(b'TMP', None)
3150 3152 tmpdir = tempfile.mkdtemp(b'', b'hgtests.', d)
3151 3153
3152 3154 self._hgtmp = osenvironb[b'HGTMP'] = os.path.realpath(tmpdir)
3153 3155
3154 3156 self._custom_bin_dir = os.path.join(self._hgtmp, b'custom-bin')
3155 3157 os.makedirs(self._custom_bin_dir)
3156 3158
3157 3159 if self.options.with_hg:
3158 3160 self._installdir = None
3159 3161 whg = self.options.with_hg
3160 3162 self._bindir = os.path.dirname(os.path.realpath(whg))
3161 3163 assert isinstance(self._bindir, bytes)
3162 3164 self._hgcommand = os.path.basename(whg)
3163 3165
3164 3166 normbin = os.path.normpath(os.path.abspath(whg))
3165 3167 normbin = normbin.replace(_sys2bytes(os.sep), b'/')
3166 3168
3167 3169 # Other Python scripts in the test harness need to
3168 3170 # `import mercurial`. If `hg` is a Python script, we assume
3169 3171 # the Mercurial modules are relative to its path and tell the tests
3170 3172 # to load Python modules from its directory.
3171 3173 with open(whg, 'rb') as fh:
3172 3174 initial = fh.read(1024)
3173 3175
3174 3176 if re.match(b'#!.*python', initial):
3175 3177 self._pythondir = self._bindir
3176 3178 # If it looks like our in-repo Rust binary, use the source root.
3177 3179 # This is a bit hacky. But rhg is still not supported outside the
3178 3180 # source directory. So until it is, do the simple thing.
3179 3181 elif re.search(b'/rust/target/[^/]+/hg', normbin):
3180 3182 self._pythondir = os.path.dirname(self._testdir)
3181 3183 # Fall back to the legacy behavior.
3182 3184 else:
3183 3185 self._pythondir = self._bindir
3184 3186 self._pythondir_inferred = True
3185 3187
3186 3188 else:
3187 3189 self._installdir = os.path.join(self._hgtmp, b"install")
3188 3190 self._bindir = os.path.join(self._installdir, b"bin")
3189 3191 self._hgcommand = b'hg'
3190 3192 self._pythondir = os.path.join(self._installdir, b"lib", b"python")
3191 3193
3192 3194 # Force the use of hg.exe instead of relying on MSYS to recognize hg is
3193 3195 # a python script and feed it to python.exe. Legacy stdio is force
3194 3196 # enabled by hg.exe, and this is a more realistic way to launch hg
3195 3197 # anyway.
3196 3198 if WINDOWS and not self._hgcommand.endswith(b'.exe'):
3197 3199 self._hgcommand += b'.exe'
3198 3200
3199 3201 real_hg = os.path.join(self._bindir, self._hgcommand)
3200 3202 osenvironb[b'HGTEST_REAL_HG'] = real_hg
3201 3203 # set CHGHG, then replace "hg" command by "chg"
3202 3204 chgbindir = self._bindir
3203 3205 if self.options.chg or self.options.with_chg:
3204 3206 osenvironb[b'CHG_INSTALLED_AS_HG'] = b'1'
3205 3207 osenvironb[b'CHGHG'] = real_hg
3206 3208 else:
3207 3209 # drop flag for hghave
3208 3210 osenvironb.pop(b'CHG_INSTALLED_AS_HG', None)
3209 3211 if self.options.chg:
3210 3212 self._hgcommand = b'chg'
3211 3213 elif self.options.with_chg:
3212 3214 chgbindir = os.path.dirname(os.path.realpath(self.options.with_chg))
3213 3215 self._hgcommand = os.path.basename(self.options.with_chg)
3214 3216
3215 3217 # configure fallback and replace "hg" command by "rhg"
3216 3218 rhgbindir = self._bindir
3217 3219 if self.options.rhg or self.options.with_rhg:
3218 3220 # Affects hghave.py
3219 3221 osenvironb[b'RHG_INSTALLED_AS_HG'] = b'1'
3220 3222 # Affects configuration. Alternatives would be setting configuration through
3221 3223 # `$HGRCPATH` but some tests override that, or changing `_hgcommand` to include
3222 3224 # `--config` but that disrupts tests that print command lines and check expected
3223 3225 # output.
3224 3226 osenvironb[b'RHG_ON_UNSUPPORTED'] = b'fallback'
3225 3227 osenvironb[b'RHG_FALLBACK_EXECUTABLE'] = real_hg
3226 3228 else:
3227 3229 # drop flag for hghave
3228 3230 osenvironb.pop(b'RHG_INSTALLED_AS_HG', None)
3229 3231 if self.options.rhg:
3230 3232 self._hgcommand = b'rhg'
3231 3233 elif self.options.with_rhg:
3232 3234 rhgbindir = os.path.dirname(os.path.realpath(self.options.with_rhg))
3233 3235 self._hgcommand = os.path.basename(self.options.with_rhg)
3234 3236
3235 3237 if self.options.pyoxidized:
3236 3238 testdir = os.path.dirname(_sys2bytes(canonpath(sys.argv[0])))
3237 3239 reporootdir = os.path.dirname(testdir)
3238 3240 # XXX we should ideally install stuff instead of using the local build
3239 3241 bin_path = (
3240 3242 b'build/pyoxidizer/x86_64-pc-windows-msvc/release/app/hg.exe'
3241 3243 )
3242 3244 full_path = os.path.join(reporootdir, bin_path)
3243 3245 self._hgcommand = full_path
3244 3246 # Affects hghave.py
3245 3247 osenvironb[b'PYOXIDIZED_INSTALLED_AS_HG'] = b'1'
3246 3248 else:
3247 3249 osenvironb.pop(b'PYOXIDIZED_INSTALLED_AS_HG', None)
3248 3250
3249 3251 osenvironb[b"BINDIR"] = self._bindir
3250 3252 osenvironb[b"PYTHON"] = PYTHON
3251 3253
3252 3254 fileb = _sys2bytes(__file__)
3253 3255 runtestdir = os.path.abspath(os.path.dirname(fileb))
3254 3256 osenvironb[b'RUNTESTDIR'] = runtestdir
3255 3257 if PYTHON3:
3256 3258 sepb = _sys2bytes(os.pathsep)
3257 3259 else:
3258 3260 sepb = os.pathsep
3259 3261 path = [self._bindir, runtestdir] + osenvironb[b"PATH"].split(sepb)
3260 3262 if os.path.islink(__file__):
3261 3263 # test helper will likely be at the end of the symlink
3262 3264 realfile = os.path.realpath(fileb)
3263 3265 realdir = os.path.abspath(os.path.dirname(realfile))
3264 3266 path.insert(2, realdir)
3265 3267 if chgbindir != self._bindir:
3266 3268 path.insert(1, chgbindir)
3267 3269 if rhgbindir != self._bindir:
3268 3270 path.insert(1, rhgbindir)
3269 3271 if self._testdir != runtestdir:
3270 3272 path = [self._testdir] + path
3271 3273 path = [self._custom_bin_dir] + path
3272 3274 osenvironb[b"PATH"] = sepb.join(path)
3273 3275
3274 3276 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
3275 3277 # can run .../tests/run-tests.py test-foo where test-foo
3276 3278 # adds an extension to HGRC. Also include run-test.py directory to
3277 3279 # import modules like heredoctest.
3278 3280 pypath = [self._pythondir, self._testdir, runtestdir]
3279 3281 # We have to augment PYTHONPATH, rather than simply replacing
3280 3282 # it, in case external libraries are only available via current
3281 3283 # PYTHONPATH. (In particular, the Subversion bindings on OS X
3282 3284 # are in /opt/subversion.)
3283 3285 oldpypath = osenvironb.get(IMPL_PATH)
3284 3286 if oldpypath:
3285 3287 pypath.append(oldpypath)
3286 3288 osenvironb[IMPL_PATH] = sepb.join(pypath)
3287 3289
3288 3290 if self.options.pure:
3289 3291 os.environ["HGTEST_RUN_TESTS_PURE"] = "--pure"
3290 3292 os.environ["HGMODULEPOLICY"] = "py"
3291 3293 if self.options.rust:
3292 3294 os.environ["HGMODULEPOLICY"] = "rust+c"
3293 3295 if self.options.no_rust:
3294 3296 current_policy = os.environ.get("HGMODULEPOLICY", "")
3295 3297 if current_policy.startswith("rust+"):
3296 3298 os.environ["HGMODULEPOLICY"] = current_policy[len("rust+") :]
3297 3299 os.environ.pop("HGWITHRUSTEXT", None)
3298 3300
3299 3301 if self.options.allow_slow_tests:
3300 3302 os.environ["HGTEST_SLOW"] = "slow"
3301 3303 elif 'HGTEST_SLOW' in os.environ:
3302 3304 del os.environ['HGTEST_SLOW']
3303 3305
3304 3306 self._coveragefile = os.path.join(self._testdir, b'.coverage')
3305 3307
3306 3308 if self.options.exceptions:
3307 3309 exceptionsdir = os.path.join(self._outputdir, b'exceptions')
3308 3310 try:
3309 3311 os.makedirs(exceptionsdir)
3310 3312 except OSError as e:
3311 3313 if e.errno != errno.EEXIST:
3312 3314 raise
3313 3315
3314 3316 # Remove all existing exception reports.
3315 3317 for f in os.listdir(exceptionsdir):
3316 3318 os.unlink(os.path.join(exceptionsdir, f))
3317 3319
3318 3320 osenvironb[b'HGEXCEPTIONSDIR'] = exceptionsdir
3319 3321 logexceptions = os.path.join(self._testdir, b'logexceptions.py')
3320 3322 self.options.extra_config_opt.append(
3321 3323 'extensions.logexceptions=%s' % logexceptions.decode('utf-8')
3322 3324 )
3323 3325
3324 3326 vlog("# Using TESTDIR", _bytes2sys(self._testdir))
3325 3327 vlog("# Using RUNTESTDIR", _bytes2sys(osenvironb[b'RUNTESTDIR']))
3326 3328 vlog("# Using HGTMP", _bytes2sys(self._hgtmp))
3327 3329 vlog("# Using PATH", os.environ["PATH"])
3328 3330 vlog(
3329 3331 "# Using",
3330 3332 _bytes2sys(IMPL_PATH),
3331 3333 _bytes2sys(osenvironb[IMPL_PATH]),
3332 3334 )
3333 3335 vlog("# Writing to directory", _bytes2sys(self._outputdir))
3334 3336
3335 3337 try:
3336 3338 return self._runtests(testdescs) or 0
3337 3339 finally:
3338 3340 time.sleep(0.1)
3339 3341 self._cleanup()
3340 3342
3341 3343 def findtests(self, args):
3342 3344 """Finds possible test files from arguments.
3343 3345
3344 3346 If you wish to inject custom tests into the test harness, this would
3345 3347 be a good function to monkeypatch or override in a derived class.
3346 3348 """
3347 3349 if not args:
3348 3350 if self.options.changed:
3349 3351 proc = Popen4(
3350 3352 b'hg st --rev "%s" -man0 .'
3351 3353 % _sys2bytes(self.options.changed),
3352 3354 None,
3353 3355 0,
3354 3356 )
3355 3357 stdout, stderr = proc.communicate()
3356 3358 args = stdout.strip(b'\0').split(b'\0')
3357 3359 else:
3358 3360 args = os.listdir(b'.')
3359 3361
3360 3362 expanded_args = []
3361 3363 for arg in args:
3362 3364 if os.path.isdir(arg):
3363 3365 if not arg.endswith(b'/'):
3364 3366 arg += b'/'
3365 3367 expanded_args.extend([arg + a for a in os.listdir(arg)])
3366 3368 else:
3367 3369 expanded_args.append(arg)
3368 3370 args = expanded_args
3369 3371
3370 3372 testcasepattern = re.compile(br'([\w-]+\.t|py)(?:#([a-zA-Z0-9_\-.#]+))')
3371 3373 tests = []
3372 3374 for t in args:
3373 3375 case = []
3374 3376
3375 3377 if not (
3376 3378 os.path.basename(t).startswith(b'test-')
3377 3379 and (t.endswith(b'.py') or t.endswith(b'.t'))
3378 3380 ):
3379 3381
3380 3382 m = testcasepattern.match(os.path.basename(t))
3381 3383 if m is not None:
3382 3384 t_basename, casestr = m.groups()
3383 3385 t = os.path.join(os.path.dirname(t), t_basename)
3384 3386 if casestr:
3385 3387 case = casestr.split(b'#')
3386 3388 else:
3387 3389 continue
3388 3390
3389 3391 if t.endswith(b'.t'):
3390 3392 # .t file may contain multiple test cases
3391 3393 casedimensions = parsettestcases(t)
3392 3394 if casedimensions:
3393 3395 cases = []
3394 3396
3395 3397 def addcases(case, casedimensions):
3396 3398 if not casedimensions:
3397 3399 cases.append(case)
3398 3400 else:
3399 3401 for c in casedimensions[0]:
3400 3402 addcases(case + [c], casedimensions[1:])
3401 3403
3402 3404 addcases([], casedimensions)
3403 3405 if case and case in cases:
3404 3406 cases = [case]
3405 3407 elif case:
3406 3408 # Ignore invalid cases
3407 3409 cases = []
3408 3410 else:
3409 3411 pass
3410 3412 tests += [{'path': t, 'case': c} for c in sorted(cases)]
3411 3413 else:
3412 3414 tests.append({'path': t})
3413 3415 else:
3414 3416 tests.append({'path': t})
3415 3417
3416 3418 if self.options.retest:
3417 3419 retest_args = []
3418 3420 for test in tests:
3419 3421 errpath = self._geterrpath(test)
3420 3422 if os.path.exists(errpath):
3421 3423 retest_args.append(test)
3422 3424 tests = retest_args
3423 3425 return tests
3424 3426
3425 3427 def _runtests(self, testdescs):
3426 3428 def _reloadtest(test, i):
3427 3429 # convert a test back to its description dict
3428 3430 desc = {'path': test.path}
3429 3431 case = getattr(test, '_case', [])
3430 3432 if case:
3431 3433 desc['case'] = case
3432 3434 return self._gettest(desc, i)
3433 3435
3434 3436 try:
3435 3437 if self.options.restart:
3436 3438 orig = list(testdescs)
3437 3439 while testdescs:
3438 3440 desc = testdescs[0]
3439 3441 errpath = self._geterrpath(desc)
3440 3442 if os.path.exists(errpath):
3441 3443 break
3442 3444 testdescs.pop(0)
3443 3445 if not testdescs:
3444 3446 print("running all tests")
3445 3447 testdescs = orig
3446 3448
3447 3449 tests = [self._gettest(d, i) for i, d in enumerate(testdescs)]
3448 3450 num_tests = len(tests) * self.options.runs_per_test
3449 3451
3450 3452 jobs = min(num_tests, self.options.jobs)
3451 3453
3452 3454 failed = False
3453 3455 kws = self.options.keywords
3454 3456 if kws is not None and PYTHON3:
3455 3457 kws = kws.encode('utf-8')
3456 3458
3457 3459 suite = TestSuite(
3458 3460 self._testdir,
3459 3461 jobs=jobs,
3460 3462 whitelist=self.options.whitelisted,
3461 3463 blacklist=self.options.blacklist,
3462 3464 keywords=kws,
3463 3465 loop=self.options.loop,
3464 3466 runs_per_test=self.options.runs_per_test,
3465 3467 showchannels=self.options.showchannels,
3466 3468 tests=tests,
3467 3469 loadtest=_reloadtest,
3468 3470 )
3469 3471 verbosity = 1
3470 3472 if self.options.list_tests:
3471 3473 verbosity = 0
3472 3474 elif self.options.verbose:
3473 3475 verbosity = 2
3474 3476 runner = TextTestRunner(self, verbosity=verbosity)
3475 3477
3476 3478 if self.options.list_tests:
3477 3479 result = runner.listtests(suite)
3478 3480 else:
3479 3481 self._usecorrectpython()
3480 3482 if self._installdir:
3481 3483 self._installhg()
3482 3484 self._checkhglib("Testing")
3483 3485 if self.options.chg:
3484 3486 assert self._installdir
3485 3487 self._installchg()
3486 3488 if self.options.rhg:
3487 3489 assert self._installdir
3488 3490 self._installrhg()
3489 3491 elif self.options.pyoxidized:
3490 3492 self._build_pyoxidized()
3491 3493 self._use_correct_mercurial()
3492 3494
3493 3495 log(
3494 3496 'running %d tests using %d parallel processes'
3495 3497 % (num_tests, jobs)
3496 3498 )
3497 3499
3498 3500 result = runner.run(suite)
3499 3501
3500 3502 if result.failures or result.errors:
3501 3503 failed = True
3502 3504
3503 3505 result.onEnd()
3504 3506
3505 3507 if self.options.anycoverage:
3506 3508 self._outputcoverage()
3507 3509 except KeyboardInterrupt:
3508 3510 failed = True
3509 3511 print("\ninterrupted!")
3510 3512
3511 3513 if failed:
3512 3514 return 1
3513 3515
3514 3516 def _geterrpath(self, test):
3515 3517 # test['path'] is a relative path
3516 3518 if 'case' in test:
3517 3519 # for multiple dimensions test cases
3518 3520 casestr = b'#'.join(test['case'])
3519 3521 errpath = b'%s#%s.err' % (test['path'], casestr)
3520 3522 else:
3521 3523 errpath = b'%s.err' % test['path']
3522 3524 if self.options.outputdir:
3523 3525 self._outputdir = canonpath(_sys2bytes(self.options.outputdir))
3524 3526 errpath = os.path.join(self._outputdir, errpath)
3525 3527 return errpath
3526 3528
3527 3529 def _getport(self, count):
3528 3530 port = self._ports.get(count) # do we have a cached entry?
3529 3531 if port is None:
3530 3532 portneeded = 3
3531 3533 # above 100 tries we just give up and let test reports failure
3532 3534 for tries in xrange(100):
3533 3535 allfree = True
3534 3536 port = self.options.port + self._portoffset
3535 3537 for idx in xrange(portneeded):
3536 3538 if not checkportisavailable(port + idx):
3537 3539 allfree = False
3538 3540 break
3539 3541 self._portoffset += portneeded
3540 3542 if allfree:
3541 3543 break
3542 3544 self._ports[count] = port
3543 3545 return port
3544 3546
3545 3547 def _gettest(self, testdesc, count):
3546 3548 """Obtain a Test by looking at its filename.
3547 3549
3548 3550 Returns a Test instance. The Test may not be runnable if it doesn't
3549 3551 map to a known type.
3550 3552 """
3551 3553 path = testdesc['path']
3552 3554 lctest = path.lower()
3553 3555 testcls = Test
3554 3556
3555 3557 for ext, cls in self.TESTTYPES:
3556 3558 if lctest.endswith(ext):
3557 3559 testcls = cls
3558 3560 break
3559 3561
3560 3562 refpath = os.path.join(getcwdb(), path)
3561 3563 tmpdir = os.path.join(self._hgtmp, b'child%d' % count)
3562 3564
3563 3565 # extra keyword parameters. 'case' is used by .t tests
3564 3566 kwds = {k: testdesc[k] for k in ['case'] if k in testdesc}
3565 3567
3566 3568 t = testcls(
3567 3569 refpath,
3568 3570 self._outputdir,
3569 3571 tmpdir,
3570 3572 keeptmpdir=self.options.keep_tmpdir,
3571 3573 debug=self.options.debug,
3572 3574 first=self.options.first,
3573 3575 timeout=self.options.timeout,
3574 3576 startport=self._getport(count),
3575 3577 extraconfigopts=self.options.extra_config_opt,
3576 3578 shell=self.options.shell,
3577 3579 hgcommand=self._hgcommand,
3578 3580 usechg=bool(self.options.with_chg or self.options.chg),
3579 3581 chgdebug=self.options.chg_debug,
3580 3582 useipv6=useipv6,
3581 3583 **kwds
3582 3584 )
3583 3585 t.should_reload = True
3584 3586 return t
3585 3587
3586 3588 def _cleanup(self):
3587 3589 """Clean up state from this test invocation."""
3588 3590 if self.options.keep_tmpdir:
3589 3591 return
3590 3592
3591 3593 vlog("# Cleaning up HGTMP", _bytes2sys(self._hgtmp))
3592 3594 shutil.rmtree(self._hgtmp, True)
3593 3595 for f in self._createdfiles:
3594 3596 try:
3595 3597 os.remove(f)
3596 3598 except OSError:
3597 3599 pass
3598 3600
3599 3601 def _usecorrectpython(self):
3600 3602 """Configure the environment to use the appropriate Python in tests."""
3601 3603 # Tests must use the same interpreter as us or bad things will happen.
3602 3604 if WINDOWS and PYTHON3:
3603 3605 pyexe_names = [b'python', b'python3', b'python.exe']
3604 3606 elif WINDOWS:
3605 3607 pyexe_names = [b'python', b'python.exe']
3606 3608 elif PYTHON3:
3607 3609 pyexe_names = [b'python', b'python3']
3608 3610 else:
3609 3611 pyexe_names = [b'python', b'python2']
3610 3612
3611 3613 # os.symlink() is a thing with py3 on Windows, but it requires
3612 3614 # Administrator rights.
3613 3615 if not WINDOWS and getattr(os, 'symlink', None):
3614 3616 msg = "# Making python executable in test path a symlink to '%s'"
3615 3617 msg %= sysexecutable
3616 3618 vlog(msg)
3617 3619 for pyexename in pyexe_names:
3618 3620 mypython = os.path.join(self._custom_bin_dir, pyexename)
3619 3621 try:
3620 3622 if os.readlink(mypython) == sysexecutable:
3621 3623 continue
3622 3624 os.unlink(mypython)
3623 3625 except OSError as err:
3624 3626 if err.errno != errno.ENOENT:
3625 3627 raise
3626 3628 if self._findprogram(pyexename) != sysexecutable:
3627 3629 try:
3628 3630 os.symlink(sysexecutable, mypython)
3629 3631 self._createdfiles.append(mypython)
3630 3632 except OSError as err:
3631 3633 # child processes may race, which is harmless
3632 3634 if err.errno != errno.EEXIST:
3633 3635 raise
3634 3636 elif WINDOWS and not os.getenv('MSYSTEM'):
3635 3637 raise AssertionError('cannot run test on Windows without MSYSTEM')
3636 3638 else:
3637 3639 # Generate explicit file instead of symlink
3638 3640 #
3639 3641 # This is especially important as Windows doesn't have
3640 3642 # `python3.exe`, and MSYS cannot understand the reparse point with
3641 3643 # that name provided by Microsoft. Create a simple script on PATH
3642 3644 # with that name that delegates to the py3 launcher so the shebang
3643 3645 # lines work.
3644 3646 esc_executable = _sys2bytes(shellquote(sysexecutable))
3645 3647 for pyexename in pyexe_names:
3646 3648 stub_exec_path = os.path.join(self._custom_bin_dir, pyexename)
3647 3649 with open(stub_exec_path, 'wb') as f:
3648 3650 f.write(b'#!/bin/sh\n')
3649 3651 f.write(b'%s "$@"\n' % esc_executable)
3650 3652
3651 3653 if WINDOWS:
3652 3654 if not PYTHON3:
3653 3655 # lets try to build a valid python3 executable for the
3654 3656 # scrip that requires it.
3655 3657 py3exe_name = os.path.join(self._custom_bin_dir, b'python3')
3656 3658 with open(py3exe_name, 'wb') as f:
3657 3659 f.write(b'#!/bin/sh\n')
3658 3660 f.write(b'py -3 "$@"\n')
3659 3661
3660 3662 # adjust the path to make sur the main python finds it own dll
3661 3663 path = os.environ['PATH'].split(os.pathsep)
3662 3664 main_exec_dir = os.path.dirname(sysexecutable)
3663 3665 extra_paths = [_bytes2sys(self._custom_bin_dir), main_exec_dir]
3664 3666
3665 3667 # Binaries installed by pip into the user area like pylint.exe may
3666 3668 # not be in PATH by default.
3667 3669 appdata = os.environ.get('APPDATA')
3668 3670 vi = sys.version_info
3669 3671 if appdata is not None:
3670 3672 python_dir = 'Python%d%d' % (vi[0], vi[1])
3671 3673 scripts_path = [appdata, 'Python', python_dir, 'Scripts']
3672 3674 if not PYTHON3:
3673 3675 scripts_path = [appdata, 'Python', 'Scripts']
3674 3676 scripts_dir = os.path.join(*scripts_path)
3675 3677 extra_paths.append(scripts_dir)
3676 3678
3677 3679 os.environ['PATH'] = os.pathsep.join(extra_paths + path)
3678 3680
3679 3681 def _use_correct_mercurial(self):
3680 3682 target_exec = os.path.join(self._custom_bin_dir, b'hg')
3681 3683 if self._hgcommand != b'hg':
3682 3684 # shutil.which only accept bytes from 3.8
3683 3685 real_exec = which(self._hgcommand)
3684 3686 if real_exec is None:
3685 3687 raise ValueError('could not find exec path for "%s"', real_exec)
3686 3688 if real_exec == target_exec:
3687 3689 # do not overwrite something with itself
3688 3690 return
3689 3691 if WINDOWS:
3690 3692 with open(target_exec, 'wb') as f:
3691 3693 f.write(b'#!/bin/sh\n')
3692 3694 escaped_exec = shellquote(_bytes2sys(real_exec))
3693 3695 f.write(b'%s "$@"\n' % _sys2bytes(escaped_exec))
3694 3696 else:
3695 3697 os.symlink(real_exec, target_exec)
3696 3698 self._createdfiles.append(target_exec)
3697 3699
3698 3700 def _installhg(self):
3699 3701 """Install hg into the test environment.
3700 3702
3701 3703 This will also configure hg with the appropriate testing settings.
3702 3704 """
3703 3705 vlog("# Performing temporary installation of HG")
3704 3706 installerrs = os.path.join(self._hgtmp, b"install.err")
3705 3707 compiler = ''
3706 3708 if self.options.compiler:
3707 3709 compiler = '--compiler ' + self.options.compiler
3708 3710 setup_opts = b""
3709 3711 if self.options.pure:
3710 3712 setup_opts = b"--pure"
3711 3713 elif self.options.rust:
3712 3714 setup_opts = b"--rust"
3713 3715 elif self.options.no_rust:
3714 3716 setup_opts = b"--no-rust"
3715 3717
3716 3718 # Run installer in hg root
3717 3719 script = os.path.realpath(sys.argv[0])
3718 3720 exe = sysexecutable
3719 3721 if PYTHON3:
3720 3722 compiler = _sys2bytes(compiler)
3721 3723 script = _sys2bytes(script)
3722 3724 exe = _sys2bytes(exe)
3723 3725 hgroot = os.path.dirname(os.path.dirname(script))
3724 3726 self._hgroot = hgroot
3725 3727 os.chdir(hgroot)
3726 3728 nohome = b'--home=""'
3727 3729 if WINDOWS:
3728 3730 # The --home="" trick works only on OS where os.sep == '/'
3729 3731 # because of a distutils convert_path() fast-path. Avoid it at
3730 3732 # least on Windows for now, deal with .pydistutils.cfg bugs
3731 3733 # when they happen.
3732 3734 nohome = b''
3733 3735 cmd = (
3734 3736 b'"%(exe)s" setup.py %(setup_opts)s clean --all'
3735 3737 b' build %(compiler)s --build-base="%(base)s"'
3736 3738 b' install --force --prefix="%(prefix)s"'
3737 3739 b' --install-lib="%(libdir)s"'
3738 3740 b' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
3739 3741 % {
3740 3742 b'exe': exe,
3741 3743 b'setup_opts': setup_opts,
3742 3744 b'compiler': compiler,
3743 3745 b'base': os.path.join(self._hgtmp, b"build"),
3744 3746 b'prefix': self._installdir,
3745 3747 b'libdir': self._pythondir,
3746 3748 b'bindir': self._bindir,
3747 3749 b'nohome': nohome,
3748 3750 b'logfile': installerrs,
3749 3751 }
3750 3752 )
3751 3753
3752 3754 # setuptools requires install directories to exist.
3753 3755 def makedirs(p):
3754 3756 try:
3755 3757 os.makedirs(p)
3756 3758 except OSError as e:
3757 3759 if e.errno != errno.EEXIST:
3758 3760 raise
3759 3761
3760 3762 makedirs(self._pythondir)
3761 3763 makedirs(self._bindir)
3762 3764
3763 3765 vlog("# Running", cmd.decode("utf-8"))
3764 3766 if subprocess.call(_bytes2sys(cmd), shell=True) == 0:
3765 3767 if not self.options.verbose:
3766 3768 try:
3767 3769 os.remove(installerrs)
3768 3770 except OSError as e:
3769 3771 if e.errno != errno.ENOENT:
3770 3772 raise
3771 3773 else:
3772 3774 with open(installerrs, 'rb') as f:
3773 3775 for line in f:
3774 3776 if PYTHON3:
3775 3777 sys.stdout.buffer.write(line)
3776 3778 else:
3777 3779 sys.stdout.write(line)
3778 3780 sys.exit(1)
3779 3781 os.chdir(self._testdir)
3780 3782
3781 3783 hgbat = os.path.join(self._bindir, b'hg.bat')
3782 3784 if os.path.isfile(hgbat):
3783 3785 # hg.bat expects to be put in bin/scripts while run-tests.py
3784 3786 # installation layout put it in bin/ directly. Fix it
3785 3787 with open(hgbat, 'rb') as f:
3786 3788 data = f.read()
3787 3789 if br'"%~dp0..\python" "%~dp0hg" %*' in data:
3788 3790 data = data.replace(
3789 3791 br'"%~dp0..\python" "%~dp0hg" %*',
3790 3792 b'"%~dp0python" "%~dp0hg" %*',
3791 3793 )
3792 3794 with open(hgbat, 'wb') as f:
3793 3795 f.write(data)
3794 3796 else:
3795 3797 print('WARNING: cannot fix hg.bat reference to python.exe')
3796 3798
3797 3799 if self.options.anycoverage:
3798 3800 custom = os.path.join(
3799 3801 osenvironb[b'RUNTESTDIR'], b'sitecustomize.py'
3800 3802 )
3801 3803 target = os.path.join(self._pythondir, b'sitecustomize.py')
3802 3804 vlog('# Installing coverage trigger to %s' % target)
3803 3805 shutil.copyfile(custom, target)
3804 3806 rc = os.path.join(self._testdir, b'.coveragerc')
3805 3807 vlog('# Installing coverage rc to %s' % rc)
3806 3808 osenvironb[b'COVERAGE_PROCESS_START'] = rc
3807 3809 covdir = os.path.join(self._installdir, b'..', b'coverage')
3808 3810 try:
3809 3811 os.mkdir(covdir)
3810 3812 except OSError as e:
3811 3813 if e.errno != errno.EEXIST:
3812 3814 raise
3813 3815
3814 3816 osenvironb[b'COVERAGE_DIR'] = covdir
3815 3817
3816 3818 def _checkhglib(self, verb):
3817 3819 """Ensure that the 'mercurial' package imported by python is
3818 3820 the one we expect it to be. If not, print a warning to stderr."""
3819 3821 if self._pythondir_inferred:
3820 3822 # The pythondir has been inferred from --with-hg flag.
3821 3823 # We cannot expect anything sensible here.
3822 3824 return
3823 3825 expecthg = os.path.join(self._pythondir, b'mercurial')
3824 3826 actualhg = self._gethgpath()
3825 3827 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
3826 3828 sys.stderr.write(
3827 3829 'warning: %s with unexpected mercurial lib: %s\n'
3828 3830 ' (expected %s)\n' % (verb, actualhg, expecthg)
3829 3831 )
3830 3832
3831 3833 def _gethgpath(self):
3832 3834 """Return the path to the mercurial package that is actually found by
3833 3835 the current Python interpreter."""
3834 3836 if self._hgpath is not None:
3835 3837 return self._hgpath
3836 3838
3837 3839 cmd = b'"%s" -c "import mercurial; print (mercurial.__path__[0])"'
3838 3840 cmd = cmd % PYTHON
3839 3841 if PYTHON3:
3840 3842 cmd = _bytes2sys(cmd)
3841 3843
3842 3844 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
3843 3845 out, err = p.communicate()
3844 3846
3845 3847 self._hgpath = out.strip()
3846 3848
3847 3849 return self._hgpath
3848 3850
3849 3851 def _installchg(self):
3850 3852 """Install chg into the test environment"""
3851 3853 vlog('# Performing temporary installation of CHG')
3852 3854 assert os.path.dirname(self._bindir) == self._installdir
3853 3855 assert self._hgroot, 'must be called after _installhg()'
3854 3856 cmd = b'"%(make)s" clean install PREFIX="%(prefix)s"' % {
3855 3857 b'make': b'make', # TODO: switch by option or environment?
3856 3858 b'prefix': self._installdir,
3857 3859 }
3858 3860 cwd = os.path.join(self._hgroot, b'contrib', b'chg')
3859 3861 vlog("# Running", cmd)
3860 3862 proc = subprocess.Popen(
3861 3863 cmd,
3862 3864 shell=True,
3863 3865 cwd=cwd,
3864 3866 stdin=subprocess.PIPE,
3865 3867 stdout=subprocess.PIPE,
3866 3868 stderr=subprocess.STDOUT,
3867 3869 )
3868 3870 out, _err = proc.communicate()
3869 3871 if proc.returncode != 0:
3870 3872 if PYTHON3:
3871 3873 sys.stdout.buffer.write(out)
3872 3874 else:
3873 3875 sys.stdout.write(out)
3874 3876 sys.exit(1)
3875 3877
3876 3878 def _installrhg(self):
3877 3879 """Install rhg into the test environment"""
3878 3880 vlog('# Performing temporary installation of rhg')
3879 3881 assert os.path.dirname(self._bindir) == self._installdir
3880 3882 assert self._hgroot, 'must be called after _installhg()'
3881 3883 cmd = b'"%(make)s" install-rhg PREFIX="%(prefix)s"' % {
3882 3884 b'make': b'make', # TODO: switch by option or environment?
3883 3885 b'prefix': self._installdir,
3884 3886 }
3885 3887 cwd = self._hgroot
3886 3888 vlog("# Running", cmd)
3887 3889 proc = subprocess.Popen(
3888 3890 cmd,
3889 3891 shell=True,
3890 3892 cwd=cwd,
3891 3893 stdin=subprocess.PIPE,
3892 3894 stdout=subprocess.PIPE,
3893 3895 stderr=subprocess.STDOUT,
3894 3896 )
3895 3897 out, _err = proc.communicate()
3896 3898 if proc.returncode != 0:
3897 3899 if PYTHON3:
3898 3900 sys.stdout.buffer.write(out)
3899 3901 else:
3900 3902 sys.stdout.write(out)
3901 3903 sys.exit(1)
3902 3904
3903 3905 def _build_pyoxidized(self):
3904 3906 """build a pyoxidized version of mercurial into the test environment
3905 3907
3906 3908 Ideally this function would be `install_pyoxidier` and would both build
3907 3909 and install pyoxidier. However we are starting small to get pyoxidizer
3908 3910 build binary to testing quickly.
3909 3911 """
3910 3912 vlog('# build a pyoxidized version of Mercurial')
3911 3913 assert os.path.dirname(self._bindir) == self._installdir
3912 3914 assert self._hgroot, 'must be called after _installhg()'
3913 3915 cmd = b'"%(make)s" pyoxidizer-windows-tests' % {
3914 3916 b'make': b'make',
3915 3917 }
3916 3918 cwd = self._hgroot
3917 3919 vlog("# Running", cmd)
3918 3920 proc = subprocess.Popen(
3919 3921 _bytes2sys(cmd),
3920 3922 shell=True,
3921 3923 cwd=_bytes2sys(cwd),
3922 3924 stdin=subprocess.PIPE,
3923 3925 stdout=subprocess.PIPE,
3924 3926 stderr=subprocess.STDOUT,
3925 3927 )
3926 3928 out, _err = proc.communicate()
3927 3929 if proc.returncode != 0:
3928 3930 if PYTHON3:
3929 3931 sys.stdout.buffer.write(out)
3930 3932 else:
3931 3933 sys.stdout.write(out)
3932 3934 sys.exit(1)
3933 3935
3934 3936 def _outputcoverage(self):
3935 3937 """Produce code coverage output."""
3936 3938 import coverage
3937 3939
3938 3940 coverage = coverage.coverage
3939 3941
3940 3942 vlog('# Producing coverage report')
3941 3943 # chdir is the easiest way to get short, relative paths in the
3942 3944 # output.
3943 3945 os.chdir(self._hgroot)
3944 3946 covdir = os.path.join(_bytes2sys(self._installdir), '..', 'coverage')
3945 3947 cov = coverage(data_file=os.path.join(covdir, 'cov'))
3946 3948
3947 3949 # Map install directory paths back to source directory.
3948 3950 cov.config.paths['srcdir'] = ['.', _bytes2sys(self._pythondir)]
3949 3951
3950 3952 cov.combine()
3951 3953
3952 3954 omit = [
3953 3955 _bytes2sys(os.path.join(x, b'*'))
3954 3956 for x in [self._bindir, self._testdir]
3955 3957 ]
3956 3958 cov.report(ignore_errors=True, omit=omit)
3957 3959
3958 3960 if self.options.htmlcov:
3959 3961 htmldir = os.path.join(_bytes2sys(self._outputdir), 'htmlcov')
3960 3962 cov.html_report(directory=htmldir, omit=omit)
3961 3963 if self.options.annotate:
3962 3964 adir = os.path.join(_bytes2sys(self._outputdir), 'annotated')
3963 3965 if not os.path.isdir(adir):
3964 3966 os.mkdir(adir)
3965 3967 cov.annotate(directory=adir, omit=omit)
3966 3968
3967 3969 def _findprogram(self, program):
3968 3970 """Search PATH for a executable program"""
3969 3971 dpb = _sys2bytes(os.defpath)
3970 3972 sepb = _sys2bytes(os.pathsep)
3971 3973 for p in osenvironb.get(b'PATH', dpb).split(sepb):
3972 3974 name = os.path.join(p, program)
3973 3975 if WINDOWS or os.access(name, os.X_OK):
3974 3976 return _bytes2sys(name)
3975 3977 return None
3976 3978
3977 3979 def _checktools(self):
3978 3980 """Ensure tools required to run tests are present."""
3979 3981 for p in self.REQUIREDTOOLS:
3980 3982 if WINDOWS and not p.endswith(b'.exe'):
3981 3983 p += b'.exe'
3982 3984 found = self._findprogram(p)
3983 3985 p = p.decode("utf-8")
3984 3986 if found:
3985 3987 vlog("# Found prerequisite", p, "at", found)
3986 3988 else:
3987 3989 print("WARNING: Did not find prerequisite tool: %s " % p)
3988 3990
3989 3991
3990 3992 def aggregateexceptions(path):
3991 3993 exceptioncounts = collections.Counter()
3992 3994 testsbyfailure = collections.defaultdict(set)
3993 3995 failuresbytest = collections.defaultdict(set)
3994 3996
3995 3997 for f in os.listdir(path):
3996 3998 with open(os.path.join(path, f), 'rb') as fh:
3997 3999 data = fh.read().split(b'\0')
3998 4000 if len(data) != 5:
3999 4001 continue
4000 4002
4001 4003 exc, mainframe, hgframe, hgline, testname = data
4002 4004 exc = exc.decode('utf-8')
4003 4005 mainframe = mainframe.decode('utf-8')
4004 4006 hgframe = hgframe.decode('utf-8')
4005 4007 hgline = hgline.decode('utf-8')
4006 4008 testname = testname.decode('utf-8')
4007 4009
4008 4010 key = (hgframe, hgline, exc)
4009 4011 exceptioncounts[key] += 1
4010 4012 testsbyfailure[key].add(testname)
4011 4013 failuresbytest[testname].add(key)
4012 4014
4013 4015 # Find test having fewest failures for each failure.
4014 4016 leastfailing = {}
4015 4017 for key, tests in testsbyfailure.items():
4016 4018 fewesttest = None
4017 4019 fewestcount = 99999999
4018 4020 for test in sorted(tests):
4019 4021 if len(failuresbytest[test]) < fewestcount:
4020 4022 fewesttest = test
4021 4023 fewestcount = len(failuresbytest[test])
4022 4024
4023 4025 leastfailing[key] = (fewestcount, fewesttest)
4024 4026
4025 4027 # Create a combined counter so we can sort by total occurrences and
4026 4028 # impacted tests.
4027 4029 combined = {}
4028 4030 for key in exceptioncounts:
4029 4031 combined[key] = (
4030 4032 exceptioncounts[key],
4031 4033 len(testsbyfailure[key]),
4032 4034 leastfailing[key][0],
4033 4035 leastfailing[key][1],
4034 4036 )
4035 4037
4036 4038 return {
4037 4039 'exceptioncounts': exceptioncounts,
4038 4040 'total': sum(exceptioncounts.values()),
4039 4041 'combined': combined,
4040 4042 'leastfailing': leastfailing,
4041 4043 'byfailure': testsbyfailure,
4042 4044 'bytest': failuresbytest,
4043 4045 }
4044 4046
4045 4047
4046 4048 if __name__ == '__main__':
4047 4049 if WINDOWS and not os.getenv('MSYSTEM'):
4048 4050 print('cannot run test on Windows without MSYSTEM', file=sys.stderr)
4049 4051 print(
4050 4052 '(if you need to do so contact the mercurial devs: '
4051 4053 'mercurial@mercurial-scm.org)',
4052 4054 file=sys.stderr,
4053 4055 )
4054 4056 sys.exit(255)
4055 4057
4056 4058 runner = TestRunner()
4057 4059
4058 4060 try:
4059 4061 import msvcrt
4060 4062
4061 4063 msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
4062 4064 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
4063 4065 msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
4064 4066 except ImportError:
4065 4067 pass
4066 4068
4067 4069 sys.exit(runner.run(sys.argv[1:]))
@@ -1,256 +1,257 b''
1 1 Create a repository:
2 2
3 3 #if no-extraextensions
4 4 $ hg config
5 5 chgserver.idletimeout=60
6 6 devel.all-warnings=true
7 7 devel.default-date=0 0
8 8 extensions.fsmonitor= (fsmonitor !)
9 9 format.exp-dirstate-v2=1 (dirstate-v2 !)
10 10 largefiles.usercache=$TESTTMP/.cache/largefiles
11 11 lfs.usercache=$TESTTMP/.cache/lfs
12 12 ui.slash=True
13 13 ui.interactive=False
14 14 ui.detailed-exit-code=True
15 15 ui.merge=internal:merge
16 16 ui.mergemarkers=detailed
17 17 ui.promptecho=True
18 ui.ssh=* (glob)
18 19 ui.timeout.warn=15
19 20 web.address=localhost
20 21 web\.ipv6=(?:True|False) (re)
21 22 web.server-header=testing stub value
22 23 #endif
23 24
24 25 $ hg init t
25 26 $ cd t
26 27
27 28 Prepare a changeset:
28 29
29 30 $ echo a > a
30 31 $ hg add a
31 32
32 33 $ hg status
33 34 A a
34 35
35 36 Writes to stdio succeed and fail appropriately
36 37
37 38 #if devfull
38 39 $ hg status 2>/dev/full
39 40 A a
40 41
41 42 $ hg status >/dev/full
42 43 abort: No space left on device
43 44 [255]
44 45 #endif
45 46
46 47 #if devfull
47 48 $ hg status >/dev/full 2>&1
48 49 [255]
49 50
50 51 $ hg status ENOENT 2>/dev/full
51 52 [255]
52 53 #endif
53 54
54 55 On Python 3, stdio may be None:
55 56
56 57 $ hg debuguiprompt --config ui.interactive=true 0<&-
57 58 abort: Bad file descriptor
58 59 [255]
59 60 $ hg version -q 0<&-
60 61 Mercurial Distributed SCM * (glob)
61 62
62 63 #if py3
63 64 $ hg version -q 1>&-
64 65 abort: Bad file descriptor
65 66 [255]
66 67 #else
67 68 $ hg version -q 1>&-
68 69 #endif
69 70 $ hg unknown -q 1>&-
70 71 hg: unknown command 'unknown'
71 72 (did you mean debugknown?)
72 73 [10]
73 74
74 75 $ hg version -q 2>&-
75 76 Mercurial Distributed SCM * (glob)
76 77 $ hg unknown -q 2>&-
77 78 [10]
78 79
79 80 $ hg commit -m test
80 81
81 82 This command is ancient:
82 83
83 84 $ hg history
84 85 changeset: 0:acb14030fe0a
85 86 tag: tip
86 87 user: test
87 88 date: Thu Jan 01 00:00:00 1970 +0000
88 89 summary: test
89 90
90 91
91 92 Verify that updating to revision 0 via commands.update() works properly
92 93
93 94 $ cat <<EOF > update_to_rev0.py
94 95 > from mercurial import commands, hg, ui as uimod
95 96 > myui = uimod.ui.load()
96 97 > repo = hg.repository(myui, path=b'.')
97 98 > commands.update(myui, repo, rev=b"0")
98 99 > EOF
99 100 $ hg up null
100 101 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
101 102 $ "$PYTHON" ./update_to_rev0.py
102 103 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
103 104 $ hg identify -n
104 105 0
105 106
106 107
107 108 Poke around at hashes:
108 109
109 110 $ hg manifest --debug
110 111 b789fdd96dc2f3bd229c1dd8eedf0fc60e2b68e3 644 a
111 112
112 113 $ hg cat a
113 114 a
114 115
115 116 Verify should succeed:
116 117
117 118 $ hg verify
118 119 checking changesets
119 120 checking manifests
120 121 crosschecking files in changesets and manifests
121 122 checking files
122 123 checked 1 changesets with 1 changes to 1 files
123 124
124 125 Repository root:
125 126
126 127 $ hg root
127 128 $TESTTMP/t
128 129 $ hg log -l1 -T '{reporoot}\n'
129 130 $TESTTMP/t
130 131 $ hg root -Tjson | sed 's|\\\\|\\|g'
131 132 [
132 133 {
133 134 "hgpath": "$TESTTMP/t/.hg",
134 135 "reporoot": "$TESTTMP/t",
135 136 "storepath": "$TESTTMP/t/.hg/store"
136 137 }
137 138 ]
138 139
139 140 At the end...
140 141
141 142 $ cd ..
142 143
143 144 Status message redirection:
144 145
145 146 $ hg init empty
146 147
147 148 status messages are sent to stdout by default:
148 149
149 150 $ hg outgoing -R t empty -Tjson 2>/dev/null
150 151 comparing with empty
151 152 searching for changes
152 153 [
153 154 {
154 155 "bookmarks": [],
155 156 "branch": "default",
156 157 "date": [0, 0],
157 158 "desc": "test",
158 159 "node": "acb14030fe0a21b60322c440ad2d20cf7685a376",
159 160 "parents": ["0000000000000000000000000000000000000000"],
160 161 "phase": "draft",
161 162 "rev": 0,
162 163 "tags": ["tip"],
163 164 "user": "test"
164 165 }
165 166 ]
166 167
167 168 which can be configured to send to stderr, so the output wouldn't be
168 169 interleaved:
169 170
170 171 $ cat <<'EOF' >> "$HGRCPATH"
171 172 > [ui]
172 173 > message-output = stderr
173 174 > EOF
174 175 $ hg outgoing -R t empty -Tjson 2>/dev/null
175 176 [
176 177 {
177 178 "bookmarks": [],
178 179 "branch": "default",
179 180 "date": [0, 0],
180 181 "desc": "test",
181 182 "node": "acb14030fe0a21b60322c440ad2d20cf7685a376",
182 183 "parents": ["0000000000000000000000000000000000000000"],
183 184 "phase": "draft",
184 185 "rev": 0,
185 186 "tags": ["tip"],
186 187 "user": "test"
187 188 }
188 189 ]
189 190 $ hg outgoing -R t empty -Tjson >/dev/null
190 191 comparing with empty
191 192 searching for changes
192 193
193 194 this option should be turned off by HGPLAIN= since it may break scripting use:
194 195
195 196 $ HGPLAIN= hg outgoing -R t empty -Tjson 2>/dev/null
196 197 comparing with empty
197 198 searching for changes
198 199 [
199 200 {
200 201 "bookmarks": [],
201 202 "branch": "default",
202 203 "date": [0, 0],
203 204 "desc": "test",
204 205 "node": "acb14030fe0a21b60322c440ad2d20cf7685a376",
205 206 "parents": ["0000000000000000000000000000000000000000"],
206 207 "phase": "draft",
207 208 "rev": 0,
208 209 "tags": ["tip"],
209 210 "user": "test"
210 211 }
211 212 ]
212 213
213 214 but still overridden by --config:
214 215
215 216 $ HGPLAIN= hg outgoing -R t empty -Tjson --config ui.message-output=stderr \
216 217 > 2>/dev/null
217 218 [
218 219 {
219 220 "bookmarks": [],
220 221 "branch": "default",
221 222 "date": [0, 0],
222 223 "desc": "test",
223 224 "node": "acb14030fe0a21b60322c440ad2d20cf7685a376",
224 225 "parents": ["0000000000000000000000000000000000000000"],
225 226 "phase": "draft",
226 227 "rev": 0,
227 228 "tags": ["tip"],
228 229 "user": "test"
229 230 }
230 231 ]
231 232
232 233 Invalid ui.message-output option:
233 234
234 235 $ hg log -R t --config ui.message-output=bad
235 236 abort: invalid ui.message-output destination: bad
236 237 [255]
237 238
238 239 Underlying message streams should be updated when ui.fout/ferr are set:
239 240
240 241 $ cat <<'EOF' > capui.py
241 242 > from mercurial import pycompat, registrar
242 243 > cmdtable = {}
243 244 > command = registrar.command(cmdtable)
244 245 > @command(b'capui', norepo=True)
245 246 > def capui(ui):
246 247 > out = ui.fout
247 248 > ui.fout = pycompat.bytesio()
248 249 > ui.status(b'status\n')
249 250 > ui.ferr = pycompat.bytesio()
250 251 > ui.warn(b'warn\n')
251 252 > out.write(b'stdout: %s' % ui.fout.getvalue())
252 253 > out.write(b'stderr: %s' % ui.ferr.getvalue())
253 254 > EOF
254 255 $ hg --config extensions.capui=capui.py --config ui.message-output=stdio capui
255 256 stdout: status
256 257 stderr: warn
@@ -1,1172 +1,1174 b''
1 1 #require no-rhg no-chg
2 2
3 3 XXX-RHG this test hangs if `hg` is really `rhg`. This was hidden by the use of
4 4 `alias hg=rhg` by run-tests.py. With such alias removed, this test is revealed
5 5 buggy. This need to be resolved sooner than later.
6 6
7 7 XXX-CHG this test hangs if `hg` is really `chg`. This was hidden by the use of
8 8 `alias hg=chg` by run-tests.py. With such alias removed, this test is revealed
9 9 buggy. This need to be resolved sooner than later.
10 10
11 11 #if windows
12 12 $ PYTHONPATH="$TESTDIR/../contrib;$PYTHONPATH"
13 13 #else
14 14 $ PYTHONPATH="$TESTDIR/../contrib:$PYTHONPATH"
15 15 #endif
16 16 $ export PYTHONPATH
17 17
18 18 typical client does not want echo-back messages, so test without it:
19 19
20 20 $ grep -v '^promptecho ' < $HGRCPATH >> $HGRCPATH.new
21 21 $ mv $HGRCPATH.new $HGRCPATH
22 22
23 23 $ hg init repo
24 24 $ cd repo
25 25
26 26 >>> from __future__ import absolute_import
27 27 >>> import os
28 28 >>> import sys
29 29 >>> from hgclient import bprint, check, readchannel, runcommand
30 30 >>> @check
31 31 ... def hellomessage(server):
32 32 ... ch, data = readchannel(server)
33 33 ... bprint(b'%c, %r' % (ch, data))
34 34 ... # run an arbitrary command to make sure the next thing the server
35 35 ... # sends isn't part of the hello message
36 36 ... runcommand(server, [b'id'])
37 37 o, 'capabilities: getencoding runcommand\nencoding: *\npid: *' (glob)
38 38 *** runcommand id
39 39 000000000000 tip
40 40
41 41 >>> from hgclient import check
42 42 >>> @check
43 43 ... def unknowncommand(server):
44 44 ... server.stdin.write(b'unknowncommand\n')
45 45 abort: unknown command unknowncommand
46 46
47 47 >>> from hgclient import check, readchannel, runcommand
48 48 >>> @check
49 49 ... def checkruncommand(server):
50 50 ... # hello block
51 51 ... readchannel(server)
52 52 ...
53 53 ... # no args
54 54 ... runcommand(server, [])
55 55 ...
56 56 ... # global options
57 57 ... runcommand(server, [b'id', b'--quiet'])
58 58 ...
59 59 ... # make sure global options don't stick through requests
60 60 ... runcommand(server, [b'id'])
61 61 ...
62 62 ... # --config
63 63 ... runcommand(server, [b'id', b'--config', b'ui.quiet=True'])
64 64 ...
65 65 ... # make sure --config doesn't stick
66 66 ... runcommand(server, [b'id'])
67 67 ...
68 68 ... # negative return code should be masked
69 69 ... runcommand(server, [b'id', b'-runknown'])
70 70 *** runcommand
71 71 Mercurial Distributed SCM
72 72
73 73 basic commands:
74 74
75 75 add add the specified files on the next commit
76 76 annotate show changeset information by line for each file
77 77 clone make a copy of an existing repository
78 78 commit commit the specified files or all outstanding changes
79 79 diff diff repository (or selected files)
80 80 export dump the header and diffs for one or more changesets
81 81 forget forget the specified files on the next commit
82 82 init create a new repository in the given directory
83 83 log show revision history of entire repository or files
84 84 merge merge another revision into working directory
85 85 pull pull changes from the specified source
86 86 push push changes to the specified destination
87 87 remove remove the specified files on the next commit
88 88 serve start stand-alone webserver
89 89 status show changed files in the working directory
90 90 summary summarize working directory state
91 91 update update working directory (or switch revisions)
92 92
93 93 (use 'hg help' for the full list of commands or 'hg -v' for details)
94 94 *** runcommand id --quiet
95 95 000000000000
96 96 *** runcommand id
97 97 000000000000 tip
98 98 *** runcommand id --config ui.quiet=True
99 99 000000000000
100 100 *** runcommand id
101 101 000000000000 tip
102 102 *** runcommand id -runknown
103 103 abort: unknown revision 'unknown'
104 104 [255]
105 105
106 106 >>> from hgclient import bprint, check, readchannel
107 107 >>> @check
108 108 ... def inputeof(server):
109 109 ... readchannel(server)
110 110 ... server.stdin.write(b'runcommand\n')
111 111 ... # close stdin while server is waiting for input
112 112 ... server.stdin.close()
113 113 ...
114 114 ... # server exits with 1 if the pipe closed while reading the command
115 115 ... bprint(b'server exit code =', b'%d' % server.wait())
116 116 server exit code = 1
117 117
118 118 >>> from hgclient import check, readchannel, runcommand, stringio
119 119 >>> @check
120 120 ... def serverinput(server):
121 121 ... readchannel(server)
122 122 ...
123 123 ... patch = b"""
124 124 ... # HG changeset patch
125 125 ... # User test
126 126 ... # Date 0 0
127 127 ... # Node ID c103a3dec114d882c98382d684d8af798d09d857
128 128 ... # Parent 0000000000000000000000000000000000000000
129 129 ... 1
130 130 ...
131 131 ... diff -r 000000000000 -r c103a3dec114 a
132 132 ... --- /dev/null Thu Jan 01 00:00:00 1970 +0000
133 133 ... +++ b/a Thu Jan 01 00:00:00 1970 +0000
134 134 ... @@ -0,0 +1,1 @@
135 135 ... +1
136 136 ... """
137 137 ...
138 138 ... runcommand(server, [b'import', b'-'], input=stringio(patch))
139 139 ... runcommand(server, [b'log'])
140 140 *** runcommand import -
141 141 applying patch from stdin
142 142 *** runcommand log
143 143 changeset: 0:eff892de26ec
144 144 tag: tip
145 145 user: test
146 146 date: Thu Jan 01 00:00:00 1970 +0000
147 147 summary: 1
148 148
149 149
150 150 check strict parsing of early options:
151 151
152 152 >>> import os
153 153 >>> from hgclient import check, readchannel, runcommand
154 154 >>> os.environ['HGPLAIN'] = '+strictflags'
155 155 >>> @check
156 156 ... def cwd(server):
157 157 ... readchannel(server)
158 158 ... runcommand(server, [b'log', b'-b', b'--config=alias.log=!echo pwned',
159 159 ... b'default'])
160 160 *** runcommand log -b --config=alias.log=!echo pwned default
161 161 abort: unknown revision '--config=alias.log=!echo pwned'
162 162 [255]
163 163
164 164 check that "histedit --commands=-" can read rules from the input channel:
165 165
166 166 >>> from hgclient import check, readchannel, runcommand, stringio
167 167 >>> @check
168 168 ... def serverinput(server):
169 169 ... readchannel(server)
170 170 ... rules = b'pick eff892de26ec\n'
171 171 ... runcommand(server, [b'histedit', b'0', b'--commands=-',
172 172 ... b'--config', b'extensions.histedit='],
173 173 ... input=stringio(rules))
174 174 *** runcommand histedit 0 --commands=- --config extensions.histedit=
175 175
176 176 check that --cwd doesn't persist between requests:
177 177
178 178 $ mkdir foo
179 179 $ touch foo/bar
180 180 >>> from hgclient import check, readchannel, runcommand
181 181 >>> @check
182 182 ... def cwd(server):
183 183 ... readchannel(server)
184 184 ... runcommand(server, [b'--cwd', b'foo', b'st', b'bar'])
185 185 ... runcommand(server, [b'st', b'foo/bar'])
186 186 *** runcommand --cwd foo st bar
187 187 ? bar
188 188 *** runcommand st foo/bar
189 189 ? foo/bar
190 190
191 191 $ rm foo/bar
192 192
193 193
194 194 check that local configs for the cached repo aren't inherited when -R is used:
195 195
196 196 $ cat <<EOF >> .hg/hgrc
197 197 > [ui]
198 198 > foo = bar
199 199 > EOF
200 200
201 201 #if no-extraextensions
202 202
203 203 >>> from hgclient import check, readchannel, runcommand, sep
204 204 >>> @check
205 205 ... def localhgrc(server):
206 206 ... readchannel(server)
207 207 ...
208 208 ... # the cached repo local hgrc contains ui.foo=bar, so showconfig should
209 209 ... # show it
210 210 ... runcommand(server, [b'showconfig'], outfilter=sep)
211 211 ...
212 212 ... # but not for this repo
213 213 ... runcommand(server, [b'init', b'foo'])
214 214 ... runcommand(server, [b'-R', b'foo', b'showconfig', b'ui', b'defaults'])
215 215 *** runcommand showconfig
216 216 bundle.mainreporoot=$TESTTMP/repo
217 217 chgserver.idletimeout=60
218 218 devel.all-warnings=true
219 219 devel.default-date=0 0
220 220 extensions.fsmonitor= (fsmonitor !)
221 221 format.exp-dirstate-v2=1 (dirstate-v2 !)
222 222 largefiles.usercache=$TESTTMP/.cache/largefiles
223 223 lfs.usercache=$TESTTMP/.cache/lfs
224 224 ui.slash=True
225 225 ui.interactive=False
226 226 ui.detailed-exit-code=True
227 227 ui.merge=internal:merge
228 228 ui.mergemarkers=detailed
229 ui.ssh=* (glob)
229 230 ui.timeout.warn=15
230 231 ui.foo=bar
231 232 ui.nontty=true
232 233 web.address=localhost
233 234 web\.ipv6=(?:True|False) (re)
234 235 web.server-header=testing stub value
235 236 *** runcommand init foo
236 237 *** runcommand -R foo showconfig ui defaults
237 238 ui.slash=True
238 239 ui.interactive=False
239 240 ui.detailed-exit-code=True
240 241 ui.merge=internal:merge
241 242 ui.mergemarkers=detailed
243 ui.ssh=* (glob)
242 244 ui.timeout.warn=15
243 245 ui.nontty=true
244 246 #endif
245 247
246 248 $ rm -R foo
247 249
248 250 #if windows
249 251 $ PYTHONPATH="$TESTTMP/repo;$PYTHONPATH"
250 252 #else
251 253 $ PYTHONPATH="$TESTTMP/repo:$PYTHONPATH"
252 254 #endif
253 255
254 256 $ cat <<EOF > hook.py
255 257 > import sys
256 258 > from hgclient import bprint
257 259 > def hook(**args):
258 260 > bprint(b'hook talking')
259 261 > bprint(b'now try to read something: %r' % sys.stdin.read())
260 262 > EOF
261 263
262 264 >>> from hgclient import check, readchannel, runcommand, stringio
263 265 >>> @check
264 266 ... def hookoutput(server):
265 267 ... readchannel(server)
266 268 ... runcommand(server, [b'--config',
267 269 ... b'hooks.pre-identify=python:hook.hook',
268 270 ... b'id'],
269 271 ... input=stringio(b'some input'))
270 272 *** runcommand --config hooks.pre-identify=python:hook.hook id
271 273 eff892de26ec tip
272 274 hook talking
273 275 now try to read something: ''
274 276
275 277 Clean hook cached version
276 278 $ rm hook.py*
277 279 $ rm -Rf __pycache__
278 280
279 281 $ echo a >> a
280 282 >>> import os
281 283 >>> from hgclient import check, readchannel, runcommand
282 284 >>> @check
283 285 ... def outsidechanges(server):
284 286 ... readchannel(server)
285 287 ... runcommand(server, [b'status'])
286 288 ... os.system('hg ci -Am2')
287 289 ... runcommand(server, [b'tip'])
288 290 ... runcommand(server, [b'status'])
289 291 *** runcommand status
290 292 M a
291 293 *** runcommand tip
292 294 changeset: 1:d3a0a68be6de
293 295 tag: tip
294 296 user: test
295 297 date: Thu Jan 01 00:00:00 1970 +0000
296 298 summary: 2
297 299
298 300 *** runcommand status
299 301
300 302 >>> import os
301 303 >>> from hgclient import bprint, check, readchannel, runcommand
302 304 >>> @check
303 305 ... def bookmarks(server):
304 306 ... readchannel(server)
305 307 ... runcommand(server, [b'bookmarks'])
306 308 ...
307 309 ... # changes .hg/bookmarks
308 310 ... os.system('hg bookmark -i bm1')
309 311 ... os.system('hg bookmark -i bm2')
310 312 ... runcommand(server, [b'bookmarks'])
311 313 ...
312 314 ... # changes .hg/bookmarks.current
313 315 ... os.system('hg upd bm1 -q')
314 316 ... runcommand(server, [b'bookmarks'])
315 317 ...
316 318 ... runcommand(server, [b'bookmarks', b'bm3'])
317 319 ... f = open('a', 'ab')
318 320 ... f.write(b'a\n') and None
319 321 ... f.close()
320 322 ... runcommand(server, [b'commit', b'-Amm'])
321 323 ... runcommand(server, [b'bookmarks'])
322 324 ... bprint(b'')
323 325 *** runcommand bookmarks
324 326 no bookmarks set
325 327 *** runcommand bookmarks
326 328 bm1 1:d3a0a68be6de
327 329 bm2 1:d3a0a68be6de
328 330 *** runcommand bookmarks
329 331 * bm1 1:d3a0a68be6de
330 332 bm2 1:d3a0a68be6de
331 333 *** runcommand bookmarks bm3
332 334 *** runcommand commit -Amm
333 335 *** runcommand bookmarks
334 336 bm1 1:d3a0a68be6de
335 337 bm2 1:d3a0a68be6de
336 338 * bm3 2:aef17e88f5f0
337 339
338 340
339 341 >>> import os
340 342 >>> from hgclient import check, readchannel, runcommand
341 343 >>> @check
342 344 ... def tagscache(server):
343 345 ... readchannel(server)
344 346 ... runcommand(server, [b'id', b'-t', b'-r', b'0'])
345 347 ... os.system('hg tag -r 0 foo')
346 348 ... runcommand(server, [b'id', b'-t', b'-r', b'0'])
347 349 *** runcommand id -t -r 0
348 350
349 351 *** runcommand id -t -r 0
350 352 foo
351 353
352 354 >>> import os
353 355 >>> from hgclient import check, readchannel, runcommand
354 356 >>> @check
355 357 ... def setphase(server):
356 358 ... readchannel(server)
357 359 ... runcommand(server, [b'phase', b'-r', b'.'])
358 360 ... os.system('hg phase -r . -p')
359 361 ... runcommand(server, [b'phase', b'-r', b'.'])
360 362 *** runcommand phase -r .
361 363 3: draft
362 364 *** runcommand phase -r .
363 365 3: public
364 366
365 367 $ echo a >> a
366 368 >>> from hgclient import bprint, check, readchannel, runcommand
367 369 >>> @check
368 370 ... def rollback(server):
369 371 ... readchannel(server)
370 372 ... runcommand(server, [b'phase', b'-r', b'.', b'-p'])
371 373 ... runcommand(server, [b'commit', b'-Am.'])
372 374 ... runcommand(server, [b'rollback'])
373 375 ... runcommand(server, [b'phase', b'-r', b'.'])
374 376 ... bprint(b'')
375 377 *** runcommand phase -r . -p
376 378 no phases changed
377 379 *** runcommand commit -Am.
378 380 *** runcommand rollback
379 381 repository tip rolled back to revision 3 (undo commit)
380 382 working directory now based on revision 3
381 383 *** runcommand phase -r .
382 384 3: public
383 385
384 386
385 387 >>> import os
386 388 >>> from hgclient import check, readchannel, runcommand
387 389 >>> @check
388 390 ... def branch(server):
389 391 ... readchannel(server)
390 392 ... runcommand(server, [b'branch'])
391 393 ... os.system('hg branch foo')
392 394 ... runcommand(server, [b'branch'])
393 395 ... os.system('hg branch default')
394 396 *** runcommand branch
395 397 default
396 398 marked working directory as branch foo
397 399 (branches are permanent and global, did you want a bookmark?)
398 400 *** runcommand branch
399 401 foo
400 402 marked working directory as branch default
401 403 (branches are permanent and global, did you want a bookmark?)
402 404
403 405 $ touch .hgignore
404 406 >>> import os
405 407 >>> from hgclient import bprint, check, readchannel, runcommand
406 408 >>> @check
407 409 ... def hgignore(server):
408 410 ... readchannel(server)
409 411 ... runcommand(server, [b'commit', b'-Am.'])
410 412 ... f = open('ignored-file', 'ab')
411 413 ... f.write(b'') and None
412 414 ... f.close()
413 415 ... f = open('.hgignore', 'ab')
414 416 ... f.write(b'ignored-file')
415 417 ... f.close()
416 418 ... runcommand(server, [b'status', b'-i', b'-u'])
417 419 ... bprint(b'')
418 420 *** runcommand commit -Am.
419 421 adding .hgignore
420 422 *** runcommand status -i -u
421 423 I ignored-file
422 424
423 425
424 426 cache of non-public revisions should be invalidated on repository change
425 427 (issue4855):
426 428
427 429 >>> import os
428 430 >>> from hgclient import bprint, check, readchannel, runcommand
429 431 >>> @check
430 432 ... def phasesetscacheaftercommit(server):
431 433 ... readchannel(server)
432 434 ... # load _phasecache._phaserevs and _phasesets
433 435 ... runcommand(server, [b'log', b'-qr', b'draft()'])
434 436 ... # create draft commits by another process
435 437 ... for i in range(5, 7):
436 438 ... f = open('a', 'ab')
437 439 ... f.seek(0, os.SEEK_END)
438 440 ... f.write(b'a\n') and None
439 441 ... f.close()
440 442 ... os.system('hg commit -Aqm%d' % i)
441 443 ... # new commits should be listed as draft revisions
442 444 ... runcommand(server, [b'log', b'-qr', b'draft()'])
443 445 ... bprint(b'')
444 446 *** runcommand log -qr draft()
445 447 4:7966c8e3734d
446 448 *** runcommand log -qr draft()
447 449 4:7966c8e3734d
448 450 5:41f6602d1c4f
449 451 6:10501e202c35
450 452
451 453
452 454 >>> import os
453 455 >>> from hgclient import bprint, check, readchannel, runcommand
454 456 >>> @check
455 457 ... def phasesetscacheafterstrip(server):
456 458 ... readchannel(server)
457 459 ... # load _phasecache._phaserevs and _phasesets
458 460 ... runcommand(server, [b'log', b'-qr', b'draft()'])
459 461 ... # strip cached revisions by another process
460 462 ... os.system('hg --config extensions.strip= strip -q 5')
461 463 ... # shouldn't abort by "unknown revision '6'"
462 464 ... runcommand(server, [b'log', b'-qr', b'draft()'])
463 465 ... bprint(b'')
464 466 *** runcommand log -qr draft()
465 467 4:7966c8e3734d
466 468 5:41f6602d1c4f
467 469 6:10501e202c35
468 470 *** runcommand log -qr draft()
469 471 4:7966c8e3734d
470 472
471 473
472 474 cache of phase roots should be invalidated on strip (issue3827):
473 475
474 476 >>> import os
475 477 >>> from hgclient import check, readchannel, runcommand, sep
476 478 >>> @check
477 479 ... def phasecacheafterstrip(server):
478 480 ... readchannel(server)
479 481 ...
480 482 ... # create new head, 5:731265503d86
481 483 ... runcommand(server, [b'update', b'-C', b'0'])
482 484 ... f = open('a', 'ab')
483 485 ... f.write(b'a\n') and None
484 486 ... f.close()
485 487 ... runcommand(server, [b'commit', b'-Am.', b'a'])
486 488 ... runcommand(server, [b'log', b'-Gq'])
487 489 ...
488 490 ... # make it public; draft marker moves to 4:7966c8e3734d
489 491 ... runcommand(server, [b'phase', b'-p', b'.'])
490 492 ... # load _phasecache.phaseroots
491 493 ... runcommand(server, [b'phase', b'.'], outfilter=sep)
492 494 ...
493 495 ... # strip 1::4 outside server
494 496 ... os.system('hg -q --config extensions.mq= strip 1')
495 497 ...
496 498 ... # shouldn't raise "7966c8e3734d: no node!"
497 499 ... runcommand(server, [b'branches'])
498 500 *** runcommand update -C 0
499 501 1 files updated, 0 files merged, 2 files removed, 0 files unresolved
500 502 (leaving bookmark bm3)
501 503 *** runcommand commit -Am. a
502 504 created new head
503 505 *** runcommand log -Gq
504 506 @ 5:731265503d86
505 507 |
506 508 | o 4:7966c8e3734d
507 509 | |
508 510 | o 3:b9b85890c400
509 511 | |
510 512 | o 2:aef17e88f5f0
511 513 | |
512 514 | o 1:d3a0a68be6de
513 515 |/
514 516 o 0:eff892de26ec
515 517
516 518 *** runcommand phase -p .
517 519 *** runcommand phase .
518 520 5: public
519 521 *** runcommand branches
520 522 default 1:731265503d86
521 523
522 524 in-memory cache must be reloaded if transaction is aborted. otherwise
523 525 changelog and manifest would have invalid node:
524 526
525 527 $ echo a >> a
526 528 >>> from hgclient import check, readchannel, runcommand
527 529 >>> @check
528 530 ... def txabort(server):
529 531 ... readchannel(server)
530 532 ... runcommand(server, [b'commit', b'--config', b'hooks.pretxncommit=false',
531 533 ... b'-mfoo'])
532 534 ... runcommand(server, [b'verify'])
533 535 *** runcommand commit --config hooks.pretxncommit=false -mfoo
534 536 transaction abort!
535 537 rollback completed
536 538 abort: pretxncommit hook exited with status 1
537 539 [40]
538 540 *** runcommand verify
539 541 checking changesets
540 542 checking manifests
541 543 crosschecking files in changesets and manifests
542 544 checking files
543 545 checked 2 changesets with 2 changes to 1 files
544 546 $ hg revert --no-backup -aq
545 547
546 548 $ cat >> .hg/hgrc << EOF
547 549 > [experimental]
548 550 > evolution.createmarkers=True
549 551 > EOF
550 552
551 553 >>> import os
552 554 >>> from hgclient import check, readchannel, runcommand
553 555 >>> @check
554 556 ... def obsolete(server):
555 557 ... readchannel(server)
556 558 ...
557 559 ... runcommand(server, [b'up', b'null'])
558 560 ... runcommand(server, [b'phase', b'-df', b'tip'])
559 561 ... cmd = 'hg debugobsolete `hg log -r tip --template {node}`'
560 562 ... if os.name == 'nt':
561 563 ... cmd = 'sh -c "%s"' % cmd # run in sh, not cmd.exe
562 564 ... os.system(cmd)
563 565 ... runcommand(server, [b'log', b'--hidden'])
564 566 ... runcommand(server, [b'log'])
565 567 *** runcommand up null
566 568 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
567 569 *** runcommand phase -df tip
568 570 1 new obsolescence markers
569 571 obsoleted 1 changesets
570 572 *** runcommand log --hidden
571 573 changeset: 1:731265503d86
572 574 tag: tip
573 575 user: test
574 576 date: Thu Jan 01 00:00:00 1970 +0000
575 577 obsolete: pruned
576 578 summary: .
577 579
578 580 changeset: 0:eff892de26ec
579 581 bookmark: bm1
580 582 bookmark: bm2
581 583 bookmark: bm3
582 584 user: test
583 585 date: Thu Jan 01 00:00:00 1970 +0000
584 586 summary: 1
585 587
586 588 *** runcommand log
587 589 changeset: 0:eff892de26ec
588 590 bookmark: bm1
589 591 bookmark: bm2
590 592 bookmark: bm3
591 593 tag: tip
592 594 user: test
593 595 date: Thu Jan 01 00:00:00 1970 +0000
594 596 summary: 1
595 597
596 598
597 599 $ cat <<EOF >> .hg/hgrc
598 600 > [extensions]
599 601 > mq =
600 602 > EOF
601 603
602 604 >>> import os
603 605 >>> from hgclient import check, readchannel, runcommand
604 606 >>> @check
605 607 ... def mqoutsidechanges(server):
606 608 ... readchannel(server)
607 609 ...
608 610 ... # load repo.mq
609 611 ... runcommand(server, [b'qapplied'])
610 612 ... os.system('hg qnew 0.diff')
611 613 ... # repo.mq should be invalidated
612 614 ... runcommand(server, [b'qapplied'])
613 615 ...
614 616 ... runcommand(server, [b'qpop', b'--all'])
615 617 ... os.system('hg qqueue --create foo')
616 618 ... # repo.mq should be recreated to point to new queue
617 619 ... runcommand(server, [b'qqueue', b'--active'])
618 620 *** runcommand qapplied
619 621 *** runcommand qapplied
620 622 0.diff
621 623 *** runcommand qpop --all
622 624 popping 0.diff
623 625 patch queue now empty
624 626 *** runcommand qqueue --active
625 627 foo
626 628
627 629 $ cat <<'EOF' > ../dbgui.py
628 630 > import os
629 631 > import sys
630 632 > from mercurial import commands, registrar
631 633 > cmdtable = {}
632 634 > command = registrar.command(cmdtable)
633 635 > @command(b"debuggetpass", norepo=True)
634 636 > def debuggetpass(ui):
635 637 > ui.write(b"%s\n" % ui.getpass())
636 638 > @command(b"debugprompt", norepo=True)
637 639 > def debugprompt(ui):
638 640 > ui.write(b"%s\n" % ui.prompt(b"prompt:"))
639 641 > @command(b"debugpromptchoice", norepo=True)
640 642 > def debugpromptchoice(ui):
641 643 > msg = b"promptchoice (y/n)? $$ &Yes $$ &No"
642 644 > ui.write(b"%d\n" % ui.promptchoice(msg))
643 645 > @command(b"debugreadstdin", norepo=True)
644 646 > def debugreadstdin(ui):
645 647 > ui.write(b"read: %r\n" % sys.stdin.read(1))
646 648 > @command(b"debugwritestdout", norepo=True)
647 649 > def debugwritestdout(ui):
648 650 > os.write(1, b"low-level stdout fd and\n")
649 651 > sys.stdout.write("stdout should be redirected to stderr\n")
650 652 > sys.stdout.flush()
651 653 > EOF
652 654 $ cat <<EOF >> .hg/hgrc
653 655 > [extensions]
654 656 > dbgui = ../dbgui.py
655 657 > EOF
656 658
657 659 >>> from hgclient import check, readchannel, runcommand, stringio
658 660 >>> @check
659 661 ... def getpass(server):
660 662 ... readchannel(server)
661 663 ... runcommand(server, [b'debuggetpass', b'--config',
662 664 ... b'ui.interactive=True'],
663 665 ... input=stringio(b'1234\n'))
664 666 ... runcommand(server, [b'debuggetpass', b'--config',
665 667 ... b'ui.interactive=True'],
666 668 ... input=stringio(b'\n'))
667 669 ... runcommand(server, [b'debuggetpass', b'--config',
668 670 ... b'ui.interactive=True'],
669 671 ... input=stringio(b''))
670 672 ... runcommand(server, [b'debugprompt', b'--config',
671 673 ... b'ui.interactive=True'],
672 674 ... input=stringio(b'5678\n'))
673 675 ... runcommand(server, [b'debugprompt', b'--config',
674 676 ... b'ui.interactive=True'],
675 677 ... input=stringio(b'\nremainder\nshould\nnot\nbe\nread\n'))
676 678 ... runcommand(server, [b'debugreadstdin'])
677 679 ... runcommand(server, [b'debugwritestdout'])
678 680 *** runcommand debuggetpass --config ui.interactive=True
679 681 password: 1234
680 682 *** runcommand debuggetpass --config ui.interactive=True
681 683 password:
682 684 *** runcommand debuggetpass --config ui.interactive=True
683 685 password: abort: response expected
684 686 [255]
685 687 *** runcommand debugprompt --config ui.interactive=True
686 688 prompt: 5678
687 689 *** runcommand debugprompt --config ui.interactive=True
688 690 prompt: y
689 691 *** runcommand debugreadstdin
690 692 read: ''
691 693 *** runcommand debugwritestdout
692 694 low-level stdout fd and
693 695 stdout should be redirected to stderr
694 696
695 697
696 698 run commandserver in commandserver, which is silly but should work:
697 699
698 700 >>> from hgclient import bprint, check, readchannel, runcommand, stringio
699 701 >>> @check
700 702 ... def nested(server):
701 703 ... bprint(b'%c, %r' % readchannel(server))
702 704 ... class nestedserver(object):
703 705 ... stdin = stringio(b'getencoding\n')
704 706 ... stdout = stringio()
705 707 ... runcommand(server, [b'serve', b'--cmdserver', b'pipe'],
706 708 ... output=nestedserver.stdout, input=nestedserver.stdin)
707 709 ... nestedserver.stdout.seek(0)
708 710 ... bprint(b'%c, %r' % readchannel(nestedserver)) # hello
709 711 ... bprint(b'%c, %r' % readchannel(nestedserver)) # getencoding
710 712 o, 'capabilities: getencoding runcommand\nencoding: *\npid: *' (glob)
711 713 *** runcommand serve --cmdserver pipe
712 714 o, 'capabilities: getencoding runcommand\nencoding: *\npid: *' (glob)
713 715 r, '*' (glob)
714 716
715 717
716 718 start without repository:
717 719
718 720 $ cd ..
719 721
720 722 >>> from hgclient import bprint, check, readchannel, runcommand
721 723 >>> @check
722 724 ... def hellomessage(server):
723 725 ... ch, data = readchannel(server)
724 726 ... bprint(b'%c, %r' % (ch, data))
725 727 ... # run an arbitrary command to make sure the next thing the server
726 728 ... # sends isn't part of the hello message
727 729 ... runcommand(server, [b'id'])
728 730 o, 'capabilities: getencoding runcommand\nencoding: *\npid: *' (glob)
729 731 *** runcommand id
730 732 abort: there is no Mercurial repository here (.hg not found)
731 733 [10]
732 734
733 735 >>> from hgclient import check, readchannel, runcommand
734 736 >>> @check
735 737 ... def startwithoutrepo(server):
736 738 ... readchannel(server)
737 739 ... runcommand(server, [b'init', b'repo2'])
738 740 ... runcommand(server, [b'id', b'-R', b'repo2'])
739 741 *** runcommand init repo2
740 742 *** runcommand id -R repo2
741 743 000000000000 tip
742 744
743 745
744 746 don't fall back to cwd if invalid -R path is specified (issue4805):
745 747
746 748 $ cd repo
747 749 $ hg serve --cmdserver pipe -R ../nonexistent
748 750 abort: repository ../nonexistent not found
749 751 [255]
750 752 $ cd ..
751 753
752 754
753 755 #if no-windows
754 756
755 757 option to not shutdown on SIGINT:
756 758
757 759 $ cat <<'EOF' > dbgint.py
758 760 > import os
759 761 > import signal
760 762 > import time
761 763 > from mercurial import commands, registrar
762 764 > cmdtable = {}
763 765 > command = registrar.command(cmdtable)
764 766 > @command(b"debugsleep", norepo=True)
765 767 > def debugsleep(ui):
766 768 > time.sleep(1)
767 769 > @command(b"debugsuicide", norepo=True)
768 770 > def debugsuicide(ui):
769 771 > os.kill(os.getpid(), signal.SIGINT)
770 772 > time.sleep(1)
771 773 > EOF
772 774
773 775 >>> import signal
774 776 >>> import time
775 777 >>> from hgclient import checkwith, readchannel, runcommand
776 778 >>> @checkwith(extraargs=[b'--config', b'cmdserver.shutdown-on-interrupt=False',
777 779 ... b'--config', b'extensions.dbgint=dbgint.py'])
778 780 ... def nointr(server):
779 781 ... readchannel(server)
780 782 ... server.send_signal(signal.SIGINT) # server won't be terminated
781 783 ... time.sleep(1)
782 784 ... runcommand(server, [b'debugsleep'])
783 785 ... server.send_signal(signal.SIGINT) # server won't be terminated
784 786 ... runcommand(server, [b'debugsleep'])
785 787 ... runcommand(server, [b'debugsuicide']) # command can be interrupted
786 788 ... server.send_signal(signal.SIGTERM) # server will be terminated
787 789 ... time.sleep(1)
788 790 *** runcommand debugsleep
789 791 *** runcommand debugsleep
790 792 *** runcommand debugsuicide
791 793 interrupted!
792 794 killed!
793 795 [255]
794 796
795 797 #endif
796 798
797 799
798 800 structured message channel:
799 801
800 802 $ cat <<'EOF' >> repo2/.hg/hgrc
801 803 > [ui]
802 804 > # server --config should precede repository option
803 805 > message-output = stdio
804 806 > EOF
805 807
806 808 >>> from hgclient import bprint, checkwith, readchannel, runcommand
807 809 >>> @checkwith(extraargs=[b'--config', b'ui.message-output=channel',
808 810 ... b'--config', b'cmdserver.message-encodings=foo cbor'])
809 811 ... def verify(server):
810 812 ... _ch, data = readchannel(server)
811 813 ... bprint(data)
812 814 ... runcommand(server, [b'-R', b'repo2', b'verify'])
813 815 capabilities: getencoding runcommand
814 816 encoding: ascii
815 817 message-encoding: cbor
816 818 pid: * (glob)
817 819 pgid: * (glob) (no-windows !)
818 820 *** runcommand -R repo2 verify
819 821 message: '\xa2DdataTchecking changesets\nDtypeFstatus'
820 822 message: '\xa6Ditem@Cpos\xf6EtopicHcheckingEtotal\xf6DtypeHprogressDunit@'
821 823 message: '\xa2DdataSchecking manifests\nDtypeFstatus'
822 824 message: '\xa6Ditem@Cpos\xf6EtopicHcheckingEtotal\xf6DtypeHprogressDunit@'
823 825 message: '\xa2DdataX0crosschecking files in changesets and manifests\nDtypeFstatus'
824 826 message: '\xa6Ditem@Cpos\xf6EtopicMcrosscheckingEtotal\xf6DtypeHprogressDunit@'
825 827 message: '\xa2DdataOchecking files\nDtypeFstatus'
826 828 message: '\xa6Ditem@Cpos\xf6EtopicHcheckingEtotal\xf6DtypeHprogressDunit@'
827 829 message: '\xa2DdataX/checked 0 changesets with 0 changes to 0 files\nDtypeFstatus'
828 830
829 831 >>> from hgclient import checkwith, readchannel, runcommand, stringio
830 832 >>> @checkwith(extraargs=[b'--config', b'ui.message-output=channel',
831 833 ... b'--config', b'cmdserver.message-encodings=cbor',
832 834 ... b'--config', b'extensions.dbgui=dbgui.py'])
833 835 ... def prompt(server):
834 836 ... readchannel(server)
835 837 ... interactive = [b'--config', b'ui.interactive=True']
836 838 ... runcommand(server, [b'debuggetpass'] + interactive,
837 839 ... input=stringio(b'1234\n'))
838 840 ... runcommand(server, [b'debugprompt'] + interactive,
839 841 ... input=stringio(b'5678\n'))
840 842 ... runcommand(server, [b'debugpromptchoice'] + interactive,
841 843 ... input=stringio(b'n\n'))
842 844 *** runcommand debuggetpass --config ui.interactive=True
843 845 message: '\xa3DdataJpassword: Hpassword\xf5DtypeFprompt'
844 846 1234
845 847 *** runcommand debugprompt --config ui.interactive=True
846 848 message: '\xa3DdataGprompt:GdefaultAyDtypeFprompt'
847 849 5678
848 850 *** runcommand debugpromptchoice --config ui.interactive=True
849 851 message: '\xa4Gchoices\x82\x82AyCYes\x82AnBNoDdataTpromptchoice (y/n)? GdefaultAyDtypeFprompt'
850 852 1
851 853
852 854 bad message encoding:
853 855
854 856 $ hg serve --cmdserver pipe --config ui.message-output=channel
855 857 abort: no supported message encodings:
856 858 [255]
857 859 $ hg serve --cmdserver pipe --config ui.message-output=channel \
858 860 > --config cmdserver.message-encodings='foo bar'
859 861 abort: no supported message encodings: foo bar
860 862 [255]
861 863
862 864 unix domain socket:
863 865
864 866 $ cd repo
865 867 $ hg update -q
866 868
867 869 #if unix-socket unix-permissions
868 870
869 871 >>> from hgclient import bprint, check, readchannel, runcommand, stringio, unixserver
870 872 >>> server = unixserver(b'.hg/server.sock', b'.hg/server.log')
871 873 >>> def hellomessage(conn):
872 874 ... ch, data = readchannel(conn)
873 875 ... bprint(b'%c, %r' % (ch, data))
874 876 ... runcommand(conn, [b'id'])
875 877 >>> check(hellomessage, server.connect)
876 878 o, 'capabilities: getencoding runcommand\nencoding: *\npid: *' (glob)
877 879 *** runcommand id
878 880 eff892de26ec tip bm1/bm2/bm3
879 881 >>> def unknowncommand(conn):
880 882 ... readchannel(conn)
881 883 ... conn.stdin.write(b'unknowncommand\n')
882 884 >>> check(unknowncommand, server.connect) # error sent to server.log
883 885 >>> def serverinput(conn):
884 886 ... readchannel(conn)
885 887 ... patch = b"""
886 888 ... # HG changeset patch
887 889 ... # User test
888 890 ... # Date 0 0
889 891 ... 2
890 892 ...
891 893 ... diff -r eff892de26ec -r 1ed24be7e7a0 a
892 894 ... --- a/a
893 895 ... +++ b/a
894 896 ... @@ -1,1 +1,2 @@
895 897 ... 1
896 898 ... +2
897 899 ... """
898 900 ... runcommand(conn, [b'import', b'-'], input=stringio(patch))
899 901 ... runcommand(conn, [b'log', b'-rtip', b'-q'])
900 902 >>> check(serverinput, server.connect)
901 903 *** runcommand import -
902 904 applying patch from stdin
903 905 *** runcommand log -rtip -q
904 906 2:1ed24be7e7a0
905 907 >>> server.shutdown()
906 908
907 909 $ cat .hg/server.log
908 910 listening at .hg/server.sock
909 911 abort: unknown command unknowncommand
910 912 killed!
911 913 $ rm .hg/server.log
912 914
913 915 if server crashed before hello, traceback will be sent to 'e' channel as
914 916 last ditch:
915 917
916 918 $ cat <<'EOF' > ../earlycrasher.py
917 919 > from mercurial import commandserver, extensions
918 920 > def _serverequest(orig, ui, repo, conn, createcmdserver, prereposetups):
919 921 > def createcmdserver(*args, **kwargs):
920 922 > raise Exception('crash')
921 923 > return orig(ui, repo, conn, createcmdserver, prereposetups)
922 924 > def extsetup(ui):
923 925 > extensions.wrapfunction(commandserver, b'_serverequest', _serverequest)
924 926 > EOF
925 927 $ cat <<EOF >> .hg/hgrc
926 928 > [extensions]
927 929 > earlycrasher = ../earlycrasher.py
928 930 > EOF
929 931 >>> from hgclient import bprint, check, readchannel, unixserver
930 932 >>> server = unixserver(b'.hg/server.sock', b'.hg/server.log')
931 933 >>> def earlycrash(conn):
932 934 ... while True:
933 935 ... try:
934 936 ... ch, data = readchannel(conn)
935 937 ... for l in data.splitlines(True):
936 938 ... if not l.startswith(b' '):
937 939 ... bprint(b'%c, %r' % (ch, l))
938 940 ... except EOFError:
939 941 ... break
940 942 >>> check(earlycrash, server.connect)
941 943 e, 'Traceback (most recent call last):\n'
942 944 e, 'Exception: crash\n'
943 945 >>> server.shutdown()
944 946
945 947 $ cat .hg/server.log | grep -v '^ '
946 948 listening at .hg/server.sock
947 949 Traceback (most recent call last):
948 950 Exception: crash
949 951 killed!
950 952 #endif
951 953 #if no-unix-socket
952 954
953 955 $ hg serve --cmdserver unix -a .hg/server.sock
954 956 abort: unsupported platform
955 957 [255]
956 958
957 959 #endif
958 960
959 961 $ cd ..
960 962
961 963 Test that accessing to invalid changelog cache is avoided at
962 964 subsequent operations even if repo object is reused even after failure
963 965 of transaction (see 0a7610758c42 also)
964 966
965 967 "hg log" after failure of transaction is needed to detect invalid
966 968 cache in repoview: this can't detect by "hg verify" only.
967 969
968 970 Combination of "finalization" and "empty-ness of changelog" (2 x 2 =
969 971 4) are tested, because '00changelog.i' are differently changed in each
970 972 cases.
971 973
972 974 $ cat > $TESTTMP/failafterfinalize.py <<EOF
973 975 > # extension to abort transaction after finalization forcibly
974 976 > from mercurial import commands, error, extensions, lock as lockmod
975 977 > from mercurial import registrar
976 978 > cmdtable = {}
977 979 > command = registrar.command(cmdtable)
978 980 > configtable = {}
979 981 > configitem = registrar.configitem(configtable)
980 982 > configitem(b'failafterfinalize', b'fail',
981 983 > default=None,
982 984 > )
983 985 > def fail(tr):
984 986 > raise error.Abort(b'fail after finalization')
985 987 > def reposetup(ui, repo):
986 988 > class failrepo(repo.__class__):
987 989 > def commitctx(self, ctx, error=False, origctx=None):
988 990 > if self.ui.configbool(b'failafterfinalize', b'fail'):
989 991 > # 'sorted()' by ASCII code on category names causes
990 992 > # invoking 'fail' after finalization of changelog
991 993 > # using "'cl-%i' % id(self)" as category name
992 994 > self.currenttransaction().addfinalize(b'zzzzzzzz', fail)
993 995 > return super(failrepo, self).commitctx(ctx, error, origctx)
994 996 > repo.__class__ = failrepo
995 997 > EOF
996 998
997 999 $ hg init repo3
998 1000 $ cd repo3
999 1001
1000 1002 $ cat <<EOF >> $HGRCPATH
1001 1003 > [command-templates]
1002 1004 > log = {rev} {desc|firstline} ({files})\n
1003 1005 >
1004 1006 > [extensions]
1005 1007 > failafterfinalize = $TESTTMP/failafterfinalize.py
1006 1008 > EOF
1007 1009
1008 1010 - test failure with "empty changelog"
1009 1011
1010 1012 $ echo foo > foo
1011 1013 $ hg add foo
1012 1014
1013 1015 (failure before finalization)
1014 1016
1015 1017 >>> from hgclient import check, readchannel, runcommand
1016 1018 >>> @check
1017 1019 ... def abort(server):
1018 1020 ... readchannel(server)
1019 1021 ... runcommand(server, [b'commit',
1020 1022 ... b'--config', b'hooks.pretxncommit=false',
1021 1023 ... b'-mfoo'])
1022 1024 ... runcommand(server, [b'log'])
1023 1025 ... runcommand(server, [b'verify', b'-q'])
1024 1026 *** runcommand commit --config hooks.pretxncommit=false -mfoo
1025 1027 transaction abort!
1026 1028 rollback completed
1027 1029 abort: pretxncommit hook exited with status 1
1028 1030 [40]
1029 1031 *** runcommand log
1030 1032 *** runcommand verify -q
1031 1033
1032 1034 (failure after finalization)
1033 1035
1034 1036 >>> from hgclient import check, readchannel, runcommand
1035 1037 >>> @check
1036 1038 ... def abort(server):
1037 1039 ... readchannel(server)
1038 1040 ... runcommand(server, [b'commit',
1039 1041 ... b'--config', b'failafterfinalize.fail=true',
1040 1042 ... b'-mfoo'])
1041 1043 ... runcommand(server, [b'log'])
1042 1044 ... runcommand(server, [b'verify', b'-q'])
1043 1045 *** runcommand commit --config failafterfinalize.fail=true -mfoo
1044 1046 transaction abort!
1045 1047 rollback completed
1046 1048 abort: fail after finalization
1047 1049 [255]
1048 1050 *** runcommand log
1049 1051 *** runcommand verify -q
1050 1052
1051 1053 - test failure with "not-empty changelog"
1052 1054
1053 1055 $ echo bar > bar
1054 1056 $ hg add bar
1055 1057 $ hg commit -mbar bar
1056 1058
1057 1059 (failure before finalization)
1058 1060
1059 1061 >>> from hgclient import check, readchannel, runcommand
1060 1062 >>> @check
1061 1063 ... def abort(server):
1062 1064 ... readchannel(server)
1063 1065 ... runcommand(server, [b'commit',
1064 1066 ... b'--config', b'hooks.pretxncommit=false',
1065 1067 ... b'-mfoo', b'foo'])
1066 1068 ... runcommand(server, [b'log'])
1067 1069 ... runcommand(server, [b'verify', b'-q'])
1068 1070 *** runcommand commit --config hooks.pretxncommit=false -mfoo foo
1069 1071 transaction abort!
1070 1072 rollback completed
1071 1073 abort: pretxncommit hook exited with status 1
1072 1074 [40]
1073 1075 *** runcommand log
1074 1076 0 bar (bar)
1075 1077 *** runcommand verify -q
1076 1078
1077 1079 (failure after finalization)
1078 1080
1079 1081 >>> from hgclient import check, readchannel, runcommand
1080 1082 >>> @check
1081 1083 ... def abort(server):
1082 1084 ... readchannel(server)
1083 1085 ... runcommand(server, [b'commit',
1084 1086 ... b'--config', b'failafterfinalize.fail=true',
1085 1087 ... b'-mfoo', b'foo'])
1086 1088 ... runcommand(server, [b'log'])
1087 1089 ... runcommand(server, [b'verify', b'-q'])
1088 1090 *** runcommand commit --config failafterfinalize.fail=true -mfoo foo
1089 1091 transaction abort!
1090 1092 rollback completed
1091 1093 abort: fail after finalization
1092 1094 [255]
1093 1095 *** runcommand log
1094 1096 0 bar (bar)
1095 1097 *** runcommand verify -q
1096 1098
1097 1099 $ cd ..
1098 1100
1099 1101 Test symlink traversal over cached audited paths:
1100 1102 -------------------------------------------------
1101 1103
1102 1104 #if symlink
1103 1105
1104 1106 set up symlink hell
1105 1107
1106 1108 $ mkdir merge-symlink-out
1107 1109 $ hg init merge-symlink
1108 1110 $ cd merge-symlink
1109 1111 $ touch base
1110 1112 $ hg commit -qAm base
1111 1113 $ ln -s ../merge-symlink-out a
1112 1114 $ hg commit -qAm 'symlink a -> ../merge-symlink-out'
1113 1115 $ hg up -q 0
1114 1116 $ mkdir a
1115 1117 $ touch a/poisoned
1116 1118 $ hg commit -qAm 'file a/poisoned'
1117 1119 $ hg log -G -T '{rev}: {desc}\n'
1118 1120 @ 2: file a/poisoned
1119 1121 |
1120 1122 | o 1: symlink a -> ../merge-symlink-out
1121 1123 |/
1122 1124 o 0: base
1123 1125
1124 1126
1125 1127 try trivial merge after update: cache of audited paths should be discarded,
1126 1128 and the merge should fail (issue5628)
1127 1129
1128 1130 $ hg up -q null
1129 1131 >>> from hgclient import check, readchannel, runcommand
1130 1132 >>> @check
1131 1133 ... def merge(server):
1132 1134 ... readchannel(server)
1133 1135 ... # audit a/poisoned as a good path
1134 1136 ... runcommand(server, [b'up', b'-qC', b'2'])
1135 1137 ... runcommand(server, [b'up', b'-qC', b'1'])
1136 1138 ... # here a is a symlink, so a/poisoned is bad
1137 1139 ... runcommand(server, [b'merge', b'2'])
1138 1140 *** runcommand up -qC 2
1139 1141 *** runcommand up -qC 1
1140 1142 *** runcommand merge 2
1141 1143 abort: path 'a/poisoned' traverses symbolic link 'a'
1142 1144 [255]
1143 1145 $ ls ../merge-symlink-out
1144 1146
1145 1147 cache of repo.auditor should be discarded, so matcher would never traverse
1146 1148 symlinks:
1147 1149
1148 1150 $ hg up -qC 0
1149 1151 $ touch ../merge-symlink-out/poisoned
1150 1152 >>> from hgclient import check, readchannel, runcommand
1151 1153 >>> @check
1152 1154 ... def files(server):
1153 1155 ... readchannel(server)
1154 1156 ... runcommand(server, [b'up', b'-qC', b'2'])
1155 1157 ... # audit a/poisoned as a good path
1156 1158 ... runcommand(server, [b'files', b'a/poisoned'])
1157 1159 ... runcommand(server, [b'up', b'-qC', b'0'])
1158 1160 ... runcommand(server, [b'up', b'-qC', b'1'])
1159 1161 ... # here 'a' is a symlink, so a/poisoned should be warned
1160 1162 ... runcommand(server, [b'files', b'a/poisoned'])
1161 1163 *** runcommand up -qC 2
1162 1164 *** runcommand files a/poisoned
1163 1165 a/poisoned
1164 1166 *** runcommand up -qC 0
1165 1167 *** runcommand up -qC 1
1166 1168 *** runcommand files a/poisoned
1167 1169 abort: path 'a/poisoned' traverses symbolic link 'a'
1168 1170 [255]
1169 1171
1170 1172 $ cd ..
1171 1173
1172 1174 #endif
@@ -1,546 +1,546 b''
1 1 Windows needs ';' as a file separator in an environment variable, and MSYS
2 2 doesn't automatically convert it in every case.
3 3
4 4 #if windows
5 5 $ path_list_var() {
6 6 > echo $1 | sed 's/:/;/'
7 7 > }
8 8 #else
9 9 $ path_list_var() {
10 10 > echo $1
11 11 > }
12 12 #endif
13 13
14 14
15 15 hide outer repo
16 16 $ hg init
17 17
18 18 Invalid syntax: no value
19 19
20 20 $ cat > .hg/hgrc << EOF
21 21 > novaluekey
22 22 > EOF
23 23 $ hg showconfig
24 24 config error at $TESTTMP/.hg/hgrc:1: novaluekey
25 25 [30]
26 26
27 27 Invalid syntax: no key
28 28
29 29 $ cat > .hg/hgrc << EOF
30 30 > =nokeyvalue
31 31 > EOF
32 32 $ hg showconfig
33 33 config error at $TESTTMP/.hg/hgrc:1: =nokeyvalue
34 34 [30]
35 35
36 36 Test hint about invalid syntax from leading white space
37 37
38 38 $ cat > .hg/hgrc << EOF
39 39 > key=value
40 40 > EOF
41 41 $ hg showconfig
42 42 config error at $TESTTMP/.hg/hgrc:1: unexpected leading whitespace: key=value
43 43 [30]
44 44
45 45 $ cat > .hg/hgrc << EOF
46 46 > [section]
47 47 > key=value
48 48 > EOF
49 49 $ hg showconfig
50 50 config error at $TESTTMP/.hg/hgrc:1: unexpected leading whitespace: [section]
51 51 [30]
52 52
53 53 Reset hgrc
54 54
55 55 $ echo > .hg/hgrc
56 56
57 57 Test case sensitive configuration
58 58
59 59 $ cat <<EOF >> $HGRCPATH
60 60 > [Section]
61 61 > KeY = Case Sensitive
62 62 > key = lower case
63 63 > EOF
64 64
65 65 $ hg showconfig Section
66 66 Section.KeY=Case Sensitive
67 67 Section.key=lower case
68 68
69 69 $ hg showconfig Section -Tjson
70 70 [
71 71 {
72 72 "defaultvalue": null,
73 73 "name": "Section.KeY",
74 74 "source": "*.hgrc:*", (glob)
75 75 "value": "Case Sensitive"
76 76 },
77 77 {
78 78 "defaultvalue": null,
79 79 "name": "Section.key",
80 80 "source": "*.hgrc:*", (glob)
81 81 "value": "lower case"
82 82 }
83 83 ]
84 84 $ hg showconfig Section.KeY -Tjson
85 85 [
86 86 {
87 87 "defaultvalue": null,
88 88 "name": "Section.KeY",
89 89 "source": "*.hgrc:*", (glob)
90 90 "value": "Case Sensitive"
91 91 }
92 92 ]
93 93 $ hg showconfig -Tjson | tail -7
94 94 {
95 95 "defaultvalue": null,
96 96 "name": "*", (glob)
97 97 "source": "*", (glob)
98 98 "value": "*" (glob)
99 99 }
100 100 ]
101 101
102 102 Test config default of various types:
103 103
104 104 {"defaultvalue": ""} for -T'json(defaultvalue)' looks weird, but that's
105 105 how the templater works. Unknown keywords are evaluated to "".
106 106
107 107 dynamicdefault
108 108
109 109 $ hg config --config alias.foo= alias -Tjson
110 110 [
111 111 {
112 112 "name": "alias.foo",
113 113 "source": "--config",
114 114 "value": ""
115 115 }
116 116 ]
117 117 $ hg config --config alias.foo= alias -T'json(defaultvalue)'
118 118 [
119 119 {"defaultvalue": ""}
120 120 ]
121 121 $ hg config --config alias.foo= alias -T'{defaultvalue}\n'
122 122
123 123
124 124 null
125 125
126 126 $ hg config --config auth.cookiefile= auth -Tjson
127 127 [
128 128 {
129 129 "defaultvalue": null,
130 130 "name": "auth.cookiefile",
131 131 "source": "--config",
132 132 "value": ""
133 133 }
134 134 ]
135 135 $ hg config --config auth.cookiefile= auth -T'json(defaultvalue)'
136 136 [
137 137 {"defaultvalue": null}
138 138 ]
139 139 $ hg config --config auth.cookiefile= auth -T'{defaultvalue}\n'
140 140
141 141
142 142 false
143 143
144 144 $ hg config --config commands.commit.post-status= commands -Tjson
145 145 [
146 146 {
147 147 "defaultvalue": false,
148 148 "name": "commands.commit.post-status",
149 149 "source": "--config",
150 150 "value": ""
151 151 }
152 152 ]
153 153 $ hg config --config commands.commit.post-status= commands -T'json(defaultvalue)'
154 154 [
155 155 {"defaultvalue": false}
156 156 ]
157 157 $ hg config --config commands.commit.post-status= commands -T'{defaultvalue}\n'
158 158 False
159 159
160 160 true
161 161
162 162 $ hg config --config format.dotencode= format.dotencode -Tjson
163 163 [
164 164 {
165 165 "defaultvalue": true,
166 166 "name": "format.dotencode",
167 167 "source": "--config",
168 168 "value": ""
169 169 }
170 170 ]
171 171 $ hg config --config format.dotencode= format.dotencode -T'json(defaultvalue)'
172 172 [
173 173 {"defaultvalue": true}
174 174 ]
175 175 $ hg config --config format.dotencode= format.dotencode -T'{defaultvalue}\n'
176 176 True
177 177
178 178 bytes
179 179
180 180 $ hg config --config commands.resolve.mark-check= commands -Tjson
181 181 [
182 182 {
183 183 "defaultvalue": "none",
184 184 "name": "commands.resolve.mark-check",
185 185 "source": "--config",
186 186 "value": ""
187 187 }
188 188 ]
189 189 $ hg config --config commands.resolve.mark-check= commands -T'json(defaultvalue)'
190 190 [
191 191 {"defaultvalue": "none"}
192 192 ]
193 193 $ hg config --config commands.resolve.mark-check= commands -T'{defaultvalue}\n'
194 194 none
195 195
196 196 empty list
197 197
198 198 $ hg config --config commands.show.aliasprefix= commands -Tjson
199 199 [
200 200 {
201 201 "defaultvalue": [],
202 202 "name": "commands.show.aliasprefix",
203 203 "source": "--config",
204 204 "value": ""
205 205 }
206 206 ]
207 207 $ hg config --config commands.show.aliasprefix= commands -T'json(defaultvalue)'
208 208 [
209 209 {"defaultvalue": []}
210 210 ]
211 211 $ hg config --config commands.show.aliasprefix= commands -T'{defaultvalue}\n'
212 212
213 213
214 214 nonempty list
215 215
216 216 $ hg config --config progress.format= progress -Tjson
217 217 [
218 218 {
219 219 "defaultvalue": ["topic", "bar", "number", "estimate"],
220 220 "name": "progress.format",
221 221 "source": "--config",
222 222 "value": ""
223 223 }
224 224 ]
225 225 $ hg config --config progress.format= progress -T'json(defaultvalue)'
226 226 [
227 227 {"defaultvalue": ["topic", "bar", "number", "estimate"]}
228 228 ]
229 229 $ hg config --config progress.format= progress -T'{defaultvalue}\n'
230 230 topic bar number estimate
231 231
232 232 int
233 233
234 234 $ hg config --config profiling.freq= profiling -Tjson
235 235 [
236 236 {
237 237 "defaultvalue": 1000,
238 238 "name": "profiling.freq",
239 239 "source": "--config",
240 240 "value": ""
241 241 }
242 242 ]
243 243 $ hg config --config profiling.freq= profiling -T'json(defaultvalue)'
244 244 [
245 245 {"defaultvalue": 1000}
246 246 ]
247 247 $ hg config --config profiling.freq= profiling -T'{defaultvalue}\n'
248 248 1000
249 249
250 250 float
251 251
252 252 $ hg config --config profiling.showmax= profiling -Tjson
253 253 [
254 254 {
255 255 "defaultvalue": 0.999,
256 256 "name": "profiling.showmax",
257 257 "source": "--config",
258 258 "value": ""
259 259 }
260 260 ]
261 261 $ hg config --config profiling.showmax= profiling -T'json(defaultvalue)'
262 262 [
263 263 {"defaultvalue": 0.999}
264 264 ]
265 265 $ hg config --config profiling.showmax= profiling -T'{defaultvalue}\n'
266 266 0.999
267 267
268 268 Test empty config source:
269 269
270 270 $ cat <<EOF > emptysource.py
271 271 > def reposetup(ui, repo):
272 272 > ui.setconfig(b'empty', b'source', b'value')
273 273 > EOF
274 274 $ cp .hg/hgrc .hg/hgrc.orig
275 275 $ cat <<EOF >> .hg/hgrc
276 276 > [extensions]
277 277 > emptysource = `pwd`/emptysource.py
278 278 > EOF
279 279
280 280 $ hg config --source empty.source
281 281 none: value
282 282 $ hg config empty.source -Tjson
283 283 [
284 284 {
285 285 "defaultvalue": null,
286 286 "name": "empty.source",
287 287 "source": "",
288 288 "value": "value"
289 289 }
290 290 ]
291 291
292 292 $ cp .hg/hgrc.orig .hg/hgrc
293 293
294 294 Test "%unset"
295 295
296 296 $ cat >> $HGRCPATH <<EOF
297 297 > [unsettest]
298 298 > local-hgrcpath = should be unset (HGRCPATH)
299 299 > %unset local-hgrcpath
300 300 >
301 301 > global = should be unset (HGRCPATH)
302 302 >
303 303 > both = should be unset (HGRCPATH)
304 304 >
305 305 > set-after-unset = should be unset (HGRCPATH)
306 306 > EOF
307 307
308 308 $ cat >> .hg/hgrc <<EOF
309 309 > [unsettest]
310 310 > local-hgrc = should be unset (.hg/hgrc)
311 311 > %unset local-hgrc
312 312 >
313 313 > %unset global
314 314 >
315 315 > both = should be unset (.hg/hgrc)
316 316 > %unset both
317 317 >
318 318 > set-after-unset = should be unset (.hg/hgrc)
319 319 > %unset set-after-unset
320 320 > set-after-unset = should be set (.hg/hgrc)
321 321 > EOF
322 322
323 323 $ hg showconfig unsettest
324 324 unsettest.set-after-unset=should be set (.hg/hgrc)
325 325
326 326 Test exit code when no config matches
327 327
328 328 $ hg config Section.idontexist
329 329 [1]
330 330
331 331 sub-options in [paths] aren't expanded
332 332
333 333 $ cat > .hg/hgrc << EOF
334 334 > [paths]
335 335 > foo = ~/foo
336 336 > foo:suboption = ~/foo
337 337 > EOF
338 338
339 339 $ hg showconfig paths
340 340 paths.foo=~/foo
341 341 paths.foo:suboption=~/foo
342 342
343 343 note: The path expansion no longer happens at the config level, but the path is
344 344 still expanded:
345 345
346 346 $ hg path | grep foo
347 347 foo = $TESTTMP/foo
348 348
349 349 edit failure
350 350
351 351 $ HGEDITOR=false hg config --edit
352 352 abort: edit failed: false exited with status 1
353 353 [10]
354 354
355 355 config affected by environment variables
356 356
357 357 $ EDITOR=e1 VISUAL=e2 hg config --source | grep 'ui\.editor'
358 358 $VISUAL: ui.editor=e2
359 359
360 360 $ VISUAL=e2 hg config --source --config ui.editor=e3 | grep 'ui\.editor'
361 361 --config: ui.editor=e3
362 362
363 363 $ PAGER=p1 hg config --source | grep 'pager\.pager'
364 364 $PAGER: pager.pager=p1
365 365
366 366 $ PAGER=p1 hg config --source --config pager.pager=p2 | grep 'pager\.pager'
367 367 --config: pager.pager=p2
368 368
369 369 verify that aliases are evaluated as well
370 370
371 371 $ hg init aliastest
372 372 $ cd aliastest
373 373 $ cat > .hg/hgrc << EOF
374 374 > [ui]
375 375 > user = repo user
376 376 > EOF
377 377 $ touch index
378 378 $ unset HGUSER
379 379 $ hg ci -Am test
380 380 adding index
381 381 $ hg log --template '{author}\n'
382 382 repo user
383 383 $ cd ..
384 384
385 385 alias has lower priority
386 386
387 387 $ hg init aliaspriority
388 388 $ cd aliaspriority
389 389 $ cat > .hg/hgrc << EOF
390 390 > [ui]
391 391 > user = alias user
392 392 > username = repo user
393 393 > EOF
394 394 $ touch index
395 395 $ unset HGUSER
396 396 $ hg ci -Am test
397 397 adding index
398 398 $ hg log --template '{author}\n'
399 399 repo user
400 400 $ cd ..
401 401
402 402 configs should be read in lexicographical order
403 403
404 404 $ mkdir configs
405 405 $ for i in `$TESTDIR/seq.py 10 99`; do
406 406 > printf "[section]\nkey=$i" > configs/$i.rc
407 407 > done
408 408 $ HGRCPATH=configs hg config section.key
409 409 99
410 410
411 411 Listing all config options
412 412 ==========================
413 413
414 414 The feature is experimental and behavior may varies. This test exists to make sure the code is run. We grep it to avoid too much variability in its current experimental state.
415 415
416 $ hg config --exp-all-known | grep commit
416 $ hg config --exp-all-known | grep commit | grep -v ssh
417 417 commands.commit.interactive.git=False
418 418 commands.commit.interactive.ignoreblanklines=False
419 419 commands.commit.interactive.ignorews=False
420 420 commands.commit.interactive.ignorewsamount=False
421 421 commands.commit.interactive.ignorewseol=False
422 422 commands.commit.interactive.nobinary=False
423 423 commands.commit.interactive.nodates=False
424 424 commands.commit.interactive.noprefix=False
425 425 commands.commit.interactive.showfunc=False
426 426 commands.commit.interactive.unified=None
427 427 commands.commit.interactive.word-diff=False
428 428 commands.commit.post-status=False
429 429 convert.git.committeractions=[*'messagedifferent'] (glob)
430 430 convert.svn.dangerous-set-commit-dates=False
431 431 experimental.copytrace.sourcecommitlimit=100
432 432 phases.new-commit=draft
433 433 ui.allowemptycommit=False
434 434 ui.commitsubrepos=False
435 435
436 436
437 437 Configuration priority
438 438 ======================
439 439
440 440 setup necessary file
441 441
442 442 $ cat > file-A.rc << EOF
443 443 > [config-test]
444 444 > basic = value-A
445 445 > pre-include= value-A
446 446 > %include ./included.rc
447 447 > post-include= value-A
448 448 > [command-templates]
449 449 > log = "value-A\n"
450 450 > EOF
451 451
452 452 $ cat > file-B.rc << EOF
453 453 > [config-test]
454 454 > basic = value-B
455 455 > [ui]
456 456 > logtemplate = "value-B\n"
457 457 > EOF
458 458
459 459
460 460 $ cat > included.rc << EOF
461 461 > [config-test]
462 462 > pre-include= value-included
463 463 > post-include= value-included
464 464 > EOF
465 465
466 466 $ cat > file-C.rc << EOF
467 467 > %include ./included-alias-C.rc
468 468 > [ui]
469 469 > logtemplate = "value-C\n"
470 470 > EOF
471 471
472 472 $ cat > included-alias-C.rc << EOF
473 473 > [command-templates]
474 474 > log = "value-included\n"
475 475 > EOF
476 476
477 477
478 478 $ cat > file-D.rc << EOF
479 479 > [command-templates]
480 480 > log = "value-D\n"
481 481 > %include ./included-alias-D.rc
482 482 > EOF
483 483
484 484 $ cat > included-alias-D.rc << EOF
485 485 > [ui]
486 486 > logtemplate = "value-included\n"
487 487 > EOF
488 488
489 489 Simple order checking
490 490 ---------------------
491 491
492 492 If file B is read after file A, value from B overwrite value from A.
493 493
494 494 $ HGRCPATH=`path_list_var "file-A.rc:file-B.rc"` hg config config-test.basic
495 495 value-B
496 496
497 497 Ordering from include
498 498 ---------------------
499 499
500 500 value from an include overwrite value defined before the include, but not the one defined after the include
501 501
502 502 $ HGRCPATH="file-A.rc" hg config config-test.pre-include
503 503 value-included
504 504 $ HGRCPATH="file-A.rc" hg config config-test.post-include
505 505 value-A
506 506
507 507 command line override
508 508 ---------------------
509 509
510 510 $ HGRCPATH=`path_list_var "file-A.rc:file-B.rc"` hg config config-test.basic --config config-test.basic=value-CLI
511 511 value-CLI
512 512
513 513 Alias ordering
514 514 --------------
515 515
516 516 The official config is now `command-templates.log`, the historical
517 517 `ui.logtemplate` is a valid alternative for it.
518 518
519 519 When both are defined, The config value read the last "win", this should keep
520 520 being true if the config have other alias. In other word, the config value read
521 521 earlier will be considered "lower level" and the config read later would be
522 522 considered "higher level". And higher level values wins.
523 523
524 524 $ HGRCPATH="file-A.rc" hg log -r .
525 525 value-A
526 526 $ HGRCPATH="file-B.rc" hg log -r .
527 527 value-B
528 528 $ HGRCPATH=`path_list_var "file-A.rc:file-B.rc"` hg log -r .
529 529 value-B
530 530
531 531 Alias and include
532 532 -----------------
533 533
534 534 The pre/post include priority should also apply when tie-breaking alternatives.
535 535 See the case above for details about the two config options used.
536 536
537 537 $ HGRCPATH="file-C.rc" hg log -r .
538 538 value-C
539 539 $ HGRCPATH="file-D.rc" hg log -r .
540 540 value-included
541 541
542 542 command line override
543 543 ---------------------
544 544
545 545 $ HGRCPATH=`path_list_var "file-A.rc:file-B.rc"` hg log -r . --config ui.logtemplate="value-CLI\n"
546 546 value-CLI
@@ -1,618 +1,618 b''
1 1 This test is a duplicate of 'test-http.t' feel free to factor out
2 2 parts that are not bundle1/bundle2 specific.
3 3
4 4 #testcases sshv1 sshv2
5 5
6 6 #if sshv2
7 7 $ cat >> $HGRCPATH << EOF
8 8 > [experimental]
9 9 > sshpeer.advertise-v2 = true
10 10 > sshserver.support-v2 = true
11 11 > EOF
12 12 #endif
13 13
14 14 $ cat << EOF >> $HGRCPATH
15 15 > [devel]
16 16 > # This test is dedicated to interaction through old bundle
17 17 > legacy.exchange = bundle1
18 18 > EOF
19 19
20 20
21 21 This test tries to exercise the ssh functionality with a dummy script
22 22
23 23 creating 'remote' repo
24 24
25 25 $ hg init remote
26 26 $ cd remote
27 27 $ echo this > foo
28 28 $ echo this > fooO
29 29 $ hg ci -A -m "init" foo fooO
30 30
31 31 insert a closed branch (issue4428)
32 32
33 33 $ hg up null
34 34 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
35 35 $ hg branch closed
36 36 marked working directory as branch closed
37 37 (branches are permanent and global, did you want a bookmark?)
38 38 $ hg ci -mc0
39 39 $ hg ci --close-branch -mc1
40 40 $ hg up -q default
41 41
42 42 configure for serving
43 43
44 44 $ cat <<EOF > .hg/hgrc
45 45 > [server]
46 46 > uncompressed = True
47 47 >
48 48 > [hooks]
49 49 > changegroup = sh -c "printenv.py --line changegroup-in-remote 0 ../dummylog"
50 50 > EOF
51 51 $ cd $TESTTMP
52 52
53 53 repo not found error
54 54
55 55 $ hg clone -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" ssh://user@dummy/nonexistent local
56 56 remote: abort: repository nonexistent not found
57 57 abort: no suitable response from remote hg
58 58 [255]
59 59
60 60 non-existent absolute path
61 61
62 62 #if no-msys
63 63 $ hg clone -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" ssh://user@dummy//`pwd`/nonexistent local
64 64 remote: abort: repository /$TESTTMP/nonexistent not found
65 65 abort: no suitable response from remote hg
66 66 [255]
67 67 #endif
68 68
69 69 clone remote via stream
70 70
71 71 #if no-reposimplestore
72 72
73 73 $ hg clone -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" --stream ssh://user@dummy/remote local-stream
74 74 streaming all changes
75 75 4 files to transfer, 602 bytes of data (no-zstd !)
76 76 transferred 602 bytes in * seconds (*) (glob) (no-zstd !)
77 77 4 files to transfer, 621 bytes of data (zstd !)
78 78 transferred 621 bytes in * seconds (* */sec) (glob) (zstd !)
79 79 searching for changes
80 80 no changes found
81 81 updating to branch default
82 82 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
83 83 $ cd local-stream
84 84 $ hg verify
85 85 checking changesets
86 86 checking manifests
87 87 crosschecking files in changesets and manifests
88 88 checking files
89 89 checked 3 changesets with 2 changes to 2 files
90 90 $ hg branches
91 91 default 0:1160648e36ce
92 92 $ cd $TESTTMP
93 93
94 94 clone bookmarks via stream
95 95
96 96 $ hg -R local-stream book mybook
97 97 $ hg clone -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" --stream ssh://user@dummy/local-stream stream2
98 98 streaming all changes
99 99 4 files to transfer, 602 bytes of data (no-zstd !)
100 100 transferred 602 bytes in * seconds (*) (glob) (no-zstd !)
101 101 4 files to transfer, 621 bytes of data (zstd !)
102 102 transferred 621 bytes in * seconds (* */sec) (glob) (zstd !)
103 103 searching for changes
104 104 no changes found
105 105 updating to branch default
106 106 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
107 107 $ cd stream2
108 108 $ hg book
109 109 mybook 0:1160648e36ce
110 110 $ cd $TESTTMP
111 111 $ rm -rf local-stream stream2
112 112
113 113 #endif
114 114
115 115 clone remote via pull
116 116
117 117 $ hg clone -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" ssh://user@dummy/remote local
118 118 requesting all changes
119 119 adding changesets
120 120 adding manifests
121 121 adding file changes
122 122 added 3 changesets with 2 changes to 2 files
123 123 new changesets 1160648e36ce:ad076bfb429d
124 124 updating to branch default
125 125 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
126 126
127 127 verify
128 128
129 129 $ cd local
130 130 $ hg verify
131 131 checking changesets
132 132 checking manifests
133 133 crosschecking files in changesets and manifests
134 134 checking files
135 135 checked 3 changesets with 2 changes to 2 files
136 136 $ cat >> .hg/hgrc <<EOF
137 137 > [hooks]
138 138 > changegroup = sh -c "printenv.py --line changegroup-in-local 0 ../dummylog"
139 139 > EOF
140 140
141 141 empty default pull
142 142
143 143 $ hg paths
144 144 default = ssh://user@dummy/remote
145 145 $ hg pull -e "\"$PYTHON\" \"$TESTDIR/dummyssh\""
146 146 pulling from ssh://user@dummy/remote
147 147 searching for changes
148 148 no changes found
149 149
150 150 pull from wrong ssh URL
151 151
152 152 $ hg pull -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" ssh://user@dummy/doesnotexist
153 153 pulling from ssh://user@dummy/doesnotexist
154 154 remote: abort: repository doesnotexist not found
155 155 abort: no suitable response from remote hg
156 156 [255]
157 157
158 158 local change
159 159
160 160 $ echo bleah > foo
161 161 $ hg ci -m "add"
162 162
163 163 updating rc
164 164
165 165 $ echo "default-push = ssh://user@dummy/remote" >> .hg/hgrc
166 166 $ echo "[ui]" >> .hg/hgrc
167 167 $ echo "ssh = \"$PYTHON\" \"$TESTDIR/dummyssh\"" >> .hg/hgrc
168 168
169 169 find outgoing
170 170
171 171 $ hg out ssh://user@dummy/remote
172 172 comparing with ssh://user@dummy/remote
173 173 searching for changes
174 174 changeset: 3:a28a9d1a809c
175 175 tag: tip
176 176 parent: 0:1160648e36ce
177 177 user: test
178 178 date: Thu Jan 01 00:00:00 1970 +0000
179 179 summary: add
180 180
181 181
182 182 find incoming on the remote side
183 183
184 184 $ hg incoming -R ../remote -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" ssh://user@dummy/local
185 185 comparing with ssh://user@dummy/local
186 186 searching for changes
187 187 changeset: 3:a28a9d1a809c
188 188 tag: tip
189 189 parent: 0:1160648e36ce
190 190 user: test
191 191 date: Thu Jan 01 00:00:00 1970 +0000
192 192 summary: add
193 193
194 194
195 195 find incoming on the remote side (using absolute path)
196 196
197 197 $ hg incoming -R ../remote -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" "ssh://user@dummy/`pwd`"
198 198 comparing with ssh://user@dummy/$TESTTMP/local
199 199 searching for changes
200 200 changeset: 3:a28a9d1a809c
201 201 tag: tip
202 202 parent: 0:1160648e36ce
203 203 user: test
204 204 date: Thu Jan 01 00:00:00 1970 +0000
205 205 summary: add
206 206
207 207
208 208 push
209 209
210 210 $ hg push
211 211 pushing to ssh://user@dummy/remote
212 212 searching for changes
213 213 remote: adding changesets
214 214 remote: adding manifests
215 215 remote: adding file changes
216 216 remote: added 1 changesets with 1 changes to 1 files
217 217 $ cd $TESTTMP/remote
218 218
219 219 check remote tip
220 220
221 221 $ hg tip
222 222 changeset: 3:a28a9d1a809c
223 223 tag: tip
224 224 parent: 0:1160648e36ce
225 225 user: test
226 226 date: Thu Jan 01 00:00:00 1970 +0000
227 227 summary: add
228 228
229 229 $ hg verify
230 230 checking changesets
231 231 checking manifests
232 232 crosschecking files in changesets and manifests
233 233 checking files
234 234 checked 4 changesets with 3 changes to 2 files
235 235 $ hg cat -r tip foo
236 236 bleah
237 237 $ echo z > z
238 238 $ hg ci -A -m z z
239 239 created new head
240 240
241 241 test pushkeys and bookmarks
242 242
243 243 $ cd $TESTTMP/local
244 244 $ hg debugpushkey --config ui.ssh="\"$PYTHON\" \"$TESTDIR/dummyssh\"" ssh://user@dummy/remote namespaces
245 245 bookmarks
246 246 namespaces
247 247 phases
248 248 $ hg book foo -r 0
249 249 $ hg out -B
250 250 comparing with ssh://user@dummy/remote
251 251 searching for changed bookmarks
252 252 foo 1160648e36ce
253 253 $ hg push -B foo
254 254 pushing to ssh://user@dummy/remote
255 255 searching for changes
256 256 no changes found
257 257 exporting bookmark foo
258 258 [1]
259 259 $ hg debugpushkey --config ui.ssh="\"$PYTHON\" \"$TESTDIR/dummyssh\"" ssh://user@dummy/remote bookmarks
260 260 foo 1160648e36cec0054048a7edc4110c6f84fde594
261 261 $ hg book -f foo
262 262 $ hg push --traceback
263 263 pushing to ssh://user@dummy/remote
264 264 searching for changes
265 265 no changes found
266 266 updating bookmark foo
267 267 [1]
268 268 $ hg book -d foo
269 269 $ hg in -B
270 270 comparing with ssh://user@dummy/remote
271 271 searching for changed bookmarks
272 272 foo a28a9d1a809c
273 273 $ hg book -f -r 0 foo
274 274 $ hg pull -B foo
275 275 pulling from ssh://user@dummy/remote
276 276 no changes found
277 277 updating bookmark foo
278 278 $ hg book -d foo
279 279 $ hg push -B foo
280 280 pushing to ssh://user@dummy/remote
281 281 searching for changes
282 282 no changes found
283 283 deleting remote bookmark foo
284 284 [1]
285 285
286 286 a bad, evil hook that prints to stdout
287 287
288 288 $ cat <<EOF > $TESTTMP/badhook
289 289 > import sys
290 290 > sys.stdout.write("KABOOM\n")
291 291 > EOF
292 292
293 293 $ echo '[hooks]' >> ../remote/.hg/hgrc
294 294 $ echo "changegroup.stdout = \"$PYTHON\" $TESTTMP/badhook" >> ../remote/.hg/hgrc
295 295 $ echo r > r
296 296 $ hg ci -A -m z r
297 297
298 298 push should succeed even though it has an unexpected response
299 299
300 300 $ hg push
301 301 pushing to ssh://user@dummy/remote
302 302 searching for changes
303 303 remote has heads on branch 'default' that are not known locally: 6c0482d977a3
304 304 remote: adding changesets
305 305 remote: adding manifests
306 306 remote: adding file changes
307 307 remote: added 1 changesets with 1 changes to 1 files (py3 !)
308 308 remote: added 1 changesets with 1 changes to 1 files (no-py3 no-chg !)
309 309 remote: KABOOM
310 310 remote: added 1 changesets with 1 changes to 1 files (no-py3 chg !)
311 311 $ hg -R ../remote heads
312 312 changeset: 5:1383141674ec
313 313 tag: tip
314 314 parent: 3:a28a9d1a809c
315 315 user: test
316 316 date: Thu Jan 01 00:00:00 1970 +0000
317 317 summary: z
318 318
319 319 changeset: 4:6c0482d977a3
320 320 parent: 0:1160648e36ce
321 321 user: test
322 322 date: Thu Jan 01 00:00:00 1970 +0000
323 323 summary: z
324 324
325 325
326 326 clone bookmarks
327 327
328 328 $ hg -R ../remote bookmark test
329 329 $ hg -R ../remote bookmarks
330 330 * test 4:6c0482d977a3
331 331 $ hg clone -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" ssh://user@dummy/remote local-bookmarks
332 332 requesting all changes
333 333 adding changesets
334 334 adding manifests
335 335 adding file changes
336 336 added 6 changesets with 5 changes to 4 files (+1 heads)
337 337 new changesets 1160648e36ce:1383141674ec
338 338 updating to branch default
339 339 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
340 340 $ hg -R local-bookmarks bookmarks
341 341 test 4:6c0482d977a3
342 342
343 343 passwords in ssh urls are not supported
344 344 (we use a glob here because different Python versions give different
345 345 results here)
346 346
347 347 $ hg push ssh://user:erroneouspwd@dummy/remote
348 348 pushing to ssh://user:*@dummy/remote (glob)
349 349 abort: password in URL not supported
350 350 [255]
351 351
352 352 $ cd $TESTTMP
353 353
354 354 hide outer repo
355 355 $ hg init
356 356
357 357 Test remote paths with spaces (issue2983):
358 358
359 359 $ hg init --ssh "\"$PYTHON\" \"$TESTDIR/dummyssh\"" "ssh://user@dummy/a repo"
360 360 $ touch "$TESTTMP/a repo/test"
361 361 $ hg -R 'a repo' commit -A -m "test"
362 362 adding test
363 363 $ hg -R 'a repo' tag tag
364 364 $ hg id --ssh "\"$PYTHON\" \"$TESTDIR/dummyssh\"" "ssh://user@dummy/a repo"
365 365 73649e48688a
366 366
367 367 $ hg id --ssh "\"$PYTHON\" \"$TESTDIR/dummyssh\"" "ssh://user@dummy/a repo#noNoNO"
368 368 abort: unknown revision 'noNoNO'
369 369 [255]
370 370
371 371 Test (non-)escaping of remote paths with spaces when cloning (issue3145):
372 372
373 373 $ hg clone --ssh "\"$PYTHON\" \"$TESTDIR/dummyssh\"" "ssh://user@dummy/a repo"
374 374 destination directory: a repo
375 375 abort: destination 'a repo' is not empty
376 376 [10]
377 377
378 378 Test hg-ssh using a helper script that will restore PYTHONPATH (which might
379 379 have been cleared by a hg.exe wrapper) and invoke hg-ssh with the right
380 380 parameters:
381 381
382 382 $ cat > ssh.sh << EOF
383 383 > userhost="\$1"
384 384 > SSH_ORIGINAL_COMMAND="\$2"
385 385 > export SSH_ORIGINAL_COMMAND
386 386 > PYTHONPATH="$PYTHONPATH"
387 387 > export PYTHONPATH
388 388 > "$PYTHON" "$TESTDIR/../contrib/hg-ssh" "$TESTTMP/a repo"
389 389 > EOF
390 390
391 391 $ hg id --ssh "sh ssh.sh" "ssh://user@dummy/a repo"
392 392 73649e48688a
393 393
394 394 $ hg id --ssh "sh ssh.sh" "ssh://user@dummy/a'repo"
395 395 remote: Illegal repository "$TESTTMP/a'repo"
396 396 abort: no suitable response from remote hg
397 397 [255]
398 398
399 399 $ hg id --ssh "sh ssh.sh" --remotecmd hacking "ssh://user@dummy/a'repo"
400 400 remote: Illegal command "hacking -R 'a'\''repo' serve --stdio"
401 401 abort: no suitable response from remote hg
402 402 [255]
403 403
404 404 $ SSH_ORIGINAL_COMMAND="'hg' serve -R 'a'repo' --stdio" "$PYTHON" "$TESTDIR/../contrib/hg-ssh"
405 405 Illegal command "'hg' serve -R 'a'repo' --stdio": No closing quotation
406 406 [255]
407 407
408 408 Test hg-ssh in read-only mode:
409 409
410 410 $ cat > ssh.sh << EOF
411 411 > userhost="\$1"
412 412 > SSH_ORIGINAL_COMMAND="\$2"
413 413 > export SSH_ORIGINAL_COMMAND
414 414 > PYTHONPATH="$PYTHONPATH"
415 415 > export PYTHONPATH
416 416 > "$PYTHON" "$TESTDIR/../contrib/hg-ssh" --read-only "$TESTTMP/remote"
417 417 > EOF
418 418
419 419 $ hg clone --ssh "sh ssh.sh" "ssh://user@dummy/$TESTTMP/remote" read-only-local
420 420 requesting all changes
421 421 adding changesets
422 422 adding manifests
423 423 adding file changes
424 424 added 6 changesets with 5 changes to 4 files (+1 heads)
425 425 new changesets 1160648e36ce:1383141674ec
426 426 updating to branch default
427 427 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
428 428
429 429 $ cd read-only-local
430 430 $ echo "baz" > bar
431 431 $ hg ci -A -m "unpushable commit" bar
432 432 $ hg push --ssh "sh ../ssh.sh"
433 433 pushing to ssh://user@dummy/*/remote (glob)
434 434 searching for changes
435 435 remote: Permission denied
436 436 remote: abort: pretxnopen.hg-ssh hook failed
437 437 remote: Permission denied
438 438 remote: pushkey-abort: prepushkey.hg-ssh hook failed
439 439 updating 6c0482d977a3 to public failed!
440 440 [1]
441 441
442 442 $ cd $TESTTMP
443 443
444 444 stderr from remote commands should be printed before stdout from local code (issue4336)
445 445
446 446 $ hg clone remote stderr-ordering
447 447 updating to branch default
448 448 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
449 449 $ cd stderr-ordering
450 450 $ cat >> localwrite.py << EOF
451 451 > from mercurial import exchange, extensions
452 452 >
453 453 > def wrappedpush(orig, repo, *args, **kwargs):
454 454 > res = orig(repo, *args, **kwargs)
455 455 > repo.ui.write(b'local stdout\n')
456 456 > return res
457 457 >
458 458 > def extsetup(ui):
459 459 > extensions.wrapfunction(exchange, b'push', wrappedpush)
460 460 > EOF
461 461
462 462 $ cat >> .hg/hgrc << EOF
463 463 > [paths]
464 464 > default-push = ssh://user@dummy/remote
465 465 > [ui]
466 466 > ssh = "$PYTHON" "$TESTDIR/dummyssh"
467 467 > [extensions]
468 468 > localwrite = localwrite.py
469 469 > EOF
470 470
471 471 $ echo localwrite > foo
472 472 $ hg commit -m 'testing localwrite'
473 473 $ hg push
474 474 pushing to ssh://user@dummy/remote
475 475 searching for changes
476 476 remote: adding changesets
477 477 remote: adding manifests
478 478 remote: adding file changes
479 479 remote: added 1 changesets with 1 changes to 1 files (py3 !)
480 480 remote: added 1 changesets with 1 changes to 1 files (no-py3 no-chg !)
481 481 remote: KABOOM
482 482 remote: added 1 changesets with 1 changes to 1 files (no-py3 chg !)
483 483 local stdout
484 484
485 485 debug output
486 486
487 487 $ hg pull --debug ssh://user@dummy/remote
488 488 pulling from ssh://user@dummy/remote
489 running .* ".*/dummyssh" ['"]user@dummy['"] ('|")hg -R remote serve --stdio('|") (re)
489 running .* ".*[/\\]dummyssh" ['"]user@dummy['"] ['"]hg -R remote serve --stdio['"] (re)
490 490 sending upgrade request: * proto=exp-ssh-v2-0003 (glob) (sshv2 !)
491 491 sending hello command
492 492 sending between command
493 493 remote: 444 (sshv1 no-rust !)
494 494 remote: 463 (sshv1 rust !)
495 495 protocol upgraded to exp-ssh-v2-0003 (sshv2 !)
496 496 remote: capabilities: batch branchmap $USUAL_BUNDLE2_CAPS$ changegroupsubset getbundle known lookup protocaps pushkey streamreqs=generaldelta,revlogv1,sparserevlog unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash (no-rust !)
497 497 remote: capabilities: batch branchmap $USUAL_BUNDLE2_CAPS$ changegroupsubset getbundle known lookup protocaps pushkey streamreqs=generaldelta,persistent-nodemap,revlogv1,sparserevlog unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash (rust !)
498 498 remote: 1 (sshv1 !)
499 499 sending protocaps command
500 500 preparing listkeys for "bookmarks"
501 501 sending listkeys command
502 502 received listkey for "bookmarks": 45 bytes
503 503 query 1; heads
504 504 sending batch command
505 505 searching for changes
506 506 all remote heads known locally
507 507 no changes found
508 508 preparing listkeys for "phases"
509 509 sending listkeys command
510 510 received listkey for "phases": 15 bytes
511 511 checking for updated bookmarks
512 512
513 513 $ cd $TESTTMP
514 514
515 515 $ cat dummylog
516 516 Got arguments 1:user@dummy 2:hg -R nonexistent serve --stdio
517 517 Got arguments 1:user@dummy 2:hg -R /$TESTTMP/nonexistent serve --stdio (no-msys !)
518 518 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
519 519 Got arguments 1:user@dummy 2:hg -R local-stream serve --stdio (no-reposimplestore !)
520 520 Got arguments 1:user@dummy 2:hg -R remote serve --stdio (no-reposimplestore !)
521 521 Got arguments 1:user@dummy 2:hg -R remote serve --stdio (no-reposimplestore !)
522 522 Got arguments 1:user@dummy 2:hg -R doesnotexist serve --stdio
523 523 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
524 524 Got arguments 1:user@dummy 2:hg -R local serve --stdio
525 525 Got arguments 1:user@dummy 2:hg -R $TESTTMP/local serve --stdio
526 526 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
527 527 changegroup-in-remote hook: HG_HOOKNAME=changegroup
528 528 HG_HOOKTYPE=changegroup
529 529 HG_NODE=a28a9d1a809cab7d4e2fde4bee738a9ede948b60
530 530 HG_NODE_LAST=a28a9d1a809cab7d4e2fde4bee738a9ede948b60
531 531 HG_SOURCE=serve
532 532 HG_TXNID=TXN:$ID$
533 533 HG_TXNNAME=serve
534 534 remote:ssh:$LOCALIP
535 535 HG_URL=remote:ssh:$LOCALIP
536 536
537 537 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
538 538 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
539 539 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
540 540 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
541 541 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
542 542 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
543 543 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
544 544 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
545 545 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
546 546 changegroup-in-remote hook: HG_HOOKNAME=changegroup
547 547 HG_HOOKTYPE=changegroup
548 548 HG_NODE=1383141674ec756a6056f6a9097618482fe0f4a6
549 549 HG_NODE_LAST=1383141674ec756a6056f6a9097618482fe0f4a6
550 550 HG_SOURCE=serve
551 551 HG_TXNID=TXN:$ID$
552 552 HG_TXNNAME=serve
553 553 remote:ssh:$LOCALIP
554 554 HG_URL=remote:ssh:$LOCALIP
555 555
556 556 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
557 557 Got arguments 1:user@dummy 2:hg init 'a repo'
558 558 Got arguments 1:user@dummy 2:hg -R 'a repo' serve --stdio
559 559 Got arguments 1:user@dummy 2:hg -R 'a repo' serve --stdio
560 560 Got arguments 1:user@dummy 2:hg -R 'a repo' serve --stdio
561 561 Got arguments 1:user@dummy 2:hg -R 'a repo' serve --stdio
562 562 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
563 563 changegroup-in-remote hook: HG_HOOKNAME=changegroup
564 564 HG_HOOKTYPE=changegroup
565 565 HG_NODE=65c38f4125f9602c8db4af56530cc221d93b8ef8
566 566 HG_NODE_LAST=65c38f4125f9602c8db4af56530cc221d93b8ef8
567 567 HG_SOURCE=serve
568 568 HG_TXNID=TXN:$ID$
569 569 HG_TXNNAME=serve
570 570 remote:ssh:$LOCALIP
571 571 HG_URL=remote:ssh:$LOCALIP
572 572
573 573 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
574 574
575 575 remote hook failure is attributed to remote
576 576
577 577 $ cat > $TESTTMP/failhook << EOF
578 578 > def hook(ui, repo, **kwargs):
579 579 > ui.write(b'hook failure!\n')
580 580 > ui.flush()
581 581 > return 1
582 582 > EOF
583 583
584 584 $ echo "pretxnchangegroup.fail = python:$TESTTMP/failhook:hook" >> remote/.hg/hgrc
585 585
586 586 $ hg -q --config ui.ssh="\"$PYTHON\" $TESTDIR/dummyssh" clone ssh://user@dummy/remote hookout
587 587 $ cd hookout
588 588 $ touch hookfailure
589 589 $ hg -q commit -A -m 'remote hook failure'
590 590 $ hg --config ui.ssh="\"$PYTHON\" $TESTDIR/dummyssh" push
591 591 pushing to ssh://user@dummy/remote
592 592 searching for changes
593 593 remote: adding changesets
594 594 remote: adding manifests
595 595 remote: adding file changes
596 596 remote: hook failure!
597 597 remote: transaction abort!
598 598 remote: rollback completed
599 599 remote: abort: pretxnchangegroup.fail hook failed
600 600 [1]
601 601
602 602 abort during pull is properly reported as such
603 603
604 604 $ echo morefoo >> ../remote/foo
605 605 $ hg -R ../remote commit --message "more foo to be pulled"
606 606 $ cat >> ../remote/.hg/hgrc << EOF
607 607 > [extensions]
608 608 > crash = ${TESTDIR}/crashgetbundler.py
609 609 > EOF
610 610 $ hg --config ui.ssh="\"$PYTHON\" $TESTDIR/dummyssh" pull
611 611 pulling from ssh://user@dummy/remote
612 612 searching for changes
613 613 adding changesets
614 614 remote: abort: this is an exercise
615 615 transaction abort!
616 616 rollback completed
617 617 abort: stream ended unexpectedly (got 0 bytes, expected 4)
618 618 [255]
@@ -1,220 +1,220 b''
1 1 This test tries to exercise the ssh functionality with a dummy script
2 2
3 3 #testcases sshv1 sshv2
4 4
5 5 #if sshv2
6 6 $ cat >> $HGRCPATH << EOF
7 7 > [experimental]
8 8 > sshpeer.advertise-v2 = true
9 9 > sshserver.support-v2 = true
10 10 > EOF
11 11 #endif
12 12
13 13 creating 'remote' repo
14 14
15 15 $ hg init remote
16 16 $ cd remote
17 17 $ hg unbundle "$TESTDIR/bundles/remote.hg"
18 18 adding changesets
19 19 adding manifests
20 20 adding file changes
21 21 added 9 changesets with 7 changes to 4 files (+1 heads)
22 22 new changesets bfaf4b5cbf01:916f1afdef90 (9 drafts)
23 23 (run 'hg heads' to see heads, 'hg merge' to merge)
24 24 $ hg up tip
25 25 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
26 26 $ cd ..
27 27
28 28 clone remote via stream
29 29
30 30 $ for i in 0 1 2 3 4 5 6 7 8; do
31 > hg clone -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" --stream -r "$i" ssh://user@dummy/remote test-"$i"
31 > hg clone --stream -r "$i" ssh://user@dummy/remote test-"$i"
32 32 > if cd test-"$i"; then
33 33 > hg verify
34 34 > cd ..
35 35 > fi
36 36 > done
37 37 adding changesets
38 38 adding manifests
39 39 adding file changes
40 40 added 1 changesets with 1 changes to 1 files
41 41 new changesets bfaf4b5cbf01
42 42 updating to branch default
43 43 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
44 44 checking changesets
45 45 checking manifests
46 46 crosschecking files in changesets and manifests
47 47 checking files
48 48 checked 1 changesets with 1 changes to 1 files
49 49 adding changesets
50 50 adding manifests
51 51 adding file changes
52 52 added 2 changesets with 2 changes to 1 files
53 53 new changesets bfaf4b5cbf01:21f32785131f
54 54 updating to branch default
55 55 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
56 56 checking changesets
57 57 checking manifests
58 58 crosschecking files in changesets and manifests
59 59 checking files
60 60 checked 2 changesets with 2 changes to 1 files
61 61 adding changesets
62 62 adding manifests
63 63 adding file changes
64 64 added 3 changesets with 3 changes to 1 files
65 65 new changesets bfaf4b5cbf01:4ce51a113780
66 66 updating to branch default
67 67 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
68 68 checking changesets
69 69 checking manifests
70 70 crosschecking files in changesets and manifests
71 71 checking files
72 72 checked 3 changesets with 3 changes to 1 files
73 73 adding changesets
74 74 adding manifests
75 75 adding file changes
76 76 added 4 changesets with 4 changes to 1 files
77 77 new changesets bfaf4b5cbf01:93ee6ab32777
78 78 updating to branch default
79 79 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
80 80 checking changesets
81 81 checking manifests
82 82 crosschecking files in changesets and manifests
83 83 checking files
84 84 checked 4 changesets with 4 changes to 1 files
85 85 adding changesets
86 86 adding manifests
87 87 adding file changes
88 88 added 2 changesets with 2 changes to 1 files
89 89 new changesets bfaf4b5cbf01:c70afb1ee985
90 90 updating to branch default
91 91 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
92 92 checking changesets
93 93 checking manifests
94 94 crosschecking files in changesets and manifests
95 95 checking files
96 96 checked 2 changesets with 2 changes to 1 files
97 97 adding changesets
98 98 adding manifests
99 99 adding file changes
100 100 added 3 changesets with 3 changes to 1 files
101 101 new changesets bfaf4b5cbf01:f03ae5a9b979
102 102 updating to branch default
103 103 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
104 104 checking changesets
105 105 checking manifests
106 106 crosschecking files in changesets and manifests
107 107 checking files
108 108 checked 3 changesets with 3 changes to 1 files
109 109 adding changesets
110 110 adding manifests
111 111 adding file changes
112 112 added 4 changesets with 5 changes to 2 files
113 113 new changesets bfaf4b5cbf01:095cb14b1b4d
114 114 updating to branch default
115 115 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
116 116 checking changesets
117 117 checking manifests
118 118 crosschecking files in changesets and manifests
119 119 checking files
120 120 checked 4 changesets with 5 changes to 2 files
121 121 adding changesets
122 122 adding manifests
123 123 adding file changes
124 124 added 5 changesets with 6 changes to 3 files
125 125 new changesets bfaf4b5cbf01:faa2e4234c7a
126 126 updating to branch default
127 127 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
128 128 checking changesets
129 129 checking manifests
130 130 crosschecking files in changesets and manifests
131 131 checking files
132 132 checked 5 changesets with 6 changes to 3 files
133 133 adding changesets
134 134 adding manifests
135 135 adding file changes
136 136 added 5 changesets with 5 changes to 2 files
137 137 new changesets bfaf4b5cbf01:916f1afdef90
138 138 updating to branch default
139 139 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
140 140 checking changesets
141 141 checking manifests
142 142 crosschecking files in changesets and manifests
143 143 checking files
144 144 checked 5 changesets with 5 changes to 2 files
145 145 $ cd test-8
146 146 $ hg pull ../test-7
147 147 pulling from ../test-7
148 148 searching for changes
149 149 adding changesets
150 150 adding manifests
151 151 adding file changes
152 152 added 4 changesets with 2 changes to 3 files (+1 heads)
153 153 new changesets c70afb1ee985:faa2e4234c7a
154 154 (run 'hg heads' to see heads, 'hg merge' to merge)
155 155 $ hg verify
156 156 checking changesets
157 157 checking manifests
158 158 crosschecking files in changesets and manifests
159 159 checking files
160 160 checked 9 changesets with 7 changes to 4 files
161 161 $ cd ..
162 162 $ cd test-1
163 $ hg pull -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" -r 4 ssh://user@dummy/remote
163 $ hg pull -r 4 ssh://user@dummy/remote
164 164 pulling from ssh://user@dummy/remote
165 165 searching for changes
166 166 adding changesets
167 167 adding manifests
168 168 adding file changes
169 169 added 1 changesets with 0 changes to 0 files (+1 heads)
170 170 new changesets c70afb1ee985
171 171 (run 'hg heads' to see heads, 'hg merge' to merge)
172 172 $ hg verify
173 173 checking changesets
174 174 checking manifests
175 175 crosschecking files in changesets and manifests
176 176 checking files
177 177 checked 3 changesets with 2 changes to 1 files
178 $ hg pull -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" ssh://user@dummy/remote
178 $ hg pull ssh://user@dummy/remote
179 179 pulling from ssh://user@dummy/remote
180 180 searching for changes
181 181 adding changesets
182 182 adding manifests
183 183 adding file changes
184 184 added 6 changesets with 5 changes to 4 files
185 185 new changesets 4ce51a113780:916f1afdef90
186 186 (run 'hg update' to get a working copy)
187 187 $ cd ..
188 188 $ cd test-2
189 $ hg pull -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" -r 5 ssh://user@dummy/remote
189 $ hg pull -r 5 ssh://user@dummy/remote
190 190 pulling from ssh://user@dummy/remote
191 191 searching for changes
192 192 adding changesets
193 193 adding manifests
194 194 adding file changes
195 195 added 2 changesets with 0 changes to 0 files (+1 heads)
196 196 new changesets c70afb1ee985:f03ae5a9b979
197 197 (run 'hg heads' to see heads, 'hg merge' to merge)
198 198 $ hg verify
199 199 checking changesets
200 200 checking manifests
201 201 crosschecking files in changesets and manifests
202 202 checking files
203 203 checked 5 changesets with 3 changes to 1 files
204 $ hg pull -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" ssh://user@dummy/remote
204 $ hg pull ssh://user@dummy/remote
205 205 pulling from ssh://user@dummy/remote
206 206 searching for changes
207 207 adding changesets
208 208 adding manifests
209 209 adding file changes
210 210 added 4 changesets with 4 changes to 4 files
211 211 new changesets 93ee6ab32777:916f1afdef90
212 212 (run 'hg update' to get a working copy)
213 213 $ hg verify
214 214 checking changesets
215 215 checking manifests
216 216 crosschecking files in changesets and manifests
217 217 checking files
218 218 checked 9 changesets with 7 changes to 4 files
219 219
220 220 $ cd ..
@@ -1,737 +1,737 b''
1 1 #testcases sshv1 sshv2
2 2
3 3 #if sshv2
4 4 $ cat >> $HGRCPATH << EOF
5 5 > [experimental]
6 6 > sshpeer.advertise-v2 = true
7 7 > sshserver.support-v2 = true
8 8 > EOF
9 9 #endif
10 10
11 11 This test tries to exercise the ssh functionality with a dummy script
12 12
13 13 creating 'remote' repo
14 14
15 15 $ hg init remote
16 16 $ cd remote
17 17 $ echo this > foo
18 18 $ echo this > fooO
19 19 $ hg ci -A -m "init" foo fooO
20 20
21 21 insert a closed branch (issue4428)
22 22
23 23 $ hg up null
24 24 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
25 25 $ hg branch closed
26 26 marked working directory as branch closed
27 27 (branches are permanent and global, did you want a bookmark?)
28 28 $ hg ci -mc0
29 29 $ hg ci --close-branch -mc1
30 30 $ hg up -q default
31 31
32 32 configure for serving
33 33
34 34 $ cat <<EOF > .hg/hgrc
35 35 > [server]
36 36 > uncompressed = True
37 37 >
38 38 > [hooks]
39 39 > changegroup = sh -c "printenv.py --line changegroup-in-remote 0 ../dummylog"
40 40 > EOF
41 41 $ cd $TESTTMP
42 42
43 43 repo not found error
44 44
45 45 $ hg clone -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" ssh://user@dummy/nonexistent local
46 46 remote: abort: repository nonexistent not found
47 47 abort: no suitable response from remote hg
48 48 [255]
49 49 $ hg clone -q -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" ssh://user@dummy/nonexistent local
50 50 remote: abort: repository nonexistent not found
51 51 abort: no suitable response from remote hg
52 52 [255]
53 53
54 54 non-existent absolute path
55 55
56 56 $ hg clone -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" ssh://user@dummy/`pwd`/nonexistent local
57 57 remote: abort: repository $TESTTMP/nonexistent not found
58 58 abort: no suitable response from remote hg
59 59 [255]
60 60
61 61 clone remote via stream
62 62
63 63 #if no-reposimplestore
64 64
65 65 $ hg clone -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" --stream ssh://user@dummy/remote local-stream
66 66 streaming all changes
67 67 8 files to transfer, 827 bytes of data (no-zstd !)
68 68 transferred 827 bytes in * seconds (*) (glob) (no-zstd !)
69 69 8 files to transfer, 846 bytes of data (zstd !)
70 70 transferred * bytes in * seconds (* */sec) (glob) (zstd !)
71 71 updating to branch default
72 72 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
73 73 $ cd local-stream
74 74 $ hg verify
75 75 checking changesets
76 76 checking manifests
77 77 crosschecking files in changesets and manifests
78 78 checking files
79 79 checked 3 changesets with 2 changes to 2 files
80 80 $ hg branches
81 81 default 0:1160648e36ce
82 82 $ cd $TESTTMP
83 83
84 84 clone bookmarks via stream
85 85
86 86 $ hg -R local-stream book mybook
87 87 $ hg clone -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" --stream ssh://user@dummy/local-stream stream2
88 88 streaming all changes
89 89 15 files to transfer, * of data (glob)
90 90 transferred * in * seconds (*) (glob)
91 91 updating to branch default
92 92 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
93 93 $ cd stream2
94 94 $ hg book
95 95 mybook 0:1160648e36ce
96 96 $ cd $TESTTMP
97 97 $ rm -rf local-stream stream2
98 98
99 99 #endif
100 100
101 101 clone remote via pull
102 102
103 103 $ hg clone -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" ssh://user@dummy/remote local
104 104 requesting all changes
105 105 adding changesets
106 106 adding manifests
107 107 adding file changes
108 108 added 3 changesets with 2 changes to 2 files
109 109 new changesets 1160648e36ce:ad076bfb429d
110 110 updating to branch default
111 111 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
112 112
113 113 verify
114 114
115 115 $ cd local
116 116 $ hg verify
117 117 checking changesets
118 118 checking manifests
119 119 crosschecking files in changesets and manifests
120 120 checking files
121 121 checked 3 changesets with 2 changes to 2 files
122 122 $ cat >> .hg/hgrc <<EOF
123 123 > [hooks]
124 124 > changegroup = sh -c "printenv.py changegroup-in-local 0 ../dummylog"
125 125 > EOF
126 126
127 127 empty default pull
128 128
129 129 $ hg paths
130 130 default = ssh://user@dummy/remote
131 131 $ hg pull -e "\"$PYTHON\" \"$TESTDIR/dummyssh\""
132 132 pulling from ssh://user@dummy/remote
133 133 searching for changes
134 134 no changes found
135 135
136 136 pull from wrong ssh URL
137 137
138 138 $ hg pull -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" ssh://user@dummy/doesnotexist
139 139 pulling from ssh://user@dummy/doesnotexist
140 140 remote: abort: repository doesnotexist not found
141 141 abort: no suitable response from remote hg
142 142 [255]
143 143
144 144 local change
145 145
146 146 $ echo bleah > foo
147 147 $ hg ci -m "add"
148 148
149 149 updating rc
150 150
151 151 $ echo "default-push = ssh://user@dummy/remote" >> .hg/hgrc
152 152 $ echo "[ui]" >> .hg/hgrc
153 153 $ echo "ssh = \"$PYTHON\" \"$TESTDIR/dummyssh\"" >> .hg/hgrc
154 154
155 155 find outgoing
156 156
157 157 $ hg out ssh://user@dummy/remote
158 158 comparing with ssh://user@dummy/remote
159 159 searching for changes
160 160 changeset: 3:a28a9d1a809c
161 161 tag: tip
162 162 parent: 0:1160648e36ce
163 163 user: test
164 164 date: Thu Jan 01 00:00:00 1970 +0000
165 165 summary: add
166 166
167 167
168 168 find incoming on the remote side
169 169
170 170 $ hg incoming -R ../remote -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" ssh://user@dummy/local
171 171 comparing with ssh://user@dummy/local
172 172 searching for changes
173 173 changeset: 3:a28a9d1a809c
174 174 tag: tip
175 175 parent: 0:1160648e36ce
176 176 user: test
177 177 date: Thu Jan 01 00:00:00 1970 +0000
178 178 summary: add
179 179
180 180
181 181 find incoming on the remote side (using absolute path)
182 182
183 183 $ hg incoming -R ../remote -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" "ssh://user@dummy/`pwd`"
184 184 comparing with ssh://user@dummy/$TESTTMP/local
185 185 searching for changes
186 186 changeset: 3:a28a9d1a809c
187 187 tag: tip
188 188 parent: 0:1160648e36ce
189 189 user: test
190 190 date: Thu Jan 01 00:00:00 1970 +0000
191 191 summary: add
192 192
193 193
194 194 push
195 195
196 196 $ hg push
197 197 pushing to ssh://user@dummy/remote
198 198 searching for changes
199 199 remote: adding changesets
200 200 remote: adding manifests
201 201 remote: adding file changes
202 202 remote: added 1 changesets with 1 changes to 1 files
203 203 $ cd $TESTTMP/remote
204 204
205 205 check remote tip
206 206
207 207 $ hg tip
208 208 changeset: 3:a28a9d1a809c
209 209 tag: tip
210 210 parent: 0:1160648e36ce
211 211 user: test
212 212 date: Thu Jan 01 00:00:00 1970 +0000
213 213 summary: add
214 214
215 215 $ hg verify
216 216 checking changesets
217 217 checking manifests
218 218 crosschecking files in changesets and manifests
219 219 checking files
220 220 checked 4 changesets with 3 changes to 2 files
221 221 $ hg cat -r tip foo
222 222 bleah
223 223 $ echo z > z
224 224 $ hg ci -A -m z z
225 225 created new head
226 226
227 227 test pushkeys and bookmarks
228 228
229 229 $ cd $TESTTMP/local
230 230 $ hg debugpushkey --config ui.ssh="\"$PYTHON\" \"$TESTDIR/dummyssh\"" ssh://user@dummy/remote namespaces
231 231 bookmarks
232 232 namespaces
233 233 phases
234 234 $ hg book foo -r 0
235 235 $ hg out -B --config paths.default=bogus://invalid --config paths.default:pushurl=`hg paths default`
236 236 comparing with ssh://user@dummy/remote
237 237 searching for changed bookmarks
238 238 foo 1160648e36ce
239 239 $ hg push -B foo
240 240 pushing to ssh://user@dummy/remote
241 241 searching for changes
242 242 no changes found
243 243 exporting bookmark foo
244 244 [1]
245 245 $ hg debugpushkey --config ui.ssh="\"$PYTHON\" \"$TESTDIR/dummyssh\"" ssh://user@dummy/remote bookmarks
246 246 foo 1160648e36cec0054048a7edc4110c6f84fde594
247 247 $ hg book -f foo
248 248 $ hg push --traceback
249 249 pushing to ssh://user@dummy/remote
250 250 searching for changes
251 251 no changes found
252 252 updating bookmark foo
253 253 [1]
254 254 $ hg book -d foo
255 255 $ hg in -B
256 256 comparing with ssh://user@dummy/remote
257 257 searching for changed bookmarks
258 258 foo a28a9d1a809c
259 259 $ hg book -f -r 0 foo
260 260 $ hg pull -B foo
261 261 pulling from ssh://user@dummy/remote
262 262 no changes found
263 263 updating bookmark foo
264 264 $ hg book -d foo
265 265 $ hg push -B foo
266 266 pushing to ssh://user@dummy/remote
267 267 searching for changes
268 268 no changes found
269 269 deleting remote bookmark foo
270 270 [1]
271 271
272 272 a bad, evil hook that prints to stdout
273 273
274 274 $ cat <<EOF > $TESTTMP/badhook
275 275 > import sys
276 276 > sys.stdout.write("KABOOM\n")
277 277 > sys.stdout.flush()
278 278 > EOF
279 279
280 280 $ cat <<EOF > $TESTTMP/badpyhook.py
281 281 > import sys
282 282 > def hook(ui, repo, hooktype, **kwargs):
283 283 > sys.stdout.write("KABOOM IN PROCESS\n")
284 284 > sys.stdout.flush()
285 285 > EOF
286 286
287 287 $ cat <<EOF >> ../remote/.hg/hgrc
288 288 > [hooks]
289 289 > changegroup.stdout = "$PYTHON" $TESTTMP/badhook
290 290 > changegroup.pystdout = python:$TESTTMP/badpyhook.py:hook
291 291 > EOF
292 292 $ echo r > r
293 293 $ hg ci -A -m z r
294 294
295 295 push should succeed even though it has an unexpected response
296 296
297 297 $ hg push
298 298 pushing to ssh://user@dummy/remote
299 299 searching for changes
300 300 remote has heads on branch 'default' that are not known locally: 6c0482d977a3
301 301 remote: adding changesets
302 302 remote: adding manifests
303 303 remote: adding file changes
304 304 remote: added 1 changesets with 1 changes to 1 files (py3 !)
305 305 remote: added 1 changesets with 1 changes to 1 files (no-py3 no-chg !)
306 306 remote: KABOOM
307 307 remote: KABOOM IN PROCESS
308 308 remote: added 1 changesets with 1 changes to 1 files (no-py3 chg !)
309 309 $ hg -R ../remote heads
310 310 changeset: 5:1383141674ec
311 311 tag: tip
312 312 parent: 3:a28a9d1a809c
313 313 user: test
314 314 date: Thu Jan 01 00:00:00 1970 +0000
315 315 summary: z
316 316
317 317 changeset: 4:6c0482d977a3
318 318 parent: 0:1160648e36ce
319 319 user: test
320 320 date: Thu Jan 01 00:00:00 1970 +0000
321 321 summary: z
322 322
323 323
324 324 #if chg
325 325
326 326 try again with remote chg, which should succeed as well
327 327
328 328 $ hg rollback -R ../remote
329 329 repository tip rolled back to revision 4 (undo serve)
330 330
331 331 $ hg push --config ui.remotecmd=chg
332 332 pushing to ssh://user@dummy/remote
333 333 searching for changes
334 334 remote has heads on branch 'default' that are not known locally: 6c0482d977a3
335 335 remote: adding changesets
336 336 remote: adding manifests
337 337 remote: adding file changes
338 338 remote: added 1 changesets with 1 changes to 1 files (py3 !)
339 339 remote: KABOOM
340 340 remote: KABOOM IN PROCESS
341 341 remote: added 1 changesets with 1 changes to 1 files (no-py3 !)
342 342
343 343 #endif
344 344
345 345 clone bookmarks
346 346
347 347 $ hg -R ../remote bookmark test
348 348 $ hg -R ../remote bookmarks
349 349 * test 4:6c0482d977a3
350 350 $ hg clone -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" ssh://user@dummy/remote local-bookmarks
351 351 requesting all changes
352 352 adding changesets
353 353 adding manifests
354 354 adding file changes
355 355 added 6 changesets with 5 changes to 4 files (+1 heads)
356 356 new changesets 1160648e36ce:1383141674ec
357 357 updating to branch default
358 358 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
359 359 $ hg -R local-bookmarks bookmarks
360 360 test 4:6c0482d977a3
361 361
362 362 passwords in ssh urls are not supported
363 363 (we use a glob here because different Python versions give different
364 364 results here)
365 365
366 366 $ hg push ssh://user:erroneouspwd@dummy/remote
367 367 pushing to ssh://user:*@dummy/remote (glob)
368 368 abort: password in URL not supported
369 369 [255]
370 370
371 371 $ cd $TESTTMP
372 372
373 373 hide outer repo
374 374 $ hg init
375 375
376 376 Test remote paths with spaces (issue2983):
377 377
378 378 $ hg init --ssh "\"$PYTHON\" \"$TESTDIR/dummyssh\"" "ssh://user@dummy/a repo"
379 379 $ touch "$TESTTMP/a repo/test"
380 380 $ hg -R 'a repo' commit -A -m "test"
381 381 adding test
382 382 $ hg -R 'a repo' tag tag
383 383 $ hg id --ssh "\"$PYTHON\" \"$TESTDIR/dummyssh\"" "ssh://user@dummy/a repo"
384 384 73649e48688a
385 385
386 386 $ hg id --ssh "\"$PYTHON\" \"$TESTDIR/dummyssh\"" "ssh://user@dummy/a repo#noNoNO"
387 387 abort: unknown revision 'noNoNO'
388 388 [255]
389 389
390 390 Test (non-)escaping of remote paths with spaces when cloning (issue3145):
391 391
392 392 $ hg clone --ssh "\"$PYTHON\" \"$TESTDIR/dummyssh\"" "ssh://user@dummy/a repo"
393 393 destination directory: a repo
394 394 abort: destination 'a repo' is not empty
395 395 [10]
396 396
397 397 #if no-rhg
398 398 Make sure hg is really paranoid in serve --stdio mode. It used to be
399 399 possible to get a debugger REPL by specifying a repo named --debugger.
400 400 $ hg -R --debugger serve --stdio
401 401 abort: potentially unsafe serve --stdio invocation: ['-R', '--debugger', 'serve', '--stdio']
402 402 [255]
403 403 $ hg -R --config=ui.debugger=yes serve --stdio
404 404 abort: potentially unsafe serve --stdio invocation: ['-R', '--config=ui.debugger=yes', 'serve', '--stdio']
405 405 [255]
406 406 Abbreviations of 'serve' also don't work, to avoid shenanigans.
407 407 $ hg -R narf serv --stdio
408 408 abort: potentially unsafe serve --stdio invocation: ['-R', 'narf', 'serv', '--stdio']
409 409 [255]
410 410 #else
411 411 rhg aborts early on -R without a repository at that path
412 412 $ hg -R --debugger serve --stdio
413 413 abort: potentially unsafe serve --stdio invocation: ['-R', '--debugger', 'serve', '--stdio'] (missing-correct-output !)
414 414 abort: repository --debugger not found (known-bad-output !)
415 415 [255]
416 416 $ hg -R --config=ui.debugger=yes serve --stdio
417 417 abort: potentially unsafe serve --stdio invocation: ['-R', '--config=ui.debugger=yes', 'serve', '--stdio'] (missing-correct-output !)
418 418 abort: repository --config=ui.debugger=yes not found (known-bad-output !)
419 419 [255]
420 420 $ hg -R narf serv --stdio
421 421 abort: potentially unsafe serve --stdio invocation: ['-R', 'narf', 'serv', '--stdio'] (missing-correct-output !)
422 422 abort: repository narf not found (known-bad-output !)
423 423 [255]
424 424 If the repo does exist, rhg finds an unsupported command and falls back to Python
425 425 which still does the right thing
426 426 $ hg init narf
427 427 $ hg -R narf serv --stdio
428 428 abort: potentially unsafe serve --stdio invocation: ['-R', 'narf', 'serv', '--stdio']
429 429 [255]
430 430 #endif
431 431
432 432 Test hg-ssh using a helper script that will restore PYTHONPATH (which might
433 433 have been cleared by a hg.exe wrapper) and invoke hg-ssh with the right
434 434 parameters:
435 435
436 436 $ cat > ssh.sh << EOF
437 437 > userhost="\$1"
438 438 > SSH_ORIGINAL_COMMAND="\$2"
439 439 > export SSH_ORIGINAL_COMMAND
440 440 > PYTHONPATH="$PYTHONPATH"
441 441 > export PYTHONPATH
442 442 > "$PYTHON" "$TESTDIR/../contrib/hg-ssh" "$TESTTMP/a repo"
443 443 > EOF
444 444
445 445 $ hg id --ssh "sh ssh.sh" "ssh://user@dummy/a repo"
446 446 73649e48688a
447 447
448 448 $ hg id --ssh "sh ssh.sh" "ssh://user@dummy/a'repo"
449 449 remote: Illegal repository "$TESTTMP/a'repo"
450 450 abort: no suitable response from remote hg
451 451 [255]
452 452
453 453 $ hg id --ssh "sh ssh.sh" --remotecmd hacking "ssh://user@dummy/a'repo"
454 454 remote: Illegal command "hacking -R 'a'\''repo' serve --stdio"
455 455 abort: no suitable response from remote hg
456 456 [255]
457 457
458 458 $ SSH_ORIGINAL_COMMAND="'hg' -R 'a'repo' serve --stdio" "$PYTHON" "$TESTDIR/../contrib/hg-ssh"
459 459 Illegal command "'hg' -R 'a'repo' serve --stdio": No closing quotation
460 460 [255]
461 461
462 462 Test hg-ssh in read-only mode:
463 463
464 464 $ cat > ssh.sh << EOF
465 465 > userhost="\$1"
466 466 > SSH_ORIGINAL_COMMAND="\$2"
467 467 > export SSH_ORIGINAL_COMMAND
468 468 > PYTHONPATH="$PYTHONPATH"
469 469 > export PYTHONPATH
470 470 > "$PYTHON" "$TESTDIR/../contrib/hg-ssh" --read-only "$TESTTMP/remote"
471 471 > EOF
472 472
473 473 $ hg clone --ssh "sh ssh.sh" "ssh://user@dummy/$TESTTMP/remote" read-only-local
474 474 requesting all changes
475 475 adding changesets
476 476 adding manifests
477 477 adding file changes
478 478 added 6 changesets with 5 changes to 4 files (+1 heads)
479 479 new changesets 1160648e36ce:1383141674ec
480 480 updating to branch default
481 481 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
482 482
483 483 $ cd read-only-local
484 484 $ echo "baz" > bar
485 485 $ hg ci -A -m "unpushable commit" bar
486 486 $ hg push --ssh "sh ../ssh.sh"
487 487 pushing to ssh://user@dummy/*/remote (glob)
488 488 searching for changes
489 489 remote: Permission denied
490 490 remote: pretxnopen.hg-ssh hook failed
491 491 abort: push failed on remote
492 492 [100]
493 493
494 494 $ cd $TESTTMP
495 495
496 496 stderr from remote commands should be printed before stdout from local code (issue4336)
497 497
498 498 $ hg clone remote stderr-ordering
499 499 updating to branch default
500 500 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
501 501 $ cd stderr-ordering
502 502 $ cat >> localwrite.py << EOF
503 503 > from mercurial import exchange, extensions
504 504 >
505 505 > def wrappedpush(orig, repo, *args, **kwargs):
506 506 > res = orig(repo, *args, **kwargs)
507 507 > repo.ui.write(b'local stdout\n')
508 508 > repo.ui.flush()
509 509 > return res
510 510 >
511 511 > def extsetup(ui):
512 512 > extensions.wrapfunction(exchange, b'push', wrappedpush)
513 513 > EOF
514 514
515 515 $ cat >> .hg/hgrc << EOF
516 516 > [paths]
517 517 > default-push = ssh://user@dummy/remote
518 518 > [ui]
519 519 > ssh = "$PYTHON" "$TESTDIR/dummyssh"
520 520 > [extensions]
521 521 > localwrite = localwrite.py
522 522 > EOF
523 523
524 524 $ echo localwrite > foo
525 525 $ hg commit -m 'testing localwrite'
526 526 $ hg push
527 527 pushing to ssh://user@dummy/remote
528 528 searching for changes
529 529 remote: adding changesets
530 530 remote: adding manifests
531 531 remote: adding file changes
532 532 remote: added 1 changesets with 1 changes to 1 files (py3 !)
533 533 remote: added 1 changesets with 1 changes to 1 files (no-py3 no-chg !)
534 534 remote: KABOOM
535 535 remote: KABOOM IN PROCESS
536 536 remote: added 1 changesets with 1 changes to 1 files (no-py3 chg !)
537 537 local stdout
538 538
539 539 debug output
540 540
541 541 $ hg pull --debug ssh://user@dummy/remote --config devel.debug.peer-request=yes
542 542 pulling from ssh://user@dummy/remote
543 running .* ".*/dummyssh" ['"]user@dummy['"] ('|")hg -R remote serve --stdio('|") (re)
543 running .* ".*[/\\]dummyssh" ['"]user@dummy['"] ['"]hg -R remote serve --stdio['"] (re)
544 544 sending upgrade request: * proto=exp-ssh-v2-0003 (glob) (sshv2 !)
545 545 devel-peer-request: hello+between
546 546 devel-peer-request: pairs: 81 bytes
547 547 sending hello command
548 548 sending between command
549 549 remote: 444 (sshv1 no-rust !)
550 550 remote: 463 (sshv1 rust !)
551 551 protocol upgraded to exp-ssh-v2-0003 (sshv2 !)
552 552 remote: capabilities: batch branchmap $USUAL_BUNDLE2_CAPS$ changegroupsubset getbundle known lookup protocaps pushkey streamreqs=generaldelta,revlogv1,sparserevlog unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash (no-rust !)
553 553 remote: capabilities: batch branchmap $USUAL_BUNDLE2_CAPS$ changegroupsubset getbundle known lookup protocaps pushkey streamreqs=generaldelta,persistent-nodemap,revlogv1,sparserevlog unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash (rust !)
554 554 remote: 1 (sshv1 !)
555 555 devel-peer-request: protocaps
556 556 devel-peer-request: caps: * bytes (glob)
557 557 sending protocaps command
558 558 query 1; heads
559 559 devel-peer-request: batched-content
560 560 devel-peer-request: - heads (0 arguments)
561 561 devel-peer-request: - known (1 arguments)
562 562 devel-peer-request: batch
563 563 devel-peer-request: cmds: 141 bytes
564 564 sending batch command
565 565 searching for changes
566 566 all remote heads known locally
567 567 no changes found
568 568 devel-peer-request: getbundle
569 569 devel-peer-request: bookmarks: 1 bytes
570 570 devel-peer-request: bundlecaps: 270 bytes
571 571 devel-peer-request: cg: 1 bytes
572 572 devel-peer-request: common: 122 bytes
573 573 devel-peer-request: heads: 122 bytes
574 574 devel-peer-request: listkeys: 9 bytes
575 575 devel-peer-request: phases: 1 bytes
576 576 sending getbundle command
577 577 bundle2-input-bundle: with-transaction
578 578 bundle2-input-part: "bookmarks" supported
579 579 bundle2-input-part: total payload size 26
580 580 bundle2-input-part: "listkeys" (params: 1 mandatory) supported
581 581 bundle2-input-part: total payload size 45
582 582 bundle2-input-part: "phase-heads" supported
583 583 bundle2-input-part: total payload size 72
584 584 bundle2-input-bundle: 3 parts total
585 585 checking for updated bookmarks
586 586
587 587 $ cd $TESTTMP
588 588
589 589 $ cat dummylog
590 590 Got arguments 1:user@dummy 2:hg -R nonexistent serve --stdio
591 591 Got arguments 1:user@dummy 2:hg -R nonexistent serve --stdio
592 592 Got arguments 1:user@dummy 2:hg -R $TESTTMP/nonexistent serve --stdio
593 593 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
594 594 Got arguments 1:user@dummy 2:hg -R local-stream serve --stdio (no-reposimplestore !)
595 595 Got arguments 1:user@dummy 2:hg -R remote serve --stdio (no-reposimplestore !)
596 596 Got arguments 1:user@dummy 2:hg -R remote serve --stdio (no-reposimplestore !)
597 597 Got arguments 1:user@dummy 2:hg -R doesnotexist serve --stdio
598 598 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
599 599 Got arguments 1:user@dummy 2:hg -R local serve --stdio
600 600 Got arguments 1:user@dummy 2:hg -R $TESTTMP/local serve --stdio
601 601 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
602 602 changegroup-in-remote hook: HG_BUNDLE2=1
603 603 HG_HOOKNAME=changegroup
604 604 HG_HOOKTYPE=changegroup
605 605 HG_NODE=a28a9d1a809cab7d4e2fde4bee738a9ede948b60
606 606 HG_NODE_LAST=a28a9d1a809cab7d4e2fde4bee738a9ede948b60
607 607 HG_SOURCE=serve
608 608 HG_TXNID=TXN:$ID$
609 609 HG_TXNNAME=serve
610 610 HG_URL=remote:ssh:$LOCALIP
611 611
612 612 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
613 613 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
614 614 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
615 615 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
616 616 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
617 617 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
618 618 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
619 619 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
620 620 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
621 621 changegroup-in-remote hook: HG_BUNDLE2=1
622 622 HG_HOOKNAME=changegroup
623 623 HG_HOOKTYPE=changegroup
624 624 HG_NODE=1383141674ec756a6056f6a9097618482fe0f4a6
625 625 HG_NODE_LAST=1383141674ec756a6056f6a9097618482fe0f4a6
626 626 HG_SOURCE=serve
627 627 HG_TXNID=TXN:$ID$
628 628 HG_TXNNAME=serve
629 629 HG_URL=remote:ssh:$LOCALIP
630 630
631 631 Got arguments 1:user@dummy 2:chg -R remote serve --stdio (chg !)
632 632 changegroup-in-remote hook: HG_BUNDLE2=1 (chg !)
633 633 HG_HOOKNAME=changegroup (chg !)
634 634 HG_HOOKTYPE=changegroup (chg !)
635 635 HG_NODE=1383141674ec756a6056f6a9097618482fe0f4a6 (chg !)
636 636 HG_NODE_LAST=1383141674ec756a6056f6a9097618482fe0f4a6 (chg !)
637 637 HG_SOURCE=serve (chg !)
638 638 HG_TXNID=TXN:$ID$ (chg !)
639 639 HG_TXNNAME=serve (chg !)
640 640 HG_URL=remote:ssh:$LOCALIP (chg !)
641 641 (chg !)
642 642 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
643 643 Got arguments 1:user@dummy 2:hg init 'a repo'
644 644 Got arguments 1:user@dummy 2:hg -R 'a repo' serve --stdio
645 645 Got arguments 1:user@dummy 2:hg -R 'a repo' serve --stdio
646 646 Got arguments 1:user@dummy 2:hg -R 'a repo' serve --stdio
647 647 Got arguments 1:user@dummy 2:hg -R 'a repo' serve --stdio
648 648 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
649 649 changegroup-in-remote hook: HG_BUNDLE2=1
650 650 HG_HOOKNAME=changegroup
651 651 HG_HOOKTYPE=changegroup
652 652 HG_NODE=65c38f4125f9602c8db4af56530cc221d93b8ef8
653 653 HG_NODE_LAST=65c38f4125f9602c8db4af56530cc221d93b8ef8
654 654 HG_SOURCE=serve
655 655 HG_TXNID=TXN:$ID$
656 656 HG_TXNNAME=serve
657 657 HG_URL=remote:ssh:$LOCALIP
658 658
659 659 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
660 660
661 661
662 662 remote hook failure is attributed to remote
663 663
664 664 $ cat > $TESTTMP/failhook << EOF
665 665 > def hook(ui, repo, **kwargs):
666 666 > ui.write(b'hook failure!\n')
667 667 > ui.flush()
668 668 > return 1
669 669 > EOF
670 670
671 671 $ echo "pretxnchangegroup.fail = python:$TESTTMP/failhook:hook" >> remote/.hg/hgrc
672 672
673 673 $ hg -q --config ui.ssh="\"$PYTHON\" $TESTDIR/dummyssh" clone ssh://user@dummy/remote hookout
674 674 $ cd hookout
675 675 $ touch hookfailure
676 676 $ hg -q commit -A -m 'remote hook failure'
677 677 $ hg --config ui.ssh="\"$PYTHON\" $TESTDIR/dummyssh" push
678 678 pushing to ssh://user@dummy/remote
679 679 searching for changes
680 680 remote: adding changesets
681 681 remote: adding manifests
682 682 remote: adding file changes
683 683 remote: hook failure!
684 684 remote: transaction abort!
685 685 remote: rollback completed
686 686 remote: pretxnchangegroup.fail hook failed
687 687 abort: push failed on remote
688 688 [100]
689 689
690 690 abort during pull is properly reported as such
691 691
692 692 $ echo morefoo >> ../remote/foo
693 693 $ hg -R ../remote commit --message "more foo to be pulled"
694 694 $ cat >> ../remote/.hg/hgrc << EOF
695 695 > [extensions]
696 696 > crash = ${TESTDIR}/crashgetbundler.py
697 697 > EOF
698 698 $ hg --config ui.ssh="\"$PYTHON\" $TESTDIR/dummyssh" pull
699 699 pulling from ssh://user@dummy/remote
700 700 searching for changes
701 701 remote: abort: this is an exercise
702 702 abort: pull failed on remote
703 703 [100]
704 704
705 705 abort with no error hint when there is a ssh problem when pulling
706 706
707 707 $ hg pull ssh://brokenrepository -e "\"$PYTHON\" \"$TESTDIR/dummyssh\""
708 708 pulling from ssh://brokenrepository/
709 709 abort: no suitable response from remote hg
710 710 [255]
711 711
712 712 abort with configured error hint when there is a ssh problem when pulling
713 713
714 714 $ hg pull ssh://brokenrepository -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" \
715 715 > --config ui.ssherrorhint="Please see http://company/internalwiki/ssh.html"
716 716 pulling from ssh://brokenrepository/
717 717 abort: no suitable response from remote hg
718 718 (Please see http://company/internalwiki/ssh.html)
719 719 [255]
720 720
721 721 test that custom environment is passed down to ssh executable
722 722 $ cat >>dumpenv <<EOF
723 723 > #! /bin/sh
724 724 > echo \$VAR >&2
725 725 > EOF
726 726 $ chmod +x dumpenv
727 727 $ hg pull ssh://something --config ui.ssh="sh dumpenv"
728 728 pulling from ssh://something/
729 729 remote:
730 730 abort: no suitable response from remote hg
731 731 [255]
732 732 $ hg pull ssh://something --config ui.ssh="sh dumpenv" --config sshenv.VAR=17
733 733 pulling from ssh://something/
734 734 remote: 17
735 735 abort: no suitable response from remote hg
736 736 [255]
737 737
General Comments 0
You need to be logged in to leave comments. Login now