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