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