##// END OF EJS Templates
tests: rename _bytespath() to _sys2bytes() and _strpath() to _sys2str()...
Manuel Jacob -
r44935:55c443fc default
parent child Browse files
Show More
@@ -1,160 +1,160 b''
1 1 #!/usr/bin/env python
2 2
3 3 # fsmonitor-run-tests.py - Run Mercurial tests with fsmonitor enabled
4 4 #
5 5 # Copyright 2017 Facebook, Inc.
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 # This is a wrapper around run-tests.py that spins up an isolated instance of
11 11 # Watchman and runs the Mercurial tests against it. This ensures that the global
12 12 # version of Watchman isn't affected by anything this test does.
13 13
14 14 from __future__ import absolute_import
15 15 from __future__ import print_function
16 16
17 17 import argparse
18 18 import contextlib
19 19 import json
20 20 import os
21 21 import shutil
22 22 import subprocess
23 23 import sys
24 24 import tempfile
25 25 import uuid
26 26
27 27 osenvironb = getattr(os, 'environb', os.environ)
28 28
29 29 if sys.version_info > (3, 5, 0):
30 30 PYTHON3 = True
31 31 xrange = range # we use xrange in one place, and we'd rather not use range
32 32
33 def _bytespath(p):
33 def _sys2bytes(p):
34 34 return p.encode('utf-8')
35 35
36 36
37 37 elif sys.version_info >= (3, 0, 0):
38 38 print(
39 39 '%s is only supported on Python 3.5+ and 2.7, not %s'
40 40 % (sys.argv[0], '.'.join(str(v) for v in sys.version_info[:3]))
41 41 )
42 42 sys.exit(70) # EX_SOFTWARE from `man 3 sysexit`
43 43 else:
44 44 PYTHON3 = False
45 45
46 46 # In python 2.x, path operations are generally done using
47 47 # bytestrings by default, so we don't have to do any extra
48 48 # fiddling there. We define the wrapper functions anyway just to
49 49 # help keep code consistent between platforms.
50 def _bytespath(p):
50 def _sys2bytes(p):
51 51 return p
52 52
53 53
54 54 def getparser():
55 55 """Obtain the argument parser used by the CLI."""
56 56 parser = argparse.ArgumentParser(
57 57 description='Run tests with fsmonitor enabled.',
58 58 epilog='Unrecognized options are passed to run-tests.py.',
59 59 )
60 60 # - keep these sorted
61 61 # - none of these options should conflict with any in run-tests.py
62 62 parser.add_argument(
63 63 '--keep-fsmonitor-tmpdir',
64 64 action='store_true',
65 65 help='keep temporary directory with fsmonitor state',
66 66 )
67 67 parser.add_argument(
68 68 '--watchman',
69 69 help='location of watchman binary (default: watchman in PATH)',
70 70 default='watchman',
71 71 )
72 72
73 73 return parser
74 74
75 75
76 76 @contextlib.contextmanager
77 77 def watchman(args):
78 78 basedir = tempfile.mkdtemp(prefix='hg-fsmonitor')
79 79 try:
80 80 # Much of this configuration is borrowed from Watchman's test harness.
81 81 cfgfile = os.path.join(basedir, 'config.json')
82 82 # TODO: allow setting a config
83 83 with open(cfgfile, 'w') as f:
84 84 f.write(json.dumps({}))
85 85
86 86 logfile = os.path.join(basedir, 'log')
87 87 clilogfile = os.path.join(basedir, 'cli-log')
88 88 if os.name == 'nt':
89 89 sockfile = '\\\\.\\pipe\\watchman-test-%s' % uuid.uuid4().hex
90 90 else:
91 91 sockfile = os.path.join(basedir, 'sock')
92 92 pidfile = os.path.join(basedir, 'pid')
93 93 statefile = os.path.join(basedir, 'state')
94 94
95 95 argv = [
96 96 args.watchman,
97 97 '--sockname',
98 98 sockfile,
99 99 '--logfile',
100 100 logfile,
101 101 '--pidfile',
102 102 pidfile,
103 103 '--statefile',
104 104 statefile,
105 105 '--foreground',
106 106 '--log-level=2', # debug logging for watchman
107 107 ]
108 108
109 109 envb = osenvironb.copy()
110 envb[b'WATCHMAN_CONFIG_FILE'] = _bytespath(cfgfile)
110 envb[b'WATCHMAN_CONFIG_FILE'] = _sys2bytes(cfgfile)
111 111 with open(clilogfile, 'wb') as f:
112 112 proc = subprocess.Popen(
113 113 argv, env=envb, stdin=None, stdout=f, stderr=f
114 114 )
115 115 try:
116 116 yield sockfile
117 117 finally:
118 118 proc.terminate()
119 119 proc.kill()
120 120 finally:
121 121 if args.keep_fsmonitor_tmpdir:
122 122 print('fsmonitor dir available at %s' % basedir)
123 123 else:
124 124 shutil.rmtree(basedir, ignore_errors=True)
125 125
126 126
127 127 def run():
128 128 parser = getparser()
129 129 args, runtestsargv = parser.parse_known_args()
130 130
131 131 with watchman(args) as sockfile:
132 osenvironb[b'WATCHMAN_SOCK'] = _bytespath(sockfile)
132 osenvironb[b'WATCHMAN_SOCK'] = _sys2bytes(sockfile)
133 133 # Indicate to hghave that we're running with fsmonitor enabled.
134 134 osenvironb[b'HGFSMONITOR_TESTS'] = b'1'
135 135
136 136 runtestdir = os.path.dirname(__file__)
137 137 runtests = os.path.join(runtestdir, 'run-tests.py')
138 138 blacklist = os.path.join(runtestdir, 'blacklists', 'fsmonitor')
139 139
140 140 runtestsargv.insert(0, runtests)
141 141 runtestsargv.extend(
142 142 [
143 143 '--extra-config',
144 144 'extensions.fsmonitor=',
145 145 # specify fsmonitor.mode=paranoid always in order to force
146 146 # fsmonitor extension execute "paranoid" code path
147 147 #
148 148 # TODO: make fsmonitor-run-tests.py accept specific options
149 149 '--extra-config',
150 150 'fsmonitor.mode=paranoid',
151 151 '--blacklist',
152 152 blacklist,
153 153 ]
154 154 )
155 155
156 156 return subprocess.call(runtestsargv)
157 157
158 158
159 159 if __name__ == '__main__':
160 160 sys.exit(run())
@@ -1,1051 +1,1051 b''
1 1 from __future__ import absolute_import, print_function
2 2
3 3 import distutils.version
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 try:
20 20 import msvcrt
21 21
22 22 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
23 23 msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
24 24 except ImportError:
25 25 pass
26 26
27 27 stdout = getattr(sys.stdout, 'buffer', sys.stdout)
28 28 stderr = getattr(sys.stderr, 'buffer', sys.stderr)
29 29
30 30 if sys.version_info[0] >= 3:
31 31
32 def _bytespath(p):
32 def _sys2bytes(p):
33 33 if p is None:
34 34 return p
35 35 return p.encode('utf-8')
36 36
37 def _strpath(p):
37 def _bytes2sys(p):
38 38 if p is None:
39 39 return p
40 40 return p.decode('utf-8')
41 41
42 42
43 43 else:
44 44
45 def _bytespath(p):
45 def _sys2bytes(p):
46 46 return p
47 47
48 _strpath = _bytespath
48 _bytes2sys = _sys2bytes
49 49
50 50
51 51 def check(name, desc):
52 52 """Registers a check function for a feature."""
53 53
54 54 def decorator(func):
55 55 checks[name] = (func, desc)
56 56 return func
57 57
58 58 return decorator
59 59
60 60
61 61 def checkvers(name, desc, vers):
62 62 """Registers a check function for each of a series of versions.
63 63
64 64 vers can be a list or an iterator.
65 65
66 66 Produces a series of feature checks that have the form <name><vers> without
67 67 any punctuation (even if there's punctuation in 'vers'; i.e. this produces
68 68 'py38', not 'py3.8' or 'py-38')."""
69 69
70 70 def decorator(func):
71 71 def funcv(v):
72 72 def f():
73 73 return func(v)
74 74
75 75 return f
76 76
77 77 for v in vers:
78 78 v = str(v)
79 79 f = funcv(v)
80 80 checks['%s%s' % (name, v.replace('.', ''))] = (f, desc % v)
81 81 return func
82 82
83 83 return decorator
84 84
85 85
86 86 def checkfeatures(features):
87 87 result = {
88 88 'error': [],
89 89 'missing': [],
90 90 'skipped': [],
91 91 }
92 92
93 93 for feature in features:
94 94 negate = feature.startswith('no-')
95 95 if negate:
96 96 feature = feature[3:]
97 97
98 98 if feature not in checks:
99 99 result['missing'].append(feature)
100 100 continue
101 101
102 102 check, desc = checks[feature]
103 103 try:
104 104 available = check()
105 105 except Exception:
106 106 result['error'].append('hghave check failed: %s' % feature)
107 107 continue
108 108
109 109 if not negate and not available:
110 110 result['skipped'].append('missing feature: %s' % desc)
111 111 elif negate and available:
112 112 result['skipped'].append('system supports %s' % desc)
113 113
114 114 return result
115 115
116 116
117 117 def require(features):
118 118 """Require that features are available, exiting if not."""
119 119 result = checkfeatures(features)
120 120
121 121 for missing in result['missing']:
122 122 stderr.write(
123 123 ('skipped: unknown feature: %s\n' % missing).encode('utf-8')
124 124 )
125 125 for msg in result['skipped']:
126 126 stderr.write(('skipped: %s\n' % msg).encode('utf-8'))
127 127 for msg in result['error']:
128 128 stderr.write(('%s\n' % msg).encode('utf-8'))
129 129
130 130 if result['missing']:
131 131 sys.exit(2)
132 132
133 133 if result['skipped'] or result['error']:
134 134 sys.exit(1)
135 135
136 136
137 137 def matchoutput(cmd, regexp, ignorestatus=False):
138 138 """Return the match object if cmd executes successfully and its output
139 139 is matched by the supplied regular expression.
140 140 """
141 141 r = re.compile(regexp)
142 142 p = subprocess.Popen(
143 143 cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
144 144 )
145 145 s = p.communicate()[0]
146 146 ret = p.returncode
147 147 return (ignorestatus or not ret) and r.search(s)
148 148
149 149
150 150 @check("baz", "GNU Arch baz client")
151 151 def has_baz():
152 152 return matchoutput('baz --version 2>&1', br'baz Bazaar version')
153 153
154 154
155 155 @check("bzr", "Canonical's Bazaar client")
156 156 def has_bzr():
157 157 try:
158 158 import bzrlib
159 159 import bzrlib.bzrdir
160 160 import bzrlib.errors
161 161 import bzrlib.revision
162 162 import bzrlib.revisionspec
163 163
164 164 bzrlib.revisionspec.RevisionSpec
165 165 return bzrlib.__doc__ is not None
166 166 except (AttributeError, ImportError):
167 167 return False
168 168
169 169
170 170 @checkvers("bzr", "Canonical's Bazaar client >= %s", (1.14,))
171 171 def has_bzr_range(v):
172 172 major, minor = v.split('rc')[0].split('.')[0:2]
173 173 try:
174 174 import bzrlib
175 175
176 176 return bzrlib.__doc__ is not None and bzrlib.version_info[:2] >= (
177 177 int(major),
178 178 int(minor),
179 179 )
180 180 except ImportError:
181 181 return False
182 182
183 183
184 184 @check("chg", "running with chg")
185 185 def has_chg():
186 186 return 'CHGHG' in os.environ
187 187
188 188
189 189 @check("cvs", "cvs client/server")
190 190 def has_cvs():
191 191 re = br'Concurrent Versions System.*?server'
192 192 return matchoutput('cvs --version 2>&1', re) and not has_msys()
193 193
194 194
195 195 @check("cvs112", "cvs client/server 1.12.* (not cvsnt)")
196 196 def has_cvs112():
197 197 re = br'Concurrent Versions System \(CVS\) 1.12.*?server'
198 198 return matchoutput('cvs --version 2>&1', re) and not has_msys()
199 199
200 200
201 201 @check("cvsnt", "cvsnt client/server")
202 202 def has_cvsnt():
203 203 re = br'Concurrent Versions System \(CVSNT\) (\d+).(\d+).*\(client/server\)'
204 204 return matchoutput('cvsnt --version 2>&1', re)
205 205
206 206
207 207 @check("darcs", "darcs client")
208 208 def has_darcs():
209 209 return matchoutput('darcs --version', br'\b2\.([2-9]|\d{2})', True)
210 210
211 211
212 212 @check("mtn", "monotone client (>= 1.0)")
213 213 def has_mtn():
214 214 return matchoutput('mtn --version', br'monotone', True) and not matchoutput(
215 215 'mtn --version', br'monotone 0\.', True
216 216 )
217 217
218 218
219 219 @check("eol-in-paths", "end-of-lines in paths")
220 220 def has_eol_in_paths():
221 221 try:
222 222 fd, path = tempfile.mkstemp(dir='.', prefix=tempprefix, suffix='\n\r')
223 223 os.close(fd)
224 224 os.remove(path)
225 225 return True
226 226 except (IOError, OSError):
227 227 return False
228 228
229 229
230 230 @check("execbit", "executable bit")
231 231 def has_executablebit():
232 232 try:
233 233 EXECFLAGS = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
234 234 fh, fn = tempfile.mkstemp(dir='.', prefix=tempprefix)
235 235 try:
236 236 os.close(fh)
237 237 m = os.stat(fn).st_mode & 0o777
238 238 new_file_has_exec = m & EXECFLAGS
239 239 os.chmod(fn, m ^ EXECFLAGS)
240 240 exec_flags_cannot_flip = (os.stat(fn).st_mode & 0o777) == m
241 241 finally:
242 242 os.unlink(fn)
243 243 except (IOError, OSError):
244 244 # we don't care, the user probably won't be able to commit anyway
245 245 return False
246 246 return not (new_file_has_exec or exec_flags_cannot_flip)
247 247
248 248
249 249 @check("icasefs", "case insensitive file system")
250 250 def has_icasefs():
251 251 # Stolen from mercurial.util
252 252 fd, path = tempfile.mkstemp(dir='.', prefix=tempprefix)
253 253 os.close(fd)
254 254 try:
255 255 s1 = os.stat(path)
256 256 d, b = os.path.split(path)
257 257 p2 = os.path.join(d, b.upper())
258 258 if path == p2:
259 259 p2 = os.path.join(d, b.lower())
260 260 try:
261 261 s2 = os.stat(p2)
262 262 return s2 == s1
263 263 except OSError:
264 264 return False
265 265 finally:
266 266 os.remove(path)
267 267
268 268
269 269 @check("fifo", "named pipes")
270 270 def has_fifo():
271 271 if getattr(os, "mkfifo", None) is None:
272 272 return False
273 273 name = tempfile.mktemp(dir='.', prefix=tempprefix)
274 274 try:
275 275 os.mkfifo(name)
276 276 os.unlink(name)
277 277 return True
278 278 except OSError:
279 279 return False
280 280
281 281
282 282 @check("killdaemons", 'killdaemons.py support')
283 283 def has_killdaemons():
284 284 return True
285 285
286 286
287 287 @check("cacheable", "cacheable filesystem")
288 288 def has_cacheable_fs():
289 289 from mercurial import util
290 290
291 291 fd, path = tempfile.mkstemp(dir='.', prefix=tempprefix)
292 292 os.close(fd)
293 293 try:
294 294 return util.cachestat(path).cacheable()
295 295 finally:
296 296 os.remove(path)
297 297
298 298
299 299 @check("lsprof", "python lsprof module")
300 300 def has_lsprof():
301 301 try:
302 302 import _lsprof
303 303
304 304 _lsprof.Profiler # silence unused import warning
305 305 return True
306 306 except ImportError:
307 307 return False
308 308
309 309
310 310 def _gethgversion():
311 311 m = matchoutput('hg --version --quiet 2>&1', br'(\d+)\.(\d+)')
312 312 if not m:
313 313 return (0, 0)
314 314 return (int(m.group(1)), int(m.group(2)))
315 315
316 316
317 317 _hgversion = None
318 318
319 319
320 320 def gethgversion():
321 321 global _hgversion
322 322 if _hgversion is None:
323 323 _hgversion = _gethgversion()
324 324 return _hgversion
325 325
326 326
327 327 @checkvers(
328 328 "hg", "Mercurial >= %s", list([(1.0 * x) / 10 for x in range(9, 99)])
329 329 )
330 330 def has_hg_range(v):
331 331 major, minor = v.split('.')[0:2]
332 332 return gethgversion() >= (int(major), int(minor))
333 333
334 334
335 335 @check("hg08", "Mercurial >= 0.8")
336 336 def has_hg08():
337 337 if checks["hg09"][0]():
338 338 return True
339 339 return matchoutput('hg help annotate 2>&1', '--date')
340 340
341 341
342 342 @check("hg07", "Mercurial >= 0.7")
343 343 def has_hg07():
344 344 if checks["hg08"][0]():
345 345 return True
346 346 return matchoutput('hg --version --quiet 2>&1', 'Mercurial Distributed SCM')
347 347
348 348
349 349 @check("hg06", "Mercurial >= 0.6")
350 350 def has_hg06():
351 351 if checks["hg07"][0]():
352 352 return True
353 353 return matchoutput('hg --version --quiet 2>&1', 'Mercurial version')
354 354
355 355
356 356 @check("gettext", "GNU Gettext (msgfmt)")
357 357 def has_gettext():
358 358 return matchoutput('msgfmt --version', br'GNU gettext-tools')
359 359
360 360
361 361 @check("git", "git command line client")
362 362 def has_git():
363 363 return matchoutput('git --version 2>&1', br'^git version')
364 364
365 365
366 366 def getgitversion():
367 367 m = matchoutput('git --version 2>&1', br'git version (\d+)\.(\d+)')
368 368 if not m:
369 369 return (0, 0)
370 370 return (int(m.group(1)), int(m.group(2)))
371 371
372 372
373 373 # https://github.com/git-lfs/lfs-test-server
374 374 @check("lfs-test-server", "git-lfs test server")
375 375 def has_lfsserver():
376 376 exe = 'lfs-test-server'
377 377 if has_windows():
378 378 exe = 'lfs-test-server.exe'
379 379 return any(
380 380 os.access(os.path.join(path, exe), os.X_OK)
381 381 for path in os.environ["PATH"].split(os.pathsep)
382 382 )
383 383
384 384
385 385 @checkvers("git", "git client (with ext::sh support) version >= %s", (1.9,))
386 386 def has_git_range(v):
387 387 major, minor = v.split('.')[0:2]
388 388 return getgitversion() >= (int(major), int(minor))
389 389
390 390
391 391 @check("docutils", "Docutils text processing library")
392 392 def has_docutils():
393 393 try:
394 394 import docutils.core
395 395
396 396 docutils.core.publish_cmdline # silence unused import
397 397 return True
398 398 except ImportError:
399 399 return False
400 400
401 401
402 402 def getsvnversion():
403 403 m = matchoutput('svn --version --quiet 2>&1', br'^(\d+)\.(\d+)')
404 404 if not m:
405 405 return (0, 0)
406 406 return (int(m.group(1)), int(m.group(2)))
407 407
408 408
409 409 @checkvers("svn", "subversion client and admin tools >= %s", (1.3, 1.5))
410 410 def has_svn_range(v):
411 411 major, minor = v.split('.')[0:2]
412 412 return getsvnversion() >= (int(major), int(minor))
413 413
414 414
415 415 @check("svn", "subversion client and admin tools")
416 416 def has_svn():
417 417 return matchoutput('svn --version 2>&1', br'^svn, version') and matchoutput(
418 418 'svnadmin --version 2>&1', br'^svnadmin, version'
419 419 )
420 420
421 421
422 422 @check("svn-bindings", "subversion python bindings")
423 423 def has_svn_bindings():
424 424 try:
425 425 import svn.core
426 426
427 427 version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR
428 428 if version < (1, 4):
429 429 return False
430 430 return True
431 431 except ImportError:
432 432 return False
433 433
434 434
435 435 @check("p4", "Perforce server and client")
436 436 def has_p4():
437 437 return matchoutput('p4 -V', br'Rev\. P4/') and matchoutput(
438 438 'p4d -V', br'Rev\. P4D/'
439 439 )
440 440
441 441
442 442 @check("symlink", "symbolic links")
443 443 def has_symlink():
444 444 # mercurial.windows.checklink() is a hard 'no' at the moment
445 445 if os.name == 'nt' or getattr(os, "symlink", None) is None:
446 446 return False
447 447 name = tempfile.mktemp(dir='.', prefix=tempprefix)
448 448 try:
449 449 os.symlink(".", name)
450 450 os.unlink(name)
451 451 return True
452 452 except (OSError, AttributeError):
453 453 return False
454 454
455 455
456 456 @check("hardlink", "hardlinks")
457 457 def has_hardlink():
458 458 from mercurial import util
459 459
460 460 fh, fn = tempfile.mkstemp(dir='.', prefix=tempprefix)
461 461 os.close(fh)
462 462 name = tempfile.mktemp(dir='.', prefix=tempprefix)
463 463 try:
464 util.oslink(_bytespath(fn), _bytespath(name))
464 util.oslink(_sys2bytes(fn), _sys2bytes(name))
465 465 os.unlink(name)
466 466 return True
467 467 except OSError:
468 468 return False
469 469 finally:
470 470 os.unlink(fn)
471 471
472 472
473 473 @check("hardlink-whitelisted", "hardlinks on whitelisted filesystems")
474 474 def has_hardlink_whitelisted():
475 475 from mercurial import util
476 476
477 477 try:
478 478 fstype = util.getfstype(b'.')
479 479 except OSError:
480 480 return False
481 481 return fstype in util._hardlinkfswhitelist
482 482
483 483
484 484 @check("rmcwd", "can remove current working directory")
485 485 def has_rmcwd():
486 486 ocwd = os.getcwd()
487 487 temp = tempfile.mkdtemp(dir='.', prefix=tempprefix)
488 488 try:
489 489 os.chdir(temp)
490 490 # On Linux, 'rmdir .' isn't allowed, but the other names are okay.
491 491 # On Solaris and Windows, the cwd can't be removed by any names.
492 492 os.rmdir(os.getcwd())
493 493 return True
494 494 except OSError:
495 495 return False
496 496 finally:
497 497 os.chdir(ocwd)
498 498 # clean up temp dir on platforms where cwd can't be removed
499 499 try:
500 500 os.rmdir(temp)
501 501 except OSError:
502 502 pass
503 503
504 504
505 505 @check("tla", "GNU Arch tla client")
506 506 def has_tla():
507 507 return matchoutput('tla --version 2>&1', br'The GNU Arch Revision')
508 508
509 509
510 510 @check("gpg", "gpg client")
511 511 def has_gpg():
512 512 return matchoutput('gpg --version 2>&1', br'GnuPG')
513 513
514 514
515 515 @check("gpg2", "gpg client v2")
516 516 def has_gpg2():
517 517 return matchoutput('gpg --version 2>&1', br'GnuPG[^0-9]+2\.')
518 518
519 519
520 520 @check("gpg21", "gpg client v2.1+")
521 521 def has_gpg21():
522 522 return matchoutput('gpg --version 2>&1', br'GnuPG[^0-9]+2\.(?!0)')
523 523
524 524
525 525 @check("unix-permissions", "unix-style permissions")
526 526 def has_unix_permissions():
527 527 d = tempfile.mkdtemp(dir='.', prefix=tempprefix)
528 528 try:
529 529 fname = os.path.join(d, 'foo')
530 530 for umask in (0o77, 0o07, 0o22):
531 531 os.umask(umask)
532 532 f = open(fname, 'w')
533 533 f.close()
534 534 mode = os.stat(fname).st_mode
535 535 os.unlink(fname)
536 536 if mode & 0o777 != ~umask & 0o666:
537 537 return False
538 538 return True
539 539 finally:
540 540 os.rmdir(d)
541 541
542 542
543 543 @check("unix-socket", "AF_UNIX socket family")
544 544 def has_unix_socket():
545 545 return getattr(socket, 'AF_UNIX', None) is not None
546 546
547 547
548 548 @check("root", "root permissions")
549 549 def has_root():
550 550 return getattr(os, 'geteuid', None) and os.geteuid() == 0
551 551
552 552
553 553 @check("pyflakes", "Pyflakes python linter")
554 554 def has_pyflakes():
555 555 return matchoutput(
556 556 "sh -c \"echo 'import re' 2>&1 | pyflakes\"",
557 557 br"<stdin>:1: 're' imported but unused",
558 558 True,
559 559 )
560 560
561 561
562 562 @check("pylint", "Pylint python linter")
563 563 def has_pylint():
564 564 return matchoutput("pylint --help", br"Usage: pylint", True)
565 565
566 566
567 567 @check("clang-format", "clang-format C code formatter")
568 568 def has_clang_format():
569 569 m = matchoutput('clang-format --version', br'clang-format version (\d)')
570 570 # style changed somewhere between 4.x and 6.x
571 571 return m and int(m.group(1)) >= 6
572 572
573 573
574 574 @check("jshint", "JSHint static code analysis tool")
575 575 def has_jshint():
576 576 return matchoutput("jshint --version 2>&1", br"jshint v")
577 577
578 578
579 579 @check("pygments", "Pygments source highlighting library")
580 580 def has_pygments():
581 581 try:
582 582 import pygments
583 583
584 584 pygments.highlight # silence unused import warning
585 585 return True
586 586 except ImportError:
587 587 return False
588 588
589 589
590 590 @check("pygments25", "Pygments version >= 2.5")
591 591 def pygments25():
592 592 try:
593 593 import pygments
594 594
595 595 v = pygments.__version__
596 596 except ImportError:
597 597 return False
598 598
599 599 parts = v.split(".")
600 600 major = int(parts[0])
601 601 minor = int(parts[1])
602 602
603 603 return (major, minor) >= (2, 5)
604 604
605 605
606 606 @check("outer-repo", "outer repo")
607 607 def has_outer_repo():
608 608 # failing for other reasons than 'no repo' imply that there is a repo
609 609 return not matchoutput('hg root 2>&1', br'abort: no repository found', True)
610 610
611 611
612 612 @check("ssl", "ssl module available")
613 613 def has_ssl():
614 614 try:
615 615 import ssl
616 616
617 617 ssl.CERT_NONE
618 618 return True
619 619 except ImportError:
620 620 return False
621 621
622 622
623 623 @check("sslcontext", "python >= 2.7.9 ssl")
624 624 def has_sslcontext():
625 625 try:
626 626 import ssl
627 627
628 628 ssl.SSLContext
629 629 return True
630 630 except (ImportError, AttributeError):
631 631 return False
632 632
633 633
634 634 @check("defaultcacerts", "can verify SSL certs by system's CA certs store")
635 635 def has_defaultcacerts():
636 636 from mercurial import sslutil, ui as uimod
637 637
638 638 ui = uimod.ui.load()
639 639 return sslutil._defaultcacerts(ui) or sslutil._canloaddefaultcerts
640 640
641 641
642 642 @check("defaultcacertsloaded", "detected presence of loaded system CA certs")
643 643 def has_defaultcacertsloaded():
644 644 import ssl
645 645 from mercurial import sslutil, ui as uimod
646 646
647 647 if not has_defaultcacerts():
648 648 return False
649 649 if not has_sslcontext():
650 650 return False
651 651
652 652 ui = uimod.ui.load()
653 653 cafile = sslutil._defaultcacerts(ui)
654 654 ctx = ssl.create_default_context()
655 655 if cafile:
656 656 ctx.load_verify_locations(cafile=cafile)
657 657 else:
658 658 ctx.load_default_certs()
659 659
660 660 return len(ctx.get_ca_certs()) > 0
661 661
662 662
663 663 @check("tls1.2", "TLS 1.2 protocol support")
664 664 def has_tls1_2():
665 665 from mercurial import sslutil
666 666
667 667 return b'tls1.2' in sslutil.supportedprotocols
668 668
669 669
670 670 @check("windows", "Windows")
671 671 def has_windows():
672 672 return os.name == 'nt'
673 673
674 674
675 675 @check("system-sh", "system() uses sh")
676 676 def has_system_sh():
677 677 return os.name != 'nt'
678 678
679 679
680 680 @check("serve", "platform and python can manage 'hg serve -d'")
681 681 def has_serve():
682 682 return True
683 683
684 684
685 685 @check("test-repo", "running tests from repository")
686 686 def has_test_repo():
687 687 t = os.environ["TESTDIR"]
688 688 return os.path.isdir(os.path.join(t, "..", ".hg"))
689 689
690 690
691 691 @check("tic", "terminfo compiler and curses module")
692 692 def has_tic():
693 693 try:
694 694 import curses
695 695
696 696 curses.COLOR_BLUE
697 697 return matchoutput('test -x "`which tic`"', br'')
698 698 except (ImportError, AttributeError):
699 699 return False
700 700
701 701
702 702 @check("xz", "xz compression utility")
703 703 def has_xz():
704 704 # When Windows invokes a subprocess in shell mode, it uses `cmd.exe`, which
705 705 # only knows `where`, not `which`. So invoke MSYS shell explicitly.
706 706 return matchoutput("sh -c 'test -x \"`which xz`\"'", b'')
707 707
708 708
709 709 @check("msys", "Windows with MSYS")
710 710 def has_msys():
711 711 return os.getenv('MSYSTEM')
712 712
713 713
714 714 @check("aix", "AIX")
715 715 def has_aix():
716 716 return sys.platform.startswith("aix")
717 717
718 718
719 719 @check("osx", "OS X")
720 720 def has_osx():
721 721 return sys.platform == 'darwin'
722 722
723 723
724 724 @check("osxpackaging", "OS X packaging tools")
725 725 def has_osxpackaging():
726 726 try:
727 727 return (
728 728 matchoutput('pkgbuild', br'Usage: pkgbuild ', ignorestatus=1)
729 729 and matchoutput(
730 730 'productbuild', br'Usage: productbuild ', ignorestatus=1
731 731 )
732 732 and matchoutput('lsbom', br'Usage: lsbom', ignorestatus=1)
733 733 and matchoutput('xar --help', br'Usage: xar', ignorestatus=1)
734 734 )
735 735 except ImportError:
736 736 return False
737 737
738 738
739 739 @check('linuxormacos', 'Linux or MacOS')
740 740 def has_linuxormacos():
741 741 # This isn't a perfect test for MacOS. But it is sufficient for our needs.
742 742 return sys.platform.startswith(('linux', 'darwin'))
743 743
744 744
745 745 @check("docker", "docker support")
746 746 def has_docker():
747 747 pat = br'A self-sufficient runtime for'
748 748 if matchoutput('docker --help', pat):
749 749 if 'linux' not in sys.platform:
750 750 # TODO: in theory we should be able to test docker-based
751 751 # package creation on non-linux using boot2docker, but in
752 752 # practice that requires extra coordination to make sure
753 753 # $TESTTEMP is going to be visible at the same path to the
754 754 # boot2docker VM. If we figure out how to verify that, we
755 755 # can use the following instead of just saying False:
756 756 # return 'DOCKER_HOST' in os.environ
757 757 return False
758 758
759 759 return True
760 760 return False
761 761
762 762
763 763 @check("debhelper", "debian packaging tools")
764 764 def has_debhelper():
765 765 # Some versions of dpkg say `dpkg', some say 'dpkg' (` vs ' on the first
766 766 # quote), so just accept anything in that spot.
767 767 dpkg = matchoutput(
768 768 'dpkg --version', br"Debian .dpkg' package management program"
769 769 )
770 770 dh = matchoutput(
771 771 'dh --help', br'dh is a part of debhelper.', ignorestatus=True
772 772 )
773 773 dh_py2 = matchoutput(
774 774 'dh_python2 --help', br'other supported Python versions'
775 775 )
776 776 # debuild comes from the 'devscripts' package, though you might want
777 777 # the 'build-debs' package instead, which has a dependency on devscripts.
778 778 debuild = matchoutput(
779 779 'debuild --help', br'to run debian/rules with given parameter'
780 780 )
781 781 return dpkg and dh and dh_py2 and debuild
782 782
783 783
784 784 @check(
785 785 "debdeps", "debian build dependencies (run dpkg-checkbuilddeps in contrib/)"
786 786 )
787 787 def has_debdeps():
788 788 # just check exit status (ignoring output)
789 789 path = '%s/../contrib/packaging/debian/control' % os.environ['TESTDIR']
790 790 return matchoutput('dpkg-checkbuilddeps %s' % path, br'')
791 791
792 792
793 793 @check("demandimport", "demandimport enabled")
794 794 def has_demandimport():
795 795 # chg disables demandimport intentionally for performance wins.
796 796 return (not has_chg()) and os.environ.get('HGDEMANDIMPORT') != 'disable'
797 797
798 798
799 799 # Add "py27", "py35", ... as possible feature checks. Note that there's no
800 800 # punctuation here.
801 801 @checkvers("py", "Python >= %s", (2.7, 3.5, 3.6, 3.7, 3.8, 3.9))
802 802 def has_python_range(v):
803 803 major, minor = v.split('.')[0:2]
804 804 py_major, py_minor = sys.version_info.major, sys.version_info.minor
805 805
806 806 return (py_major, py_minor) >= (int(major), int(minor))
807 807
808 808
809 809 @check("py3", "running with Python 3.x")
810 810 def has_py3():
811 811 return 3 == sys.version_info[0]
812 812
813 813
814 814 @check("py3exe", "a Python 3.x interpreter is available")
815 815 def has_python3exe():
816 816 return matchoutput('python3 -V', br'^Python 3.(5|6|7|8|9)')
817 817
818 818
819 819 @check("pure", "running with pure Python code")
820 820 def has_pure():
821 821 return any(
822 822 [
823 823 os.environ.get("HGMODULEPOLICY") == "py",
824 824 os.environ.get("HGTEST_RUN_TESTS_PURE") == "--pure",
825 825 ]
826 826 )
827 827
828 828
829 829 @check("slow", "allow slow tests (use --allow-slow-tests)")
830 830 def has_slow():
831 831 return os.environ.get('HGTEST_SLOW') == 'slow'
832 832
833 833
834 834 @check("hypothesis", "Hypothesis automated test generation")
835 835 def has_hypothesis():
836 836 try:
837 837 import hypothesis
838 838
839 839 hypothesis.given
840 840 return True
841 841 except ImportError:
842 842 return False
843 843
844 844
845 845 @check("unziplinks", "unzip(1) understands and extracts symlinks")
846 846 def unzip_understands_symlinks():
847 847 return matchoutput('unzip --help', br'Info-ZIP')
848 848
849 849
850 850 @check("zstd", "zstd Python module available")
851 851 def has_zstd():
852 852 try:
853 853 import mercurial.zstd
854 854
855 855 mercurial.zstd.__version__
856 856 return True
857 857 except ImportError:
858 858 return False
859 859
860 860
861 861 @check("devfull", "/dev/full special file")
862 862 def has_dev_full():
863 863 return os.path.exists('/dev/full')
864 864
865 865
866 866 @check("ensurepip", "ensurepip module")
867 867 def has_ensurepip():
868 868 try:
869 869 import ensurepip
870 870
871 871 ensurepip.bootstrap
872 872 return True
873 873 except ImportError:
874 874 return False
875 875
876 876
877 877 @check("virtualenv", "Python virtualenv support")
878 878 def has_virtualenv():
879 879 try:
880 880 import virtualenv
881 881
882 882 virtualenv.ACTIVATE_SH
883 883 return True
884 884 except ImportError:
885 885 return False
886 886
887 887
888 888 @check("fsmonitor", "running tests with fsmonitor")
889 889 def has_fsmonitor():
890 890 return 'HGFSMONITOR_TESTS' in os.environ
891 891
892 892
893 893 @check("fuzzywuzzy", "Fuzzy string matching library")
894 894 def has_fuzzywuzzy():
895 895 try:
896 896 import fuzzywuzzy
897 897
898 898 fuzzywuzzy.__version__
899 899 return True
900 900 except ImportError:
901 901 return False
902 902
903 903
904 904 @check("clang-libfuzzer", "clang new enough to include libfuzzer")
905 905 def has_clang_libfuzzer():
906 906 mat = matchoutput('clang --version', br'clang version (\d)')
907 907 if mat:
908 908 # libfuzzer is new in clang 6
909 909 return int(mat.group(1)) > 5
910 910 return False
911 911
912 912
913 913 @check("clang-6.0", "clang 6.0 with version suffix (libfuzzer included)")
914 914 def has_clang60():
915 915 return matchoutput('clang-6.0 --version', br'clang version 6\.')
916 916
917 917
918 918 @check("xdiff", "xdiff algorithm")
919 919 def has_xdiff():
920 920 try:
921 921 from mercurial import policy
922 922
923 923 bdiff = policy.importmod('bdiff')
924 924 return bdiff.xdiffblocks(b'', b'') == [(0, 0, 0, 0)]
925 925 except (ImportError, AttributeError):
926 926 return False
927 927
928 928
929 929 @check('extraextensions', 'whether tests are running with extra extensions')
930 930 def has_extraextensions():
931 931 return 'HGTESTEXTRAEXTENSIONS' in os.environ
932 932
933 933
934 934 def getrepofeatures():
935 935 """Obtain set of repository features in use.
936 936
937 937 HGREPOFEATURES can be used to define or remove features. It contains
938 938 a space-delimited list of feature strings. Strings beginning with ``-``
939 939 mean to remove.
940 940 """
941 941 # Default list provided by core.
942 942 features = {
943 943 'bundlerepo',
944 944 'revlogstore',
945 945 'fncache',
946 946 }
947 947
948 948 # Features that imply other features.
949 949 implies = {
950 950 'simplestore': ['-revlogstore', '-bundlerepo', '-fncache'],
951 951 }
952 952
953 953 for override in os.environ.get('HGREPOFEATURES', '').split(' '):
954 954 if not override:
955 955 continue
956 956
957 957 if override.startswith('-'):
958 958 if override[1:] in features:
959 959 features.remove(override[1:])
960 960 else:
961 961 features.add(override)
962 962
963 963 for imply in implies.get(override, []):
964 964 if imply.startswith('-'):
965 965 if imply[1:] in features:
966 966 features.remove(imply[1:])
967 967 else:
968 968 features.add(imply)
969 969
970 970 return features
971 971
972 972
973 973 @check('reporevlogstore', 'repository using the default revlog store')
974 974 def has_reporevlogstore():
975 975 return 'revlogstore' in getrepofeatures()
976 976
977 977
978 978 @check('reposimplestore', 'repository using simple storage extension')
979 979 def has_reposimplestore():
980 980 return 'simplestore' in getrepofeatures()
981 981
982 982
983 983 @check('repobundlerepo', 'whether we can open bundle files as repos')
984 984 def has_repobundlerepo():
985 985 return 'bundlerepo' in getrepofeatures()
986 986
987 987
988 988 @check('repofncache', 'repository has an fncache')
989 989 def has_repofncache():
990 990 return 'fncache' in getrepofeatures()
991 991
992 992
993 993 @check('sqlite', 'sqlite3 module is available')
994 994 def has_sqlite():
995 995 try:
996 996 import sqlite3
997 997
998 998 version = sqlite3.sqlite_version_info
999 999 except ImportError:
1000 1000 return False
1001 1001
1002 1002 if version < (3, 8, 3):
1003 1003 # WITH clause not supported
1004 1004 return False
1005 1005
1006 1006 return matchoutput('sqlite3 -version', br'^3\.\d+')
1007 1007
1008 1008
1009 1009 @check('vcr', 'vcr http mocking library')
1010 1010 def has_vcr():
1011 1011 try:
1012 1012 import vcr
1013 1013
1014 1014 vcr.VCR
1015 1015 return True
1016 1016 except (ImportError, AttributeError):
1017 1017 pass
1018 1018 return False
1019 1019
1020 1020
1021 1021 @check('emacs', 'GNU Emacs')
1022 1022 def has_emacs():
1023 1023 # Our emacs lisp uses `with-eval-after-load` which is new in emacs
1024 1024 # 24.4, so we allow emacs 24.4, 24.5, and 25+ (24.5 was the last
1025 1025 # 24 release)
1026 1026 return matchoutput('emacs --version', b'GNU Emacs 2(4.4|4.5|5|6|7|8|9)')
1027 1027
1028 1028
1029 1029 @check('black', 'the black formatter for python')
1030 1030 def has_black():
1031 1031 blackcmd = 'black --version'
1032 1032 version_regex = b'black, version ([0-9a-b.]+)'
1033 1033 version = matchoutput(blackcmd, version_regex)
1034 1034 sv = distutils.version.StrictVersion
1035 return version and sv(_strpath(version.group(1))) >= sv('19.10b0')
1035 return version and sv(_bytes2sys(version.group(1))) >= sv('19.10b0')
1036 1036
1037 1037
1038 1038 @check('pytype', 'the pytype type checker')
1039 1039 def has_pytype():
1040 1040 pytypecmd = 'pytype --version'
1041 1041 version = matchoutput(pytypecmd, b'[0-9a-b.]+')
1042 1042 sv = distutils.version.StrictVersion
1043 return version and sv(_strpath(version.group(0))) >= sv('2019.10.17')
1043 return version and sv(_bytes2sys(version.group(0))) >= sv('2019.10.17')
1044 1044
1045 1045
1046 1046 @check("rustfmt", "rustfmt tool")
1047 1047 def has_rustfmt():
1048 1048 # We use Nightly's rustfmt due to current unstable config options.
1049 1049 return matchoutput(
1050 1050 '`rustup which --toolchain nightly rustfmt` --version', b'rustfmt'
1051 1051 )
@@ -1,3721 +1,3724 b''
1 1 #!/usr/bin/env python
2 2 #
3 3 # run-tests.py - Run a set of tests on Mercurial
4 4 #
5 5 # Copyright 2006 Matt Mackall <mpm@selenic.com>
6 6 #
7 7 # This software may be used and distributed according to the terms of the
8 8 # GNU General Public License version 2 or any later version.
9 9
10 10 # Modifying this script is tricky because it has many modes:
11 11 # - serial (default) vs parallel (-jN, N > 1)
12 12 # - no coverage (default) vs coverage (-c, -C, -s)
13 13 # - temp install (default) vs specific hg script (--with-hg, --local)
14 14 # - tests are a mix of shell scripts and Python scripts
15 15 #
16 16 # If you change this script, it is recommended that you ensure you
17 17 # haven't broken it by running it in various modes with a representative
18 18 # sample of test scripts. For example:
19 19 #
20 20 # 1) serial, no coverage, temp install:
21 21 # ./run-tests.py test-s*
22 22 # 2) serial, no coverage, local hg:
23 23 # ./run-tests.py --local test-s*
24 24 # 3) serial, coverage, temp install:
25 25 # ./run-tests.py -c test-s*
26 26 # 4) serial, coverage, local hg:
27 27 # ./run-tests.py -c --local test-s* # unsupported
28 28 # 5) parallel, no coverage, temp install:
29 29 # ./run-tests.py -j2 test-s*
30 30 # 6) parallel, no coverage, local hg:
31 31 # ./run-tests.py -j2 --local test-s*
32 32 # 7) parallel, coverage, temp install:
33 33 # ./run-tests.py -j2 -c test-s* # currently broken
34 34 # 8) parallel, coverage, local install:
35 35 # ./run-tests.py -j2 -c --local test-s* # unsupported (and broken)
36 36 # 9) parallel, custom tmp dir:
37 37 # ./run-tests.py -j2 --tmpdir /tmp/myhgtests
38 38 # 10) parallel, pure, tests that call run-tests:
39 39 # ./run-tests.py --pure `grep -l run-tests.py *.t`
40 40 #
41 41 # (You could use any subset of the tests: test-s* happens to match
42 42 # enough that it's worth doing parallel runs, few enough that it
43 43 # completes fairly quickly, includes both shell and Python scripts, and
44 44 # includes some scripts that run daemon processes.)
45 45
46 46 from __future__ import absolute_import, print_function
47 47
48 48 import argparse
49 49 import collections
50 50 import difflib
51 51 import distutils.version as version
52 52 import errno
53 53 import json
54 54 import multiprocessing
55 55 import os
56 56 import random
57 57 import re
58 58 import shutil
59 59 import signal
60 60 import socket
61 61 import subprocess
62 62 import sys
63 63 import sysconfig
64 64 import tempfile
65 65 import threading
66 66 import time
67 67 import unittest
68 68 import uuid
69 69 import xml.dom.minidom as minidom
70 70
71 71 try:
72 72 import Queue as queue
73 73 except ImportError:
74 74 import queue
75 75
76 76 try:
77 77 import shlex
78 78
79 79 shellquote = shlex.quote
80 80 except (ImportError, AttributeError):
81 81 import pipes
82 82
83 83 shellquote = pipes.quote
84 84
85 85 processlock = threading.Lock()
86 86
87 87 pygmentspresent = False
88 88 # ANSI color is unsupported prior to Windows 10
89 89 if os.name != 'nt':
90 90 try: # is pygments installed
91 91 import pygments
92 92 import pygments.lexers as lexers
93 93 import pygments.lexer as lexer
94 94 import pygments.formatters as formatters
95 95 import pygments.token as token
96 96 import pygments.style as style
97 97
98 98 pygmentspresent = True
99 99 difflexer = lexers.DiffLexer()
100 100 terminal256formatter = formatters.Terminal256Formatter()
101 101 except ImportError:
102 102 pass
103 103
104 104 if pygmentspresent:
105 105
106 106 class TestRunnerStyle(style.Style):
107 107 default_style = ""
108 108 skipped = token.string_to_tokentype("Token.Generic.Skipped")
109 109 failed = token.string_to_tokentype("Token.Generic.Failed")
110 110 skippedname = token.string_to_tokentype("Token.Generic.SName")
111 111 failedname = token.string_to_tokentype("Token.Generic.FName")
112 112 styles = {
113 113 skipped: '#e5e5e5',
114 114 skippedname: '#00ffff',
115 115 failed: '#7f0000',
116 116 failedname: '#ff0000',
117 117 }
118 118
119 119 class TestRunnerLexer(lexer.RegexLexer):
120 120 testpattern = r'[\w-]+\.(t|py)(#[a-zA-Z0-9_\-\.]+)?'
121 121 tokens = {
122 122 'root': [
123 123 (r'^Skipped', token.Generic.Skipped, 'skipped'),
124 124 (r'^Failed ', token.Generic.Failed, 'failed'),
125 125 (r'^ERROR: ', token.Generic.Failed, 'failed'),
126 126 ],
127 127 'skipped': [
128 128 (testpattern, token.Generic.SName),
129 129 (r':.*', token.Generic.Skipped),
130 130 ],
131 131 'failed': [
132 132 (testpattern, token.Generic.FName),
133 133 (r'(:| ).*', token.Generic.Failed),
134 134 ],
135 135 }
136 136
137 137 runnerformatter = formatters.Terminal256Formatter(style=TestRunnerStyle)
138 138 runnerlexer = TestRunnerLexer()
139 139
140 140 origenviron = os.environ.copy()
141 141
142 142 if sys.version_info > (3, 5, 0):
143 143 PYTHON3 = True
144 144 xrange = range # we use xrange in one place, and we'd rather not use range
145 145
146 def _bytespath(p):
146 def _sys2bytes(p):
147 147 if p is None:
148 148 return p
149 149 return p.encode('utf-8')
150 150
151 def _strpath(p):
151 def _bytes2sys(p):
152 152 if p is None:
153 153 return p
154 154 return p.decode('utf-8')
155 155
156 156 osenvironb = getattr(os, 'environb', None)
157 157 if osenvironb is None:
158 158 # Windows lacks os.environb, for instance. A proxy over the real thing
159 159 # instead of a copy allows the environment to be updated via bytes on
160 160 # all platforms.
161 161 class environbytes(object):
162 162 def __init__(self, strenv):
163 163 self.__len__ = strenv.__len__
164 164 self.clear = strenv.clear
165 165 self._strenv = strenv
166 166
167 167 def __getitem__(self, k):
168 v = self._strenv.__getitem__(_strpath(k))
169 return _bytespath(v)
168 v = self._strenv.__getitem__(_bytes2sys(k))
169 return _sys2bytes(v)
170 170
171 171 def __setitem__(self, k, v):
172 self._strenv.__setitem__(_strpath(k), _strpath(v))
172 self._strenv.__setitem__(_bytes2sys(k), _bytes2sys(v))
173 173
174 174 def __delitem__(self, k):
175 self._strenv.__delitem__(_strpath(k))
175 self._strenv.__delitem__(_bytes2sys(k))
176 176
177 177 def __contains__(self, k):
178 return self._strenv.__contains__(_strpath(k))
178 return self._strenv.__contains__(_bytes2sys(k))
179 179
180 180 def __iter__(self):
181 return iter([_bytespath(k) for k in iter(self._strenv)])
181 return iter([_sys2bytes(k) for k in iter(self._strenv)])
182 182
183 183 def get(self, k, default=None):
184 v = self._strenv.get(_strpath(k), _strpath(default))
185 return _bytespath(v)
184 v = self._strenv.get(_bytes2sys(k), _bytes2sys(default))
185 return _sys2bytes(v)
186 186
187 187 def pop(self, k, default=None):
188 v = self._strenv.pop(_strpath(k), _strpath(default))
189 return _bytespath(v)
188 v = self._strenv.pop(_bytes2sys(k), _bytes2sys(default))
189 return _sys2bytes(v)
190 190
191 191 osenvironb = environbytes(os.environ)
192 192
193 193 getcwdb = getattr(os, 'getcwdb')
194 194 if not getcwdb or os.name == 'nt':
195 getcwdb = lambda: _bytespath(os.getcwd())
195 getcwdb = lambda: _sys2bytes(os.getcwd())
196 196
197 197 elif sys.version_info >= (3, 0, 0):
198 198 print(
199 199 '%s is only supported on Python 3.5+ and 2.7, not %s'
200 200 % (sys.argv[0], '.'.join(str(v) for v in sys.version_info[:3]))
201 201 )
202 202 sys.exit(70) # EX_SOFTWARE from `man 3 sysexit`
203 203 else:
204 204 PYTHON3 = False
205 205
206 206 # In python 2.x, path operations are generally done using
207 207 # bytestrings by default, so we don't have to do any extra
208 208 # fiddling there. We define the wrapper functions anyway just to
209 209 # help keep code consistent between platforms.
210 def _bytespath(p):
210 def _sys2bytes(p):
211 211 return p
212 212
213 _strpath = _bytespath
213 _bytes2sys = _sys2bytes
214 214 osenvironb = os.environ
215 215 getcwdb = os.getcwd
216 216
217 217 # For Windows support
218 218 wifexited = getattr(os, "WIFEXITED", lambda x: False)
219 219
220 220 # Whether to use IPv6
221 221 def checksocketfamily(name, port=20058):
222 222 """return true if we can listen on localhost using family=name
223 223
224 224 name should be either 'AF_INET', or 'AF_INET6'.
225 225 port being used is okay - EADDRINUSE is considered as successful.
226 226 """
227 227 family = getattr(socket, name, None)
228 228 if family is None:
229 229 return False
230 230 try:
231 231 s = socket.socket(family, socket.SOCK_STREAM)
232 232 s.bind(('localhost', port))
233 233 s.close()
234 234 return True
235 235 except socket.error as exc:
236 236 if exc.errno == errno.EADDRINUSE:
237 237 return True
238 238 elif exc.errno in (errno.EADDRNOTAVAIL, errno.EPROTONOSUPPORT):
239 239 return False
240 240 else:
241 241 raise
242 242 else:
243 243 return False
244 244
245 245
246 246 # useipv6 will be set by parseargs
247 247 useipv6 = None
248 248
249 249
250 250 def checkportisavailable(port):
251 251 """return true if a port seems free to bind on localhost"""
252 252 if useipv6:
253 253 family = socket.AF_INET6
254 254 else:
255 255 family = socket.AF_INET
256 256 try:
257 257 s = socket.socket(family, socket.SOCK_STREAM)
258 258 s.bind(('localhost', port))
259 259 s.close()
260 260 return True
261 261 except socket.error as exc:
262 262 if exc.errno not in (
263 263 errno.EADDRINUSE,
264 264 errno.EADDRNOTAVAIL,
265 265 errno.EPROTONOSUPPORT,
266 266 ):
267 267 raise
268 268 return False
269 269
270 270
271 271 closefds = os.name == 'posix'
272 272
273 273
274 274 def Popen4(cmd, wd, timeout, env=None):
275 275 processlock.acquire()
276 276 p = subprocess.Popen(
277 _strpath(cmd),
277 _bytes2sys(cmd),
278 278 shell=True,
279 279 bufsize=-1,
280 cwd=_strpath(wd),
280 cwd=_bytes2sys(wd),
281 281 env=env,
282 282 close_fds=closefds,
283 283 stdin=subprocess.PIPE,
284 284 stdout=subprocess.PIPE,
285 285 stderr=subprocess.STDOUT,
286 286 )
287 287 processlock.release()
288 288
289 289 p.fromchild = p.stdout
290 290 p.tochild = p.stdin
291 291 p.childerr = p.stderr
292 292
293 293 p.timeout = False
294 294 if timeout:
295 295
296 296 def t():
297 297 start = time.time()
298 298 while time.time() - start < timeout and p.returncode is None:
299 299 time.sleep(0.1)
300 300 p.timeout = True
301 301 if p.returncode is None:
302 302 terminate(p)
303 303
304 304 threading.Thread(target=t).start()
305 305
306 306 return p
307 307
308 308
309 309 if sys.executable:
310 310 sysexecutable = sys.executable
311 311 elif os.environ.get('PYTHONEXECUTABLE'):
312 312 sysexecutable = os.environ['PYTHONEXECUTABLE']
313 313 elif os.environ.get('PYTHON'):
314 314 sysexecutable = os.environ['PYTHON']
315 315 else:
316 316 raise AssertionError('Could not find Python interpreter')
317 317
318 PYTHON = _bytespath(sysexecutable.replace('\\', '/'))
318 PYTHON = _sys2bytes(sysexecutable.replace('\\', '/'))
319 319 IMPL_PATH = b'PYTHONPATH'
320 320 if 'java' in sys.platform:
321 321 IMPL_PATH = b'JYTHONPATH'
322 322
323 323 defaults = {
324 324 'jobs': ('HGTEST_JOBS', multiprocessing.cpu_count()),
325 325 'timeout': ('HGTEST_TIMEOUT', 180),
326 326 'slowtimeout': ('HGTEST_SLOWTIMEOUT', 1500),
327 327 'port': ('HGTEST_PORT', 20059),
328 328 'shell': ('HGTEST_SHELL', 'sh'),
329 329 }
330 330
331 331
332 332 def canonpath(path):
333 333 return os.path.realpath(os.path.expanduser(path))
334 334
335 335
336 336 def parselistfiles(files, listtype, warn=True):
337 337 entries = dict()
338 338 for filename in files:
339 339 try:
340 340 path = os.path.expanduser(os.path.expandvars(filename))
341 341 f = open(path, "rb")
342 342 except IOError as err:
343 343 if err.errno != errno.ENOENT:
344 344 raise
345 345 if warn:
346 346 print("warning: no such %s file: %s" % (listtype, filename))
347 347 continue
348 348
349 349 for line in f.readlines():
350 350 line = line.split(b'#', 1)[0].strip()
351 351 if line:
352 352 entries[line] = filename
353 353
354 354 f.close()
355 355 return entries
356 356
357 357
358 358 def parsettestcases(path):
359 359 """read a .t test file, return a set of test case names
360 360
361 361 If path does not exist, return an empty set.
362 362 """
363 363 cases = []
364 364 try:
365 365 with open(path, 'rb') as f:
366 366 for l in f:
367 367 if l.startswith(b'#testcases '):
368 368 cases.append(sorted(l[11:].split()))
369 369 except IOError as ex:
370 370 if ex.errno != errno.ENOENT:
371 371 raise
372 372 return cases
373 373
374 374
375 375 def getparser():
376 376 """Obtain the OptionParser used by the CLI."""
377 377 parser = argparse.ArgumentParser(usage='%(prog)s [options] [tests]')
378 378
379 379 selection = parser.add_argument_group('Test Selection')
380 380 selection.add_argument(
381 381 '--allow-slow-tests',
382 382 action='store_true',
383 383 help='allow extremely slow tests',
384 384 )
385 385 selection.add_argument(
386 386 "--blacklist",
387 387 action="append",
388 388 help="skip tests listed in the specified blacklist file",
389 389 )
390 390 selection.add_argument(
391 391 "--changed",
392 392 help="run tests that are changed in parent rev or working directory",
393 393 )
394 394 selection.add_argument(
395 395 "-k", "--keywords", help="run tests matching keywords"
396 396 )
397 397 selection.add_argument(
398 398 "-r", "--retest", action="store_true", help="retest failed tests"
399 399 )
400 400 selection.add_argument(
401 401 "--test-list",
402 402 action="append",
403 403 help="read tests to run from the specified file",
404 404 )
405 405 selection.add_argument(
406 406 "--whitelist",
407 407 action="append",
408 408 help="always run tests listed in the specified whitelist file",
409 409 )
410 410 selection.add_argument(
411 411 'tests', metavar='TESTS', nargs='*', help='Tests to run'
412 412 )
413 413
414 414 harness = parser.add_argument_group('Test Harness Behavior')
415 415 harness.add_argument(
416 416 '--bisect-repo',
417 417 metavar='bisect_repo',
418 418 help=(
419 419 "Path of a repo to bisect. Use together with " "--known-good-rev"
420 420 ),
421 421 )
422 422 harness.add_argument(
423 423 "-d",
424 424 "--debug",
425 425 action="store_true",
426 426 help="debug mode: write output of test scripts to console"
427 427 " rather than capturing and diffing it (disables timeout)",
428 428 )
429 429 harness.add_argument(
430 430 "-f",
431 431 "--first",
432 432 action="store_true",
433 433 help="exit on the first test failure",
434 434 )
435 435 harness.add_argument(
436 436 "-i",
437 437 "--interactive",
438 438 action="store_true",
439 439 help="prompt to accept changed output",
440 440 )
441 441 harness.add_argument(
442 442 "-j",
443 443 "--jobs",
444 444 type=int,
445 445 help="number of jobs to run in parallel"
446 446 " (default: $%s or %d)" % defaults['jobs'],
447 447 )
448 448 harness.add_argument(
449 449 "--keep-tmpdir",
450 450 action="store_true",
451 451 help="keep temporary directory after running tests",
452 452 )
453 453 harness.add_argument(
454 454 '--known-good-rev',
455 455 metavar="known_good_rev",
456 456 help=(
457 457 "Automatically bisect any failures using this "
458 458 "revision as a known-good revision."
459 459 ),
460 460 )
461 461 harness.add_argument(
462 462 "--list-tests",
463 463 action="store_true",
464 464 help="list tests instead of running them",
465 465 )
466 466 harness.add_argument(
467 467 "--loop", action="store_true", help="loop tests repeatedly"
468 468 )
469 469 harness.add_argument(
470 470 '--random', action="store_true", help='run tests in random order'
471 471 )
472 472 harness.add_argument(
473 473 '--order-by-runtime',
474 474 action="store_true",
475 475 help='run slowest tests first, according to .testtimes',
476 476 )
477 477 harness.add_argument(
478 478 "-p",
479 479 "--port",
480 480 type=int,
481 481 help="port on which servers should listen"
482 482 " (default: $%s or %d)" % defaults['port'],
483 483 )
484 484 harness.add_argument(
485 485 '--profile-runner',
486 486 action='store_true',
487 487 help='run statprof on run-tests',
488 488 )
489 489 harness.add_argument(
490 490 "-R", "--restart", action="store_true", help="restart at last error"
491 491 )
492 492 harness.add_argument(
493 493 "--runs-per-test",
494 494 type=int,
495 495 dest="runs_per_test",
496 496 help="run each test N times (default=1)",
497 497 default=1,
498 498 )
499 499 harness.add_argument(
500 500 "--shell", help="shell to use (default: $%s or %s)" % defaults['shell']
501 501 )
502 502 harness.add_argument(
503 503 '--showchannels', action='store_true', help='show scheduling channels'
504 504 )
505 505 harness.add_argument(
506 506 "--slowtimeout",
507 507 type=int,
508 508 help="kill errant slow tests after SLOWTIMEOUT seconds"
509 509 " (default: $%s or %d)" % defaults['slowtimeout'],
510 510 )
511 511 harness.add_argument(
512 512 "-t",
513 513 "--timeout",
514 514 type=int,
515 515 help="kill errant tests after TIMEOUT seconds"
516 516 " (default: $%s or %d)" % defaults['timeout'],
517 517 )
518 518 harness.add_argument(
519 519 "--tmpdir",
520 520 help="run tests in the given temporary directory"
521 521 " (implies --keep-tmpdir)",
522 522 )
523 523 harness.add_argument(
524 524 "-v", "--verbose", action="store_true", help="output verbose messages"
525 525 )
526 526
527 527 hgconf = parser.add_argument_group('Mercurial Configuration')
528 528 hgconf.add_argument(
529 529 "--chg",
530 530 action="store_true",
531 531 help="install and use chg wrapper in place of hg",
532 532 )
533 533 hgconf.add_argument("--compiler", help="compiler to build with")
534 534 hgconf.add_argument(
535 535 '--extra-config-opt',
536 536 action="append",
537 537 default=[],
538 538 help='set the given config opt in the test hgrc',
539 539 )
540 540 hgconf.add_argument(
541 541 "-l",
542 542 "--local",
543 543 action="store_true",
544 544 help="shortcut for --with-hg=<testdir>/../hg, "
545 545 "and --with-chg=<testdir>/../contrib/chg/chg if --chg is set",
546 546 )
547 547 hgconf.add_argument(
548 548 "--ipv6",
549 549 action="store_true",
550 550 help="prefer IPv6 to IPv4 for network related tests",
551 551 )
552 552 hgconf.add_argument(
553 553 "--pure",
554 554 action="store_true",
555 555 help="use pure Python code instead of C extensions",
556 556 )
557 557 hgconf.add_argument(
558 558 "--with-chg",
559 559 metavar="CHG",
560 560 help="use specified chg wrapper in place of hg",
561 561 )
562 562 hgconf.add_argument(
563 563 "--with-hg",
564 564 metavar="HG",
565 565 help="test using specified hg script rather than a "
566 566 "temporary installation",
567 567 )
568 568
569 569 reporting = parser.add_argument_group('Results Reporting')
570 570 reporting.add_argument(
571 571 "-C",
572 572 "--annotate",
573 573 action="store_true",
574 574 help="output files annotated with coverage",
575 575 )
576 576 reporting.add_argument(
577 577 "--color",
578 578 choices=["always", "auto", "never"],
579 579 default=os.environ.get('HGRUNTESTSCOLOR', 'auto'),
580 580 help="colorisation: always|auto|never (default: auto)",
581 581 )
582 582 reporting.add_argument(
583 583 "-c",
584 584 "--cover",
585 585 action="store_true",
586 586 help="print a test coverage report",
587 587 )
588 588 reporting.add_argument(
589 589 '--exceptions',
590 590 action='store_true',
591 591 help='log all exceptions and generate an exception report',
592 592 )
593 593 reporting.add_argument(
594 594 "-H",
595 595 "--htmlcov",
596 596 action="store_true",
597 597 help="create an HTML report of the coverage of the files",
598 598 )
599 599 reporting.add_argument(
600 600 "--json",
601 601 action="store_true",
602 602 help="store test result data in 'report.json' file",
603 603 )
604 604 reporting.add_argument(
605 605 "--outputdir",
606 606 help="directory to write error logs to (default=test directory)",
607 607 )
608 608 reporting.add_argument(
609 609 "-n", "--nodiff", action="store_true", help="skip showing test changes"
610 610 )
611 611 reporting.add_argument(
612 612 "-S",
613 613 "--noskips",
614 614 action="store_true",
615 615 help="don't report skip tests verbosely",
616 616 )
617 617 reporting.add_argument(
618 618 "--time", action="store_true", help="time how long each test takes"
619 619 )
620 620 reporting.add_argument("--view", help="external diff viewer")
621 621 reporting.add_argument(
622 622 "--xunit", help="record xunit results at specified path"
623 623 )
624 624
625 625 for option, (envvar, default) in defaults.items():
626 626 defaults[option] = type(default)(os.environ.get(envvar, default))
627 627 parser.set_defaults(**defaults)
628 628
629 629 return parser
630 630
631 631
632 632 def parseargs(args, parser):
633 633 """Parse arguments with our OptionParser and validate results."""
634 634 options = parser.parse_args(args)
635 635
636 636 # jython is always pure
637 637 if 'java' in sys.platform or '__pypy__' in sys.modules:
638 638 options.pure = True
639 639
640 640 if options.local:
641 641 if options.with_hg or options.with_chg:
642 642 parser.error('--local cannot be used with --with-hg or --with-chg')
643 testdir = os.path.dirname(_bytespath(canonpath(sys.argv[0])))
643 testdir = os.path.dirname(_sys2bytes(canonpath(sys.argv[0])))
644 644 reporootdir = os.path.dirname(testdir)
645 645 pathandattrs = [(b'hg', 'with_hg')]
646 646 if options.chg:
647 647 pathandattrs.append((b'contrib/chg/chg', 'with_chg'))
648 648 for relpath, attr in pathandattrs:
649 649 binpath = os.path.join(reporootdir, relpath)
650 650 if os.name != 'nt' and not os.access(binpath, os.X_OK):
651 651 parser.error(
652 652 '--local specified, but %r not found or '
653 653 'not executable' % binpath
654 654 )
655 setattr(options, attr, _strpath(binpath))
655 setattr(options, attr, _bytes2sys(binpath))
656 656
657 657 if options.with_hg:
658 options.with_hg = canonpath(_bytespath(options.with_hg))
658 options.with_hg = canonpath(_sys2bytes(options.with_hg))
659 659 if not (
660 660 os.path.isfile(options.with_hg)
661 661 and os.access(options.with_hg, os.X_OK)
662 662 ):
663 663 parser.error('--with-hg must specify an executable hg script')
664 664 if os.path.basename(options.with_hg) not in [b'hg', b'hg.exe']:
665 665 sys.stderr.write('warning: --with-hg should specify an hg script\n')
666 666 sys.stderr.flush()
667 667
668 668 if (options.chg or options.with_chg) and os.name == 'nt':
669 669 parser.error('chg does not work on %s' % os.name)
670 670 if options.with_chg:
671 671 options.chg = False # no installation to temporary location
672 options.with_chg = canonpath(_bytespath(options.with_chg))
672 options.with_chg = canonpath(_sys2bytes(options.with_chg))
673 673 if not (
674 674 os.path.isfile(options.with_chg)
675 675 and os.access(options.with_chg, os.X_OK)
676 676 ):
677 677 parser.error('--with-chg must specify a chg executable')
678 678 if options.chg and options.with_hg:
679 679 # chg shares installation location with hg
680 680 parser.error(
681 681 '--chg does not work when --with-hg is specified '
682 682 '(use --with-chg instead)'
683 683 )
684 684
685 685 if options.color == 'always' and not pygmentspresent:
686 686 sys.stderr.write(
687 687 'warning: --color=always ignored because '
688 688 'pygments is not installed\n'
689 689 )
690 690
691 691 if options.bisect_repo and not options.known_good_rev:
692 692 parser.error("--bisect-repo cannot be used without --known-good-rev")
693 693
694 694 global useipv6
695 695 if options.ipv6:
696 696 useipv6 = checksocketfamily('AF_INET6')
697 697 else:
698 698 # only use IPv6 if IPv4 is unavailable and IPv6 is available
699 699 useipv6 = (not checksocketfamily('AF_INET')) and checksocketfamily(
700 700 'AF_INET6'
701 701 )
702 702
703 703 options.anycoverage = options.cover or options.annotate or options.htmlcov
704 704 if options.anycoverage:
705 705 try:
706 706 import coverage
707 707
708 708 covver = version.StrictVersion(coverage.__version__).version
709 709 if covver < (3, 3):
710 710 parser.error('coverage options require coverage 3.3 or later')
711 711 except ImportError:
712 712 parser.error('coverage options now require the coverage package')
713 713
714 714 if options.anycoverage and options.local:
715 715 # this needs some path mangling somewhere, I guess
716 716 parser.error(
717 717 "sorry, coverage options do not work when --local " "is specified"
718 718 )
719 719
720 720 if options.anycoverage and options.with_hg:
721 721 parser.error(
722 722 "sorry, coverage options do not work when --with-hg " "is specified"
723 723 )
724 724
725 725 global verbose
726 726 if options.verbose:
727 727 verbose = ''
728 728
729 729 if options.tmpdir:
730 730 options.tmpdir = canonpath(options.tmpdir)
731 731
732 732 if options.jobs < 1:
733 733 parser.error('--jobs must be positive')
734 734 if options.interactive and options.debug:
735 735 parser.error("-i/--interactive and -d/--debug are incompatible")
736 736 if options.debug:
737 737 if options.timeout != defaults['timeout']:
738 738 sys.stderr.write('warning: --timeout option ignored with --debug\n')
739 739 if options.slowtimeout != defaults['slowtimeout']:
740 740 sys.stderr.write(
741 741 'warning: --slowtimeout option ignored with --debug\n'
742 742 )
743 743 options.timeout = 0
744 744 options.slowtimeout = 0
745 745
746 746 if options.blacklist:
747 747 options.blacklist = parselistfiles(options.blacklist, 'blacklist')
748 748 if options.whitelist:
749 749 options.whitelisted = parselistfiles(options.whitelist, 'whitelist')
750 750 else:
751 751 options.whitelisted = {}
752 752
753 753 if options.showchannels:
754 754 options.nodiff = True
755 755
756 756 return options
757 757
758 758
759 759 def rename(src, dst):
760 760 """Like os.rename(), trade atomicity and opened files friendliness
761 761 for existing destination support.
762 762 """
763 763 shutil.copy(src, dst)
764 764 os.remove(src)
765 765
766 766
767 767 def makecleanable(path):
768 768 """Try to fix directory permission recursively so that the entire tree
769 769 can be deleted"""
770 770 for dirpath, dirnames, _filenames in os.walk(path, topdown=True):
771 771 for d in dirnames:
772 772 p = os.path.join(dirpath, d)
773 773 try:
774 774 os.chmod(p, os.stat(p).st_mode & 0o777 | 0o700) # chmod u+rwx
775 775 except OSError:
776 776 pass
777 777
778 778
779 779 _unified_diff = difflib.unified_diff
780 780 if PYTHON3:
781 781 import functools
782 782
783 783 _unified_diff = functools.partial(difflib.diff_bytes, difflib.unified_diff)
784 784
785 785
786 786 def getdiff(expected, output, ref, err):
787 787 servefail = False
788 788 lines = []
789 789 for line in _unified_diff(expected, output, ref, err):
790 790 if line.startswith(b'+++') or line.startswith(b'---'):
791 791 line = line.replace(b'\\', b'/')
792 792 if line.endswith(b' \n'):
793 793 line = line[:-2] + b'\n'
794 794 lines.append(line)
795 795 if not servefail and line.startswith(
796 796 b'+ abort: child process failed to start'
797 797 ):
798 798 servefail = True
799 799
800 800 return servefail, lines
801 801
802 802
803 803 verbose = False
804 804
805 805
806 806 def vlog(*msg):
807 807 """Log only when in verbose mode."""
808 808 if verbose is False:
809 809 return
810 810
811 811 return log(*msg)
812 812
813 813
814 814 # Bytes that break XML even in a CDATA block: control characters 0-31
815 815 # sans \t, \n and \r
816 816 CDATA_EVIL = re.compile(br"[\000-\010\013\014\016-\037]")
817 817
818 818 # Match feature conditionalized output lines in the form, capturing the feature
819 819 # list in group 2, and the preceeding line output in group 1:
820 820 #
821 821 # output..output (feature !)\n
822 822 optline = re.compile(br'(.*) \((.+?) !\)\n$')
823 823
824 824
825 825 def cdatasafe(data):
826 826 """Make a string safe to include in a CDATA block.
827 827
828 828 Certain control characters are illegal in a CDATA block, and
829 829 there's no way to include a ]]> in a CDATA either. This function
830 830 replaces illegal bytes with ? and adds a space between the ]] so
831 831 that it won't break the CDATA block.
832 832 """
833 833 return CDATA_EVIL.sub(b'?', data).replace(b']]>', b'] ]>')
834 834
835 835
836 836 def log(*msg):
837 837 """Log something to stdout.
838 838
839 839 Arguments are strings to print.
840 840 """
841 841 with iolock:
842 842 if verbose:
843 843 print(verbose, end=' ')
844 844 for m in msg:
845 845 print(m, end=' ')
846 846 print()
847 847 sys.stdout.flush()
848 848
849 849
850 850 def highlightdiff(line, color):
851 851 if not color:
852 852 return line
853 853 assert pygmentspresent
854 854 return pygments.highlight(
855 855 line.decode('latin1'), difflexer, terminal256formatter
856 856 ).encode('latin1')
857 857
858 858
859 859 def highlightmsg(msg, color):
860 860 if not color:
861 861 return msg
862 862 assert pygmentspresent
863 863 return pygments.highlight(msg, runnerlexer, runnerformatter)
864 864
865 865
866 866 def terminate(proc):
867 867 """Terminate subprocess"""
868 868 vlog('# Terminating process %d' % proc.pid)
869 869 try:
870 870 proc.terminate()
871 871 except OSError:
872 872 pass
873 873
874 874
875 875 def killdaemons(pidfile):
876 876 import killdaemons as killmod
877 877
878 878 return killmod.killdaemons(pidfile, tryhard=False, remove=True, logfn=vlog)
879 879
880 880
881 881 class Test(unittest.TestCase):
882 882 """Encapsulates a single, runnable test.
883 883
884 884 While this class conforms to the unittest.TestCase API, it differs in that
885 885 instances need to be instantiated manually. (Typically, unittest.TestCase
886 886 classes are instantiated automatically by scanning modules.)
887 887 """
888 888
889 889 # Status code reserved for skipped tests (used by hghave).
890 890 SKIPPED_STATUS = 80
891 891
892 892 def __init__(
893 893 self,
894 894 path,
895 895 outputdir,
896 896 tmpdir,
897 897 keeptmpdir=False,
898 898 debug=False,
899 899 first=False,
900 900 timeout=None,
901 901 startport=None,
902 902 extraconfigopts=None,
903 903 shell=None,
904 904 hgcommand=None,
905 905 slowtimeout=None,
906 906 usechg=False,
907 907 useipv6=False,
908 908 ):
909 909 """Create a test from parameters.
910 910
911 911 path is the full path to the file defining the test.
912 912
913 913 tmpdir is the main temporary directory to use for this test.
914 914
915 915 keeptmpdir determines whether to keep the test's temporary directory
916 916 after execution. It defaults to removal (False).
917 917
918 918 debug mode will make the test execute verbosely, with unfiltered
919 919 output.
920 920
921 921 timeout controls the maximum run time of the test. It is ignored when
922 922 debug is True. See slowtimeout for tests with #require slow.
923 923
924 924 slowtimeout overrides timeout if the test has #require slow.
925 925
926 926 startport controls the starting port number to use for this test. Each
927 927 test will reserve 3 port numbers for execution. It is the caller's
928 928 responsibility to allocate a non-overlapping port range to Test
929 929 instances.
930 930
931 931 extraconfigopts is an iterable of extra hgrc config options. Values
932 932 must have the form "key=value" (something understood by hgrc). Values
933 933 of the form "foo.key=value" will result in "[foo] key=value".
934 934
935 935 shell is the shell to execute tests in.
936 936 """
937 937 if timeout is None:
938 938 timeout = defaults['timeout']
939 939 if startport is None:
940 940 startport = defaults['port']
941 941 if slowtimeout is None:
942 942 slowtimeout = defaults['slowtimeout']
943 943 self.path = path
944 944 self.bname = os.path.basename(path)
945 self.name = _strpath(self.bname)
945 self.name = _bytes2sys(self.bname)
946 946 self._testdir = os.path.dirname(path)
947 947 self._outputdir = outputdir
948 948 self._tmpname = os.path.basename(path)
949 949 self.errpath = os.path.join(self._outputdir, b'%s.err' % self.bname)
950 950
951 951 self._threadtmp = tmpdir
952 952 self._keeptmpdir = keeptmpdir
953 953 self._debug = debug
954 954 self._first = first
955 955 self._timeout = timeout
956 956 self._slowtimeout = slowtimeout
957 957 self._startport = startport
958 958 self._extraconfigopts = extraconfigopts or []
959 self._shell = _bytespath(shell)
959 self._shell = _sys2bytes(shell)
960 960 self._hgcommand = hgcommand or b'hg'
961 961 self._usechg = usechg
962 962 self._useipv6 = useipv6
963 963
964 964 self._aborted = False
965 965 self._daemonpids = []
966 966 self._finished = None
967 967 self._ret = None
968 968 self._out = None
969 969 self._skipped = None
970 970 self._testtmp = None
971 971 self._chgsockdir = None
972 972
973 973 self._refout = self.readrefout()
974 974
975 975 def readrefout(self):
976 976 """read reference output"""
977 977 # If we're not in --debug mode and reference output file exists,
978 978 # check test output against it.
979 979 if self._debug:
980 980 return None # to match "out is None"
981 981 elif os.path.exists(self.refpath):
982 982 with open(self.refpath, 'rb') as f:
983 983 return f.read().splitlines(True)
984 984 else:
985 985 return []
986 986
987 987 # needed to get base class __repr__ running
988 988 @property
989 989 def _testMethodName(self):
990 990 return self.name
991 991
992 992 def __str__(self):
993 993 return self.name
994 994
995 995 def shortDescription(self):
996 996 return self.name
997 997
998 998 def setUp(self):
999 999 """Tasks to perform before run()."""
1000 1000 self._finished = False
1001 1001 self._ret = None
1002 1002 self._out = None
1003 1003 self._skipped = None
1004 1004
1005 1005 try:
1006 1006 os.mkdir(self._threadtmp)
1007 1007 except OSError as e:
1008 1008 if e.errno != errno.EEXIST:
1009 1009 raise
1010 1010
1011 1011 name = self._tmpname
1012 1012 self._testtmp = os.path.join(self._threadtmp, name)
1013 1013 os.mkdir(self._testtmp)
1014 1014
1015 1015 # Remove any previous output files.
1016 1016 if os.path.exists(self.errpath):
1017 1017 try:
1018 1018 os.remove(self.errpath)
1019 1019 except OSError as e:
1020 1020 # We might have raced another test to clean up a .err
1021 1021 # file, so ignore ENOENT when removing a previous .err
1022 1022 # file.
1023 1023 if e.errno != errno.ENOENT:
1024 1024 raise
1025 1025
1026 1026 if self._usechg:
1027 1027 self._chgsockdir = os.path.join(
1028 1028 self._threadtmp, b'%s.chgsock' % name
1029 1029 )
1030 1030 os.mkdir(self._chgsockdir)
1031 1031
1032 1032 def run(self, result):
1033 1033 """Run this test and report results against a TestResult instance."""
1034 1034 # This function is extremely similar to unittest.TestCase.run(). Once
1035 1035 # we require Python 2.7 (or at least its version of unittest), this
1036 1036 # function can largely go away.
1037 1037 self._result = result
1038 1038 result.startTest(self)
1039 1039 try:
1040 1040 try:
1041 1041 self.setUp()
1042 1042 except (KeyboardInterrupt, SystemExit):
1043 1043 self._aborted = True
1044 1044 raise
1045 1045 except Exception:
1046 1046 result.addError(self, sys.exc_info())
1047 1047 return
1048 1048
1049 1049 success = False
1050 1050 try:
1051 1051 self.runTest()
1052 1052 except KeyboardInterrupt:
1053 1053 self._aborted = True
1054 1054 raise
1055 1055 except unittest.SkipTest as e:
1056 1056 result.addSkip(self, str(e))
1057 1057 # The base class will have already counted this as a
1058 1058 # test we "ran", but we want to exclude skipped tests
1059 1059 # from those we count towards those run.
1060 1060 result.testsRun -= 1
1061 1061 except self.failureException as e:
1062 1062 # This differs from unittest in that we don't capture
1063 1063 # the stack trace. This is for historical reasons and
1064 1064 # this decision could be revisited in the future,
1065 1065 # especially for PythonTest instances.
1066 1066 if result.addFailure(self, str(e)):
1067 1067 success = True
1068 1068 except Exception:
1069 1069 result.addError(self, sys.exc_info())
1070 1070 else:
1071 1071 success = True
1072 1072
1073 1073 try:
1074 1074 self.tearDown()
1075 1075 except (KeyboardInterrupt, SystemExit):
1076 1076 self._aborted = True
1077 1077 raise
1078 1078 except Exception:
1079 1079 result.addError(self, sys.exc_info())
1080 1080 success = False
1081 1081
1082 1082 if success:
1083 1083 result.addSuccess(self)
1084 1084 finally:
1085 1085 result.stopTest(self, interrupted=self._aborted)
1086 1086
1087 1087 def runTest(self):
1088 1088 """Run this test instance.
1089 1089
1090 1090 This will return a tuple describing the result of the test.
1091 1091 """
1092 1092 env = self._getenv()
1093 1093 self._genrestoreenv(env)
1094 1094 self._daemonpids.append(env['DAEMON_PIDS'])
1095 1095 self._createhgrc(env['HGRCPATH'])
1096 1096
1097 1097 vlog('# Test', self.name)
1098 1098
1099 1099 ret, out = self._run(env)
1100 1100 self._finished = True
1101 1101 self._ret = ret
1102 1102 self._out = out
1103 1103
1104 1104 def describe(ret):
1105 1105 if ret < 0:
1106 1106 return 'killed by signal: %d' % -ret
1107 1107 return 'returned error code %d' % ret
1108 1108
1109 1109 self._skipped = False
1110 1110
1111 1111 if ret == self.SKIPPED_STATUS:
1112 1112 if out is None: # Debug mode, nothing to parse.
1113 1113 missing = ['unknown']
1114 1114 failed = None
1115 1115 else:
1116 1116 missing, failed = TTest.parsehghaveoutput(out)
1117 1117
1118 1118 if not missing:
1119 1119 missing = ['skipped']
1120 1120
1121 1121 if failed:
1122 1122 self.fail('hg have failed checking for %s' % failed[-1])
1123 1123 else:
1124 1124 self._skipped = True
1125 1125 raise unittest.SkipTest(missing[-1])
1126 1126 elif ret == 'timeout':
1127 1127 self.fail('timed out')
1128 1128 elif ret is False:
1129 1129 self.fail('no result code from test')
1130 1130 elif out != self._refout:
1131 1131 # Diff generation may rely on written .err file.
1132 1132 if (
1133 1133 (ret != 0 or out != self._refout)
1134 1134 and not self._skipped
1135 1135 and not self._debug
1136 1136 ):
1137 1137 with open(self.errpath, 'wb') as f:
1138 1138 for line in out:
1139 1139 f.write(line)
1140 1140
1141 1141 # The result object handles diff calculation for us.
1142 1142 with firstlock:
1143 1143 if self._result.addOutputMismatch(self, ret, out, self._refout):
1144 1144 # change was accepted, skip failing
1145 1145 return
1146 1146 if self._first:
1147 1147 global firsterror
1148 1148 firsterror = True
1149 1149
1150 1150 if ret:
1151 1151 msg = 'output changed and ' + describe(ret)
1152 1152 else:
1153 1153 msg = 'output changed'
1154 1154
1155 1155 self.fail(msg)
1156 1156 elif ret:
1157 1157 self.fail(describe(ret))
1158 1158
1159 1159 def tearDown(self):
1160 1160 """Tasks to perform after run()."""
1161 1161 for entry in self._daemonpids:
1162 1162 killdaemons(entry)
1163 1163 self._daemonpids = []
1164 1164
1165 1165 if self._keeptmpdir:
1166 1166 log(
1167 1167 '\nKeeping testtmp dir: %s\nKeeping threadtmp dir: %s'
1168 1168 % (
1169 1169 self._testtmp.decode('utf-8'),
1170 1170 self._threadtmp.decode('utf-8'),
1171 1171 )
1172 1172 )
1173 1173 else:
1174 1174 try:
1175 1175 shutil.rmtree(self._testtmp)
1176 1176 except OSError:
1177 1177 # unreadable directory may be left in $TESTTMP; fix permission
1178 1178 # and try again
1179 1179 makecleanable(self._testtmp)
1180 1180 shutil.rmtree(self._testtmp, True)
1181 1181 shutil.rmtree(self._threadtmp, True)
1182 1182
1183 1183 if self._usechg:
1184 1184 # chgservers will stop automatically after they find the socket
1185 1185 # files are deleted
1186 1186 shutil.rmtree(self._chgsockdir, True)
1187 1187
1188 1188 if (
1189 1189 (self._ret != 0 or self._out != self._refout)
1190 1190 and not self._skipped
1191 1191 and not self._debug
1192 1192 and self._out
1193 1193 ):
1194 1194 with open(self.errpath, 'wb') as f:
1195 1195 for line in self._out:
1196 1196 f.write(line)
1197 1197
1198 1198 vlog("# Ret was:", self._ret, '(%s)' % self.name)
1199 1199
1200 1200 def _run(self, env):
1201 1201 # This should be implemented in child classes to run tests.
1202 1202 raise unittest.SkipTest('unknown test type')
1203 1203
1204 1204 def abort(self):
1205 1205 """Terminate execution of this test."""
1206 1206 self._aborted = True
1207 1207
1208 1208 def _portmap(self, i):
1209 1209 offset = b'' if i == 0 else b'%d' % i
1210 1210 return (br':%d\b' % (self._startport + i), b':$HGPORT%s' % offset)
1211 1211
1212 1212 def _getreplacements(self):
1213 1213 """Obtain a mapping of text replacements to apply to test output.
1214 1214
1215 1215 Test output needs to be normalized so it can be compared to expected
1216 1216 output. This function defines how some of that normalization will
1217 1217 occur.
1218 1218 """
1219 1219 r = [
1220 1220 # This list should be parallel to defineport in _getenv
1221 1221 self._portmap(0),
1222 1222 self._portmap(1),
1223 1223 self._portmap(2),
1224 1224 (br'([^0-9])%s' % re.escape(self._localip()), br'\1$LOCALIP'),
1225 1225 (br'\bHG_TXNID=TXN:[a-f0-9]{40}\b', br'HG_TXNID=TXN:$ID$'),
1226 1226 ]
1227 1227 r.append((self._escapepath(self._testtmp), b'$TESTTMP'))
1228 1228
1229 1229 replacementfile = os.path.join(self._testdir, b'common-pattern.py')
1230 1230
1231 1231 if os.path.exists(replacementfile):
1232 1232 data = {}
1233 1233 with open(replacementfile, mode='rb') as source:
1234 1234 # the intermediate 'compile' step help with debugging
1235 1235 code = compile(source.read(), replacementfile, 'exec')
1236 1236 exec(code, data)
1237 1237 for value in data.get('substitutions', ()):
1238 1238 if len(value) != 2:
1239 1239 msg = 'malformatted substitution in %s: %r'
1240 1240 msg %= (replacementfile, value)
1241 1241 raise ValueError(msg)
1242 1242 r.append(value)
1243 1243 return r
1244 1244
1245 1245 def _escapepath(self, p):
1246 1246 if os.name == 'nt':
1247 1247 return b''.join(
1248 1248 c.isalpha()
1249 1249 and b'[%s%s]' % (c.lower(), c.upper())
1250 1250 or c in b'/\\'
1251 1251 and br'[/\\]'
1252 1252 or c.isdigit()
1253 1253 and c
1254 1254 or b'\\' + c
1255 1255 for c in [p[i : i + 1] for i in range(len(p))]
1256 1256 )
1257 1257 else:
1258 1258 return re.escape(p)
1259 1259
1260 1260 def _localip(self):
1261 1261 if self._useipv6:
1262 1262 return b'::1'
1263 1263 else:
1264 1264 return b'127.0.0.1'
1265 1265
1266 1266 def _genrestoreenv(self, testenv):
1267 1267 """Generate a script that can be used by tests to restore the original
1268 1268 environment."""
1269 1269 # Put the restoreenv script inside self._threadtmp
1270 1270 scriptpath = os.path.join(self._threadtmp, b'restoreenv.sh')
1271 testenv['HGTEST_RESTOREENV'] = _strpath(scriptpath)
1271 testenv['HGTEST_RESTOREENV'] = _bytes2sys(scriptpath)
1272 1272
1273 1273 # Only restore environment variable names that the shell allows
1274 1274 # us to export.
1275 1275 name_regex = re.compile('^[a-zA-Z][a-zA-Z0-9_]*$')
1276 1276
1277 1277 # Do not restore these variables; otherwise tests would fail.
1278 1278 reqnames = {'PYTHON', 'TESTDIR', 'TESTTMP'}
1279 1279
1280 1280 with open(scriptpath, 'w') as envf:
1281 1281 for name, value in origenviron.items():
1282 1282 if not name_regex.match(name):
1283 1283 # Skip environment variables with unusual names not
1284 1284 # allowed by most shells.
1285 1285 continue
1286 1286 if name in reqnames:
1287 1287 continue
1288 1288 envf.write('%s=%s\n' % (name, shellquote(value)))
1289 1289
1290 1290 for name in testenv:
1291 1291 if name in origenviron or name in reqnames:
1292 1292 continue
1293 1293 envf.write('unset %s\n' % (name,))
1294 1294
1295 1295 def _getenv(self):
1296 1296 """Obtain environment variables to use during test execution."""
1297 1297
1298 1298 def defineport(i):
1299 1299 offset = '' if i == 0 else '%s' % i
1300 1300 env["HGPORT%s" % offset] = '%s' % (self._startport + i)
1301 1301
1302 1302 env = os.environ.copy()
1303 1303 env['PYTHONUSERBASE'] = sysconfig.get_config_var('userbase') or ''
1304 1304 env['HGEMITWARNINGS'] = '1'
1305 env['TESTTMP'] = _strpath(self._testtmp)
1305 env['TESTTMP'] = _bytes2sys(self._testtmp)
1306 1306 env['TESTNAME'] = self.name
1307 env['HOME'] = _strpath(self._testtmp)
1307 env['HOME'] = _bytes2sys(self._testtmp)
1308 1308 # This number should match portneeded in _getport
1309 1309 for port in xrange(3):
1310 1310 # This list should be parallel to _portmap in _getreplacements
1311 1311 defineport(port)
1312 env["HGRCPATH"] = _strpath(os.path.join(self._threadtmp, b'.hgrc'))
1313 env["DAEMON_PIDS"] = _strpath(
1312 env["HGRCPATH"] = _bytes2sys(os.path.join(self._threadtmp, b'.hgrc'))
1313 env["DAEMON_PIDS"] = _bytes2sys(
1314 1314 os.path.join(self._threadtmp, b'daemon.pids')
1315 1315 )
1316 1316 env["HGEDITOR"] = (
1317 1317 '"' + sysexecutable + '"' + ' -c "import sys; sys.exit(0)"'
1318 1318 )
1319 1319 env["HGUSER"] = "test"
1320 1320 env["HGENCODING"] = "ascii"
1321 1321 env["HGENCODINGMODE"] = "strict"
1322 1322 env["HGHOSTNAME"] = "test-hostname"
1323 1323 env['HGIPV6'] = str(int(self._useipv6))
1324 1324 # See contrib/catapipe.py for how to use this functionality.
1325 1325 if 'HGTESTCATAPULTSERVERPIPE' not in env:
1326 1326 # If we don't have HGTESTCATAPULTSERVERPIPE explicitly set, pull the
1327 1327 # non-test one in as a default, otherwise set to devnull
1328 1328 env['HGTESTCATAPULTSERVERPIPE'] = env.get(
1329 1329 'HGCATAPULTSERVERPIPE', os.devnull
1330 1330 )
1331 1331
1332 1332 extraextensions = []
1333 1333 for opt in self._extraconfigopts:
1334 1334 section, key = opt.encode('utf-8').split(b'.', 1)
1335 1335 if section != 'extensions':
1336 1336 continue
1337 1337 name = key.split(b'=', 1)[0]
1338 1338 extraextensions.append(name)
1339 1339
1340 1340 if extraextensions:
1341 1341 env['HGTESTEXTRAEXTENSIONS'] = b' '.join(extraextensions)
1342 1342
1343 1343 # LOCALIP could be ::1 or 127.0.0.1. Useful for tests that require raw
1344 1344 # IP addresses.
1345 env['LOCALIP'] = _strpath(self._localip())
1345 env['LOCALIP'] = _bytes2sys(self._localip())
1346 1346
1347 1347 # This has the same effect as Py_LegacyWindowsStdioFlag in exewrapper.c,
1348 1348 # but this is needed for testing python instances like dummyssh,
1349 1349 # dummysmtpd.py, and dumbhttp.py.
1350 1350 if PYTHON3 and os.name == 'nt':
1351 1351 env['PYTHONLEGACYWINDOWSSTDIO'] = '1'
1352 1352
1353 1353 # Modified HOME in test environment can confuse Rust tools. So set
1354 1354 # CARGO_HOME and RUSTUP_HOME automatically if a Rust toolchain is
1355 1355 # present and these variables aren't already defined.
1356 1356 cargo_home_path = os.path.expanduser('~/.cargo')
1357 1357 rustup_home_path = os.path.expanduser('~/.rustup')
1358 1358
1359 1359 if os.path.exists(cargo_home_path) and b'CARGO_HOME' not in osenvironb:
1360 1360 env['CARGO_HOME'] = cargo_home_path
1361 1361 if (
1362 1362 os.path.exists(rustup_home_path)
1363 1363 and b'RUSTUP_HOME' not in osenvironb
1364 1364 ):
1365 1365 env['RUSTUP_HOME'] = rustup_home_path
1366 1366
1367 1367 # Reset some environment variables to well-known values so that
1368 1368 # the tests produce repeatable output.
1369 1369 env['LANG'] = env['LC_ALL'] = env['LANGUAGE'] = 'C'
1370 1370 env['TZ'] = 'GMT'
1371 1371 env["EMAIL"] = "Foo Bar <foo.bar@example.com>"
1372 1372 env['COLUMNS'] = '80'
1373 1373 env['TERM'] = 'xterm'
1374 1374
1375 1375 dropped = [
1376 1376 'CDPATH',
1377 1377 'CHGDEBUG',
1378 1378 'EDITOR',
1379 1379 'GREP_OPTIONS',
1380 1380 'HG',
1381 1381 'HGMERGE',
1382 1382 'HGPLAIN',
1383 1383 'HGPLAINEXCEPT',
1384 1384 'HGPROF',
1385 1385 'http_proxy',
1386 1386 'no_proxy',
1387 1387 'NO_PROXY',
1388 1388 'PAGER',
1389 1389 'VISUAL',
1390 1390 ]
1391 1391
1392 1392 for k in dropped:
1393 1393 if k in env:
1394 1394 del env[k]
1395 1395
1396 1396 # unset env related to hooks
1397 1397 for k in list(env):
1398 1398 if k.startswith('HG_'):
1399 1399 del env[k]
1400 1400
1401 1401 if self._usechg:
1402 1402 env['CHGSOCKNAME'] = os.path.join(self._chgsockdir, b'server')
1403 1403
1404 1404 return env
1405 1405
1406 1406 def _createhgrc(self, path):
1407 1407 """Create an hgrc file for this test."""
1408 1408 with open(path, 'wb') as hgrc:
1409 1409 hgrc.write(b'[ui]\n')
1410 1410 hgrc.write(b'slash = True\n')
1411 1411 hgrc.write(b'interactive = False\n')
1412 1412 hgrc.write(b'merge = internal:merge\n')
1413 1413 hgrc.write(b'mergemarkers = detailed\n')
1414 1414 hgrc.write(b'promptecho = True\n')
1415 1415 hgrc.write(b'[defaults]\n')
1416 1416 hgrc.write(b'[devel]\n')
1417 1417 hgrc.write(b'all-warnings = true\n')
1418 1418 hgrc.write(b'default-date = 0 0\n')
1419 1419 hgrc.write(b'[largefiles]\n')
1420 1420 hgrc.write(
1421 1421 b'usercache = %s\n'
1422 1422 % (os.path.join(self._testtmp, b'.cache/largefiles'))
1423 1423 )
1424 1424 hgrc.write(b'[lfs]\n')
1425 1425 hgrc.write(
1426 1426 b'usercache = %s\n'
1427 1427 % (os.path.join(self._testtmp, b'.cache/lfs'))
1428 1428 )
1429 1429 hgrc.write(b'[web]\n')
1430 1430 hgrc.write(b'address = localhost\n')
1431 1431 hgrc.write(b'ipv6 = %r\n' % self._useipv6)
1432 1432 hgrc.write(b'server-header = testing stub value\n')
1433 1433
1434 1434 for opt in self._extraconfigopts:
1435 1435 section, key = opt.encode('utf-8').split(b'.', 1)
1436 1436 assert b'=' in key, (
1437 1437 'extra config opt %s must ' 'have an = for assignment' % opt
1438 1438 )
1439 1439 hgrc.write(b'[%s]\n%s\n' % (section, key))
1440 1440
1441 1441 def fail(self, msg):
1442 1442 # unittest differentiates between errored and failed.
1443 1443 # Failed is denoted by AssertionError (by default at least).
1444 1444 raise AssertionError(msg)
1445 1445
1446 1446 def _runcommand(self, cmd, env, normalizenewlines=False):
1447 1447 """Run command in a sub-process, capturing the output (stdout and
1448 1448 stderr).
1449 1449
1450 1450 Return a tuple (exitcode, output). output is None in debug mode.
1451 1451 """
1452 1452 if self._debug:
1453 1453 proc = subprocess.Popen(
1454 _strpath(cmd), shell=True, cwd=_strpath(self._testtmp), env=env
1454 _bytes2sys(cmd),
1455 shell=True,
1456 cwd=_bytes2sys(self._testtmp),
1457 env=env,
1455 1458 )
1456 1459 ret = proc.wait()
1457 1460 return (ret, None)
1458 1461
1459 1462 proc = Popen4(cmd, self._testtmp, self._timeout, env)
1460 1463
1461 1464 def cleanup():
1462 1465 terminate(proc)
1463 1466 ret = proc.wait()
1464 1467 if ret == 0:
1465 1468 ret = signal.SIGTERM << 8
1466 1469 killdaemons(env['DAEMON_PIDS'])
1467 1470 return ret
1468 1471
1469 1472 proc.tochild.close()
1470 1473
1471 1474 try:
1472 1475 output = proc.fromchild.read()
1473 1476 except KeyboardInterrupt:
1474 1477 vlog('# Handling keyboard interrupt')
1475 1478 cleanup()
1476 1479 raise
1477 1480
1478 1481 ret = proc.wait()
1479 1482 if wifexited(ret):
1480 1483 ret = os.WEXITSTATUS(ret)
1481 1484
1482 1485 if proc.timeout:
1483 1486 ret = 'timeout'
1484 1487
1485 1488 if ret:
1486 1489 killdaemons(env['DAEMON_PIDS'])
1487 1490
1488 1491 for s, r in self._getreplacements():
1489 1492 output = re.sub(s, r, output)
1490 1493
1491 1494 if normalizenewlines:
1492 1495 output = output.replace(b'\r\n', b'\n')
1493 1496
1494 1497 return ret, output.splitlines(True)
1495 1498
1496 1499
1497 1500 class PythonTest(Test):
1498 1501 """A Python-based test."""
1499 1502
1500 1503 @property
1501 1504 def refpath(self):
1502 1505 return os.path.join(self._testdir, b'%s.out' % self.bname)
1503 1506
1504 1507 def _run(self, env):
1505 1508 # Quote the python(3) executable for Windows
1506 1509 cmd = b'"%s" "%s"' % (PYTHON, self.path)
1507 1510 vlog("# Running", cmd.decode("utf-8"))
1508 1511 normalizenewlines = os.name == 'nt'
1509 1512 result = self._runcommand(cmd, env, normalizenewlines=normalizenewlines)
1510 1513 if self._aborted:
1511 1514 raise KeyboardInterrupt()
1512 1515
1513 1516 return result
1514 1517
1515 1518
1516 1519 # Some glob patterns apply only in some circumstances, so the script
1517 1520 # might want to remove (glob) annotations that otherwise should be
1518 1521 # retained.
1519 1522 checkcodeglobpats = [
1520 1523 # On Windows it looks like \ doesn't require a (glob), but we know
1521 1524 # better.
1522 1525 re.compile(br'^pushing to \$TESTTMP/.*[^)]$'),
1523 1526 re.compile(br'^moving \S+/.*[^)]$'),
1524 1527 re.compile(br'^pulling from \$TESTTMP/.*[^)]$'),
1525 1528 # Not all platforms have 127.0.0.1 as loopback (though most do),
1526 1529 # so we always glob that too.
1527 1530 re.compile(br'.*\$LOCALIP.*$'),
1528 1531 ]
1529 1532
1530 1533 bchr = chr
1531 1534 if PYTHON3:
1532 1535 bchr = lambda x: bytes([x])
1533 1536
1534 1537 WARN_UNDEFINED = 1
1535 1538 WARN_YES = 2
1536 1539 WARN_NO = 3
1537 1540
1538 1541 MARK_OPTIONAL = b" (?)\n"
1539 1542
1540 1543
1541 1544 def isoptional(line):
1542 1545 return line.endswith(MARK_OPTIONAL)
1543 1546
1544 1547
1545 1548 class TTest(Test):
1546 1549 """A "t test" is a test backed by a .t file."""
1547 1550
1548 1551 SKIPPED_PREFIX = b'skipped: '
1549 1552 FAILED_PREFIX = b'hghave check failed: '
1550 1553 NEEDESCAPE = re.compile(br'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
1551 1554
1552 1555 ESCAPESUB = re.compile(br'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
1553 1556 ESCAPEMAP = dict((bchr(i), br'\x%02x' % i) for i in range(256))
1554 1557 ESCAPEMAP.update({b'\\': b'\\\\', b'\r': br'\r'})
1555 1558
1556 1559 def __init__(self, path, *args, **kwds):
1557 1560 # accept an extra "case" parameter
1558 1561 case = kwds.pop('case', [])
1559 1562 self._case = case
1560 1563 self._allcases = {x for y in parsettestcases(path) for x in y}
1561 1564 super(TTest, self).__init__(path, *args, **kwds)
1562 1565 if case:
1563 1566 casepath = b'#'.join(case)
1564 self.name = '%s#%s' % (self.name, _strpath(casepath))
1567 self.name = '%s#%s' % (self.name, _bytes2sys(casepath))
1565 1568 self.errpath = b'%s#%s.err' % (self.errpath[:-4], casepath)
1566 1569 self._tmpname += b'-%s' % casepath
1567 1570 self._have = {}
1568 1571
1569 1572 @property
1570 1573 def refpath(self):
1571 1574 return os.path.join(self._testdir, self.bname)
1572 1575
1573 1576 def _run(self, env):
1574 1577 with open(self.path, 'rb') as f:
1575 1578 lines = f.readlines()
1576 1579
1577 1580 # .t file is both reference output and the test input, keep reference
1578 1581 # output updated with the the test input. This avoids some race
1579 1582 # conditions where the reference output does not match the actual test.
1580 1583 if self._refout is not None:
1581 1584 self._refout = lines
1582 1585
1583 1586 salt, script, after, expected = self._parsetest(lines)
1584 1587
1585 1588 # Write out the generated script.
1586 1589 fname = b'%s.sh' % self._testtmp
1587 1590 with open(fname, 'wb') as f:
1588 1591 for l in script:
1589 1592 f.write(l)
1590 1593
1591 1594 cmd = b'%s "%s"' % (self._shell, fname)
1592 1595 vlog("# Running", cmd.decode("utf-8"))
1593 1596
1594 1597 exitcode, output = self._runcommand(cmd, env)
1595 1598
1596 1599 if self._aborted:
1597 1600 raise KeyboardInterrupt()
1598 1601
1599 1602 # Do not merge output if skipped. Return hghave message instead.
1600 1603 # Similarly, with --debug, output is None.
1601 1604 if exitcode == self.SKIPPED_STATUS or output is None:
1602 1605 return exitcode, output
1603 1606
1604 1607 return self._processoutput(exitcode, output, salt, after, expected)
1605 1608
1606 1609 def _hghave(self, reqs):
1607 1610 allreqs = b' '.join(reqs)
1608 1611
1609 1612 self._detectslow(reqs)
1610 1613
1611 1614 if allreqs in self._have:
1612 1615 return self._have.get(allreqs)
1613 1616
1614 1617 # TODO do something smarter when all other uses of hghave are gone.
1615 runtestdir = os.path.abspath(os.path.dirname(_bytespath(__file__)))
1618 runtestdir = os.path.abspath(os.path.dirname(_sys2bytes(__file__)))
1616 1619 tdir = runtestdir.replace(b'\\', b'/')
1617 1620 proc = Popen4(
1618 1621 b'%s -c "%s/hghave %s"' % (self._shell, tdir, allreqs),
1619 1622 self._testtmp,
1620 1623 0,
1621 1624 self._getenv(),
1622 1625 )
1623 1626 stdout, stderr = proc.communicate()
1624 1627 ret = proc.wait()
1625 1628 if wifexited(ret):
1626 1629 ret = os.WEXITSTATUS(ret)
1627 1630 if ret == 2:
1628 1631 print(stdout.decode('utf-8'))
1629 1632 sys.exit(1)
1630 1633
1631 1634 if ret != 0:
1632 1635 self._have[allreqs] = (False, stdout)
1633 1636 return False, stdout
1634 1637
1635 1638 self._have[allreqs] = (True, None)
1636 1639 return True, None
1637 1640
1638 1641 def _detectslow(self, reqs):
1639 1642 """update the timeout of slow test when appropriate"""
1640 1643 if b'slow' in reqs:
1641 1644 self._timeout = self._slowtimeout
1642 1645
1643 1646 def _iftest(self, args):
1644 1647 # implements "#if"
1645 1648 reqs = []
1646 1649 for arg in args:
1647 1650 if arg.startswith(b'no-') and arg[3:] in self._allcases:
1648 1651 if arg[3:] in self._case:
1649 1652 return False
1650 1653 elif arg in self._allcases:
1651 1654 if arg not in self._case:
1652 1655 return False
1653 1656 else:
1654 1657 reqs.append(arg)
1655 1658 self._detectslow(reqs)
1656 1659 return self._hghave(reqs)[0]
1657 1660
1658 1661 def _parsetest(self, lines):
1659 1662 # We generate a shell script which outputs unique markers to line
1660 1663 # up script results with our source. These markers include input
1661 1664 # line number and the last return code.
1662 1665 salt = b"SALT%d" % time.time()
1663 1666
1664 1667 def addsalt(line, inpython):
1665 1668 if inpython:
1666 1669 script.append(b'%s %d 0\n' % (salt, line))
1667 1670 else:
1668 1671 script.append(b'echo %s %d $?\n' % (salt, line))
1669 1672
1670 1673 activetrace = []
1671 1674 session = str(uuid.uuid4())
1672 1675 if PYTHON3:
1673 1676 session = session.encode('ascii')
1674 1677 hgcatapult = os.getenv('HGTESTCATAPULTSERVERPIPE') or os.getenv(
1675 1678 'HGCATAPULTSERVERPIPE'
1676 1679 )
1677 1680
1678 1681 def toggletrace(cmd=None):
1679 1682 if not hgcatapult or hgcatapult == os.devnull:
1680 1683 return
1681 1684
1682 1685 if activetrace:
1683 1686 script.append(
1684 1687 b'echo END %s %s >> "$HGTESTCATAPULTSERVERPIPE"\n'
1685 1688 % (session, activetrace[0])
1686 1689 )
1687 1690 if cmd is None:
1688 1691 return
1689 1692
1690 1693 if isinstance(cmd, str):
1691 1694 quoted = shellquote(cmd.strip())
1692 1695 else:
1693 1696 quoted = shellquote(cmd.strip().decode('utf8')).encode('utf8')
1694 1697 quoted = quoted.replace(b'\\', b'\\\\')
1695 1698 script.append(
1696 1699 b'echo START %s %s >> "$HGTESTCATAPULTSERVERPIPE"\n'
1697 1700 % (session, quoted)
1698 1701 )
1699 1702 activetrace[0:] = [quoted]
1700 1703
1701 1704 script = []
1702 1705
1703 1706 # After we run the shell script, we re-unify the script output
1704 1707 # with non-active parts of the source, with synchronization by our
1705 1708 # SALT line number markers. The after table contains the non-active
1706 1709 # components, ordered by line number.
1707 1710 after = {}
1708 1711
1709 1712 # Expected shell script output.
1710 1713 expected = {}
1711 1714
1712 1715 pos = prepos = -1
1713 1716
1714 1717 # True or False when in a true or false conditional section
1715 1718 skipping = None
1716 1719
1717 1720 # We keep track of whether or not we're in a Python block so we
1718 1721 # can generate the surrounding doctest magic.
1719 1722 inpython = False
1720 1723
1721 1724 if self._debug:
1722 1725 script.append(b'set -x\n')
1723 1726 if self._hgcommand != b'hg':
1724 1727 script.append(b'alias hg="%s"\n' % self._hgcommand)
1725 1728 if os.getenv('MSYSTEM'):
1726 1729 script.append(b'alias pwd="pwd -W"\n')
1727 1730
1728 1731 if hgcatapult and hgcatapult != os.devnull:
1729 1732 if PYTHON3:
1730 1733 hgcatapult = hgcatapult.encode('utf8')
1731 1734 cataname = self.name.encode('utf8')
1732 1735 else:
1733 1736 cataname = self.name
1734 1737
1735 1738 # Kludge: use a while loop to keep the pipe from getting
1736 1739 # closed by our echo commands. The still-running file gets
1737 1740 # reaped at the end of the script, which causes the while
1738 1741 # loop to exit and closes the pipe. Sigh.
1739 1742 script.append(
1740 1743 b'rtendtracing() {\n'
1741 1744 b' echo END %(session)s %(name)s >> %(catapult)s\n'
1742 1745 b' rm -f "$TESTTMP/.still-running"\n'
1743 1746 b'}\n'
1744 1747 b'trap "rtendtracing" 0\n'
1745 1748 b'touch "$TESTTMP/.still-running"\n'
1746 1749 b'while [ -f "$TESTTMP/.still-running" ]; do sleep 1; done '
1747 1750 b'> %(catapult)s &\n'
1748 1751 b'HGCATAPULTSESSION=%(session)s ; export HGCATAPULTSESSION\n'
1749 1752 b'echo START %(session)s %(name)s >> %(catapult)s\n'
1750 1753 % {
1751 1754 b'name': cataname,
1752 1755 b'session': session,
1753 1756 b'catapult': hgcatapult,
1754 1757 }
1755 1758 )
1756 1759
1757 1760 if self._case:
1758 1761 casestr = b'#'.join(self._case)
1759 1762 if isinstance(self._case, str):
1760 1763 quoted = shellquote(casestr)
1761 1764 else:
1762 1765 quoted = shellquote(casestr.decode('utf8')).encode('utf8')
1763 1766 script.append(b'TESTCASE=%s\n' % quoted)
1764 1767 script.append(b'export TESTCASE\n')
1765 1768
1766 1769 n = 0
1767 1770 for n, l in enumerate(lines):
1768 1771 if not l.endswith(b'\n'):
1769 1772 l += b'\n'
1770 1773 if l.startswith(b'#require'):
1771 1774 lsplit = l.split()
1772 1775 if len(lsplit) < 2 or lsplit[0] != b'#require':
1773 1776 after.setdefault(pos, []).append(
1774 1777 b' !!! invalid #require\n'
1775 1778 )
1776 1779 if not skipping:
1777 1780 haveresult, message = self._hghave(lsplit[1:])
1778 1781 if not haveresult:
1779 1782 script = [b'echo "%s"\nexit 80\n' % message]
1780 1783 break
1781 1784 after.setdefault(pos, []).append(l)
1782 1785 elif l.startswith(b'#if'):
1783 1786 lsplit = l.split()
1784 1787 if len(lsplit) < 2 or lsplit[0] != b'#if':
1785 1788 after.setdefault(pos, []).append(b' !!! invalid #if\n')
1786 1789 if skipping is not None:
1787 1790 after.setdefault(pos, []).append(b' !!! nested #if\n')
1788 1791 skipping = not self._iftest(lsplit[1:])
1789 1792 after.setdefault(pos, []).append(l)
1790 1793 elif l.startswith(b'#else'):
1791 1794 if skipping is None:
1792 1795 after.setdefault(pos, []).append(b' !!! missing #if\n')
1793 1796 skipping = not skipping
1794 1797 after.setdefault(pos, []).append(l)
1795 1798 elif l.startswith(b'#endif'):
1796 1799 if skipping is None:
1797 1800 after.setdefault(pos, []).append(b' !!! missing #if\n')
1798 1801 skipping = None
1799 1802 after.setdefault(pos, []).append(l)
1800 1803 elif skipping:
1801 1804 after.setdefault(pos, []).append(l)
1802 1805 elif l.startswith(b' >>> '): # python inlines
1803 1806 after.setdefault(pos, []).append(l)
1804 1807 prepos = pos
1805 1808 pos = n
1806 1809 if not inpython:
1807 1810 # We've just entered a Python block. Add the header.
1808 1811 inpython = True
1809 1812 addsalt(prepos, False) # Make sure we report the exit code.
1810 1813 script.append(b'"%s" -m heredoctest <<EOF\n' % PYTHON)
1811 1814 addsalt(n, True)
1812 1815 script.append(l[2:])
1813 1816 elif l.startswith(b' ... '): # python inlines
1814 1817 after.setdefault(prepos, []).append(l)
1815 1818 script.append(l[2:])
1816 1819 elif l.startswith(b' $ '): # commands
1817 1820 if inpython:
1818 1821 script.append(b'EOF\n')
1819 1822 inpython = False
1820 1823 after.setdefault(pos, []).append(l)
1821 1824 prepos = pos
1822 1825 pos = n
1823 1826 addsalt(n, False)
1824 1827 rawcmd = l[4:]
1825 1828 cmd = rawcmd.split()
1826 1829 toggletrace(rawcmd)
1827 1830 if len(cmd) == 2 and cmd[0] == b'cd':
1828 1831 rawcmd = b'cd %s || exit 1\n' % cmd[1]
1829 1832 script.append(rawcmd)
1830 1833 elif l.startswith(b' > '): # continuations
1831 1834 after.setdefault(prepos, []).append(l)
1832 1835 script.append(l[4:])
1833 1836 elif l.startswith(b' '): # results
1834 1837 # Queue up a list of expected results.
1835 1838 expected.setdefault(pos, []).append(l[2:])
1836 1839 else:
1837 1840 if inpython:
1838 1841 script.append(b'EOF\n')
1839 1842 inpython = False
1840 1843 # Non-command/result. Queue up for merged output.
1841 1844 after.setdefault(pos, []).append(l)
1842 1845
1843 1846 if inpython:
1844 1847 script.append(b'EOF\n')
1845 1848 if skipping is not None:
1846 1849 after.setdefault(pos, []).append(b' !!! missing #endif\n')
1847 1850 addsalt(n + 1, False)
1848 1851 # Need to end any current per-command trace
1849 1852 if activetrace:
1850 1853 toggletrace()
1851 1854 return salt, script, after, expected
1852 1855
1853 1856 def _processoutput(self, exitcode, output, salt, after, expected):
1854 1857 # Merge the script output back into a unified test.
1855 1858 warnonly = WARN_UNDEFINED # 1: not yet; 2: yes; 3: for sure not
1856 1859 if exitcode != 0:
1857 1860 warnonly = WARN_NO
1858 1861
1859 1862 pos = -1
1860 1863 postout = []
1861 1864 for out_rawline in output:
1862 1865 out_line, cmd_line = out_rawline, None
1863 1866 if salt in out_rawline:
1864 1867 out_line, cmd_line = out_rawline.split(salt, 1)
1865 1868
1866 1869 pos, postout, warnonly = self._process_out_line(
1867 1870 out_line, pos, postout, expected, warnonly
1868 1871 )
1869 1872 pos, postout = self._process_cmd_line(cmd_line, pos, postout, after)
1870 1873
1871 1874 if pos in after:
1872 1875 postout += after.pop(pos)
1873 1876
1874 1877 if warnonly == WARN_YES:
1875 1878 exitcode = False # Set exitcode to warned.
1876 1879
1877 1880 return exitcode, postout
1878 1881
1879 1882 def _process_out_line(self, out_line, pos, postout, expected, warnonly):
1880 1883 while out_line:
1881 1884 if not out_line.endswith(b'\n'):
1882 1885 out_line += b' (no-eol)\n'
1883 1886
1884 1887 # Find the expected output at the current position.
1885 1888 els = [None]
1886 1889 if expected.get(pos, None):
1887 1890 els = expected[pos]
1888 1891
1889 1892 optional = []
1890 1893 for i, el in enumerate(els):
1891 1894 r = False
1892 1895 if el:
1893 1896 r, exact = self.linematch(el, out_line)
1894 1897 if isinstance(r, str):
1895 1898 if r == '-glob':
1896 1899 out_line = ''.join(el.rsplit(' (glob)', 1))
1897 1900 r = '' # Warn only this line.
1898 1901 elif r == "retry":
1899 1902 postout.append(b' ' + el)
1900 1903 else:
1901 1904 log('\ninfo, unknown linematch result: %r\n' % r)
1902 1905 r = False
1903 1906 if r:
1904 1907 els.pop(i)
1905 1908 break
1906 1909 if el:
1907 1910 if isoptional(el):
1908 1911 optional.append(i)
1909 1912 else:
1910 1913 m = optline.match(el)
1911 1914 if m:
1912 1915 conditions = [c for c in m.group(2).split(b' ')]
1913 1916
1914 1917 if not self._iftest(conditions):
1915 1918 optional.append(i)
1916 1919 if exact:
1917 1920 # Don't allow line to be matches against a later
1918 1921 # line in the output
1919 1922 els.pop(i)
1920 1923 break
1921 1924
1922 1925 if r:
1923 1926 if r == "retry":
1924 1927 continue
1925 1928 # clean up any optional leftovers
1926 1929 for i in optional:
1927 1930 postout.append(b' ' + els[i])
1928 1931 for i in reversed(optional):
1929 1932 del els[i]
1930 1933 postout.append(b' ' + el)
1931 1934 else:
1932 1935 if self.NEEDESCAPE(out_line):
1933 1936 out_line = TTest._stringescape(
1934 1937 b'%s (esc)\n' % out_line.rstrip(b'\n')
1935 1938 )
1936 1939 postout.append(b' ' + out_line) # Let diff deal with it.
1937 1940 if r != '': # If line failed.
1938 1941 warnonly = WARN_NO
1939 1942 elif warnonly == WARN_UNDEFINED:
1940 1943 warnonly = WARN_YES
1941 1944 break
1942 1945 else:
1943 1946 # clean up any optional leftovers
1944 1947 while expected.get(pos, None):
1945 1948 el = expected[pos].pop(0)
1946 1949 if el:
1947 1950 if not isoptional(el):
1948 1951 m = optline.match(el)
1949 1952 if m:
1950 1953 conditions = [c for c in m.group(2).split(b' ')]
1951 1954
1952 1955 if self._iftest(conditions):
1953 1956 # Don't append as optional line
1954 1957 continue
1955 1958 else:
1956 1959 continue
1957 1960 postout.append(b' ' + el)
1958 1961 return pos, postout, warnonly
1959 1962
1960 1963 def _process_cmd_line(self, cmd_line, pos, postout, after):
1961 1964 """process a "command" part of a line from unified test output"""
1962 1965 if cmd_line:
1963 1966 # Add on last return code.
1964 1967 ret = int(cmd_line.split()[1])
1965 1968 if ret != 0:
1966 1969 postout.append(b' [%d]\n' % ret)
1967 1970 if pos in after:
1968 1971 # Merge in non-active test bits.
1969 1972 postout += after.pop(pos)
1970 1973 pos = int(cmd_line.split()[0])
1971 1974 return pos, postout
1972 1975
1973 1976 @staticmethod
1974 1977 def rematch(el, l):
1975 1978 try:
1976 1979 # parse any flags at the beginning of the regex. Only 'i' is
1977 1980 # supported right now, but this should be easy to extend.
1978 1981 flags, el = re.match(br'^(\(\?i\))?(.*)', el).groups()[0:2]
1979 1982 flags = flags or b''
1980 1983 el = flags + b'(?:' + el + b')'
1981 1984 # use \Z to ensure that the regex matches to the end of the string
1982 1985 if os.name == 'nt':
1983 1986 return re.match(el + br'\r?\n\Z', l)
1984 1987 return re.match(el + br'\n\Z', l)
1985 1988 except re.error:
1986 1989 # el is an invalid regex
1987 1990 return False
1988 1991
1989 1992 @staticmethod
1990 1993 def globmatch(el, l):
1991 1994 # The only supported special characters are * and ? plus / which also
1992 1995 # matches \ on windows. Escaping of these characters is supported.
1993 1996 if el + b'\n' == l:
1994 1997 if os.altsep:
1995 1998 # matching on "/" is not needed for this line
1996 1999 for pat in checkcodeglobpats:
1997 2000 if pat.match(el):
1998 2001 return True
1999 2002 return b'-glob'
2000 2003 return True
2001 2004 el = el.replace(b'$LOCALIP', b'*')
2002 2005 i, n = 0, len(el)
2003 2006 res = b''
2004 2007 while i < n:
2005 2008 c = el[i : i + 1]
2006 2009 i += 1
2007 2010 if c == b'\\' and i < n and el[i : i + 1] in b'*?\\/':
2008 2011 res += el[i - 1 : i + 1]
2009 2012 i += 1
2010 2013 elif c == b'*':
2011 2014 res += b'.*'
2012 2015 elif c == b'?':
2013 2016 res += b'.'
2014 2017 elif c == b'/' and os.altsep:
2015 2018 res += b'[/\\\\]'
2016 2019 else:
2017 2020 res += re.escape(c)
2018 2021 return TTest.rematch(res, l)
2019 2022
2020 2023 def linematch(self, el, l):
2021 2024 if el == l: # perfect match (fast)
2022 2025 return True, True
2023 2026 retry = False
2024 2027 if isoptional(el):
2025 2028 retry = "retry"
2026 2029 el = el[: -len(MARK_OPTIONAL)] + b"\n"
2027 2030 else:
2028 2031 m = optline.match(el)
2029 2032 if m:
2030 2033 conditions = [c for c in m.group(2).split(b' ')]
2031 2034
2032 2035 el = m.group(1) + b"\n"
2033 2036 if not self._iftest(conditions):
2034 2037 # listed feature missing, should not match
2035 2038 return "retry", False
2036 2039
2037 2040 if el.endswith(b" (esc)\n"):
2038 2041 if PYTHON3:
2039 2042 el = el[:-7].decode('unicode_escape') + '\n'
2040 2043 el = el.encode('utf-8')
2041 2044 else:
2042 2045 el = el[:-7].decode('string-escape') + '\n'
2043 2046 if el == l or os.name == 'nt' and el[:-1] + b'\r\n' == l:
2044 2047 return True, True
2045 2048 if el.endswith(b" (re)\n"):
2046 2049 return (TTest.rematch(el[:-6], l) or retry), False
2047 2050 if el.endswith(b" (glob)\n"):
2048 2051 # ignore '(glob)' added to l by 'replacements'
2049 2052 if l.endswith(b" (glob)\n"):
2050 2053 l = l[:-8] + b"\n"
2051 2054 return (TTest.globmatch(el[:-8], l) or retry), False
2052 2055 if os.altsep:
2053 2056 _l = l.replace(b'\\', b'/')
2054 2057 if el == _l or os.name == 'nt' and el[:-1] + b'\r\n' == _l:
2055 2058 return True, True
2056 2059 return retry, True
2057 2060
2058 2061 @staticmethod
2059 2062 def parsehghaveoutput(lines):
2060 2063 '''Parse hghave log lines.
2061 2064
2062 2065 Return tuple of lists (missing, failed):
2063 2066 * the missing/unknown features
2064 2067 * the features for which existence check failed'''
2065 2068 missing = []
2066 2069 failed = []
2067 2070 for line in lines:
2068 2071 if line.startswith(TTest.SKIPPED_PREFIX):
2069 2072 line = line.splitlines()[0]
2070 2073 missing.append(
2071 2074 line[len(TTest.SKIPPED_PREFIX) :].decode('utf-8')
2072 2075 )
2073 2076 elif line.startswith(TTest.FAILED_PREFIX):
2074 2077 line = line.splitlines()[0]
2075 2078 failed.append(line[len(TTest.FAILED_PREFIX) :].decode('utf-8'))
2076 2079
2077 2080 return missing, failed
2078 2081
2079 2082 @staticmethod
2080 2083 def _escapef(m):
2081 2084 return TTest.ESCAPEMAP[m.group(0)]
2082 2085
2083 2086 @staticmethod
2084 2087 def _stringescape(s):
2085 2088 return TTest.ESCAPESUB(TTest._escapef, s)
2086 2089
2087 2090
2088 2091 iolock = threading.RLock()
2089 2092 firstlock = threading.RLock()
2090 2093 firsterror = False
2091 2094
2092 2095
2093 2096 class TestResult(unittest._TextTestResult):
2094 2097 """Holds results when executing via unittest."""
2095 2098
2096 2099 # Don't worry too much about accessing the non-public _TextTestResult.
2097 2100 # It is relatively common in Python testing tools.
2098 2101 def __init__(self, options, *args, **kwargs):
2099 2102 super(TestResult, self).__init__(*args, **kwargs)
2100 2103
2101 2104 self._options = options
2102 2105
2103 2106 # unittest.TestResult didn't have skipped until 2.7. We need to
2104 2107 # polyfill it.
2105 2108 self.skipped = []
2106 2109
2107 2110 # We have a custom "ignored" result that isn't present in any Python
2108 2111 # unittest implementation. It is very similar to skipped. It may make
2109 2112 # sense to map it into skip some day.
2110 2113 self.ignored = []
2111 2114
2112 2115 self.times = []
2113 2116 self._firststarttime = None
2114 2117 # Data stored for the benefit of generating xunit reports.
2115 2118 self.successes = []
2116 2119 self.faildata = {}
2117 2120
2118 2121 if options.color == 'auto':
2119 2122 self.color = pygmentspresent and self.stream.isatty()
2120 2123 elif options.color == 'never':
2121 2124 self.color = False
2122 2125 else: # 'always', for testing purposes
2123 2126 self.color = pygmentspresent
2124 2127
2125 2128 def onStart(self, test):
2126 2129 """ Can be overriden by custom TestResult
2127 2130 """
2128 2131
2129 2132 def onEnd(self):
2130 2133 """ Can be overriden by custom TestResult
2131 2134 """
2132 2135
2133 2136 def addFailure(self, test, reason):
2134 2137 self.failures.append((test, reason))
2135 2138
2136 2139 if self._options.first:
2137 2140 self.stop()
2138 2141 else:
2139 2142 with iolock:
2140 2143 if reason == "timed out":
2141 2144 self.stream.write('t')
2142 2145 else:
2143 2146 if not self._options.nodiff:
2144 2147 self.stream.write('\n')
2145 2148 # Exclude the '\n' from highlighting to lex correctly
2146 2149 formatted = 'ERROR: %s output changed\n' % test
2147 2150 self.stream.write(highlightmsg(formatted, self.color))
2148 2151 self.stream.write('!')
2149 2152
2150 2153 self.stream.flush()
2151 2154
2152 2155 def addSuccess(self, test):
2153 2156 with iolock:
2154 2157 super(TestResult, self).addSuccess(test)
2155 2158 self.successes.append(test)
2156 2159
2157 2160 def addError(self, test, err):
2158 2161 super(TestResult, self).addError(test, err)
2159 2162 if self._options.first:
2160 2163 self.stop()
2161 2164
2162 2165 # Polyfill.
2163 2166 def addSkip(self, test, reason):
2164 2167 self.skipped.append((test, reason))
2165 2168 with iolock:
2166 2169 if self.showAll:
2167 2170 self.stream.writeln('skipped %s' % reason)
2168 2171 else:
2169 2172 self.stream.write('s')
2170 2173 self.stream.flush()
2171 2174
2172 2175 def addIgnore(self, test, reason):
2173 2176 self.ignored.append((test, reason))
2174 2177 with iolock:
2175 2178 if self.showAll:
2176 2179 self.stream.writeln('ignored %s' % reason)
2177 2180 else:
2178 2181 if reason not in ('not retesting', "doesn't match keyword"):
2179 2182 self.stream.write('i')
2180 2183 else:
2181 2184 self.testsRun += 1
2182 2185 self.stream.flush()
2183 2186
2184 2187 def addOutputMismatch(self, test, ret, got, expected):
2185 2188 """Record a mismatch in test output for a particular test."""
2186 2189 if self.shouldStop or firsterror:
2187 2190 # don't print, some other test case already failed and
2188 2191 # printed, we're just stale and probably failed due to our
2189 2192 # temp dir getting cleaned up.
2190 2193 return
2191 2194
2192 2195 accepted = False
2193 2196 lines = []
2194 2197
2195 2198 with iolock:
2196 2199 if self._options.nodiff:
2197 2200 pass
2198 2201 elif self._options.view:
2199 2202 v = self._options.view
2200 2203 subprocess.call(
2201 2204 r'"%s" "%s" "%s"'
2202 % (v, _strpath(test.refpath), _strpath(test.errpath)),
2205 % (v, _bytes2sys(test.refpath), _bytes2sys(test.errpath)),
2203 2206 shell=True,
2204 2207 )
2205 2208 else:
2206 2209 servefail, lines = getdiff(
2207 2210 expected, got, test.refpath, test.errpath
2208 2211 )
2209 2212 self.stream.write('\n')
2210 2213 for line in lines:
2211 2214 line = highlightdiff(line, self.color)
2212 2215 if PYTHON3:
2213 2216 self.stream.flush()
2214 2217 self.stream.buffer.write(line)
2215 2218 self.stream.buffer.flush()
2216 2219 else:
2217 2220 self.stream.write(line)
2218 2221 self.stream.flush()
2219 2222
2220 2223 if servefail:
2221 2224 raise test.failureException(
2222 2225 'server failed to start (HGPORT=%s)' % test._startport
2223 2226 )
2224 2227
2225 2228 # handle interactive prompt without releasing iolock
2226 2229 if self._options.interactive:
2227 2230 if test.readrefout() != expected:
2228 2231 self.stream.write(
2229 2232 'Reference output has changed (run again to prompt '
2230 2233 'changes)'
2231 2234 )
2232 2235 else:
2233 2236 self.stream.write('Accept this change? [n] ')
2234 2237 self.stream.flush()
2235 2238 answer = sys.stdin.readline().strip()
2236 2239 if answer.lower() in ('y', 'yes'):
2237 2240 if test.path.endswith(b'.t'):
2238 2241 rename(test.errpath, test.path)
2239 2242 else:
2240 2243 rename(test.errpath, '%s.out' % test.path)
2241 2244 accepted = True
2242 2245 if not accepted:
2243 2246 self.faildata[test.name] = b''.join(lines)
2244 2247
2245 2248 return accepted
2246 2249
2247 2250 def startTest(self, test):
2248 2251 super(TestResult, self).startTest(test)
2249 2252
2250 2253 # os.times module computes the user time and system time spent by
2251 2254 # child's processes along with real elapsed time taken by a process.
2252 2255 # This module has one limitation. It can only work for Linux user
2253 2256 # and not for Windows. Hence why we fall back to another function
2254 2257 # for wall time calculations.
2255 2258 test.started_times = os.times()
2256 2259 # TODO use a monotonic clock once support for Python 2.7 is dropped.
2257 2260 test.started_time = time.time()
2258 2261 if self._firststarttime is None: # thread racy but irrelevant
2259 2262 self._firststarttime = test.started_time
2260 2263
2261 2264 def stopTest(self, test, interrupted=False):
2262 2265 super(TestResult, self).stopTest(test)
2263 2266
2264 2267 test.stopped_times = os.times()
2265 2268 stopped_time = time.time()
2266 2269
2267 2270 starttime = test.started_times
2268 2271 endtime = test.stopped_times
2269 2272 origin = self._firststarttime
2270 2273 self.times.append(
2271 2274 (
2272 2275 test.name,
2273 2276 endtime[2] - starttime[2], # user space CPU time
2274 2277 endtime[3] - starttime[3], # sys space CPU time
2275 2278 stopped_time - test.started_time, # real time
2276 2279 test.started_time - origin, # start date in run context
2277 2280 stopped_time - origin, # end date in run context
2278 2281 )
2279 2282 )
2280 2283
2281 2284 if interrupted:
2282 2285 with iolock:
2283 2286 self.stream.writeln(
2284 2287 'INTERRUPTED: %s (after %d seconds)'
2285 2288 % (test.name, self.times[-1][3])
2286 2289 )
2287 2290
2288 2291
2289 2292 def getTestResult():
2290 2293 """
2291 2294 Returns the relevant test result
2292 2295 """
2293 2296 if "CUSTOM_TEST_RESULT" in os.environ:
2294 2297 testresultmodule = __import__(os.environ["CUSTOM_TEST_RESULT"])
2295 2298 return testresultmodule.TestResult
2296 2299 else:
2297 2300 return TestResult
2298 2301
2299 2302
2300 2303 class TestSuite(unittest.TestSuite):
2301 2304 """Custom unittest TestSuite that knows how to execute Mercurial tests."""
2302 2305
2303 2306 def __init__(
2304 2307 self,
2305 2308 testdir,
2306 2309 jobs=1,
2307 2310 whitelist=None,
2308 2311 blacklist=None,
2309 2312 retest=False,
2310 2313 keywords=None,
2311 2314 loop=False,
2312 2315 runs_per_test=1,
2313 2316 loadtest=None,
2314 2317 showchannels=False,
2315 2318 *args,
2316 2319 **kwargs
2317 2320 ):
2318 2321 """Create a new instance that can run tests with a configuration.
2319 2322
2320 2323 testdir specifies the directory where tests are executed from. This
2321 2324 is typically the ``tests`` directory from Mercurial's source
2322 2325 repository.
2323 2326
2324 2327 jobs specifies the number of jobs to run concurrently. Each test
2325 2328 executes on its own thread. Tests actually spawn new processes, so
2326 2329 state mutation should not be an issue.
2327 2330
2328 2331 If there is only one job, it will use the main thread.
2329 2332
2330 2333 whitelist and blacklist denote tests that have been whitelisted and
2331 2334 blacklisted, respectively. These arguments don't belong in TestSuite.
2332 2335 Instead, whitelist and blacklist should be handled by the thing that
2333 2336 populates the TestSuite with tests. They are present to preserve
2334 2337 backwards compatible behavior which reports skipped tests as part
2335 2338 of the results.
2336 2339
2337 2340 retest denotes whether to retest failed tests. This arguably belongs
2338 2341 outside of TestSuite.
2339 2342
2340 2343 keywords denotes key words that will be used to filter which tests
2341 2344 to execute. This arguably belongs outside of TestSuite.
2342 2345
2343 2346 loop denotes whether to loop over tests forever.
2344 2347 """
2345 2348 super(TestSuite, self).__init__(*args, **kwargs)
2346 2349
2347 2350 self._jobs = jobs
2348 2351 self._whitelist = whitelist
2349 2352 self._blacklist = blacklist
2350 2353 self._retest = retest
2351 2354 self._keywords = keywords
2352 2355 self._loop = loop
2353 2356 self._runs_per_test = runs_per_test
2354 2357 self._loadtest = loadtest
2355 2358 self._showchannels = showchannels
2356 2359
2357 2360 def run(self, result):
2358 2361 # We have a number of filters that need to be applied. We do this
2359 2362 # here instead of inside Test because it makes the running logic for
2360 2363 # Test simpler.
2361 2364 tests = []
2362 2365 num_tests = [0]
2363 2366 for test in self._tests:
2364 2367
2365 2368 def get():
2366 2369 num_tests[0] += 1
2367 2370 if getattr(test, 'should_reload', False):
2368 2371 return self._loadtest(test, num_tests[0])
2369 2372 return test
2370 2373
2371 2374 if not os.path.exists(test.path):
2372 2375 result.addSkip(test, "Doesn't exist")
2373 2376 continue
2374 2377
2375 2378 if not (self._whitelist and test.bname in self._whitelist):
2376 2379 if self._blacklist and test.bname in self._blacklist:
2377 2380 result.addSkip(test, 'blacklisted')
2378 2381 continue
2379 2382
2380 2383 if self._retest and not os.path.exists(test.errpath):
2381 2384 result.addIgnore(test, 'not retesting')
2382 2385 continue
2383 2386
2384 2387 if self._keywords:
2385 2388 with open(test.path, 'rb') as f:
2386 2389 t = f.read().lower() + test.bname.lower()
2387 2390 ignored = False
2388 2391 for k in self._keywords.lower().split():
2389 2392 if k not in t:
2390 2393 result.addIgnore(test, "doesn't match keyword")
2391 2394 ignored = True
2392 2395 break
2393 2396
2394 2397 if ignored:
2395 2398 continue
2396 2399 for _ in xrange(self._runs_per_test):
2397 2400 tests.append(get())
2398 2401
2399 2402 runtests = list(tests)
2400 2403 done = queue.Queue()
2401 2404 running = 0
2402 2405
2403 2406 channels = [""] * self._jobs
2404 2407
2405 2408 def job(test, result):
2406 2409 for n, v in enumerate(channels):
2407 2410 if not v:
2408 2411 channel = n
2409 2412 break
2410 2413 else:
2411 2414 raise ValueError('Could not find output channel')
2412 2415 channels[channel] = "=" + test.name[5:].split(".")[0]
2413 2416 try:
2414 2417 test(result)
2415 2418 done.put(None)
2416 2419 except KeyboardInterrupt:
2417 2420 pass
2418 2421 except: # re-raises
2419 2422 done.put(('!', test, 'run-test raised an error, see traceback'))
2420 2423 raise
2421 2424 finally:
2422 2425 try:
2423 2426 channels[channel] = ''
2424 2427 except IndexError:
2425 2428 pass
2426 2429
2427 2430 def stat():
2428 2431 count = 0
2429 2432 while channels:
2430 2433 d = '\n%03s ' % count
2431 2434 for n, v in enumerate(channels):
2432 2435 if v:
2433 2436 d += v[0]
2434 2437 channels[n] = v[1:] or '.'
2435 2438 else:
2436 2439 d += ' '
2437 2440 d += ' '
2438 2441 with iolock:
2439 2442 sys.stdout.write(d + ' ')
2440 2443 sys.stdout.flush()
2441 2444 for x in xrange(10):
2442 2445 if channels:
2443 2446 time.sleep(0.1)
2444 2447 count += 1
2445 2448
2446 2449 stoppedearly = False
2447 2450
2448 2451 if self._showchannels:
2449 2452 statthread = threading.Thread(target=stat, name="stat")
2450 2453 statthread.start()
2451 2454
2452 2455 try:
2453 2456 while tests or running:
2454 2457 if not done.empty() or running == self._jobs or not tests:
2455 2458 try:
2456 2459 done.get(True, 1)
2457 2460 running -= 1
2458 2461 if result and result.shouldStop:
2459 2462 stoppedearly = True
2460 2463 break
2461 2464 except queue.Empty:
2462 2465 continue
2463 2466 if tests and not running == self._jobs:
2464 2467 test = tests.pop(0)
2465 2468 if self._loop:
2466 2469 if getattr(test, 'should_reload', False):
2467 2470 num_tests[0] += 1
2468 2471 tests.append(self._loadtest(test, num_tests[0]))
2469 2472 else:
2470 2473 tests.append(test)
2471 2474 if self._jobs == 1:
2472 2475 job(test, result)
2473 2476 else:
2474 2477 t = threading.Thread(
2475 2478 target=job, name=test.name, args=(test, result)
2476 2479 )
2477 2480 t.start()
2478 2481 running += 1
2479 2482
2480 2483 # If we stop early we still need to wait on started tests to
2481 2484 # finish. Otherwise, there is a race between the test completing
2482 2485 # and the test's cleanup code running. This could result in the
2483 2486 # test reporting incorrect.
2484 2487 if stoppedearly:
2485 2488 while running:
2486 2489 try:
2487 2490 done.get(True, 1)
2488 2491 running -= 1
2489 2492 except queue.Empty:
2490 2493 continue
2491 2494 except KeyboardInterrupt:
2492 2495 for test in runtests:
2493 2496 test.abort()
2494 2497
2495 2498 channels = []
2496 2499
2497 2500 return result
2498 2501
2499 2502
2500 2503 # Save the most recent 5 wall-clock runtimes of each test to a
2501 2504 # human-readable text file named .testtimes. Tests are sorted
2502 2505 # alphabetically, while times for each test are listed from oldest to
2503 2506 # newest.
2504 2507
2505 2508
2506 2509 def loadtimes(outputdir):
2507 2510 times = []
2508 2511 try:
2509 2512 with open(os.path.join(outputdir, b'.testtimes')) as fp:
2510 2513 for line in fp:
2511 2514 m = re.match('(.*?) ([0-9. ]+)', line)
2512 2515 times.append(
2513 2516 (m.group(1), [float(t) for t in m.group(2).split()])
2514 2517 )
2515 2518 except IOError as err:
2516 2519 if err.errno != errno.ENOENT:
2517 2520 raise
2518 2521 return times
2519 2522
2520 2523
2521 2524 def savetimes(outputdir, result):
2522 2525 saved = dict(loadtimes(outputdir))
2523 2526 maxruns = 5
2524 2527 skipped = set([str(t[0]) for t in result.skipped])
2525 2528 for tdata in result.times:
2526 2529 test, real = tdata[0], tdata[3]
2527 2530 if test not in skipped:
2528 2531 ts = saved.setdefault(test, [])
2529 2532 ts.append(real)
2530 2533 ts[:] = ts[-maxruns:]
2531 2534
2532 2535 fd, tmpname = tempfile.mkstemp(
2533 2536 prefix=b'.testtimes', dir=outputdir, text=True
2534 2537 )
2535 2538 with os.fdopen(fd, 'w') as fp:
2536 2539 for name, ts in sorted(saved.items()):
2537 2540 fp.write('%s %s\n' % (name, ' '.join(['%.3f' % (t,) for t in ts])))
2538 2541 timepath = os.path.join(outputdir, b'.testtimes')
2539 2542 try:
2540 2543 os.unlink(timepath)
2541 2544 except OSError:
2542 2545 pass
2543 2546 try:
2544 2547 os.rename(tmpname, timepath)
2545 2548 except OSError:
2546 2549 pass
2547 2550
2548 2551
2549 2552 class TextTestRunner(unittest.TextTestRunner):
2550 2553 """Custom unittest test runner that uses appropriate settings."""
2551 2554
2552 2555 def __init__(self, runner, *args, **kwargs):
2553 2556 super(TextTestRunner, self).__init__(*args, **kwargs)
2554 2557
2555 2558 self._runner = runner
2556 2559
2557 2560 self._result = getTestResult()(
2558 2561 self._runner.options, self.stream, self.descriptions, self.verbosity
2559 2562 )
2560 2563
2561 2564 def listtests(self, test):
2562 2565 test = sorted(test, key=lambda t: t.name)
2563 2566
2564 2567 self._result.onStart(test)
2565 2568
2566 2569 for t in test:
2567 2570 print(t.name)
2568 2571 self._result.addSuccess(t)
2569 2572
2570 2573 if self._runner.options.xunit:
2571 2574 with open(self._runner.options.xunit, "wb") as xuf:
2572 2575 self._writexunit(self._result, xuf)
2573 2576
2574 2577 if self._runner.options.json:
2575 2578 jsonpath = os.path.join(self._runner._outputdir, b'report.json')
2576 2579 with open(jsonpath, 'w') as fp:
2577 2580 self._writejson(self._result, fp)
2578 2581
2579 2582 return self._result
2580 2583
2581 2584 def run(self, test):
2582 2585 self._result.onStart(test)
2583 2586 test(self._result)
2584 2587
2585 2588 failed = len(self._result.failures)
2586 2589 skipped = len(self._result.skipped)
2587 2590 ignored = len(self._result.ignored)
2588 2591
2589 2592 with iolock:
2590 2593 self.stream.writeln('')
2591 2594
2592 2595 if not self._runner.options.noskips:
2593 2596 for test, msg in sorted(
2594 2597 self._result.skipped, key=lambda s: s[0].name
2595 2598 ):
2596 2599 formatted = 'Skipped %s: %s\n' % (test.name, msg)
2597 2600 msg = highlightmsg(formatted, self._result.color)
2598 2601 self.stream.write(msg)
2599 2602 for test, msg in sorted(
2600 2603 self._result.failures, key=lambda f: f[0].name
2601 2604 ):
2602 2605 formatted = 'Failed %s: %s\n' % (test.name, msg)
2603 2606 self.stream.write(highlightmsg(formatted, self._result.color))
2604 2607 for test, msg in sorted(
2605 2608 self._result.errors, key=lambda e: e[0].name
2606 2609 ):
2607 2610 self.stream.writeln('Errored %s: %s' % (test.name, msg))
2608 2611
2609 2612 if self._runner.options.xunit:
2610 2613 with open(self._runner.options.xunit, "wb") as xuf:
2611 2614 self._writexunit(self._result, xuf)
2612 2615
2613 2616 if self._runner.options.json:
2614 2617 jsonpath = os.path.join(self._runner._outputdir, b'report.json')
2615 2618 with open(jsonpath, 'w') as fp:
2616 2619 self._writejson(self._result, fp)
2617 2620
2618 2621 self._runner._checkhglib('Tested')
2619 2622
2620 2623 savetimes(self._runner._outputdir, self._result)
2621 2624
2622 2625 if failed and self._runner.options.known_good_rev:
2623 2626 self._bisecttests(t for t, m in self._result.failures)
2624 2627 self.stream.writeln(
2625 2628 '# Ran %d tests, %d skipped, %d failed.'
2626 2629 % (self._result.testsRun, skipped + ignored, failed)
2627 2630 )
2628 2631 if failed:
2629 2632 self.stream.writeln(
2630 2633 'python hash seed: %s' % os.environ['PYTHONHASHSEED']
2631 2634 )
2632 2635 if self._runner.options.time:
2633 2636 self.printtimes(self._result.times)
2634 2637
2635 2638 if self._runner.options.exceptions:
2636 2639 exceptions = aggregateexceptions(
2637 2640 os.path.join(self._runner._outputdir, b'exceptions')
2638 2641 )
2639 2642
2640 2643 self.stream.writeln('Exceptions Report:')
2641 2644 self.stream.writeln(
2642 2645 '%d total from %d frames'
2643 2646 % (exceptions['total'], len(exceptions['exceptioncounts']))
2644 2647 )
2645 2648 combined = exceptions['combined']
2646 2649 for key in sorted(combined, key=combined.get, reverse=True):
2647 2650 frame, line, exc = key
2648 2651 totalcount, testcount, leastcount, leasttest = combined[key]
2649 2652
2650 2653 self.stream.writeln(
2651 2654 '%d (%d tests)\t%s: %s (%s - %d total)'
2652 2655 % (
2653 2656 totalcount,
2654 2657 testcount,
2655 2658 frame,
2656 2659 exc,
2657 2660 leasttest,
2658 2661 leastcount,
2659 2662 )
2660 2663 )
2661 2664
2662 2665 self.stream.flush()
2663 2666
2664 2667 return self._result
2665 2668
2666 2669 def _bisecttests(self, tests):
2667 2670 bisectcmd = ['hg', 'bisect']
2668 2671 bisectrepo = self._runner.options.bisect_repo
2669 2672 if bisectrepo:
2670 2673 bisectcmd.extend(['-R', os.path.abspath(bisectrepo)])
2671 2674
2672 2675 def pread(args):
2673 2676 env = os.environ.copy()
2674 2677 env['HGPLAIN'] = '1'
2675 2678 p = subprocess.Popen(
2676 2679 args, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, env=env
2677 2680 )
2678 2681 data = p.stdout.read()
2679 2682 p.wait()
2680 2683 return data
2681 2684
2682 2685 for test in tests:
2683 2686 pread(bisectcmd + ['--reset']),
2684 2687 pread(bisectcmd + ['--bad', '.'])
2685 2688 pread(bisectcmd + ['--good', self._runner.options.known_good_rev])
2686 2689 # TODO: we probably need to forward more options
2687 2690 # that alter hg's behavior inside the tests.
2688 2691 opts = ''
2689 2692 withhg = self._runner.options.with_hg
2690 2693 if withhg:
2691 opts += ' --with-hg=%s ' % shellquote(_strpath(withhg))
2694 opts += ' --with-hg=%s ' % shellquote(_bytes2sys(withhg))
2692 2695 rtc = '%s %s %s %s' % (sysexecutable, sys.argv[0], opts, test)
2693 2696 data = pread(bisectcmd + ['--command', rtc])
2694 2697 m = re.search(
2695 2698 (
2696 2699 br'\nThe first (?P<goodbad>bad|good) revision '
2697 2700 br'is:\nchangeset: +\d+:(?P<node>[a-f0-9]+)\n.*\n'
2698 2701 br'summary: +(?P<summary>[^\n]+)\n'
2699 2702 ),
2700 2703 data,
2701 2704 (re.MULTILINE | re.DOTALL),
2702 2705 )
2703 2706 if m is None:
2704 2707 self.stream.writeln(
2705 2708 'Failed to identify failure point for %s' % test
2706 2709 )
2707 2710 continue
2708 2711 dat = m.groupdict()
2709 2712 verb = 'broken' if dat['goodbad'] == b'bad' else 'fixed'
2710 2713 self.stream.writeln(
2711 2714 '%s %s by %s (%s)'
2712 2715 % (
2713 2716 test,
2714 2717 verb,
2715 2718 dat['node'].decode('ascii'),
2716 2719 dat['summary'].decode('utf8', 'ignore'),
2717 2720 )
2718 2721 )
2719 2722
2720 2723 def printtimes(self, times):
2721 2724 # iolock held by run
2722 2725 self.stream.writeln('# Producing time report')
2723 2726 times.sort(key=lambda t: (t[3]))
2724 2727 cols = '%7.3f %7.3f %7.3f %7.3f %7.3f %s'
2725 2728 self.stream.writeln(
2726 2729 '%-7s %-7s %-7s %-7s %-7s %s'
2727 2730 % ('start', 'end', 'cuser', 'csys', 'real', 'Test')
2728 2731 )
2729 2732 for tdata in times:
2730 2733 test = tdata[0]
2731 2734 cuser, csys, real, start, end = tdata[1:6]
2732 2735 self.stream.writeln(cols % (start, end, cuser, csys, real, test))
2733 2736
2734 2737 @staticmethod
2735 2738 def _writexunit(result, outf):
2736 2739 # See http://llg.cubic.org/docs/junit/ for a reference.
2737 2740 timesd = dict((t[0], t[3]) for t in result.times)
2738 2741 doc = minidom.Document()
2739 2742 s = doc.createElement('testsuite')
2740 2743 s.setAttribute('errors', "0") # TODO
2741 2744 s.setAttribute('failures', str(len(result.failures)))
2742 2745 s.setAttribute('name', 'run-tests')
2743 2746 s.setAttribute(
2744 2747 'skipped', str(len(result.skipped) + len(result.ignored))
2745 2748 )
2746 2749 s.setAttribute('tests', str(result.testsRun))
2747 2750 doc.appendChild(s)
2748 2751 for tc in result.successes:
2749 2752 t = doc.createElement('testcase')
2750 2753 t.setAttribute('name', tc.name)
2751 2754 tctime = timesd.get(tc.name)
2752 2755 if tctime is not None:
2753 2756 t.setAttribute('time', '%.3f' % tctime)
2754 2757 s.appendChild(t)
2755 2758 for tc, err in sorted(result.faildata.items()):
2756 2759 t = doc.createElement('testcase')
2757 2760 t.setAttribute('name', tc)
2758 2761 tctime = timesd.get(tc)
2759 2762 if tctime is not None:
2760 2763 t.setAttribute('time', '%.3f' % tctime)
2761 2764 # createCDATASection expects a unicode or it will
2762 2765 # convert using default conversion rules, which will
2763 2766 # fail if string isn't ASCII.
2764 2767 err = cdatasafe(err).decode('utf-8', 'replace')
2765 2768 cd = doc.createCDATASection(err)
2766 2769 # Use 'failure' here instead of 'error' to match errors = 0,
2767 2770 # failures = len(result.failures) in the testsuite element.
2768 2771 failelem = doc.createElement('failure')
2769 2772 failelem.setAttribute('message', 'output changed')
2770 2773 failelem.setAttribute('type', 'output-mismatch')
2771 2774 failelem.appendChild(cd)
2772 2775 t.appendChild(failelem)
2773 2776 s.appendChild(t)
2774 2777 for tc, message in result.skipped:
2775 2778 # According to the schema, 'skipped' has no attributes. So store
2776 2779 # the skip message as a text node instead.
2777 2780 t = doc.createElement('testcase')
2778 2781 t.setAttribute('name', tc.name)
2779 2782 binmessage = message.encode('utf-8')
2780 2783 message = cdatasafe(binmessage).decode('utf-8', 'replace')
2781 2784 cd = doc.createCDATASection(message)
2782 2785 skipelem = doc.createElement('skipped')
2783 2786 skipelem.appendChild(cd)
2784 2787 t.appendChild(skipelem)
2785 2788 s.appendChild(t)
2786 2789 outf.write(doc.toprettyxml(indent=' ', encoding='utf-8'))
2787 2790
2788 2791 @staticmethod
2789 2792 def _writejson(result, outf):
2790 2793 timesd = {}
2791 2794 for tdata in result.times:
2792 2795 test = tdata[0]
2793 2796 timesd[test] = tdata[1:]
2794 2797
2795 2798 outcome = {}
2796 2799 groups = [
2797 2800 ('success', ((tc, None) for tc in result.successes)),
2798 2801 ('failure', result.failures),
2799 2802 ('skip', result.skipped),
2800 2803 ]
2801 2804 for res, testcases in groups:
2802 2805 for tc, __ in testcases:
2803 2806 if tc.name in timesd:
2804 2807 diff = result.faildata.get(tc.name, b'')
2805 2808 try:
2806 2809 diff = diff.decode('unicode_escape')
2807 2810 except UnicodeDecodeError as e:
2808 2811 diff = '%r decoding diff, sorry' % e
2809 2812 tres = {
2810 2813 'result': res,
2811 2814 'time': ('%0.3f' % timesd[tc.name][2]),
2812 2815 'cuser': ('%0.3f' % timesd[tc.name][0]),
2813 2816 'csys': ('%0.3f' % timesd[tc.name][1]),
2814 2817 'start': ('%0.3f' % timesd[tc.name][3]),
2815 2818 'end': ('%0.3f' % timesd[tc.name][4]),
2816 2819 'diff': diff,
2817 2820 }
2818 2821 else:
2819 2822 # blacklisted test
2820 2823 tres = {'result': res}
2821 2824
2822 2825 outcome[tc.name] = tres
2823 2826 jsonout = json.dumps(
2824 2827 outcome, sort_keys=True, indent=4, separators=(',', ': ')
2825 2828 )
2826 2829 outf.writelines(("testreport =", jsonout))
2827 2830
2828 2831
2829 2832 def sorttests(testdescs, previoustimes, shuffle=False):
2830 2833 """Do an in-place sort of tests."""
2831 2834 if shuffle:
2832 2835 random.shuffle(testdescs)
2833 2836 return
2834 2837
2835 2838 if previoustimes:
2836 2839
2837 2840 def sortkey(f):
2838 2841 f = f['path']
2839 2842 if f in previoustimes:
2840 2843 # Use most recent time as estimate
2841 2844 return -(previoustimes[f][-1])
2842 2845 else:
2843 2846 # Default to a rather arbitrary value of 1 second for new tests
2844 2847 return -1.0
2845 2848
2846 2849 else:
2847 2850 # keywords for slow tests
2848 2851 slow = {
2849 2852 b'svn': 10,
2850 2853 b'cvs': 10,
2851 2854 b'hghave': 10,
2852 2855 b'largefiles-update': 10,
2853 2856 b'run-tests': 10,
2854 2857 b'corruption': 10,
2855 2858 b'race': 10,
2856 2859 b'i18n': 10,
2857 2860 b'check': 100,
2858 2861 b'gendoc': 100,
2859 2862 b'contrib-perf': 200,
2860 2863 b'merge-combination': 100,
2861 2864 }
2862 2865 perf = {}
2863 2866
2864 2867 def sortkey(f):
2865 2868 # run largest tests first, as they tend to take the longest
2866 2869 f = f['path']
2867 2870 try:
2868 2871 return perf[f]
2869 2872 except KeyError:
2870 2873 try:
2871 2874 val = -os.stat(f).st_size
2872 2875 except OSError as e:
2873 2876 if e.errno != errno.ENOENT:
2874 2877 raise
2875 2878 perf[f] = -1e9 # file does not exist, tell early
2876 2879 return -1e9
2877 2880 for kw, mul in slow.items():
2878 2881 if kw in f:
2879 2882 val *= mul
2880 2883 if f.endswith(b'.py'):
2881 2884 val /= 10.0
2882 2885 perf[f] = val / 1000.0
2883 2886 return perf[f]
2884 2887
2885 2888 testdescs.sort(key=sortkey)
2886 2889
2887 2890
2888 2891 class TestRunner(object):
2889 2892 """Holds context for executing tests.
2890 2893
2891 2894 Tests rely on a lot of state. This object holds it for them.
2892 2895 """
2893 2896
2894 2897 # Programs required to run tests.
2895 2898 REQUIREDTOOLS = [
2896 2899 b'diff',
2897 2900 b'grep',
2898 2901 b'unzip',
2899 2902 b'gunzip',
2900 2903 b'bunzip2',
2901 2904 b'sed',
2902 2905 ]
2903 2906
2904 2907 # Maps file extensions to test class.
2905 2908 TESTTYPES = [
2906 2909 (b'.py', PythonTest),
2907 2910 (b'.t', TTest),
2908 2911 ]
2909 2912
2910 2913 def __init__(self):
2911 2914 self.options = None
2912 2915 self._hgroot = None
2913 2916 self._testdir = None
2914 2917 self._outputdir = None
2915 2918 self._hgtmp = None
2916 2919 self._installdir = None
2917 2920 self._bindir = None
2918 2921 self._tmpbinddir = None
2919 2922 self._pythondir = None
2920 2923 self._coveragefile = None
2921 2924 self._createdfiles = []
2922 2925 self._hgcommand = None
2923 2926 self._hgpath = None
2924 2927 self._portoffset = 0
2925 2928 self._ports = {}
2926 2929
2927 2930 def run(self, args, parser=None):
2928 2931 """Run the test suite."""
2929 2932 oldmask = os.umask(0o22)
2930 2933 try:
2931 2934 parser = parser or getparser()
2932 2935 options = parseargs(args, parser)
2933 tests = [_bytespath(a) for a in options.tests]
2936 tests = [_sys2bytes(a) for a in options.tests]
2934 2937 if options.test_list is not None:
2935 2938 for listfile in options.test_list:
2936 2939 with open(listfile, 'rb') as f:
2937 2940 tests.extend(t for t in f.read().splitlines() if t)
2938 2941 self.options = options
2939 2942
2940 2943 self._checktools()
2941 2944 testdescs = self.findtests(tests)
2942 2945 if options.profile_runner:
2943 2946 import statprof
2944 2947
2945 2948 statprof.start()
2946 2949 result = self._run(testdescs)
2947 2950 if options.profile_runner:
2948 2951 statprof.stop()
2949 2952 statprof.display()
2950 2953 return result
2951 2954
2952 2955 finally:
2953 2956 os.umask(oldmask)
2954 2957
2955 2958 def _run(self, testdescs):
2956 2959 testdir = getcwdb()
2957 2960 self._testdir = osenvironb[b'TESTDIR'] = getcwdb()
2958 2961 # assume all tests in same folder for now
2959 2962 if testdescs:
2960 2963 pathname = os.path.dirname(testdescs[0]['path'])
2961 2964 if pathname:
2962 2965 testdir = os.path.join(testdir, pathname)
2963 2966 self._testdir = osenvironb[b'TESTDIR'] = testdir
2964 2967 if self.options.outputdir:
2965 self._outputdir = canonpath(_bytespath(self.options.outputdir))
2968 self._outputdir = canonpath(_sys2bytes(self.options.outputdir))
2966 2969 else:
2967 2970 self._outputdir = getcwdb()
2968 2971 if testdescs and pathname:
2969 2972 self._outputdir = os.path.join(self._outputdir, pathname)
2970 2973 previoustimes = {}
2971 2974 if self.options.order_by_runtime:
2972 2975 previoustimes = dict(loadtimes(self._outputdir))
2973 2976 sorttests(testdescs, previoustimes, shuffle=self.options.random)
2974 2977
2975 2978 if 'PYTHONHASHSEED' not in os.environ:
2976 2979 # use a random python hash seed all the time
2977 2980 # we do the randomness ourself to know what seed is used
2978 2981 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
2979 2982
2980 2983 if self.options.tmpdir:
2981 2984 self.options.keep_tmpdir = True
2982 tmpdir = _bytespath(self.options.tmpdir)
2985 tmpdir = _sys2bytes(self.options.tmpdir)
2983 2986 if os.path.exists(tmpdir):
2984 2987 # Meaning of tmpdir has changed since 1.3: we used to create
2985 2988 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
2986 2989 # tmpdir already exists.
2987 2990 print("error: temp dir %r already exists" % tmpdir)
2988 2991 return 1
2989 2992
2990 2993 os.makedirs(tmpdir)
2991 2994 else:
2992 2995 d = None
2993 2996 if os.name == 'nt':
2994 2997 # without this, we get the default temp dir location, but
2995 2998 # in all lowercase, which causes troubles with paths (issue3490)
2996 2999 d = osenvironb.get(b'TMP', None)
2997 3000 tmpdir = tempfile.mkdtemp(b'', b'hgtests.', d)
2998 3001
2999 3002 self._hgtmp = osenvironb[b'HGTMP'] = os.path.realpath(tmpdir)
3000 3003
3001 3004 if self.options.with_hg:
3002 3005 self._installdir = None
3003 3006 whg = self.options.with_hg
3004 3007 self._bindir = os.path.dirname(os.path.realpath(whg))
3005 3008 assert isinstance(self._bindir, bytes)
3006 3009 self._hgcommand = os.path.basename(whg)
3007 3010 self._tmpbindir = os.path.join(self._hgtmp, b'install', b'bin')
3008 3011 os.makedirs(self._tmpbindir)
3009 3012
3010 3013 normbin = os.path.normpath(os.path.abspath(whg))
3011 normbin = normbin.replace(_bytespath(os.sep), b'/')
3014 normbin = normbin.replace(_sys2bytes(os.sep), b'/')
3012 3015
3013 3016 # Other Python scripts in the test harness need to
3014 3017 # `import mercurial`. If `hg` is a Python script, we assume
3015 3018 # the Mercurial modules are relative to its path and tell the tests
3016 3019 # to load Python modules from its directory.
3017 3020 with open(whg, 'rb') as fh:
3018 3021 initial = fh.read(1024)
3019 3022
3020 3023 if re.match(b'#!.*python', initial):
3021 3024 self._pythondir = self._bindir
3022 3025 # If it looks like our in-repo Rust binary, use the source root.
3023 3026 # This is a bit hacky. But rhg is still not supported outside the
3024 3027 # source directory. So until it is, do the simple thing.
3025 3028 elif re.search(b'/rust/target/[^/]+/hg', normbin):
3026 3029 self._pythondir = os.path.dirname(self._testdir)
3027 3030 # Fall back to the legacy behavior.
3028 3031 else:
3029 3032 self._pythondir = self._bindir
3030 3033
3031 3034 else:
3032 3035 self._installdir = os.path.join(self._hgtmp, b"install")
3033 3036 self._bindir = os.path.join(self._installdir, b"bin")
3034 3037 self._hgcommand = b'hg'
3035 3038 self._tmpbindir = self._bindir
3036 3039 self._pythondir = os.path.join(self._installdir, b"lib", b"python")
3037 3040
3038 3041 # Force the use of hg.exe instead of relying on MSYS to recognize hg is
3039 3042 # a python script and feed it to python.exe. Legacy stdio is force
3040 3043 # enabled by hg.exe, and this is a more realistic way to launch hg
3041 3044 # anyway.
3042 3045 if os.name == 'nt' and not self._hgcommand.endswith(b'.exe'):
3043 3046 self._hgcommand += b'.exe'
3044 3047
3045 3048 # set CHGHG, then replace "hg" command by "chg"
3046 3049 chgbindir = self._bindir
3047 3050 if self.options.chg or self.options.with_chg:
3048 3051 osenvironb[b'CHGHG'] = os.path.join(self._bindir, self._hgcommand)
3049 3052 else:
3050 3053 osenvironb.pop(b'CHGHG', None) # drop flag for hghave
3051 3054 if self.options.chg:
3052 3055 self._hgcommand = b'chg'
3053 3056 elif self.options.with_chg:
3054 3057 chgbindir = os.path.dirname(os.path.realpath(self.options.with_chg))
3055 3058 self._hgcommand = os.path.basename(self.options.with_chg)
3056 3059
3057 3060 osenvironb[b"BINDIR"] = self._bindir
3058 3061 osenvironb[b"PYTHON"] = PYTHON
3059 3062
3060 fileb = _bytespath(__file__)
3063 fileb = _sys2bytes(__file__)
3061 3064 runtestdir = os.path.abspath(os.path.dirname(fileb))
3062 3065 osenvironb[b'RUNTESTDIR'] = runtestdir
3063 3066 if PYTHON3:
3064 sepb = _bytespath(os.pathsep)
3067 sepb = _sys2bytes(os.pathsep)
3065 3068 else:
3066 3069 sepb = os.pathsep
3067 3070 path = [self._bindir, runtestdir] + osenvironb[b"PATH"].split(sepb)
3068 3071 if os.path.islink(__file__):
3069 3072 # test helper will likely be at the end of the symlink
3070 3073 realfile = os.path.realpath(fileb)
3071 3074 realdir = os.path.abspath(os.path.dirname(realfile))
3072 3075 path.insert(2, realdir)
3073 3076 if chgbindir != self._bindir:
3074 3077 path.insert(1, chgbindir)
3075 3078 if self._testdir != runtestdir:
3076 3079 path = [self._testdir] + path
3077 3080 if self._tmpbindir != self._bindir:
3078 3081 path = [self._tmpbindir] + path
3079 3082 osenvironb[b"PATH"] = sepb.join(path)
3080 3083
3081 3084 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
3082 3085 # can run .../tests/run-tests.py test-foo where test-foo
3083 3086 # adds an extension to HGRC. Also include run-test.py directory to
3084 3087 # import modules like heredoctest.
3085 3088 pypath = [self._pythondir, self._testdir, runtestdir]
3086 3089 # We have to augment PYTHONPATH, rather than simply replacing
3087 3090 # it, in case external libraries are only available via current
3088 3091 # PYTHONPATH. (In particular, the Subversion bindings on OS X
3089 3092 # are in /opt/subversion.)
3090 3093 oldpypath = osenvironb.get(IMPL_PATH)
3091 3094 if oldpypath:
3092 3095 pypath.append(oldpypath)
3093 3096 osenvironb[IMPL_PATH] = sepb.join(pypath)
3094 3097
3095 3098 if self.options.pure:
3096 3099 os.environ["HGTEST_RUN_TESTS_PURE"] = "--pure"
3097 3100 os.environ["HGMODULEPOLICY"] = "py"
3098 3101
3099 3102 if self.options.allow_slow_tests:
3100 3103 os.environ["HGTEST_SLOW"] = "slow"
3101 3104 elif 'HGTEST_SLOW' in os.environ:
3102 3105 del os.environ['HGTEST_SLOW']
3103 3106
3104 3107 self._coveragefile = os.path.join(self._testdir, b'.coverage')
3105 3108
3106 3109 if self.options.exceptions:
3107 3110 exceptionsdir = os.path.join(self._outputdir, b'exceptions')
3108 3111 try:
3109 3112 os.makedirs(exceptionsdir)
3110 3113 except OSError as e:
3111 3114 if e.errno != errno.EEXIST:
3112 3115 raise
3113 3116
3114 3117 # Remove all existing exception reports.
3115 3118 for f in os.listdir(exceptionsdir):
3116 3119 os.unlink(os.path.join(exceptionsdir, f))
3117 3120
3118 3121 osenvironb[b'HGEXCEPTIONSDIR'] = exceptionsdir
3119 3122 logexceptions = os.path.join(self._testdir, b'logexceptions.py')
3120 3123 self.options.extra_config_opt.append(
3121 3124 'extensions.logexceptions=%s' % logexceptions.decode('utf-8')
3122 3125 )
3123 3126
3124 vlog("# Using TESTDIR", _strpath(self._testdir))
3125 vlog("# Using RUNTESTDIR", _strpath(osenvironb[b'RUNTESTDIR']))
3126 vlog("# Using HGTMP", _strpath(self._hgtmp))
3127 vlog("# Using TESTDIR", _bytes2sys(self._testdir))
3128 vlog("# Using RUNTESTDIR", _bytes2sys(osenvironb[b'RUNTESTDIR']))
3129 vlog("# Using HGTMP", _bytes2sys(self._hgtmp))
3127 3130 vlog("# Using PATH", os.environ["PATH"])
3128 3131 vlog(
3129 "# Using", _strpath(IMPL_PATH), _strpath(osenvironb[IMPL_PATH]),
3132 "# Using", _bytes2sys(IMPL_PATH), _bytes2sys(osenvironb[IMPL_PATH]),
3130 3133 )
3131 vlog("# Writing to directory", _strpath(self._outputdir))
3134 vlog("# Writing to directory", _bytes2sys(self._outputdir))
3132 3135
3133 3136 try:
3134 3137 return self._runtests(testdescs) or 0
3135 3138 finally:
3136 3139 time.sleep(0.1)
3137 3140 self._cleanup()
3138 3141
3139 3142 def findtests(self, args):
3140 3143 """Finds possible test files from arguments.
3141 3144
3142 3145 If you wish to inject custom tests into the test harness, this would
3143 3146 be a good function to monkeypatch or override in a derived class.
3144 3147 """
3145 3148 if not args:
3146 3149 if self.options.changed:
3147 3150 proc = Popen4(
3148 3151 b'hg st --rev "%s" -man0 .'
3149 % _bytespath(self.options.changed),
3152 % _sys2bytes(self.options.changed),
3150 3153 None,
3151 3154 0,
3152 3155 )
3153 3156 stdout, stderr = proc.communicate()
3154 3157 args = stdout.strip(b'\0').split(b'\0')
3155 3158 else:
3156 3159 args = os.listdir(b'.')
3157 3160
3158 3161 expanded_args = []
3159 3162 for arg in args:
3160 3163 if os.path.isdir(arg):
3161 3164 if not arg.endswith(b'/'):
3162 3165 arg += b'/'
3163 3166 expanded_args.extend([arg + a for a in os.listdir(arg)])
3164 3167 else:
3165 3168 expanded_args.append(arg)
3166 3169 args = expanded_args
3167 3170
3168 3171 testcasepattern = re.compile(br'([\w-]+\.t|py)(?:#([a-zA-Z0-9_\-.#]+))')
3169 3172 tests = []
3170 3173 for t in args:
3171 3174 case = []
3172 3175
3173 3176 if not (
3174 3177 os.path.basename(t).startswith(b'test-')
3175 3178 and (t.endswith(b'.py') or t.endswith(b'.t'))
3176 3179 ):
3177 3180
3178 3181 m = testcasepattern.match(os.path.basename(t))
3179 3182 if m is not None:
3180 3183 t_basename, casestr = m.groups()
3181 3184 t = os.path.join(os.path.dirname(t), t_basename)
3182 3185 if casestr:
3183 3186 case = casestr.split(b'#')
3184 3187 else:
3185 3188 continue
3186 3189
3187 3190 if t.endswith(b'.t'):
3188 3191 # .t file may contain multiple test cases
3189 3192 casedimensions = parsettestcases(t)
3190 3193 if casedimensions:
3191 3194 cases = []
3192 3195
3193 3196 def addcases(case, casedimensions):
3194 3197 if not casedimensions:
3195 3198 cases.append(case)
3196 3199 else:
3197 3200 for c in casedimensions[0]:
3198 3201 addcases(case + [c], casedimensions[1:])
3199 3202
3200 3203 addcases([], casedimensions)
3201 3204 if case and case in cases:
3202 3205 cases = [case]
3203 3206 elif case:
3204 3207 # Ignore invalid cases
3205 3208 cases = []
3206 3209 else:
3207 3210 pass
3208 3211 tests += [{'path': t, 'case': c} for c in sorted(cases)]
3209 3212 else:
3210 3213 tests.append({'path': t})
3211 3214 else:
3212 3215 tests.append({'path': t})
3213 3216 return tests
3214 3217
3215 3218 def _runtests(self, testdescs):
3216 3219 def _reloadtest(test, i):
3217 3220 # convert a test back to its description dict
3218 3221 desc = {'path': test.path}
3219 3222 case = getattr(test, '_case', [])
3220 3223 if case:
3221 3224 desc['case'] = case
3222 3225 return self._gettest(desc, i)
3223 3226
3224 3227 try:
3225 3228 if self.options.restart:
3226 3229 orig = list(testdescs)
3227 3230 while testdescs:
3228 3231 desc = testdescs[0]
3229 3232 # desc['path'] is a relative path
3230 3233 if 'case' in desc:
3231 3234 casestr = b'#'.join(desc['case'])
3232 3235 errpath = b'%s#%s.err' % (desc['path'], casestr)
3233 3236 else:
3234 3237 errpath = b'%s.err' % desc['path']
3235 3238 errpath = os.path.join(self._outputdir, errpath)
3236 3239 if os.path.exists(errpath):
3237 3240 break
3238 3241 testdescs.pop(0)
3239 3242 if not testdescs:
3240 3243 print("running all tests")
3241 3244 testdescs = orig
3242 3245
3243 3246 tests = [self._gettest(d, i) for i, d in enumerate(testdescs)]
3244 3247 num_tests = len(tests) * self.options.runs_per_test
3245 3248
3246 3249 jobs = min(num_tests, self.options.jobs)
3247 3250
3248 3251 failed = False
3249 3252 kws = self.options.keywords
3250 3253 if kws is not None and PYTHON3:
3251 3254 kws = kws.encode('utf-8')
3252 3255
3253 3256 suite = TestSuite(
3254 3257 self._testdir,
3255 3258 jobs=jobs,
3256 3259 whitelist=self.options.whitelisted,
3257 3260 blacklist=self.options.blacklist,
3258 3261 retest=self.options.retest,
3259 3262 keywords=kws,
3260 3263 loop=self.options.loop,
3261 3264 runs_per_test=self.options.runs_per_test,
3262 3265 showchannels=self.options.showchannels,
3263 3266 tests=tests,
3264 3267 loadtest=_reloadtest,
3265 3268 )
3266 3269 verbosity = 1
3267 3270 if self.options.list_tests:
3268 3271 verbosity = 0
3269 3272 elif self.options.verbose:
3270 3273 verbosity = 2
3271 3274 runner = TextTestRunner(self, verbosity=verbosity)
3272 3275
3273 3276 if self.options.list_tests:
3274 3277 result = runner.listtests(suite)
3275 3278 else:
3276 3279 if self._installdir:
3277 3280 self._installhg()
3278 3281 self._checkhglib("Testing")
3279 3282 else:
3280 3283 self._usecorrectpython()
3281 3284 if self.options.chg:
3282 3285 assert self._installdir
3283 3286 self._installchg()
3284 3287
3285 3288 log(
3286 3289 'running %d tests using %d parallel processes'
3287 3290 % (num_tests, jobs)
3288 3291 )
3289 3292
3290 3293 result = runner.run(suite)
3291 3294
3292 3295 if result.failures or result.errors:
3293 3296 failed = True
3294 3297
3295 3298 result.onEnd()
3296 3299
3297 3300 if self.options.anycoverage:
3298 3301 self._outputcoverage()
3299 3302 except KeyboardInterrupt:
3300 3303 failed = True
3301 3304 print("\ninterrupted!")
3302 3305
3303 3306 if failed:
3304 3307 return 1
3305 3308
3306 3309 def _getport(self, count):
3307 3310 port = self._ports.get(count) # do we have a cached entry?
3308 3311 if port is None:
3309 3312 portneeded = 3
3310 3313 # above 100 tries we just give up and let test reports failure
3311 3314 for tries in xrange(100):
3312 3315 allfree = True
3313 3316 port = self.options.port + self._portoffset
3314 3317 for idx in xrange(portneeded):
3315 3318 if not checkportisavailable(port + idx):
3316 3319 allfree = False
3317 3320 break
3318 3321 self._portoffset += portneeded
3319 3322 if allfree:
3320 3323 break
3321 3324 self._ports[count] = port
3322 3325 return port
3323 3326
3324 3327 def _gettest(self, testdesc, count):
3325 3328 """Obtain a Test by looking at its filename.
3326 3329
3327 3330 Returns a Test instance. The Test may not be runnable if it doesn't
3328 3331 map to a known type.
3329 3332 """
3330 3333 path = testdesc['path']
3331 3334 lctest = path.lower()
3332 3335 testcls = Test
3333 3336
3334 3337 for ext, cls in self.TESTTYPES:
3335 3338 if lctest.endswith(ext):
3336 3339 testcls = cls
3337 3340 break
3338 3341
3339 3342 refpath = os.path.join(getcwdb(), path)
3340 3343 tmpdir = os.path.join(self._hgtmp, b'child%d' % count)
3341 3344
3342 3345 # extra keyword parameters. 'case' is used by .t tests
3343 3346 kwds = dict((k, testdesc[k]) for k in ['case'] if k in testdesc)
3344 3347
3345 3348 t = testcls(
3346 3349 refpath,
3347 3350 self._outputdir,
3348 3351 tmpdir,
3349 3352 keeptmpdir=self.options.keep_tmpdir,
3350 3353 debug=self.options.debug,
3351 3354 first=self.options.first,
3352 3355 timeout=self.options.timeout,
3353 3356 startport=self._getport(count),
3354 3357 extraconfigopts=self.options.extra_config_opt,
3355 3358 shell=self.options.shell,
3356 3359 hgcommand=self._hgcommand,
3357 3360 usechg=bool(self.options.with_chg or self.options.chg),
3358 3361 useipv6=useipv6,
3359 3362 **kwds
3360 3363 )
3361 3364 t.should_reload = True
3362 3365 return t
3363 3366
3364 3367 def _cleanup(self):
3365 3368 """Clean up state from this test invocation."""
3366 3369 if self.options.keep_tmpdir:
3367 3370 return
3368 3371
3369 vlog("# Cleaning up HGTMP", _strpath(self._hgtmp))
3372 vlog("# Cleaning up HGTMP", _bytes2sys(self._hgtmp))
3370 3373 shutil.rmtree(self._hgtmp, True)
3371 3374 for f in self._createdfiles:
3372 3375 try:
3373 3376 os.remove(f)
3374 3377 except OSError:
3375 3378 pass
3376 3379
3377 3380 def _usecorrectpython(self):
3378 3381 """Configure the environment to use the appropriate Python in tests."""
3379 3382 # Tests must use the same interpreter as us or bad things will happen.
3380 3383 pyexename = sys.platform == 'win32' and b'python.exe' or b'python'
3381 3384
3382 3385 # os.symlink() is a thing with py3 on Windows, but it requires
3383 3386 # Administrator rights.
3384 3387 if getattr(os, 'symlink', None) and os.name != 'nt':
3385 3388 vlog(
3386 3389 "# Making python executable in test path a symlink to '%s'"
3387 3390 % sysexecutable
3388 3391 )
3389 3392 mypython = os.path.join(self._tmpbindir, pyexename)
3390 3393 try:
3391 3394 if os.readlink(mypython) == sysexecutable:
3392 3395 return
3393 3396 os.unlink(mypython)
3394 3397 except OSError as err:
3395 3398 if err.errno != errno.ENOENT:
3396 3399 raise
3397 3400 if self._findprogram(pyexename) != sysexecutable:
3398 3401 try:
3399 3402 os.symlink(sysexecutable, mypython)
3400 3403 self._createdfiles.append(mypython)
3401 3404 except OSError as err:
3402 3405 # child processes may race, which is harmless
3403 3406 if err.errno != errno.EEXIST:
3404 3407 raise
3405 3408 else:
3406 3409 exedir, exename = os.path.split(sysexecutable)
3407 3410 vlog(
3408 3411 "# Modifying search path to find %s as %s in '%s'"
3409 3412 % (exename, pyexename, exedir)
3410 3413 )
3411 3414 path = os.environ['PATH'].split(os.pathsep)
3412 3415 while exedir in path:
3413 3416 path.remove(exedir)
3414 3417 os.environ['PATH'] = os.pathsep.join([exedir] + path)
3415 3418 if not self._findprogram(pyexename):
3416 3419 print("WARNING: Cannot find %s in search path" % pyexename)
3417 3420
3418 3421 def _installhg(self):
3419 3422 """Install hg into the test environment.
3420 3423
3421 3424 This will also configure hg with the appropriate testing settings.
3422 3425 """
3423 3426 vlog("# Performing temporary installation of HG")
3424 3427 installerrs = os.path.join(self._hgtmp, b"install.err")
3425 3428 compiler = ''
3426 3429 if self.options.compiler:
3427 3430 compiler = '--compiler ' + self.options.compiler
3428 3431 if self.options.pure:
3429 3432 pure = b"--pure"
3430 3433 else:
3431 3434 pure = b""
3432 3435
3433 3436 # Run installer in hg root
3434 3437 script = os.path.realpath(sys.argv[0])
3435 3438 exe = sysexecutable
3436 3439 if PYTHON3:
3437 compiler = _bytespath(compiler)
3438 script = _bytespath(script)
3439 exe = _bytespath(exe)
3440 compiler = _sys2bytes(compiler)
3441 script = _sys2bytes(script)
3442 exe = _sys2bytes(exe)
3440 3443 hgroot = os.path.dirname(os.path.dirname(script))
3441 3444 self._hgroot = hgroot
3442 3445 os.chdir(hgroot)
3443 3446 nohome = b'--home=""'
3444 3447 if os.name == 'nt':
3445 3448 # The --home="" trick works only on OS where os.sep == '/'
3446 3449 # because of a distutils convert_path() fast-path. Avoid it at
3447 3450 # least on Windows for now, deal with .pydistutils.cfg bugs
3448 3451 # when they happen.
3449 3452 nohome = b''
3450 3453 cmd = (
3451 3454 b'"%(exe)s" setup.py %(pure)s clean --all'
3452 3455 b' build %(compiler)s --build-base="%(base)s"'
3453 3456 b' install --force --prefix="%(prefix)s"'
3454 3457 b' --install-lib="%(libdir)s"'
3455 3458 b' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
3456 3459 % {
3457 3460 b'exe': exe,
3458 3461 b'pure': pure,
3459 3462 b'compiler': compiler,
3460 3463 b'base': os.path.join(self._hgtmp, b"build"),
3461 3464 b'prefix': self._installdir,
3462 3465 b'libdir': self._pythondir,
3463 3466 b'bindir': self._bindir,
3464 3467 b'nohome': nohome,
3465 3468 b'logfile': installerrs,
3466 3469 }
3467 3470 )
3468 3471
3469 3472 # setuptools requires install directories to exist.
3470 3473 def makedirs(p):
3471 3474 try:
3472 3475 os.makedirs(p)
3473 3476 except OSError as e:
3474 3477 if e.errno != errno.EEXIST:
3475 3478 raise
3476 3479
3477 3480 makedirs(self._pythondir)
3478 3481 makedirs(self._bindir)
3479 3482
3480 3483 vlog("# Running", cmd.decode("utf-8"))
3481 if subprocess.call(_strpath(cmd), shell=True) == 0:
3484 if subprocess.call(_bytes2sys(cmd), shell=True) == 0:
3482 3485 if not self.options.verbose:
3483 3486 try:
3484 3487 os.remove(installerrs)
3485 3488 except OSError as e:
3486 3489 if e.errno != errno.ENOENT:
3487 3490 raise
3488 3491 else:
3489 3492 with open(installerrs, 'rb') as f:
3490 3493 for line in f:
3491 3494 if PYTHON3:
3492 3495 sys.stdout.buffer.write(line)
3493 3496 else:
3494 3497 sys.stdout.write(line)
3495 3498 sys.exit(1)
3496 3499 os.chdir(self._testdir)
3497 3500
3498 3501 self._usecorrectpython()
3499 3502
3500 3503 hgbat = os.path.join(self._bindir, b'hg.bat')
3501 3504 if os.path.isfile(hgbat):
3502 3505 # hg.bat expects to be put in bin/scripts while run-tests.py
3503 3506 # installation layout put it in bin/ directly. Fix it
3504 3507 with open(hgbat, 'rb') as f:
3505 3508 data = f.read()
3506 3509 if br'"%~dp0..\python" "%~dp0hg" %*' in data:
3507 3510 data = data.replace(
3508 3511 br'"%~dp0..\python" "%~dp0hg" %*',
3509 3512 b'"%~dp0python" "%~dp0hg" %*',
3510 3513 )
3511 3514 with open(hgbat, 'wb') as f:
3512 3515 f.write(data)
3513 3516 else:
3514 3517 print('WARNING: cannot fix hg.bat reference to python.exe')
3515 3518
3516 3519 if self.options.anycoverage:
3517 3520 custom = os.path.join(
3518 3521 osenvironb[b'RUNTESTDIR'], b'sitecustomize.py'
3519 3522 )
3520 3523 target = os.path.join(self._pythondir, b'sitecustomize.py')
3521 3524 vlog('# Installing coverage trigger to %s' % target)
3522 3525 shutil.copyfile(custom, target)
3523 3526 rc = os.path.join(self._testdir, b'.coveragerc')
3524 3527 vlog('# Installing coverage rc to %s' % rc)
3525 3528 osenvironb[b'COVERAGE_PROCESS_START'] = rc
3526 3529 covdir = os.path.join(self._installdir, b'..', b'coverage')
3527 3530 try:
3528 3531 os.mkdir(covdir)
3529 3532 except OSError as e:
3530 3533 if e.errno != errno.EEXIST:
3531 3534 raise
3532 3535
3533 3536 osenvironb[b'COVERAGE_DIR'] = covdir
3534 3537
3535 3538 def _checkhglib(self, verb):
3536 3539 """Ensure that the 'mercurial' package imported by python is
3537 3540 the one we expect it to be. If not, print a warning to stderr."""
3538 3541 if (self._bindir == self._pythondir) and (
3539 3542 self._bindir != self._tmpbindir
3540 3543 ):
3541 3544 # The pythondir has been inferred from --with-hg flag.
3542 3545 # We cannot expect anything sensible here.
3543 3546 return
3544 3547 expecthg = os.path.join(self._pythondir, b'mercurial')
3545 3548 actualhg = self._gethgpath()
3546 3549 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
3547 3550 sys.stderr.write(
3548 3551 'warning: %s with unexpected mercurial lib: %s\n'
3549 3552 ' (expected %s)\n' % (verb, actualhg, expecthg)
3550 3553 )
3551 3554
3552 3555 def _gethgpath(self):
3553 3556 """Return the path to the mercurial package that is actually found by
3554 3557 the current Python interpreter."""
3555 3558 if self._hgpath is not None:
3556 3559 return self._hgpath
3557 3560
3558 3561 cmd = b'"%s" -c "import mercurial; print (mercurial.__path__[0])"'
3559 3562 cmd = cmd % PYTHON
3560 3563 if PYTHON3:
3561 cmd = _strpath(cmd)
3564 cmd = _bytes2sys(cmd)
3562 3565
3563 3566 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
3564 3567 out, err = p.communicate()
3565 3568
3566 3569 self._hgpath = out.strip()
3567 3570
3568 3571 return self._hgpath
3569 3572
3570 3573 def _installchg(self):
3571 3574 """Install chg into the test environment"""
3572 3575 vlog('# Performing temporary installation of CHG')
3573 3576 assert os.path.dirname(self._bindir) == self._installdir
3574 3577 assert self._hgroot, 'must be called after _installhg()'
3575 3578 cmd = b'"%(make)s" clean install PREFIX="%(prefix)s"' % {
3576 3579 b'make': b'make', # TODO: switch by option or environment?
3577 3580 b'prefix': self._installdir,
3578 3581 }
3579 3582 cwd = os.path.join(self._hgroot, b'contrib', b'chg')
3580 3583 vlog("# Running", cmd)
3581 3584 proc = subprocess.Popen(
3582 3585 cmd,
3583 3586 shell=True,
3584 3587 cwd=cwd,
3585 3588 stdin=subprocess.PIPE,
3586 3589 stdout=subprocess.PIPE,
3587 3590 stderr=subprocess.STDOUT,
3588 3591 )
3589 3592 out, _err = proc.communicate()
3590 3593 if proc.returncode != 0:
3591 3594 if PYTHON3:
3592 3595 sys.stdout.buffer.write(out)
3593 3596 else:
3594 3597 sys.stdout.write(out)
3595 3598 sys.exit(1)
3596 3599
3597 3600 def _outputcoverage(self):
3598 3601 """Produce code coverage output."""
3599 3602 import coverage
3600 3603
3601 3604 coverage = coverage.coverage
3602 3605
3603 3606 vlog('# Producing coverage report')
3604 3607 # chdir is the easiest way to get short, relative paths in the
3605 3608 # output.
3606 3609 os.chdir(self._hgroot)
3607 covdir = os.path.join(_strpath(self._installdir), '..', 'coverage')
3610 covdir = os.path.join(_bytes2sys(self._installdir), '..', 'coverage')
3608 3611 cov = coverage(data_file=os.path.join(covdir, 'cov'))
3609 3612
3610 3613 # Map install directory paths back to source directory.
3611 cov.config.paths['srcdir'] = ['.', _strpath(self._pythondir)]
3614 cov.config.paths['srcdir'] = ['.', _bytes2sys(self._pythondir)]
3612 3615
3613 3616 cov.combine()
3614 3617
3615 3618 omit = [
3616 _strpath(os.path.join(x, b'*'))
3619 _bytes2sys(os.path.join(x, b'*'))
3617 3620 for x in [self._bindir, self._testdir]
3618 3621 ]
3619 3622 cov.report(ignore_errors=True, omit=omit)
3620 3623
3621 3624 if self.options.htmlcov:
3622 htmldir = os.path.join(_strpath(self._outputdir), 'htmlcov')
3625 htmldir = os.path.join(_bytes2sys(self._outputdir), 'htmlcov')
3623 3626 cov.html_report(directory=htmldir, omit=omit)
3624 3627 if self.options.annotate:
3625 adir = os.path.join(_strpath(self._outputdir), 'annotated')
3628 adir = os.path.join(_bytes2sys(self._outputdir), 'annotated')
3626 3629 if not os.path.isdir(adir):
3627 3630 os.mkdir(adir)
3628 3631 cov.annotate(directory=adir, omit=omit)
3629 3632
3630 3633 def _findprogram(self, program):
3631 3634 """Search PATH for a executable program"""
3632 dpb = _bytespath(os.defpath)
3633 sepb = _bytespath(os.pathsep)
3635 dpb = _sys2bytes(os.defpath)
3636 sepb = _sys2bytes(os.pathsep)
3634 3637 for p in osenvironb.get(b'PATH', dpb).split(sepb):
3635 3638 name = os.path.join(p, program)
3636 3639 if os.name == 'nt' or os.access(name, os.X_OK):
3637 3640 return name
3638 3641 return None
3639 3642
3640 3643 def _checktools(self):
3641 3644 """Ensure tools required to run tests are present."""
3642 3645 for p in self.REQUIREDTOOLS:
3643 3646 if os.name == 'nt' and not p.endswith(b'.exe'):
3644 3647 p += b'.exe'
3645 3648 found = self._findprogram(p)
3646 3649 p = p.decode("utf-8")
3647 3650 if found:
3648 vlog("# Found prerequisite", p, "at", _strpath(found))
3651 vlog("# Found prerequisite", p, "at", _bytes2sys(found))
3649 3652 else:
3650 3653 print("WARNING: Did not find prerequisite tool: %s " % p)
3651 3654
3652 3655
3653 3656 def aggregateexceptions(path):
3654 3657 exceptioncounts = collections.Counter()
3655 3658 testsbyfailure = collections.defaultdict(set)
3656 3659 failuresbytest = collections.defaultdict(set)
3657 3660
3658 3661 for f in os.listdir(path):
3659 3662 with open(os.path.join(path, f), 'rb') as fh:
3660 3663 data = fh.read().split(b'\0')
3661 3664 if len(data) != 5:
3662 3665 continue
3663 3666
3664 3667 exc, mainframe, hgframe, hgline, testname = data
3665 3668 exc = exc.decode('utf-8')
3666 3669 mainframe = mainframe.decode('utf-8')
3667 3670 hgframe = hgframe.decode('utf-8')
3668 3671 hgline = hgline.decode('utf-8')
3669 3672 testname = testname.decode('utf-8')
3670 3673
3671 3674 key = (hgframe, hgline, exc)
3672 3675 exceptioncounts[key] += 1
3673 3676 testsbyfailure[key].add(testname)
3674 3677 failuresbytest[testname].add(key)
3675 3678
3676 3679 # Find test having fewest failures for each failure.
3677 3680 leastfailing = {}
3678 3681 for key, tests in testsbyfailure.items():
3679 3682 fewesttest = None
3680 3683 fewestcount = 99999999
3681 3684 for test in sorted(tests):
3682 3685 if len(failuresbytest[test]) < fewestcount:
3683 3686 fewesttest = test
3684 3687 fewestcount = len(failuresbytest[test])
3685 3688
3686 3689 leastfailing[key] = (fewestcount, fewesttest)
3687 3690
3688 3691 # Create a combined counter so we can sort by total occurrences and
3689 3692 # impacted tests.
3690 3693 combined = {}
3691 3694 for key in exceptioncounts:
3692 3695 combined[key] = (
3693 3696 exceptioncounts[key],
3694 3697 len(testsbyfailure[key]),
3695 3698 leastfailing[key][0],
3696 3699 leastfailing[key][1],
3697 3700 )
3698 3701
3699 3702 return {
3700 3703 'exceptioncounts': exceptioncounts,
3701 3704 'total': sum(exceptioncounts.values()),
3702 3705 'combined': combined,
3703 3706 'leastfailing': leastfailing,
3704 3707 'byfailure': testsbyfailure,
3705 3708 'bytest': failuresbytest,
3706 3709 }
3707 3710
3708 3711
3709 3712 if __name__ == '__main__':
3710 3713 runner = TestRunner()
3711 3714
3712 3715 try:
3713 3716 import msvcrt
3714 3717
3715 3718 msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
3716 3719 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
3717 3720 msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
3718 3721 except ImportError:
3719 3722 pass
3720 3723
3721 3724 sys.exit(runner.run(sys.argv[1:]))
General Comments 0
You need to be logged in to leave comments. Login now