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