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