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