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