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