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