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