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