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