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