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