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