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