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