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