##// END OF EJS Templates
run-tests: include testdir in $PATH so tests easily can use helper tools...
Mads Kiilerich -
r23859:661b246b default
parent child Browse files
Show More
@@ -1,1998 +1,1998 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 (r'(?m)^(saved backup bundle to .*\.hg)( \(glob\))?$',
631 631 r'\1 (glob)'),
632 632 ]
633 633
634 634 if os.name == 'nt':
635 635 r.append(
636 636 (''.join(c.isalpha() and '[%s%s]' % (c.lower(), c.upper()) or
637 637 c in '/\\' and r'[/\\]' or c.isdigit() and c or '\\' + c
638 638 for c in self._testtmp), '$TESTTMP'))
639 639 else:
640 640 r.append((re.escape(self._testtmp), '$TESTTMP'))
641 641
642 642 return r
643 643
644 644 def _getenv(self):
645 645 """Obtain environment variables to use during test execution."""
646 646 env = os.environ.copy()
647 647 env['TESTTMP'] = self._testtmp
648 648 env['HOME'] = self._testtmp
649 649 env["HGPORT"] = str(self._startport)
650 650 env["HGPORT1"] = str(self._startport + 1)
651 651 env["HGPORT2"] = str(self._startport + 2)
652 652 env["HGRCPATH"] = os.path.join(self._threadtmp, '.hgrc')
653 653 env["DAEMON_PIDS"] = os.path.join(self._threadtmp, 'daemon.pids')
654 654 env["HGEDITOR"] = ('"' + sys.executable + '"'
655 655 + ' -c "import sys; sys.exit(0)"')
656 656 env["HGMERGE"] = "internal:merge"
657 657 env["HGUSER"] = "test"
658 658 env["HGENCODING"] = "ascii"
659 659 env["HGENCODINGMODE"] = "strict"
660 660
661 661 # Reset some environment variables to well-known values so that
662 662 # the tests produce repeatable output.
663 663 env['LANG'] = env['LC_ALL'] = env['LANGUAGE'] = 'C'
664 664 env['TZ'] = 'GMT'
665 665 env["EMAIL"] = "Foo Bar <foo.bar@example.com>"
666 666 env['COLUMNS'] = '80'
667 667 env['TERM'] = 'xterm'
668 668
669 669 for k in ('HG HGPROF CDPATH GREP_OPTIONS http_proxy no_proxy ' +
670 670 'NO_PROXY').split():
671 671 if k in env:
672 672 del env[k]
673 673
674 674 # unset env related to hooks
675 675 for k in env.keys():
676 676 if k.startswith('HG_'):
677 677 del env[k]
678 678
679 679 return env
680 680
681 681 def _createhgrc(self, path):
682 682 """Create an hgrc file for this test."""
683 683 hgrc = open(path, 'wb')
684 684 hgrc.write('[ui]\n')
685 685 hgrc.write('slash = True\n')
686 686 hgrc.write('interactive = False\n')
687 687 hgrc.write('mergemarkers = detailed\n')
688 688 hgrc.write('promptecho = True\n')
689 689 hgrc.write('[defaults]\n')
690 690 hgrc.write('backout = -d "0 0"\n')
691 691 hgrc.write('commit = -d "0 0"\n')
692 692 hgrc.write('shelve = --date "0 0"\n')
693 693 hgrc.write('tag = -d "0 0"\n')
694 694 hgrc.write('[largefiles]\n')
695 695 hgrc.write('usercache = %s\n' %
696 696 (os.path.join(self._testtmp, '.cache/largefiles')))
697 697
698 698 for opt in self._extraconfigopts:
699 699 section, key = opt.split('.', 1)
700 700 assert '=' in key, ('extra config opt %s must '
701 701 'have an = for assignment' % opt)
702 702 hgrc.write('[%s]\n%s\n' % (section, key))
703 703 hgrc.close()
704 704
705 705 def fail(self, msg):
706 706 # unittest differentiates between errored and failed.
707 707 # Failed is denoted by AssertionError (by default at least).
708 708 raise AssertionError(msg)
709 709
710 710 class PythonTest(Test):
711 711 """A Python-based test."""
712 712
713 713 @property
714 714 def refpath(self):
715 715 return os.path.join(self._testdir, '%s.out' % self.name)
716 716
717 717 def _run(self, replacements, env):
718 718 py3kswitch = self._py3kwarnings and ' -3' or ''
719 719 cmd = '%s%s "%s"' % (PYTHON, py3kswitch, self.path)
720 720 vlog("# Running", cmd)
721 721 if os.name == 'nt':
722 722 replacements.append((r'\r\n', '\n'))
723 723 result = run(cmd, self._testtmp, replacements, env,
724 724 debug=self._debug, timeout=self._timeout)
725 725 if self._aborted:
726 726 raise KeyboardInterrupt()
727 727
728 728 return result
729 729
730 730 # This script may want to drop globs from lines matching these patterns on
731 731 # Windows, but check-code.py wants a glob on these lines unconditionally. Don't
732 732 # warn if that is the case for anything matching these lines.
733 733 checkcodeglobpats = [
734 734 re.compile(r'^pushing to \$TESTTMP/.*[^)]$'),
735 735 re.compile(r'^moving \S+/.*[^)]$'),
736 736 re.compile(r'^pulling from \$TESTTMP/.*[^)]$')
737 737 ]
738 738
739 739 class TTest(Test):
740 740 """A "t test" is a test backed by a .t file."""
741 741
742 742 SKIPPED_PREFIX = 'skipped: '
743 743 FAILED_PREFIX = 'hghave check failed: '
744 744 NEEDESCAPE = re.compile(r'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
745 745
746 746 ESCAPESUB = re.compile(r'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
747 747 ESCAPEMAP = dict((chr(i), r'\x%02x' % i) for i in range(256))
748 748 ESCAPEMAP.update({'\\': '\\\\', '\r': r'\r'})
749 749
750 750 @property
751 751 def refpath(self):
752 752 return os.path.join(self._testdir, self.name)
753 753
754 754 def _run(self, replacements, env):
755 755 f = open(self.path, 'rb')
756 756 lines = f.readlines()
757 757 f.close()
758 758
759 759 salt, script, after, expected = self._parsetest(lines)
760 760
761 761 # Write out the generated script.
762 762 fname = '%s.sh' % self._testtmp
763 763 f = open(fname, 'wb')
764 764 for l in script:
765 765 f.write(l)
766 766 f.close()
767 767
768 768 cmd = '%s "%s"' % (self._shell, fname)
769 769 vlog("# Running", cmd)
770 770
771 771 exitcode, output = run(cmd, self._testtmp, replacements, env,
772 772 debug=self._debug, timeout=self._timeout)
773 773
774 774 if self._aborted:
775 775 raise KeyboardInterrupt()
776 776
777 777 # Do not merge output if skipped. Return hghave message instead.
778 778 # Similarly, with --debug, output is None.
779 779 if exitcode == self.SKIPPED_STATUS or output is None:
780 780 return exitcode, output
781 781
782 782 return self._processoutput(exitcode, output, salt, after, expected)
783 783
784 784 def _hghave(self, reqs):
785 785 # TODO do something smarter when all other uses of hghave are gone.
786 786 tdir = self._testdir.replace('\\', '/')
787 787 proc = Popen4('%s -c "%s/hghave %s"' %
788 788 (self._shell, tdir, ' '.join(reqs)),
789 789 self._testtmp, 0)
790 790 stdout, stderr = proc.communicate()
791 791 ret = proc.wait()
792 792 if wifexited(ret):
793 793 ret = os.WEXITSTATUS(ret)
794 794 if ret == 2:
795 795 print stdout
796 796 sys.exit(1)
797 797
798 798 return ret == 0
799 799
800 800 def _parsetest(self, lines):
801 801 # We generate a shell script which outputs unique markers to line
802 802 # up script results with our source. These markers include input
803 803 # line number and the last return code.
804 804 salt = "SALT" + str(time.time())
805 805 def addsalt(line, inpython):
806 806 if inpython:
807 807 script.append('%s %d 0\n' % (salt, line))
808 808 else:
809 809 script.append('echo %s %s $?\n' % (salt, line))
810 810
811 811 script = []
812 812
813 813 # After we run the shell script, we re-unify the script output
814 814 # with non-active parts of the source, with synchronization by our
815 815 # SALT line number markers. The after table contains the non-active
816 816 # components, ordered by line number.
817 817 after = {}
818 818
819 819 # Expected shell script output.
820 820 expected = {}
821 821
822 822 pos = prepos = -1
823 823
824 824 # True or False when in a true or false conditional section
825 825 skipping = None
826 826
827 827 # We keep track of whether or not we're in a Python block so we
828 828 # can generate the surrounding doctest magic.
829 829 inpython = False
830 830
831 831 if self._debug:
832 832 script.append('set -x\n')
833 833 if os.getenv('MSYSTEM'):
834 834 script.append('alias pwd="pwd -W"\n')
835 835
836 836 for n, l in enumerate(lines):
837 837 if not l.endswith('\n'):
838 838 l += '\n'
839 839 if l.startswith('#require'):
840 840 lsplit = l.split()
841 841 if len(lsplit) < 2 or lsplit[0] != '#require':
842 842 after.setdefault(pos, []).append(' !!! invalid #require\n')
843 843 if not self._hghave(lsplit[1:]):
844 844 script = ["exit 80\n"]
845 845 break
846 846 after.setdefault(pos, []).append(l)
847 847 elif l.startswith('#if'):
848 848 lsplit = l.split()
849 849 if len(lsplit) < 2 or lsplit[0] != '#if':
850 850 after.setdefault(pos, []).append(' !!! invalid #if\n')
851 851 if skipping is not None:
852 852 after.setdefault(pos, []).append(' !!! nested #if\n')
853 853 skipping = not self._hghave(lsplit[1:])
854 854 after.setdefault(pos, []).append(l)
855 855 elif l.startswith('#else'):
856 856 if skipping is None:
857 857 after.setdefault(pos, []).append(' !!! missing #if\n')
858 858 skipping = not skipping
859 859 after.setdefault(pos, []).append(l)
860 860 elif l.startswith('#endif'):
861 861 if skipping is None:
862 862 after.setdefault(pos, []).append(' !!! missing #if\n')
863 863 skipping = None
864 864 after.setdefault(pos, []).append(l)
865 865 elif skipping:
866 866 after.setdefault(pos, []).append(l)
867 867 elif l.startswith(' >>> '): # python inlines
868 868 after.setdefault(pos, []).append(l)
869 869 prepos = pos
870 870 pos = n
871 871 if not inpython:
872 872 # We've just entered a Python block. Add the header.
873 873 inpython = True
874 874 addsalt(prepos, False) # Make sure we report the exit code.
875 875 script.append('%s -m heredoctest <<EOF\n' % PYTHON)
876 876 addsalt(n, True)
877 877 script.append(l[2:])
878 878 elif l.startswith(' ... '): # python inlines
879 879 after.setdefault(prepos, []).append(l)
880 880 script.append(l[2:])
881 881 elif l.startswith(' $ '): # commands
882 882 if inpython:
883 883 script.append('EOF\n')
884 884 inpython = False
885 885 after.setdefault(pos, []).append(l)
886 886 prepos = pos
887 887 pos = n
888 888 addsalt(n, False)
889 889 cmd = l[4:].split()
890 890 if len(cmd) == 2 and cmd[0] == 'cd':
891 891 l = ' $ cd %s || exit 1\n' % cmd[1]
892 892 script.append(l[4:])
893 893 elif l.startswith(' > '): # continuations
894 894 after.setdefault(prepos, []).append(l)
895 895 script.append(l[4:])
896 896 elif l.startswith(' '): # results
897 897 # Queue up a list of expected results.
898 898 expected.setdefault(pos, []).append(l[2:])
899 899 else:
900 900 if inpython:
901 901 script.append('EOF\n')
902 902 inpython = False
903 903 # Non-command/result. Queue up for merged output.
904 904 after.setdefault(pos, []).append(l)
905 905
906 906 if inpython:
907 907 script.append('EOF\n')
908 908 if skipping is not None:
909 909 after.setdefault(pos, []).append(' !!! missing #endif\n')
910 910 addsalt(n + 1, False)
911 911
912 912 return salt, script, after, expected
913 913
914 914 def _processoutput(self, exitcode, output, salt, after, expected):
915 915 # Merge the script output back into a unified test.
916 916 warnonly = 1 # 1: not yet; 2: yes; 3: for sure not
917 917 if exitcode != 0:
918 918 warnonly = 3
919 919
920 920 pos = -1
921 921 postout = []
922 922 for l in output:
923 923 lout, lcmd = l, None
924 924 if salt in l:
925 925 lout, lcmd = l.split(salt, 1)
926 926
927 927 if lout:
928 928 if not lout.endswith('\n'):
929 929 lout += ' (no-eol)\n'
930 930
931 931 # Find the expected output at the current position.
932 932 el = None
933 933 if expected.get(pos, None):
934 934 el = expected[pos].pop(0)
935 935
936 936 r = TTest.linematch(el, lout)
937 937 if isinstance(r, str):
938 938 if r == '+glob':
939 939 lout = el[:-1] + ' (glob)\n'
940 940 r = '' # Warn only this line.
941 941 elif r == '-glob':
942 942 lout = ''.join(el.rsplit(' (glob)', 1))
943 943 r = '' # Warn only this line.
944 944 else:
945 945 log('\ninfo, unknown linematch result: %r\n' % r)
946 946 r = False
947 947 if r:
948 948 postout.append(' ' + el)
949 949 else:
950 950 if self.NEEDESCAPE(lout):
951 951 lout = TTest._stringescape('%s (esc)\n' %
952 952 lout.rstrip('\n'))
953 953 postout.append(' ' + lout) # Let diff deal with it.
954 954 if r != '': # If line failed.
955 955 warnonly = 3 # for sure not
956 956 elif warnonly == 1: # Is "not yet" and line is warn only.
957 957 warnonly = 2 # Yes do warn.
958 958
959 959 if lcmd:
960 960 # Add on last return code.
961 961 ret = int(lcmd.split()[1])
962 962 if ret != 0:
963 963 postout.append(' [%s]\n' % ret)
964 964 if pos in after:
965 965 # Merge in non-active test bits.
966 966 postout += after.pop(pos)
967 967 pos = int(lcmd.split()[0])
968 968
969 969 if pos in after:
970 970 postout += after.pop(pos)
971 971
972 972 if warnonly == 2:
973 973 exitcode = False # Set exitcode to warned.
974 974
975 975 return exitcode, postout
976 976
977 977 @staticmethod
978 978 def rematch(el, l):
979 979 try:
980 980 # use \Z to ensure that the regex matches to the end of the string
981 981 if os.name == 'nt':
982 982 return re.match(el + r'\r?\n\Z', l)
983 983 return re.match(el + r'\n\Z', l)
984 984 except re.error:
985 985 # el is an invalid regex
986 986 return False
987 987
988 988 @staticmethod
989 989 def globmatch(el, l):
990 990 # The only supported special characters are * and ? plus / which also
991 991 # matches \ on windows. Escaping of these characters is supported.
992 992 if el + '\n' == l:
993 993 if os.altsep:
994 994 # matching on "/" is not needed for this line
995 995 for pat in checkcodeglobpats:
996 996 if pat.match(el):
997 997 return True
998 998 return '-glob'
999 999 return True
1000 1000 i, n = 0, len(el)
1001 1001 res = ''
1002 1002 while i < n:
1003 1003 c = el[i]
1004 1004 i += 1
1005 1005 if c == '\\' and el[i] in '*?\\/':
1006 1006 res += el[i - 1:i + 1]
1007 1007 i += 1
1008 1008 elif c == '*':
1009 1009 res += '.*'
1010 1010 elif c == '?':
1011 1011 res += '.'
1012 1012 elif c == '/' and os.altsep:
1013 1013 res += '[/\\\\]'
1014 1014 else:
1015 1015 res += re.escape(c)
1016 1016 return TTest.rematch(res, l)
1017 1017
1018 1018 @staticmethod
1019 1019 def linematch(el, l):
1020 1020 if el == l: # perfect match (fast)
1021 1021 return True
1022 1022 if el:
1023 1023 if el.endswith(" (esc)\n"):
1024 1024 el = el[:-7].decode('string-escape') + '\n'
1025 1025 if el == l or os.name == 'nt' and el[:-1] + '\r\n' == l:
1026 1026 return True
1027 1027 if el.endswith(" (re)\n"):
1028 1028 return TTest.rematch(el[:-6], l)
1029 1029 if el.endswith(" (glob)\n"):
1030 1030 # ignore '(glob)' added to l by 'replacements'
1031 1031 if l.endswith(" (glob)\n"):
1032 1032 l = l[:-8] + "\n"
1033 1033 return TTest.globmatch(el[:-8], l)
1034 1034 if os.altsep and l.replace('\\', '/') == el:
1035 1035 return '+glob'
1036 1036 return False
1037 1037
1038 1038 @staticmethod
1039 1039 def parsehghaveoutput(lines):
1040 1040 '''Parse hghave log lines.
1041 1041
1042 1042 Return tuple of lists (missing, failed):
1043 1043 * the missing/unknown features
1044 1044 * the features for which existence check failed'''
1045 1045 missing = []
1046 1046 failed = []
1047 1047 for line in lines:
1048 1048 if line.startswith(TTest.SKIPPED_PREFIX):
1049 1049 line = line.splitlines()[0]
1050 1050 missing.append(line[len(TTest.SKIPPED_PREFIX):])
1051 1051 elif line.startswith(TTest.FAILED_PREFIX):
1052 1052 line = line.splitlines()[0]
1053 1053 failed.append(line[len(TTest.FAILED_PREFIX):])
1054 1054
1055 1055 return missing, failed
1056 1056
1057 1057 @staticmethod
1058 1058 def _escapef(m):
1059 1059 return TTest.ESCAPEMAP[m.group(0)]
1060 1060
1061 1061 @staticmethod
1062 1062 def _stringescape(s):
1063 1063 return TTest.ESCAPESUB(TTest._escapef, s)
1064 1064
1065 1065
1066 1066 wifexited = getattr(os, "WIFEXITED", lambda x: False)
1067 1067 def run(cmd, wd, replacements, env, debug=False, timeout=None):
1068 1068 """Run command in a sub-process, capturing the output (stdout and stderr).
1069 1069 Return a tuple (exitcode, output). output is None in debug mode."""
1070 1070 if debug:
1071 1071 proc = subprocess.Popen(cmd, shell=True, cwd=wd, env=env)
1072 1072 ret = proc.wait()
1073 1073 return (ret, None)
1074 1074
1075 1075 proc = Popen4(cmd, wd, timeout, env)
1076 1076 def cleanup():
1077 1077 terminate(proc)
1078 1078 ret = proc.wait()
1079 1079 if ret == 0:
1080 1080 ret = signal.SIGTERM << 8
1081 1081 killdaemons(env['DAEMON_PIDS'])
1082 1082 return ret
1083 1083
1084 1084 output = ''
1085 1085 proc.tochild.close()
1086 1086
1087 1087 try:
1088 1088 output = proc.fromchild.read()
1089 1089 except KeyboardInterrupt:
1090 1090 vlog('# Handling keyboard interrupt')
1091 1091 cleanup()
1092 1092 raise
1093 1093
1094 1094 ret = proc.wait()
1095 1095 if wifexited(ret):
1096 1096 ret = os.WEXITSTATUS(ret)
1097 1097
1098 1098 if proc.timeout:
1099 1099 ret = 'timeout'
1100 1100
1101 1101 if ret:
1102 1102 killdaemons(env['DAEMON_PIDS'])
1103 1103
1104 1104 for s, r in replacements:
1105 1105 output = re.sub(s, r, output)
1106 1106 return ret, output.splitlines(True)
1107 1107
1108 1108 iolock = threading.RLock()
1109 1109
1110 1110 class SkipTest(Exception):
1111 1111 """Raised to indicate that a test is to be skipped."""
1112 1112
1113 1113 class IgnoreTest(Exception):
1114 1114 """Raised to indicate that a test is to be ignored."""
1115 1115
1116 1116 class WarnTest(Exception):
1117 1117 """Raised to indicate that a test warned."""
1118 1118
1119 1119 class TestResult(unittest._TextTestResult):
1120 1120 """Holds results when executing via unittest."""
1121 1121 # Don't worry too much about accessing the non-public _TextTestResult.
1122 1122 # It is relatively common in Python testing tools.
1123 1123 def __init__(self, options, *args, **kwargs):
1124 1124 super(TestResult, self).__init__(*args, **kwargs)
1125 1125
1126 1126 self._options = options
1127 1127
1128 1128 # unittest.TestResult didn't have skipped until 2.7. We need to
1129 1129 # polyfill it.
1130 1130 self.skipped = []
1131 1131
1132 1132 # We have a custom "ignored" result that isn't present in any Python
1133 1133 # unittest implementation. It is very similar to skipped. It may make
1134 1134 # sense to map it into skip some day.
1135 1135 self.ignored = []
1136 1136
1137 1137 # We have a custom "warned" result that isn't present in any Python
1138 1138 # unittest implementation. It is very similar to failed. It may make
1139 1139 # sense to map it into fail some day.
1140 1140 self.warned = []
1141 1141
1142 1142 self.times = []
1143 1143 self._started = {}
1144 1144 self._stopped = {}
1145 1145 # Data stored for the benefit of generating xunit reports.
1146 1146 self.successes = []
1147 1147 self.faildata = {}
1148 1148
1149 1149 def addFailure(self, test, reason):
1150 1150 self.failures.append((test, reason))
1151 1151
1152 1152 if self._options.first:
1153 1153 self.stop()
1154 1154 else:
1155 1155 iolock.acquire()
1156 1156 if not self._options.nodiff:
1157 1157 self.stream.write('\nERROR: %s output changed\n' % test)
1158 1158
1159 1159 self.stream.write('!')
1160 1160 self.stream.flush()
1161 1161 iolock.release()
1162 1162
1163 1163 def addSuccess(self, test):
1164 1164 iolock.acquire()
1165 1165 super(TestResult, self).addSuccess(test)
1166 1166 iolock.release()
1167 1167 self.successes.append(test)
1168 1168
1169 1169 def addError(self, test, err):
1170 1170 super(TestResult, self).addError(test, err)
1171 1171 if self._options.first:
1172 1172 self.stop()
1173 1173
1174 1174 # Polyfill.
1175 1175 def addSkip(self, test, reason):
1176 1176 self.skipped.append((test, reason))
1177 1177 iolock.acquire()
1178 1178 if self.showAll:
1179 1179 self.stream.writeln('skipped %s' % reason)
1180 1180 else:
1181 1181 self.stream.write('s')
1182 1182 self.stream.flush()
1183 1183 iolock.release()
1184 1184
1185 1185 def addIgnore(self, test, reason):
1186 1186 self.ignored.append((test, reason))
1187 1187 iolock.acquire()
1188 1188 if self.showAll:
1189 1189 self.stream.writeln('ignored %s' % reason)
1190 1190 else:
1191 1191 if reason != 'not retesting' and reason != "doesn't match keyword":
1192 1192 self.stream.write('i')
1193 1193 else:
1194 1194 self.testsRun += 1
1195 1195 self.stream.flush()
1196 1196 iolock.release()
1197 1197
1198 1198 def addWarn(self, test, reason):
1199 1199 self.warned.append((test, reason))
1200 1200
1201 1201 if self._options.first:
1202 1202 self.stop()
1203 1203
1204 1204 iolock.acquire()
1205 1205 if self.showAll:
1206 1206 self.stream.writeln('warned %s' % reason)
1207 1207 else:
1208 1208 self.stream.write('~')
1209 1209 self.stream.flush()
1210 1210 iolock.release()
1211 1211
1212 1212 def addOutputMismatch(self, test, ret, got, expected):
1213 1213 """Record a mismatch in test output for a particular test."""
1214 1214 if self.shouldStop:
1215 1215 # don't print, some other test case already failed and
1216 1216 # printed, we're just stale and probably failed due to our
1217 1217 # temp dir getting cleaned up.
1218 1218 return
1219 1219
1220 1220 accepted = False
1221 1221 failed = False
1222 1222 lines = []
1223 1223
1224 1224 iolock.acquire()
1225 1225 if self._options.nodiff:
1226 1226 pass
1227 1227 elif self._options.view:
1228 1228 os.system("%s %s %s" %
1229 1229 (self._options.view, test.refpath, test.errpath))
1230 1230 else:
1231 1231 servefail, lines = getdiff(expected, got,
1232 1232 test.refpath, test.errpath)
1233 1233 if servefail:
1234 1234 self.addFailure(
1235 1235 test,
1236 1236 'server failed to start (HGPORT=%s)' % test._startport)
1237 1237 else:
1238 1238 self.stream.write('\n')
1239 1239 for line in lines:
1240 1240 self.stream.write(line)
1241 1241 self.stream.flush()
1242 1242
1243 1243 # handle interactive prompt without releasing iolock
1244 1244 if self._options.interactive:
1245 1245 self.stream.write('Accept this change? [n] ')
1246 1246 answer = sys.stdin.readline().strip()
1247 1247 if answer.lower() in ('y', 'yes'):
1248 1248 if test.name.endswith('.t'):
1249 1249 rename(test.errpath, test.path)
1250 1250 else:
1251 1251 rename(test.errpath, '%s.out' % test.path)
1252 1252 accepted = True
1253 1253 if not accepted and not failed:
1254 1254 self.faildata[test.name] = ''.join(lines)
1255 1255 iolock.release()
1256 1256
1257 1257 return accepted
1258 1258
1259 1259 def startTest(self, test):
1260 1260 super(TestResult, self).startTest(test)
1261 1261
1262 1262 # os.times module computes the user time and system time spent by
1263 1263 # child's processes along with real elapsed time taken by a process.
1264 1264 # This module has one limitation. It can only work for Linux user
1265 1265 # and not for Windows.
1266 1266 self._started[test.name] = os.times()
1267 1267
1268 1268 def stopTest(self, test, interrupted=False):
1269 1269 super(TestResult, self).stopTest(test)
1270 1270
1271 1271 self._stopped[test.name] = os.times()
1272 1272
1273 1273 starttime = self._started[test.name]
1274 1274 endtime = self._stopped[test.name]
1275 1275 self.times.append((test.name, endtime[2] - starttime[2],
1276 1276 endtime[3] - starttime[3], endtime[4] - starttime[4]))
1277 1277
1278 1278 del self._started[test.name]
1279 1279 del self._stopped[test.name]
1280 1280
1281 1281 if interrupted:
1282 1282 iolock.acquire()
1283 1283 self.stream.writeln('INTERRUPTED: %s (after %d seconds)' % (
1284 1284 test.name, self.times[-1][3]))
1285 1285 iolock.release()
1286 1286
1287 1287 class TestSuite(unittest.TestSuite):
1288 1288 """Custom unittest TestSuite that knows how to execute Mercurial tests."""
1289 1289
1290 1290 def __init__(self, testdir, jobs=1, whitelist=None, blacklist=None,
1291 1291 retest=False, keywords=None, loop=False,
1292 1292 *args, **kwargs):
1293 1293 """Create a new instance that can run tests with a configuration.
1294 1294
1295 1295 testdir specifies the directory where tests are executed from. This
1296 1296 is typically the ``tests`` directory from Mercurial's source
1297 1297 repository.
1298 1298
1299 1299 jobs specifies the number of jobs to run concurrently. Each test
1300 1300 executes on its own thread. Tests actually spawn new processes, so
1301 1301 state mutation should not be an issue.
1302 1302
1303 1303 whitelist and blacklist denote tests that have been whitelisted and
1304 1304 blacklisted, respectively. These arguments don't belong in TestSuite.
1305 1305 Instead, whitelist and blacklist should be handled by the thing that
1306 1306 populates the TestSuite with tests. They are present to preserve
1307 1307 backwards compatible behavior which reports skipped tests as part
1308 1308 of the results.
1309 1309
1310 1310 retest denotes whether to retest failed tests. This arguably belongs
1311 1311 outside of TestSuite.
1312 1312
1313 1313 keywords denotes key words that will be used to filter which tests
1314 1314 to execute. This arguably belongs outside of TestSuite.
1315 1315
1316 1316 loop denotes whether to loop over tests forever.
1317 1317 """
1318 1318 super(TestSuite, self).__init__(*args, **kwargs)
1319 1319
1320 1320 self._jobs = jobs
1321 1321 self._whitelist = whitelist
1322 1322 self._blacklist = blacklist
1323 1323 self._retest = retest
1324 1324 self._keywords = keywords
1325 1325 self._loop = loop
1326 1326
1327 1327 def run(self, result):
1328 1328 # We have a number of filters that need to be applied. We do this
1329 1329 # here instead of inside Test because it makes the running logic for
1330 1330 # Test simpler.
1331 1331 tests = []
1332 1332 for test in self._tests:
1333 1333 if not os.path.exists(test.path):
1334 1334 result.addSkip(test, "Doesn't exist")
1335 1335 continue
1336 1336
1337 1337 if not (self._whitelist and test.name in self._whitelist):
1338 1338 if self._blacklist and test.name in self._blacklist:
1339 1339 result.addSkip(test, 'blacklisted')
1340 1340 continue
1341 1341
1342 1342 if self._retest and not os.path.exists(test.errpath):
1343 1343 result.addIgnore(test, 'not retesting')
1344 1344 continue
1345 1345
1346 1346 if self._keywords:
1347 1347 f = open(test.path, 'rb')
1348 1348 t = f.read().lower() + test.name.lower()
1349 1349 f.close()
1350 1350 ignored = False
1351 1351 for k in self._keywords.lower().split():
1352 1352 if k not in t:
1353 1353 result.addIgnore(test, "doesn't match keyword")
1354 1354 ignored = True
1355 1355 break
1356 1356
1357 1357 if ignored:
1358 1358 continue
1359 1359
1360 1360 tests.append(test)
1361 1361
1362 1362 runtests = list(tests)
1363 1363 done = queue.Queue()
1364 1364 running = 0
1365 1365
1366 1366 def job(test, result):
1367 1367 try:
1368 1368 test(result)
1369 1369 done.put(None)
1370 1370 except KeyboardInterrupt:
1371 1371 pass
1372 1372 except: # re-raises
1373 1373 done.put(('!', test, 'run-test raised an error, see traceback'))
1374 1374 raise
1375 1375
1376 1376 try:
1377 1377 while tests or running:
1378 1378 if not done.empty() or running == self._jobs or not tests:
1379 1379 try:
1380 1380 done.get(True, 1)
1381 1381 if result and result.shouldStop:
1382 1382 break
1383 1383 except queue.Empty:
1384 1384 continue
1385 1385 running -= 1
1386 1386 if tests and not running == self._jobs:
1387 1387 test = tests.pop(0)
1388 1388 if self._loop:
1389 1389 tests.append(test)
1390 1390 t = threading.Thread(target=job, name=test.name,
1391 1391 args=(test, result))
1392 1392 t.start()
1393 1393 running += 1
1394 1394 except KeyboardInterrupt:
1395 1395 for test in runtests:
1396 1396 test.abort()
1397 1397
1398 1398 return result
1399 1399
1400 1400 class TextTestRunner(unittest.TextTestRunner):
1401 1401 """Custom unittest test runner that uses appropriate settings."""
1402 1402
1403 1403 def __init__(self, runner, *args, **kwargs):
1404 1404 super(TextTestRunner, self).__init__(*args, **kwargs)
1405 1405
1406 1406 self._runner = runner
1407 1407
1408 1408 def run(self, test):
1409 1409 result = TestResult(self._runner.options, self.stream,
1410 1410 self.descriptions, self.verbosity)
1411 1411
1412 1412 test(result)
1413 1413
1414 1414 failed = len(result.failures)
1415 1415 warned = len(result.warned)
1416 1416 skipped = len(result.skipped)
1417 1417 ignored = len(result.ignored)
1418 1418
1419 1419 iolock.acquire()
1420 1420 self.stream.writeln('')
1421 1421
1422 1422 if not self._runner.options.noskips:
1423 1423 for test, msg in result.skipped:
1424 1424 self.stream.writeln('Skipped %s: %s' % (test.name, msg))
1425 1425 for test, msg in result.warned:
1426 1426 self.stream.writeln('Warned %s: %s' % (test.name, msg))
1427 1427 for test, msg in result.failures:
1428 1428 self.stream.writeln('Failed %s: %s' % (test.name, msg))
1429 1429 for test, msg in result.errors:
1430 1430 self.stream.writeln('Errored %s: %s' % (test.name, msg))
1431 1431
1432 1432 if self._runner.options.xunit:
1433 1433 xuf = open(self._runner.options.xunit, 'wb')
1434 1434 try:
1435 1435 timesd = dict(
1436 1436 (test, real) for test, cuser, csys, real in result.times)
1437 1437 doc = minidom.Document()
1438 1438 s = doc.createElement('testsuite')
1439 1439 s.setAttribute('name', 'run-tests')
1440 1440 s.setAttribute('tests', str(result.testsRun))
1441 1441 s.setAttribute('errors', "0") # TODO
1442 1442 s.setAttribute('failures', str(failed))
1443 1443 s.setAttribute('skipped', str(skipped + ignored))
1444 1444 doc.appendChild(s)
1445 1445 for tc in result.successes:
1446 1446 t = doc.createElement('testcase')
1447 1447 t.setAttribute('name', tc.name)
1448 1448 t.setAttribute('time', '%.3f' % timesd[tc.name])
1449 1449 s.appendChild(t)
1450 1450 for tc, err in sorted(result.faildata.iteritems()):
1451 1451 t = doc.createElement('testcase')
1452 1452 t.setAttribute('name', tc)
1453 1453 t.setAttribute('time', '%.3f' % timesd[tc])
1454 1454 cd = doc.createCDATASection(cdatasafe(err))
1455 1455 t.appendChild(cd)
1456 1456 s.appendChild(t)
1457 1457 xuf.write(doc.toprettyxml(indent=' ', encoding='utf-8'))
1458 1458 finally:
1459 1459 xuf.close()
1460 1460
1461 1461 if self._runner.options.json:
1462 1462 if json is None:
1463 1463 raise ImportError("json module not installed")
1464 1464 jsonpath = os.path.join(self._runner._testdir, 'report.json')
1465 1465 fp = open(jsonpath, 'w')
1466 1466 try:
1467 1467 timesd = {}
1468 1468 for test, cuser, csys, real in result.times:
1469 1469 timesd[test] = (real, cuser, csys)
1470 1470
1471 1471 outcome = {}
1472 1472 for tc in result.successes:
1473 1473 testresult = {'result': 'success',
1474 1474 'time': ('%0.3f' % timesd[tc.name][0]),
1475 1475 'cuser': ('%0.3f' % timesd[tc.name][1]),
1476 1476 'csys': ('%0.3f' % timesd[tc.name][2])}
1477 1477 outcome[tc.name] = testresult
1478 1478
1479 1479 for tc, err in sorted(result.faildata.iteritems()):
1480 1480 testresult = {'result': 'failure',
1481 1481 'time': ('%0.3f' % timesd[tc][0]),
1482 1482 'cuser': ('%0.3f' % timesd[tc][1]),
1483 1483 'csys': ('%0.3f' % timesd[tc][2])}
1484 1484 outcome[tc] = testresult
1485 1485
1486 1486 for tc, reason in result.skipped:
1487 1487 testresult = {'result': 'skip',
1488 1488 'time': ('%0.3f' % timesd[tc.name][0]),
1489 1489 'cuser': ('%0.3f' % timesd[tc.name][1]),
1490 1490 'csys': ('%0.3f' % timesd[tc.name][2])}
1491 1491 outcome[tc.name] = testresult
1492 1492
1493 1493 jsonout = json.dumps(outcome, sort_keys=True, indent=4)
1494 1494 fp.writelines(("testreport =", jsonout))
1495 1495 finally:
1496 1496 fp.close()
1497 1497
1498 1498 self._runner._checkhglib('Tested')
1499 1499
1500 1500 self.stream.writeln('# Ran %d tests, %d skipped, %d warned, %d failed.'
1501 1501 % (result.testsRun,
1502 1502 skipped + ignored, warned, failed))
1503 1503 if failed:
1504 1504 self.stream.writeln('python hash seed: %s' %
1505 1505 os.environ['PYTHONHASHSEED'])
1506 1506 if self._runner.options.time:
1507 1507 self.printtimes(result.times)
1508 1508
1509 1509 iolock.release()
1510 1510
1511 1511 return result
1512 1512
1513 1513 def printtimes(self, times):
1514 1514 # iolock held by run
1515 1515 self.stream.writeln('# Producing time report')
1516 1516 times.sort(key=lambda t: (t[3]))
1517 1517 cols = '%7.3f %7.3f %7.3f %s'
1518 1518 self.stream.writeln('%-7s %-7s %-7s %s' % ('cuser', 'csys', 'real',
1519 1519 'Test'))
1520 1520 for test, cuser, csys, real in times:
1521 1521 self.stream.writeln(cols % (cuser, csys, real, test))
1522 1522
1523 1523 class TestRunner(object):
1524 1524 """Holds context for executing tests.
1525 1525
1526 1526 Tests rely on a lot of state. This object holds it for them.
1527 1527 """
1528 1528
1529 1529 # Programs required to run tests.
1530 1530 REQUIREDTOOLS = [
1531 1531 os.path.basename(sys.executable),
1532 1532 'diff',
1533 1533 'grep',
1534 1534 'unzip',
1535 1535 'gunzip',
1536 1536 'bunzip2',
1537 1537 'sed',
1538 1538 ]
1539 1539
1540 1540 # Maps file extensions to test class.
1541 1541 TESTTYPES = [
1542 1542 ('.py', PythonTest),
1543 1543 ('.t', TTest),
1544 1544 ]
1545 1545
1546 1546 def __init__(self):
1547 1547 self.options = None
1548 1548 self._testdir = None
1549 1549 self._hgtmp = None
1550 1550 self._installdir = None
1551 1551 self._bindir = None
1552 1552 self._tmpbinddir = None
1553 1553 self._pythondir = None
1554 1554 self._coveragefile = None
1555 1555 self._createdfiles = []
1556 1556 self._hgpath = None
1557 1557
1558 1558 def run(self, args, parser=None):
1559 1559 """Run the test suite."""
1560 1560 oldmask = os.umask(022)
1561 1561 try:
1562 1562 parser = parser or getparser()
1563 1563 options, args = parseargs(args, parser)
1564 1564 self.options = options
1565 1565
1566 1566 self._checktools()
1567 1567 tests = self.findtests(args)
1568 1568 return self._run(tests)
1569 1569 finally:
1570 1570 os.umask(oldmask)
1571 1571
1572 1572 def _run(self, tests):
1573 1573 if self.options.random:
1574 1574 random.shuffle(tests)
1575 1575 else:
1576 1576 # keywords for slow tests
1577 1577 slow = 'svn gendoc check-code-hg'.split()
1578 1578 def sortkey(f):
1579 1579 # run largest tests first, as they tend to take the longest
1580 1580 try:
1581 1581 val = -os.stat(f).st_size
1582 1582 except OSError, e:
1583 1583 if e.errno != errno.ENOENT:
1584 1584 raise
1585 1585 return -1e9 # file does not exist, tell early
1586 1586 for kw in slow:
1587 1587 if kw in f:
1588 1588 val *= 10
1589 1589 return val
1590 1590 tests.sort(key=sortkey)
1591 1591
1592 1592 self._testdir = os.environ['TESTDIR'] = os.getcwd()
1593 1593
1594 1594 if 'PYTHONHASHSEED' not in os.environ:
1595 1595 # use a random python hash seed all the time
1596 1596 # we do the randomness ourself to know what seed is used
1597 1597 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
1598 1598
1599 1599 if self.options.tmpdir:
1600 1600 self.options.keep_tmpdir = True
1601 1601 tmpdir = self.options.tmpdir
1602 1602 if os.path.exists(tmpdir):
1603 1603 # Meaning of tmpdir has changed since 1.3: we used to create
1604 1604 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
1605 1605 # tmpdir already exists.
1606 1606 print "error: temp dir %r already exists" % tmpdir
1607 1607 return 1
1608 1608
1609 1609 # Automatically removing tmpdir sounds convenient, but could
1610 1610 # really annoy anyone in the habit of using "--tmpdir=/tmp"
1611 1611 # or "--tmpdir=$HOME".
1612 1612 #vlog("# Removing temp dir", tmpdir)
1613 1613 #shutil.rmtree(tmpdir)
1614 1614 os.makedirs(tmpdir)
1615 1615 else:
1616 1616 d = None
1617 1617 if os.name == 'nt':
1618 1618 # without this, we get the default temp dir location, but
1619 1619 # in all lowercase, which causes troubles with paths (issue3490)
1620 1620 d = os.getenv('TMP')
1621 1621 tmpdir = tempfile.mkdtemp('', 'hgtests.', d)
1622 1622 self._hgtmp = os.environ['HGTMP'] = os.path.realpath(tmpdir)
1623 1623
1624 1624 if self.options.with_hg:
1625 1625 self._installdir = None
1626 1626 self._bindir = os.path.dirname(os.path.realpath(
1627 1627 self.options.with_hg))
1628 1628 self._tmpbindir = os.path.join(self._hgtmp, 'install', 'bin')
1629 1629 os.makedirs(self._tmpbindir)
1630 1630
1631 1631 # This looks redundant with how Python initializes sys.path from
1632 1632 # the location of the script being executed. Needed because the
1633 1633 # "hg" specified by --with-hg is not the only Python script
1634 1634 # executed in the test suite that needs to import 'mercurial'
1635 1635 # ... which means it's not really redundant at all.
1636 1636 self._pythondir = self._bindir
1637 1637 else:
1638 1638 self._installdir = os.path.join(self._hgtmp, "install")
1639 1639 self._bindir = os.environ["BINDIR"] = \
1640 1640 os.path.join(self._installdir, "bin")
1641 1641 self._tmpbindir = self._bindir
1642 1642 self._pythondir = os.path.join(self._installdir, "lib", "python")
1643 1643
1644 1644 os.environ["BINDIR"] = self._bindir
1645 1645 os.environ["PYTHON"] = PYTHON
1646 1646
1647 path = [self._bindir] + os.environ["PATH"].split(os.pathsep)
1647 runtestdir = os.path.abspath(os.path.dirname(__file__))
1648 path = [self._bindir, runtestdir] + os.environ["PATH"].split(os.pathsep)
1648 1649 if self._tmpbindir != self._bindir:
1649 1650 path = [self._tmpbindir] + path
1650 1651 os.environ["PATH"] = os.pathsep.join(path)
1651 1652
1652 1653 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
1653 1654 # can run .../tests/run-tests.py test-foo where test-foo
1654 1655 # adds an extension to HGRC. Also include run-test.py directory to
1655 1656 # import modules like heredoctest.
1656 pypath = [self._pythondir, self._testdir,
1657 os.path.abspath(os.path.dirname(__file__))]
1657 pypath = [self._pythondir, self._testdir, runtestdir]
1658 1658 # We have to augment PYTHONPATH, rather than simply replacing
1659 1659 # it, in case external libraries are only available via current
1660 1660 # PYTHONPATH. (In particular, the Subversion bindings on OS X
1661 1661 # are in /opt/subversion.)
1662 1662 oldpypath = os.environ.get(IMPL_PATH)
1663 1663 if oldpypath:
1664 1664 pypath.append(oldpypath)
1665 1665 os.environ[IMPL_PATH] = os.pathsep.join(pypath)
1666 1666
1667 1667 self._coveragefile = os.path.join(self._testdir, '.coverage')
1668 1668
1669 1669 vlog("# Using TESTDIR", self._testdir)
1670 1670 vlog("# Using HGTMP", self._hgtmp)
1671 1671 vlog("# Using PATH", os.environ["PATH"])
1672 1672 vlog("# Using", IMPL_PATH, os.environ[IMPL_PATH])
1673 1673
1674 1674 try:
1675 1675 return self._runtests(tests) or 0
1676 1676 finally:
1677 1677 time.sleep(.1)
1678 1678 self._cleanup()
1679 1679
1680 1680 def findtests(self, args):
1681 1681 """Finds possible test files from arguments.
1682 1682
1683 1683 If you wish to inject custom tests into the test harness, this would
1684 1684 be a good function to monkeypatch or override in a derived class.
1685 1685 """
1686 1686 if not args:
1687 1687 if self.options.changed:
1688 1688 proc = Popen4('hg st --rev "%s" -man0 .' %
1689 1689 self.options.changed, None, 0)
1690 1690 stdout, stderr = proc.communicate()
1691 1691 args = stdout.strip('\0').split('\0')
1692 1692 else:
1693 1693 args = os.listdir('.')
1694 1694
1695 1695 return [t for t in args
1696 1696 if os.path.basename(t).startswith('test-')
1697 1697 and (t.endswith('.py') or t.endswith('.t'))]
1698 1698
1699 1699 def _runtests(self, tests):
1700 1700 try:
1701 1701 if self._installdir:
1702 1702 self._installhg()
1703 1703 self._checkhglib("Testing")
1704 1704 else:
1705 1705 self._usecorrectpython()
1706 1706
1707 1707 if self.options.restart:
1708 1708 orig = list(tests)
1709 1709 while tests:
1710 1710 if os.path.exists(tests[0] + ".err"):
1711 1711 break
1712 1712 tests.pop(0)
1713 1713 if not tests:
1714 1714 print "running all tests"
1715 1715 tests = orig
1716 1716
1717 1717 tests = [self._gettest(t, i) for i, t in enumerate(tests)]
1718 1718
1719 1719 failed = False
1720 1720 warned = False
1721 1721
1722 1722 suite = TestSuite(self._testdir,
1723 1723 jobs=self.options.jobs,
1724 1724 whitelist=self.options.whitelisted,
1725 1725 blacklist=self.options.blacklist,
1726 1726 retest=self.options.retest,
1727 1727 keywords=self.options.keywords,
1728 1728 loop=self.options.loop,
1729 1729 tests=tests)
1730 1730 verbosity = 1
1731 1731 if self.options.verbose:
1732 1732 verbosity = 2
1733 1733 runner = TextTestRunner(self, verbosity=verbosity)
1734 1734 result = runner.run(suite)
1735 1735
1736 1736 if result.failures:
1737 1737 failed = True
1738 1738 if result.warned:
1739 1739 warned = True
1740 1740
1741 1741 if self.options.anycoverage:
1742 1742 self._outputcoverage()
1743 1743 except KeyboardInterrupt:
1744 1744 failed = True
1745 1745 print "\ninterrupted!"
1746 1746
1747 1747 if failed:
1748 1748 return 1
1749 1749 if warned:
1750 1750 return 80
1751 1751
1752 1752 def _gettest(self, test, count):
1753 1753 """Obtain a Test by looking at its filename.
1754 1754
1755 1755 Returns a Test instance. The Test may not be runnable if it doesn't
1756 1756 map to a known type.
1757 1757 """
1758 1758 lctest = test.lower()
1759 1759 testcls = Test
1760 1760
1761 1761 for ext, cls in self.TESTTYPES:
1762 1762 if lctest.endswith(ext):
1763 1763 testcls = cls
1764 1764 break
1765 1765
1766 1766 refpath = os.path.join(self._testdir, test)
1767 1767 tmpdir = os.path.join(self._hgtmp, 'child%d' % count)
1768 1768
1769 1769 return testcls(refpath, tmpdir,
1770 1770 keeptmpdir=self.options.keep_tmpdir,
1771 1771 debug=self.options.debug,
1772 1772 timeout=self.options.timeout,
1773 1773 startport=self.options.port + count * 3,
1774 1774 extraconfigopts=self.options.extra_config_opt,
1775 1775 py3kwarnings=self.options.py3k_warnings,
1776 1776 shell=self.options.shell)
1777 1777
1778 1778 def _cleanup(self):
1779 1779 """Clean up state from this test invocation."""
1780 1780
1781 1781 if self.options.keep_tmpdir:
1782 1782 return
1783 1783
1784 1784 vlog("# Cleaning up HGTMP", self._hgtmp)
1785 1785 shutil.rmtree(self._hgtmp, True)
1786 1786 for f in self._createdfiles:
1787 1787 try:
1788 1788 os.remove(f)
1789 1789 except OSError:
1790 1790 pass
1791 1791
1792 1792 def _usecorrectpython(self):
1793 1793 """Configure the environment to use the appropriate Python in tests."""
1794 1794 # Tests must use the same interpreter as us or bad things will happen.
1795 1795 pyexename = sys.platform == 'win32' and 'python.exe' or 'python'
1796 1796 if getattr(os, 'symlink', None):
1797 1797 vlog("# Making python executable in test path a symlink to '%s'" %
1798 1798 sys.executable)
1799 1799 mypython = os.path.join(self._tmpbindir, pyexename)
1800 1800 try:
1801 1801 if os.readlink(mypython) == sys.executable:
1802 1802 return
1803 1803 os.unlink(mypython)
1804 1804 except OSError, err:
1805 1805 if err.errno != errno.ENOENT:
1806 1806 raise
1807 1807 if self._findprogram(pyexename) != sys.executable:
1808 1808 try:
1809 1809 os.symlink(sys.executable, mypython)
1810 1810 self._createdfiles.append(mypython)
1811 1811 except OSError, err:
1812 1812 # child processes may race, which is harmless
1813 1813 if err.errno != errno.EEXIST:
1814 1814 raise
1815 1815 else:
1816 1816 exedir, exename = os.path.split(sys.executable)
1817 1817 vlog("# Modifying search path to find %s as %s in '%s'" %
1818 1818 (exename, pyexename, exedir))
1819 1819 path = os.environ['PATH'].split(os.pathsep)
1820 1820 while exedir in path:
1821 1821 path.remove(exedir)
1822 1822 os.environ['PATH'] = os.pathsep.join([exedir] + path)
1823 1823 if not self._findprogram(pyexename):
1824 1824 print "WARNING: Cannot find %s in search path" % pyexename
1825 1825
1826 1826 def _installhg(self):
1827 1827 """Install hg into the test environment.
1828 1828
1829 1829 This will also configure hg with the appropriate testing settings.
1830 1830 """
1831 1831 vlog("# Performing temporary installation of HG")
1832 1832 installerrs = os.path.join("tests", "install.err")
1833 1833 compiler = ''
1834 1834 if self.options.compiler:
1835 1835 compiler = '--compiler ' + self.options.compiler
1836 1836 pure = self.options.pure and "--pure" or ""
1837 1837 py3 = ''
1838 1838 if sys.version_info[0] == 3:
1839 1839 py3 = '--c2to3'
1840 1840
1841 1841 # Run installer in hg root
1842 1842 script = os.path.realpath(sys.argv[0])
1843 1843 hgroot = os.path.dirname(os.path.dirname(script))
1844 1844 os.chdir(hgroot)
1845 1845 nohome = '--home=""'
1846 1846 if os.name == 'nt':
1847 1847 # The --home="" trick works only on OS where os.sep == '/'
1848 1848 # because of a distutils convert_path() fast-path. Avoid it at
1849 1849 # least on Windows for now, deal with .pydistutils.cfg bugs
1850 1850 # when they happen.
1851 1851 nohome = ''
1852 1852 cmd = ('%(exe)s setup.py %(py3)s %(pure)s clean --all'
1853 1853 ' build %(compiler)s --build-base="%(base)s"'
1854 1854 ' install --force --prefix="%(prefix)s"'
1855 1855 ' --install-lib="%(libdir)s"'
1856 1856 ' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
1857 1857 % {'exe': sys.executable, 'py3': py3, 'pure': pure,
1858 1858 'compiler': compiler,
1859 1859 'base': os.path.join(self._hgtmp, "build"),
1860 1860 'prefix': self._installdir, 'libdir': self._pythondir,
1861 1861 'bindir': self._bindir,
1862 1862 'nohome': nohome, 'logfile': installerrs})
1863 1863 vlog("# Running", cmd)
1864 1864 if os.system(cmd) == 0:
1865 1865 if not self.options.verbose:
1866 1866 os.remove(installerrs)
1867 1867 else:
1868 1868 f = open(installerrs, 'rb')
1869 1869 for line in f:
1870 1870 print line
1871 1871 f.close()
1872 1872 sys.exit(1)
1873 1873 os.chdir(self._testdir)
1874 1874
1875 1875 self._usecorrectpython()
1876 1876
1877 1877 if self.options.py3k_warnings and not self.options.anycoverage:
1878 1878 vlog("# Updating hg command to enable Py3k Warnings switch")
1879 1879 f = open(os.path.join(self._bindir, 'hg'), 'rb')
1880 1880 lines = [line.rstrip() for line in f]
1881 1881 lines[0] += ' -3'
1882 1882 f.close()
1883 1883 f = open(os.path.join(self._bindir, 'hg'), 'wb')
1884 1884 for line in lines:
1885 1885 f.write(line + '\n')
1886 1886 f.close()
1887 1887
1888 1888 hgbat = os.path.join(self._bindir, 'hg.bat')
1889 1889 if os.path.isfile(hgbat):
1890 1890 # hg.bat expects to be put in bin/scripts while run-tests.py
1891 1891 # installation layout put it in bin/ directly. Fix it
1892 1892 f = open(hgbat, 'rb')
1893 1893 data = f.read()
1894 1894 f.close()
1895 1895 if '"%~dp0..\python" "%~dp0hg" %*' in data:
1896 1896 data = data.replace('"%~dp0..\python" "%~dp0hg" %*',
1897 1897 '"%~dp0python" "%~dp0hg" %*')
1898 1898 f = open(hgbat, 'wb')
1899 1899 f.write(data)
1900 1900 f.close()
1901 1901 else:
1902 1902 print 'WARNING: cannot fix hg.bat reference to python.exe'
1903 1903
1904 1904 if self.options.anycoverage:
1905 1905 custom = os.path.join(self._testdir, 'sitecustomize.py')
1906 1906 target = os.path.join(self._pythondir, 'sitecustomize.py')
1907 1907 vlog('# Installing coverage trigger to %s' % target)
1908 1908 shutil.copyfile(custom, target)
1909 1909 rc = os.path.join(self._testdir, '.coveragerc')
1910 1910 vlog('# Installing coverage rc to %s' % rc)
1911 1911 os.environ['COVERAGE_PROCESS_START'] = rc
1912 1912 fn = os.path.join(self._installdir, '..', '.coverage')
1913 1913 os.environ['COVERAGE_FILE'] = fn
1914 1914
1915 1915 def _checkhglib(self, verb):
1916 1916 """Ensure that the 'mercurial' package imported by python is
1917 1917 the one we expect it to be. If not, print a warning to stderr."""
1918 1918 if ((self._bindir == self._pythondir) and
1919 1919 (self._bindir != self._tmpbindir)):
1920 1920 # The pythondir has been inferred from --with-hg flag.
1921 1921 # We cannot expect anything sensible here.
1922 1922 return
1923 1923 expecthg = os.path.join(self._pythondir, 'mercurial')
1924 1924 actualhg = self._gethgpath()
1925 1925 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
1926 1926 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
1927 1927 ' (expected %s)\n'
1928 1928 % (verb, actualhg, expecthg))
1929 1929 def _gethgpath(self):
1930 1930 """Return the path to the mercurial package that is actually found by
1931 1931 the current Python interpreter."""
1932 1932 if self._hgpath is not None:
1933 1933 return self._hgpath
1934 1934
1935 1935 cmd = '%s -c "import mercurial; print (mercurial.__path__[0])"'
1936 1936 pipe = os.popen(cmd % PYTHON)
1937 1937 try:
1938 1938 self._hgpath = pipe.read().strip()
1939 1939 finally:
1940 1940 pipe.close()
1941 1941
1942 1942 return self._hgpath
1943 1943
1944 1944 def _outputcoverage(self):
1945 1945 """Produce code coverage output."""
1946 1946 vlog('# Producing coverage report')
1947 1947 os.chdir(self._pythondir)
1948 1948
1949 1949 def covrun(*args):
1950 1950 cmd = 'coverage %s' % ' '.join(args)
1951 1951 vlog('# Running: %s' % cmd)
1952 1952 os.system(cmd)
1953 1953
1954 1954 covrun('-c')
1955 1955 omit = ','.join(os.path.join(x, '*') for x in
1956 1956 [self._bindir, self._testdir])
1957 1957 covrun('-i', '-r', '"--omit=%s"' % omit) # report
1958 1958 if self.options.htmlcov:
1959 1959 htmldir = os.path.join(self._testdir, 'htmlcov')
1960 1960 covrun('-i', '-b', '"--directory=%s"' % htmldir,
1961 1961 '"--omit=%s"' % omit)
1962 1962 if self.options.annotate:
1963 1963 adir = os.path.join(self._testdir, 'annotated')
1964 1964 if not os.path.isdir(adir):
1965 1965 os.mkdir(adir)
1966 1966 covrun('-i', '-a', '"--directory=%s"' % adir, '"--omit=%s"' % omit)
1967 1967
1968 1968 def _findprogram(self, program):
1969 1969 """Search PATH for a executable program"""
1970 1970 for p in os.environ.get('PATH', os.defpath).split(os.pathsep):
1971 1971 name = os.path.join(p, program)
1972 1972 if os.name == 'nt' or os.access(name, os.X_OK):
1973 1973 return name
1974 1974 return None
1975 1975
1976 1976 def _checktools(self):
1977 1977 """Ensure tools required to run tests are present."""
1978 1978 for p in self.REQUIREDTOOLS:
1979 1979 if os.name == 'nt' and not p.endswith('.exe'):
1980 1980 p += '.exe'
1981 1981 found = self._findprogram(p)
1982 1982 if found:
1983 1983 vlog("# Found prerequisite", p, "at", found)
1984 1984 else:
1985 1985 print "WARNING: Did not find prerequisite tool: %s " % p
1986 1986
1987 1987 if __name__ == '__main__':
1988 1988 runner = TestRunner()
1989 1989
1990 1990 try:
1991 1991 import msvcrt
1992 1992 msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
1993 1993 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
1994 1994 msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
1995 1995 except ImportError:
1996 1996 pass
1997 1997
1998 1998 sys.exit(runner.run(sys.argv[1:]))
General Comments 0
You need to be logged in to leave comments. Login now