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