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