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