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