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