##// END OF EJS Templates
tests: load json with no fallback...
Yuya Nishihara -
r28126:562a073a default
parent child Browse files
Show More
@@ -1,479 +1,464 b''
1 1 from __future__ import absolute_import
2 2
3 3 import errno
4 4 import os
5 5 import re
6 6 import socket
7 7 import stat
8 8 import subprocess
9 9 import sys
10 10 import tempfile
11 11
12 12 tempprefix = 'hg-hghave-'
13 13
14 14 checks = {
15 15 "true": (lambda: True, "yak shaving"),
16 16 "false": (lambda: False, "nail clipper"),
17 17 }
18 18
19 19 def check(name, desc):
20 20 def decorator(func):
21 21 checks[name] = (func, desc)
22 22 return func
23 23 return decorator
24 24
25 25 def checkfeatures(features):
26 26 result = {
27 27 'error': [],
28 28 'missing': [],
29 29 'skipped': [],
30 30 }
31 31
32 32 for feature in features:
33 33 negate = feature.startswith('no-')
34 34 if negate:
35 35 feature = feature[3:]
36 36
37 37 if feature not in checks:
38 38 result['missing'].append(feature)
39 39 continue
40 40
41 41 check, desc = checks[feature]
42 42 try:
43 43 available = check()
44 44 except Exception:
45 45 result['error'].append('hghave check failed: %s' % feature)
46 46 continue
47 47
48 48 if not negate and not available:
49 49 result['skipped'].append('missing feature: %s' % desc)
50 50 elif negate and available:
51 51 result['skipped'].append('system supports %s' % desc)
52 52
53 53 return result
54 54
55 55 def require(features):
56 56 """Require that features are available, exiting if not."""
57 57 result = checkfeatures(features)
58 58
59 59 for missing in result['missing']:
60 60 sys.stderr.write('skipped: unknown feature: %s\n' % missing)
61 61 for msg in result['skipped']:
62 62 sys.stderr.write('skipped: %s\n' % msg)
63 63 for msg in result['error']:
64 64 sys.stderr.write('%s\n' % msg)
65 65
66 66 if result['missing']:
67 67 sys.exit(2)
68 68
69 69 if result['skipped'] or result['error']:
70 70 sys.exit(1)
71 71
72 72 def matchoutput(cmd, regexp, ignorestatus=False):
73 73 """Return the match object if cmd executes successfully and its output
74 74 is matched by the supplied regular expression.
75 75 """
76 76 r = re.compile(regexp)
77 77 try:
78 78 p = subprocess.Popen(
79 79 cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
80 80 except OSError as e:
81 81 if e.errno != errno.ENOENT:
82 82 raise
83 83 ret = -1
84 84 ret = p.wait()
85 85 s = p.stdout.read()
86 86 return (ignorestatus or not ret) and r.search(s)
87 87
88 88 @check("baz", "GNU Arch baz client")
89 89 def has_baz():
90 90 return matchoutput('baz --version 2>&1', r'baz Bazaar version')
91 91
92 92 @check("bzr", "Canonical's Bazaar client")
93 93 def has_bzr():
94 94 try:
95 95 import bzrlib
96 96 return bzrlib.__doc__ is not None
97 97 except ImportError:
98 98 return False
99 99
100 100 @check("bzr114", "Canonical's Bazaar client >= 1.14")
101 101 def has_bzr114():
102 102 try:
103 103 import bzrlib
104 104 return (bzrlib.__doc__ is not None
105 105 and bzrlib.version_info[:2] >= (1, 14))
106 106 except ImportError:
107 107 return False
108 108
109 109 @check("cvs", "cvs client/server")
110 110 def has_cvs():
111 111 re = r'Concurrent Versions System.*?server'
112 112 return matchoutput('cvs --version 2>&1', re) and not has_msys()
113 113
114 114 @check("cvs112", "cvs client/server >= 1.12")
115 115 def has_cvs112():
116 116 re = r'Concurrent Versions System \(CVS\) 1.12.*?server'
117 117 return matchoutput('cvs --version 2>&1', re) and not has_msys()
118 118
119 119 @check("darcs", "darcs client")
120 120 def has_darcs():
121 121 return matchoutput('darcs --version', r'2\.[2-9]', True)
122 122
123 123 @check("mtn", "monotone client (>= 1.0)")
124 124 def has_mtn():
125 125 return matchoutput('mtn --version', r'monotone', True) and not matchoutput(
126 126 'mtn --version', r'monotone 0\.', True)
127 127
128 128 @check("eol-in-paths", "end-of-lines in paths")
129 129 def has_eol_in_paths():
130 130 try:
131 131 fd, path = tempfile.mkstemp(dir='.', prefix=tempprefix, suffix='\n\r')
132 132 os.close(fd)
133 133 os.remove(path)
134 134 return True
135 135 except (IOError, OSError):
136 136 return False
137 137
138 138 @check("execbit", "executable bit")
139 139 def has_executablebit():
140 140 try:
141 141 EXECFLAGS = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
142 142 fh, fn = tempfile.mkstemp(dir='.', prefix=tempprefix)
143 143 try:
144 144 os.close(fh)
145 145 m = os.stat(fn).st_mode & 0o777
146 146 new_file_has_exec = m & EXECFLAGS
147 147 os.chmod(fn, m ^ EXECFLAGS)
148 148 exec_flags_cannot_flip = ((os.stat(fn).st_mode & 0o777) == m)
149 149 finally:
150 150 os.unlink(fn)
151 151 except (IOError, OSError):
152 152 # we don't care, the user probably won't be able to commit anyway
153 153 return False
154 154 return not (new_file_has_exec or exec_flags_cannot_flip)
155 155
156 156 @check("icasefs", "case insensitive file system")
157 157 def has_icasefs():
158 158 # Stolen from mercurial.util
159 159 fd, path = tempfile.mkstemp(dir='.', prefix=tempprefix)
160 160 os.close(fd)
161 161 try:
162 162 s1 = os.stat(path)
163 163 d, b = os.path.split(path)
164 164 p2 = os.path.join(d, b.upper())
165 165 if path == p2:
166 166 p2 = os.path.join(d, b.lower())
167 167 try:
168 168 s2 = os.stat(p2)
169 169 return s2 == s1
170 170 except OSError:
171 171 return False
172 172 finally:
173 173 os.remove(path)
174 174
175 175 @check("fifo", "named pipes")
176 176 def has_fifo():
177 177 if getattr(os, "mkfifo", None) is None:
178 178 return False
179 179 name = tempfile.mktemp(dir='.', prefix=tempprefix)
180 180 try:
181 181 os.mkfifo(name)
182 182 os.unlink(name)
183 183 return True
184 184 except OSError:
185 185 return False
186 186
187 187 @check("killdaemons", 'killdaemons.py support')
188 188 def has_killdaemons():
189 189 return True
190 190
191 191 @check("cacheable", "cacheable filesystem")
192 192 def has_cacheable_fs():
193 193 from mercurial import util
194 194
195 195 fd, path = tempfile.mkstemp(dir='.', prefix=tempprefix)
196 196 os.close(fd)
197 197 try:
198 198 return util.cachestat(path).cacheable()
199 199 finally:
200 200 os.remove(path)
201 201
202 202 @check("lsprof", "python lsprof module")
203 203 def has_lsprof():
204 204 try:
205 205 import _lsprof
206 206 _lsprof.Profiler # silence unused import warning
207 207 return True
208 208 except ImportError:
209 209 return False
210 210
211 211 @check("gettext", "GNU Gettext (msgfmt)")
212 212 def has_gettext():
213 213 return matchoutput('msgfmt --version', 'GNU gettext-tools')
214 214
215 215 @check("git", "git command line client")
216 216 def has_git():
217 217 return matchoutput('git --version 2>&1', r'^git version')
218 218
219 219 @check("docutils", "Docutils text processing library")
220 220 def has_docutils():
221 221 try:
222 222 from docutils.core import publish_cmdline
223 223 publish_cmdline # silence unused import
224 224 return True
225 225 except ImportError:
226 226 return False
227 227
228 228 def getsvnversion():
229 229 m = matchoutput('svn --version --quiet 2>&1', r'^(\d+)\.(\d+)')
230 230 if not m:
231 231 return (0, 0)
232 232 return (int(m.group(1)), int(m.group(2)))
233 233
234 234 @check("svn15", "subversion client and admin tools >= 1.5")
235 235 def has_svn15():
236 236 return getsvnversion() >= (1, 5)
237 237
238 238 @check("svn13", "subversion client and admin tools >= 1.3")
239 239 def has_svn13():
240 240 return getsvnversion() >= (1, 3)
241 241
242 242 @check("svn", "subversion client and admin tools")
243 243 def has_svn():
244 244 return matchoutput('svn --version 2>&1', r'^svn, version') and \
245 245 matchoutput('svnadmin --version 2>&1', r'^svnadmin, version')
246 246
247 247 @check("svn-bindings", "subversion python bindings")
248 248 def has_svn_bindings():
249 249 try:
250 250 import svn.core
251 251 version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR
252 252 if version < (1, 4):
253 253 return False
254 254 return True
255 255 except ImportError:
256 256 return False
257 257
258 258 @check("p4", "Perforce server and client")
259 259 def has_p4():
260 260 return (matchoutput('p4 -V', r'Rev\. P4/') and
261 261 matchoutput('p4d -V', r'Rev\. P4D/'))
262 262
263 263 @check("symlink", "symbolic links")
264 264 def has_symlink():
265 265 if getattr(os, "symlink", None) is None:
266 266 return False
267 267 name = tempfile.mktemp(dir='.', prefix=tempprefix)
268 268 try:
269 269 os.symlink(".", name)
270 270 os.unlink(name)
271 271 return True
272 272 except (OSError, AttributeError):
273 273 return False
274 274
275 275 @check("hardlink", "hardlinks")
276 276 def has_hardlink():
277 277 from mercurial import util
278 278 fh, fn = tempfile.mkstemp(dir='.', prefix=tempprefix)
279 279 os.close(fh)
280 280 name = tempfile.mktemp(dir='.', prefix=tempprefix)
281 281 try:
282 282 util.oslink(fn, name)
283 283 os.unlink(name)
284 284 return True
285 285 except OSError:
286 286 return False
287 287 finally:
288 288 os.unlink(fn)
289 289
290 290 @check("tla", "GNU Arch tla client")
291 291 def has_tla():
292 292 return matchoutput('tla --version 2>&1', r'The GNU Arch Revision')
293 293
294 294 @check("gpg", "gpg client")
295 295 def has_gpg():
296 296 return matchoutput('gpg --version 2>&1', r'GnuPG')
297 297
298 298 @check("unix-permissions", "unix-style permissions")
299 299 def has_unix_permissions():
300 300 d = tempfile.mkdtemp(dir='.', prefix=tempprefix)
301 301 try:
302 302 fname = os.path.join(d, 'foo')
303 303 for umask in (0o77, 0o07, 0o22):
304 304 os.umask(umask)
305 305 f = open(fname, 'w')
306 306 f.close()
307 307 mode = os.stat(fname).st_mode
308 308 os.unlink(fname)
309 309 if mode & 0o777 != ~umask & 0o666:
310 310 return False
311 311 return True
312 312 finally:
313 313 os.rmdir(d)
314 314
315 315 @check("unix-socket", "AF_UNIX socket family")
316 316 def has_unix_socket():
317 317 return getattr(socket, 'AF_UNIX', None) is not None
318 318
319 319 @check("root", "root permissions")
320 320 def has_root():
321 321 return getattr(os, 'geteuid', None) and os.geteuid() == 0
322 322
323 323 @check("pyflakes", "Pyflakes python linter")
324 324 def has_pyflakes():
325 325 return matchoutput("sh -c \"echo 'import re' 2>&1 | pyflakes\"",
326 326 r"<stdin>:1: 're' imported but unused",
327 327 True)
328 328
329 329 @check("pygments", "Pygments source highlighting library")
330 330 def has_pygments():
331 331 try:
332 332 import pygments
333 333 pygments.highlight # silence unused import warning
334 334 return True
335 335 except ImportError:
336 336 return False
337 337
338 @check("json", "some json module available")
339 def has_json():
340 try:
341 import json
342 json.dumps
343 return True
344 except ImportError:
345 try:
346 import simplejson as json
347 json.dumps
348 return True
349 except ImportError:
350 pass
351 return False
352
353 338 @check("outer-repo", "outer repo")
354 339 def has_outer_repo():
355 340 # failing for other reasons than 'no repo' imply that there is a repo
356 341 return not matchoutput('hg root 2>&1',
357 342 r'abort: no repository found', True)
358 343
359 344 @check("ssl", ("(python >= 2.6 ssl module and python OpenSSL) "
360 345 "OR python >= 2.7.9 ssl"))
361 346 def has_ssl():
362 347 try:
363 348 import ssl
364 349 if getattr(ssl, 'create_default_context', False):
365 350 return True
366 351 import OpenSSL
367 352 OpenSSL.SSL.Context
368 353 return True
369 354 except ImportError:
370 355 return False
371 356
372 357 @check("sslcontext", "python >= 2.7.9 ssl")
373 358 def has_sslcontext():
374 359 try:
375 360 import ssl
376 361 ssl.SSLContext
377 362 return True
378 363 except (ImportError, AttributeError):
379 364 return False
380 365
381 366 @check("defaultcacerts", "can verify SSL certs by system's CA certs store")
382 367 def has_defaultcacerts():
383 368 from mercurial import sslutil
384 369 return sslutil._defaultcacerts() != '!'
385 370
386 371 @check("windows", "Windows")
387 372 def has_windows():
388 373 return os.name == 'nt'
389 374
390 375 @check("system-sh", "system() uses sh")
391 376 def has_system_sh():
392 377 return os.name != 'nt'
393 378
394 379 @check("serve", "platform and python can manage 'hg serve -d'")
395 380 def has_serve():
396 381 return os.name != 'nt' # gross approximation
397 382
398 383 @check("test-repo", "running tests from repository")
399 384 def has_test_repo():
400 385 t = os.environ["TESTDIR"]
401 386 return os.path.isdir(os.path.join(t, "..", ".hg"))
402 387
403 388 @check("tic", "terminfo compiler and curses module")
404 389 def has_tic():
405 390 try:
406 391 import curses
407 392 curses.COLOR_BLUE
408 393 return matchoutput('test -x "`which tic`"', '')
409 394 except ImportError:
410 395 return False
411 396
412 397 @check("msys", "Windows with MSYS")
413 398 def has_msys():
414 399 return os.getenv('MSYSTEM')
415 400
416 401 @check("aix", "AIX")
417 402 def has_aix():
418 403 return sys.platform.startswith("aix")
419 404
420 405 @check("osx", "OS X")
421 406 def has_osx():
422 407 return sys.platform == 'darwin'
423 408
424 409 @check("docker", "docker support")
425 410 def has_docker():
426 411 pat = r'A self-sufficient runtime for linux containers\.'
427 412 if matchoutput('docker --help', pat):
428 413 if 'linux' not in sys.platform:
429 414 # TODO: in theory we should be able to test docker-based
430 415 # package creation on non-linux using boot2docker, but in
431 416 # practice that requires extra coordination to make sure
432 417 # $TESTTEMP is going to be visible at the same path to the
433 418 # boot2docker VM. If we figure out how to verify that, we
434 419 # can use the following instead of just saying False:
435 420 # return 'DOCKER_HOST' in os.environ
436 421 return False
437 422
438 423 return True
439 424 return False
440 425
441 426 @check("debhelper", "debian packaging tools")
442 427 def has_debhelper():
443 428 dpkg = matchoutput('dpkg --version',
444 429 "Debian `dpkg' package management program")
445 430 dh = matchoutput('dh --help',
446 431 'dh is a part of debhelper.', ignorestatus=True)
447 432 dh_py2 = matchoutput('dh_python2 --help',
448 433 'other supported Python versions')
449 434 return dpkg and dh and dh_py2
450 435
451 436 @check("absimport", "absolute_import in __future__")
452 437 def has_absimport():
453 438 import __future__
454 439 from mercurial import util
455 440 return util.safehasattr(__future__, "absolute_import")
456 441
457 442 @check("py3k", "running with Python 3.x")
458 443 def has_py3k():
459 444 return 3 == sys.version_info[0]
460 445
461 446 @check("pure", "running with pure Python code")
462 447 def has_pure():
463 448 return any([
464 449 os.environ.get("HGMODULEPOLICY") == "py",
465 450 os.environ.get("HGTEST_RUN_TESTS_PURE") == "--pure",
466 451 ])
467 452
468 453 @check("slow", "allow slow tests")
469 454 def has_slow():
470 455 return os.environ.get('HGTEST_SLOW') == 'slow'
471 456
472 457 @check("hypothesis", "is Hypothesis installed")
473 458 def has_hypothesis():
474 459 try:
475 460 import hypothesis
476 461 hypothesis.given
477 462 return True
478 463 except ImportError:
479 464 return False
@@ -1,2392 +1,2382 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 print_function
47 47
48 48 from distutils import version
49 49 import difflib
50 50 import errno
51 import json
51 52 import optparse
52 53 import os
53 54 import shutil
54 55 import subprocess
55 56 import signal
56 57 import socket
57 58 import sys
58 59 import tempfile
59 60 import time
60 61 import random
61 62 import re
62 63 import threading
63 64 import killdaemons as killmod
64 65 try:
65 66 import Queue as queue
66 67 except ImportError:
67 68 import queue
68 69 from xml.dom import minidom
69 70 import unittest
70 71
71 72 osenvironb = getattr(os, 'environb', os.environ)
72
73 try:
74 import json
75 except ImportError:
76 try:
77 import simplejson as json
78 except ImportError:
79 json = None
80
81 73 processlock = threading.Lock()
82 74
83 75 if sys.version_info > (3, 5, 0):
84 76 PYTHON3 = True
85 77 xrange = range # we use xrange in one place, and we'd rather not use range
86 78 def _bytespath(p):
87 79 return p.encode('utf-8')
88 80
89 81 def _strpath(p):
90 82 return p.decode('utf-8')
91 83
92 84 elif sys.version_info >= (3, 0, 0):
93 85 print('%s is only supported on Python 3.5+ and 2.6-2.7, not %s' %
94 86 (sys.argv[0], '.'.join(str(v) for v in sys.version_info[:3])))
95 87 sys.exit(70) # EX_SOFTWARE from `man 3 sysexit`
96 88 else:
97 89 PYTHON3 = False
98 90
99 91 # In python 2.x, path operations are generally done using
100 92 # bytestrings by default, so we don't have to do any extra
101 93 # fiddling there. We define the wrapper functions anyway just to
102 94 # help keep code consistent between platforms.
103 95 def _bytespath(p):
104 96 return p
105 97
106 98 _strpath = _bytespath
107 99
108 100 # For Windows support
109 101 wifexited = getattr(os, "WIFEXITED", lambda x: False)
110 102
111 103 def checkportisavailable(port):
112 104 """return true if a port seems free to bind on localhost"""
113 105 try:
114 106 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
115 107 s.bind(('localhost', port))
116 108 s.close()
117 109 return True
118 110 except socket.error as exc:
119 111 if not exc.errno == errno.EADDRINUSE:
120 112 raise
121 113 return False
122 114
123 115 closefds = os.name == 'posix'
124 116 def Popen4(cmd, wd, timeout, env=None):
125 117 processlock.acquire()
126 118 p = subprocess.Popen(cmd, shell=True, bufsize=-1, cwd=wd, env=env,
127 119 close_fds=closefds,
128 120 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
129 121 stderr=subprocess.STDOUT)
130 122 processlock.release()
131 123
132 124 p.fromchild = p.stdout
133 125 p.tochild = p.stdin
134 126 p.childerr = p.stderr
135 127
136 128 p.timeout = False
137 129 if timeout:
138 130 def t():
139 131 start = time.time()
140 132 while time.time() - start < timeout and p.returncode is None:
141 133 time.sleep(.1)
142 134 p.timeout = True
143 135 if p.returncode is None:
144 136 terminate(p)
145 137 threading.Thread(target=t).start()
146 138
147 139 return p
148 140
149 141 PYTHON = _bytespath(sys.executable.replace('\\', '/'))
150 142 IMPL_PATH = b'PYTHONPATH'
151 143 if 'java' in sys.platform:
152 144 IMPL_PATH = b'JYTHONPATH'
153 145
154 146 defaults = {
155 147 'jobs': ('HGTEST_JOBS', 1),
156 148 'timeout': ('HGTEST_TIMEOUT', 180),
157 149 'slowtimeout': ('HGTEST_SLOWTIMEOUT', 500),
158 150 'port': ('HGTEST_PORT', 20059),
159 151 'shell': ('HGTEST_SHELL', 'sh'),
160 152 }
161 153
162 154 def parselistfiles(files, listtype, warn=True):
163 155 entries = dict()
164 156 for filename in files:
165 157 try:
166 158 path = os.path.expanduser(os.path.expandvars(filename))
167 159 f = open(path, "rb")
168 160 except IOError as err:
169 161 if err.errno != errno.ENOENT:
170 162 raise
171 163 if warn:
172 164 print("warning: no such %s file: %s" % (listtype, filename))
173 165 continue
174 166
175 167 for line in f.readlines():
176 168 line = line.split(b'#', 1)[0].strip()
177 169 if line:
178 170 entries[line] = filename
179 171
180 172 f.close()
181 173 return entries
182 174
183 175 def getparser():
184 176 """Obtain the OptionParser used by the CLI."""
185 177 parser = optparse.OptionParser("%prog [options] [tests]")
186 178
187 179 # keep these sorted
188 180 parser.add_option("--blacklist", action="append",
189 181 help="skip tests listed in the specified blacklist file")
190 182 parser.add_option("--whitelist", action="append",
191 183 help="always run tests listed in the specified whitelist file")
192 184 parser.add_option("--changed", type="string",
193 185 help="run tests that are changed in parent rev or working directory")
194 186 parser.add_option("-C", "--annotate", action="store_true",
195 187 help="output files annotated with coverage")
196 188 parser.add_option("-c", "--cover", action="store_true",
197 189 help="print a test coverage report")
198 190 parser.add_option("-d", "--debug", action="store_true",
199 191 help="debug mode: write output of test scripts to console"
200 192 " rather than capturing and diffing it (disables timeout)")
201 193 parser.add_option("-f", "--first", action="store_true",
202 194 help="exit on the first test failure")
203 195 parser.add_option("-H", "--htmlcov", action="store_true",
204 196 help="create an HTML report of the coverage of the files")
205 197 parser.add_option("-i", "--interactive", action="store_true",
206 198 help="prompt to accept changed output")
207 199 parser.add_option("-j", "--jobs", type="int",
208 200 help="number of jobs to run in parallel"
209 201 " (default: $%s or %d)" % defaults['jobs'])
210 202 parser.add_option("--keep-tmpdir", action="store_true",
211 203 help="keep temporary directory after running tests")
212 204 parser.add_option("-k", "--keywords",
213 205 help="run tests matching keywords")
214 206 parser.add_option("-l", "--local", action="store_true",
215 207 help="shortcut for --with-hg=<testdir>/../hg")
216 208 parser.add_option("--loop", action="store_true",
217 209 help="loop tests repeatedly")
218 210 parser.add_option("--runs-per-test", type="int", dest="runs_per_test",
219 211 help="run each test N times (default=1)", default=1)
220 212 parser.add_option("-n", "--nodiff", action="store_true",
221 213 help="skip showing test changes")
222 214 parser.add_option("-p", "--port", type="int",
223 215 help="port on which servers should listen"
224 216 " (default: $%s or %d)" % defaults['port'])
225 217 parser.add_option("--compiler", type="string",
226 218 help="compiler to build with")
227 219 parser.add_option("--pure", action="store_true",
228 220 help="use pure Python code instead of C extensions")
229 221 parser.add_option("-R", "--restart", action="store_true",
230 222 help="restart at last error")
231 223 parser.add_option("-r", "--retest", action="store_true",
232 224 help="retest failed tests")
233 225 parser.add_option("-S", "--noskips", action="store_true",
234 226 help="don't report skip tests verbosely")
235 227 parser.add_option("--shell", type="string",
236 228 help="shell to use (default: $%s or %s)" % defaults['shell'])
237 229 parser.add_option("-t", "--timeout", type="int",
238 230 help="kill errant tests after TIMEOUT seconds"
239 231 " (default: $%s or %d)" % defaults['timeout'])
240 232 parser.add_option("--slowtimeout", type="int",
241 233 help="kill errant slow tests after SLOWTIMEOUT seconds"
242 234 " (default: $%s or %d)" % defaults['slowtimeout'])
243 235 parser.add_option("--time", action="store_true",
244 236 help="time how long each test takes")
245 237 parser.add_option("--json", action="store_true",
246 238 help="store test result data in 'report.json' file")
247 239 parser.add_option("--tmpdir", type="string",
248 240 help="run tests in the given temporary directory"
249 241 " (implies --keep-tmpdir)")
250 242 parser.add_option("-v", "--verbose", action="store_true",
251 243 help="output verbose messages")
252 244 parser.add_option("--xunit", type="string",
253 245 help="record xunit results at specified path")
254 246 parser.add_option("--view", type="string",
255 247 help="external diff viewer")
256 248 parser.add_option("--with-hg", type="string",
257 249 metavar="HG",
258 250 help="test using specified hg script rather than a "
259 251 "temporary installation")
260 252 parser.add_option("-3", "--py3k-warnings", action="store_true",
261 253 help="enable Py3k warnings on Python 2.6+")
262 254 parser.add_option('--extra-config-opt', action="append",
263 255 help='set the given config opt in the test hgrc')
264 256 parser.add_option('--random', action="store_true",
265 257 help='run tests in random order')
266 258 parser.add_option('--profile-runner', action='store_true',
267 259 help='run statprof on run-tests')
268 260 parser.add_option('--allow-slow-tests', action='store_true',
269 261 help='allow extremely slow tests')
270 262 parser.add_option('--showchannels', action='store_true',
271 263 help='show scheduling channels')
272 264
273 265 for option, (envvar, default) in defaults.items():
274 266 defaults[option] = type(default)(os.environ.get(envvar, default))
275 267 parser.set_defaults(**defaults)
276 268
277 269 return parser
278 270
279 271 def parseargs(args, parser):
280 272 """Parse arguments with our OptionParser and validate results."""
281 273 (options, args) = parser.parse_args(args)
282 274
283 275 # jython is always pure
284 276 if 'java' in sys.platform or '__pypy__' in sys.modules:
285 277 options.pure = True
286 278
287 279 if options.with_hg:
288 280 options.with_hg = os.path.realpath(
289 281 os.path.expanduser(_bytespath(options.with_hg)))
290 282 if not (os.path.isfile(options.with_hg) and
291 283 os.access(options.with_hg, os.X_OK)):
292 284 parser.error('--with-hg must specify an executable hg script')
293 285 if not os.path.basename(options.with_hg) == b'hg':
294 286 sys.stderr.write('warning: --with-hg should specify an hg script\n')
295 287 if options.local:
296 288 testdir = os.path.dirname(_bytespath(os.path.realpath(sys.argv[0])))
297 289 hgbin = os.path.join(os.path.dirname(testdir), b'hg')
298 290 if os.name != 'nt' and not os.access(hgbin, os.X_OK):
299 291 parser.error('--local specified, but %r not found or not executable'
300 292 % hgbin)
301 293 options.with_hg = hgbin
302 294
303 295 options.anycoverage = options.cover or options.annotate or options.htmlcov
304 296 if options.anycoverage:
305 297 try:
306 298 import coverage
307 299 covver = version.StrictVersion(coverage.__version__).version
308 300 if covver < (3, 3):
309 301 parser.error('coverage options require coverage 3.3 or later')
310 302 except ImportError:
311 303 parser.error('coverage options now require the coverage package')
312 304
313 305 if options.anycoverage and options.local:
314 306 # this needs some path mangling somewhere, I guess
315 307 parser.error("sorry, coverage options do not work when --local "
316 308 "is specified")
317 309
318 310 if options.anycoverage and options.with_hg:
319 311 parser.error("sorry, coverage options do not work when --with-hg "
320 312 "is specified")
321 313
322 314 global verbose
323 315 if options.verbose:
324 316 verbose = ''
325 317
326 318 if options.tmpdir:
327 319 options.tmpdir = os.path.expanduser(options.tmpdir)
328 320
329 321 if options.jobs < 1:
330 322 parser.error('--jobs must be positive')
331 323 if options.interactive and options.debug:
332 324 parser.error("-i/--interactive and -d/--debug are incompatible")
333 325 if options.debug:
334 326 if options.timeout != defaults['timeout']:
335 327 sys.stderr.write(
336 328 'warning: --timeout option ignored with --debug\n')
337 329 if options.slowtimeout != defaults['slowtimeout']:
338 330 sys.stderr.write(
339 331 'warning: --slowtimeout option ignored with --debug\n')
340 332 options.timeout = 0
341 333 options.slowtimeout = 0
342 334 if options.py3k_warnings:
343 335 if PYTHON3:
344 336 parser.error(
345 337 '--py3k-warnings can only be used on Python 2.6 and 2.7')
346 338 if options.blacklist:
347 339 options.blacklist = parselistfiles(options.blacklist, 'blacklist')
348 340 if options.whitelist:
349 341 options.whitelisted = parselistfiles(options.whitelist, 'whitelist')
350 342 else:
351 343 options.whitelisted = {}
352 344
353 345 if options.showchannels:
354 346 options.nodiff = True
355 347
356 348 return (options, args)
357 349
358 350 def rename(src, dst):
359 351 """Like os.rename(), trade atomicity and opened files friendliness
360 352 for existing destination support.
361 353 """
362 354 shutil.copy(src, dst)
363 355 os.remove(src)
364 356
365 357 _unified_diff = difflib.unified_diff
366 358 if PYTHON3:
367 359 import functools
368 360 _unified_diff = functools.partial(difflib.diff_bytes, difflib.unified_diff)
369 361
370 362 def getdiff(expected, output, ref, err):
371 363 servefail = False
372 364 lines = []
373 365 for line in _unified_diff(expected, output, ref, err):
374 366 if line.startswith(b'+++') or line.startswith(b'---'):
375 367 line = line.replace(b'\\', b'/')
376 368 if line.endswith(b' \n'):
377 369 line = line[:-2] + b'\n'
378 370 lines.append(line)
379 371 if not servefail and line.startswith(
380 372 b'+ abort: child process failed to start'):
381 373 servefail = True
382 374
383 375 return servefail, lines
384 376
385 377 verbose = False
386 378 def vlog(*msg):
387 379 """Log only when in verbose mode."""
388 380 if verbose is False:
389 381 return
390 382
391 383 return log(*msg)
392 384
393 385 # Bytes that break XML even in a CDATA block: control characters 0-31
394 386 # sans \t, \n and \r
395 387 CDATA_EVIL = re.compile(br"[\000-\010\013\014\016-\037]")
396 388
397 389 def cdatasafe(data):
398 390 """Make a string safe to include in a CDATA block.
399 391
400 392 Certain control characters are illegal in a CDATA block, and
401 393 there's no way to include a ]]> in a CDATA either. This function
402 394 replaces illegal bytes with ? and adds a space between the ]] so
403 395 that it won't break the CDATA block.
404 396 """
405 397 return CDATA_EVIL.sub(b'?', data).replace(b']]>', b'] ]>')
406 398
407 399 def log(*msg):
408 400 """Log something to stdout.
409 401
410 402 Arguments are strings to print.
411 403 """
412 404 with iolock:
413 405 if verbose:
414 406 print(verbose, end=' ')
415 407 for m in msg:
416 408 print(m, end=' ')
417 409 print()
418 410 sys.stdout.flush()
419 411
420 412 def terminate(proc):
421 413 """Terminate subprocess (with fallback for Python versions < 2.6)"""
422 414 vlog('# Terminating process %d' % proc.pid)
423 415 try:
424 416 getattr(proc, 'terminate', lambda : os.kill(proc.pid, signal.SIGTERM))()
425 417 except OSError:
426 418 pass
427 419
428 420 def killdaemons(pidfile):
429 421 return killmod.killdaemons(pidfile, tryhard=False, remove=True,
430 422 logfn=vlog)
431 423
432 424 class Test(unittest.TestCase):
433 425 """Encapsulates a single, runnable test.
434 426
435 427 While this class conforms to the unittest.TestCase API, it differs in that
436 428 instances need to be instantiated manually. (Typically, unittest.TestCase
437 429 classes are instantiated automatically by scanning modules.)
438 430 """
439 431
440 432 # Status code reserved for skipped tests (used by hghave).
441 433 SKIPPED_STATUS = 80
442 434
443 435 def __init__(self, path, tmpdir, keeptmpdir=False,
444 436 debug=False,
445 437 timeout=defaults['timeout'],
446 438 startport=defaults['port'], extraconfigopts=None,
447 439 py3kwarnings=False, shell=None, hgcommand=None,
448 440 slowtimeout=defaults['slowtimeout']):
449 441 """Create a test from parameters.
450 442
451 443 path is the full path to the file defining the test.
452 444
453 445 tmpdir is the main temporary directory to use for this test.
454 446
455 447 keeptmpdir determines whether to keep the test's temporary directory
456 448 after execution. It defaults to removal (False).
457 449
458 450 debug mode will make the test execute verbosely, with unfiltered
459 451 output.
460 452
461 453 timeout controls the maximum run time of the test. It is ignored when
462 454 debug is True. See slowtimeout for tests with #require slow.
463 455
464 456 slowtimeout overrides timeout if the test has #require slow.
465 457
466 458 startport controls the starting port number to use for this test. Each
467 459 test will reserve 3 port numbers for execution. It is the caller's
468 460 responsibility to allocate a non-overlapping port range to Test
469 461 instances.
470 462
471 463 extraconfigopts is an iterable of extra hgrc config options. Values
472 464 must have the form "key=value" (something understood by hgrc). Values
473 465 of the form "foo.key=value" will result in "[foo] key=value".
474 466
475 467 py3kwarnings enables Py3k warnings.
476 468
477 469 shell is the shell to execute tests in.
478 470 """
479 471 self.path = path
480 472 self.bname = os.path.basename(path)
481 473 self.name = _strpath(self.bname)
482 474 self._testdir = os.path.dirname(path)
483 475 self.errpath = os.path.join(self._testdir, b'%s.err' % self.bname)
484 476
485 477 self._threadtmp = tmpdir
486 478 self._keeptmpdir = keeptmpdir
487 479 self._debug = debug
488 480 self._timeout = timeout
489 481 self._slowtimeout = slowtimeout
490 482 self._startport = startport
491 483 self._extraconfigopts = extraconfigopts or []
492 484 self._py3kwarnings = py3kwarnings
493 485 self._shell = _bytespath(shell)
494 486 self._hgcommand = hgcommand or b'hg'
495 487
496 488 self._aborted = False
497 489 self._daemonpids = []
498 490 self._finished = None
499 491 self._ret = None
500 492 self._out = None
501 493 self._skipped = None
502 494 self._testtmp = None
503 495
504 496 # If we're not in --debug mode and reference output file exists,
505 497 # check test output against it.
506 498 if debug:
507 499 self._refout = None # to match "out is None"
508 500 elif os.path.exists(self.refpath):
509 501 f = open(self.refpath, 'rb')
510 502 self._refout = f.read().splitlines(True)
511 503 f.close()
512 504 else:
513 505 self._refout = []
514 506
515 507 # needed to get base class __repr__ running
516 508 @property
517 509 def _testMethodName(self):
518 510 return self.name
519 511
520 512 def __str__(self):
521 513 return self.name
522 514
523 515 def shortDescription(self):
524 516 return self.name
525 517
526 518 def setUp(self):
527 519 """Tasks to perform before run()."""
528 520 self._finished = False
529 521 self._ret = None
530 522 self._out = None
531 523 self._skipped = None
532 524
533 525 try:
534 526 os.mkdir(self._threadtmp)
535 527 except OSError as e:
536 528 if e.errno != errno.EEXIST:
537 529 raise
538 530
539 531 self._testtmp = os.path.join(self._threadtmp,
540 532 os.path.basename(self.path))
541 533 os.mkdir(self._testtmp)
542 534
543 535 # Remove any previous output files.
544 536 if os.path.exists(self.errpath):
545 537 try:
546 538 os.remove(self.errpath)
547 539 except OSError as e:
548 540 # We might have raced another test to clean up a .err
549 541 # file, so ignore ENOENT when removing a previous .err
550 542 # file.
551 543 if e.errno != errno.ENOENT:
552 544 raise
553 545
554 546 def run(self, result):
555 547 """Run this test and report results against a TestResult instance."""
556 548 # This function is extremely similar to unittest.TestCase.run(). Once
557 549 # we require Python 2.7 (or at least its version of unittest), this
558 550 # function can largely go away.
559 551 self._result = result
560 552 result.startTest(self)
561 553 try:
562 554 try:
563 555 self.setUp()
564 556 except (KeyboardInterrupt, SystemExit):
565 557 self._aborted = True
566 558 raise
567 559 except Exception:
568 560 result.addError(self, sys.exc_info())
569 561 return
570 562
571 563 success = False
572 564 try:
573 565 self.runTest()
574 566 except KeyboardInterrupt:
575 567 self._aborted = True
576 568 raise
577 569 except SkipTest as e:
578 570 result.addSkip(self, str(e))
579 571 # The base class will have already counted this as a
580 572 # test we "ran", but we want to exclude skipped tests
581 573 # from those we count towards those run.
582 574 result.testsRun -= 1
583 575 except IgnoreTest as e:
584 576 result.addIgnore(self, str(e))
585 577 # As with skips, ignores also should be excluded from
586 578 # the number of tests executed.
587 579 result.testsRun -= 1
588 580 except WarnTest as e:
589 581 result.addWarn(self, str(e))
590 582 except ReportedTest as e:
591 583 pass
592 584 except self.failureException as e:
593 585 # This differs from unittest in that we don't capture
594 586 # the stack trace. This is for historical reasons and
595 587 # this decision could be revisited in the future,
596 588 # especially for PythonTest instances.
597 589 if result.addFailure(self, str(e)):
598 590 success = True
599 591 except Exception:
600 592 result.addError(self, sys.exc_info())
601 593 else:
602 594 success = True
603 595
604 596 try:
605 597 self.tearDown()
606 598 except (KeyboardInterrupt, SystemExit):
607 599 self._aborted = True
608 600 raise
609 601 except Exception:
610 602 result.addError(self, sys.exc_info())
611 603 success = False
612 604
613 605 if success:
614 606 result.addSuccess(self)
615 607 finally:
616 608 result.stopTest(self, interrupted=self._aborted)
617 609
618 610 def runTest(self):
619 611 """Run this test instance.
620 612
621 613 This will return a tuple describing the result of the test.
622 614 """
623 615 env = self._getenv()
624 616 self._daemonpids.append(env['DAEMON_PIDS'])
625 617 self._createhgrc(env['HGRCPATH'])
626 618
627 619 vlog('# Test', self.name)
628 620
629 621 ret, out = self._run(env)
630 622 self._finished = True
631 623 self._ret = ret
632 624 self._out = out
633 625
634 626 def describe(ret):
635 627 if ret < 0:
636 628 return 'killed by signal: %d' % -ret
637 629 return 'returned error code %d' % ret
638 630
639 631 self._skipped = False
640 632
641 633 if ret == self.SKIPPED_STATUS:
642 634 if out is None: # Debug mode, nothing to parse.
643 635 missing = ['unknown']
644 636 failed = None
645 637 else:
646 638 missing, failed = TTest.parsehghaveoutput(out)
647 639
648 640 if not missing:
649 641 missing = ['skipped']
650 642
651 643 if failed:
652 644 self.fail('hg have failed checking for %s' % failed[-1])
653 645 else:
654 646 self._skipped = True
655 647 raise SkipTest(missing[-1])
656 648 elif ret == 'timeout':
657 649 self.fail('timed out')
658 650 elif ret is False:
659 651 raise WarnTest('no result code from test')
660 652 elif out != self._refout:
661 653 # Diff generation may rely on written .err file.
662 654 if (ret != 0 or out != self._refout) and not self._skipped \
663 655 and not self._debug:
664 656 f = open(self.errpath, 'wb')
665 657 for line in out:
666 658 f.write(line)
667 659 f.close()
668 660
669 661 # The result object handles diff calculation for us.
670 662 if self._result.addOutputMismatch(self, ret, out, self._refout):
671 663 # change was accepted, skip failing
672 664 return
673 665
674 666 if ret:
675 667 msg = 'output changed and ' + describe(ret)
676 668 else:
677 669 msg = 'output changed'
678 670
679 671 self.fail(msg)
680 672 elif ret:
681 673 self.fail(describe(ret))
682 674
683 675 def tearDown(self):
684 676 """Tasks to perform after run()."""
685 677 for entry in self._daemonpids:
686 678 killdaemons(entry)
687 679 self._daemonpids = []
688 680
689 681 if self._keeptmpdir:
690 682 log('\nKeeping testtmp dir: %s\nKeeping threadtmp dir: %s' %
691 683 (self._testtmp, self._threadtmp))
692 684 else:
693 685 shutil.rmtree(self._testtmp, True)
694 686 shutil.rmtree(self._threadtmp, True)
695 687
696 688 if (self._ret != 0 or self._out != self._refout) and not self._skipped \
697 689 and not self._debug and self._out:
698 690 f = open(self.errpath, 'wb')
699 691 for line in self._out:
700 692 f.write(line)
701 693 f.close()
702 694
703 695 vlog("# Ret was:", self._ret, '(%s)' % self.name)
704 696
705 697 def _run(self, env):
706 698 # This should be implemented in child classes to run tests.
707 699 raise SkipTest('unknown test type')
708 700
709 701 def abort(self):
710 702 """Terminate execution of this test."""
711 703 self._aborted = True
712 704
713 705 def _getreplacements(self):
714 706 """Obtain a mapping of text replacements to apply to test output.
715 707
716 708 Test output needs to be normalized so it can be compared to expected
717 709 output. This function defines how some of that normalization will
718 710 occur.
719 711 """
720 712 r = [
721 713 (br':%d\b' % self._startport, b':$HGPORT'),
722 714 (br':%d\b' % (self._startport + 1), b':$HGPORT1'),
723 715 (br':%d\b' % (self._startport + 2), b':$HGPORT2'),
724 716 (br':%d\b' % (self._startport + 2), b':$HGPORT3'),
725 717 (br':%d\b' % (self._startport + 2), b':$HGPORT4'),
726 718 (br'(?m)^(saved backup bundle to .*\.hg)( \(glob\))?$',
727 719 br'\1 (glob)'),
728 720 ]
729 721 r.append((self._escapepath(self._testtmp), b'$TESTTMP'))
730 722
731 723 return r
732 724
733 725 def _escapepath(self, p):
734 726 if os.name == 'nt':
735 727 return (
736 728 (b''.join(c.isalpha() and b'[%s%s]' % (c.lower(), c.upper()) or
737 729 c in b'/\\' and br'[/\\]' or c.isdigit() and c or b'\\' + c
738 730 for c in p))
739 731 )
740 732 else:
741 733 return re.escape(p)
742 734
743 735 def _getenv(self):
744 736 """Obtain environment variables to use during test execution."""
745 737 env = os.environ.copy()
746 738 env['TESTTMP'] = self._testtmp
747 739 env['HOME'] = self._testtmp
748 740 env["HGPORT"] = str(self._startport)
749 741 env["HGPORT1"] = str(self._startport + 1)
750 742 env["HGPORT2"] = str(self._startport + 2)
751 743 env["HGPORT3"] = str(self._startport + 3)
752 744 env["HGPORT4"] = str(self._startport + 4)
753 745 env["HGRCPATH"] = os.path.join(self._threadtmp, b'.hgrc')
754 746 env["DAEMON_PIDS"] = os.path.join(self._threadtmp, b'daemon.pids')
755 747 env["HGEDITOR"] = ('"' + sys.executable + '"'
756 748 + ' -c "import sys; sys.exit(0)"')
757 749 env["HGMERGE"] = "internal:merge"
758 750 env["HGUSER"] = "test"
759 751 env["HGENCODING"] = "ascii"
760 752 env["HGENCODINGMODE"] = "strict"
761 753
762 754 # Reset some environment variables to well-known values so that
763 755 # the tests produce repeatable output.
764 756 env['LANG'] = env['LC_ALL'] = env['LANGUAGE'] = 'C'
765 757 env['TZ'] = 'GMT'
766 758 env["EMAIL"] = "Foo Bar <foo.bar@example.com>"
767 759 env['COLUMNS'] = '80'
768 760 env['TERM'] = 'xterm'
769 761
770 762 for k in ('HG HGPROF CDPATH GREP_OPTIONS http_proxy no_proxy ' +
771 763 'NO_PROXY').split():
772 764 if k in env:
773 765 del env[k]
774 766
775 767 # unset env related to hooks
776 768 for k in env.keys():
777 769 if k.startswith('HG_'):
778 770 del env[k]
779 771
780 772 return env
781 773
782 774 def _createhgrc(self, path):
783 775 """Create an hgrc file for this test."""
784 776 hgrc = open(path, 'wb')
785 777 hgrc.write(b'[ui]\n')
786 778 hgrc.write(b'slash = True\n')
787 779 hgrc.write(b'interactive = False\n')
788 780 hgrc.write(b'mergemarkers = detailed\n')
789 781 hgrc.write(b'promptecho = True\n')
790 782 hgrc.write(b'[defaults]\n')
791 783 hgrc.write(b'backout = -d "0 0"\n')
792 784 hgrc.write(b'commit = -d "0 0"\n')
793 785 hgrc.write(b'shelve = --date "0 0"\n')
794 786 hgrc.write(b'tag = -d "0 0"\n')
795 787 hgrc.write(b'[devel]\n')
796 788 hgrc.write(b'all-warnings = true\n')
797 789 hgrc.write(b'[largefiles]\n')
798 790 hgrc.write(b'usercache = %s\n' %
799 791 (os.path.join(self._testtmp, b'.cache/largefiles')))
800 792
801 793 for opt in self._extraconfigopts:
802 794 section, key = opt.split('.', 1)
803 795 assert '=' in key, ('extra config opt %s must '
804 796 'have an = for assignment' % opt)
805 797 hgrc.write(b'[%s]\n%s\n' % (section, key))
806 798 hgrc.close()
807 799
808 800 def fail(self, msg):
809 801 # unittest differentiates between errored and failed.
810 802 # Failed is denoted by AssertionError (by default at least).
811 803 raise AssertionError(msg)
812 804
813 805 def _runcommand(self, cmd, env, normalizenewlines=False):
814 806 """Run command in a sub-process, capturing the output (stdout and
815 807 stderr).
816 808
817 809 Return a tuple (exitcode, output). output is None in debug mode.
818 810 """
819 811 if self._debug:
820 812 proc = subprocess.Popen(cmd, shell=True, cwd=self._testtmp,
821 813 env=env)
822 814 ret = proc.wait()
823 815 return (ret, None)
824 816
825 817 proc = Popen4(cmd, self._testtmp, self._timeout, env)
826 818 def cleanup():
827 819 terminate(proc)
828 820 ret = proc.wait()
829 821 if ret == 0:
830 822 ret = signal.SIGTERM << 8
831 823 killdaemons(env['DAEMON_PIDS'])
832 824 return ret
833 825
834 826 output = ''
835 827 proc.tochild.close()
836 828
837 829 try:
838 830 output = proc.fromchild.read()
839 831 except KeyboardInterrupt:
840 832 vlog('# Handling keyboard interrupt')
841 833 cleanup()
842 834 raise
843 835
844 836 ret = proc.wait()
845 837 if wifexited(ret):
846 838 ret = os.WEXITSTATUS(ret)
847 839
848 840 if proc.timeout:
849 841 ret = 'timeout'
850 842
851 843 if ret:
852 844 killdaemons(env['DAEMON_PIDS'])
853 845
854 846 for s, r in self._getreplacements():
855 847 output = re.sub(s, r, output)
856 848
857 849 if normalizenewlines:
858 850 output = output.replace('\r\n', '\n')
859 851
860 852 return ret, output.splitlines(True)
861 853
862 854 class PythonTest(Test):
863 855 """A Python-based test."""
864 856
865 857 @property
866 858 def refpath(self):
867 859 return os.path.join(self._testdir, b'%s.out' % self.bname)
868 860
869 861 def _run(self, env):
870 862 py3kswitch = self._py3kwarnings and b' -3' or b''
871 863 cmd = b'%s%s "%s"' % (PYTHON, py3kswitch, self.path)
872 864 vlog("# Running", cmd)
873 865 normalizenewlines = os.name == 'nt'
874 866 result = self._runcommand(cmd, env,
875 867 normalizenewlines=normalizenewlines)
876 868 if self._aborted:
877 869 raise KeyboardInterrupt()
878 870
879 871 return result
880 872
881 873 # This script may want to drop globs from lines matching these patterns on
882 874 # Windows, but check-code.py wants a glob on these lines unconditionally. Don't
883 875 # warn if that is the case for anything matching these lines.
884 876 checkcodeglobpats = [
885 877 re.compile(br'^pushing to \$TESTTMP/.*[^)]$'),
886 878 re.compile(br'^moving \S+/.*[^)]$'),
887 879 re.compile(br'^pulling from \$TESTTMP/.*[^)]$')
888 880 ]
889 881
890 882 bchr = chr
891 883 if PYTHON3:
892 884 bchr = lambda x: bytes([x])
893 885
894 886 class TTest(Test):
895 887 """A "t test" is a test backed by a .t file."""
896 888
897 889 SKIPPED_PREFIX = 'skipped: '
898 890 FAILED_PREFIX = 'hghave check failed: '
899 891 NEEDESCAPE = re.compile(br'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
900 892
901 893 ESCAPESUB = re.compile(br'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
902 894 ESCAPEMAP = dict((bchr(i), br'\x%02x' % i) for i in range(256))
903 895 ESCAPEMAP.update({b'\\': b'\\\\', b'\r': br'\r'})
904 896
905 897 @property
906 898 def refpath(self):
907 899 return os.path.join(self._testdir, self.bname)
908 900
909 901 def _run(self, env):
910 902 f = open(self.path, 'rb')
911 903 lines = f.readlines()
912 904 f.close()
913 905
914 906 salt, script, after, expected = self._parsetest(lines)
915 907
916 908 # Write out the generated script.
917 909 fname = b'%s.sh' % self._testtmp
918 910 f = open(fname, 'wb')
919 911 for l in script:
920 912 f.write(l)
921 913 f.close()
922 914
923 915 cmd = b'%s "%s"' % (self._shell, fname)
924 916 vlog("# Running", cmd)
925 917
926 918 exitcode, output = self._runcommand(cmd, env)
927 919
928 920 if self._aborted:
929 921 raise KeyboardInterrupt()
930 922
931 923 # Do not merge output if skipped. Return hghave message instead.
932 924 # Similarly, with --debug, output is None.
933 925 if exitcode == self.SKIPPED_STATUS or output is None:
934 926 return exitcode, output
935 927
936 928 return self._processoutput(exitcode, output, salt, after, expected)
937 929
938 930 def _hghave(self, reqs):
939 931 # TODO do something smarter when all other uses of hghave are gone.
940 932 runtestdir = os.path.abspath(os.path.dirname(_bytespath(__file__)))
941 933 tdir = runtestdir.replace(b'\\', b'/')
942 934 proc = Popen4(b'%s -c "%s/hghave %s"' %
943 935 (self._shell, tdir, b' '.join(reqs)),
944 936 self._testtmp, 0, self._getenv())
945 937 stdout, stderr = proc.communicate()
946 938 ret = proc.wait()
947 939 if wifexited(ret):
948 940 ret = os.WEXITSTATUS(ret)
949 941 if ret == 2:
950 942 print(stdout)
951 943 sys.exit(1)
952 944
953 945 if ret != 0:
954 946 return False, stdout
955 947
956 948 if 'slow' in reqs:
957 949 self._timeout = self._slowtimeout
958 950 return True, None
959 951
960 952 def _parsetest(self, lines):
961 953 # We generate a shell script which outputs unique markers to line
962 954 # up script results with our source. These markers include input
963 955 # line number and the last return code.
964 956 salt = b"SALT%d" % time.time()
965 957 def addsalt(line, inpython):
966 958 if inpython:
967 959 script.append(b'%s %d 0\n' % (salt, line))
968 960 else:
969 961 script.append(b'echo %s %d $?\n' % (salt, line))
970 962
971 963 script = []
972 964
973 965 # After we run the shell script, we re-unify the script output
974 966 # with non-active parts of the source, with synchronization by our
975 967 # SALT line number markers. The after table contains the non-active
976 968 # components, ordered by line number.
977 969 after = {}
978 970
979 971 # Expected shell script output.
980 972 expected = {}
981 973
982 974 pos = prepos = -1
983 975
984 976 # True or False when in a true or false conditional section
985 977 skipping = None
986 978
987 979 # We keep track of whether or not we're in a Python block so we
988 980 # can generate the surrounding doctest magic.
989 981 inpython = False
990 982
991 983 if self._debug:
992 984 script.append(b'set -x\n')
993 985 if self._hgcommand != b'hg':
994 986 script.append(b'alias hg="%s"\n' % self._hgcommand)
995 987 if os.getenv('MSYSTEM'):
996 988 script.append(b'alias pwd="pwd -W"\n')
997 989
998 990 for n, l in enumerate(lines):
999 991 if not l.endswith(b'\n'):
1000 992 l += b'\n'
1001 993 if l.startswith(b'#require'):
1002 994 lsplit = l.split()
1003 995 if len(lsplit) < 2 or lsplit[0] != b'#require':
1004 996 after.setdefault(pos, []).append(' !!! invalid #require\n')
1005 997 haveresult, message = self._hghave(lsplit[1:])
1006 998 if not haveresult:
1007 999 script = [b'echo "%s"\nexit 80\n' % message]
1008 1000 break
1009 1001 after.setdefault(pos, []).append(l)
1010 1002 elif l.startswith(b'#if'):
1011 1003 lsplit = l.split()
1012 1004 if len(lsplit) < 2 or lsplit[0] != b'#if':
1013 1005 after.setdefault(pos, []).append(' !!! invalid #if\n')
1014 1006 if skipping is not None:
1015 1007 after.setdefault(pos, []).append(' !!! nested #if\n')
1016 1008 skipping = not self._hghave(lsplit[1:])[0]
1017 1009 after.setdefault(pos, []).append(l)
1018 1010 elif l.startswith(b'#else'):
1019 1011 if skipping is None:
1020 1012 after.setdefault(pos, []).append(' !!! missing #if\n')
1021 1013 skipping = not skipping
1022 1014 after.setdefault(pos, []).append(l)
1023 1015 elif l.startswith(b'#endif'):
1024 1016 if skipping is None:
1025 1017 after.setdefault(pos, []).append(' !!! missing #if\n')
1026 1018 skipping = None
1027 1019 after.setdefault(pos, []).append(l)
1028 1020 elif skipping:
1029 1021 after.setdefault(pos, []).append(l)
1030 1022 elif l.startswith(b' >>> '): # python inlines
1031 1023 after.setdefault(pos, []).append(l)
1032 1024 prepos = pos
1033 1025 pos = n
1034 1026 if not inpython:
1035 1027 # We've just entered a Python block. Add the header.
1036 1028 inpython = True
1037 1029 addsalt(prepos, False) # Make sure we report the exit code.
1038 1030 script.append(b'%s -m heredoctest <<EOF\n' % PYTHON)
1039 1031 addsalt(n, True)
1040 1032 script.append(l[2:])
1041 1033 elif l.startswith(b' ... '): # python inlines
1042 1034 after.setdefault(prepos, []).append(l)
1043 1035 script.append(l[2:])
1044 1036 elif l.startswith(b' $ '): # commands
1045 1037 if inpython:
1046 1038 script.append(b'EOF\n')
1047 1039 inpython = False
1048 1040 after.setdefault(pos, []).append(l)
1049 1041 prepos = pos
1050 1042 pos = n
1051 1043 addsalt(n, False)
1052 1044 cmd = l[4:].split()
1053 1045 if len(cmd) == 2 and cmd[0] == b'cd':
1054 1046 l = b' $ cd %s || exit 1\n' % cmd[1]
1055 1047 script.append(l[4:])
1056 1048 elif l.startswith(b' > '): # continuations
1057 1049 after.setdefault(prepos, []).append(l)
1058 1050 script.append(l[4:])
1059 1051 elif l.startswith(b' '): # results
1060 1052 # Queue up a list of expected results.
1061 1053 expected.setdefault(pos, []).append(l[2:])
1062 1054 else:
1063 1055 if inpython:
1064 1056 script.append(b'EOF\n')
1065 1057 inpython = False
1066 1058 # Non-command/result. Queue up for merged output.
1067 1059 after.setdefault(pos, []).append(l)
1068 1060
1069 1061 if inpython:
1070 1062 script.append(b'EOF\n')
1071 1063 if skipping is not None:
1072 1064 after.setdefault(pos, []).append(' !!! missing #endif\n')
1073 1065 addsalt(n + 1, False)
1074 1066
1075 1067 return salt, script, after, expected
1076 1068
1077 1069 def _processoutput(self, exitcode, output, salt, after, expected):
1078 1070 # Merge the script output back into a unified test.
1079 1071 warnonly = 1 # 1: not yet; 2: yes; 3: for sure not
1080 1072 if exitcode != 0:
1081 1073 warnonly = 3
1082 1074
1083 1075 pos = -1
1084 1076 postout = []
1085 1077 for l in output:
1086 1078 lout, lcmd = l, None
1087 1079 if salt in l:
1088 1080 lout, lcmd = l.split(salt, 1)
1089 1081
1090 1082 while lout:
1091 1083 if not lout.endswith(b'\n'):
1092 1084 lout += b' (no-eol)\n'
1093 1085
1094 1086 # Find the expected output at the current position.
1095 1087 el = None
1096 1088 if expected.get(pos, None):
1097 1089 el = expected[pos].pop(0)
1098 1090
1099 1091 r = TTest.linematch(el, lout)
1100 1092 if isinstance(r, str):
1101 1093 if r == '+glob':
1102 1094 lout = el[:-1] + ' (glob)\n'
1103 1095 r = '' # Warn only this line.
1104 1096 elif r == '-glob':
1105 1097 lout = ''.join(el.rsplit(' (glob)', 1))
1106 1098 r = '' # Warn only this line.
1107 1099 elif r == "retry":
1108 1100 postout.append(b' ' + el)
1109 1101 continue
1110 1102 else:
1111 1103 log('\ninfo, unknown linematch result: %r\n' % r)
1112 1104 r = False
1113 1105 if r:
1114 1106 postout.append(b' ' + el)
1115 1107 else:
1116 1108 if self.NEEDESCAPE(lout):
1117 1109 lout = TTest._stringescape(b'%s (esc)\n' %
1118 1110 lout.rstrip(b'\n'))
1119 1111 postout.append(b' ' + lout) # Let diff deal with it.
1120 1112 if r != '': # If line failed.
1121 1113 warnonly = 3 # for sure not
1122 1114 elif warnonly == 1: # Is "not yet" and line is warn only.
1123 1115 warnonly = 2 # Yes do warn.
1124 1116 break
1125 1117
1126 1118 # clean up any optional leftovers
1127 1119 while expected.get(pos, None):
1128 1120 el = expected[pos].pop(0)
1129 1121 if not el.endswith(b" (?)\n"):
1130 1122 expected[pos].insert(0, el)
1131 1123 break
1132 1124 postout.append(b' ' + el)
1133 1125
1134 1126 if lcmd:
1135 1127 # Add on last return code.
1136 1128 ret = int(lcmd.split()[1])
1137 1129 if ret != 0:
1138 1130 postout.append(b' [%d]\n' % ret)
1139 1131 if pos in after:
1140 1132 # Merge in non-active test bits.
1141 1133 postout += after.pop(pos)
1142 1134 pos = int(lcmd.split()[0])
1143 1135
1144 1136 if pos in after:
1145 1137 postout += after.pop(pos)
1146 1138
1147 1139 if warnonly == 2:
1148 1140 exitcode = False # Set exitcode to warned.
1149 1141
1150 1142 return exitcode, postout
1151 1143
1152 1144 @staticmethod
1153 1145 def rematch(el, l):
1154 1146 try:
1155 1147 # use \Z to ensure that the regex matches to the end of the string
1156 1148 if os.name == 'nt':
1157 1149 return re.match(el + br'\r?\n\Z', l)
1158 1150 return re.match(el + br'\n\Z', l)
1159 1151 except re.error:
1160 1152 # el is an invalid regex
1161 1153 return False
1162 1154
1163 1155 @staticmethod
1164 1156 def globmatch(el, l):
1165 1157 # The only supported special characters are * and ? plus / which also
1166 1158 # matches \ on windows. Escaping of these characters is supported.
1167 1159 if el + b'\n' == l:
1168 1160 if os.altsep:
1169 1161 # matching on "/" is not needed for this line
1170 1162 for pat in checkcodeglobpats:
1171 1163 if pat.match(el):
1172 1164 return True
1173 1165 return b'-glob'
1174 1166 return True
1175 1167 i, n = 0, len(el)
1176 1168 res = b''
1177 1169 while i < n:
1178 1170 c = el[i:i + 1]
1179 1171 i += 1
1180 1172 if c == b'\\' and i < n and el[i:i + 1] in b'*?\\/':
1181 1173 res += el[i - 1:i + 1]
1182 1174 i += 1
1183 1175 elif c == b'*':
1184 1176 res += b'.*'
1185 1177 elif c == b'?':
1186 1178 res += b'.'
1187 1179 elif c == b'/' and os.altsep:
1188 1180 res += b'[/\\\\]'
1189 1181 else:
1190 1182 res += re.escape(c)
1191 1183 return TTest.rematch(res, l)
1192 1184
1193 1185 @staticmethod
1194 1186 def linematch(el, l):
1195 1187 retry = False
1196 1188 if el == l: # perfect match (fast)
1197 1189 return True
1198 1190 if el:
1199 1191 if el.endswith(b" (?)\n"):
1200 1192 retry = "retry"
1201 1193 el = el[:-5] + "\n"
1202 1194 if el.endswith(b" (esc)\n"):
1203 1195 if PYTHON3:
1204 1196 el = el[:-7].decode('unicode_escape') + '\n'
1205 1197 el = el.encode('utf-8')
1206 1198 else:
1207 1199 el = el[:-7].decode('string-escape') + '\n'
1208 1200 if el == l or os.name == 'nt' and el[:-1] + b'\r\n' == l:
1209 1201 return True
1210 1202 if el.endswith(b" (re)\n"):
1211 1203 return TTest.rematch(el[:-6], l) or retry
1212 1204 if el.endswith(b" (glob)\n"):
1213 1205 # ignore '(glob)' added to l by 'replacements'
1214 1206 if l.endswith(b" (glob)\n"):
1215 1207 l = l[:-8] + b"\n"
1216 1208 return TTest.globmatch(el[:-8], l)
1217 1209 if os.altsep and l.replace(b'\\', b'/') == el:
1218 1210 return b'+glob'
1219 1211 return retry
1220 1212
1221 1213 @staticmethod
1222 1214 def parsehghaveoutput(lines):
1223 1215 '''Parse hghave log lines.
1224 1216
1225 1217 Return tuple of lists (missing, failed):
1226 1218 * the missing/unknown features
1227 1219 * the features for which existence check failed'''
1228 1220 missing = []
1229 1221 failed = []
1230 1222 for line in lines:
1231 1223 if line.startswith(TTest.SKIPPED_PREFIX):
1232 1224 line = line.splitlines()[0]
1233 1225 missing.append(line[len(TTest.SKIPPED_PREFIX):])
1234 1226 elif line.startswith(TTest.FAILED_PREFIX):
1235 1227 line = line.splitlines()[0]
1236 1228 failed.append(line[len(TTest.FAILED_PREFIX):])
1237 1229
1238 1230 return missing, failed
1239 1231
1240 1232 @staticmethod
1241 1233 def _escapef(m):
1242 1234 return TTest.ESCAPEMAP[m.group(0)]
1243 1235
1244 1236 @staticmethod
1245 1237 def _stringescape(s):
1246 1238 return TTest.ESCAPESUB(TTest._escapef, s)
1247 1239
1248 1240 iolock = threading.RLock()
1249 1241
1250 1242 class SkipTest(Exception):
1251 1243 """Raised to indicate that a test is to be skipped."""
1252 1244
1253 1245 class IgnoreTest(Exception):
1254 1246 """Raised to indicate that a test is to be ignored."""
1255 1247
1256 1248 class WarnTest(Exception):
1257 1249 """Raised to indicate that a test warned."""
1258 1250
1259 1251 class ReportedTest(Exception):
1260 1252 """Raised to indicate that a test already reported."""
1261 1253
1262 1254 class TestResult(unittest._TextTestResult):
1263 1255 """Holds results when executing via unittest."""
1264 1256 # Don't worry too much about accessing the non-public _TextTestResult.
1265 1257 # It is relatively common in Python testing tools.
1266 1258 def __init__(self, options, *args, **kwargs):
1267 1259 super(TestResult, self).__init__(*args, **kwargs)
1268 1260
1269 1261 self._options = options
1270 1262
1271 1263 # unittest.TestResult didn't have skipped until 2.7. We need to
1272 1264 # polyfill it.
1273 1265 self.skipped = []
1274 1266
1275 1267 # We have a custom "ignored" result that isn't present in any Python
1276 1268 # unittest implementation. It is very similar to skipped. It may make
1277 1269 # sense to map it into skip some day.
1278 1270 self.ignored = []
1279 1271
1280 1272 # We have a custom "warned" result that isn't present in any Python
1281 1273 # unittest implementation. It is very similar to failed. It may make
1282 1274 # sense to map it into fail some day.
1283 1275 self.warned = []
1284 1276
1285 1277 self.times = []
1286 1278 self._firststarttime = None
1287 1279 # Data stored for the benefit of generating xunit reports.
1288 1280 self.successes = []
1289 1281 self.faildata = {}
1290 1282
1291 1283 def addFailure(self, test, reason):
1292 1284 self.failures.append((test, reason))
1293 1285
1294 1286 if self._options.first:
1295 1287 self.stop()
1296 1288 else:
1297 1289 with iolock:
1298 1290 if reason == "timed out":
1299 1291 self.stream.write('t')
1300 1292 else:
1301 1293 if not self._options.nodiff:
1302 1294 self.stream.write('\nERROR: %s output changed\n' % test)
1303 1295 self.stream.write('!')
1304 1296
1305 1297 self.stream.flush()
1306 1298
1307 1299 def addSuccess(self, test):
1308 1300 with iolock:
1309 1301 super(TestResult, self).addSuccess(test)
1310 1302 self.successes.append(test)
1311 1303
1312 1304 def addError(self, test, err):
1313 1305 super(TestResult, self).addError(test, err)
1314 1306 if self._options.first:
1315 1307 self.stop()
1316 1308
1317 1309 # Polyfill.
1318 1310 def addSkip(self, test, reason):
1319 1311 self.skipped.append((test, reason))
1320 1312 with iolock:
1321 1313 if self.showAll:
1322 1314 self.stream.writeln('skipped %s' % reason)
1323 1315 else:
1324 1316 self.stream.write('s')
1325 1317 self.stream.flush()
1326 1318
1327 1319 def addIgnore(self, test, reason):
1328 1320 self.ignored.append((test, reason))
1329 1321 with iolock:
1330 1322 if self.showAll:
1331 1323 self.stream.writeln('ignored %s' % reason)
1332 1324 else:
1333 1325 if reason not in ('not retesting', "doesn't match keyword"):
1334 1326 self.stream.write('i')
1335 1327 else:
1336 1328 self.testsRun += 1
1337 1329 self.stream.flush()
1338 1330
1339 1331 def addWarn(self, test, reason):
1340 1332 self.warned.append((test, reason))
1341 1333
1342 1334 if self._options.first:
1343 1335 self.stop()
1344 1336
1345 1337 with iolock:
1346 1338 if self.showAll:
1347 1339 self.stream.writeln('warned %s' % reason)
1348 1340 else:
1349 1341 self.stream.write('~')
1350 1342 self.stream.flush()
1351 1343
1352 1344 def addOutputMismatch(self, test, ret, got, expected):
1353 1345 """Record a mismatch in test output for a particular test."""
1354 1346 if self.shouldStop:
1355 1347 # don't print, some other test case already failed and
1356 1348 # printed, we're just stale and probably failed due to our
1357 1349 # temp dir getting cleaned up.
1358 1350 return
1359 1351
1360 1352 accepted = False
1361 1353 failed = False
1362 1354 lines = []
1363 1355
1364 1356 with iolock:
1365 1357 if self._options.nodiff:
1366 1358 pass
1367 1359 elif self._options.view:
1368 1360 v = self._options.view
1369 1361 if PYTHON3:
1370 1362 v = _bytespath(v)
1371 1363 os.system(b"%s %s %s" %
1372 1364 (v, test.refpath, test.errpath))
1373 1365 else:
1374 1366 servefail, lines = getdiff(expected, got,
1375 1367 test.refpath, test.errpath)
1376 1368 if servefail:
1377 1369 self.addFailure(
1378 1370 test,
1379 1371 'server failed to start (HGPORT=%s)' % test._startport)
1380 1372 raise ReportedTest('server failed to start')
1381 1373 else:
1382 1374 self.stream.write('\n')
1383 1375 for line in lines:
1384 1376 if PYTHON3:
1385 1377 self.stream.flush()
1386 1378 self.stream.buffer.write(line)
1387 1379 self.stream.buffer.flush()
1388 1380 else:
1389 1381 self.stream.write(line)
1390 1382 self.stream.flush()
1391 1383
1392 1384 # handle interactive prompt without releasing iolock
1393 1385 if self._options.interactive:
1394 1386 self.stream.write('Accept this change? [n] ')
1395 1387 answer = sys.stdin.readline().strip()
1396 1388 if answer.lower() in ('y', 'yes'):
1397 1389 if test.name.endswith('.t'):
1398 1390 rename(test.errpath, test.path)
1399 1391 else:
1400 1392 rename(test.errpath, '%s.out' % test.path)
1401 1393 accepted = True
1402 1394 if not accepted and not failed:
1403 1395 self.faildata[test.name] = b''.join(lines)
1404 1396
1405 1397 return accepted
1406 1398
1407 1399 def startTest(self, test):
1408 1400 super(TestResult, self).startTest(test)
1409 1401
1410 1402 # os.times module computes the user time and system time spent by
1411 1403 # child's processes along with real elapsed time taken by a process.
1412 1404 # This module has one limitation. It can only work for Linux user
1413 1405 # and not for Windows.
1414 1406 test.started = os.times()
1415 1407 if self._firststarttime is None: # thread racy but irrelevant
1416 1408 self._firststarttime = test.started[4]
1417 1409
1418 1410 def stopTest(self, test, interrupted=False):
1419 1411 super(TestResult, self).stopTest(test)
1420 1412
1421 1413 test.stopped = os.times()
1422 1414
1423 1415 starttime = test.started
1424 1416 endtime = test.stopped
1425 1417 origin = self._firststarttime
1426 1418 self.times.append((test.name,
1427 1419 endtime[2] - starttime[2], # user space CPU time
1428 1420 endtime[3] - starttime[3], # sys space CPU time
1429 1421 endtime[4] - starttime[4], # real time
1430 1422 starttime[4] - origin, # start date in run context
1431 1423 endtime[4] - origin, # end date in run context
1432 1424 ))
1433 1425
1434 1426 if interrupted:
1435 1427 with iolock:
1436 1428 self.stream.writeln('INTERRUPTED: %s (after %d seconds)' % (
1437 1429 test.name, self.times[-1][3]))
1438 1430
1439 1431 class TestSuite(unittest.TestSuite):
1440 1432 """Custom unittest TestSuite that knows how to execute Mercurial tests."""
1441 1433
1442 1434 def __init__(self, testdir, jobs=1, whitelist=None, blacklist=None,
1443 1435 retest=False, keywords=None, loop=False, runs_per_test=1,
1444 1436 loadtest=None, showchannels=False,
1445 1437 *args, **kwargs):
1446 1438 """Create a new instance that can run tests with a configuration.
1447 1439
1448 1440 testdir specifies the directory where tests are executed from. This
1449 1441 is typically the ``tests`` directory from Mercurial's source
1450 1442 repository.
1451 1443
1452 1444 jobs specifies the number of jobs to run concurrently. Each test
1453 1445 executes on its own thread. Tests actually spawn new processes, so
1454 1446 state mutation should not be an issue.
1455 1447
1456 1448 If there is only one job, it will use the main thread.
1457 1449
1458 1450 whitelist and blacklist denote tests that have been whitelisted and
1459 1451 blacklisted, respectively. These arguments don't belong in TestSuite.
1460 1452 Instead, whitelist and blacklist should be handled by the thing that
1461 1453 populates the TestSuite with tests. They are present to preserve
1462 1454 backwards compatible behavior which reports skipped tests as part
1463 1455 of the results.
1464 1456
1465 1457 retest denotes whether to retest failed tests. This arguably belongs
1466 1458 outside of TestSuite.
1467 1459
1468 1460 keywords denotes key words that will be used to filter which tests
1469 1461 to execute. This arguably belongs outside of TestSuite.
1470 1462
1471 1463 loop denotes whether to loop over tests forever.
1472 1464 """
1473 1465 super(TestSuite, self).__init__(*args, **kwargs)
1474 1466
1475 1467 self._jobs = jobs
1476 1468 self._whitelist = whitelist
1477 1469 self._blacklist = blacklist
1478 1470 self._retest = retest
1479 1471 self._keywords = keywords
1480 1472 self._loop = loop
1481 1473 self._runs_per_test = runs_per_test
1482 1474 self._loadtest = loadtest
1483 1475 self._showchannels = showchannels
1484 1476
1485 1477 def run(self, result):
1486 1478 # We have a number of filters that need to be applied. We do this
1487 1479 # here instead of inside Test because it makes the running logic for
1488 1480 # Test simpler.
1489 1481 tests = []
1490 1482 num_tests = [0]
1491 1483 for test in self._tests:
1492 1484 def get():
1493 1485 num_tests[0] += 1
1494 1486 if getattr(test, 'should_reload', False):
1495 1487 return self._loadtest(test.bname, num_tests[0])
1496 1488 return test
1497 1489 if not os.path.exists(test.path):
1498 1490 result.addSkip(test, "Doesn't exist")
1499 1491 continue
1500 1492
1501 1493 if not (self._whitelist and test.name in self._whitelist):
1502 1494 if self._blacklist and test.bname in self._blacklist:
1503 1495 result.addSkip(test, 'blacklisted')
1504 1496 continue
1505 1497
1506 1498 if self._retest and not os.path.exists(test.errpath):
1507 1499 result.addIgnore(test, 'not retesting')
1508 1500 continue
1509 1501
1510 1502 if self._keywords:
1511 1503 f = open(test.path, 'rb')
1512 1504 t = f.read().lower() + test.bname.lower()
1513 1505 f.close()
1514 1506 ignored = False
1515 1507 for k in self._keywords.lower().split():
1516 1508 if k not in t:
1517 1509 result.addIgnore(test, "doesn't match keyword")
1518 1510 ignored = True
1519 1511 break
1520 1512
1521 1513 if ignored:
1522 1514 continue
1523 1515 for _ in xrange(self._runs_per_test):
1524 1516 tests.append(get())
1525 1517
1526 1518 runtests = list(tests)
1527 1519 done = queue.Queue()
1528 1520 running = 0
1529 1521
1530 1522 channels = [""] * self._jobs
1531 1523
1532 1524 def job(test, result):
1533 1525 for n, v in enumerate(channels):
1534 1526 if not v:
1535 1527 channel = n
1536 1528 break
1537 1529 channels[channel] = "=" + test.name[5:].split(".")[0]
1538 1530 try:
1539 1531 test(result)
1540 1532 done.put(None)
1541 1533 except KeyboardInterrupt:
1542 1534 pass
1543 1535 except: # re-raises
1544 1536 done.put(('!', test, 'run-test raised an error, see traceback'))
1545 1537 raise
1546 1538 try:
1547 1539 channels[channel] = ''
1548 1540 except IndexError:
1549 1541 pass
1550 1542
1551 1543 def stat():
1552 1544 count = 0
1553 1545 while channels:
1554 1546 d = '\n%03s ' % count
1555 1547 for n, v in enumerate(channels):
1556 1548 if v:
1557 1549 d += v[0]
1558 1550 channels[n] = v[1:] or '.'
1559 1551 else:
1560 1552 d += ' '
1561 1553 d += ' '
1562 1554 with iolock:
1563 1555 sys.stdout.write(d + ' ')
1564 1556 sys.stdout.flush()
1565 1557 for x in xrange(10):
1566 1558 if channels:
1567 1559 time.sleep(.1)
1568 1560 count += 1
1569 1561
1570 1562 stoppedearly = False
1571 1563
1572 1564 if self._showchannels:
1573 1565 statthread = threading.Thread(target=stat, name="stat")
1574 1566 statthread.start()
1575 1567
1576 1568 try:
1577 1569 while tests or running:
1578 1570 if not done.empty() or running == self._jobs or not tests:
1579 1571 try:
1580 1572 done.get(True, 1)
1581 1573 running -= 1
1582 1574 if result and result.shouldStop:
1583 1575 stoppedearly = True
1584 1576 break
1585 1577 except queue.Empty:
1586 1578 continue
1587 1579 if tests and not running == self._jobs:
1588 1580 test = tests.pop(0)
1589 1581 if self._loop:
1590 1582 if getattr(test, 'should_reload', False):
1591 1583 num_tests[0] += 1
1592 1584 tests.append(
1593 1585 self._loadtest(test.name, num_tests[0]))
1594 1586 else:
1595 1587 tests.append(test)
1596 1588 if self._jobs == 1:
1597 1589 job(test, result)
1598 1590 else:
1599 1591 t = threading.Thread(target=job, name=test.name,
1600 1592 args=(test, result))
1601 1593 t.start()
1602 1594 running += 1
1603 1595
1604 1596 # If we stop early we still need to wait on started tests to
1605 1597 # finish. Otherwise, there is a race between the test completing
1606 1598 # and the test's cleanup code running. This could result in the
1607 1599 # test reporting incorrect.
1608 1600 if stoppedearly:
1609 1601 while running:
1610 1602 try:
1611 1603 done.get(True, 1)
1612 1604 running -= 1
1613 1605 except queue.Empty:
1614 1606 continue
1615 1607 except KeyboardInterrupt:
1616 1608 for test in runtests:
1617 1609 test.abort()
1618 1610
1619 1611 channels = []
1620 1612
1621 1613 return result
1622 1614
1623 1615 # Save the most recent 5 wall-clock runtimes of each test to a
1624 1616 # human-readable text file named .testtimes. Tests are sorted
1625 1617 # alphabetically, while times for each test are listed from oldest to
1626 1618 # newest.
1627 1619
1628 1620 def loadtimes(testdir):
1629 1621 times = []
1630 1622 try:
1631 1623 with open(os.path.join(testdir, '.testtimes-')) as fp:
1632 1624 for line in fp:
1633 1625 ts = line.split()
1634 1626 times.append((ts[0], [float(t) for t in ts[1:]]))
1635 1627 except IOError as err:
1636 1628 if err.errno != errno.ENOENT:
1637 1629 raise
1638 1630 return times
1639 1631
1640 1632 def savetimes(testdir, result):
1641 1633 saved = dict(loadtimes(testdir))
1642 1634 maxruns = 5
1643 1635 skipped = set([str(t[0]) for t in result.skipped])
1644 1636 for tdata in result.times:
1645 1637 test, real = tdata[0], tdata[3]
1646 1638 if test not in skipped:
1647 1639 ts = saved.setdefault(test, [])
1648 1640 ts.append(real)
1649 1641 ts[:] = ts[-maxruns:]
1650 1642
1651 1643 fd, tmpname = tempfile.mkstemp(prefix='.testtimes',
1652 1644 dir=testdir, text=True)
1653 1645 with os.fdopen(fd, 'w') as fp:
1654 1646 for name, ts in sorted(saved.iteritems()):
1655 1647 fp.write('%s %s\n' % (name, ' '.join(['%.3f' % (t,) for t in ts])))
1656 1648 timepath = os.path.join(testdir, '.testtimes')
1657 1649 try:
1658 1650 os.unlink(timepath)
1659 1651 except OSError:
1660 1652 pass
1661 1653 try:
1662 1654 os.rename(tmpname, timepath)
1663 1655 except OSError:
1664 1656 pass
1665 1657
1666 1658 class TextTestRunner(unittest.TextTestRunner):
1667 1659 """Custom unittest test runner that uses appropriate settings."""
1668 1660
1669 1661 def __init__(self, runner, *args, **kwargs):
1670 1662 super(TextTestRunner, self).__init__(*args, **kwargs)
1671 1663
1672 1664 self._runner = runner
1673 1665
1674 1666 def run(self, test):
1675 1667 result = TestResult(self._runner.options, self.stream,
1676 1668 self.descriptions, self.verbosity)
1677 1669
1678 1670 test(result)
1679 1671
1680 1672 failed = len(result.failures)
1681 1673 warned = len(result.warned)
1682 1674 skipped = len(result.skipped)
1683 1675 ignored = len(result.ignored)
1684 1676
1685 1677 with iolock:
1686 1678 self.stream.writeln('')
1687 1679
1688 1680 if not self._runner.options.noskips:
1689 1681 for test, msg in result.skipped:
1690 1682 self.stream.writeln('Skipped %s: %s' % (test.name, msg))
1691 1683 for test, msg in result.warned:
1692 1684 self.stream.writeln('Warned %s: %s' % (test.name, msg))
1693 1685 for test, msg in result.failures:
1694 1686 self.stream.writeln('Failed %s: %s' % (test.name, msg))
1695 1687 for test, msg in result.errors:
1696 1688 self.stream.writeln('Errored %s: %s' % (test.name, msg))
1697 1689
1698 1690 if self._runner.options.xunit:
1699 1691 with open(self._runner.options.xunit, 'wb') as xuf:
1700 1692 timesd = dict((t[0], t[3]) for t in result.times)
1701 1693 doc = minidom.Document()
1702 1694 s = doc.createElement('testsuite')
1703 1695 s.setAttribute('name', 'run-tests')
1704 1696 s.setAttribute('tests', str(result.testsRun))
1705 1697 s.setAttribute('errors', "0") # TODO
1706 1698 s.setAttribute('failures', str(failed))
1707 1699 s.setAttribute('skipped', str(skipped + ignored))
1708 1700 doc.appendChild(s)
1709 1701 for tc in result.successes:
1710 1702 t = doc.createElement('testcase')
1711 1703 t.setAttribute('name', tc.name)
1712 1704 t.setAttribute('time', '%.3f' % timesd[tc.name])
1713 1705 s.appendChild(t)
1714 1706 for tc, err in sorted(result.faildata.items()):
1715 1707 t = doc.createElement('testcase')
1716 1708 t.setAttribute('name', tc)
1717 1709 t.setAttribute('time', '%.3f' % timesd[tc])
1718 1710 # createCDATASection expects a unicode or it will
1719 1711 # convert using default conversion rules, which will
1720 1712 # fail if string isn't ASCII.
1721 1713 err = cdatasafe(err).decode('utf-8', 'replace')
1722 1714 cd = doc.createCDATASection(err)
1723 1715 t.appendChild(cd)
1724 1716 s.appendChild(t)
1725 1717 xuf.write(doc.toprettyxml(indent=' ', encoding='utf-8'))
1726 1718
1727 1719 if self._runner.options.json:
1728 if json is None:
1729 raise ImportError("json module not installed")
1730 1720 jsonpath = os.path.join(self._runner._testdir, 'report.json')
1731 1721 with open(jsonpath, 'w') as fp:
1732 1722 timesd = {}
1733 1723 for tdata in result.times:
1734 1724 test = tdata[0]
1735 1725 timesd[test] = tdata[1:]
1736 1726
1737 1727 outcome = {}
1738 1728 groups = [('success', ((tc, None)
1739 1729 for tc in result.successes)),
1740 1730 ('failure', result.failures),
1741 1731 ('skip', result.skipped)]
1742 1732 for res, testcases in groups:
1743 1733 for tc, __ in testcases:
1744 1734 if tc.name in timesd:
1745 1735 tres = {'result': res,
1746 1736 'time': ('%0.3f' % timesd[tc.name][2]),
1747 1737 'cuser': ('%0.3f' % timesd[tc.name][0]),
1748 1738 'csys': ('%0.3f' % timesd[tc.name][1]),
1749 1739 'start': ('%0.3f' % timesd[tc.name][3]),
1750 1740 'end': ('%0.3f' % timesd[tc.name][4]),
1751 1741 'diff': result.faildata.get(tc.name,
1752 1742 ''),
1753 1743 }
1754 1744 else:
1755 1745 # blacklisted test
1756 1746 tres = {'result': res}
1757 1747
1758 1748 outcome[tc.name] = tres
1759 1749 jsonout = json.dumps(outcome, sort_keys=True, indent=4)
1760 1750 fp.writelines(("testreport =", jsonout))
1761 1751
1762 1752 self._runner._checkhglib('Tested')
1763 1753
1764 1754 savetimes(self._runner._testdir, result)
1765 1755 self.stream.writeln(
1766 1756 '# Ran %d tests, %d skipped, %d warned, %d failed.'
1767 1757 % (result.testsRun,
1768 1758 skipped + ignored, warned, failed))
1769 1759 if failed:
1770 1760 self.stream.writeln('python hash seed: %s' %
1771 1761 os.environ['PYTHONHASHSEED'])
1772 1762 if self._runner.options.time:
1773 1763 self.printtimes(result.times)
1774 1764
1775 1765 return result
1776 1766
1777 1767 def printtimes(self, times):
1778 1768 # iolock held by run
1779 1769 self.stream.writeln('# Producing time report')
1780 1770 times.sort(key=lambda t: (t[3]))
1781 1771 cols = '%7.3f %7.3f %7.3f %7.3f %7.3f %s'
1782 1772 self.stream.writeln('%-7s %-7s %-7s %-7s %-7s %s' %
1783 1773 ('start', 'end', 'cuser', 'csys', 'real', 'Test'))
1784 1774 for tdata in times:
1785 1775 test = tdata[0]
1786 1776 cuser, csys, real, start, end = tdata[1:6]
1787 1777 self.stream.writeln(cols % (start, end, cuser, csys, real, test))
1788 1778
1789 1779 class TestRunner(object):
1790 1780 """Holds context for executing tests.
1791 1781
1792 1782 Tests rely on a lot of state. This object holds it for them.
1793 1783 """
1794 1784
1795 1785 # Programs required to run tests.
1796 1786 REQUIREDTOOLS = [
1797 1787 os.path.basename(_bytespath(sys.executable)),
1798 1788 b'diff',
1799 1789 b'grep',
1800 1790 b'unzip',
1801 1791 b'gunzip',
1802 1792 b'bunzip2',
1803 1793 b'sed',
1804 1794 ]
1805 1795
1806 1796 # Maps file extensions to test class.
1807 1797 TESTTYPES = [
1808 1798 (b'.py', PythonTest),
1809 1799 (b'.t', TTest),
1810 1800 ]
1811 1801
1812 1802 def __init__(self):
1813 1803 self.options = None
1814 1804 self._hgroot = None
1815 1805 self._testdir = None
1816 1806 self._hgtmp = None
1817 1807 self._installdir = None
1818 1808 self._bindir = None
1819 1809 self._tmpbinddir = None
1820 1810 self._pythondir = None
1821 1811 self._coveragefile = None
1822 1812 self._createdfiles = []
1823 1813 self._hgcommand = None
1824 1814 self._hgpath = None
1825 1815 self._portoffset = 0
1826 1816 self._ports = {}
1827 1817
1828 1818 def run(self, args, parser=None):
1829 1819 """Run the test suite."""
1830 1820 oldmask = os.umask(0o22)
1831 1821 try:
1832 1822 parser = parser or getparser()
1833 1823 options, args = parseargs(args, parser)
1834 1824 # positional arguments are paths to test files to run, so
1835 1825 # we make sure they're all bytestrings
1836 1826 args = [_bytespath(a) for a in args]
1837 1827 self.options = options
1838 1828
1839 1829 self._checktools()
1840 1830 tests = self.findtests(args)
1841 1831 if options.profile_runner:
1842 1832 import statprof
1843 1833 statprof.start()
1844 1834 result = self._run(tests)
1845 1835 if options.profile_runner:
1846 1836 statprof.stop()
1847 1837 statprof.display()
1848 1838 return result
1849 1839
1850 1840 finally:
1851 1841 os.umask(oldmask)
1852 1842
1853 1843 def _run(self, tests):
1854 1844 if self.options.random:
1855 1845 random.shuffle(tests)
1856 1846 else:
1857 1847 # keywords for slow tests
1858 1848 slow = {b'svn': 10,
1859 1849 b'cvs': 10,
1860 1850 b'hghave': 10,
1861 1851 b'largefiles-update': 10,
1862 1852 b'run-tests': 10,
1863 1853 b'corruption': 10,
1864 1854 b'race': 10,
1865 1855 b'i18n': 10,
1866 1856 b'check': 100,
1867 1857 b'gendoc': 100,
1868 1858 b'contrib-perf': 200,
1869 1859 }
1870 1860 perf = {}
1871 1861 def sortkey(f):
1872 1862 # run largest tests first, as they tend to take the longest
1873 1863 try:
1874 1864 return perf[f]
1875 1865 except KeyError:
1876 1866 try:
1877 1867 val = -os.stat(f).st_size
1878 1868 except OSError as e:
1879 1869 if e.errno != errno.ENOENT:
1880 1870 raise
1881 1871 perf[f] = -1e9 # file does not exist, tell early
1882 1872 return -1e9
1883 1873 for kw, mul in slow.items():
1884 1874 if kw in f:
1885 1875 val *= mul
1886 1876 if f.endswith(b'.py'):
1887 1877 val /= 10.0
1888 1878 perf[f] = val / 1000.0
1889 1879 return perf[f]
1890 1880 tests.sort(key=sortkey)
1891 1881
1892 1882 self._testdir = osenvironb[b'TESTDIR'] = getattr(
1893 1883 os, 'getcwdb', os.getcwd)()
1894 1884
1895 1885 if 'PYTHONHASHSEED' not in os.environ:
1896 1886 # use a random python hash seed all the time
1897 1887 # we do the randomness ourself to know what seed is used
1898 1888 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
1899 1889
1900 1890 if self.options.tmpdir:
1901 1891 self.options.keep_tmpdir = True
1902 1892 tmpdir = _bytespath(self.options.tmpdir)
1903 1893 if os.path.exists(tmpdir):
1904 1894 # Meaning of tmpdir has changed since 1.3: we used to create
1905 1895 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
1906 1896 # tmpdir already exists.
1907 1897 print("error: temp dir %r already exists" % tmpdir)
1908 1898 return 1
1909 1899
1910 1900 # Automatically removing tmpdir sounds convenient, but could
1911 1901 # really annoy anyone in the habit of using "--tmpdir=/tmp"
1912 1902 # or "--tmpdir=$HOME".
1913 1903 #vlog("# Removing temp dir", tmpdir)
1914 1904 #shutil.rmtree(tmpdir)
1915 1905 os.makedirs(tmpdir)
1916 1906 else:
1917 1907 d = None
1918 1908 if os.name == 'nt':
1919 1909 # without this, we get the default temp dir location, but
1920 1910 # in all lowercase, which causes troubles with paths (issue3490)
1921 1911 d = osenvironb.get(b'TMP', None)
1922 1912 tmpdir = tempfile.mkdtemp(b'', b'hgtests.', d)
1923 1913
1924 1914 self._hgtmp = osenvironb[b'HGTMP'] = (
1925 1915 os.path.realpath(tmpdir))
1926 1916
1927 1917 if self.options.with_hg:
1928 1918 self._installdir = None
1929 1919 whg = self.options.with_hg
1930 1920 self._bindir = os.path.dirname(os.path.realpath(whg))
1931 1921 assert isinstance(self._bindir, bytes)
1932 1922 self._hgcommand = os.path.basename(whg)
1933 1923 self._tmpbindir = os.path.join(self._hgtmp, b'install', b'bin')
1934 1924 os.makedirs(self._tmpbindir)
1935 1925
1936 1926 # This looks redundant with how Python initializes sys.path from
1937 1927 # the location of the script being executed. Needed because the
1938 1928 # "hg" specified by --with-hg is not the only Python script
1939 1929 # executed in the test suite that needs to import 'mercurial'
1940 1930 # ... which means it's not really redundant at all.
1941 1931 self._pythondir = self._bindir
1942 1932 else:
1943 1933 self._installdir = os.path.join(self._hgtmp, b"install")
1944 1934 self._bindir = os.path.join(self._installdir, b"bin")
1945 1935 self._hgcommand = b'hg'
1946 1936 self._tmpbindir = self._bindir
1947 1937 self._pythondir = os.path.join(self._installdir, b"lib", b"python")
1948 1938
1949 1939 osenvironb[b"BINDIR"] = self._bindir
1950 1940 osenvironb[b"PYTHON"] = PYTHON
1951 1941
1952 1942 fileb = _bytespath(__file__)
1953 1943 runtestdir = os.path.abspath(os.path.dirname(fileb))
1954 1944 osenvironb[b'RUNTESTDIR'] = runtestdir
1955 1945 if PYTHON3:
1956 1946 sepb = _bytespath(os.pathsep)
1957 1947 else:
1958 1948 sepb = os.pathsep
1959 1949 path = [self._bindir, runtestdir] + osenvironb[b"PATH"].split(sepb)
1960 1950 if os.path.islink(__file__):
1961 1951 # test helper will likely be at the end of the symlink
1962 1952 realfile = os.path.realpath(fileb)
1963 1953 realdir = os.path.abspath(os.path.dirname(realfile))
1964 1954 path.insert(2, realdir)
1965 1955 if self._testdir != runtestdir:
1966 1956 path = [self._testdir] + path
1967 1957 if self._tmpbindir != self._bindir:
1968 1958 path = [self._tmpbindir] + path
1969 1959 osenvironb[b"PATH"] = sepb.join(path)
1970 1960
1971 1961 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
1972 1962 # can run .../tests/run-tests.py test-foo where test-foo
1973 1963 # adds an extension to HGRC. Also include run-test.py directory to
1974 1964 # import modules like heredoctest.
1975 1965 pypath = [self._pythondir, self._testdir, runtestdir]
1976 1966 # We have to augment PYTHONPATH, rather than simply replacing
1977 1967 # it, in case external libraries are only available via current
1978 1968 # PYTHONPATH. (In particular, the Subversion bindings on OS X
1979 1969 # are in /opt/subversion.)
1980 1970 oldpypath = osenvironb.get(IMPL_PATH)
1981 1971 if oldpypath:
1982 1972 pypath.append(oldpypath)
1983 1973 osenvironb[IMPL_PATH] = sepb.join(pypath)
1984 1974
1985 1975 if self.options.pure:
1986 1976 os.environ["HGTEST_RUN_TESTS_PURE"] = "--pure"
1987 1977
1988 1978 if self.options.allow_slow_tests:
1989 1979 os.environ["HGTEST_SLOW"] = "slow"
1990 1980 elif 'HGTEST_SLOW' in os.environ:
1991 1981 del os.environ['HGTEST_SLOW']
1992 1982
1993 1983 self._coveragefile = os.path.join(self._testdir, b'.coverage')
1994 1984
1995 1985 vlog("# Using TESTDIR", self._testdir)
1996 1986 vlog("# Using RUNTESTDIR", osenvironb[b'RUNTESTDIR'])
1997 1987 vlog("# Using HGTMP", self._hgtmp)
1998 1988 vlog("# Using PATH", os.environ["PATH"])
1999 1989 vlog("# Using", IMPL_PATH, osenvironb[IMPL_PATH])
2000 1990
2001 1991 try:
2002 1992 return self._runtests(tests) or 0
2003 1993 finally:
2004 1994 time.sleep(.1)
2005 1995 self._cleanup()
2006 1996
2007 1997 def findtests(self, args):
2008 1998 """Finds possible test files from arguments.
2009 1999
2010 2000 If you wish to inject custom tests into the test harness, this would
2011 2001 be a good function to monkeypatch or override in a derived class.
2012 2002 """
2013 2003 if not args:
2014 2004 if self.options.changed:
2015 2005 proc = Popen4('hg st --rev "%s" -man0 .' %
2016 2006 self.options.changed, None, 0)
2017 2007 stdout, stderr = proc.communicate()
2018 2008 args = stdout.strip(b'\0').split(b'\0')
2019 2009 else:
2020 2010 args = os.listdir(b'.')
2021 2011
2022 2012 return [t for t in args
2023 2013 if os.path.basename(t).startswith(b'test-')
2024 2014 and (t.endswith(b'.py') or t.endswith(b'.t'))]
2025 2015
2026 2016 def _runtests(self, tests):
2027 2017 try:
2028 2018 if self._installdir:
2029 2019 self._installhg()
2030 2020 self._checkhglib("Testing")
2031 2021 else:
2032 2022 self._usecorrectpython()
2033 2023
2034 2024 if self.options.restart:
2035 2025 orig = list(tests)
2036 2026 while tests:
2037 2027 if os.path.exists(tests[0] + ".err"):
2038 2028 break
2039 2029 tests.pop(0)
2040 2030 if not tests:
2041 2031 print("running all tests")
2042 2032 tests = orig
2043 2033
2044 2034 tests = [self._gettest(t, i) for i, t in enumerate(tests)]
2045 2035
2046 2036 failed = False
2047 2037 warned = False
2048 2038 kws = self.options.keywords
2049 2039 if kws is not None and PYTHON3:
2050 2040 kws = kws.encode('utf-8')
2051 2041
2052 2042 suite = TestSuite(self._testdir,
2053 2043 jobs=self.options.jobs,
2054 2044 whitelist=self.options.whitelisted,
2055 2045 blacklist=self.options.blacklist,
2056 2046 retest=self.options.retest,
2057 2047 keywords=kws,
2058 2048 loop=self.options.loop,
2059 2049 runs_per_test=self.options.runs_per_test,
2060 2050 showchannels=self.options.showchannels,
2061 2051 tests=tests, loadtest=self._gettest)
2062 2052 verbosity = 1
2063 2053 if self.options.verbose:
2064 2054 verbosity = 2
2065 2055 runner = TextTestRunner(self, verbosity=verbosity)
2066 2056 result = runner.run(suite)
2067 2057
2068 2058 if result.failures:
2069 2059 failed = True
2070 2060 if result.warned:
2071 2061 warned = True
2072 2062
2073 2063 if self.options.anycoverage:
2074 2064 self._outputcoverage()
2075 2065 except KeyboardInterrupt:
2076 2066 failed = True
2077 2067 print("\ninterrupted!")
2078 2068
2079 2069 if failed:
2080 2070 return 1
2081 2071 if warned:
2082 2072 return 80
2083 2073
2084 2074 def _getport(self, count):
2085 2075 port = self._ports.get(count) # do we have a cached entry?
2086 2076 if port is None:
2087 2077 portneeded = 3
2088 2078 # above 100 tries we just give up and let test reports failure
2089 2079 for tries in xrange(100):
2090 2080 allfree = True
2091 2081 port = self.options.port + self._portoffset
2092 2082 for idx in xrange(portneeded):
2093 2083 if not checkportisavailable(port + idx):
2094 2084 allfree = False
2095 2085 break
2096 2086 self._portoffset += portneeded
2097 2087 if allfree:
2098 2088 break
2099 2089 self._ports[count] = port
2100 2090 return port
2101 2091
2102 2092 def _gettest(self, test, count):
2103 2093 """Obtain a Test by looking at its filename.
2104 2094
2105 2095 Returns a Test instance. The Test may not be runnable if it doesn't
2106 2096 map to a known type.
2107 2097 """
2108 2098 lctest = test.lower()
2109 2099 testcls = Test
2110 2100
2111 2101 for ext, cls in self.TESTTYPES:
2112 2102 if lctest.endswith(ext):
2113 2103 testcls = cls
2114 2104 break
2115 2105
2116 2106 refpath = os.path.join(self._testdir, test)
2117 2107 tmpdir = os.path.join(self._hgtmp, b'child%d' % count)
2118 2108
2119 2109 t = testcls(refpath, tmpdir,
2120 2110 keeptmpdir=self.options.keep_tmpdir,
2121 2111 debug=self.options.debug,
2122 2112 timeout=self.options.timeout,
2123 2113 startport=self._getport(count),
2124 2114 extraconfigopts=self.options.extra_config_opt,
2125 2115 py3kwarnings=self.options.py3k_warnings,
2126 2116 shell=self.options.shell,
2127 2117 hgcommand=self._hgcommand)
2128 2118 t.should_reload = True
2129 2119 return t
2130 2120
2131 2121 def _cleanup(self):
2132 2122 """Clean up state from this test invocation."""
2133 2123
2134 2124 if self.options.keep_tmpdir:
2135 2125 return
2136 2126
2137 2127 vlog("# Cleaning up HGTMP", self._hgtmp)
2138 2128 shutil.rmtree(self._hgtmp, True)
2139 2129 for f in self._createdfiles:
2140 2130 try:
2141 2131 os.remove(f)
2142 2132 except OSError:
2143 2133 pass
2144 2134
2145 2135 def _usecorrectpython(self):
2146 2136 """Configure the environment to use the appropriate Python in tests."""
2147 2137 # Tests must use the same interpreter as us or bad things will happen.
2148 2138 pyexename = sys.platform == 'win32' and b'python.exe' or b'python'
2149 2139 if getattr(os, 'symlink', None):
2150 2140 vlog("# Making python executable in test path a symlink to '%s'" %
2151 2141 sys.executable)
2152 2142 mypython = os.path.join(self._tmpbindir, pyexename)
2153 2143 try:
2154 2144 if os.readlink(mypython) == sys.executable:
2155 2145 return
2156 2146 os.unlink(mypython)
2157 2147 except OSError as err:
2158 2148 if err.errno != errno.ENOENT:
2159 2149 raise
2160 2150 if self._findprogram(pyexename) != sys.executable:
2161 2151 try:
2162 2152 os.symlink(sys.executable, mypython)
2163 2153 self._createdfiles.append(mypython)
2164 2154 except OSError as err:
2165 2155 # child processes may race, which is harmless
2166 2156 if err.errno != errno.EEXIST:
2167 2157 raise
2168 2158 else:
2169 2159 exedir, exename = os.path.split(sys.executable)
2170 2160 vlog("# Modifying search path to find %s as %s in '%s'" %
2171 2161 (exename, pyexename, exedir))
2172 2162 path = os.environ['PATH'].split(os.pathsep)
2173 2163 while exedir in path:
2174 2164 path.remove(exedir)
2175 2165 os.environ['PATH'] = os.pathsep.join([exedir] + path)
2176 2166 if not self._findprogram(pyexename):
2177 2167 print("WARNING: Cannot find %s in search path" % pyexename)
2178 2168
2179 2169 def _installhg(self):
2180 2170 """Install hg into the test environment.
2181 2171
2182 2172 This will also configure hg with the appropriate testing settings.
2183 2173 """
2184 2174 vlog("# Performing temporary installation of HG")
2185 2175 installerrs = os.path.join(b"tests", b"install.err")
2186 2176 compiler = ''
2187 2177 if self.options.compiler:
2188 2178 compiler = '--compiler ' + self.options.compiler
2189 2179 if self.options.pure:
2190 2180 pure = b"--pure"
2191 2181 else:
2192 2182 pure = b""
2193 2183 py3 = ''
2194 2184
2195 2185 # Run installer in hg root
2196 2186 script = os.path.realpath(sys.argv[0])
2197 2187 exe = sys.executable
2198 2188 if PYTHON3:
2199 2189 py3 = b'--c2to3'
2200 2190 compiler = _bytespath(compiler)
2201 2191 script = _bytespath(script)
2202 2192 exe = _bytespath(exe)
2203 2193 hgroot = os.path.dirname(os.path.dirname(script))
2204 2194 self._hgroot = hgroot
2205 2195 os.chdir(hgroot)
2206 2196 nohome = b'--home=""'
2207 2197 if os.name == 'nt':
2208 2198 # The --home="" trick works only on OS where os.sep == '/'
2209 2199 # because of a distutils convert_path() fast-path. Avoid it at
2210 2200 # least on Windows for now, deal with .pydistutils.cfg bugs
2211 2201 # when they happen.
2212 2202 nohome = b''
2213 2203 cmd = (b'%(exe)s setup.py %(py3)s %(pure)s clean --all'
2214 2204 b' build %(compiler)s --build-base="%(base)s"'
2215 2205 b' install --force --prefix="%(prefix)s"'
2216 2206 b' --install-lib="%(libdir)s"'
2217 2207 b' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
2218 2208 % {b'exe': exe, b'py3': py3, b'pure': pure,
2219 2209 b'compiler': compiler,
2220 2210 b'base': os.path.join(self._hgtmp, b"build"),
2221 2211 b'prefix': self._installdir, b'libdir': self._pythondir,
2222 2212 b'bindir': self._bindir,
2223 2213 b'nohome': nohome, b'logfile': installerrs})
2224 2214
2225 2215 # setuptools requires install directories to exist.
2226 2216 def makedirs(p):
2227 2217 try:
2228 2218 os.makedirs(p)
2229 2219 except OSError as e:
2230 2220 if e.errno != errno.EEXIST:
2231 2221 raise
2232 2222 makedirs(self._pythondir)
2233 2223 makedirs(self._bindir)
2234 2224
2235 2225 vlog("# Running", cmd)
2236 2226 if os.system(cmd) == 0:
2237 2227 if not self.options.verbose:
2238 2228 try:
2239 2229 os.remove(installerrs)
2240 2230 except OSError as e:
2241 2231 if e.errno != errno.ENOENT:
2242 2232 raise
2243 2233 else:
2244 2234 f = open(installerrs, 'rb')
2245 2235 for line in f:
2246 2236 if PYTHON3:
2247 2237 sys.stdout.buffer.write(line)
2248 2238 else:
2249 2239 sys.stdout.write(line)
2250 2240 f.close()
2251 2241 sys.exit(1)
2252 2242 os.chdir(self._testdir)
2253 2243
2254 2244 self._usecorrectpython()
2255 2245
2256 2246 if self.options.py3k_warnings and not self.options.anycoverage:
2257 2247 vlog("# Updating hg command to enable Py3k Warnings switch")
2258 2248 f = open(os.path.join(self._bindir, 'hg'), 'rb')
2259 2249 lines = [line.rstrip() for line in f]
2260 2250 lines[0] += ' -3'
2261 2251 f.close()
2262 2252 f = open(os.path.join(self._bindir, 'hg'), 'wb')
2263 2253 for line in lines:
2264 2254 f.write(line + '\n')
2265 2255 f.close()
2266 2256
2267 2257 hgbat = os.path.join(self._bindir, b'hg.bat')
2268 2258 if os.path.isfile(hgbat):
2269 2259 # hg.bat expects to be put in bin/scripts while run-tests.py
2270 2260 # installation layout put it in bin/ directly. Fix it
2271 2261 f = open(hgbat, 'rb')
2272 2262 data = f.read()
2273 2263 f.close()
2274 2264 if b'"%~dp0..\python" "%~dp0hg" %*' in data:
2275 2265 data = data.replace(b'"%~dp0..\python" "%~dp0hg" %*',
2276 2266 b'"%~dp0python" "%~dp0hg" %*')
2277 2267 f = open(hgbat, 'wb')
2278 2268 f.write(data)
2279 2269 f.close()
2280 2270 else:
2281 2271 print('WARNING: cannot fix hg.bat reference to python.exe')
2282 2272
2283 2273 if self.options.anycoverage:
2284 2274 custom = os.path.join(self._testdir, 'sitecustomize.py')
2285 2275 target = os.path.join(self._pythondir, 'sitecustomize.py')
2286 2276 vlog('# Installing coverage trigger to %s' % target)
2287 2277 shutil.copyfile(custom, target)
2288 2278 rc = os.path.join(self._testdir, '.coveragerc')
2289 2279 vlog('# Installing coverage rc to %s' % rc)
2290 2280 os.environ['COVERAGE_PROCESS_START'] = rc
2291 2281 covdir = os.path.join(self._installdir, '..', 'coverage')
2292 2282 try:
2293 2283 os.mkdir(covdir)
2294 2284 except OSError as e:
2295 2285 if e.errno != errno.EEXIST:
2296 2286 raise
2297 2287
2298 2288 os.environ['COVERAGE_DIR'] = covdir
2299 2289
2300 2290 def _checkhglib(self, verb):
2301 2291 """Ensure that the 'mercurial' package imported by python is
2302 2292 the one we expect it to be. If not, print a warning to stderr."""
2303 2293 if ((self._bindir == self._pythondir) and
2304 2294 (self._bindir != self._tmpbindir)):
2305 2295 # The pythondir has been inferred from --with-hg flag.
2306 2296 # We cannot expect anything sensible here.
2307 2297 return
2308 2298 expecthg = os.path.join(self._pythondir, b'mercurial')
2309 2299 actualhg = self._gethgpath()
2310 2300 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
2311 2301 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
2312 2302 ' (expected %s)\n'
2313 2303 % (verb, actualhg, expecthg))
2314 2304 def _gethgpath(self):
2315 2305 """Return the path to the mercurial package that is actually found by
2316 2306 the current Python interpreter."""
2317 2307 if self._hgpath is not None:
2318 2308 return self._hgpath
2319 2309
2320 2310 cmd = b'%s -c "import mercurial; print (mercurial.__path__[0])"'
2321 2311 cmd = cmd % PYTHON
2322 2312 if PYTHON3:
2323 2313 cmd = _strpath(cmd)
2324 2314 pipe = os.popen(cmd)
2325 2315 try:
2326 2316 self._hgpath = _bytespath(pipe.read().strip())
2327 2317 finally:
2328 2318 pipe.close()
2329 2319
2330 2320 return self._hgpath
2331 2321
2332 2322 def _outputcoverage(self):
2333 2323 """Produce code coverage output."""
2334 2324 from coverage import coverage
2335 2325
2336 2326 vlog('# Producing coverage report')
2337 2327 # chdir is the easiest way to get short, relative paths in the
2338 2328 # output.
2339 2329 os.chdir(self._hgroot)
2340 2330 covdir = os.path.join(self._installdir, '..', 'coverage')
2341 2331 cov = coverage(data_file=os.path.join(covdir, 'cov'))
2342 2332
2343 2333 # Map install directory paths back to source directory.
2344 2334 cov.config.paths['srcdir'] = ['.', self._pythondir]
2345 2335
2346 2336 cov.combine()
2347 2337
2348 2338 omit = [os.path.join(x, '*') for x in [self._bindir, self._testdir]]
2349 2339 cov.report(ignore_errors=True, omit=omit)
2350 2340
2351 2341 if self.options.htmlcov:
2352 2342 htmldir = os.path.join(self._testdir, 'htmlcov')
2353 2343 cov.html_report(directory=htmldir, omit=omit)
2354 2344 if self.options.annotate:
2355 2345 adir = os.path.join(self._testdir, 'annotated')
2356 2346 if not os.path.isdir(adir):
2357 2347 os.mkdir(adir)
2358 2348 cov.annotate(directory=adir, omit=omit)
2359 2349
2360 2350 def _findprogram(self, program):
2361 2351 """Search PATH for a executable program"""
2362 2352 dpb = _bytespath(os.defpath)
2363 2353 sepb = _bytespath(os.pathsep)
2364 2354 for p in osenvironb.get(b'PATH', dpb).split(sepb):
2365 2355 name = os.path.join(p, program)
2366 2356 if os.name == 'nt' or os.access(name, os.X_OK):
2367 2357 return name
2368 2358 return None
2369 2359
2370 2360 def _checktools(self):
2371 2361 """Ensure tools required to run tests are present."""
2372 2362 for p in self.REQUIREDTOOLS:
2373 2363 if os.name == 'nt' and not p.endswith('.exe'):
2374 2364 p += '.exe'
2375 2365 found = self._findprogram(p)
2376 2366 if found:
2377 2367 vlog("# Found prerequisite", p, "at", found)
2378 2368 else:
2379 2369 print("WARNING: Did not find prerequisite tool: %s " % p)
2380 2370
2381 2371 if __name__ == '__main__':
2382 2372 runner = TestRunner()
2383 2373
2384 2374 try:
2385 2375 import msvcrt
2386 2376 msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
2387 2377 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
2388 2378 msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
2389 2379 except ImportError:
2390 2380 pass
2391 2381
2392 2382 sys.exit(runner.run(sys.argv[1:]))
@@ -1,1119 +1,1118 b''
1 #require json
2 1 #require serve
3 2
4 3 $ request() {
5 4 > get-with-headers.py --json localhost:$HGPORT "$1"
6 5 > }
7 6
8 7 $ hg init test
9 8 $ cd test
10 9 $ mkdir da
11 10 $ echo foo > da/foo
12 11 $ echo foo > foo
13 12 $ hg -q ci -A -m initial
14 13 $ echo bar > foo
15 14 $ hg ci -m 'modify foo'
16 15 $ echo bar > da/foo
17 16 $ hg ci -m 'modify da/foo'
18 17 $ hg bookmark bookmark1
19 18 $ hg up default
20 19 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
21 20 (leaving bookmark bookmark1)
22 21 $ hg mv foo foo-new
23 22 $ hg commit -m 'move foo'
24 23 $ hg tag -m 'create tag' tag1
25 24 $ hg phase --public -r .
26 25 $ echo baz > da/foo
27 26 $ hg commit -m 'another commit to da/foo'
28 27 $ hg tag -m 'create tag2' tag2
29 28 $ hg bookmark bookmark2
30 29 $ hg -q up -r 0
31 30 $ hg -q branch test-branch
32 31 $ echo branch > foo
33 32 $ hg commit -m 'create test branch'
34 33 $ echo branch_commit_2 > foo
35 34 $ hg commit -m 'another commit in test-branch'
36 35 $ hg -q up default
37 36 $ hg merge --tool :local test-branch
38 37 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
39 38 (branch merge, don't forget to commit)
40 39 $ hg commit -m 'merge test-branch into default'
41 40
42 41 $ hg log -G
43 42 @ changeset: 9:cc725e08502a
44 43 |\ tag: tip
45 44 | | parent: 6:ceed296fe500
46 45 | | parent: 8:ed66c30e87eb
47 46 | | user: test
48 47 | | date: Thu Jan 01 00:00:00 1970 +0000
49 48 | | summary: merge test-branch into default
50 49 | |
51 50 | o changeset: 8:ed66c30e87eb
52 51 | | branch: test-branch
53 52 | | user: test
54 53 | | date: Thu Jan 01 00:00:00 1970 +0000
55 54 | | summary: another commit in test-branch
56 55 | |
57 56 | o changeset: 7:6ab967a8ab34
58 57 | | branch: test-branch
59 58 | | parent: 0:06e557f3edf6
60 59 | | user: test
61 60 | | date: Thu Jan 01 00:00:00 1970 +0000
62 61 | | summary: create test branch
63 62 | |
64 63 o | changeset: 6:ceed296fe500
65 64 | | bookmark: bookmark2
66 65 | | user: test
67 66 | | date: Thu Jan 01 00:00:00 1970 +0000
68 67 | | summary: create tag2
69 68 | |
70 69 o | changeset: 5:f2890a05fea4
71 70 | | tag: tag2
72 71 | | user: test
73 72 | | date: Thu Jan 01 00:00:00 1970 +0000
74 73 | | summary: another commit to da/foo
75 74 | |
76 75 o | changeset: 4:93a8ce14f891
77 76 | | user: test
78 77 | | date: Thu Jan 01 00:00:00 1970 +0000
79 78 | | summary: create tag
80 79 | |
81 80 o | changeset: 3:78896eb0e102
82 81 | | tag: tag1
83 82 | | user: test
84 83 | | date: Thu Jan 01 00:00:00 1970 +0000
85 84 | | summary: move foo
86 85 | |
87 86 o | changeset: 2:8d7c456572ac
88 87 | | bookmark: bookmark1
89 88 | | user: test
90 89 | | date: Thu Jan 01 00:00:00 1970 +0000
91 90 | | summary: modify da/foo
92 91 | |
93 92 o | changeset: 1:f8bbb9024b10
94 93 |/ user: test
95 94 | date: Thu Jan 01 00:00:00 1970 +0000
96 95 | summary: modify foo
97 96 |
98 97 o changeset: 0:06e557f3edf6
99 98 user: test
100 99 date: Thu Jan 01 00:00:00 1970 +0000
101 100 summary: initial
102 101
103 102
104 103 $ hg serve -p $HGPORT -d --pid-file=hg.pid -A access.log -E error.log
105 104 $ cat hg.pid >> $DAEMON_PIDS
106 105
107 106 (Try to keep these in roughly the order they are defined in webcommands.py)
108 107
109 108 (log is handled by filelog/ and changelog/ - ignore it)
110 109
111 110 (rawfile/ doesn't use templating - nothing to test)
112 111
113 112 file/{revision}/{path} shows file revision
114 113
115 114 $ request json-file/06e557f3edf6/foo
116 115 200 Script output follows
117 116
118 117 "not yet implemented"
119 118
120 119 file/{revision} shows root directory info
121 120
122 121 $ request json-file/cc725e08502a
123 122 200 Script output follows
124 123
125 124 {
126 125 "abspath": "/",
127 126 "bookmarks": [],
128 127 "directories": [
129 128 {
130 129 "abspath": "/da",
131 130 "basename": "da",
132 131 "emptydirs": ""
133 132 }
134 133 ],
135 134 "files": [
136 135 {
137 136 "abspath": ".hgtags",
138 137 "basename": ".hgtags",
139 138 "date": [
140 139 0.0,
141 140 0
142 141 ],
143 142 "flags": "",
144 143 "size": 92
145 144 },
146 145 {
147 146 "abspath": "foo-new",
148 147 "basename": "foo-new",
149 148 "date": [
150 149 0.0,
151 150 0
152 151 ],
153 152 "flags": "",
154 153 "size": 4
155 154 }
156 155 ],
157 156 "node": "cc725e08502a79dd1eda913760fbe06ed7a9abc7",
158 157 "tags": [
159 158 "tip"
160 159 ]
161 160 }
162 161
163 162 changelog/ shows information about several changesets
164 163
165 164 $ request json-changelog
166 165 200 Script output follows
167 166
168 167 {
169 168 "changeset_count": 10,
170 169 "changesets": [
171 170 {
172 171 "bookmarks": [],
173 172 "date": [
174 173 0.0,
175 174 0
176 175 ],
177 176 "desc": "merge test-branch into default",
178 177 "node": "cc725e08502a79dd1eda913760fbe06ed7a9abc7",
179 178 "tags": [
180 179 "tip"
181 180 ],
182 181 "user": "test"
183 182 },
184 183 {
185 184 "bookmarks": [],
186 185 "date": [
187 186 0.0,
188 187 0
189 188 ],
190 189 "desc": "another commit in test-branch",
191 190 "node": "ed66c30e87eb65337c05a4229efaa5f1d5285a90",
192 191 "tags": [],
193 192 "user": "test"
194 193 },
195 194 {
196 195 "bookmarks": [],
197 196 "date": [
198 197 0.0,
199 198 0
200 199 ],
201 200 "desc": "create test branch",
202 201 "node": "6ab967a8ab3489227a83f80e920faa039a71819f",
203 202 "tags": [],
204 203 "user": "test"
205 204 },
206 205 {
207 206 "bookmarks": [
208 207 "bookmark2"
209 208 ],
210 209 "date": [
211 210 0.0,
212 211 0
213 212 ],
214 213 "desc": "create tag2",
215 214 "node": "ceed296fe500c3fac9541e31dad860cb49c89e45",
216 215 "tags": [],
217 216 "user": "test"
218 217 },
219 218 {
220 219 "bookmarks": [],
221 220 "date": [
222 221 0.0,
223 222 0
224 223 ],
225 224 "desc": "another commit to da/foo",
226 225 "node": "f2890a05fea49bfaf9fb27ed5490894eba32da78",
227 226 "tags": [
228 227 "tag2"
229 228 ],
230 229 "user": "test"
231 230 },
232 231 {
233 232 "bookmarks": [],
234 233 "date": [
235 234 0.0,
236 235 0
237 236 ],
238 237 "desc": "create tag",
239 238 "node": "93a8ce14f89156426b7fa981af8042da53f03aa0",
240 239 "tags": [],
241 240 "user": "test"
242 241 },
243 242 {
244 243 "bookmarks": [],
245 244 "date": [
246 245 0.0,
247 246 0
248 247 ],
249 248 "desc": "move foo",
250 249 "node": "78896eb0e102174ce9278438a95e12543e4367a7",
251 250 "tags": [
252 251 "tag1"
253 252 ],
254 253 "user": "test"
255 254 },
256 255 {
257 256 "bookmarks": [
258 257 "bookmark1"
259 258 ],
260 259 "date": [
261 260 0.0,
262 261 0
263 262 ],
264 263 "desc": "modify da/foo",
265 264 "node": "8d7c456572acf3557e8ed8a07286b10c408bcec5",
266 265 "tags": [],
267 266 "user": "test"
268 267 },
269 268 {
270 269 "bookmarks": [],
271 270 "date": [
272 271 0.0,
273 272 0
274 273 ],
275 274 "desc": "modify foo",
276 275 "node": "f8bbb9024b10f93cdbb8d940337398291d40dea8",
277 276 "tags": [],
278 277 "user": "test"
279 278 },
280 279 {
281 280 "bookmarks": [],
282 281 "date": [
283 282 0.0,
284 283 0
285 284 ],
286 285 "desc": "initial",
287 286 "node": "06e557f3edf66faa1ccaba5dd8c203c21cc79f1e",
288 287 "tags": [],
289 288 "user": "test"
290 289 }
291 290 ],
292 291 "node": "cc725e08502a79dd1eda913760fbe06ed7a9abc7"
293 292 }
294 293
295 294 changelog/{revision} shows information starting at a specific changeset
296 295
297 296 $ request json-changelog/f8bbb9024b10
298 297 200 Script output follows
299 298
300 299 {
301 300 "changeset_count": 10,
302 301 "changesets": [
303 302 {
304 303 "bookmarks": [],
305 304 "date": [
306 305 0.0,
307 306 0
308 307 ],
309 308 "desc": "modify foo",
310 309 "node": "f8bbb9024b10f93cdbb8d940337398291d40dea8",
311 310 "tags": [],
312 311 "user": "test"
313 312 },
314 313 {
315 314 "bookmarks": [],
316 315 "date": [
317 316 0.0,
318 317 0
319 318 ],
320 319 "desc": "initial",
321 320 "node": "06e557f3edf66faa1ccaba5dd8c203c21cc79f1e",
322 321 "tags": [],
323 322 "user": "test"
324 323 }
325 324 ],
326 325 "node": "f8bbb9024b10f93cdbb8d940337398291d40dea8"
327 326 }
328 327
329 328 shortlog/ shows information about a set of changesets
330 329
331 330 $ request json-shortlog
332 331 200 Script output follows
333 332
334 333 {
335 334 "changeset_count": 10,
336 335 "changesets": [
337 336 {
338 337 "bookmarks": [],
339 338 "date": [
340 339 0.0,
341 340 0
342 341 ],
343 342 "desc": "merge test-branch into default",
344 343 "node": "cc725e08502a79dd1eda913760fbe06ed7a9abc7",
345 344 "tags": [
346 345 "tip"
347 346 ],
348 347 "user": "test"
349 348 },
350 349 {
351 350 "bookmarks": [],
352 351 "date": [
353 352 0.0,
354 353 0
355 354 ],
356 355 "desc": "another commit in test-branch",
357 356 "node": "ed66c30e87eb65337c05a4229efaa5f1d5285a90",
358 357 "tags": [],
359 358 "user": "test"
360 359 },
361 360 {
362 361 "bookmarks": [],
363 362 "date": [
364 363 0.0,
365 364 0
366 365 ],
367 366 "desc": "create test branch",
368 367 "node": "6ab967a8ab3489227a83f80e920faa039a71819f",
369 368 "tags": [],
370 369 "user": "test"
371 370 },
372 371 {
373 372 "bookmarks": [
374 373 "bookmark2"
375 374 ],
376 375 "date": [
377 376 0.0,
378 377 0
379 378 ],
380 379 "desc": "create tag2",
381 380 "node": "ceed296fe500c3fac9541e31dad860cb49c89e45",
382 381 "tags": [],
383 382 "user": "test"
384 383 },
385 384 {
386 385 "bookmarks": [],
387 386 "date": [
388 387 0.0,
389 388 0
390 389 ],
391 390 "desc": "another commit to da/foo",
392 391 "node": "f2890a05fea49bfaf9fb27ed5490894eba32da78",
393 392 "tags": [
394 393 "tag2"
395 394 ],
396 395 "user": "test"
397 396 },
398 397 {
399 398 "bookmarks": [],
400 399 "date": [
401 400 0.0,
402 401 0
403 402 ],
404 403 "desc": "create tag",
405 404 "node": "93a8ce14f89156426b7fa981af8042da53f03aa0",
406 405 "tags": [],
407 406 "user": "test"
408 407 },
409 408 {
410 409 "bookmarks": [],
411 410 "date": [
412 411 0.0,
413 412 0
414 413 ],
415 414 "desc": "move foo",
416 415 "node": "78896eb0e102174ce9278438a95e12543e4367a7",
417 416 "tags": [
418 417 "tag1"
419 418 ],
420 419 "user": "test"
421 420 },
422 421 {
423 422 "bookmarks": [
424 423 "bookmark1"
425 424 ],
426 425 "date": [
427 426 0.0,
428 427 0
429 428 ],
430 429 "desc": "modify da/foo",
431 430 "node": "8d7c456572acf3557e8ed8a07286b10c408bcec5",
432 431 "tags": [],
433 432 "user": "test"
434 433 },
435 434 {
436 435 "bookmarks": [],
437 436 "date": [
438 437 0.0,
439 438 0
440 439 ],
441 440 "desc": "modify foo",
442 441 "node": "f8bbb9024b10f93cdbb8d940337398291d40dea8",
443 442 "tags": [],
444 443 "user": "test"
445 444 },
446 445 {
447 446 "bookmarks": [],
448 447 "date": [
449 448 0.0,
450 449 0
451 450 ],
452 451 "desc": "initial",
453 452 "node": "06e557f3edf66faa1ccaba5dd8c203c21cc79f1e",
454 453 "tags": [],
455 454 "user": "test"
456 455 }
457 456 ],
458 457 "node": "cc725e08502a79dd1eda913760fbe06ed7a9abc7"
459 458 }
460 459
461 460 changeset/ renders the tip changeset
462 461
463 462 $ request json-rev
464 463 200 Script output follows
465 464
466 465 {
467 466 "bookmarks": [],
468 467 "branch": "default",
469 468 "date": [
470 469 0.0,
471 470 0
472 471 ],
473 472 "desc": "merge test-branch into default",
474 473 "node": "cc725e08502a79dd1eda913760fbe06ed7a9abc7",
475 474 "parents": [
476 475 "ceed296fe500c3fac9541e31dad860cb49c89e45",
477 476 "ed66c30e87eb65337c05a4229efaa5f1d5285a90"
478 477 ],
479 478 "phase": "draft",
480 479 "tags": [
481 480 "tip"
482 481 ],
483 482 "user": "test"
484 483 }
485 484
486 485 changeset/{revision} shows tags
487 486
488 487 $ request json-rev/78896eb0e102
489 488 200 Script output follows
490 489
491 490 {
492 491 "bookmarks": [],
493 492 "branch": "default",
494 493 "date": [
495 494 0.0,
496 495 0
497 496 ],
498 497 "desc": "move foo",
499 498 "node": "78896eb0e102174ce9278438a95e12543e4367a7",
500 499 "parents": [
501 500 "8d7c456572acf3557e8ed8a07286b10c408bcec5"
502 501 ],
503 502 "phase": "public",
504 503 "tags": [
505 504 "tag1"
506 505 ],
507 506 "user": "test"
508 507 }
509 508
510 509 changeset/{revision} shows bookmarks
511 510
512 511 $ request json-rev/8d7c456572ac
513 512 200 Script output follows
514 513
515 514 {
516 515 "bookmarks": [
517 516 "bookmark1"
518 517 ],
519 518 "branch": "default",
520 519 "date": [
521 520 0.0,
522 521 0
523 522 ],
524 523 "desc": "modify da/foo",
525 524 "node": "8d7c456572acf3557e8ed8a07286b10c408bcec5",
526 525 "parents": [
527 526 "f8bbb9024b10f93cdbb8d940337398291d40dea8"
528 527 ],
529 528 "phase": "public",
530 529 "tags": [],
531 530 "user": "test"
532 531 }
533 532
534 533 changeset/{revision} shows branches
535 534
536 535 $ request json-rev/6ab967a8ab34
537 536 200 Script output follows
538 537
539 538 {
540 539 "bookmarks": [],
541 540 "branch": "test-branch",
542 541 "date": [
543 542 0.0,
544 543 0
545 544 ],
546 545 "desc": "create test branch",
547 546 "node": "6ab967a8ab3489227a83f80e920faa039a71819f",
548 547 "parents": [
549 548 "06e557f3edf66faa1ccaba5dd8c203c21cc79f1e"
550 549 ],
551 550 "phase": "draft",
552 551 "tags": [],
553 552 "user": "test"
554 553 }
555 554
556 555 manifest/{revision}/{path} shows info about a directory at a revision
557 556
558 557 $ request json-manifest/06e557f3edf6/
559 558 200 Script output follows
560 559
561 560 {
562 561 "abspath": "/",
563 562 "bookmarks": [],
564 563 "directories": [
565 564 {
566 565 "abspath": "/da",
567 566 "basename": "da",
568 567 "emptydirs": ""
569 568 }
570 569 ],
571 570 "files": [
572 571 {
573 572 "abspath": "foo",
574 573 "basename": "foo",
575 574 "date": [
576 575 0.0,
577 576 0
578 577 ],
579 578 "flags": "",
580 579 "size": 4
581 580 }
582 581 ],
583 582 "node": "06e557f3edf66faa1ccaba5dd8c203c21cc79f1e",
584 583 "tags": []
585 584 }
586 585
587 586 tags/ shows tags info
588 587
589 588 $ request json-tags
590 589 200 Script output follows
591 590
592 591 {
593 592 "node": "cc725e08502a79dd1eda913760fbe06ed7a9abc7",
594 593 "tags": [
595 594 {
596 595 "date": [
597 596 0.0,
598 597 0
599 598 ],
600 599 "node": "f2890a05fea49bfaf9fb27ed5490894eba32da78",
601 600 "tag": "tag2"
602 601 },
603 602 {
604 603 "date": [
605 604 0.0,
606 605 0
607 606 ],
608 607 "node": "78896eb0e102174ce9278438a95e12543e4367a7",
609 608 "tag": "tag1"
610 609 }
611 610 ]
612 611 }
613 612
614 613 bookmarks/ shows bookmarks info
615 614
616 615 $ request json-bookmarks
617 616 200 Script output follows
618 617
619 618 {
620 619 "bookmarks": [
621 620 {
622 621 "bookmark": "bookmark1",
623 622 "date": [
624 623 0.0,
625 624 0
626 625 ],
627 626 "node": "8d7c456572acf3557e8ed8a07286b10c408bcec5"
628 627 },
629 628 {
630 629 "bookmark": "bookmark2",
631 630 "date": [
632 631 0.0,
633 632 0
634 633 ],
635 634 "node": "ceed296fe500c3fac9541e31dad860cb49c89e45"
636 635 }
637 636 ],
638 637 "node": "cc725e08502a79dd1eda913760fbe06ed7a9abc7"
639 638 }
640 639
641 640 branches/ shows branches info
642 641
643 642 $ request json-branches
644 643 200 Script output follows
645 644
646 645 {
647 646 "branches": [
648 647 {
649 648 "branch": "default",
650 649 "date": [
651 650 0.0,
652 651 0
653 652 ],
654 653 "node": "cc725e08502a79dd1eda913760fbe06ed7a9abc7",
655 654 "status": "open"
656 655 },
657 656 {
658 657 "branch": "test-branch",
659 658 "date": [
660 659 0.0,
661 660 0
662 661 ],
663 662 "node": "ed66c30e87eb65337c05a4229efaa5f1d5285a90",
664 663 "status": "inactive"
665 664 }
666 665 ]
667 666 }
668 667
669 668 summary/ shows a summary of repository state
670 669
671 670 $ request json-summary
672 671 200 Script output follows
673 672
674 673 "not yet implemented"
675 674
676 675 filediff/{revision}/{path} shows changes to a file in a revision
677 676
678 677 $ request json-diff/f8bbb9024b10/foo
679 678 200 Script output follows
680 679
681 680 {
682 681 "author": "test",
683 682 "children": [],
684 683 "date": [
685 684 0.0,
686 685 0
687 686 ],
688 687 "desc": "modify foo",
689 688 "diff": [
690 689 {
691 690 "blockno": 1,
692 691 "lines": [
693 692 {
694 693 "l": "--- a/foo\tThu Jan 01 00:00:00 1970 +0000\n",
695 694 "n": 1,
696 695 "t": "-"
697 696 },
698 697 {
699 698 "l": "+++ b/foo\tThu Jan 01 00:00:00 1970 +0000\n",
700 699 "n": 2,
701 700 "t": "+"
702 701 },
703 702 {
704 703 "l": "@@ -1,1 +1,1 @@\n",
705 704 "n": 3,
706 705 "t": "@"
707 706 },
708 707 {
709 708 "l": "-foo\n",
710 709 "n": 4,
711 710 "t": "-"
712 711 },
713 712 {
714 713 "l": "+bar\n",
715 714 "n": 5,
716 715 "t": "+"
717 716 }
718 717 ]
719 718 }
720 719 ],
721 720 "node": "f8bbb9024b10f93cdbb8d940337398291d40dea8",
722 721 "parents": [
723 722 "06e557f3edf66faa1ccaba5dd8c203c21cc79f1e"
724 723 ],
725 724 "path": "foo"
726 725 }
727 726
728 727 comparison/{revision}/{path} shows information about before and after for a file
729 728
730 729 $ request json-comparison/f8bbb9024b10/foo
731 730 200 Script output follows
732 731
733 732 {
734 733 "author": "test",
735 734 "children": [],
736 735 "comparison": [
737 736 {
738 737 "lines": [
739 738 {
740 739 "ll": "foo",
741 740 "ln": 1,
742 741 "rl": "bar",
743 742 "rn": 1,
744 743 "t": "replace"
745 744 }
746 745 ]
747 746 }
748 747 ],
749 748 "date": [
750 749 0.0,
751 750 0
752 751 ],
753 752 "desc": "modify foo",
754 753 "leftnode": "06e557f3edf66faa1ccaba5dd8c203c21cc79f1e",
755 754 "node": "f8bbb9024b10f93cdbb8d940337398291d40dea8",
756 755 "parents": [
757 756 "06e557f3edf66faa1ccaba5dd8c203c21cc79f1e"
758 757 ],
759 758 "path": "foo",
760 759 "rightnode": "f8bbb9024b10f93cdbb8d940337398291d40dea8"
761 760 }
762 761
763 762 annotate/{revision}/{path} shows annotations for each line
764 763
765 764 $ request json-annotate/f8bbb9024b10/foo
766 765 200 Script output follows
767 766
768 767 {
769 768 "abspath": "foo",
770 769 "annotate": [
771 770 {
772 771 "abspath": "foo",
773 772 "author": "test",
774 773 "desc": "modify foo",
775 774 "line": "bar\n",
776 775 "lineno": 1,
777 776 "node": "f8bbb9024b10f93cdbb8d940337398291d40dea8",
778 777 "revdate": [
779 778 0.0,
780 779 0
781 780 ],
782 781 "targetline": 1
783 782 }
784 783 ],
785 784 "author": "test",
786 785 "children": [],
787 786 "date": [
788 787 0.0,
789 788 0
790 789 ],
791 790 "desc": "modify foo",
792 791 "node": "f8bbb9024b10f93cdbb8d940337398291d40dea8",
793 792 "parents": [
794 793 "06e557f3edf66faa1ccaba5dd8c203c21cc79f1e"
795 794 ],
796 795 "permissions": ""
797 796 }
798 797
799 798 filelog/{revision}/{path} shows history of a single file
800 799
801 800 $ request json-filelog/f8bbb9024b10/foo
802 801 200 Script output follows
803 802
804 803 "not yet implemented"
805 804
806 805 (archive/ doesn't use templating, so ignore it)
807 806
808 807 (static/ doesn't use templating, so ignore it)
809 808
810 809 graph/ shows information that can be used to render a graph of the DAG
811 810
812 811 $ request json-graph
813 812 200 Script output follows
814 813
815 814 "not yet implemented"
816 815
817 816 help/ shows help topics
818 817
819 818 $ request json-help
820 819 200 Script output follows
821 820
822 821 {
823 822 "earlycommands": [
824 823 {
825 824 "summary": "add the specified files on the next commit",
826 825 "topic": "add"
827 826 },
828 827 {
829 828 "summary": "show changeset information by line for each file",
830 829 "topic": "annotate"
831 830 },
832 831 {
833 832 "summary": "make a copy of an existing repository",
834 833 "topic": "clone"
835 834 },
836 835 {
837 836 "summary": "commit the specified files or all outstanding changes",
838 837 "topic": "commit"
839 838 },
840 839 {
841 840 "summary": "diff repository (or selected files)",
842 841 "topic": "diff"
843 842 },
844 843 {
845 844 "summary": "dump the header and diffs for one or more changesets",
846 845 "topic": "export"
847 846 },
848 847 {
849 848 "summary": "forget the specified files on the next commit",
850 849 "topic": "forget"
851 850 },
852 851 {
853 852 "summary": "create a new repository in the given directory",
854 853 "topic": "init"
855 854 },
856 855 {
857 856 "summary": "show revision history of entire repository or files",
858 857 "topic": "log"
859 858 },
860 859 {
861 860 "summary": "merge another revision into working directory",
862 861 "topic": "merge"
863 862 },
864 863 {
865 864 "summary": "pull changes from the specified source",
866 865 "topic": "pull"
867 866 },
868 867 {
869 868 "summary": "push changes to the specified destination",
870 869 "topic": "push"
871 870 },
872 871 {
873 872 "summary": "remove the specified files on the next commit",
874 873 "topic": "remove"
875 874 },
876 875 {
877 876 "summary": "start stand-alone webserver",
878 877 "topic": "serve"
879 878 },
880 879 {
881 880 "summary": "show changed files in the working directory",
882 881 "topic": "status"
883 882 },
884 883 {
885 884 "summary": "summarize working directory state",
886 885 "topic": "summary"
887 886 },
888 887 {
889 888 "summary": "update working directory (or switch revisions)",
890 889 "topic": "update"
891 890 }
892 891 ],
893 892 "othercommands": [
894 893 {
895 894 "summary": "add all new files, delete all missing files",
896 895 "topic": "addremove"
897 896 },
898 897 {
899 898 "summary": "create an unversioned archive of a repository revision",
900 899 "topic": "archive"
901 900 },
902 901 {
903 902 "summary": "reverse effect of earlier changeset",
904 903 "topic": "backout"
905 904 },
906 905 {
907 906 "summary": "subdivision search of changesets",
908 907 "topic": "bisect"
909 908 },
910 909 {
911 910 "summary": "create a new bookmark or list existing bookmarks",
912 911 "topic": "bookmarks"
913 912 },
914 913 {
915 914 "summary": "set or show the current branch name",
916 915 "topic": "branch"
917 916 },
918 917 {
919 918 "summary": "list repository named branches",
920 919 "topic": "branches"
921 920 },
922 921 {
923 922 "summary": "create a changegroup file",
924 923 "topic": "bundle"
925 924 },
926 925 {
927 926 "summary": "output the current or given revision of files",
928 927 "topic": "cat"
929 928 },
930 929 {
931 930 "summary": "show combined config settings from all hgrc files",
932 931 "topic": "config"
933 932 },
934 933 {
935 934 "summary": "mark files as copied for the next commit",
936 935 "topic": "copy"
937 936 },
938 937 {
939 938 "summary": "list tracked files",
940 939 "topic": "files"
941 940 },
942 941 {
943 942 "summary": "copy changes from other branches onto the current branch",
944 943 "topic": "graft"
945 944 },
946 945 {
947 946 "summary": "search for a pattern in specified files and revisions",
948 947 "topic": "grep"
949 948 },
950 949 {
951 950 "summary": "show branch heads",
952 951 "topic": "heads"
953 952 },
954 953 {
955 954 "summary": "show help for a given topic or a help overview",
956 955 "topic": "help"
957 956 },
958 957 {
959 958 "summary": "identify the working directory or specified revision",
960 959 "topic": "identify"
961 960 },
962 961 {
963 962 "summary": "import an ordered set of patches",
964 963 "topic": "import"
965 964 },
966 965 {
967 966 "summary": "show new changesets found in source",
968 967 "topic": "incoming"
969 968 },
970 969 {
971 970 "summary": "output the current or given revision of the project manifest",
972 971 "topic": "manifest"
973 972 },
974 973 {
975 974 "summary": "show changesets not found in the destination",
976 975 "topic": "outgoing"
977 976 },
978 977 {
979 978 "summary": "show aliases for remote repositories",
980 979 "topic": "paths"
981 980 },
982 981 {
983 982 "summary": "set or show the current phase name",
984 983 "topic": "phase"
985 984 },
986 985 {
987 986 "summary": "roll back an interrupted transaction",
988 987 "topic": "recover"
989 988 },
990 989 {
991 990 "summary": "rename files; equivalent of copy + remove",
992 991 "topic": "rename"
993 992 },
994 993 {
995 994 "summary": "redo merges or set/view the merge status of files",
996 995 "topic": "resolve"
997 996 },
998 997 {
999 998 "summary": "restore files to their checkout state",
1000 999 "topic": "revert"
1001 1000 },
1002 1001 {
1003 1002 "summary": "print the root (top) of the current working directory",
1004 1003 "topic": "root"
1005 1004 },
1006 1005 {
1007 1006 "summary": "add one or more tags for the current or given revision",
1008 1007 "topic": "tag"
1009 1008 },
1010 1009 {
1011 1010 "summary": "list repository tags",
1012 1011 "topic": "tags"
1013 1012 },
1014 1013 {
1015 1014 "summary": "apply one or more changegroup files",
1016 1015 "topic": "unbundle"
1017 1016 },
1018 1017 {
1019 1018 "summary": "verify the integrity of the repository",
1020 1019 "topic": "verify"
1021 1020 },
1022 1021 {
1023 1022 "summary": "output version and copyright information",
1024 1023 "topic": "version"
1025 1024 }
1026 1025 ],
1027 1026 "topics": [
1028 1027 {
1029 1028 "summary": "Configuration Files",
1030 1029 "topic": "config"
1031 1030 },
1032 1031 {
1033 1032 "summary": "Date Formats",
1034 1033 "topic": "dates"
1035 1034 },
1036 1035 {
1037 1036 "summary": "Diff Formats",
1038 1037 "topic": "diffs"
1039 1038 },
1040 1039 {
1041 1040 "summary": "Environment Variables",
1042 1041 "topic": "environment"
1043 1042 },
1044 1043 {
1045 1044 "summary": "Using Additional Features",
1046 1045 "topic": "extensions"
1047 1046 },
1048 1047 {
1049 1048 "summary": "Specifying File Sets",
1050 1049 "topic": "filesets"
1051 1050 },
1052 1051 {
1053 1052 "summary": "Glossary",
1054 1053 "topic": "glossary"
1055 1054 },
1056 1055 {
1057 1056 "summary": "Syntax for Mercurial Ignore Files",
1058 1057 "topic": "hgignore"
1059 1058 },
1060 1059 {
1061 1060 "summary": "Configuring hgweb",
1062 1061 "topic": "hgweb"
1063 1062 },
1064 1063 {
1065 1064 "summary": "Technical implementation topics",
1066 1065 "topic": "internals"
1067 1066 },
1068 1067 {
1069 1068 "summary": "Merge Tools",
1070 1069 "topic": "merge-tools"
1071 1070 },
1072 1071 {
1073 1072 "summary": "Specifying Multiple Revisions",
1074 1073 "topic": "multirevs"
1075 1074 },
1076 1075 {
1077 1076 "summary": "File Name Patterns",
1078 1077 "topic": "patterns"
1079 1078 },
1080 1079 {
1081 1080 "summary": "Working with Phases",
1082 1081 "topic": "phases"
1083 1082 },
1084 1083 {
1085 1084 "summary": "Specifying Single Revisions",
1086 1085 "topic": "revisions"
1087 1086 },
1088 1087 {
1089 1088 "summary": "Specifying Revision Sets",
1090 1089 "topic": "revsets"
1091 1090 },
1092 1091 {
1093 1092 "summary": "Using Mercurial from scripts and automation",
1094 1093 "topic": "scripting"
1095 1094 },
1096 1095 {
1097 1096 "summary": "Subrepositories",
1098 1097 "topic": "subrepos"
1099 1098 },
1100 1099 {
1101 1100 "summary": "Template Usage",
1102 1101 "topic": "templating"
1103 1102 },
1104 1103 {
1105 1104 "summary": "URL Paths",
1106 1105 "topic": "urls"
1107 1106 }
1108 1107 ]
1109 1108 }
1110 1109
1111 1110 help/{topic} shows an individual help topic
1112 1111
1113 1112 $ request json-help/phases
1114 1113 200 Script output follows
1115 1114
1116 1115 {
1117 1116 "rawdoc": "Working with Phases\n*", (glob)
1118 1117 "topic": "phases"
1119 1118 }
@@ -1,724 +1,720 b''
1 1 This file tests the behavior of run-tests.py itself.
2 2
3 3 Avoid interference from actual test env:
4 4
5 5 $ unset HGTEST_JOBS
6 6 $ unset HGTEST_TIMEOUT
7 7 $ unset HGTEST_PORT
8 8 $ unset HGTEST_SHELL
9 9
10 10 Smoke test with install
11 11 ============
12 12
13 13 $ run-tests.py $HGTEST_RUN_TESTS_PURE -l
14 14
15 15 # Ran 0 tests, 0 skipped, 0 warned, 0 failed.
16 16
17 17 Define a helper to avoid the install step
18 18 =============
19 19 $ rt()
20 20 > {
21 21 > run-tests.py --with-hg=`which hg` "$@"
22 22 > }
23 23
24 24 error paths
25 25
26 26 #if symlink
27 27 $ ln -s `which true` hg
28 28 $ run-tests.py --with-hg=./hg
29 29 warning: --with-hg should specify an hg script
30 30
31 31 # Ran 0 tests, 0 skipped, 0 warned, 0 failed.
32 32 $ rm hg
33 33 #endif
34 34
35 35 #if execbit
36 36 $ touch hg
37 37 $ run-tests.py --with-hg=./hg
38 38 Usage: run-tests.py [options] [tests]
39 39
40 40 run-tests.py: error: --with-hg must specify an executable hg script
41 41 [2]
42 42 $ rm hg
43 43 #endif
44 44
45 45 a succesful test
46 46 =======================
47 47
48 48 $ cat > test-success.t << EOF
49 49 > $ echo babar
50 50 > babar
51 51 > $ echo xyzzy
52 52 > never happens (?)
53 53 > xyzzy
54 54 > nor this (?)
55 55 > EOF
56 56
57 57 $ rt
58 58 .
59 59 # Ran 1 tests, 0 skipped, 0 warned, 0 failed.
60 60
61 61 failing test
62 62 ==================
63 63
64 64 $ cat > test-failure.t << EOF
65 65 > $ echo babar
66 66 > rataxes
67 67 > This is a noop statement so that
68 68 > this test is still more bytes than success.
69 69 > EOF
70 70
71 71 >>> fh = open('test-failure-unicode.t', 'wb')
72 72 >>> fh.write(u' $ echo babar\u03b1\n'.encode('utf-8')) and None
73 73 >>> fh.write(u' l\u03b5\u03b5t\n'.encode('utf-8')) and None
74 74
75 75 $ rt
76 76
77 77 --- $TESTTMP/test-failure.t
78 78 +++ $TESTTMP/test-failure.t.err
79 79 @@ -1,4 +1,4 @@
80 80 $ echo babar
81 81 - rataxes
82 82 + babar
83 83 This is a noop statement so that
84 84 this test is still more bytes than success.
85 85
86 86 ERROR: test-failure.t output changed
87 87 !.
88 88 --- $TESTTMP/test-failure-unicode.t
89 89 +++ $TESTTMP/test-failure-unicode.t.err
90 90 @@ -1,2 +1,2 @@
91 91 $ echo babar\xce\xb1 (esc)
92 92 - l\xce\xb5\xce\xb5t (esc)
93 93 + babar\xce\xb1 (esc)
94 94
95 95 ERROR: test-failure-unicode.t output changed
96 96 !
97 97 Failed test-failure.t: output changed
98 98 Failed test-failure-unicode.t: output changed
99 99 # Ran 3 tests, 0 skipped, 0 warned, 2 failed.
100 100 python hash seed: * (glob)
101 101 [1]
102 102
103 103 test --xunit support
104 104 $ rt --xunit=xunit.xml
105 105
106 106 --- $TESTTMP/test-failure.t
107 107 +++ $TESTTMP/test-failure.t.err
108 108 @@ -1,4 +1,4 @@
109 109 $ echo babar
110 110 - rataxes
111 111 + babar
112 112 This is a noop statement so that
113 113 this test is still more bytes than success.
114 114
115 115 ERROR: test-failure.t output changed
116 116 !.
117 117 --- $TESTTMP/test-failure-unicode.t
118 118 +++ $TESTTMP/test-failure-unicode.t.err
119 119 @@ -1,2 +1,2 @@
120 120 $ echo babar\xce\xb1 (esc)
121 121 - l\xce\xb5\xce\xb5t (esc)
122 122 + babar\xce\xb1 (esc)
123 123
124 124 ERROR: test-failure-unicode.t output changed
125 125 !
126 126 Failed test-failure.t: output changed
127 127 Failed test-failure-unicode.t: output changed
128 128 # Ran 3 tests, 0 skipped, 0 warned, 2 failed.
129 129 python hash seed: * (glob)
130 130 [1]
131 131 $ cat xunit.xml
132 132 <?xml version="1.0" encoding="utf-8"?>
133 133 <testsuite errors="0" failures="2" name="run-tests" skipped="0" tests="3">
134 134 <testcase name="test-success.t" time="*"/> (glob)
135 135 <testcase name="test-failure-unicode.t" time="*"> (glob)
136 136 <![CDATA[--- $TESTTMP/test-failure-unicode.t
137 137 +++ $TESTTMP/test-failure-unicode.t.err
138 138 @@ -1,2 +1,2 @@
139 139 $ echo babar\xce\xb1 (esc)
140 140 - l\xce\xb5\xce\xb5t (esc)
141 141 + babar\xce\xb1 (esc)
142 142 ]]> </testcase>
143 143 <testcase name="test-failure.t" time="*"> (glob)
144 144 <![CDATA[--- $TESTTMP/test-failure.t
145 145 +++ $TESTTMP/test-failure.t.err
146 146 @@ -1,4 +1,4 @@
147 147 $ echo babar
148 148 - rataxes
149 149 + babar
150 150 This is a noop statement so that
151 151 this test is still more bytes than success.
152 152 ]]> </testcase>
153 153 </testsuite>
154 154
155 155 $ rm test-failure-unicode.t
156 156
157 157 test for --retest
158 158 ====================
159 159
160 160 $ rt --retest
161 161
162 162 --- $TESTTMP/test-failure.t
163 163 +++ $TESTTMP/test-failure.t.err
164 164 @@ -1,4 +1,4 @@
165 165 $ echo babar
166 166 - rataxes
167 167 + babar
168 168 This is a noop statement so that
169 169 this test is still more bytes than success.
170 170
171 171 ERROR: test-failure.t output changed
172 172 !
173 173 Failed test-failure.t: output changed
174 174 # Ran 2 tests, 1 skipped, 0 warned, 1 failed.
175 175 python hash seed: * (glob)
176 176 [1]
177 177
178 178 Selecting Tests To Run
179 179 ======================
180 180
181 181 successful
182 182
183 183 $ rt test-success.t
184 184 .
185 185 # Ran 1 tests, 0 skipped, 0 warned, 0 failed.
186 186
187 187 success w/ keyword
188 188 $ rt -k xyzzy
189 189 .
190 190 # Ran 2 tests, 1 skipped, 0 warned, 0 failed.
191 191
192 192 failed
193 193
194 194 $ rt test-failure.t
195 195
196 196 --- $TESTTMP/test-failure.t
197 197 +++ $TESTTMP/test-failure.t.err
198 198 @@ -1,4 +1,4 @@
199 199 $ echo babar
200 200 - rataxes
201 201 + babar
202 202 This is a noop statement so that
203 203 this test is still more bytes than success.
204 204
205 205 ERROR: test-failure.t output changed
206 206 !
207 207 Failed test-failure.t: output changed
208 208 # Ran 1 tests, 0 skipped, 0 warned, 1 failed.
209 209 python hash seed: * (glob)
210 210 [1]
211 211
212 212 failure w/ keyword
213 213 $ rt -k rataxes
214 214
215 215 --- $TESTTMP/test-failure.t
216 216 +++ $TESTTMP/test-failure.t.err
217 217 @@ -1,4 +1,4 @@
218 218 $ echo babar
219 219 - rataxes
220 220 + babar
221 221 This is a noop statement so that
222 222 this test is still more bytes than success.
223 223
224 224 ERROR: test-failure.t output changed
225 225 !
226 226 Failed test-failure.t: output changed
227 227 # Ran 2 tests, 1 skipped, 0 warned, 1 failed.
228 228 python hash seed: * (glob)
229 229 [1]
230 230
231 231 Verify that when a process fails to start we show a useful message
232 232 ==================================================================
233 233
234 234 $ cat > test-serve-fail.t <<EOF
235 235 > $ echo 'abort: child process failed to start blah'
236 236 > EOF
237 237 $ rt test-serve-fail.t
238 238
239 239 ERROR: test-serve-fail.t output changed
240 240 !
241 241 Failed test-serve-fail.t: server failed to start (HGPORT=*) (glob)
242 242 # Ran 1 tests, 0 skipped, 0 warned, 1 failed.
243 243 python hash seed: * (glob)
244 244 [1]
245 245 $ rm test-serve-fail.t
246 246
247 247 Verify that we can try other ports
248 248 ===================================
249 249 $ hg init inuse
250 250 $ hg serve -R inuse -p $HGPORT -d --pid-file=blocks.pid
251 251 $ cat blocks.pid >> $DAEMON_PIDS
252 252 $ cat > test-serve-inuse.t <<EOF
253 253 > $ hg serve -R `pwd`/inuse -p \$HGPORT -d --pid-file=hg.pid
254 254 > $ cat hg.pid >> \$DAEMON_PIDS
255 255 > EOF
256 256 $ rt test-serve-inuse.t
257 257 .
258 258 # Ran 1 tests, 0 skipped, 0 warned, 0 failed.
259 259 $ rm test-serve-inuse.t
260 260
261 261 Running In Debug Mode
262 262 ======================
263 263
264 264 $ rt --debug 2>&1 | grep -v pwd
265 265 + echo *SALT* 0 0 (glob)
266 266 *SALT* 0 0 (glob)
267 267 + echo babar
268 268 babar
269 269 + echo *SALT* 4 0 (glob)
270 270 *SALT* 4 0 (glob)
271 271 *+ echo *SALT* 0 0 (glob)
272 272 *SALT* 0 0 (glob)
273 273 + echo babar
274 274 babar
275 275 + echo *SALT* 2 0 (glob)
276 276 *SALT* 2 0 (glob)
277 277 + echo xyzzy
278 278 xyzzy
279 279 + echo *SALT* 6 0 (glob)
280 280 *SALT* 6 0 (glob)
281 281 .
282 282 # Ran 2 tests, 0 skipped, 0 warned, 0 failed.
283 283
284 284 Parallel runs
285 285 ==============
286 286
287 287 (duplicate the failing test to get predictable output)
288 288 $ cp test-failure.t test-failure-copy.t
289 289
290 290 $ rt --jobs 2 test-failure*.t -n
291 291 !!
292 292 Failed test-failure*.t: output changed (glob)
293 293 Failed test-failure*.t: output changed (glob)
294 294 # Ran 2 tests, 0 skipped, 0 warned, 2 failed.
295 295 python hash seed: * (glob)
296 296 [1]
297 297
298 298 failures in parallel with --first should only print one failure
299 299 >>> f = open('test-nothing.t', 'w')
300 300 >>> f.write('foo\n' * 1024) and None
301 301 >>> f.write(' $ sleep 1') and None
302 302 $ rt --jobs 2 --first
303 303
304 304 --- $TESTTMP/test-failure*.t (glob)
305 305 +++ $TESTTMP/test-failure*.t.err (glob)
306 306 @@ -1,4 +1,4 @@
307 307 $ echo babar
308 308 - rataxes
309 309 + babar
310 310 This is a noop statement so that
311 311 this test is still more bytes than success.
312 312
313 313 Failed test-failure*.t: output changed (glob)
314 314 Failed test-nothing.t: output changed
315 315 # Ran 2 tests, 0 skipped, 0 warned, 2 failed.
316 316 python hash seed: * (glob)
317 317 [1]
318 318
319 319
320 320 (delete the duplicated test file)
321 321 $ rm test-failure-copy.t test-nothing.t
322 322
323 323
324 324 Interactive run
325 325 ===============
326 326
327 327 (backup the failing test)
328 328 $ cp test-failure.t backup
329 329
330 330 Refuse the fix
331 331
332 332 $ echo 'n' | rt -i
333 333
334 334 --- $TESTTMP/test-failure.t
335 335 +++ $TESTTMP/test-failure.t.err
336 336 @@ -1,4 +1,4 @@
337 337 $ echo babar
338 338 - rataxes
339 339 + babar
340 340 This is a noop statement so that
341 341 this test is still more bytes than success.
342 342 Accept this change? [n]
343 343 ERROR: test-failure.t output changed
344 344 !.
345 345 Failed test-failure.t: output changed
346 346 # Ran 2 tests, 0 skipped, 0 warned, 1 failed.
347 347 python hash seed: * (glob)
348 348 [1]
349 349
350 350 $ cat test-failure.t
351 351 $ echo babar
352 352 rataxes
353 353 This is a noop statement so that
354 354 this test is still more bytes than success.
355 355
356 356 Interactive with custom view
357 357
358 358 $ echo 'n' | rt -i --view echo
359 359 $TESTTMP/test-failure.t $TESTTMP/test-failure.t.err (glob)
360 360 Accept this change? [n]* (glob)
361 361 ERROR: test-failure.t output changed
362 362 !.
363 363 Failed test-failure.t: output changed
364 364 # Ran 2 tests, 0 skipped, 0 warned, 1 failed.
365 365 python hash seed: * (glob)
366 366 [1]
367 367
368 368 View the fix
369 369
370 370 $ echo 'y' | rt --view echo
371 371 $TESTTMP/test-failure.t $TESTTMP/test-failure.t.err (glob)
372 372
373 373 ERROR: test-failure.t output changed
374 374 !.
375 375 Failed test-failure.t: output changed
376 376 # Ran 2 tests, 0 skipped, 0 warned, 1 failed.
377 377 python hash seed: * (glob)
378 378 [1]
379 379
380 380 Accept the fix
381 381
382 382 $ echo " $ echo 'saved backup bundle to \$TESTTMP/foo.hg'" >> test-failure.t
383 383 $ echo " saved backup bundle to \$TESTTMP/foo.hg" >> test-failure.t
384 384 $ echo " $ echo 'saved backup bundle to \$TESTTMP/foo.hg'" >> test-failure.t
385 385 $ echo " saved backup bundle to \$TESTTMP/foo.hg (glob)" >> test-failure.t
386 386 $ echo " $ echo 'saved backup bundle to \$TESTTMP/foo.hg'" >> test-failure.t
387 387 $ echo " saved backup bundle to \$TESTTMP/*.hg (glob)" >> test-failure.t
388 388 $ echo 'y' | rt -i 2>&1
389 389
390 390 --- $TESTTMP/test-failure.t
391 391 +++ $TESTTMP/test-failure.t.err
392 392 @@ -1,9 +1,9 @@
393 393 $ echo babar
394 394 - rataxes
395 395 + babar
396 396 This is a noop statement so that
397 397 this test is still more bytes than success.
398 398 $ echo 'saved backup bundle to $TESTTMP/foo.hg'
399 399 - saved backup bundle to $TESTTMP/foo.hg
400 400 + saved backup bundle to $TESTTMP/foo.hg* (glob)
401 401 $ echo 'saved backup bundle to $TESTTMP/foo.hg'
402 402 saved backup bundle to $TESTTMP/foo.hg* (glob)
403 403 $ echo 'saved backup bundle to $TESTTMP/foo.hg'
404 404 Accept this change? [n] ..
405 405 # Ran 2 tests, 0 skipped, 0 warned, 0 failed.
406 406
407 407 $ sed -e 's,(glob)$,&<,g' test-failure.t
408 408 $ echo babar
409 409 babar
410 410 This is a noop statement so that
411 411 this test is still more bytes than success.
412 412 $ echo 'saved backup bundle to $TESTTMP/foo.hg'
413 413 saved backup bundle to $TESTTMP/foo.hg (glob)<
414 414 $ echo 'saved backup bundle to $TESTTMP/foo.hg'
415 415 saved backup bundle to $TESTTMP/foo.hg (glob)<
416 416 $ echo 'saved backup bundle to $TESTTMP/foo.hg'
417 417 saved backup bundle to $TESTTMP/*.hg (glob)<
418 418
419 419 (reinstall)
420 420 $ mv backup test-failure.t
421 421
422 422 No Diff
423 423 ===============
424 424
425 425 $ rt --nodiff
426 426 !.
427 427 Failed test-failure.t: output changed
428 428 # Ran 2 tests, 0 skipped, 0 warned, 1 failed.
429 429 python hash seed: * (glob)
430 430 [1]
431 431
432 432 test --tmpdir support
433 433 $ rt --tmpdir=$TESTTMP/keep test-success.t
434 434
435 435 Keeping testtmp dir: $TESTTMP/keep/child1/test-success.t (glob)
436 436 Keeping threadtmp dir: $TESTTMP/keep/child1 (glob)
437 437 .
438 438 # Ran 1 tests, 0 skipped, 0 warned, 0 failed.
439 439
440 440 timeouts
441 441 ========
442 442 $ cat > test-timeout.t <<EOF
443 443 > $ sleep 2
444 444 > $ echo pass
445 445 > pass
446 446 > EOF
447 447 > echo '#require slow' > test-slow-timeout.t
448 448 > cat test-timeout.t >> test-slow-timeout.t
449 449 $ rt --timeout=1 --slowtimeout=3 test-timeout.t test-slow-timeout.t
450 450 st
451 451 Skipped test-slow-timeout.t: missing feature: allow slow tests
452 452 Failed test-timeout.t: timed out
453 453 # Ran 1 tests, 1 skipped, 0 warned, 1 failed.
454 454 python hash seed: * (glob)
455 455 [1]
456 456 $ rt --timeout=1 --slowtimeout=3 \
457 457 > test-timeout.t test-slow-timeout.t --allow-slow-tests
458 458 .t
459 459 Failed test-timeout.t: timed out
460 460 # Ran 2 tests, 0 skipped, 0 warned, 1 failed.
461 461 python hash seed: * (glob)
462 462 [1]
463 463 $ rm test-timeout.t test-slow-timeout.t
464 464
465 465 test for --time
466 466 ==================
467 467
468 468 $ rt test-success.t --time
469 469 .
470 470 # Ran 1 tests, 0 skipped, 0 warned, 0 failed.
471 471 # Producing time report
472 472 start end cuser csys real Test
473 473 \s*[\d\.]{5} \s*[\d\.]{5} \s*[\d\.]{5} \s*[\d\.]{5} \s*[\d\.]{5} test-success.t (re)
474 474
475 475 test for --time with --job enabled
476 476 ====================================
477 477
478 478 $ rt test-success.t --time --jobs 2
479 479 .
480 480 # Ran 1 tests, 0 skipped, 0 warned, 0 failed.
481 481 # Producing time report
482 482 start end cuser csys real Test
483 483 \s*[\d\.]{5} \s*[\d\.]{5} \s*[\d\.]{5} \s*[\d\.]{5} \s*[\d\.]{5} test-success.t (re)
484 484
485 485 Skips
486 486 ================
487 487 $ cat > test-skip.t <<EOF
488 488 > $ echo xyzzy
489 489 > #require false
490 490 > EOF
491 491 $ rt --nodiff
492 492 !.s
493 493 Skipped test-skip.t: missing feature: nail clipper
494 494 Failed test-failure.t: output changed
495 495 # Ran 2 tests, 1 skipped, 0 warned, 1 failed.
496 496 python hash seed: * (glob)
497 497 [1]
498 498
499 499 $ rt --keyword xyzzy
500 500 .s
501 501 Skipped test-skip.t: missing feature: nail clipper
502 502 # Ran 2 tests, 2 skipped, 0 warned, 0 failed.
503 503
504 504 Skips with xml
505 505 $ rt --keyword xyzzy \
506 506 > --xunit=xunit.xml
507 507 .s
508 508 Skipped test-skip.t: missing feature: nail clipper
509 509 # Ran 2 tests, 2 skipped, 0 warned, 0 failed.
510 510 $ cat xunit.xml
511 511 <?xml version="1.0" encoding="utf-8"?>
512 512 <testsuite errors="0" failures="0" name="run-tests" skipped="2" tests="2">
513 513 <testcase name="test-success.t" time="*"/> (glob)
514 514 </testsuite>
515 515
516 516 Missing skips or blacklisted skips don't count as executed:
517 517 $ echo test-failure.t > blacklist
518 518 $ rt --blacklist=blacklist --json\
519 519 > test-failure.t test-bogus.t
520 520 ss
521 521 Skipped test-bogus.t: Doesn't exist
522 522 Skipped test-failure.t: blacklisted
523 523 # Ran 0 tests, 2 skipped, 0 warned, 0 failed.
524 524 $ cat report.json
525 525 testreport ={
526 526 "test-bogus.t": {
527 527 "result": "skip"
528 528 },
529 529 "test-failure.t": {
530 530 "result": "skip"
531 531 }
532 532 } (no-eol)
533 #if json
534
535 533 test for --json
536 534 ==================
537 535
538 536 $ rt --json
539 537
540 538 --- $TESTTMP/test-failure.t
541 539 +++ $TESTTMP/test-failure.t.err
542 540 @@ -1,4 +1,4 @@
543 541 $ echo babar
544 542 - rataxes
545 543 + babar
546 544 This is a noop statement so that
547 545 this test is still more bytes than success.
548 546
549 547 ERROR: test-failure.t output changed
550 548 !.s
551 549 Skipped test-skip.t: missing feature: nail clipper
552 550 Failed test-failure.t: output changed
553 551 # Ran 2 tests, 1 skipped, 0 warned, 1 failed.
554 552 python hash seed: * (glob)
555 553 [1]
556 554
557 555 $ cat report.json
558 556 testreport ={
559 557 "test-failure.t": [\{] (re)
560 558 "csys": "\s*[\d\.]{4,5}", ? (re)
561 559 "cuser": "\s*[\d\.]{4,5}", ? (re)
562 560 "diff": "---.+\+\+\+.+", ? (re)
563 561 "end": "\s*[\d\.]{4,5}", ? (re)
564 562 "result": "failure", ? (re)
565 563 "start": "\s*[\d\.]{4,5}", ? (re)
566 564 "time": "\s*[\d\.]{4,5}" (re)
567 565 }, ? (re)
568 566 "test-skip.t": {
569 567 "csys": "\s*[\d\.]{4,5}", ? (re)
570 568 "cuser": "\s*[\d\.]{4,5}", ? (re)
571 569 "diff": "", ? (re)
572 570 "end": "\s*[\d\.]{4,5}", ? (re)
573 571 "result": "skip", ? (re)
574 572 "start": "\s*[\d\.]{4,5}", ? (re)
575 573 "time": "\s*[\d\.]{4,5}" (re)
576 574 }, ? (re)
577 575 "test-success.t": [\{] (re)
578 576 "csys": "\s*[\d\.]{4,5}", ? (re)
579 577 "cuser": "\s*[\d\.]{4,5}", ? (re)
580 578 "diff": "", ? (re)
581 579 "end": "\s*[\d\.]{4,5}", ? (re)
582 580 "result": "success", ? (re)
583 581 "start": "\s*[\d\.]{4,5}", ? (re)
584 582 "time": "\s*[\d\.]{4,5}" (re)
585 583 }
586 584 } (no-eol)
587 585
588 586 Test that failed test accepted through interactive are properly reported:
589 587
590 588 $ cp test-failure.t backup
591 589 $ echo y | rt --json -i
592 590
593 591 --- $TESTTMP/test-failure.t
594 592 +++ $TESTTMP/test-failure.t.err
595 593 @@ -1,4 +1,4 @@
596 594 $ echo babar
597 595 - rataxes
598 596 + babar
599 597 This is a noop statement so that
600 598 this test is still more bytes than success.
601 599 Accept this change? [n] ..s
602 600 Skipped test-skip.t: missing feature: nail clipper
603 601 # Ran 2 tests, 1 skipped, 0 warned, 0 failed.
604 602
605 603 $ cat report.json
606 604 testreport ={
607 605 "test-failure.t": [\{] (re)
608 606 "csys": "\s*[\d\.]{4,5}", ? (re)
609 607 "cuser": "\s*[\d\.]{4,5}", ? (re)
610 608 "diff": "", ? (re)
611 609 "end": "\s*[\d\.]{4,5}", ? (re)
612 610 "result": "success", ? (re)
613 611 "start": "\s*[\d\.]{4,5}", ? (re)
614 612 "time": "\s*[\d\.]{4,5}" (re)
615 613 }, ? (re)
616 614 "test-skip.t": {
617 615 "csys": "\s*[\d\.]{4,5}", ? (re)
618 616 "cuser": "\s*[\d\.]{4,5}", ? (re)
619 617 "diff": "", ? (re)
620 618 "end": "\s*[\d\.]{4,5}", ? (re)
621 619 "result": "skip", ? (re)
622 620 "start": "\s*[\d\.]{4,5}", ? (re)
623 621 "time": "\s*[\d\.]{4,5}" (re)
624 622 }, ? (re)
625 623 "test-success.t": [\{] (re)
626 624 "csys": "\s*[\d\.]{4,5}", ? (re)
627 625 "cuser": "\s*[\d\.]{4,5}", ? (re)
628 626 "diff": "", ? (re)
629 627 "end": "\s*[\d\.]{4,5}", ? (re)
630 628 "result": "success", ? (re)
631 629 "start": "\s*[\d\.]{4,5}", ? (re)
632 630 "time": "\s*[\d\.]{4,5}" (re)
633 631 }
634 632 } (no-eol)
635 633 $ mv backup test-failure.t
636 634
637 #endif
638
639 635 backslash on end of line with glob matching is handled properly
640 636
641 637 $ cat > test-glob-backslash.t << EOF
642 638 > $ echo 'foo bar \\'
643 639 > foo * \ (glob)
644 640 > EOF
645 641
646 642 $ rt test-glob-backslash.t
647 643 .
648 644 # Ran 1 tests, 0 skipped, 0 warned, 0 failed.
649 645
650 646 $ rm -f test-glob-backslash.t
651 647
652 648 Test reusability for third party tools
653 649 ======================================
654 650
655 651 $ mkdir "$TESTTMP"/anothertests
656 652 $ cd "$TESTTMP"/anothertests
657 653
658 654 test that `run-tests.py` can execute hghave, even if it runs not in
659 655 Mercurial source tree.
660 656
661 657 $ cat > test-hghave.t <<EOF
662 658 > #require true
663 659 > $ echo foo
664 660 > foo
665 661 > EOF
666 662 $ rt $HGTEST_RUN_TESTS_PURE test-hghave.t
667 663 .
668 664 # Ran 1 tests, 0 skipped, 0 warned, 0 failed.
669 665
670 666 test that RUNTESTDIR refers the directory, in which `run-tests.py` now
671 667 running is placed.
672 668
673 669 $ cat > test-runtestdir.t <<EOF
674 670 > - $TESTDIR, in which test-run-tests.t is placed
675 671 > - \$TESTDIR, in which test-runtestdir.t is placed (expanded at runtime)
676 672 > - \$RUNTESTDIR, in which run-tests.py is placed (expanded at runtime)
677 673 >
678 674 > #if windows
679 675 > $ test "\$TESTDIR" = "$TESTTMP\anothertests"
680 676 > #else
681 677 > $ test "\$TESTDIR" = "$TESTTMP"/anothertests
682 678 > #endif
683 679 > $ test "\$RUNTESTDIR" = "$TESTDIR"
684 680 > $ head -n 3 "\$RUNTESTDIR"/../contrib/check-code.py
685 681 > #!/usr/bin/env python
686 682 > #
687 683 > # check-code - a style and portability checker for Mercurial
688 684 > EOF
689 685 $ rt $HGTEST_RUN_TESTS_PURE test-runtestdir.t
690 686 .
691 687 # Ran 1 tests, 0 skipped, 0 warned, 0 failed.
692 688
693 689 #if execbit
694 690
695 691 test that TESTDIR is referred in PATH
696 692
697 693 $ cat > custom-command.sh <<EOF
698 694 > #!/bin/sh
699 695 > echo "hello world"
700 696 > EOF
701 697 $ chmod +x custom-command.sh
702 698 $ cat > test-testdir-path.t <<EOF
703 699 > $ custom-command.sh
704 700 > hello world
705 701 > EOF
706 702 $ rt $HGTEST_RUN_TESTS_PURE test-testdir-path.t
707 703 .
708 704 # Ran 1 tests, 0 skipped, 0 warned, 0 failed.
709 705
710 706 #endif
711 707
712 708 test support for --allow-slow-tests
713 709 $ cat > test-very-slow-test.t <<EOF
714 710 > #require slow
715 711 > $ echo pass
716 712 > pass
717 713 > EOF
718 714 $ rt $HGTEST_RUN_TESTS_PURE test-very-slow-test.t
719 715 s
720 716 Skipped test-very-slow-test.t: missing feature: allow slow tests
721 717 # Ran 0 tests, 1 skipped, 0 warned, 0 failed.
722 718 $ rt $HGTEST_RUN_TESTS_PURE --allow-slow-tests test-very-slow-test.t
723 719 .
724 720 # Ran 1 tests, 0 skipped, 0 warned, 0 failed.
General Comments 0
You need to be logged in to leave comments. Login now