##// END OF EJS Templates
run-tests: replace open-coded .decode()s on paths with a helper (issue4667)...
Augie Fackler -
r25162:153b9c52 default
parent child Browse files
Show More
@@ -1,2198 +1,2209 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 #
39 39 # (You could use any subset of the tests: test-s* happens to match
40 40 # enough that it's worth doing parallel runs, few enough that it
41 41 # completes fairly quickly, includes both shell and Python scripts, and
42 42 # includes some scripts that run daemon processes.)
43 43
44 44 from __future__ import print_function
45 45
46 46 from distutils import version
47 47 import difflib
48 48 import errno
49 49 import optparse
50 50 import os
51 51 import shutil
52 52 import subprocess
53 53 import signal
54 54 import socket
55 55 import sys
56 56 import tempfile
57 57 import time
58 58 import random
59 59 import re
60 60 import threading
61 61 import killdaemons as killmod
62 62 try:
63 63 import Queue as queue
64 64 except ImportError:
65 65 import queue
66 66 from xml.dom import minidom
67 67 import unittest
68 68
69 69 osenvironb = getattr(os, 'environb', os.environ)
70 70
71 71 try:
72 72 import json
73 73 except ImportError:
74 74 try:
75 75 import simplejson as json
76 76 except ImportError:
77 77 json = None
78 78
79 79 processlock = threading.Lock()
80 80
81 81 if sys.version_info > (3, 5, 0):
82 82 PYTHON3 = True
83 83 xrange = range # we use xrange in one place, and we'd rather not use range
84 84 def _bytespath(p):
85 85 return p.encode('utf-8')
86
87 def _strpath(p):
88 return p.decode('utf-8')
89
86 90 elif sys.version_info >= (3, 0, 0):
87 91 print('%s is only supported on Python 3.5+ and 2.6-2.7, not %s' %
88 92 (sys.argv[0], '.'.join(str(v) for v in sys.version_info[:3])))
89 93 sys.exit(70) # EX_SOFTWARE from `man 3 sysexit`
90 94 else:
91 95 PYTHON3 = False
96
97 # In python 2.x, path operations are generally done using
98 # bytestrings by default, so we don't have to do any extra
99 # fiddling there. We define the wrapper functions anyway just to
100 # help keep code consistent between platforms.
92 101 def _bytespath(p):
93 102 return p
94 103
104 _strpath = _bytespath
105
95 106 def checkportisavailable(port):
96 107 """return true if a port seems free to bind on localhost"""
97 108 try:
98 109 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
99 110 s.bind(('localhost', port))
100 111 s.close()
101 112 return True
102 113 except socket.error as exc:
103 114 if not exc.errno == errno.EADDRINUSE:
104 115 raise
105 116 return False
106 117
107 118 closefds = os.name == 'posix'
108 119 def Popen4(cmd, wd, timeout, env=None):
109 120 processlock.acquire()
110 121 p = subprocess.Popen(cmd, shell=True, bufsize=-1, cwd=wd, env=env,
111 122 close_fds=closefds,
112 123 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
113 124 stderr=subprocess.STDOUT)
114 125 processlock.release()
115 126
116 127 p.fromchild = p.stdout
117 128 p.tochild = p.stdin
118 129 p.childerr = p.stderr
119 130
120 131 p.timeout = False
121 132 if timeout:
122 133 def t():
123 134 start = time.time()
124 135 while time.time() - start < timeout and p.returncode is None:
125 136 time.sleep(.1)
126 137 p.timeout = True
127 138 if p.returncode is None:
128 139 terminate(p)
129 140 threading.Thread(target=t).start()
130 141
131 142 return p
132 143
133 144 PYTHON = _bytespath(sys.executable.replace('\\', '/'))
134 145 IMPL_PATH = b'PYTHONPATH'
135 146 if 'java' in sys.platform:
136 147 IMPL_PATH = b'JYTHONPATH'
137 148
138 149 defaults = {
139 150 'jobs': ('HGTEST_JOBS', 1),
140 151 'timeout': ('HGTEST_TIMEOUT', 180),
141 152 'port': ('HGTEST_PORT', 20059),
142 153 'shell': ('HGTEST_SHELL', 'sh'),
143 154 }
144 155
145 156 def parselistfiles(files, listtype, warn=True):
146 157 entries = dict()
147 158 for filename in files:
148 159 try:
149 160 path = os.path.expanduser(os.path.expandvars(filename))
150 161 f = open(path, "rb")
151 162 except IOError as err:
152 163 if err.errno != errno.ENOENT:
153 164 raise
154 165 if warn:
155 166 print("warning: no such %s file: %s" % (listtype, filename))
156 167 continue
157 168
158 169 for line in f.readlines():
159 170 line = line.split(b'#', 1)[0].strip()
160 171 if line:
161 172 entries[line] = filename
162 173
163 174 f.close()
164 175 return entries
165 176
166 177 def getparser():
167 178 """Obtain the OptionParser used by the CLI."""
168 179 parser = optparse.OptionParser("%prog [options] [tests]")
169 180
170 181 # keep these sorted
171 182 parser.add_option("--blacklist", action="append",
172 183 help="skip tests listed in the specified blacklist file")
173 184 parser.add_option("--whitelist", action="append",
174 185 help="always run tests listed in the specified whitelist file")
175 186 parser.add_option("--changed", type="string",
176 187 help="run tests that are changed in parent rev or working directory")
177 188 parser.add_option("-C", "--annotate", action="store_true",
178 189 help="output files annotated with coverage")
179 190 parser.add_option("-c", "--cover", action="store_true",
180 191 help="print a test coverage report")
181 192 parser.add_option("-d", "--debug", action="store_true",
182 193 help="debug mode: write output of test scripts to console"
183 194 " rather than capturing and diffing it (disables timeout)")
184 195 parser.add_option("-f", "--first", action="store_true",
185 196 help="exit on the first test failure")
186 197 parser.add_option("-H", "--htmlcov", action="store_true",
187 198 help="create an HTML report of the coverage of the files")
188 199 parser.add_option("-i", "--interactive", action="store_true",
189 200 help="prompt to accept changed output")
190 201 parser.add_option("-j", "--jobs", type="int",
191 202 help="number of jobs to run in parallel"
192 203 " (default: $%s or %d)" % defaults['jobs'])
193 204 parser.add_option("--keep-tmpdir", action="store_true",
194 205 help="keep temporary directory after running tests")
195 206 parser.add_option("-k", "--keywords",
196 207 help="run tests matching keywords")
197 208 parser.add_option("-l", "--local", action="store_true",
198 209 help="shortcut for --with-hg=<testdir>/../hg")
199 210 parser.add_option("--loop", action="store_true",
200 211 help="loop tests repeatedly")
201 212 parser.add_option("--runs-per-test", type="int", dest="runs_per_test",
202 213 help="run each test N times (default=1)", default=1)
203 214 parser.add_option("-n", "--nodiff", action="store_true",
204 215 help="skip showing test changes")
205 216 parser.add_option("-p", "--port", type="int",
206 217 help="port on which servers should listen"
207 218 " (default: $%s or %d)" % defaults['port'])
208 219 parser.add_option("--compiler", type="string",
209 220 help="compiler to build with")
210 221 parser.add_option("--pure", action="store_true",
211 222 help="use pure Python code instead of C extensions")
212 223 parser.add_option("-R", "--restart", action="store_true",
213 224 help="restart at last error")
214 225 parser.add_option("-r", "--retest", action="store_true",
215 226 help="retest failed tests")
216 227 parser.add_option("-S", "--noskips", action="store_true",
217 228 help="don't report skip tests verbosely")
218 229 parser.add_option("--shell", type="string",
219 230 help="shell to use (default: $%s or %s)" % defaults['shell'])
220 231 parser.add_option("-t", "--timeout", type="int",
221 232 help="kill errant tests after TIMEOUT seconds"
222 233 " (default: $%s or %d)" % defaults['timeout'])
223 234 parser.add_option("--time", action="store_true",
224 235 help="time how long each test takes")
225 236 parser.add_option("--json", action="store_true",
226 237 help="store test result data in 'report.json' file")
227 238 parser.add_option("--tmpdir", type="string",
228 239 help="run tests in the given temporary directory"
229 240 " (implies --keep-tmpdir)")
230 241 parser.add_option("-v", "--verbose", action="store_true",
231 242 help="output verbose messages")
232 243 parser.add_option("--xunit", type="string",
233 244 help="record xunit results at specified path")
234 245 parser.add_option("--view", type="string",
235 246 help="external diff viewer")
236 247 parser.add_option("--with-hg", type="string",
237 248 metavar="HG",
238 249 help="test using specified hg script rather than a "
239 250 "temporary installation")
240 251 parser.add_option("-3", "--py3k-warnings", action="store_true",
241 252 help="enable Py3k warnings on Python 2.6+")
242 253 parser.add_option('--extra-config-opt', action="append",
243 254 help='set the given config opt in the test hgrc')
244 255 parser.add_option('--random', action="store_true",
245 256 help='run tests in random order')
246 257 parser.add_option('--profile-runner', action='store_true',
247 258 help='run statprof on run-tests')
248 259
249 260 for option, (envvar, default) in defaults.items():
250 261 defaults[option] = type(default)(os.environ.get(envvar, default))
251 262 parser.set_defaults(**defaults)
252 263
253 264 return parser
254 265
255 266 def parseargs(args, parser):
256 267 """Parse arguments with our OptionParser and validate results."""
257 268 (options, args) = parser.parse_args(args)
258 269
259 270 # jython is always pure
260 271 if 'java' in sys.platform or '__pypy__' in sys.modules:
261 272 options.pure = True
262 273
263 274 if options.with_hg:
264 275 options.with_hg = os.path.expanduser(options.with_hg)
265 276 if not (os.path.isfile(options.with_hg) and
266 277 os.access(options.with_hg, os.X_OK)):
267 278 parser.error('--with-hg must specify an executable hg script')
268 279 if not os.path.basename(options.with_hg) == 'hg':
269 280 sys.stderr.write('warning: --with-hg should specify an hg script\n')
270 281 if options.local:
271 282 testdir = os.path.dirname(_bytespath(os.path.realpath(sys.argv[0])))
272 283 hgbin = os.path.join(os.path.dirname(testdir), b'hg')
273 284 if os.name != 'nt' and not os.access(hgbin, os.X_OK):
274 285 parser.error('--local specified, but %r not found or not executable'
275 286 % hgbin)
276 287 options.with_hg = hgbin
277 288
278 289 options.anycoverage = options.cover or options.annotate or options.htmlcov
279 290 if options.anycoverage:
280 291 try:
281 292 import coverage
282 293 covver = version.StrictVersion(coverage.__version__).version
283 294 if covver < (3, 3):
284 295 parser.error('coverage options require coverage 3.3 or later')
285 296 except ImportError:
286 297 parser.error('coverage options now require the coverage package')
287 298
288 299 if options.anycoverage and options.local:
289 300 # this needs some path mangling somewhere, I guess
290 301 parser.error("sorry, coverage options do not work when --local "
291 302 "is specified")
292 303
293 304 if options.anycoverage and options.with_hg:
294 305 parser.error("sorry, coverage options do not work when --with-hg "
295 306 "is specified")
296 307
297 308 global verbose
298 309 if options.verbose:
299 310 verbose = ''
300 311
301 312 if options.tmpdir:
302 313 options.tmpdir = os.path.expanduser(options.tmpdir)
303 314
304 315 if options.jobs < 1:
305 316 parser.error('--jobs must be positive')
306 317 if options.interactive and options.debug:
307 318 parser.error("-i/--interactive and -d/--debug are incompatible")
308 319 if options.debug:
309 320 if options.timeout != defaults['timeout']:
310 321 sys.stderr.write(
311 322 'warning: --timeout option ignored with --debug\n')
312 323 options.timeout = 0
313 324 if options.py3k_warnings:
314 325 if PYTHON3:
315 326 parser.error(
316 327 '--py3k-warnings can only be used on Python 2.6 and 2.7')
317 328 if options.blacklist:
318 329 options.blacklist = parselistfiles(options.blacklist, 'blacklist')
319 330 if options.whitelist:
320 331 options.whitelisted = parselistfiles(options.whitelist, 'whitelist')
321 332 else:
322 333 options.whitelisted = {}
323 334
324 335 return (options, args)
325 336
326 337 def rename(src, dst):
327 338 """Like os.rename(), trade atomicity and opened files friendliness
328 339 for existing destination support.
329 340 """
330 341 shutil.copy(src, dst)
331 342 os.remove(src)
332 343
333 344 _unified_diff = difflib.unified_diff
334 345 if PYTHON3:
335 346 import functools
336 347 _unified_diff = functools.partial(difflib.diff_bytes, difflib.unified_diff)
337 348
338 349 def getdiff(expected, output, ref, err):
339 350 servefail = False
340 351 lines = []
341 352 for line in _unified_diff(expected, output, ref, err):
342 353 if line.startswith(b'+++') or line.startswith(b'---'):
343 354 line = line.replace(b'\\', b'/')
344 355 if line.endswith(b' \n'):
345 356 line = line[:-2] + b'\n'
346 357 lines.append(line)
347 358 if not servefail and line.startswith(
348 359 b'+ abort: child process failed to start'):
349 360 servefail = True
350 361
351 362 return servefail, lines
352 363
353 364 verbose = False
354 365 def vlog(*msg):
355 366 """Log only when in verbose mode."""
356 367 if verbose is False:
357 368 return
358 369
359 370 return log(*msg)
360 371
361 372 # Bytes that break XML even in a CDATA block: control characters 0-31
362 373 # sans \t, \n and \r
363 374 CDATA_EVIL = re.compile(br"[\000-\010\013\014\016-\037]")
364 375
365 376 def cdatasafe(data):
366 377 """Make a string safe to include in a CDATA block.
367 378
368 379 Certain control characters are illegal in a CDATA block, and
369 380 there's no way to include a ]]> in a CDATA either. This function
370 381 replaces illegal bytes with ? and adds a space between the ]] so
371 382 that it won't break the CDATA block.
372 383 """
373 384 return CDATA_EVIL.sub(b'?', data).replace(b']]>', b'] ]>')
374 385
375 386 def log(*msg):
376 387 """Log something to stdout.
377 388
378 389 Arguments are strings to print.
379 390 """
380 391 with iolock:
381 392 if verbose:
382 393 print(verbose, end=' ')
383 394 for m in msg:
384 395 print(m, end=' ')
385 396 print()
386 397 sys.stdout.flush()
387 398
388 399 def terminate(proc):
389 400 """Terminate subprocess (with fallback for Python versions < 2.6)"""
390 401 vlog('# Terminating process %d' % proc.pid)
391 402 try:
392 403 getattr(proc, 'terminate', lambda : os.kill(proc.pid, signal.SIGTERM))()
393 404 except OSError:
394 405 pass
395 406
396 407 def killdaemons(pidfile):
397 408 return killmod.killdaemons(pidfile, tryhard=False, remove=True,
398 409 logfn=vlog)
399 410
400 411 class Test(unittest.TestCase):
401 412 """Encapsulates a single, runnable test.
402 413
403 414 While this class conforms to the unittest.TestCase API, it differs in that
404 415 instances need to be instantiated manually. (Typically, unittest.TestCase
405 416 classes are instantiated automatically by scanning modules.)
406 417 """
407 418
408 419 # Status code reserved for skipped tests (used by hghave).
409 420 SKIPPED_STATUS = 80
410 421
411 422 def __init__(self, path, tmpdir, keeptmpdir=False,
412 423 debug=False,
413 424 timeout=defaults['timeout'],
414 425 startport=defaults['port'], extraconfigopts=None,
415 426 py3kwarnings=False, shell=None):
416 427 """Create a test from parameters.
417 428
418 429 path is the full path to the file defining the test.
419 430
420 431 tmpdir is the main temporary directory to use for this test.
421 432
422 433 keeptmpdir determines whether to keep the test's temporary directory
423 434 after execution. It defaults to removal (False).
424 435
425 436 debug mode will make the test execute verbosely, with unfiltered
426 437 output.
427 438
428 439 timeout controls the maximum run time of the test. It is ignored when
429 440 debug is True.
430 441
431 442 startport controls the starting port number to use for this test. Each
432 443 test will reserve 3 port numbers for execution. It is the caller's
433 444 responsibility to allocate a non-overlapping port range to Test
434 445 instances.
435 446
436 447 extraconfigopts is an iterable of extra hgrc config options. Values
437 448 must have the form "key=value" (something understood by hgrc). Values
438 449 of the form "foo.key=value" will result in "[foo] key=value".
439 450
440 451 py3kwarnings enables Py3k warnings.
441 452
442 453 shell is the shell to execute tests in.
443 454 """
444 455 self.path = path
445 456 self.bname = os.path.basename(path)
446 self.name = self.bname.decode('utf-8')
457 self.name = _strpath(self.bname)
447 458 self._testdir = os.path.dirname(path)
448 459 self.errpath = os.path.join(self._testdir, b'%s.err' % self.bname)
449 460
450 461 self._threadtmp = tmpdir
451 462 self._keeptmpdir = keeptmpdir
452 463 self._debug = debug
453 464 self._timeout = timeout
454 465 self._startport = startport
455 466 self._extraconfigopts = extraconfigopts or []
456 467 self._py3kwarnings = py3kwarnings
457 468 self._shell = _bytespath(shell)
458 469
459 470 self._aborted = False
460 471 self._daemonpids = []
461 472 self._finished = None
462 473 self._ret = None
463 474 self._out = None
464 475 self._skipped = None
465 476 self._testtmp = None
466 477
467 478 # If we're not in --debug mode and reference output file exists,
468 479 # check test output against it.
469 480 if debug:
470 481 self._refout = None # to match "out is None"
471 482 elif os.path.exists(self.refpath):
472 483 f = open(self.refpath, 'rb')
473 484 self._refout = f.read().splitlines(True)
474 485 f.close()
475 486 else:
476 487 self._refout = []
477 488
478 489 # needed to get base class __repr__ running
479 490 @property
480 491 def _testMethodName(self):
481 492 return self.name
482 493
483 494 def __str__(self):
484 495 return self.name
485 496
486 497 def shortDescription(self):
487 498 return self.name
488 499
489 500 def setUp(self):
490 501 """Tasks to perform before run()."""
491 502 self._finished = False
492 503 self._ret = None
493 504 self._out = None
494 505 self._skipped = None
495 506
496 507 try:
497 508 os.mkdir(self._threadtmp)
498 509 except OSError as e:
499 510 if e.errno != errno.EEXIST:
500 511 raise
501 512
502 513 self._testtmp = os.path.join(self._threadtmp,
503 514 os.path.basename(self.path))
504 515 os.mkdir(self._testtmp)
505 516
506 517 # Remove any previous output files.
507 518 if os.path.exists(self.errpath):
508 519 try:
509 520 os.remove(self.errpath)
510 521 except OSError as e:
511 522 # We might have raced another test to clean up a .err
512 523 # file, so ignore ENOENT when removing a previous .err
513 524 # file.
514 525 if e.errno != errno.ENOENT:
515 526 raise
516 527
517 528 def run(self, result):
518 529 """Run this test and report results against a TestResult instance."""
519 530 # This function is extremely similar to unittest.TestCase.run(). Once
520 531 # we require Python 2.7 (or at least its version of unittest), this
521 532 # function can largely go away.
522 533 self._result = result
523 534 result.startTest(self)
524 535 try:
525 536 try:
526 537 self.setUp()
527 538 except (KeyboardInterrupt, SystemExit):
528 539 self._aborted = True
529 540 raise
530 541 except Exception:
531 542 result.addError(self, sys.exc_info())
532 543 return
533 544
534 545 success = False
535 546 try:
536 547 self.runTest()
537 548 except KeyboardInterrupt:
538 549 self._aborted = True
539 550 raise
540 551 except SkipTest as e:
541 552 result.addSkip(self, str(e))
542 553 # The base class will have already counted this as a
543 554 # test we "ran", but we want to exclude skipped tests
544 555 # from those we count towards those run.
545 556 result.testsRun -= 1
546 557 except IgnoreTest as e:
547 558 result.addIgnore(self, str(e))
548 559 # As with skips, ignores also should be excluded from
549 560 # the number of tests executed.
550 561 result.testsRun -= 1
551 562 except WarnTest as e:
552 563 result.addWarn(self, str(e))
553 564 except self.failureException as e:
554 565 # This differs from unittest in that we don't capture
555 566 # the stack trace. This is for historical reasons and
556 567 # this decision could be revisited in the future,
557 568 # especially for PythonTest instances.
558 569 if result.addFailure(self, str(e)):
559 570 success = True
560 571 except Exception:
561 572 result.addError(self, sys.exc_info())
562 573 else:
563 574 success = True
564 575
565 576 try:
566 577 self.tearDown()
567 578 except (KeyboardInterrupt, SystemExit):
568 579 self._aborted = True
569 580 raise
570 581 except Exception:
571 582 result.addError(self, sys.exc_info())
572 583 success = False
573 584
574 585 if success:
575 586 result.addSuccess(self)
576 587 finally:
577 588 result.stopTest(self, interrupted=self._aborted)
578 589
579 590 def runTest(self):
580 591 """Run this test instance.
581 592
582 593 This will return a tuple describing the result of the test.
583 594 """
584 595 env = self._getenv()
585 596 self._daemonpids.append(env['DAEMON_PIDS'])
586 597 self._createhgrc(env['HGRCPATH'])
587 598
588 599 vlog('# Test', self.name)
589 600
590 601 ret, out = self._run(env)
591 602 self._finished = True
592 603 self._ret = ret
593 604 self._out = out
594 605
595 606 def describe(ret):
596 607 if ret < 0:
597 608 return 'killed by signal: %d' % -ret
598 609 return 'returned error code %d' % ret
599 610
600 611 self._skipped = False
601 612
602 613 if ret == self.SKIPPED_STATUS:
603 614 if out is None: # Debug mode, nothing to parse.
604 615 missing = ['unknown']
605 616 failed = None
606 617 else:
607 618 missing, failed = TTest.parsehghaveoutput(out)
608 619
609 620 if not missing:
610 621 missing = ['skipped']
611 622
612 623 if failed:
613 624 self.fail('hg have failed checking for %s' % failed[-1])
614 625 else:
615 626 self._skipped = True
616 627 raise SkipTest(missing[-1])
617 628 elif ret == 'timeout':
618 629 self.fail('timed out')
619 630 elif ret is False:
620 631 raise WarnTest('no result code from test')
621 632 elif out != self._refout:
622 633 # Diff generation may rely on written .err file.
623 634 if (ret != 0 or out != self._refout) and not self._skipped \
624 635 and not self._debug:
625 636 f = open(self.errpath, 'wb')
626 637 for line in out:
627 638 f.write(line)
628 639 f.close()
629 640
630 641 # The result object handles diff calculation for us.
631 642 if self._result.addOutputMismatch(self, ret, out, self._refout):
632 643 # change was accepted, skip failing
633 644 return
634 645
635 646 if ret:
636 647 msg = 'output changed and ' + describe(ret)
637 648 else:
638 649 msg = 'output changed'
639 650
640 651 self.fail(msg)
641 652 elif ret:
642 653 self.fail(describe(ret))
643 654
644 655 def tearDown(self):
645 656 """Tasks to perform after run()."""
646 657 for entry in self._daemonpids:
647 658 killdaemons(entry)
648 659 self._daemonpids = []
649 660
650 661 if not self._keeptmpdir:
651 662 shutil.rmtree(self._testtmp, True)
652 663 shutil.rmtree(self._threadtmp, True)
653 664
654 665 if (self._ret != 0 or self._out != self._refout) and not self._skipped \
655 666 and not self._debug and self._out:
656 667 f = open(self.errpath, 'wb')
657 668 for line in self._out:
658 669 f.write(line)
659 670 f.close()
660 671
661 672 vlog("# Ret was:", self._ret, '(%s)' % self.name)
662 673
663 674 def _run(self, env):
664 675 # This should be implemented in child classes to run tests.
665 676 raise SkipTest('unknown test type')
666 677
667 678 def abort(self):
668 679 """Terminate execution of this test."""
669 680 self._aborted = True
670 681
671 682 def _getreplacements(self):
672 683 """Obtain a mapping of text replacements to apply to test output.
673 684
674 685 Test output needs to be normalized so it can be compared to expected
675 686 output. This function defines how some of that normalization will
676 687 occur.
677 688 """
678 689 r = [
679 690 (br':%d\b' % self._startport, b':$HGPORT'),
680 691 (br':%d\b' % (self._startport + 1), b':$HGPORT1'),
681 692 (br':%d\b' % (self._startport + 2), b':$HGPORT2'),
682 693 (br'(?m)^(saved backup bundle to .*\.hg)( \(glob\))?$',
683 694 br'\1 (glob)'),
684 695 ]
685 696
686 697 if os.name == 'nt':
687 698 r.append(
688 699 (b''.join(c.isalpha() and b'[%s%s]' % (c.lower(), c.upper()) or
689 700 c in b'/\\' and br'[/\\]' or c.isdigit() and c or b'\\' + c
690 701 for c in self._testtmp), b'$TESTTMP'))
691 702 else:
692 703 r.append((re.escape(self._testtmp), b'$TESTTMP'))
693 704
694 705 return r
695 706
696 707 def _getenv(self):
697 708 """Obtain environment variables to use during test execution."""
698 709 env = os.environ.copy()
699 710 env['TESTTMP'] = self._testtmp
700 711 env['HOME'] = self._testtmp
701 712 env["HGPORT"] = str(self._startport)
702 713 env["HGPORT1"] = str(self._startport + 1)
703 714 env["HGPORT2"] = str(self._startport + 2)
704 715 env["HGRCPATH"] = os.path.join(self._threadtmp, b'.hgrc')
705 716 env["DAEMON_PIDS"] = os.path.join(self._threadtmp, b'daemon.pids')
706 717 env["HGEDITOR"] = ('"' + sys.executable + '"'
707 718 + ' -c "import sys; sys.exit(0)"')
708 719 env["HGMERGE"] = "internal:merge"
709 720 env["HGUSER"] = "test"
710 721 env["HGENCODING"] = "ascii"
711 722 env["HGENCODINGMODE"] = "strict"
712 723
713 724 # Reset some environment variables to well-known values so that
714 725 # the tests produce repeatable output.
715 726 env['LANG'] = env['LC_ALL'] = env['LANGUAGE'] = 'C'
716 727 env['TZ'] = 'GMT'
717 728 env["EMAIL"] = "Foo Bar <foo.bar@example.com>"
718 729 env['COLUMNS'] = '80'
719 730 env['TERM'] = 'xterm'
720 731
721 732 for k in ('HG HGPROF CDPATH GREP_OPTIONS http_proxy no_proxy ' +
722 733 'NO_PROXY').split():
723 734 if k in env:
724 735 del env[k]
725 736
726 737 # unset env related to hooks
727 738 for k in env.keys():
728 739 if k.startswith('HG_'):
729 740 del env[k]
730 741
731 742 return env
732 743
733 744 def _createhgrc(self, path):
734 745 """Create an hgrc file for this test."""
735 746 hgrc = open(path, 'wb')
736 747 hgrc.write(b'[ui]\n')
737 748 hgrc.write(b'slash = True\n')
738 749 hgrc.write(b'interactive = False\n')
739 750 hgrc.write(b'mergemarkers = detailed\n')
740 751 hgrc.write(b'promptecho = True\n')
741 752 hgrc.write(b'[defaults]\n')
742 753 hgrc.write(b'backout = -d "0 0"\n')
743 754 hgrc.write(b'commit = -d "0 0"\n')
744 755 hgrc.write(b'shelve = --date "0 0"\n')
745 756 hgrc.write(b'tag = -d "0 0"\n')
746 757 hgrc.write(b'[devel]\n')
747 758 hgrc.write(b'all = true\n')
748 759 hgrc.write(b'[largefiles]\n')
749 760 hgrc.write(b'usercache = %s\n' %
750 761 (os.path.join(self._testtmp, b'.cache/largefiles')))
751 762
752 763 for opt in self._extraconfigopts:
753 764 section, key = opt.split('.', 1)
754 765 assert '=' in key, ('extra config opt %s must '
755 766 'have an = for assignment' % opt)
756 767 hgrc.write(b'[%s]\n%s\n' % (section, key))
757 768 hgrc.close()
758 769
759 770 def fail(self, msg):
760 771 # unittest differentiates between errored and failed.
761 772 # Failed is denoted by AssertionError (by default at least).
762 773 raise AssertionError(msg)
763 774
764 775 def _runcommand(self, cmd, env, normalizenewlines=False):
765 776 """Run command in a sub-process, capturing the output (stdout and
766 777 stderr).
767 778
768 779 Return a tuple (exitcode, output). output is None in debug mode.
769 780 """
770 781 if self._debug:
771 782 proc = subprocess.Popen(cmd, shell=True, cwd=self._testtmp,
772 783 env=env)
773 784 ret = proc.wait()
774 785 return (ret, None)
775 786
776 787 proc = Popen4(cmd, self._testtmp, self._timeout, env)
777 788 def cleanup():
778 789 terminate(proc)
779 790 ret = proc.wait()
780 791 if ret == 0:
781 792 ret = signal.SIGTERM << 8
782 793 killdaemons(env['DAEMON_PIDS'])
783 794 return ret
784 795
785 796 output = ''
786 797 proc.tochild.close()
787 798
788 799 try:
789 800 output = proc.fromchild.read()
790 801 except KeyboardInterrupt:
791 802 vlog('# Handling keyboard interrupt')
792 803 cleanup()
793 804 raise
794 805
795 806 ret = proc.wait()
796 807 if os.WIFEXITED(ret):
797 808 ret = os.WEXITSTATUS(ret)
798 809
799 810 if proc.timeout:
800 811 ret = 'timeout'
801 812
802 813 if ret:
803 814 killdaemons(env['DAEMON_PIDS'])
804 815
805 816 for s, r in self._getreplacements():
806 817 output = re.sub(s, r, output)
807 818
808 819 if normalizenewlines:
809 820 output = output.replace('\r\n', '\n')
810 821
811 822 return ret, output.splitlines(True)
812 823
813 824 class PythonTest(Test):
814 825 """A Python-based test."""
815 826
816 827 @property
817 828 def refpath(self):
818 829 return os.path.join(self._testdir, b'%s.out' % self.bname)
819 830
820 831 def _run(self, env):
821 832 py3kswitch = self._py3kwarnings and b' -3' or b''
822 833 cmd = b'%s%s "%s"' % (PYTHON, py3kswitch, self.path)
823 834 vlog("# Running", cmd)
824 835 normalizenewlines = os.name == 'nt'
825 836 result = self._runcommand(cmd, env,
826 837 normalizenewlines=normalizenewlines)
827 838 if self._aborted:
828 839 raise KeyboardInterrupt()
829 840
830 841 return result
831 842
832 843 # This script may want to drop globs from lines matching these patterns on
833 844 # Windows, but check-code.py wants a glob on these lines unconditionally. Don't
834 845 # warn if that is the case for anything matching these lines.
835 846 checkcodeglobpats = [
836 847 re.compile(br'^pushing to \$TESTTMP/.*[^)]$'),
837 848 re.compile(br'^moving \S+/.*[^)]$'),
838 849 re.compile(br'^pulling from \$TESTTMP/.*[^)]$')
839 850 ]
840 851
841 852 bchr = chr
842 853 if PYTHON3:
843 854 bchr = lambda x: bytes([x])
844 855
845 856 class TTest(Test):
846 857 """A "t test" is a test backed by a .t file."""
847 858
848 859 SKIPPED_PREFIX = 'skipped: '
849 860 FAILED_PREFIX = 'hghave check failed: '
850 861 NEEDESCAPE = re.compile(br'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
851 862
852 863 ESCAPESUB = re.compile(br'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
853 864 ESCAPEMAP = dict((bchr(i), br'\x%02x' % i) for i in range(256))
854 865 ESCAPEMAP.update({b'\\': b'\\\\', b'\r': br'\r'})
855 866
856 867 @property
857 868 def refpath(self):
858 869 return os.path.join(self._testdir, self.bname)
859 870
860 871 def _run(self, env):
861 872 f = open(self.path, 'rb')
862 873 lines = f.readlines()
863 874 f.close()
864 875
865 876 salt, script, after, expected = self._parsetest(lines)
866 877
867 878 # Write out the generated script.
868 879 fname = b'%s.sh' % self._testtmp
869 880 f = open(fname, 'wb')
870 881 for l in script:
871 882 f.write(l)
872 883 f.close()
873 884
874 885 cmd = b'%s "%s"' % (self._shell, fname)
875 886 vlog("# Running", cmd)
876 887
877 888 exitcode, output = self._runcommand(cmd, env)
878 889
879 890 if self._aborted:
880 891 raise KeyboardInterrupt()
881 892
882 893 # Do not merge output if skipped. Return hghave message instead.
883 894 # Similarly, with --debug, output is None.
884 895 if exitcode == self.SKIPPED_STATUS or output is None:
885 896 return exitcode, output
886 897
887 898 return self._processoutput(exitcode, output, salt, after, expected)
888 899
889 900 def _hghave(self, reqs):
890 901 # TODO do something smarter when all other uses of hghave are gone.
891 902 tdir = self._testdir.replace(b'\\', b'/')
892 903 proc = Popen4(b'%s -c "%s/hghave %s"' %
893 904 (self._shell, tdir, b' '.join(reqs)),
894 905 self._testtmp, 0, self._getenv())
895 906 stdout, stderr = proc.communicate()
896 907 ret = proc.wait()
897 908 if os.WIFEXITED(ret):
898 909 ret = os.WEXITSTATUS(ret)
899 910 if ret == 2:
900 911 print(stdout)
901 912 sys.exit(1)
902 913
903 914 return ret == 0
904 915
905 916 def _parsetest(self, lines):
906 917 # We generate a shell script which outputs unique markers to line
907 918 # up script results with our source. These markers include input
908 919 # line number and the last return code.
909 920 salt = b"SALT%d" % time.time()
910 921 def addsalt(line, inpython):
911 922 if inpython:
912 923 script.append(b'%s %d 0\n' % (salt, line))
913 924 else:
914 925 script.append(b'echo %s %d $?\n' % (salt, line))
915 926
916 927 script = []
917 928
918 929 # After we run the shell script, we re-unify the script output
919 930 # with non-active parts of the source, with synchronization by our
920 931 # SALT line number markers. The after table contains the non-active
921 932 # components, ordered by line number.
922 933 after = {}
923 934
924 935 # Expected shell script output.
925 936 expected = {}
926 937
927 938 pos = prepos = -1
928 939
929 940 # True or False when in a true or false conditional section
930 941 skipping = None
931 942
932 943 # We keep track of whether or not we're in a Python block so we
933 944 # can generate the surrounding doctest magic.
934 945 inpython = False
935 946
936 947 if self._debug:
937 948 script.append(b'set -x\n')
938 949 if os.getenv('MSYSTEM'):
939 950 script.append(b'alias pwd="pwd -W"\n')
940 951
941 952 for n, l in enumerate(lines):
942 953 if not l.endswith(b'\n'):
943 954 l += b'\n'
944 955 if l.startswith(b'#require'):
945 956 lsplit = l.split()
946 957 if len(lsplit) < 2 or lsplit[0] != b'#require':
947 958 after.setdefault(pos, []).append(' !!! invalid #require\n')
948 959 if not self._hghave(lsplit[1:]):
949 960 script = [b"exit 80\n"]
950 961 break
951 962 after.setdefault(pos, []).append(l)
952 963 elif l.startswith(b'#if'):
953 964 lsplit = l.split()
954 965 if len(lsplit) < 2 or lsplit[0] != b'#if':
955 966 after.setdefault(pos, []).append(' !!! invalid #if\n')
956 967 if skipping is not None:
957 968 after.setdefault(pos, []).append(' !!! nested #if\n')
958 969 skipping = not self._hghave(lsplit[1:])
959 970 after.setdefault(pos, []).append(l)
960 971 elif l.startswith(b'#else'):
961 972 if skipping is None:
962 973 after.setdefault(pos, []).append(' !!! missing #if\n')
963 974 skipping = not skipping
964 975 after.setdefault(pos, []).append(l)
965 976 elif l.startswith(b'#endif'):
966 977 if skipping is None:
967 978 after.setdefault(pos, []).append(' !!! missing #if\n')
968 979 skipping = None
969 980 after.setdefault(pos, []).append(l)
970 981 elif skipping:
971 982 after.setdefault(pos, []).append(l)
972 983 elif l.startswith(b' >>> '): # python inlines
973 984 after.setdefault(pos, []).append(l)
974 985 prepos = pos
975 986 pos = n
976 987 if not inpython:
977 988 # We've just entered a Python block. Add the header.
978 989 inpython = True
979 990 addsalt(prepos, False) # Make sure we report the exit code.
980 991 script.append(b'%s -m heredoctest <<EOF\n' % PYTHON)
981 992 addsalt(n, True)
982 993 script.append(l[2:])
983 994 elif l.startswith(b' ... '): # python inlines
984 995 after.setdefault(prepos, []).append(l)
985 996 script.append(l[2:])
986 997 elif l.startswith(b' $ '): # commands
987 998 if inpython:
988 999 script.append(b'EOF\n')
989 1000 inpython = False
990 1001 after.setdefault(pos, []).append(l)
991 1002 prepos = pos
992 1003 pos = n
993 1004 addsalt(n, False)
994 1005 cmd = l[4:].split()
995 1006 if len(cmd) == 2 and cmd[0] == b'cd':
996 1007 l = b' $ cd %s || exit 1\n' % cmd[1]
997 1008 script.append(l[4:])
998 1009 elif l.startswith(b' > '): # continuations
999 1010 after.setdefault(prepos, []).append(l)
1000 1011 script.append(l[4:])
1001 1012 elif l.startswith(b' '): # results
1002 1013 # Queue up a list of expected results.
1003 1014 expected.setdefault(pos, []).append(l[2:])
1004 1015 else:
1005 1016 if inpython:
1006 1017 script.append(b'EOF\n')
1007 1018 inpython = False
1008 1019 # Non-command/result. Queue up for merged output.
1009 1020 after.setdefault(pos, []).append(l)
1010 1021
1011 1022 if inpython:
1012 1023 script.append(b'EOF\n')
1013 1024 if skipping is not None:
1014 1025 after.setdefault(pos, []).append(' !!! missing #endif\n')
1015 1026 addsalt(n + 1, False)
1016 1027
1017 1028 return salt, script, after, expected
1018 1029
1019 1030 def _processoutput(self, exitcode, output, salt, after, expected):
1020 1031 # Merge the script output back into a unified test.
1021 1032 warnonly = 1 # 1: not yet; 2: yes; 3: for sure not
1022 1033 if exitcode != 0:
1023 1034 warnonly = 3
1024 1035
1025 1036 pos = -1
1026 1037 postout = []
1027 1038 for l in output:
1028 1039 lout, lcmd = l, None
1029 1040 if salt in l:
1030 1041 lout, lcmd = l.split(salt, 1)
1031 1042
1032 1043 if lout:
1033 1044 if not lout.endswith(b'\n'):
1034 1045 lout += b' (no-eol)\n'
1035 1046
1036 1047 # Find the expected output at the current position.
1037 1048 el = None
1038 1049 if expected.get(pos, None):
1039 1050 el = expected[pos].pop(0)
1040 1051
1041 1052 r = TTest.linematch(el, lout)
1042 1053 if isinstance(r, str):
1043 1054 if r == '+glob':
1044 1055 lout = el[:-1] + ' (glob)\n'
1045 1056 r = '' # Warn only this line.
1046 1057 elif r == '-glob':
1047 1058 lout = ''.join(el.rsplit(' (glob)', 1))
1048 1059 r = '' # Warn only this line.
1049 1060 else:
1050 1061 log('\ninfo, unknown linematch result: %r\n' % r)
1051 1062 r = False
1052 1063 if r:
1053 1064 postout.append(b' ' + el)
1054 1065 else:
1055 1066 if self.NEEDESCAPE(lout):
1056 1067 lout = TTest._stringescape(b'%s (esc)\n' %
1057 1068 lout.rstrip(b'\n'))
1058 1069 postout.append(b' ' + lout) # Let diff deal with it.
1059 1070 if r != '': # If line failed.
1060 1071 warnonly = 3 # for sure not
1061 1072 elif warnonly == 1: # Is "not yet" and line is warn only.
1062 1073 warnonly = 2 # Yes do warn.
1063 1074
1064 1075 if lcmd:
1065 1076 # Add on last return code.
1066 1077 ret = int(lcmd.split()[1])
1067 1078 if ret != 0:
1068 1079 postout.append(b' [%d]\n' % ret)
1069 1080 if pos in after:
1070 1081 # Merge in non-active test bits.
1071 1082 postout += after.pop(pos)
1072 1083 pos = int(lcmd.split()[0])
1073 1084
1074 1085 if pos in after:
1075 1086 postout += after.pop(pos)
1076 1087
1077 1088 if warnonly == 2:
1078 1089 exitcode = False # Set exitcode to warned.
1079 1090
1080 1091 return exitcode, postout
1081 1092
1082 1093 @staticmethod
1083 1094 def rematch(el, l):
1084 1095 try:
1085 1096 # use \Z to ensure that the regex matches to the end of the string
1086 1097 if os.name == 'nt':
1087 1098 return re.match(el + br'\r?\n\Z', l)
1088 1099 return re.match(el + br'\n\Z', l)
1089 1100 except re.error:
1090 1101 # el is an invalid regex
1091 1102 return False
1092 1103
1093 1104 @staticmethod
1094 1105 def globmatch(el, l):
1095 1106 # The only supported special characters are * and ? plus / which also
1096 1107 # matches \ on windows. Escaping of these characters is supported.
1097 1108 if el + b'\n' == l:
1098 1109 if os.altsep:
1099 1110 # matching on "/" is not needed for this line
1100 1111 for pat in checkcodeglobpats:
1101 1112 if pat.match(el):
1102 1113 return True
1103 1114 return b'-glob'
1104 1115 return True
1105 1116 i, n = 0, len(el)
1106 1117 res = b''
1107 1118 while i < n:
1108 1119 c = el[i:i + 1]
1109 1120 i += 1
1110 1121 if c == b'\\' and i < n and el[i:i + 1] in b'*?\\/':
1111 1122 res += el[i - 1:i + 1]
1112 1123 i += 1
1113 1124 elif c == b'*':
1114 1125 res += b'.*'
1115 1126 elif c == b'?':
1116 1127 res += b'.'
1117 1128 elif c == b'/' and os.altsep:
1118 1129 res += b'[/\\\\]'
1119 1130 else:
1120 1131 res += re.escape(c)
1121 1132 return TTest.rematch(res, l)
1122 1133
1123 1134 @staticmethod
1124 1135 def linematch(el, l):
1125 1136 if el == l: # perfect match (fast)
1126 1137 return True
1127 1138 if el:
1128 1139 if el.endswith(b" (esc)\n"):
1129 1140 if PYTHON3:
1130 1141 el = el[:-7].decode('unicode_escape') + '\n'
1131 1142 el = el.encode('utf-8')
1132 1143 else:
1133 1144 el = el[:-7].decode('string-escape') + '\n'
1134 1145 if el == l or os.name == 'nt' and el[:-1] + b'\r\n' == l:
1135 1146 return True
1136 1147 if el.endswith(b" (re)\n"):
1137 1148 return TTest.rematch(el[:-6], l)
1138 1149 if el.endswith(b" (glob)\n"):
1139 1150 # ignore '(glob)' added to l by 'replacements'
1140 1151 if l.endswith(b" (glob)\n"):
1141 1152 l = l[:-8] + b"\n"
1142 1153 return TTest.globmatch(el[:-8], l)
1143 1154 if os.altsep and l.replace(b'\\', b'/') == el:
1144 1155 return b'+glob'
1145 1156 return False
1146 1157
1147 1158 @staticmethod
1148 1159 def parsehghaveoutput(lines):
1149 1160 '''Parse hghave log lines.
1150 1161
1151 1162 Return tuple of lists (missing, failed):
1152 1163 * the missing/unknown features
1153 1164 * the features for which existence check failed'''
1154 1165 missing = []
1155 1166 failed = []
1156 1167 for line in lines:
1157 1168 if line.startswith(TTest.SKIPPED_PREFIX):
1158 1169 line = line.splitlines()[0]
1159 1170 missing.append(line[len(TTest.SKIPPED_PREFIX):])
1160 1171 elif line.startswith(TTest.FAILED_PREFIX):
1161 1172 line = line.splitlines()[0]
1162 1173 failed.append(line[len(TTest.FAILED_PREFIX):])
1163 1174
1164 1175 return missing, failed
1165 1176
1166 1177 @staticmethod
1167 1178 def _escapef(m):
1168 1179 return TTest.ESCAPEMAP[m.group(0)]
1169 1180
1170 1181 @staticmethod
1171 1182 def _stringescape(s):
1172 1183 return TTest.ESCAPESUB(TTest._escapef, s)
1173 1184
1174 1185 iolock = threading.RLock()
1175 1186
1176 1187 class SkipTest(Exception):
1177 1188 """Raised to indicate that a test is to be skipped."""
1178 1189
1179 1190 class IgnoreTest(Exception):
1180 1191 """Raised to indicate that a test is to be ignored."""
1181 1192
1182 1193 class WarnTest(Exception):
1183 1194 """Raised to indicate that a test warned."""
1184 1195
1185 1196 class TestResult(unittest._TextTestResult):
1186 1197 """Holds results when executing via unittest."""
1187 1198 # Don't worry too much about accessing the non-public _TextTestResult.
1188 1199 # It is relatively common in Python testing tools.
1189 1200 def __init__(self, options, *args, **kwargs):
1190 1201 super(TestResult, self).__init__(*args, **kwargs)
1191 1202
1192 1203 self._options = options
1193 1204
1194 1205 # unittest.TestResult didn't have skipped until 2.7. We need to
1195 1206 # polyfill it.
1196 1207 self.skipped = []
1197 1208
1198 1209 # We have a custom "ignored" result that isn't present in any Python
1199 1210 # unittest implementation. It is very similar to skipped. It may make
1200 1211 # sense to map it into skip some day.
1201 1212 self.ignored = []
1202 1213
1203 1214 # We have a custom "warned" result that isn't present in any Python
1204 1215 # unittest implementation. It is very similar to failed. It may make
1205 1216 # sense to map it into fail some day.
1206 1217 self.warned = []
1207 1218
1208 1219 self.times = []
1209 1220 self._firststarttime = None
1210 1221 # Data stored for the benefit of generating xunit reports.
1211 1222 self.successes = []
1212 1223 self.faildata = {}
1213 1224
1214 1225 def addFailure(self, test, reason):
1215 1226 self.failures.append((test, reason))
1216 1227
1217 1228 if self._options.first:
1218 1229 self.stop()
1219 1230 else:
1220 1231 with iolock:
1221 1232 if not self._options.nodiff:
1222 1233 self.stream.write('\nERROR: %s output changed\n' % test)
1223 1234
1224 1235 self.stream.write('!')
1225 1236 self.stream.flush()
1226 1237
1227 1238 def addSuccess(self, test):
1228 1239 with iolock:
1229 1240 super(TestResult, self).addSuccess(test)
1230 1241 self.successes.append(test)
1231 1242
1232 1243 def addError(self, test, err):
1233 1244 super(TestResult, self).addError(test, err)
1234 1245 if self._options.first:
1235 1246 self.stop()
1236 1247
1237 1248 # Polyfill.
1238 1249 def addSkip(self, test, reason):
1239 1250 self.skipped.append((test, reason))
1240 1251 with iolock:
1241 1252 if self.showAll:
1242 1253 self.stream.writeln('skipped %s' % reason)
1243 1254 else:
1244 1255 self.stream.write('s')
1245 1256 self.stream.flush()
1246 1257
1247 1258 def addIgnore(self, test, reason):
1248 1259 self.ignored.append((test, reason))
1249 1260 with iolock:
1250 1261 if self.showAll:
1251 1262 self.stream.writeln('ignored %s' % reason)
1252 1263 else:
1253 1264 if reason not in ('not retesting', "doesn't match keyword"):
1254 1265 self.stream.write('i')
1255 1266 else:
1256 1267 self.testsRun += 1
1257 1268 self.stream.flush()
1258 1269
1259 1270 def addWarn(self, test, reason):
1260 1271 self.warned.append((test, reason))
1261 1272
1262 1273 if self._options.first:
1263 1274 self.stop()
1264 1275
1265 1276 with iolock:
1266 1277 if self.showAll:
1267 1278 self.stream.writeln('warned %s' % reason)
1268 1279 else:
1269 1280 self.stream.write('~')
1270 1281 self.stream.flush()
1271 1282
1272 1283 def addOutputMismatch(self, test, ret, got, expected):
1273 1284 """Record a mismatch in test output for a particular test."""
1274 1285 if self.shouldStop:
1275 1286 # don't print, some other test case already failed and
1276 1287 # printed, we're just stale and probably failed due to our
1277 1288 # temp dir getting cleaned up.
1278 1289 return
1279 1290
1280 1291 accepted = False
1281 1292 failed = False
1282 1293 lines = []
1283 1294
1284 1295 with iolock:
1285 1296 if self._options.nodiff:
1286 1297 pass
1287 1298 elif self._options.view:
1288 1299 v = self._options.view
1289 1300 if PYTHON3:
1290 1301 v = _bytespath(v)
1291 1302 os.system(b"%s %s %s" %
1292 1303 (v, test.refpath, test.errpath))
1293 1304 else:
1294 1305 servefail, lines = getdiff(expected, got,
1295 1306 test.refpath, test.errpath)
1296 1307 if servefail:
1297 1308 self.addFailure(
1298 1309 test,
1299 1310 'server failed to start (HGPORT=%s)' % test._startport)
1300 1311 else:
1301 1312 self.stream.write('\n')
1302 1313 for line in lines:
1303 1314 if PYTHON3:
1304 1315 self.stream.flush()
1305 1316 self.stream.buffer.write(line)
1306 1317 self.stream.buffer.flush()
1307 1318 else:
1308 1319 self.stream.write(line)
1309 1320 self.stream.flush()
1310 1321
1311 1322 # handle interactive prompt without releasing iolock
1312 1323 if self._options.interactive:
1313 1324 self.stream.write('Accept this change? [n] ')
1314 1325 answer = sys.stdin.readline().strip()
1315 1326 if answer.lower() in ('y', 'yes'):
1316 1327 if test.name.endswith('.t'):
1317 1328 rename(test.errpath, test.path)
1318 1329 else:
1319 1330 rename(test.errpath, '%s.out' % test.path)
1320 1331 accepted = True
1321 1332 if not accepted and not failed:
1322 1333 self.faildata[test.name] = b''.join(lines)
1323 1334
1324 1335 return accepted
1325 1336
1326 1337 def startTest(self, test):
1327 1338 super(TestResult, self).startTest(test)
1328 1339
1329 1340 # os.times module computes the user time and system time spent by
1330 1341 # child's processes along with real elapsed time taken by a process.
1331 1342 # This module has one limitation. It can only work for Linux user
1332 1343 # and not for Windows.
1333 1344 test.started = os.times()
1334 1345 if self._firststarttime is None: # thread racy but irrelevant
1335 1346 self._firststarttime = test.started[4]
1336 1347
1337 1348 def stopTest(self, test, interrupted=False):
1338 1349 super(TestResult, self).stopTest(test)
1339 1350
1340 1351 test.stopped = os.times()
1341 1352
1342 1353 starttime = test.started
1343 1354 endtime = test.stopped
1344 1355 origin = self._firststarttime
1345 1356 self.times.append((test.name,
1346 1357 endtime[2] - starttime[2], # user space CPU time
1347 1358 endtime[3] - starttime[3], # sys space CPU time
1348 1359 endtime[4] - starttime[4], # real time
1349 1360 starttime[4] - origin, # start date in run context
1350 1361 endtime[4] - origin, # end date in run context
1351 1362 ))
1352 1363
1353 1364 if interrupted:
1354 1365 with iolock:
1355 1366 self.stream.writeln('INTERRUPTED: %s (after %d seconds)' % (
1356 1367 test.name, self.times[-1][3]))
1357 1368
1358 1369 class TestSuite(unittest.TestSuite):
1359 1370 """Custom unittest TestSuite that knows how to execute Mercurial tests."""
1360 1371
1361 1372 def __init__(self, testdir, jobs=1, whitelist=None, blacklist=None,
1362 1373 retest=False, keywords=None, loop=False, runs_per_test=1,
1363 1374 loadtest=None,
1364 1375 *args, **kwargs):
1365 1376 """Create a new instance that can run tests with a configuration.
1366 1377
1367 1378 testdir specifies the directory where tests are executed from. This
1368 1379 is typically the ``tests`` directory from Mercurial's source
1369 1380 repository.
1370 1381
1371 1382 jobs specifies the number of jobs to run concurrently. Each test
1372 1383 executes on its own thread. Tests actually spawn new processes, so
1373 1384 state mutation should not be an issue.
1374 1385
1375 1386 whitelist and blacklist denote tests that have been whitelisted and
1376 1387 blacklisted, respectively. These arguments don't belong in TestSuite.
1377 1388 Instead, whitelist and blacklist should be handled by the thing that
1378 1389 populates the TestSuite with tests. They are present to preserve
1379 1390 backwards compatible behavior which reports skipped tests as part
1380 1391 of the results.
1381 1392
1382 1393 retest denotes whether to retest failed tests. This arguably belongs
1383 1394 outside of TestSuite.
1384 1395
1385 1396 keywords denotes key words that will be used to filter which tests
1386 1397 to execute. This arguably belongs outside of TestSuite.
1387 1398
1388 1399 loop denotes whether to loop over tests forever.
1389 1400 """
1390 1401 super(TestSuite, self).__init__(*args, **kwargs)
1391 1402
1392 1403 self._jobs = jobs
1393 1404 self._whitelist = whitelist
1394 1405 self._blacklist = blacklist
1395 1406 self._retest = retest
1396 1407 self._keywords = keywords
1397 1408 self._loop = loop
1398 1409 self._runs_per_test = runs_per_test
1399 1410 self._loadtest = loadtest
1400 1411
1401 1412 def run(self, result):
1402 1413 # We have a number of filters that need to be applied. We do this
1403 1414 # here instead of inside Test because it makes the running logic for
1404 1415 # Test simpler.
1405 1416 tests = []
1406 1417 num_tests = [0]
1407 1418 for test in self._tests:
1408 1419 def get():
1409 1420 num_tests[0] += 1
1410 1421 if getattr(test, 'should_reload', False):
1411 1422 return self._loadtest(test.bname, num_tests[0])
1412 1423 return test
1413 1424 if not os.path.exists(test.path):
1414 1425 result.addSkip(test, "Doesn't exist")
1415 1426 continue
1416 1427
1417 1428 if not (self._whitelist and test.name in self._whitelist):
1418 1429 if self._blacklist and test.bname in self._blacklist:
1419 1430 result.addSkip(test, 'blacklisted')
1420 1431 continue
1421 1432
1422 1433 if self._retest and not os.path.exists(test.errpath):
1423 1434 result.addIgnore(test, 'not retesting')
1424 1435 continue
1425 1436
1426 1437 if self._keywords:
1427 1438 f = open(test.path, 'rb')
1428 1439 t = f.read().lower() + test.bname.lower()
1429 1440 f.close()
1430 1441 ignored = False
1431 1442 for k in self._keywords.lower().split():
1432 1443 if k not in t:
1433 1444 result.addIgnore(test, "doesn't match keyword")
1434 1445 ignored = True
1435 1446 break
1436 1447
1437 1448 if ignored:
1438 1449 continue
1439 1450 for _ in xrange(self._runs_per_test):
1440 1451 tests.append(get())
1441 1452
1442 1453 runtests = list(tests)
1443 1454 done = queue.Queue()
1444 1455 running = 0
1445 1456
1446 1457 def job(test, result):
1447 1458 try:
1448 1459 test(result)
1449 1460 done.put(None)
1450 1461 except KeyboardInterrupt:
1451 1462 pass
1452 1463 except: # re-raises
1453 1464 done.put(('!', test, 'run-test raised an error, see traceback'))
1454 1465 raise
1455 1466
1456 1467 stoppedearly = False
1457 1468
1458 1469 try:
1459 1470 while tests or running:
1460 1471 if not done.empty() or running == self._jobs or not tests:
1461 1472 try:
1462 1473 done.get(True, 1)
1463 1474 running -= 1
1464 1475 if result and result.shouldStop:
1465 1476 stoppedearly = True
1466 1477 break
1467 1478 except queue.Empty:
1468 1479 continue
1469 1480 if tests and not running == self._jobs:
1470 1481 test = tests.pop(0)
1471 1482 if self._loop:
1472 1483 if getattr(test, 'should_reload', False):
1473 1484 num_tests[0] += 1
1474 1485 tests.append(
1475 1486 self._loadtest(test.name, num_tests[0]))
1476 1487 else:
1477 1488 tests.append(test)
1478 1489 t = threading.Thread(target=job, name=test.name,
1479 1490 args=(test, result))
1480 1491 t.start()
1481 1492 running += 1
1482 1493
1483 1494 # If we stop early we still need to wait on started tests to
1484 1495 # finish. Otherwise, there is a race between the test completing
1485 1496 # and the test's cleanup code running. This could result in the
1486 1497 # test reporting incorrect.
1487 1498 if stoppedearly:
1488 1499 while running:
1489 1500 try:
1490 1501 done.get(True, 1)
1491 1502 running -= 1
1492 1503 except queue.Empty:
1493 1504 continue
1494 1505 except KeyboardInterrupt:
1495 1506 for test in runtests:
1496 1507 test.abort()
1497 1508
1498 1509 return result
1499 1510
1500 1511 class TextTestRunner(unittest.TextTestRunner):
1501 1512 """Custom unittest test runner that uses appropriate settings."""
1502 1513
1503 1514 def __init__(self, runner, *args, **kwargs):
1504 1515 super(TextTestRunner, self).__init__(*args, **kwargs)
1505 1516
1506 1517 self._runner = runner
1507 1518
1508 1519 def run(self, test):
1509 1520 result = TestResult(self._runner.options, self.stream,
1510 1521 self.descriptions, self.verbosity)
1511 1522
1512 1523 test(result)
1513 1524
1514 1525 failed = len(result.failures)
1515 1526 warned = len(result.warned)
1516 1527 skipped = len(result.skipped)
1517 1528 ignored = len(result.ignored)
1518 1529
1519 1530 with iolock:
1520 1531 self.stream.writeln('')
1521 1532
1522 1533 if not self._runner.options.noskips:
1523 1534 for test, msg in result.skipped:
1524 1535 self.stream.writeln('Skipped %s: %s' % (test.name, msg))
1525 1536 for test, msg in result.warned:
1526 1537 self.stream.writeln('Warned %s: %s' % (test.name, msg))
1527 1538 for test, msg in result.failures:
1528 1539 self.stream.writeln('Failed %s: %s' % (test.name, msg))
1529 1540 for test, msg in result.errors:
1530 1541 self.stream.writeln('Errored %s: %s' % (test.name, msg))
1531 1542
1532 1543 if self._runner.options.xunit:
1533 1544 xuf = open(self._runner.options.xunit, 'wb')
1534 1545 try:
1535 1546 timesd = dict((t[0], t[3]) for t in result.times)
1536 1547 doc = minidom.Document()
1537 1548 s = doc.createElement('testsuite')
1538 1549 s.setAttribute('name', 'run-tests')
1539 1550 s.setAttribute('tests', str(result.testsRun))
1540 1551 s.setAttribute('errors', "0") # TODO
1541 1552 s.setAttribute('failures', str(failed))
1542 1553 s.setAttribute('skipped', str(skipped + ignored))
1543 1554 doc.appendChild(s)
1544 1555 for tc in result.successes:
1545 1556 t = doc.createElement('testcase')
1546 1557 t.setAttribute('name', tc.name)
1547 1558 t.setAttribute('time', '%.3f' % timesd[tc.name])
1548 1559 s.appendChild(t)
1549 1560 for tc, err in sorted(result.faildata.items()):
1550 1561 t = doc.createElement('testcase')
1551 1562 t.setAttribute('name', tc)
1552 1563 t.setAttribute('time', '%.3f' % timesd[tc])
1553 1564 # createCDATASection expects a unicode or it will
1554 1565 # convert using default conversion rules, which will
1555 1566 # fail if string isn't ASCII.
1556 1567 err = cdatasafe(err).decode('utf-8', 'replace')
1557 1568 cd = doc.createCDATASection(err)
1558 1569 t.appendChild(cd)
1559 1570 s.appendChild(t)
1560 1571 xuf.write(doc.toprettyxml(indent=' ', encoding='utf-8'))
1561 1572 finally:
1562 1573 xuf.close()
1563 1574
1564 1575 if self._runner.options.json:
1565 1576 if json is None:
1566 1577 raise ImportError("json module not installed")
1567 1578 jsonpath = os.path.join(self._runner._testdir, 'report.json')
1568 1579 fp = open(jsonpath, 'w')
1569 1580 try:
1570 1581 timesd = {}
1571 1582 for tdata in result.times:
1572 1583 test = tdata[0]
1573 1584 timesd[test] = tdata[1:]
1574 1585
1575 1586 outcome = {}
1576 1587 groups = [('success', ((tc, None)
1577 1588 for tc in result.successes)),
1578 1589 ('failure', result.failures),
1579 1590 ('skip', result.skipped)]
1580 1591 for res, testcases in groups:
1581 1592 for tc, __ in testcases:
1582 1593 tres = {'result': res,
1583 1594 'time': ('%0.3f' % timesd[tc.name][2]),
1584 1595 'cuser': ('%0.3f' % timesd[tc.name][0]),
1585 1596 'csys': ('%0.3f' % timesd[tc.name][1]),
1586 1597 'start': ('%0.3f' % timesd[tc.name][3]),
1587 1598 'end': ('%0.3f' % timesd[tc.name][4])}
1588 1599 outcome[tc.name] = tres
1589 1600 jsonout = json.dumps(outcome, sort_keys=True, indent=4)
1590 1601 fp.writelines(("testreport =", jsonout))
1591 1602 finally:
1592 1603 fp.close()
1593 1604
1594 1605 self._runner._checkhglib('Tested')
1595 1606
1596 1607 self.stream.writeln(
1597 1608 '# Ran %d tests, %d skipped, %d warned, %d failed.'
1598 1609 % (result.testsRun,
1599 1610 skipped + ignored, warned, failed))
1600 1611 if failed:
1601 1612 self.stream.writeln('python hash seed: %s' %
1602 1613 os.environ['PYTHONHASHSEED'])
1603 1614 if self._runner.options.time:
1604 1615 self.printtimes(result.times)
1605 1616
1606 1617 return result
1607 1618
1608 1619 def printtimes(self, times):
1609 1620 # iolock held by run
1610 1621 self.stream.writeln('# Producing time report')
1611 1622 times.sort(key=lambda t: (t[3]))
1612 1623 cols = '%7.3f %7.3f %7.3f %7.3f %7.3f %s'
1613 1624 self.stream.writeln('%-7s %-7s %-7s %-7s %-7s %s' %
1614 1625 ('start', 'end', 'cuser', 'csys', 'real', 'Test'))
1615 1626 for tdata in times:
1616 1627 test = tdata[0]
1617 1628 cuser, csys, real, start, end = tdata[1:6]
1618 1629 self.stream.writeln(cols % (start, end, cuser, csys, real, test))
1619 1630
1620 1631 class TestRunner(object):
1621 1632 """Holds context for executing tests.
1622 1633
1623 1634 Tests rely on a lot of state. This object holds it for them.
1624 1635 """
1625 1636
1626 1637 # Programs required to run tests.
1627 1638 REQUIREDTOOLS = [
1628 1639 os.path.basename(_bytespath(sys.executable)),
1629 1640 b'diff',
1630 1641 b'grep',
1631 1642 b'unzip',
1632 1643 b'gunzip',
1633 1644 b'bunzip2',
1634 1645 b'sed',
1635 1646 ]
1636 1647
1637 1648 # Maps file extensions to test class.
1638 1649 TESTTYPES = [
1639 1650 (b'.py', PythonTest),
1640 1651 (b'.t', TTest),
1641 1652 ]
1642 1653
1643 1654 def __init__(self):
1644 1655 self.options = None
1645 1656 self._hgroot = None
1646 1657 self._testdir = None
1647 1658 self._hgtmp = None
1648 1659 self._installdir = None
1649 1660 self._bindir = None
1650 1661 self._tmpbinddir = None
1651 1662 self._pythondir = None
1652 1663 self._coveragefile = None
1653 1664 self._createdfiles = []
1654 1665 self._hgpath = None
1655 1666 self._portoffset = 0
1656 1667 self._ports = {}
1657 1668
1658 1669 def run(self, args, parser=None):
1659 1670 """Run the test suite."""
1660 1671 oldmask = os.umask(0o22)
1661 1672 try:
1662 1673 parser = parser or getparser()
1663 1674 options, args = parseargs(args, parser)
1664 1675 # positional arguments are paths to test files to run, so
1665 1676 # we make sure they're all bytestrings
1666 1677 args = [_bytespath(a) for a in args]
1667 1678 self.options = options
1668 1679
1669 1680 self._checktools()
1670 1681 tests = self.findtests(args)
1671 1682 if options.profile_runner:
1672 1683 import statprof
1673 1684 statprof.start()
1674 1685 result = self._run(tests)
1675 1686 if options.profile_runner:
1676 1687 statprof.stop()
1677 1688 statprof.display()
1678 1689 return result
1679 1690
1680 1691 finally:
1681 1692 os.umask(oldmask)
1682 1693
1683 1694 def _run(self, tests):
1684 1695 if self.options.random:
1685 1696 random.shuffle(tests)
1686 1697 else:
1687 1698 # keywords for slow tests
1688 1699 slow = {b'svn': 10,
1689 1700 b'gendoc': 10,
1690 1701 b'check-code-hg': 100,
1691 1702 }
1692 1703 def sortkey(f):
1693 1704 # run largest tests first, as they tend to take the longest
1694 1705 try:
1695 1706 val = -os.stat(f).st_size
1696 1707 except OSError as e:
1697 1708 if e.errno != errno.ENOENT:
1698 1709 raise
1699 1710 return -1e9 # file does not exist, tell early
1700 1711 for kw, mul in slow.iteritems():
1701 1712 if kw in f:
1702 1713 val *= mul
1703 1714 return val
1704 1715 tests.sort(key=sortkey)
1705 1716
1706 1717 self._testdir = osenvironb[b'TESTDIR'] = getattr(
1707 1718 os, 'getcwdb', os.getcwd)()
1708 1719
1709 1720 if 'PYTHONHASHSEED' not in os.environ:
1710 1721 # use a random python hash seed all the time
1711 1722 # we do the randomness ourself to know what seed is used
1712 1723 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
1713 1724
1714 1725 if self.options.tmpdir:
1715 1726 self.options.keep_tmpdir = True
1716 1727 tmpdir = _bytespath(self.options.tmpdir)
1717 1728 if os.path.exists(tmpdir):
1718 1729 # Meaning of tmpdir has changed since 1.3: we used to create
1719 1730 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
1720 1731 # tmpdir already exists.
1721 1732 print("error: temp dir %r already exists" % tmpdir)
1722 1733 return 1
1723 1734
1724 1735 # Automatically removing tmpdir sounds convenient, but could
1725 1736 # really annoy anyone in the habit of using "--tmpdir=/tmp"
1726 1737 # or "--tmpdir=$HOME".
1727 1738 #vlog("# Removing temp dir", tmpdir)
1728 1739 #shutil.rmtree(tmpdir)
1729 1740 os.makedirs(tmpdir)
1730 1741 else:
1731 1742 d = None
1732 1743 if os.name == 'nt':
1733 1744 # without this, we get the default temp dir location, but
1734 1745 # in all lowercase, which causes troubles with paths (issue3490)
1735 1746 d = osenvironb.get(b'TMP', None)
1736 1747 # FILE BUG: mkdtemp works only on unicode in Python 3
1737 tmpdir = tempfile.mkdtemp('', 'hgtests.',
1738 d and d.decode('utf-8')).encode('utf-8')
1748 tmpdir = tempfile.mkdtemp('', 'hgtests.', d and _strpath(d))
1749 tmpdir = _bytespath(tmpdir)
1739 1750
1740 1751 self._hgtmp = osenvironb[b'HGTMP'] = (
1741 1752 os.path.realpath(tmpdir))
1742 1753
1743 1754 if self.options.with_hg:
1744 1755 self._installdir = None
1745 1756 whg = self.options.with_hg
1746 1757 # If --with-hg is not specified, we have bytes already,
1747 1758 # but if it was specified in python3 we get a str, so we
1748 1759 # have to encode it back into a bytes.
1749 1760 if PYTHON3:
1750 1761 if not isinstance(whg, bytes):
1751 1762 whg = _bytespath(whg)
1752 1763 self._bindir = os.path.dirname(os.path.realpath(whg))
1753 1764 assert isinstance(self._bindir, bytes)
1754 1765 self._tmpbindir = os.path.join(self._hgtmp, b'install', b'bin')
1755 1766 os.makedirs(self._tmpbindir)
1756 1767
1757 1768 # This looks redundant with how Python initializes sys.path from
1758 1769 # the location of the script being executed. Needed because the
1759 1770 # "hg" specified by --with-hg is not the only Python script
1760 1771 # executed in the test suite that needs to import 'mercurial'
1761 1772 # ... which means it's not really redundant at all.
1762 1773 self._pythondir = self._bindir
1763 1774 else:
1764 1775 self._installdir = os.path.join(self._hgtmp, b"install")
1765 1776 self._bindir = osenvironb[b"BINDIR"] = \
1766 1777 os.path.join(self._installdir, b"bin")
1767 1778 self._tmpbindir = self._bindir
1768 1779 self._pythondir = os.path.join(self._installdir, b"lib", b"python")
1769 1780
1770 1781 osenvironb[b"BINDIR"] = self._bindir
1771 1782 osenvironb[b"PYTHON"] = PYTHON
1772 1783
1773 1784 fileb = _bytespath(__file__)
1774 1785 runtestdir = os.path.abspath(os.path.dirname(fileb))
1775 1786 if PYTHON3:
1776 1787 sepb = _bytespath(os.pathsep)
1777 1788 else:
1778 1789 sepb = os.pathsep
1779 1790 path = [self._bindir, runtestdir] + osenvironb[b"PATH"].split(sepb)
1780 1791 if os.path.islink(__file__):
1781 1792 # test helper will likely be at the end of the symlink
1782 1793 realfile = os.path.realpath(fileb)
1783 1794 realdir = os.path.abspath(os.path.dirname(realfile))
1784 1795 path.insert(2, realdir)
1785 1796 if self._tmpbindir != self._bindir:
1786 1797 path = [self._tmpbindir] + path
1787 1798 osenvironb[b"PATH"] = sepb.join(path)
1788 1799
1789 1800 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
1790 1801 # can run .../tests/run-tests.py test-foo where test-foo
1791 1802 # adds an extension to HGRC. Also include run-test.py directory to
1792 1803 # import modules like heredoctest.
1793 1804 pypath = [self._pythondir, self._testdir, runtestdir]
1794 1805 # We have to augment PYTHONPATH, rather than simply replacing
1795 1806 # it, in case external libraries are only available via current
1796 1807 # PYTHONPATH. (In particular, the Subversion bindings on OS X
1797 1808 # are in /opt/subversion.)
1798 1809 oldpypath = osenvironb.get(IMPL_PATH)
1799 1810 if oldpypath:
1800 1811 pypath.append(oldpypath)
1801 1812 osenvironb[IMPL_PATH] = sepb.join(pypath)
1802 1813
1803 1814 if self.options.pure:
1804 1815 os.environ["HGTEST_RUN_TESTS_PURE"] = "--pure"
1805 1816
1806 1817 self._coveragefile = os.path.join(self._testdir, b'.coverage')
1807 1818
1808 1819 vlog("# Using TESTDIR", self._testdir)
1809 1820 vlog("# Using HGTMP", self._hgtmp)
1810 1821 vlog("# Using PATH", os.environ["PATH"])
1811 1822 vlog("# Using", IMPL_PATH, osenvironb[IMPL_PATH])
1812 1823
1813 1824 try:
1814 1825 return self._runtests(tests) or 0
1815 1826 finally:
1816 1827 time.sleep(.1)
1817 1828 self._cleanup()
1818 1829
1819 1830 def findtests(self, args):
1820 1831 """Finds possible test files from arguments.
1821 1832
1822 1833 If you wish to inject custom tests into the test harness, this would
1823 1834 be a good function to monkeypatch or override in a derived class.
1824 1835 """
1825 1836 if not args:
1826 1837 if self.options.changed:
1827 1838 proc = Popen4('hg st --rev "%s" -man0 .' %
1828 1839 self.options.changed, None, 0)
1829 1840 stdout, stderr = proc.communicate()
1830 1841 args = stdout.strip(b'\0').split(b'\0')
1831 1842 else:
1832 1843 args = os.listdir(b'.')
1833 1844
1834 1845 return [t for t in args
1835 1846 if os.path.basename(t).startswith(b'test-')
1836 1847 and (t.endswith(b'.py') or t.endswith(b'.t'))]
1837 1848
1838 1849 def _runtests(self, tests):
1839 1850 try:
1840 1851 if self._installdir:
1841 1852 self._installhg()
1842 1853 self._checkhglib("Testing")
1843 1854 else:
1844 1855 self._usecorrectpython()
1845 1856
1846 1857 if self.options.restart:
1847 1858 orig = list(tests)
1848 1859 while tests:
1849 1860 if os.path.exists(tests[0] + ".err"):
1850 1861 break
1851 1862 tests.pop(0)
1852 1863 if not tests:
1853 1864 print("running all tests")
1854 1865 tests = orig
1855 1866
1856 1867 tests = [self._gettest(t, i) for i, t in enumerate(tests)]
1857 1868
1858 1869 failed = False
1859 1870 warned = False
1860 1871 kws = self.options.keywords
1861 1872 if kws is not None and PYTHON3:
1862 1873 kws = kws.encode('utf-8')
1863 1874
1864 1875 suite = TestSuite(self._testdir,
1865 1876 jobs=self.options.jobs,
1866 1877 whitelist=self.options.whitelisted,
1867 1878 blacklist=self.options.blacklist,
1868 1879 retest=self.options.retest,
1869 1880 keywords=kws,
1870 1881 loop=self.options.loop,
1871 1882 runs_per_test=self.options.runs_per_test,
1872 1883 tests=tests, loadtest=self._gettest)
1873 1884 verbosity = 1
1874 1885 if self.options.verbose:
1875 1886 verbosity = 2
1876 1887 runner = TextTestRunner(self, verbosity=verbosity)
1877 1888 result = runner.run(suite)
1878 1889
1879 1890 if result.failures:
1880 1891 failed = True
1881 1892 if result.warned:
1882 1893 warned = True
1883 1894
1884 1895 if self.options.anycoverage:
1885 1896 self._outputcoverage()
1886 1897 except KeyboardInterrupt:
1887 1898 failed = True
1888 1899 print("\ninterrupted!")
1889 1900
1890 1901 if failed:
1891 1902 return 1
1892 1903 if warned:
1893 1904 return 80
1894 1905
1895 1906 def _getport(self, count):
1896 1907 port = self._ports.get(count) # do we have a cached entry?
1897 1908 if port is None:
1898 1909 port = self.options.port + self._portoffset
1899 1910 portneeded = 3
1900 1911 # above 100 tries we just give up and let test reports failure
1901 1912 for tries in xrange(100):
1902 1913 allfree = True
1903 1914 for idx in xrange(portneeded):
1904 1915 if not checkportisavailable(port + idx):
1905 1916 allfree = False
1906 1917 break
1907 1918 self._portoffset += portneeded
1908 1919 if allfree:
1909 1920 break
1910 1921 self._ports[count] = port
1911 1922 return port
1912 1923
1913 1924 def _gettest(self, test, count):
1914 1925 """Obtain a Test by looking at its filename.
1915 1926
1916 1927 Returns a Test instance. The Test may not be runnable if it doesn't
1917 1928 map to a known type.
1918 1929 """
1919 1930 lctest = test.lower()
1920 1931 testcls = Test
1921 1932
1922 1933 for ext, cls in self.TESTTYPES:
1923 1934 if lctest.endswith(ext):
1924 1935 testcls = cls
1925 1936 break
1926 1937
1927 1938 refpath = os.path.join(self._testdir, test)
1928 1939 tmpdir = os.path.join(self._hgtmp, b'child%d' % count)
1929 1940
1930 1941 t = testcls(refpath, tmpdir,
1931 1942 keeptmpdir=self.options.keep_tmpdir,
1932 1943 debug=self.options.debug,
1933 1944 timeout=self.options.timeout,
1934 1945 startport=self._getport(count),
1935 1946 extraconfigopts=self.options.extra_config_opt,
1936 1947 py3kwarnings=self.options.py3k_warnings,
1937 1948 shell=self.options.shell)
1938 1949 t.should_reload = True
1939 1950 return t
1940 1951
1941 1952 def _cleanup(self):
1942 1953 """Clean up state from this test invocation."""
1943 1954
1944 1955 if self.options.keep_tmpdir:
1945 1956 return
1946 1957
1947 1958 vlog("# Cleaning up HGTMP", self._hgtmp)
1948 1959 shutil.rmtree(self._hgtmp, True)
1949 1960 for f in self._createdfiles:
1950 1961 try:
1951 1962 os.remove(f)
1952 1963 except OSError:
1953 1964 pass
1954 1965
1955 1966 def _usecorrectpython(self):
1956 1967 """Configure the environment to use the appropriate Python in tests."""
1957 1968 # Tests must use the same interpreter as us or bad things will happen.
1958 1969 pyexename = sys.platform == 'win32' and b'python.exe' or b'python'
1959 1970 if getattr(os, 'symlink', None):
1960 1971 vlog("# Making python executable in test path a symlink to '%s'" %
1961 1972 sys.executable)
1962 1973 mypython = os.path.join(self._tmpbindir, pyexename)
1963 1974 try:
1964 1975 if os.readlink(mypython) == sys.executable:
1965 1976 return
1966 1977 os.unlink(mypython)
1967 1978 except OSError as err:
1968 1979 if err.errno != errno.ENOENT:
1969 1980 raise
1970 1981 if self._findprogram(pyexename) != sys.executable:
1971 1982 try:
1972 1983 os.symlink(sys.executable, mypython)
1973 1984 self._createdfiles.append(mypython)
1974 1985 except OSError as err:
1975 1986 # child processes may race, which is harmless
1976 1987 if err.errno != errno.EEXIST:
1977 1988 raise
1978 1989 else:
1979 1990 exedir, exename = os.path.split(sys.executable)
1980 1991 vlog("# Modifying search path to find %s as %s in '%s'" %
1981 1992 (exename, pyexename, exedir))
1982 1993 path = os.environ['PATH'].split(os.pathsep)
1983 1994 while exedir in path:
1984 1995 path.remove(exedir)
1985 1996 os.environ['PATH'] = os.pathsep.join([exedir] + path)
1986 1997 if not self._findprogram(pyexename):
1987 1998 print("WARNING: Cannot find %s in search path" % pyexename)
1988 1999
1989 2000 def _installhg(self):
1990 2001 """Install hg into the test environment.
1991 2002
1992 2003 This will also configure hg with the appropriate testing settings.
1993 2004 """
1994 2005 vlog("# Performing temporary installation of HG")
1995 2006 installerrs = os.path.join(b"tests", b"install.err")
1996 2007 compiler = ''
1997 2008 if self.options.compiler:
1998 2009 compiler = '--compiler ' + self.options.compiler
1999 2010 if self.options.pure:
2000 2011 pure = b"--pure"
2001 2012 else:
2002 2013 pure = b""
2003 2014 py3 = ''
2004 2015
2005 2016 # Run installer in hg root
2006 2017 script = os.path.realpath(sys.argv[0])
2007 2018 exe = sys.executable
2008 2019 if PYTHON3:
2009 2020 py3 = b'--c2to3'
2010 2021 compiler = _bytespath(compiler)
2011 2022 script = _bytespath(script)
2012 2023 exe = _bytespath(exe)
2013 2024 hgroot = os.path.dirname(os.path.dirname(script))
2014 2025 self._hgroot = hgroot
2015 2026 os.chdir(hgroot)
2016 2027 nohome = b'--home=""'
2017 2028 if os.name == 'nt':
2018 2029 # The --home="" trick works only on OS where os.sep == '/'
2019 2030 # because of a distutils convert_path() fast-path. Avoid it at
2020 2031 # least on Windows for now, deal with .pydistutils.cfg bugs
2021 2032 # when they happen.
2022 2033 nohome = b''
2023 2034 cmd = (b'%(exe)s setup.py %(py3)s %(pure)s clean --all'
2024 2035 b' build %(compiler)s --build-base="%(base)s"'
2025 2036 b' install --force --prefix="%(prefix)s"'
2026 2037 b' --install-lib="%(libdir)s"'
2027 2038 b' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
2028 2039 % {b'exe': exe, b'py3': py3, b'pure': pure,
2029 2040 b'compiler': compiler,
2030 2041 b'base': os.path.join(self._hgtmp, b"build"),
2031 2042 b'prefix': self._installdir, b'libdir': self._pythondir,
2032 2043 b'bindir': self._bindir,
2033 2044 b'nohome': nohome, b'logfile': installerrs})
2034 2045
2035 2046 # setuptools requires install directories to exist.
2036 2047 def makedirs(p):
2037 2048 try:
2038 2049 os.makedirs(p)
2039 2050 except OSError as e:
2040 2051 if e.errno != errno.EEXIST:
2041 2052 raise
2042 2053 makedirs(self._pythondir)
2043 2054 makedirs(self._bindir)
2044 2055
2045 2056 vlog("# Running", cmd)
2046 2057 if os.system(cmd) == 0:
2047 2058 if not self.options.verbose:
2048 2059 os.remove(installerrs)
2049 2060 else:
2050 2061 f = open(installerrs, 'rb')
2051 2062 for line in f:
2052 2063 if PYTHON3:
2053 2064 sys.stdout.buffer.write(line)
2054 2065 else:
2055 2066 sys.stdout.write(line)
2056 2067 f.close()
2057 2068 sys.exit(1)
2058 2069 os.chdir(self._testdir)
2059 2070
2060 2071 self._usecorrectpython()
2061 2072
2062 2073 if self.options.py3k_warnings and not self.options.anycoverage:
2063 2074 vlog("# Updating hg command to enable Py3k Warnings switch")
2064 2075 f = open(os.path.join(self._bindir, 'hg'), 'rb')
2065 2076 lines = [line.rstrip() for line in f]
2066 2077 lines[0] += ' -3'
2067 2078 f.close()
2068 2079 f = open(os.path.join(self._bindir, 'hg'), 'wb')
2069 2080 for line in lines:
2070 2081 f.write(line + '\n')
2071 2082 f.close()
2072 2083
2073 2084 hgbat = os.path.join(self._bindir, b'hg.bat')
2074 2085 if os.path.isfile(hgbat):
2075 2086 # hg.bat expects to be put in bin/scripts while run-tests.py
2076 2087 # installation layout put it in bin/ directly. Fix it
2077 2088 f = open(hgbat, 'rb')
2078 2089 data = f.read()
2079 2090 f.close()
2080 2091 if b'"%~dp0..\python" "%~dp0hg" %*' in data:
2081 2092 data = data.replace(b'"%~dp0..\python" "%~dp0hg" %*',
2082 2093 b'"%~dp0python" "%~dp0hg" %*')
2083 2094 f = open(hgbat, 'wb')
2084 2095 f.write(data)
2085 2096 f.close()
2086 2097 else:
2087 2098 print('WARNING: cannot fix hg.bat reference to python.exe')
2088 2099
2089 2100 if self.options.anycoverage:
2090 2101 custom = os.path.join(self._testdir, 'sitecustomize.py')
2091 2102 target = os.path.join(self._pythondir, 'sitecustomize.py')
2092 2103 vlog('# Installing coverage trigger to %s' % target)
2093 2104 shutil.copyfile(custom, target)
2094 2105 rc = os.path.join(self._testdir, '.coveragerc')
2095 2106 vlog('# Installing coverage rc to %s' % rc)
2096 2107 os.environ['COVERAGE_PROCESS_START'] = rc
2097 2108 covdir = os.path.join(self._installdir, '..', 'coverage')
2098 2109 try:
2099 2110 os.mkdir(covdir)
2100 2111 except OSError as e:
2101 2112 if e.errno != errno.EEXIST:
2102 2113 raise
2103 2114
2104 2115 os.environ['COVERAGE_DIR'] = covdir
2105 2116
2106 2117 def _checkhglib(self, verb):
2107 2118 """Ensure that the 'mercurial' package imported by python is
2108 2119 the one we expect it to be. If not, print a warning to stderr."""
2109 2120 if ((self._bindir == self._pythondir) and
2110 2121 (self._bindir != self._tmpbindir)):
2111 2122 # The pythondir has been inferred from --with-hg flag.
2112 2123 # We cannot expect anything sensible here.
2113 2124 return
2114 2125 expecthg = os.path.join(self._pythondir, b'mercurial')
2115 2126 actualhg = self._gethgpath()
2116 2127 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
2117 2128 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
2118 2129 ' (expected %s)\n'
2119 2130 % (verb, actualhg, expecthg))
2120 2131 def _gethgpath(self):
2121 2132 """Return the path to the mercurial package that is actually found by
2122 2133 the current Python interpreter."""
2123 2134 if self._hgpath is not None:
2124 2135 return self._hgpath
2125 2136
2126 2137 cmd = b'%s -c "import mercurial; print (mercurial.__path__[0])"'
2127 2138 cmd = cmd % PYTHON
2128 2139 if PYTHON3:
2129 cmd = cmd.decode('utf-8')
2140 cmd = _strpath(cmd)
2130 2141 pipe = os.popen(cmd)
2131 2142 try:
2132 2143 self._hgpath = _bytespath(pipe.read().strip())
2133 2144 finally:
2134 2145 pipe.close()
2135 2146
2136 2147 return self._hgpath
2137 2148
2138 2149 def _outputcoverage(self):
2139 2150 """Produce code coverage output."""
2140 2151 from coverage import coverage
2141 2152
2142 2153 vlog('# Producing coverage report')
2143 2154 # chdir is the easiest way to get short, relative paths in the
2144 2155 # output.
2145 2156 os.chdir(self._hgroot)
2146 2157 covdir = os.path.join(self._installdir, '..', 'coverage')
2147 2158 cov = coverage(data_file=os.path.join(covdir, 'cov'))
2148 2159
2149 2160 # Map install directory paths back to source directory.
2150 2161 cov.config.paths['srcdir'] = ['.', self._pythondir]
2151 2162
2152 2163 cov.combine()
2153 2164
2154 2165 omit = [os.path.join(x, '*') for x in [self._bindir, self._testdir]]
2155 2166 cov.report(ignore_errors=True, omit=omit)
2156 2167
2157 2168 if self.options.htmlcov:
2158 2169 htmldir = os.path.join(self._testdir, 'htmlcov')
2159 2170 cov.html_report(directory=htmldir, omit=omit)
2160 2171 if self.options.annotate:
2161 2172 adir = os.path.join(self._testdir, 'annotated')
2162 2173 if not os.path.isdir(adir):
2163 2174 os.mkdir(adir)
2164 2175 cov.annotate(directory=adir, omit=omit)
2165 2176
2166 2177 def _findprogram(self, program):
2167 2178 """Search PATH for a executable program"""
2168 2179 dpb = _bytespath(os.defpath)
2169 2180 sepb = _bytespath(os.pathsep)
2170 2181 for p in osenvironb.get(b'PATH', dpb).split(sepb):
2171 2182 name = os.path.join(p, program)
2172 2183 if os.name == 'nt' or os.access(name, os.X_OK):
2173 2184 return name
2174 2185 return None
2175 2186
2176 2187 def _checktools(self):
2177 2188 """Ensure tools required to run tests are present."""
2178 2189 for p in self.REQUIREDTOOLS:
2179 2190 if os.name == 'nt' and not p.endswith('.exe'):
2180 2191 p += '.exe'
2181 2192 found = self._findprogram(p)
2182 2193 if found:
2183 2194 vlog("# Found prerequisite", p, "at", found)
2184 2195 else:
2185 2196 print("WARNING: Did not find prerequisite tool: %s " % p)
2186 2197
2187 2198 if __name__ == '__main__':
2188 2199 runner = TestRunner()
2189 2200
2190 2201 try:
2191 2202 import msvcrt
2192 2203 msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
2193 2204 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
2194 2205 msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
2195 2206 except ImportError:
2196 2207 pass
2197 2208
2198 2209 sys.exit(runner.run(sys.argv[1:]))
General Comments 0
You need to be logged in to leave comments. Login now