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