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