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