##// END OF EJS Templates
tests: use pyflakes as a tool, not a python module...
marmoute -
r52956:43602c67 default
parent child Browse files
Show More
@@ -1,1179 +1,1172
1 import os
1 import os
2 import re
2 import re
3 import socket
3 import socket
4 import stat
4 import stat
5 import subprocess
5 import subprocess
6 import sys
6 import sys
7 import tempfile
7 import tempfile
8
8
9 try:
9 try:
10 from setuptools.extern.packaging.version import Version
10 from setuptools.extern.packaging.version import Version
11 except ImportError:
11 except ImportError:
12 from distutils.version import StrictVersion as Version
12 from distutils.version import StrictVersion as Version
13
13
14 tempprefix = 'hg-hghave-'
14 tempprefix = 'hg-hghave-'
15
15
16 checks = {
16 checks = {
17 "true": (lambda: True, "yak shaving"),
17 "true": (lambda: True, "yak shaving"),
18 "false": (lambda: False, "nail clipper"),
18 "false": (lambda: False, "nail clipper"),
19 "known-bad-output": (lambda: True, "use for currently known bad output"),
19 "known-bad-output": (lambda: True, "use for currently known bad output"),
20 "missing-correct-output": (lambda: False, "use for missing good output"),
20 "missing-correct-output": (lambda: False, "use for missing good output"),
21 }
21 }
22
22
23 try:
23 try:
24 import msvcrt
24 import msvcrt
25
25
26 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
26 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
27 msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
27 msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
28 except ImportError:
28 except ImportError:
29 pass
29 pass
30
30
31 stdout = getattr(sys.stdout, 'buffer', sys.stdout)
31 stdout = getattr(sys.stdout, 'buffer', sys.stdout)
32 stderr = getattr(sys.stderr, 'buffer', sys.stderr)
32 stderr = getattr(sys.stderr, 'buffer', sys.stderr)
33
33
34
34
35 def _sys2bytes(p):
35 def _sys2bytes(p):
36 if p is None:
36 if p is None:
37 return p
37 return p
38 return p.encode('utf-8')
38 return p.encode('utf-8')
39
39
40
40
41 def _bytes2sys(p):
41 def _bytes2sys(p):
42 if p is None:
42 if p is None:
43 return p
43 return p
44 return p.decode('utf-8')
44 return p.decode('utf-8')
45
45
46
46
47 def check(name, desc):
47 def check(name, desc):
48 """Registers a check function for a feature."""
48 """Registers a check function for a feature."""
49
49
50 def decorator(func):
50 def decorator(func):
51 checks[name] = (func, desc)
51 checks[name] = (func, desc)
52 return func
52 return func
53
53
54 return decorator
54 return decorator
55
55
56
56
57 def checkvers(name, desc, vers):
57 def checkvers(name, desc, vers):
58 """Registers a check function for each of a series of versions.
58 """Registers a check function for each of a series of versions.
59
59
60 vers can be a list or an iterator.
60 vers can be a list or an iterator.
61
61
62 Produces a series of feature checks that have the form <name><vers> without
62 Produces a series of feature checks that have the form <name><vers> without
63 any punctuation (even if there's punctuation in 'vers'; i.e. this produces
63 any punctuation (even if there's punctuation in 'vers'; i.e. this produces
64 'py38', not 'py3.8' or 'py-38')."""
64 'py38', not 'py3.8' or 'py-38')."""
65
65
66 def decorator(func):
66 def decorator(func):
67 def funcv(v):
67 def funcv(v):
68 def f():
68 def f():
69 return func(v)
69 return func(v)
70
70
71 return f
71 return f
72
72
73 for v in vers:
73 for v in vers:
74 assert isinstance(v, str)
74 assert isinstance(v, str)
75 f = funcv(v)
75 f = funcv(v)
76 checks['%s%s' % (name, v.replace('.', ''))] = (f, desc % v)
76 checks['%s%s' % (name, v.replace('.', ''))] = (f, desc % v)
77 return func
77 return func
78
78
79 return decorator
79 return decorator
80
80
81
81
82 def checkfeatures(features):
82 def checkfeatures(features):
83 result = {
83 result = {
84 'error': [],
84 'error': [],
85 'missing': [],
85 'missing': [],
86 'skipped': [],
86 'skipped': [],
87 }
87 }
88
88
89 for feature in features:
89 for feature in features:
90 negate = feature.startswith('no-')
90 negate = feature.startswith('no-')
91 if negate:
91 if negate:
92 feature = feature[3:]
92 feature = feature[3:]
93
93
94 if feature not in checks:
94 if feature not in checks:
95 result['missing'].append(feature)
95 result['missing'].append(feature)
96 continue
96 continue
97
97
98 check, desc = checks[feature]
98 check, desc = checks[feature]
99 try:
99 try:
100 available = check()
100 available = check()
101 except Exception as e:
101 except Exception as e:
102 result['error'].append('hghave check %s failed: %r' % (feature, e))
102 result['error'].append('hghave check %s failed: %r' % (feature, e))
103 continue
103 continue
104
104
105 if not negate and not available:
105 if not negate and not available:
106 result['skipped'].append('missing feature: %s' % desc)
106 result['skipped'].append('missing feature: %s' % desc)
107 elif negate and available:
107 elif negate and available:
108 result['skipped'].append('system supports %s' % desc)
108 result['skipped'].append('system supports %s' % desc)
109
109
110 return result
110 return result
111
111
112
112
113 def require(features):
113 def require(features):
114 """Require that features are available, exiting if not."""
114 """Require that features are available, exiting if not."""
115 result = checkfeatures(features)
115 result = checkfeatures(features)
116
116
117 for missing in result['missing']:
117 for missing in result['missing']:
118 stderr.write(
118 stderr.write(
119 ('skipped: unknown feature: %s\n' % missing).encode('utf-8')
119 ('skipped: unknown feature: %s\n' % missing).encode('utf-8')
120 )
120 )
121 for msg in result['skipped']:
121 for msg in result['skipped']:
122 stderr.write(('skipped: %s\n' % msg).encode('utf-8'))
122 stderr.write(('skipped: %s\n' % msg).encode('utf-8'))
123 for msg in result['error']:
123 for msg in result['error']:
124 stderr.write(('%s\n' % msg).encode('utf-8'))
124 stderr.write(('%s\n' % msg).encode('utf-8'))
125
125
126 if result['missing']:
126 if result['missing']:
127 sys.exit(2)
127 sys.exit(2)
128
128
129 if result['skipped'] or result['error']:
129 if result['skipped'] or result['error']:
130 sys.exit(1)
130 sys.exit(1)
131
131
132
132
133 def matchoutput(cmd, regexp, ignorestatus=False):
133 def matchoutput(cmd, regexp, ignorestatus=False):
134 """Return the match object if cmd executes successfully and its output
134 """Return the match object if cmd executes successfully and its output
135 is matched by the supplied regular expression.
135 is matched by the supplied regular expression.
136 """
136 """
137
137
138 # Tests on Windows have to fake USERPROFILE to point to the test area so
138 # Tests on Windows have to fake USERPROFILE to point to the test area so
139 # that `~` is properly expanded on py3.8+. However, some tools like black
139 # that `~` is properly expanded on py3.8+. However, some tools like black
140 # make calls that need the real USERPROFILE in order to run `foo --version`.
140 # make calls that need the real USERPROFILE in order to run `foo --version`.
141 env = os.environ
141 env = os.environ
142 if os.name == 'nt':
142 if os.name == 'nt':
143 env = os.environ.copy()
143 env = os.environ.copy()
144 env['USERPROFILE'] = env['REALUSERPROFILE']
144 env['USERPROFILE'] = env['REALUSERPROFILE']
145
145
146 r = re.compile(regexp)
146 r = re.compile(regexp)
147 p = subprocess.Popen(
147 p = subprocess.Popen(
148 cmd,
148 cmd,
149 shell=True,
149 shell=True,
150 stdout=subprocess.PIPE,
150 stdout=subprocess.PIPE,
151 stderr=subprocess.STDOUT,
151 stderr=subprocess.STDOUT,
152 env=env,
152 env=env,
153 )
153 )
154 s = p.communicate()[0]
154 s = p.communicate()[0]
155 ret = p.returncode
155 ret = p.returncode
156 return (ignorestatus or not ret) and r.search(s)
156 return (ignorestatus or not ret) and r.search(s)
157
157
158
158
159 @check("baz", "GNU Arch baz client")
159 @check("baz", "GNU Arch baz client")
160 def has_baz():
160 def has_baz():
161 return matchoutput('baz --version 2>&1', br'baz Bazaar version')
161 return matchoutput('baz --version 2>&1', br'baz Bazaar version')
162
162
163
163
164 @check("bzr", "Breezy library and executable version >= 3.1")
164 @check("bzr", "Breezy library and executable version >= 3.1")
165 def has_bzr():
165 def has_bzr():
166 try:
166 try:
167 # Test the Breezy python lib
167 # Test the Breezy python lib
168 import breezy
168 import breezy
169 import breezy.bzr.bzrdir
169 import breezy.bzr.bzrdir
170 import breezy.errors
170 import breezy.errors
171 import breezy.revision
171 import breezy.revision
172 import breezy.revisionspec
172 import breezy.revisionspec
173
173
174 breezy.revisionspec.RevisionSpec
174 breezy.revisionspec.RevisionSpec
175 if breezy.__doc__ is None or breezy.version_info[:2] < (3, 1):
175 if breezy.__doc__ is None or breezy.version_info[:2] < (3, 1):
176 return False
176 return False
177 except (AttributeError, ImportError):
177 except (AttributeError, ImportError):
178 return False
178 return False
179 # Test the executable
179 # Test the executable
180 return matchoutput('brz --version 2>&1', br'Breezy \(brz\) ')
180 return matchoutput('brz --version 2>&1', br'Breezy \(brz\) ')
181
181
182
182
183 @check("chg", "running with chg")
183 @check("chg", "running with chg")
184 def has_chg():
184 def has_chg():
185 return 'CHG_INSTALLED_AS_HG' in os.environ
185 return 'CHG_INSTALLED_AS_HG' in os.environ
186
186
187
187
188 @check("rhg", "running with rhg as 'hg'")
188 @check("rhg", "running with rhg as 'hg'")
189 def has_rhg():
189 def has_rhg():
190 return 'RHG_INSTALLED_AS_HG' in os.environ
190 return 'RHG_INSTALLED_AS_HG' in os.environ
191
191
192
192
193 @check("pyoxidizer", "running with pyoxidizer build as 'hg'")
193 @check("pyoxidizer", "running with pyoxidizer build as 'hg'")
194 def has_pyoxidizer():
194 def has_pyoxidizer():
195 return 'PYOXIDIZED_INSTALLED_AS_HG' in os.environ
195 return 'PYOXIDIZED_INSTALLED_AS_HG' in os.environ
196
196
197
197
198 @check(
198 @check(
199 "pyoxidizer-in-memory",
199 "pyoxidizer-in-memory",
200 "running with pyoxidizer build as 'hg' with embedded resources",
200 "running with pyoxidizer build as 'hg' with embedded resources",
201 )
201 )
202 def has_pyoxidizer_mem():
202 def has_pyoxidizer_mem():
203 return 'PYOXIDIZED_IN_MEMORY_RSRC' in os.environ
203 return 'PYOXIDIZED_IN_MEMORY_RSRC' in os.environ
204
204
205
205
206 @check(
206 @check(
207 "pyoxidizer-in-filesystem",
207 "pyoxidizer-in-filesystem",
208 "running with pyoxidizer build as 'hg' with external resources",
208 "running with pyoxidizer build as 'hg' with external resources",
209 )
209 )
210 def has_pyoxidizer_fs():
210 def has_pyoxidizer_fs():
211 return 'PYOXIDIZED_FILESYSTEM_RSRC' in os.environ
211 return 'PYOXIDIZED_FILESYSTEM_RSRC' in os.environ
212
212
213
213
214 @check("cvs", "cvs client/server")
214 @check("cvs", "cvs client/server")
215 def has_cvs():
215 def has_cvs():
216 re = br'Concurrent Versions System.*?server'
216 re = br'Concurrent Versions System.*?server'
217 return matchoutput('cvs --version 2>&1', re) and not has_msys()
217 return matchoutput('cvs --version 2>&1', re) and not has_msys()
218
218
219
219
220 @check("cvs112", "cvs client/server 1.12.* (not cvsnt)")
220 @check("cvs112", "cvs client/server 1.12.* (not cvsnt)")
221 def has_cvs112():
221 def has_cvs112():
222 re = br'Concurrent Versions System \(CVS\) 1.12.*?server'
222 re = br'Concurrent Versions System \(CVS\) 1.12.*?server'
223 return matchoutput('cvs --version 2>&1', re) and not has_msys()
223 return matchoutput('cvs --version 2>&1', re) and not has_msys()
224
224
225
225
226 @check("cvsnt", "cvsnt client/server")
226 @check("cvsnt", "cvsnt client/server")
227 def has_cvsnt():
227 def has_cvsnt():
228 re = br'Concurrent Versions System \(CVSNT\) (\d+).(\d+).*\(client/server\)'
228 re = br'Concurrent Versions System \(CVSNT\) (\d+).(\d+).*\(client/server\)'
229 return matchoutput('cvsnt --version 2>&1', re)
229 return matchoutput('cvsnt --version 2>&1', re)
230
230
231
231
232 @check("darcs", "darcs client")
232 @check("darcs", "darcs client")
233 def has_darcs():
233 def has_darcs():
234 return matchoutput('darcs --version', br'\b2\.([2-9]|\d{2})', True)
234 return matchoutput('darcs --version', br'\b2\.([2-9]|\d{2})', True)
235
235
236
236
237 @check("mtn", "monotone client (>= 1.0)")
237 @check("mtn", "monotone client (>= 1.0)")
238 def has_mtn():
238 def has_mtn():
239 return matchoutput('mtn --version', br'monotone', True) and not matchoutput(
239 return matchoutput('mtn --version', br'monotone', True) and not matchoutput(
240 'mtn --version', br'monotone 0\.', True
240 'mtn --version', br'monotone 0\.', True
241 )
241 )
242
242
243
243
244 @check("eol-in-paths", "end-of-lines in paths")
244 @check("eol-in-paths", "end-of-lines in paths")
245 def has_eol_in_paths():
245 def has_eol_in_paths():
246 try:
246 try:
247 fd, path = tempfile.mkstemp(dir='.', prefix=tempprefix, suffix='\n\r')
247 fd, path = tempfile.mkstemp(dir='.', prefix=tempprefix, suffix='\n\r')
248 os.close(fd)
248 os.close(fd)
249 os.remove(path)
249 os.remove(path)
250 return True
250 return True
251 except (IOError, OSError):
251 except (IOError, OSError):
252 return False
252 return False
253
253
254
254
255 @check("execbit", "executable bit")
255 @check("execbit", "executable bit")
256 def has_executablebit():
256 def has_executablebit():
257 try:
257 try:
258 EXECFLAGS = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
258 EXECFLAGS = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
259 fh, fn = tempfile.mkstemp(dir='.', prefix=tempprefix)
259 fh, fn = tempfile.mkstemp(dir='.', prefix=tempprefix)
260 try:
260 try:
261 os.close(fh)
261 os.close(fh)
262 m = os.stat(fn).st_mode & 0o777
262 m = os.stat(fn).st_mode & 0o777
263 new_file_has_exec = m & EXECFLAGS
263 new_file_has_exec = m & EXECFLAGS
264 os.chmod(fn, m ^ EXECFLAGS)
264 os.chmod(fn, m ^ EXECFLAGS)
265 exec_flags_cannot_flip = (os.stat(fn).st_mode & 0o777) == m
265 exec_flags_cannot_flip = (os.stat(fn).st_mode & 0o777) == m
266 finally:
266 finally:
267 os.unlink(fn)
267 os.unlink(fn)
268 except (IOError, OSError):
268 except (IOError, OSError):
269 # we don't care, the user probably won't be able to commit anyway
269 # we don't care, the user probably won't be able to commit anyway
270 return False
270 return False
271 return not (new_file_has_exec or exec_flags_cannot_flip)
271 return not (new_file_has_exec or exec_flags_cannot_flip)
272
272
273
273
274 @check("suidbit", "setuid and setgid bit")
274 @check("suidbit", "setuid and setgid bit")
275 def has_suidbit():
275 def has_suidbit():
276 if (
276 if (
277 getattr(os, "statvfs", None) is None
277 getattr(os, "statvfs", None) is None
278 or getattr(os, "ST_NOSUID", None) is None
278 or getattr(os, "ST_NOSUID", None) is None
279 ):
279 ):
280 return False
280 return False
281 return bool(os.statvfs('.').f_flag & os.ST_NOSUID)
281 return bool(os.statvfs('.').f_flag & os.ST_NOSUID)
282
282
283
283
284 @check("icasefs", "case insensitive file system")
284 @check("icasefs", "case insensitive file system")
285 def has_icasefs():
285 def has_icasefs():
286 # Stolen from mercurial.util
286 # Stolen from mercurial.util
287 fd, path = tempfile.mkstemp(dir='.', prefix=tempprefix)
287 fd, path = tempfile.mkstemp(dir='.', prefix=tempprefix)
288 os.close(fd)
288 os.close(fd)
289 try:
289 try:
290 s1 = os.stat(path)
290 s1 = os.stat(path)
291 d, b = os.path.split(path)
291 d, b = os.path.split(path)
292 p2 = os.path.join(d, b.upper())
292 p2 = os.path.join(d, b.upper())
293 if path == p2:
293 if path == p2:
294 p2 = os.path.join(d, b.lower())
294 p2 = os.path.join(d, b.lower())
295 try:
295 try:
296 s2 = os.stat(p2)
296 s2 = os.stat(p2)
297 return s2 == s1
297 return s2 == s1
298 except OSError:
298 except OSError:
299 return False
299 return False
300 finally:
300 finally:
301 os.remove(path)
301 os.remove(path)
302
302
303
303
304 @check("fifo", "named pipes")
304 @check("fifo", "named pipes")
305 def has_fifo():
305 def has_fifo():
306 if getattr(os, "mkfifo", None) is None:
306 if getattr(os, "mkfifo", None) is None:
307 return False
307 return False
308 name = tempfile.mktemp(dir='.', prefix=tempprefix)
308 name = tempfile.mktemp(dir='.', prefix=tempprefix)
309 try:
309 try:
310 os.mkfifo(name)
310 os.mkfifo(name)
311 os.unlink(name)
311 os.unlink(name)
312 return True
312 return True
313 except OSError:
313 except OSError:
314 return False
314 return False
315
315
316
316
317 @check("killdaemons", 'killdaemons.py support')
317 @check("killdaemons", 'killdaemons.py support')
318 def has_killdaemons():
318 def has_killdaemons():
319 return True
319 return True
320
320
321
321
322 @check("cacheable", "cacheable filesystem")
322 @check("cacheable", "cacheable filesystem")
323 def has_cacheable_fs():
323 def has_cacheable_fs():
324 from mercurial import util
324 from mercurial import util
325
325
326 fd, path = tempfile.mkstemp(dir='.', prefix=tempprefix)
326 fd, path = tempfile.mkstemp(dir='.', prefix=tempprefix)
327 os.close(fd)
327 os.close(fd)
328 try:
328 try:
329 return util.cachestat(_sys2bytes(path)).cacheable()
329 return util.cachestat(_sys2bytes(path)).cacheable()
330 finally:
330 finally:
331 os.remove(path)
331 os.remove(path)
332
332
333
333
334 @check("lsprof", "python lsprof module")
334 @check("lsprof", "python lsprof module")
335 def has_lsprof():
335 def has_lsprof():
336 try:
336 try:
337 import _lsprof
337 import _lsprof
338
338
339 _lsprof.Profiler # silence unused import warning
339 _lsprof.Profiler # silence unused import warning
340 return True
340 return True
341 except ImportError:
341 except ImportError:
342 return False
342 return False
343
343
344
344
345 def _gethgversion():
345 def _gethgversion():
346 m = matchoutput('hg --version --quiet 2>&1', br'(\d+)\.(\d+)')
346 m = matchoutput('hg --version --quiet 2>&1', br'(\d+)\.(\d+)')
347 if not m:
347 if not m:
348 return (0, 0)
348 return (0, 0)
349 return (int(m.group(1)), int(m.group(2)))
349 return (int(m.group(1)), int(m.group(2)))
350
350
351
351
352 _hgversion = None
352 _hgversion = None
353
353
354
354
355 def gethgversion():
355 def gethgversion():
356 global _hgversion
356 global _hgversion
357 if _hgversion is None:
357 if _hgversion is None:
358 _hgversion = _gethgversion()
358 _hgversion = _gethgversion()
359 return _hgversion
359 return _hgversion
360
360
361
361
362 @checkvers(
362 @checkvers(
363 "hg", "Mercurial >= %s", ['%d.%d' % divmod(x, 10) for x in range(9, 99)]
363 "hg", "Mercurial >= %s", ['%d.%d' % divmod(x, 10) for x in range(9, 99)]
364 )
364 )
365 def has_hg_range(v):
365 def has_hg_range(v):
366 major, minor = v.split('.')[0:2]
366 major, minor = v.split('.')[0:2]
367 return gethgversion() >= (int(major), int(minor))
367 return gethgversion() >= (int(major), int(minor))
368
368
369
369
370 @check("rust", "Using the Rust extensions")
370 @check("rust", "Using the Rust extensions")
371 def has_rust():
371 def has_rust():
372 """Check is the mercurial currently running is using some rust code"""
372 """Check is the mercurial currently running is using some rust code"""
373 cmd = 'hg debuginstall --quiet 2>&1'
373 cmd = 'hg debuginstall --quiet 2>&1'
374 match = br'checking module policy \(([^)]+)\)'
374 match = br'checking module policy \(([^)]+)\)'
375 policy = matchoutput(cmd, match)
375 policy = matchoutput(cmd, match)
376 if not policy:
376 if not policy:
377 return False
377 return False
378 return b'rust' in policy.group(1)
378 return b'rust' in policy.group(1)
379
379
380
380
381 @check("hg08", "Mercurial >= 0.8")
381 @check("hg08", "Mercurial >= 0.8")
382 def has_hg08():
382 def has_hg08():
383 if checks["hg09"][0]():
383 if checks["hg09"][0]():
384 return True
384 return True
385 return matchoutput('hg help annotate 2>&1', '--date')
385 return matchoutput('hg help annotate 2>&1', '--date')
386
386
387
387
388 @check("hg07", "Mercurial >= 0.7")
388 @check("hg07", "Mercurial >= 0.7")
389 def has_hg07():
389 def has_hg07():
390 if checks["hg08"][0]():
390 if checks["hg08"][0]():
391 return True
391 return True
392 return matchoutput('hg --version --quiet 2>&1', 'Mercurial Distributed SCM')
392 return matchoutput('hg --version --quiet 2>&1', 'Mercurial Distributed SCM')
393
393
394
394
395 @check("hg06", "Mercurial >= 0.6")
395 @check("hg06", "Mercurial >= 0.6")
396 def has_hg06():
396 def has_hg06():
397 if checks["hg07"][0]():
397 if checks["hg07"][0]():
398 return True
398 return True
399 return matchoutput('hg --version --quiet 2>&1', 'Mercurial version')
399 return matchoutput('hg --version --quiet 2>&1', 'Mercurial version')
400
400
401
401
402 @check("gettext", "GNU Gettext (msgfmt)")
402 @check("gettext", "GNU Gettext (msgfmt)")
403 def has_gettext():
403 def has_gettext():
404 return matchoutput('msgfmt --version', br'GNU gettext-tools')
404 return matchoutput('msgfmt --version', br'GNU gettext-tools')
405
405
406
406
407 @check("git", "git command line client")
407 @check("git", "git command line client")
408 def has_git():
408 def has_git():
409 return matchoutput('git --version 2>&1', br'^git version')
409 return matchoutput('git --version 2>&1', br'^git version')
410
410
411
411
412 def getgitversion():
412 def getgitversion():
413 m = matchoutput('git --version 2>&1', br'git version (\d+)\.(\d+)')
413 m = matchoutput('git --version 2>&1', br'git version (\d+)\.(\d+)')
414 if not m:
414 if not m:
415 return (0, 0)
415 return (0, 0)
416 return (int(m.group(1)), int(m.group(2)))
416 return (int(m.group(1)), int(m.group(2)))
417
417
418
418
419 @check("pygit2", "pygit2 Python library")
419 @check("pygit2", "pygit2 Python library")
420 def has_pygit2():
420 def has_pygit2():
421 try:
421 try:
422 import pygit2
422 import pygit2
423
423
424 pygit2.Oid # silence unused import
424 pygit2.Oid # silence unused import
425 return True
425 return True
426 except ImportError:
426 except ImportError:
427 return False
427 return False
428
428
429
429
430 # https://github.com/git-lfs/lfs-test-server
430 # https://github.com/git-lfs/lfs-test-server
431 @check("lfs-test-server", "git-lfs test server")
431 @check("lfs-test-server", "git-lfs test server")
432 def has_lfsserver():
432 def has_lfsserver():
433 exe = 'lfs-test-server'
433 exe = 'lfs-test-server'
434 if has_windows():
434 if has_windows():
435 exe = 'lfs-test-server.exe'
435 exe = 'lfs-test-server.exe'
436 return any(
436 return any(
437 os.access(os.path.join(path, exe), os.X_OK)
437 os.access(os.path.join(path, exe), os.X_OK)
438 for path in os.environ["PATH"].split(os.pathsep)
438 for path in os.environ["PATH"].split(os.pathsep)
439 )
439 )
440
440
441
441
442 @checkvers("git", "git client (with ext::sh support) version >= %s", ('1.9',))
442 @checkvers("git", "git client (with ext::sh support) version >= %s", ('1.9',))
443 def has_git_range(v):
443 def has_git_range(v):
444 major, minor = v.split('.')[0:2]
444 major, minor = v.split('.')[0:2]
445 return getgitversion() >= (int(major), int(minor))
445 return getgitversion() >= (int(major), int(minor))
446
446
447
447
448 @check("docutils", "Docutils text processing library")
448 @check("docutils", "Docutils text processing library")
449 def has_docutils():
449 def has_docutils():
450 try:
450 try:
451 import docutils.core
451 import docutils.core
452
452
453 docutils.core.publish_cmdline # silence unused import
453 docutils.core.publish_cmdline # silence unused import
454 return True
454 return True
455 except ImportError:
455 except ImportError:
456 return False
456 return False
457
457
458
458
459 def getsvnversion():
459 def getsvnversion():
460 m = matchoutput('svn --version --quiet 2>&1', br'^(\d+)\.(\d+)')
460 m = matchoutput('svn --version --quiet 2>&1', br'^(\d+)\.(\d+)')
461 if not m:
461 if not m:
462 return (0, 0)
462 return (0, 0)
463 return (int(m.group(1)), int(m.group(2)))
463 return (int(m.group(1)), int(m.group(2)))
464
464
465
465
466 @checkvers("svn", "subversion client and admin tools >= %s", ('1.3', '1.5'))
466 @checkvers("svn", "subversion client and admin tools >= %s", ('1.3', '1.5'))
467 def has_svn_range(v):
467 def has_svn_range(v):
468 major, minor = v.split('.')[0:2]
468 major, minor = v.split('.')[0:2]
469 return getsvnversion() >= (int(major), int(minor))
469 return getsvnversion() >= (int(major), int(minor))
470
470
471
471
472 @check("svn", "subversion client and admin tools")
472 @check("svn", "subversion client and admin tools")
473 def has_svn():
473 def has_svn():
474 return matchoutput('svn --version 2>&1', br'^svn, version') and matchoutput(
474 return matchoutput('svn --version 2>&1', br'^svn, version') and matchoutput(
475 'svnadmin --version 2>&1', br'^svnadmin, version'
475 'svnadmin --version 2>&1', br'^svnadmin, version'
476 )
476 )
477
477
478
478
479 @check("svn-bindings", "subversion python bindings")
479 @check("svn-bindings", "subversion python bindings")
480 def has_svn_bindings():
480 def has_svn_bindings():
481 try:
481 try:
482 import svn.core
482 import svn.core
483
483
484 version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR
484 version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR
485 if version < (1, 4):
485 if version < (1, 4):
486 return False
486 return False
487 return True
487 return True
488 except ImportError:
488 except ImportError:
489 return False
489 return False
490
490
491
491
492 @check("p4", "Perforce server and client")
492 @check("p4", "Perforce server and client")
493 def has_p4():
493 def has_p4():
494 return matchoutput('p4 -V', br'Rev\. P4/') and matchoutput(
494 return matchoutput('p4 -V', br'Rev\. P4/') and matchoutput(
495 'p4d -V', br'Rev\. P4D/'
495 'p4d -V', br'Rev\. P4D/'
496 )
496 )
497
497
498
498
499 @check("symlink", "symbolic links")
499 @check("symlink", "symbolic links")
500 def has_symlink():
500 def has_symlink():
501 # mercurial.windows.checklink() is a hard 'no' at the moment
501 # mercurial.windows.checklink() is a hard 'no' at the moment
502 if os.name == 'nt' or getattr(os, "symlink", None) is None:
502 if os.name == 'nt' or getattr(os, "symlink", None) is None:
503 return False
503 return False
504 name = tempfile.mktemp(dir='.', prefix=tempprefix)
504 name = tempfile.mktemp(dir='.', prefix=tempprefix)
505 try:
505 try:
506 os.symlink(".", name)
506 os.symlink(".", name)
507 os.unlink(name)
507 os.unlink(name)
508 return True
508 return True
509 except (OSError, AttributeError):
509 except (OSError, AttributeError):
510 return False
510 return False
511
511
512
512
513 @check("hardlink", "hardlinks")
513 @check("hardlink", "hardlinks")
514 def has_hardlink():
514 def has_hardlink():
515 from mercurial import util
515 from mercurial import util
516
516
517 fh, fn = tempfile.mkstemp(dir='.', prefix=tempprefix)
517 fh, fn = tempfile.mkstemp(dir='.', prefix=tempprefix)
518 os.close(fh)
518 os.close(fh)
519 name = tempfile.mktemp(dir='.', prefix=tempprefix)
519 name = tempfile.mktemp(dir='.', prefix=tempprefix)
520 try:
520 try:
521 util.oslink(_sys2bytes(fn), _sys2bytes(name))
521 util.oslink(_sys2bytes(fn), _sys2bytes(name))
522 os.unlink(name)
522 os.unlink(name)
523 return True
523 return True
524 except OSError:
524 except OSError:
525 return False
525 return False
526 finally:
526 finally:
527 os.unlink(fn)
527 os.unlink(fn)
528
528
529
529
530 @check("hardlink-whitelisted", "hardlinks on whitelisted filesystems")
530 @check("hardlink-whitelisted", "hardlinks on whitelisted filesystems")
531 def has_hardlink_whitelisted():
531 def has_hardlink_whitelisted():
532 from mercurial import util
532 from mercurial import util
533
533
534 try:
534 try:
535 fstype = util.getfstype(b'.')
535 fstype = util.getfstype(b'.')
536 except OSError:
536 except OSError:
537 return False
537 return False
538 return fstype in util._hardlinkfswhitelist
538 return fstype in util._hardlinkfswhitelist
539
539
540
540
541 @check("rmcwd", "can remove current working directory")
541 @check("rmcwd", "can remove current working directory")
542 def has_rmcwd():
542 def has_rmcwd():
543 ocwd = os.getcwd()
543 ocwd = os.getcwd()
544 temp = tempfile.mkdtemp(dir='.', prefix=tempprefix)
544 temp = tempfile.mkdtemp(dir='.', prefix=tempprefix)
545 try:
545 try:
546 os.chdir(temp)
546 os.chdir(temp)
547 # On Linux, 'rmdir .' isn't allowed, but the other names are okay.
547 # On Linux, 'rmdir .' isn't allowed, but the other names are okay.
548 # On Solaris and Windows, the cwd can't be removed by any names.
548 # On Solaris and Windows, the cwd can't be removed by any names.
549 os.rmdir(os.getcwd())
549 os.rmdir(os.getcwd())
550 return True
550 return True
551 except OSError:
551 except OSError:
552 return False
552 return False
553 finally:
553 finally:
554 os.chdir(ocwd)
554 os.chdir(ocwd)
555 # clean up temp dir on platforms where cwd can't be removed
555 # clean up temp dir on platforms where cwd can't be removed
556 try:
556 try:
557 os.rmdir(temp)
557 os.rmdir(temp)
558 except OSError:
558 except OSError:
559 pass
559 pass
560
560
561
561
562 @check("tla", "GNU Arch tla client")
562 @check("tla", "GNU Arch tla client")
563 def has_tla():
563 def has_tla():
564 return matchoutput('tla --version 2>&1', br'The GNU Arch Revision')
564 return matchoutput('tla --version 2>&1', br'The GNU Arch Revision')
565
565
566
566
567 @check("gpg", "gpg client")
567 @check("gpg", "gpg client")
568 def has_gpg():
568 def has_gpg():
569 return matchoutput('gpg --version 2>&1', br'GnuPG')
569 return matchoutput('gpg --version 2>&1', br'GnuPG')
570
570
571
571
572 @check("gpg2", "gpg client v2")
572 @check("gpg2", "gpg client v2")
573 def has_gpg2():
573 def has_gpg2():
574 return matchoutput('gpg --version 2>&1', br'GnuPG[^0-9]+2\.')
574 return matchoutput('gpg --version 2>&1', br'GnuPG[^0-9]+2\.')
575
575
576
576
577 @check("gpg21", "gpg client v2.1+")
577 @check("gpg21", "gpg client v2.1+")
578 def has_gpg21():
578 def has_gpg21():
579 return matchoutput('gpg --version 2>&1', br'GnuPG[^0-9]+2\.(?!0)')
579 return matchoutput('gpg --version 2>&1', br'GnuPG[^0-9]+2\.(?!0)')
580
580
581
581
582 @check("unix-permissions", "unix-style permissions")
582 @check("unix-permissions", "unix-style permissions")
583 def has_unix_permissions():
583 def has_unix_permissions():
584 d = tempfile.mkdtemp(dir='.', prefix=tempprefix)
584 d = tempfile.mkdtemp(dir='.', prefix=tempprefix)
585 try:
585 try:
586 fname = os.path.join(d, 'foo')
586 fname = os.path.join(d, 'foo')
587 for umask in (0o77, 0o07, 0o22):
587 for umask in (0o77, 0o07, 0o22):
588 os.umask(umask)
588 os.umask(umask)
589 f = open(fname, 'w')
589 f = open(fname, 'w')
590 f.close()
590 f.close()
591 mode = os.stat(fname).st_mode
591 mode = os.stat(fname).st_mode
592 os.unlink(fname)
592 os.unlink(fname)
593 if mode & 0o777 != ~umask & 0o666:
593 if mode & 0o777 != ~umask & 0o666:
594 return False
594 return False
595 return True
595 return True
596 finally:
596 finally:
597 os.rmdir(d)
597 os.rmdir(d)
598
598
599
599
600 @check("unix-socket", "AF_UNIX socket family")
600 @check("unix-socket", "AF_UNIX socket family")
601 def has_unix_socket():
601 def has_unix_socket():
602 return getattr(socket, 'AF_UNIX', None) is not None
602 return getattr(socket, 'AF_UNIX', None) is not None
603
603
604
604
605 @check("root", "root permissions")
605 @check("root", "root permissions")
606 def has_root():
606 def has_root():
607 return getattr(os, 'geteuid', None) and os.geteuid() == 0
607 return getattr(os, 'geteuid', None) and os.geteuid() == 0
608
608
609
609
610 @check("pyflakes", "Pyflakes python linter")
610 @check("pyflakes", "Pyflakes python linter")
611 def has_pyflakes():
611 def has_pyflakes():
612 try:
612 return matchoutput("pyflakes --version", br"^\d+\.\d+\.\d+\b", True)
613 import pyflakes
614
615 pyflakes.__version__
616 except ImportError:
617 return False
618 else:
619 return True
620
613
621
614
622 @check("pylint", "Pylint python linter")
615 @check("pylint", "Pylint python linter")
623 def has_pylint():
616 def has_pylint():
624 return matchoutput("pylint --help", br"[Uu]sage:[ ]+pylint", True)
617 return matchoutput("pylint --help", br"[Uu]sage:[ ]+pylint", True)
625
618
626
619
627 @check("clang-format", "clang-format C code formatter (11 <= … < 19)")
620 @check("clang-format", "clang-format C code formatter (11 <= … < 19)")
628 def has_clang_format():
621 def has_clang_format():
629 m = matchoutput('clang-format --version', br'clang-format version (\d+)')
622 m = matchoutput('clang-format --version', br'clang-format version (\d+)')
630 # style changed somewhere between 10.x and 11.x and after 19.
623 # style changed somewhere between 10.x and 11.x and after 19.
631 if m:
624 if m:
632 major_version = int(m.group(1))
625 major_version = int(m.group(1))
633 return 11 <= major_version < 19
626 return 11 <= major_version < 19
634 # Assist Googler contributors, they have a centrally-maintained version of
627 # Assist Googler contributors, they have a centrally-maintained version of
635 # clang-format that is generally very fresh, but unlike most builds (both
628 # clang-format that is generally very fresh, but unlike most builds (both
636 # official and unofficial), it does *not* include a version number.
629 # official and unofficial), it does *not* include a version number.
637 return matchoutput(
630 return matchoutput(
638 'clang-format --version', br'clang-format .*google3-trunk \([0-9a-f]+\)'
631 'clang-format --version', br'clang-format .*google3-trunk \([0-9a-f]+\)'
639 )
632 )
640
633
641
634
642 @check("jshint", "JSHint static code analysis tool")
635 @check("jshint", "JSHint static code analysis tool")
643 def has_jshint():
636 def has_jshint():
644 return matchoutput("jshint --version 2>&1", br"jshint v")
637 return matchoutput("jshint --version 2>&1", br"jshint v")
645
638
646
639
647 @check("pygments", "Pygments source highlighting library")
640 @check("pygments", "Pygments source highlighting library")
648 def has_pygments():
641 def has_pygments():
649 try:
642 try:
650 import pygments
643 import pygments
651
644
652 pygments.highlight # silence unused import warning
645 pygments.highlight # silence unused import warning
653 return True
646 return True
654 except ImportError:
647 except ImportError:
655 return False
648 return False
656
649
657
650
658 def getpygmentsversion():
651 def getpygmentsversion():
659 try:
652 try:
660 import pygments
653 import pygments
661
654
662 v = pygments.__version__
655 v = pygments.__version__
663
656
664 parts = v.split(".")
657 parts = v.split(".")
665 return (int(parts[0]), int(parts[1]))
658 return (int(parts[0]), int(parts[1]))
666 except ImportError:
659 except ImportError:
667 return (0, 0)
660 return (0, 0)
668
661
669
662
670 @checkvers("pygments", "Pygments version >= %s", ('2.5', '2.11', '2.14'))
663 @checkvers("pygments", "Pygments version >= %s", ('2.5', '2.11', '2.14'))
671 def has_pygments_range(v):
664 def has_pygments_range(v):
672 major, minor = v.split('.')[0:2]
665 major, minor = v.split('.')[0:2]
673 return getpygmentsversion() >= (int(major), int(minor))
666 return getpygmentsversion() >= (int(major), int(minor))
674
667
675
668
676 @check("outer-repo", "outer repo")
669 @check("outer-repo", "outer repo")
677 def has_outer_repo():
670 def has_outer_repo():
678 # failing for other reasons than 'no repo' imply that there is a repo
671 # failing for other reasons than 'no repo' imply that there is a repo
679 return not matchoutput('hg root 2>&1', br'abort: no repository found', True)
672 return not matchoutput('hg root 2>&1', br'abort: no repository found', True)
680
673
681
674
682 @check("ssl", "ssl module available")
675 @check("ssl", "ssl module available")
683 def has_ssl():
676 def has_ssl():
684 try:
677 try:
685 import ssl
678 import ssl
686
679
687 ssl.CERT_NONE
680 ssl.CERT_NONE
688 return True
681 return True
689 except ImportError:
682 except ImportError:
690 return False
683 return False
691
684
692
685
693 @check("defaultcacertsloaded", "detected presence of loaded system CA certs")
686 @check("defaultcacertsloaded", "detected presence of loaded system CA certs")
694 def has_defaultcacertsloaded():
687 def has_defaultcacertsloaded():
695 import ssl
688 import ssl
696 from mercurial import sslutil, ui as uimod
689 from mercurial import sslutil, ui as uimod
697
690
698 ui = uimod.ui.load()
691 ui = uimod.ui.load()
699 cafile = sslutil._defaultcacerts(ui)
692 cafile = sslutil._defaultcacerts(ui)
700 ctx = ssl.create_default_context()
693 ctx = ssl.create_default_context()
701 if cafile:
694 if cafile:
702 ctx.load_verify_locations(cafile=cafile)
695 ctx.load_verify_locations(cafile=cafile)
703 else:
696 else:
704 ctx.load_default_certs()
697 ctx.load_default_certs()
705
698
706 return len(ctx.get_ca_certs()) > 0
699 return len(ctx.get_ca_certs()) > 0
707
700
708
701
709 @check("tls1.2", "TLS 1.2 protocol support")
702 @check("tls1.2", "TLS 1.2 protocol support")
710 def has_tls1_2():
703 def has_tls1_2():
711 from mercurial import sslutil
704 from mercurial import sslutil
712
705
713 return b'tls1.2' in sslutil.supportedprotocols
706 return b'tls1.2' in sslutil.supportedprotocols
714
707
715
708
716 @check("windows", "Windows")
709 @check("windows", "Windows")
717 def has_windows():
710 def has_windows():
718 return os.name == 'nt'
711 return os.name == 'nt'
719
712
720
713
721 @check("system-sh", "system() uses sh")
714 @check("system-sh", "system() uses sh")
722 def has_system_sh():
715 def has_system_sh():
723 return os.name != 'nt'
716 return os.name != 'nt'
724
717
725
718
726 @check("serve", "platform and python can manage 'hg serve -d'")
719 @check("serve", "platform and python can manage 'hg serve -d'")
727 def has_serve():
720 def has_serve():
728 return True
721 return True
729
722
730
723
731 @check("setprocname", "whether osutil.setprocname is available or not")
724 @check("setprocname", "whether osutil.setprocname is available or not")
732 def has_setprocname():
725 def has_setprocname():
733 try:
726 try:
734 from mercurial.utils import procutil
727 from mercurial.utils import procutil
735
728
736 procutil.setprocname
729 procutil.setprocname
737 return True
730 return True
738 except AttributeError:
731 except AttributeError:
739 return False
732 return False
740
733
741
734
742 @check("gui", "whether a gui environment is available or not")
735 @check("gui", "whether a gui environment is available or not")
743 def has_gui():
736 def has_gui():
744 from mercurial.utils import procutil
737 from mercurial.utils import procutil
745
738
746 return procutil.gui()
739 return procutil.gui()
747
740
748
741
749 @check("test-repo", "running tests from repository")
742 @check("test-repo", "running tests from repository")
750 def has_test_repo():
743 def has_test_repo():
751 t = os.environ["TESTDIR"]
744 t = os.environ["TESTDIR"]
752 return os.path.isdir(os.path.join(t, "..", ".hg"))
745 return os.path.isdir(os.path.join(t, "..", ".hg"))
753
746
754
747
755 @check("network-io", "whether tests are allowed to access 3rd party services")
748 @check("network-io", "whether tests are allowed to access 3rd party services")
756 def has_network_io():
749 def has_network_io():
757 t = os.environ.get("HGTESTS_ALLOW_NETIO")
750 t = os.environ.get("HGTESTS_ALLOW_NETIO")
758 return t == "1"
751 return t == "1"
759
752
760
753
761 @check("curses", "terminfo compiler and curses module")
754 @check("curses", "terminfo compiler and curses module")
762 def has_curses():
755 def has_curses():
763 try:
756 try:
764 import curses
757 import curses
765
758
766 curses.COLOR_BLUE
759 curses.COLOR_BLUE
767
760
768 # Windows doesn't have a `tic` executable, but the windows_curses
761 # Windows doesn't have a `tic` executable, but the windows_curses
769 # package is sufficient to run the tests without it.
762 # package is sufficient to run the tests without it.
770 if os.name == 'nt':
763 if os.name == 'nt':
771 return True
764 return True
772
765
773 return has_tic()
766 return has_tic()
774
767
775 except (ImportError, AttributeError):
768 except (ImportError, AttributeError):
776 return False
769 return False
777
770
778
771
779 @check("tic", "terminfo compiler")
772 @check("tic", "terminfo compiler")
780 def has_tic():
773 def has_tic():
781 return matchoutput('test -x "`which tic`"', br'')
774 return matchoutput('test -x "`which tic`"', br'')
782
775
783
776
784 @check("xz", "xz compression utility")
777 @check("xz", "xz compression utility")
785 def has_xz():
778 def has_xz():
786 # When Windows invokes a subprocess in shell mode, it uses `cmd.exe`, which
779 # When Windows invokes a subprocess in shell mode, it uses `cmd.exe`, which
787 # only knows `where`, not `which`. So invoke MSYS shell explicitly.
780 # only knows `where`, not `which`. So invoke MSYS shell explicitly.
788 return matchoutput("sh -c 'test -x \"`which xz`\"'", b'')
781 return matchoutput("sh -c 'test -x \"`which xz`\"'", b'')
789
782
790
783
791 @check("msys", "Windows with MSYS")
784 @check("msys", "Windows with MSYS")
792 def has_msys():
785 def has_msys():
793 return os.getenv('MSYSTEM')
786 return os.getenv('MSYSTEM')
794
787
795
788
796 @check("aix", "AIX")
789 @check("aix", "AIX")
797 def has_aix():
790 def has_aix():
798 return sys.platform.startswith("aix")
791 return sys.platform.startswith("aix")
799
792
800
793
801 @check("osx", "OS X")
794 @check("osx", "OS X")
802 def has_osx():
795 def has_osx():
803 return sys.platform == 'darwin'
796 return sys.platform == 'darwin'
804
797
805
798
806 @check("osxpackaging", "OS X packaging tools")
799 @check("osxpackaging", "OS X packaging tools")
807 def has_osxpackaging():
800 def has_osxpackaging():
808 try:
801 try:
809 return (
802 return (
810 matchoutput('pkgbuild', br'Usage: pkgbuild ', ignorestatus=1)
803 matchoutput('pkgbuild', br'Usage: pkgbuild ', ignorestatus=1)
811 and matchoutput(
804 and matchoutput(
812 'productbuild', br'Usage: productbuild ', ignorestatus=1
805 'productbuild', br'Usage: productbuild ', ignorestatus=1
813 )
806 )
814 and matchoutput('lsbom', br'Usage: lsbom', ignorestatus=1)
807 and matchoutput('lsbom', br'Usage: lsbom', ignorestatus=1)
815 and matchoutput('xar --help', br'Usage: xar', ignorestatus=1)
808 and matchoutput('xar --help', br'Usage: xar', ignorestatus=1)
816 )
809 )
817 except ImportError:
810 except ImportError:
818 return False
811 return False
819
812
820
813
821 @check('linuxormacos', 'Linux or MacOS')
814 @check('linuxormacos', 'Linux or MacOS')
822 def has_linuxormacos():
815 def has_linuxormacos():
823 # This isn't a perfect test for MacOS. But it is sufficient for our needs.
816 # This isn't a perfect test for MacOS. But it is sufficient for our needs.
824 return sys.platform.startswith(('linux', 'darwin'))
817 return sys.platform.startswith(('linux', 'darwin'))
825
818
826
819
827 @check("docker", "docker support")
820 @check("docker", "docker support")
828 def has_docker():
821 def has_docker():
829 pat = br'A self-sufficient runtime for'
822 pat = br'A self-sufficient runtime for'
830 if matchoutput('docker --help', pat):
823 if matchoutput('docker --help', pat):
831 if 'linux' not in sys.platform:
824 if 'linux' not in sys.platform:
832 # TODO: in theory we should be able to test docker-based
825 # TODO: in theory we should be able to test docker-based
833 # package creation on non-linux using boot2docker, but in
826 # package creation on non-linux using boot2docker, but in
834 # practice that requires extra coordination to make sure
827 # practice that requires extra coordination to make sure
835 # $TESTTEMP is going to be visible at the same path to the
828 # $TESTTEMP is going to be visible at the same path to the
836 # boot2docker VM. If we figure out how to verify that, we
829 # boot2docker VM. If we figure out how to verify that, we
837 # can use the following instead of just saying False:
830 # can use the following instead of just saying False:
838 # return 'DOCKER_HOST' in os.environ
831 # return 'DOCKER_HOST' in os.environ
839 return False
832 return False
840
833
841 return True
834 return True
842 return False
835 return False
843
836
844
837
845 @check("debhelper", "debian packaging tools")
838 @check("debhelper", "debian packaging tools")
846 def has_debhelper():
839 def has_debhelper():
847 # Some versions of dpkg say `dpkg', some say 'dpkg' (` vs ' on the first
840 # Some versions of dpkg say `dpkg', some say 'dpkg' (` vs ' on the first
848 # quote), so just accept anything in that spot.
841 # quote), so just accept anything in that spot.
849 dpkg = matchoutput(
842 dpkg = matchoutput(
850 'dpkg --version', br"Debian .dpkg' package management program"
843 'dpkg --version', br"Debian .dpkg' package management program"
851 )
844 )
852 dh = matchoutput(
845 dh = matchoutput(
853 'dh --help', br'dh is a part of debhelper.', ignorestatus=True
846 'dh --help', br'dh is a part of debhelper.', ignorestatus=True
854 )
847 )
855 dh_py2 = matchoutput(
848 dh_py2 = matchoutput(
856 'dh_python2 --help', br'other supported Python versions'
849 'dh_python2 --help', br'other supported Python versions'
857 )
850 )
858 # debuild comes from the 'devscripts' package, though you might want
851 # debuild comes from the 'devscripts' package, though you might want
859 # the 'build-debs' package instead, which has a dependency on devscripts.
852 # the 'build-debs' package instead, which has a dependency on devscripts.
860 debuild = matchoutput(
853 debuild = matchoutput(
861 'debuild --help', br'to run debian/rules with given parameter'
854 'debuild --help', br'to run debian/rules with given parameter'
862 )
855 )
863 return dpkg and dh and dh_py2 and debuild
856 return dpkg and dh and dh_py2 and debuild
864
857
865
858
866 @check(
859 @check(
867 "debdeps", "debian build dependencies (run dpkg-checkbuilddeps in contrib/)"
860 "debdeps", "debian build dependencies (run dpkg-checkbuilddeps in contrib/)"
868 )
861 )
869 def has_debdeps():
862 def has_debdeps():
870 # just check exit status (ignoring output)
863 # just check exit status (ignoring output)
871 path = '%s/../contrib/packaging/debian/control' % os.environ['TESTDIR']
864 path = '%s/../contrib/packaging/debian/control' % os.environ['TESTDIR']
872 return matchoutput('dpkg-checkbuilddeps %s' % path, br'')
865 return matchoutput('dpkg-checkbuilddeps %s' % path, br'')
873
866
874
867
875 @check("demandimport", "demandimport enabled")
868 @check("demandimport", "demandimport enabled")
876 def has_demandimport():
869 def has_demandimport():
877 # chg disables demandimport intentionally for performance wins.
870 # chg disables demandimport intentionally for performance wins.
878 return (not has_chg()) and os.environ.get('HGDEMANDIMPORT') != 'disable'
871 return (not has_chg()) and os.environ.get('HGDEMANDIMPORT') != 'disable'
879
872
880
873
881 # Add "py36", "py37", ... as possible feature checks. Note that there's no
874 # Add "py36", "py37", ... as possible feature checks. Note that there's no
882 # punctuation here.
875 # punctuation here.
883 @checkvers(
876 @checkvers(
884 "py",
877 "py",
885 "Python >= %s",
878 "Python >= %s",
886 ('3.6', '3.7', '3.8', '3.9', '3.10', '3.11', '3.12', '3.13'),
879 ('3.6', '3.7', '3.8', '3.9', '3.10', '3.11', '3.12', '3.13'),
887 )
880 )
888 def has_python_range(v):
881 def has_python_range(v):
889 major, minor = v.split('.')[0:2]
882 major, minor = v.split('.')[0:2]
890 py_major, py_minor = sys.version_info.major, sys.version_info.minor
883 py_major, py_minor = sys.version_info.major, sys.version_info.minor
891
884
892 return (py_major, py_minor) >= (int(major), int(minor))
885 return (py_major, py_minor) >= (int(major), int(minor))
893
886
894
887
895 @check("py3", "running with Python 3.x")
888 @check("py3", "running with Python 3.x")
896 def has_py3():
889 def has_py3():
897 return 3 == sys.version_info[0]
890 return 3 == sys.version_info[0]
898
891
899
892
900 @check("py3exe", "a Python 3.x interpreter is available")
893 @check("py3exe", "a Python 3.x interpreter is available")
901 def has_python3exe():
894 def has_python3exe():
902 py = 'python3'
895 py = 'python3'
903 if os.name == 'nt':
896 if os.name == 'nt':
904 py = 'py -3'
897 py = 'py -3'
905 return matchoutput('%s -V' % py, br'^Python 3.(6|7|8|9|10|11)')
898 return matchoutput('%s -V' % py, br'^Python 3.(6|7|8|9|10|11)')
906
899
907
900
908 @check("pure", "running with pure Python code")
901 @check("pure", "running with pure Python code")
909 def has_pure():
902 def has_pure():
910 return any(
903 return any(
911 [
904 [
912 os.environ.get("HGMODULEPOLICY") == "py",
905 os.environ.get("HGMODULEPOLICY") == "py",
913 os.environ.get("HGTEST_RUN_TESTS_PURE") == "--pure",
906 os.environ.get("HGTEST_RUN_TESTS_PURE") == "--pure",
914 ]
907 ]
915 )
908 )
916
909
917
910
918 @check("slow", "allow slow tests (use --allow-slow-tests)")
911 @check("slow", "allow slow tests (use --allow-slow-tests)")
919 def has_slow():
912 def has_slow():
920 return os.environ.get('HGTEST_SLOW') == 'slow'
913 return os.environ.get('HGTEST_SLOW') == 'slow'
921
914
922
915
923 @check("hypothesis", "Hypothesis automated test generation")
916 @check("hypothesis", "Hypothesis automated test generation")
924 def has_hypothesis():
917 def has_hypothesis():
925 try:
918 try:
926 import hypothesis
919 import hypothesis
927
920
928 hypothesis.given
921 hypothesis.given
929 return True
922 return True
930 except ImportError:
923 except ImportError:
931 return False
924 return False
932
925
933
926
934 @check("unziplinks", "unzip(1) understands and extracts symlinks")
927 @check("unziplinks", "unzip(1) understands and extracts symlinks")
935 def unzip_understands_symlinks():
928 def unzip_understands_symlinks():
936 return matchoutput('unzip --help', br'Info-ZIP')
929 return matchoutput('unzip --help', br'Info-ZIP')
937
930
938
931
939 @check("zstd", "zstd Python module available")
932 @check("zstd", "zstd Python module available")
940 def has_zstd():
933 def has_zstd():
941 try:
934 try:
942 import mercurial.zstd
935 import mercurial.zstd
943
936
944 mercurial.zstd.__version__
937 mercurial.zstd.__version__
945 return True
938 return True
946 except ImportError:
939 except ImportError:
947 return False
940 return False
948
941
949
942
950 @check("devfull", "/dev/full special file")
943 @check("devfull", "/dev/full special file")
951 def has_dev_full():
944 def has_dev_full():
952 return os.path.exists('/dev/full')
945 return os.path.exists('/dev/full')
953
946
954
947
955 @check("ensurepip", "ensurepip module")
948 @check("ensurepip", "ensurepip module")
956 def has_ensurepip():
949 def has_ensurepip():
957 try:
950 try:
958 import ensurepip
951 import ensurepip
959
952
960 ensurepip.bootstrap
953 ensurepip.bootstrap
961 return True
954 return True
962 except ImportError:
955 except ImportError:
963 return False
956 return False
964
957
965
958
966 @check("virtualenv", "virtualenv support")
959 @check("virtualenv", "virtualenv support")
967 def has_virtualenv():
960 def has_virtualenv():
968 try:
961 try:
969 import virtualenv
962 import virtualenv
970
963
971 # --no-site-package became the default in 1.7 (Nov 2011), and the
964 # --no-site-package became the default in 1.7 (Nov 2011), and the
972 # argument was removed in 20.0 (Feb 2020). Rather than make the
965 # argument was removed in 20.0 (Feb 2020). Rather than make the
973 # script complicated, just ignore ancient versions.
966 # script complicated, just ignore ancient versions.
974 return int(virtualenv.__version__.split('.')[0]) > 1
967 return int(virtualenv.__version__.split('.')[0]) > 1
975 except (AttributeError, ImportError, IndexError):
968 except (AttributeError, ImportError, IndexError):
976 return False
969 return False
977
970
978
971
979 @check("fsmonitor", "running tests with fsmonitor")
972 @check("fsmonitor", "running tests with fsmonitor")
980 def has_fsmonitor():
973 def has_fsmonitor():
981 return 'HGFSMONITOR_TESTS' in os.environ
974 return 'HGFSMONITOR_TESTS' in os.environ
982
975
983
976
984 @check("fuzzywuzzy", "Fuzzy string matching library")
977 @check("fuzzywuzzy", "Fuzzy string matching library")
985 def has_fuzzywuzzy():
978 def has_fuzzywuzzy():
986 try:
979 try:
987 import fuzzywuzzy
980 import fuzzywuzzy
988
981
989 fuzzywuzzy.__version__
982 fuzzywuzzy.__version__
990 return True
983 return True
991 except ImportError:
984 except ImportError:
992 return False
985 return False
993
986
994
987
995 @check("clang-libfuzzer", "clang new enough to include libfuzzer")
988 @check("clang-libfuzzer", "clang new enough to include libfuzzer")
996 def has_clang_libfuzzer():
989 def has_clang_libfuzzer():
997 mat = matchoutput('clang --version', br'clang version (\d)')
990 mat = matchoutput('clang --version', br'clang version (\d)')
998 if mat:
991 if mat:
999 # libfuzzer is new in clang 6
992 # libfuzzer is new in clang 6
1000 return int(mat.group(1)) > 5
993 return int(mat.group(1)) > 5
1001 return False
994 return False
1002
995
1003
996
1004 @check("clang-6.0", "clang 6.0 with version suffix (libfuzzer included)")
997 @check("clang-6.0", "clang 6.0 with version suffix (libfuzzer included)")
1005 def has_clang60():
998 def has_clang60():
1006 return matchoutput('clang-6.0 --version', br'clang version 6\.')
999 return matchoutput('clang-6.0 --version', br'clang version 6\.')
1007
1000
1008
1001
1009 @check("xdiff", "xdiff algorithm")
1002 @check("xdiff", "xdiff algorithm")
1010 def has_xdiff():
1003 def has_xdiff():
1011 try:
1004 try:
1012 from mercurial import policy
1005 from mercurial import policy
1013
1006
1014 bdiff = policy.importmod('bdiff')
1007 bdiff = policy.importmod('bdiff')
1015 return bdiff.xdiffblocks(b'', b'') == [(0, 0, 0, 0)]
1008 return bdiff.xdiffblocks(b'', b'') == [(0, 0, 0, 0)]
1016 except (ImportError, AttributeError):
1009 except (ImportError, AttributeError):
1017 return False
1010 return False
1018
1011
1019
1012
1020 @check('extraextensions', 'whether tests are running with extra extensions')
1013 @check('extraextensions', 'whether tests are running with extra extensions')
1021 def has_extraextensions():
1014 def has_extraextensions():
1022 return 'HGTESTEXTRAEXTENSIONS' in os.environ
1015 return 'HGTESTEXTRAEXTENSIONS' in os.environ
1023
1016
1024
1017
1025 def getrepofeatures():
1018 def getrepofeatures():
1026 """Obtain set of repository features in use.
1019 """Obtain set of repository features in use.
1027
1020
1028 HGREPOFEATURES can be used to define or remove features. It contains
1021 HGREPOFEATURES can be used to define or remove features. It contains
1029 a space-delimited list of feature strings. Strings beginning with ``-``
1022 a space-delimited list of feature strings. Strings beginning with ``-``
1030 mean to remove.
1023 mean to remove.
1031 """
1024 """
1032 # Default list provided by core.
1025 # Default list provided by core.
1033 features = {
1026 features = {
1034 'bundlerepo',
1027 'bundlerepo',
1035 'revlogstore',
1028 'revlogstore',
1036 'fncache',
1029 'fncache',
1037 }
1030 }
1038
1031
1039 # Features that imply other features.
1032 # Features that imply other features.
1040 implies = {
1033 implies = {
1041 'simplestore': ['-revlogstore', '-bundlerepo', '-fncache'],
1034 'simplestore': ['-revlogstore', '-bundlerepo', '-fncache'],
1042 }
1035 }
1043
1036
1044 for override in os.environ.get('HGREPOFEATURES', '').split(' '):
1037 for override in os.environ.get('HGREPOFEATURES', '').split(' '):
1045 if not override:
1038 if not override:
1046 continue
1039 continue
1047
1040
1048 if override.startswith('-'):
1041 if override.startswith('-'):
1049 if override[1:] in features:
1042 if override[1:] in features:
1050 features.remove(override[1:])
1043 features.remove(override[1:])
1051 else:
1044 else:
1052 features.add(override)
1045 features.add(override)
1053
1046
1054 for imply in implies.get(override, []):
1047 for imply in implies.get(override, []):
1055 if imply.startswith('-'):
1048 if imply.startswith('-'):
1056 if imply[1:] in features:
1049 if imply[1:] in features:
1057 features.remove(imply[1:])
1050 features.remove(imply[1:])
1058 else:
1051 else:
1059 features.add(imply)
1052 features.add(imply)
1060
1053
1061 return features
1054 return features
1062
1055
1063
1056
1064 @check('reporevlogstore', 'repository using the default revlog store')
1057 @check('reporevlogstore', 'repository using the default revlog store')
1065 def has_reporevlogstore():
1058 def has_reporevlogstore():
1066 return 'revlogstore' in getrepofeatures()
1059 return 'revlogstore' in getrepofeatures()
1067
1060
1068
1061
1069 @check('reposimplestore', 'repository using simple storage extension')
1062 @check('reposimplestore', 'repository using simple storage extension')
1070 def has_reposimplestore():
1063 def has_reposimplestore():
1071 return 'simplestore' in getrepofeatures()
1064 return 'simplestore' in getrepofeatures()
1072
1065
1073
1066
1074 @check('repobundlerepo', 'whether we can open bundle files as repos')
1067 @check('repobundlerepo', 'whether we can open bundle files as repos')
1075 def has_repobundlerepo():
1068 def has_repobundlerepo():
1076 return 'bundlerepo' in getrepofeatures()
1069 return 'bundlerepo' in getrepofeatures()
1077
1070
1078
1071
1079 @check('repofncache', 'repository has an fncache')
1072 @check('repofncache', 'repository has an fncache')
1080 def has_repofncache():
1073 def has_repofncache():
1081 return 'fncache' in getrepofeatures()
1074 return 'fncache' in getrepofeatures()
1082
1075
1083
1076
1084 @check('dirstate-v2', 'using the v2 format of .hg/dirstate')
1077 @check('dirstate-v2', 'using the v2 format of .hg/dirstate')
1085 def has_dirstate_v2():
1078 def has_dirstate_v2():
1086 # Keep this logic in sync with `newreporequirements()` in `mercurial/localrepo.py`
1079 # Keep this logic in sync with `newreporequirements()` in `mercurial/localrepo.py`
1087 return matchoutput(
1080 return matchoutput(
1088 'hg config format.use-dirstate-v2', b'(?i)1|yes|true|on|always'
1081 'hg config format.use-dirstate-v2', b'(?i)1|yes|true|on|always'
1089 )
1082 )
1090
1083
1091
1084
1092 @check('sqlite', 'sqlite3 module and matching cli is available')
1085 @check('sqlite', 'sqlite3 module and matching cli is available')
1093 def has_sqlite():
1086 def has_sqlite():
1094 try:
1087 try:
1095 import sqlite3
1088 import sqlite3
1096
1089
1097 version = sqlite3.sqlite_version_info
1090 version = sqlite3.sqlite_version_info
1098 except ImportError:
1091 except ImportError:
1099 return False
1092 return False
1100
1093
1101 if version < (3, 8, 3):
1094 if version < (3, 8, 3):
1102 # WITH clause not supported
1095 # WITH clause not supported
1103 return False
1096 return False
1104
1097
1105 return matchoutput('sqlite3 -version', br'^3\.\d+')
1098 return matchoutput('sqlite3 -version', br'^3\.\d+')
1106
1099
1107
1100
1108 @check('vcr', 'vcr http mocking library (pytest-vcr)')
1101 @check('vcr', 'vcr http mocking library (pytest-vcr)')
1109 def has_vcr():
1102 def has_vcr():
1110 try:
1103 try:
1111 import vcr
1104 import vcr
1112
1105
1113 vcr.VCR
1106 vcr.VCR
1114 return True
1107 return True
1115 except (ImportError, AttributeError):
1108 except (ImportError, AttributeError):
1116 pass
1109 pass
1117 return False
1110 return False
1118
1111
1119
1112
1120 @check('emacs', 'GNU Emacs')
1113 @check('emacs', 'GNU Emacs')
1121 def has_emacs():
1114 def has_emacs():
1122 # Our emacs lisp uses `with-eval-after-load` which is new in emacs
1115 # Our emacs lisp uses `with-eval-after-load` which is new in emacs
1123 # 24.4, so we allow emacs 24.4, 24.5, and 25+ (24.5 was the last
1116 # 24.4, so we allow emacs 24.4, 24.5, and 25+ (24.5 was the last
1124 # 24 release)
1117 # 24 release)
1125 return matchoutput('emacs --version', b'GNU Emacs 2(4.4|4.5|5|6|7|8|9)')
1118 return matchoutput('emacs --version', b'GNU Emacs 2(4.4|4.5|5|6|7|8|9)')
1126
1119
1127
1120
1128 @check('black', 'the black formatter for python >=23.3.0')
1121 @check('black', 'the black formatter for python >=23.3.0')
1129 def has_black():
1122 def has_black():
1130 blackcmd = 'black --version'
1123 blackcmd = 'black --version'
1131 version_regex = b'black, (?:version )?([0-9a-b.]+)'
1124 version_regex = b'black, (?:version )?([0-9a-b.]+)'
1132 version = matchoutput(blackcmd, version_regex)
1125 version = matchoutput(blackcmd, version_regex)
1133 if not version:
1126 if not version:
1134 return False
1127 return False
1135 return Version(_bytes2sys(version.group(1))) >= Version('23.3.0')
1128 return Version(_bytes2sys(version.group(1))) >= Version('23.3.0')
1136
1129
1137
1130
1138 @check('pytype', 'the pytype type checker')
1131 @check('pytype', 'the pytype type checker')
1139 def has_pytype():
1132 def has_pytype():
1140 pytypecmd = 'pytype --version'
1133 pytypecmd = 'pytype --version'
1141 version = matchoutput(pytypecmd, b'[0-9a-b.]+')
1134 version = matchoutput(pytypecmd, b'[0-9a-b.]+')
1142 if not version:
1135 if not version:
1143 return False
1136 return False
1144 return Version(_bytes2sys(version.group(0))) >= Version('2019.10.17')
1137 return Version(_bytes2sys(version.group(0))) >= Version('2019.10.17')
1145
1138
1146
1139
1147 @check("rustfmt", "rustfmt tool at version nightly-2024-07-16")
1140 @check("rustfmt", "rustfmt tool at version nightly-2024-07-16")
1148 def has_rustfmt():
1141 def has_rustfmt():
1149 # We use Nightly's rustfmt due to current unstable config options.
1142 # We use Nightly's rustfmt due to current unstable config options.
1150 return matchoutput(
1143 return matchoutput(
1151 '`rustup which --toolchain nightly-2024-07-16 rustfmt` --version',
1144 '`rustup which --toolchain nightly-2024-07-16 rustfmt` --version',
1152 b'rustfmt',
1145 b'rustfmt',
1153 )
1146 )
1154
1147
1155
1148
1156 @check("cargo", "cargo tool")
1149 @check("cargo", "cargo tool")
1157 def has_cargo():
1150 def has_cargo():
1158 return matchoutput('`rustup which cargo` --version', b'cargo')
1151 return matchoutput('`rustup which cargo` --version', b'cargo')
1159
1152
1160
1153
1161 @check("lzma", "python lzma module")
1154 @check("lzma", "python lzma module")
1162 def has_lzma():
1155 def has_lzma():
1163 try:
1156 try:
1164 import _lzma
1157 import _lzma
1165
1158
1166 _lzma.FORMAT_XZ
1159 _lzma.FORMAT_XZ
1167 return True
1160 return True
1168 except ImportError:
1161 except ImportError:
1169 return False
1162 return False
1170
1163
1171
1164
1172 @check("bash", "bash shell")
1165 @check("bash", "bash shell")
1173 def has_bash():
1166 def has_bash():
1174 return matchoutput("bash -c 'echo hi'", b'^hi$')
1167 return matchoutput("bash -c 'echo hi'", b'^hi$')
1175
1168
1176
1169
1177 @check("bigendian", "big-endian CPU")
1170 @check("bigendian", "big-endian CPU")
1178 def has_bigendian():
1171 def has_bigendian():
1179 return sys.byteorder == 'big'
1172 return sys.byteorder == 'big'
@@ -1,32 +1,32
1 #require test-repo pyflakes hg10
1 #require test-repo pyflakes hg10
2
2
3 $ . "$TESTDIR/helpers-testrepo.sh"
3 $ . "$TESTDIR/helpers-testrepo.sh"
4
4
5 run pyflakes on all tracked files ending in .py or without a file ending
5 run pyflakes on all tracked files ending in .py or without a file ending
6 (skipping binary file random-seed)
6 (skipping binary file random-seed)
7
7
8 $ cat > test.py <<EOF
8 $ cat > test.py <<EOF
9 > print(undefinedname)
9 > print(undefinedname)
10 > EOF
10 > EOF
11 $ "$PYTHON" -m pyflakes test.py 2>/dev/null | "$TESTDIR/filterpyflakes.py"
11 $ pyflakes test.py 2>/dev/null | "$TESTDIR/filterpyflakes.py"
12 test.py:1:* undefined name 'undefinedname' (glob)
12 test.py:1:* undefined name 'undefinedname' (glob)
13
13
14 $ cd "`dirname "$TESTDIR"`"
14 $ cd "`dirname "$TESTDIR"`"
15
15
16 $ testrepohg locate 'set:**.py or grep("^#!.*python")' \
16 $ testrepohg locate 'set:**.py or grep("^#!.*python")' \
17 > -X hgext/fsmonitor/pywatchman \
17 > -X hgext/fsmonitor/pywatchman \
18 > -X contrib/python-zstandard \
18 > -X contrib/python-zstandard \
19 > -X mercurial/thirdparty \
19 > -X mercurial/thirdparty \
20 > 2>/dev/null \
20 > 2>/dev/null \
21 > | xargs "$PYTHON" -m pyflakes 2>/dev/null | "$TESTDIR/filterpyflakes.py"
21 > | xargs pyflakes 2>/dev/null | "$TESTDIR/filterpyflakes.py"
22 contrib/perf.py:*:* undefined name 'xrange' (glob) (?)
22 contrib/perf.py:*:* undefined name 'xrange' (glob) (?)
23 mercurial/pycompat.py:*:* 'codecs' imported but unused (glob)
23 mercurial/pycompat.py:*:* 'codecs' imported but unused (glob)
24 mercurial/pycompat.py:*:* 'concurrent.futures' imported but unused (glob)
24 mercurial/pycompat.py:*:* 'concurrent.futures' imported but unused (glob)
25 mercurial/pycompat.py:*:* 'http.client as httplib' imported but unused (glob)
25 mercurial/pycompat.py:*:* 'http.client as httplib' imported but unused (glob)
26 mercurial/pycompat.py:*:* 'http.cookiejar as cookielib' imported but unused (glob)
26 mercurial/pycompat.py:*:* 'http.cookiejar as cookielib' imported but unused (glob)
27 mercurial/pycompat.py:*:* 'io' imported but unused (glob)
27 mercurial/pycompat.py:*:* 'io' imported but unused (glob)
28 mercurial/pycompat.py:*:* 'queue' imported but unused (glob)
28 mercurial/pycompat.py:*:* 'queue' imported but unused (glob)
29 mercurial/pycompat.py:*:* 'socketserver' imported but unused (glob)
29 mercurial/pycompat.py:*:* 'socketserver' imported but unused (glob)
30 mercurial/pycompat.py:*:* 'xmlrpc.client as xmlrpclib' imported but unused (glob)
30 mercurial/pycompat.py:*:* 'xmlrpc.client as xmlrpclib' imported but unused (glob)
31 mercurial/util.py:*:* 'pickle' imported but unused (glob)
31 mercurial/util.py:*:* 'pickle' imported but unused (glob)
32
32
General Comments 0
You need to be logged in to leave comments. Login now