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