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