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