##// END OF EJS Templates
run-tests: obtain replacements inside Test._runcommand...
Gregory Szorc -
r24516:62fb03e0 default
parent child Browse files
Show More
@@ -1,2077 +1,2076 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 #
2 #
3 # run-tests.py - Run a set of tests on Mercurial
3 # run-tests.py - Run a set of tests on Mercurial
4 #
4 #
5 # Copyright 2006 Matt Mackall <mpm@selenic.com>
5 # Copyright 2006 Matt Mackall <mpm@selenic.com>
6 #
6 #
7 # This software may be used and distributed according to the terms of the
7 # This software may be used and distributed according to the terms of the
8 # GNU General Public License version 2 or any later version.
8 # GNU General Public License version 2 or any later version.
9
9
10 # Modifying this script is tricky because it has many modes:
10 # Modifying this script is tricky because it has many modes:
11 # - serial (default) vs parallel (-jN, N > 1)
11 # - serial (default) vs parallel (-jN, N > 1)
12 # - no coverage (default) vs coverage (-c, -C, -s)
12 # - no coverage (default) vs coverage (-c, -C, -s)
13 # - temp install (default) vs specific hg script (--with-hg, --local)
13 # - temp install (default) vs specific hg script (--with-hg, --local)
14 # - tests are a mix of shell scripts and Python scripts
14 # - tests are a mix of shell scripts and Python scripts
15 #
15 #
16 # If you change this script, it is recommended that you ensure you
16 # If you change this script, it is recommended that you ensure you
17 # haven't broken it by running it in various modes with a representative
17 # haven't broken it by running it in various modes with a representative
18 # sample of test scripts. For example:
18 # sample of test scripts. For example:
19 #
19 #
20 # 1) serial, no coverage, temp install:
20 # 1) serial, no coverage, temp install:
21 # ./run-tests.py test-s*
21 # ./run-tests.py test-s*
22 # 2) serial, no coverage, local hg:
22 # 2) serial, no coverage, local hg:
23 # ./run-tests.py --local test-s*
23 # ./run-tests.py --local test-s*
24 # 3) serial, coverage, temp install:
24 # 3) serial, coverage, temp install:
25 # ./run-tests.py -c test-s*
25 # ./run-tests.py -c test-s*
26 # 4) serial, coverage, local hg:
26 # 4) serial, coverage, local hg:
27 # ./run-tests.py -c --local test-s* # unsupported
27 # ./run-tests.py -c --local test-s* # unsupported
28 # 5) parallel, no coverage, temp install:
28 # 5) parallel, no coverage, temp install:
29 # ./run-tests.py -j2 test-s*
29 # ./run-tests.py -j2 test-s*
30 # 6) parallel, no coverage, local hg:
30 # 6) parallel, no coverage, local hg:
31 # ./run-tests.py -j2 --local test-s*
31 # ./run-tests.py -j2 --local test-s*
32 # 7) parallel, coverage, temp install:
32 # 7) parallel, coverage, temp install:
33 # ./run-tests.py -j2 -c test-s* # currently broken
33 # ./run-tests.py -j2 -c test-s* # currently broken
34 # 8) parallel, coverage, local install:
34 # 8) parallel, coverage, local install:
35 # ./run-tests.py -j2 -c --local test-s* # unsupported (and broken)
35 # ./run-tests.py -j2 -c --local test-s* # unsupported (and broken)
36 # 9) parallel, custom tmp dir:
36 # 9) parallel, custom tmp dir:
37 # ./run-tests.py -j2 --tmpdir /tmp/myhgtests
37 # ./run-tests.py -j2 --tmpdir /tmp/myhgtests
38 #
38 #
39 # (You could use any subset of the tests: test-s* happens to match
39 # (You could use any subset of the tests: test-s* happens to match
40 # enough that it's worth doing parallel runs, few enough that it
40 # enough that it's worth doing parallel runs, few enough that it
41 # completes fairly quickly, includes both shell and Python scripts, and
41 # completes fairly quickly, includes both shell and Python scripts, and
42 # includes some scripts that run daemon processes.)
42 # includes some scripts that run daemon processes.)
43
43
44 from distutils import version
44 from distutils import version
45 import difflib
45 import difflib
46 import errno
46 import errno
47 import optparse
47 import optparse
48 import os
48 import os
49 import shutil
49 import shutil
50 import subprocess
50 import subprocess
51 import signal
51 import signal
52 import sys
52 import sys
53 import tempfile
53 import tempfile
54 import time
54 import time
55 import random
55 import random
56 import re
56 import re
57 import threading
57 import threading
58 import killdaemons as killmod
58 import killdaemons as killmod
59 import Queue as queue
59 import Queue as queue
60 from xml.dom import minidom
60 from xml.dom import minidom
61 import unittest
61 import unittest
62
62
63 try:
63 try:
64 import json
64 import json
65 except ImportError:
65 except ImportError:
66 try:
66 try:
67 import simplejson as json
67 import simplejson as json
68 except ImportError:
68 except ImportError:
69 json = None
69 json = None
70
70
71 processlock = threading.Lock()
71 processlock = threading.Lock()
72
72
73 # subprocess._cleanup can race with any Popen.wait or Popen.poll on py24
73 # subprocess._cleanup can race with any Popen.wait or Popen.poll on py24
74 # http://bugs.python.org/issue1731717 for details. We shouldn't be producing
74 # http://bugs.python.org/issue1731717 for details. We shouldn't be producing
75 # zombies but it's pretty harmless even if we do.
75 # zombies but it's pretty harmless even if we do.
76 if sys.version_info < (2, 5):
76 if sys.version_info < (2, 5):
77 subprocess._cleanup = lambda: None
77 subprocess._cleanup = lambda: None
78
78
79 wifexited = getattr(os, "WIFEXITED", lambda x: False)
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()
547 env = self._getenv()
546 env = self._getenv()
548 self._daemonpids.append(env['DAEMON_PIDS'])
547 self._daemonpids.append(env['DAEMON_PIDS'])
549 self._createhgrc(env['HGRCPATH'])
548 self._createhgrc(env['HGRCPATH'])
550
549
551 vlog('# Test', self.name)
550 vlog('# Test', self.name)
552
551
553 ret, out = self._run(replacements, env)
552 ret, out = self._run(env)
554 self._finished = True
553 self._finished = True
555 self._ret = ret
554 self._ret = ret
556 self._out = out
555 self._out = out
557
556
558 def describe(ret):
557 def describe(ret):
559 if ret < 0:
558 if ret < 0:
560 return 'killed by signal: %d' % -ret
559 return 'killed by signal: %d' % -ret
561 return 'returned error code %d' % ret
560 return 'returned error code %d' % ret
562
561
563 self._skipped = False
562 self._skipped = False
564
563
565 if ret == self.SKIPPED_STATUS:
564 if ret == self.SKIPPED_STATUS:
566 if out is None: # Debug mode, nothing to parse.
565 if out is None: # Debug mode, nothing to parse.
567 missing = ['unknown']
566 missing = ['unknown']
568 failed = None
567 failed = None
569 else:
568 else:
570 missing, failed = TTest.parsehghaveoutput(out)
569 missing, failed = TTest.parsehghaveoutput(out)
571
570
572 if not missing:
571 if not missing:
573 missing = ['skipped']
572 missing = ['skipped']
574
573
575 if failed:
574 if failed:
576 self.fail('hg have failed checking for %s' % failed[-1])
575 self.fail('hg have failed checking for %s' % failed[-1])
577 else:
576 else:
578 self._skipped = True
577 self._skipped = True
579 raise SkipTest(missing[-1])
578 raise SkipTest(missing[-1])
580 elif ret == 'timeout':
579 elif ret == 'timeout':
581 self.fail('timed out')
580 self.fail('timed out')
582 elif ret is False:
581 elif ret is False:
583 raise WarnTest('no result code from test')
582 raise WarnTest('no result code from test')
584 elif out != self._refout:
583 elif out != self._refout:
585 # Diff generation may rely on written .err file.
584 # Diff generation may rely on written .err file.
586 if (ret != 0 or out != self._refout) and not self._skipped \
585 if (ret != 0 or out != self._refout) and not self._skipped \
587 and not self._debug:
586 and not self._debug:
588 f = open(self.errpath, 'wb')
587 f = open(self.errpath, 'wb')
589 for line in out:
588 for line in out:
590 f.write(line)
589 f.write(line)
591 f.close()
590 f.close()
592
591
593 # The result object handles diff calculation for us.
592 # The result object handles diff calculation for us.
594 if self._result.addOutputMismatch(self, ret, out, self._refout):
593 if self._result.addOutputMismatch(self, ret, out, self._refout):
595 # change was accepted, skip failing
594 # change was accepted, skip failing
596 return
595 return
597
596
598 if ret:
597 if ret:
599 msg = 'output changed and ' + describe(ret)
598 msg = 'output changed and ' + describe(ret)
600 else:
599 else:
601 msg = 'output changed'
600 msg = 'output changed'
602
601
603 self.fail(msg)
602 self.fail(msg)
604 elif ret:
603 elif ret:
605 self.fail(describe(ret))
604 self.fail(describe(ret))
606
605
607 def tearDown(self):
606 def tearDown(self):
608 """Tasks to perform after run()."""
607 """Tasks to perform after run()."""
609 for entry in self._daemonpids:
608 for entry in self._daemonpids:
610 killdaemons(entry)
609 killdaemons(entry)
611 self._daemonpids = []
610 self._daemonpids = []
612
611
613 if not self._keeptmpdir:
612 if not self._keeptmpdir:
614 shutil.rmtree(self._testtmp, True)
613 shutil.rmtree(self._testtmp, True)
615 shutil.rmtree(self._threadtmp, True)
614 shutil.rmtree(self._threadtmp, True)
616
615
617 if (self._ret != 0 or self._out != self._refout) and not self._skipped \
616 if (self._ret != 0 or self._out != self._refout) and not self._skipped \
618 and not self._debug and self._out:
617 and not self._debug and self._out:
619 f = open(self.errpath, 'wb')
618 f = open(self.errpath, 'wb')
620 for line in self._out:
619 for line in self._out:
621 f.write(line)
620 f.write(line)
622 f.close()
621 f.close()
623
622
624 vlog("# Ret was:", self._ret)
623 vlog("# Ret was:", self._ret)
625
624
626 def _run(self, replacements, env):
625 def _run(self, env):
627 # This should be implemented in child classes to run tests.
626 # This should be implemented in child classes to run tests.
628 raise SkipTest('unknown test type')
627 raise SkipTest('unknown test type')
629
628
630 def abort(self):
629 def abort(self):
631 """Terminate execution of this test."""
630 """Terminate execution of this test."""
632 self._aborted = True
631 self._aborted = True
633
632
634 def _getreplacements(self):
633 def _getreplacements(self):
635 """Obtain a mapping of text replacements to apply to test output.
634 """Obtain a mapping of text replacements to apply to test output.
636
635
637 Test output needs to be normalized so it can be compared to expected
636 Test output needs to be normalized so it can be compared to expected
638 output. This function defines how some of that normalization will
637 output. This function defines how some of that normalization will
639 occur.
638 occur.
640 """
639 """
641 r = [
640 r = [
642 (r':%s\b' % self._startport, ':$HGPORT'),
641 (r':%s\b' % self._startport, ':$HGPORT'),
643 (r':%s\b' % (self._startport + 1), ':$HGPORT1'),
642 (r':%s\b' % (self._startport + 1), ':$HGPORT1'),
644 (r':%s\b' % (self._startport + 2), ':$HGPORT2'),
643 (r':%s\b' % (self._startport + 2), ':$HGPORT2'),
645 (r'(?m)^(saved backup bundle to .*\.hg)( \(glob\))?$',
644 (r'(?m)^(saved backup bundle to .*\.hg)( \(glob\))?$',
646 r'\1 (glob)'),
645 r'\1 (glob)'),
647 ]
646 ]
648
647
649 if os.name == 'nt':
648 if os.name == 'nt':
650 r.append(
649 r.append(
651 (''.join(c.isalpha() and '[%s%s]' % (c.lower(), c.upper()) or
650 (''.join(c.isalpha() and '[%s%s]' % (c.lower(), c.upper()) or
652 c in '/\\' and r'[/\\]' or c.isdigit() and c or '\\' + c
651 c in '/\\' and r'[/\\]' or c.isdigit() and c or '\\' + c
653 for c in self._testtmp), '$TESTTMP'))
652 for c in self._testtmp), '$TESTTMP'))
654 else:
653 else:
655 r.append((re.escape(self._testtmp), '$TESTTMP'))
654 r.append((re.escape(self._testtmp), '$TESTTMP'))
656
655
657 return r
656 return r
658
657
659 def _getenv(self):
658 def _getenv(self):
660 """Obtain environment variables to use during test execution."""
659 """Obtain environment variables to use during test execution."""
661 env = os.environ.copy()
660 env = os.environ.copy()
662 env['TESTTMP'] = self._testtmp
661 env['TESTTMP'] = self._testtmp
663 env['HOME'] = self._testtmp
662 env['HOME'] = self._testtmp
664 env["HGPORT"] = str(self._startport)
663 env["HGPORT"] = str(self._startport)
665 env["HGPORT1"] = str(self._startport + 1)
664 env["HGPORT1"] = str(self._startport + 1)
666 env["HGPORT2"] = str(self._startport + 2)
665 env["HGPORT2"] = str(self._startport + 2)
667 env["HGRCPATH"] = os.path.join(self._threadtmp, '.hgrc')
666 env["HGRCPATH"] = os.path.join(self._threadtmp, '.hgrc')
668 env["DAEMON_PIDS"] = os.path.join(self._threadtmp, 'daemon.pids')
667 env["DAEMON_PIDS"] = os.path.join(self._threadtmp, 'daemon.pids')
669 env["HGEDITOR"] = ('"' + sys.executable + '"'
668 env["HGEDITOR"] = ('"' + sys.executable + '"'
670 + ' -c "import sys; sys.exit(0)"')
669 + ' -c "import sys; sys.exit(0)"')
671 env["HGMERGE"] = "internal:merge"
670 env["HGMERGE"] = "internal:merge"
672 env["HGUSER"] = "test"
671 env["HGUSER"] = "test"
673 env["HGENCODING"] = "ascii"
672 env["HGENCODING"] = "ascii"
674 env["HGENCODINGMODE"] = "strict"
673 env["HGENCODINGMODE"] = "strict"
675
674
676 # Reset some environment variables to well-known values so that
675 # Reset some environment variables to well-known values so that
677 # the tests produce repeatable output.
676 # the tests produce repeatable output.
678 env['LANG'] = env['LC_ALL'] = env['LANGUAGE'] = 'C'
677 env['LANG'] = env['LC_ALL'] = env['LANGUAGE'] = 'C'
679 env['TZ'] = 'GMT'
678 env['TZ'] = 'GMT'
680 env["EMAIL"] = "Foo Bar <foo.bar@example.com>"
679 env["EMAIL"] = "Foo Bar <foo.bar@example.com>"
681 env['COLUMNS'] = '80'
680 env['COLUMNS'] = '80'
682 env['TERM'] = 'xterm'
681 env['TERM'] = 'xterm'
683
682
684 for k in ('HG HGPROF CDPATH GREP_OPTIONS http_proxy no_proxy ' +
683 for k in ('HG HGPROF CDPATH GREP_OPTIONS http_proxy no_proxy ' +
685 'NO_PROXY').split():
684 'NO_PROXY').split():
686 if k in env:
685 if k in env:
687 del env[k]
686 del env[k]
688
687
689 # unset env related to hooks
688 # unset env related to hooks
690 for k in env.keys():
689 for k in env.keys():
691 if k.startswith('HG_'):
690 if k.startswith('HG_'):
692 del env[k]
691 del env[k]
693
692
694 return env
693 return env
695
694
696 def _createhgrc(self, path):
695 def _createhgrc(self, path):
697 """Create an hgrc file for this test."""
696 """Create an hgrc file for this test."""
698 hgrc = open(path, 'wb')
697 hgrc = open(path, 'wb')
699 hgrc.write('[ui]\n')
698 hgrc.write('[ui]\n')
700 hgrc.write('slash = True\n')
699 hgrc.write('slash = True\n')
701 hgrc.write('interactive = False\n')
700 hgrc.write('interactive = False\n')
702 hgrc.write('mergemarkers = detailed\n')
701 hgrc.write('mergemarkers = detailed\n')
703 hgrc.write('promptecho = True\n')
702 hgrc.write('promptecho = True\n')
704 hgrc.write('[defaults]\n')
703 hgrc.write('[defaults]\n')
705 hgrc.write('backout = -d "0 0"\n')
704 hgrc.write('backout = -d "0 0"\n')
706 hgrc.write('commit = -d "0 0"\n')
705 hgrc.write('commit = -d "0 0"\n')
707 hgrc.write('shelve = --date "0 0"\n')
706 hgrc.write('shelve = --date "0 0"\n')
708 hgrc.write('tag = -d "0 0"\n')
707 hgrc.write('tag = -d "0 0"\n')
709 hgrc.write('[largefiles]\n')
708 hgrc.write('[largefiles]\n')
710 hgrc.write('usercache = %s\n' %
709 hgrc.write('usercache = %s\n' %
711 (os.path.join(self._testtmp, '.cache/largefiles')))
710 (os.path.join(self._testtmp, '.cache/largefiles')))
712
711
713 for opt in self._extraconfigopts:
712 for opt in self._extraconfigopts:
714 section, key = opt.split('.', 1)
713 section, key = opt.split('.', 1)
715 assert '=' in key, ('extra config opt %s must '
714 assert '=' in key, ('extra config opt %s must '
716 'have an = for assignment' % opt)
715 'have an = for assignment' % opt)
717 hgrc.write('[%s]\n%s\n' % (section, key))
716 hgrc.write('[%s]\n%s\n' % (section, key))
718 hgrc.close()
717 hgrc.close()
719
718
720 def fail(self, msg):
719 def fail(self, msg):
721 # unittest differentiates between errored and failed.
720 # unittest differentiates between errored and failed.
722 # Failed is denoted by AssertionError (by default at least).
721 # Failed is denoted by AssertionError (by default at least).
723 raise AssertionError(msg)
722 raise AssertionError(msg)
724
723
725 def _runcommand(self, cmd, replacements, env, normalizenewlines=False):
724 def _runcommand(self, cmd, env, normalizenewlines=False):
726 """Run command in a sub-process, capturing the output (stdout and
725 """Run command in a sub-process, capturing the output (stdout and
727 stderr).
726 stderr).
728
727
729 Return a tuple (exitcode, output). output is None in debug mode.
728 Return a tuple (exitcode, output). output is None in debug mode.
730 """
729 """
731 if self._debug:
730 if self._debug:
732 proc = subprocess.Popen(cmd, shell=True, cwd=self._testtmp,
731 proc = subprocess.Popen(cmd, shell=True, cwd=self._testtmp,
733 env=env)
732 env=env)
734 ret = proc.wait()
733 ret = proc.wait()
735 return (ret, None)
734 return (ret, None)
736
735
737 proc = Popen4(cmd, self._testtmp, self._timeout, env)
736 proc = Popen4(cmd, self._testtmp, self._timeout, env)
738 def cleanup():
737 def cleanup():
739 terminate(proc)
738 terminate(proc)
740 ret = proc.wait()
739 ret = proc.wait()
741 if ret == 0:
740 if ret == 0:
742 ret = signal.SIGTERM << 8
741 ret = signal.SIGTERM << 8
743 killdaemons(env['DAEMON_PIDS'])
742 killdaemons(env['DAEMON_PIDS'])
744 return ret
743 return ret
745
744
746 output = ''
745 output = ''
747 proc.tochild.close()
746 proc.tochild.close()
748
747
749 try:
748 try:
750 output = proc.fromchild.read()
749 output = proc.fromchild.read()
751 except KeyboardInterrupt:
750 except KeyboardInterrupt:
752 vlog('# Handling keyboard interrupt')
751 vlog('# Handling keyboard interrupt')
753 cleanup()
752 cleanup()
754 raise
753 raise
755
754
756 ret = proc.wait()
755 ret = proc.wait()
757 if wifexited(ret):
756 if wifexited(ret):
758 ret = os.WEXITSTATUS(ret)
757 ret = os.WEXITSTATUS(ret)
759
758
760 if proc.timeout:
759 if proc.timeout:
761 ret = 'timeout'
760 ret = 'timeout'
762
761
763 if ret:
762 if ret:
764 killdaemons(env['DAEMON_PIDS'])
763 killdaemons(env['DAEMON_PIDS'])
765
764
766 for s, r in replacements:
765 for s, r in self._getreplacements():
767 output = re.sub(s, r, output)
766 output = re.sub(s, r, output)
768
767
769 if normalizenewlines:
768 if normalizenewlines:
770 output = output.replace('\r\n', '\n')
769 output = output.replace('\r\n', '\n')
771
770
772 return ret, output.splitlines(True)
771 return ret, output.splitlines(True)
773
772
774 class PythonTest(Test):
773 class PythonTest(Test):
775 """A Python-based test."""
774 """A Python-based test."""
776
775
777 @property
776 @property
778 def refpath(self):
777 def refpath(self):
779 return os.path.join(self._testdir, '%s.out' % self.name)
778 return os.path.join(self._testdir, '%s.out' % self.name)
780
779
781 def _run(self, replacements, env):
780 def _run(self, env):
782 py3kswitch = self._py3kwarnings and ' -3' or ''
781 py3kswitch = self._py3kwarnings and ' -3' or ''
783 cmd = '%s%s "%s"' % (PYTHON, py3kswitch, self.path)
782 cmd = '%s%s "%s"' % (PYTHON, py3kswitch, self.path)
784 vlog("# Running", cmd)
783 vlog("# Running", cmd)
785 normalizenewlines = os.name == 'nt'
784 normalizenewlines = os.name == 'nt'
786 result = self._runcommand(cmd, replacements, env,
785 result = self._runcommand(cmd, env,
787 normalizenewlines=normalizenewlines)
786 normalizenewlines=normalizenewlines)
788 if self._aborted:
787 if self._aborted:
789 raise KeyboardInterrupt()
788 raise KeyboardInterrupt()
790
789
791 return result
790 return result
792
791
793 # This script may want to drop globs from lines matching these patterns on
792 # This script may want to drop globs from lines matching these patterns on
794 # Windows, but check-code.py wants a glob on these lines unconditionally. Don't
793 # Windows, but check-code.py wants a glob on these lines unconditionally. Don't
795 # warn if that is the case for anything matching these lines.
794 # warn if that is the case for anything matching these lines.
796 checkcodeglobpats = [
795 checkcodeglobpats = [
797 re.compile(r'^pushing to \$TESTTMP/.*[^)]$'),
796 re.compile(r'^pushing to \$TESTTMP/.*[^)]$'),
798 re.compile(r'^moving \S+/.*[^)]$'),
797 re.compile(r'^moving \S+/.*[^)]$'),
799 re.compile(r'^pulling from \$TESTTMP/.*[^)]$')
798 re.compile(r'^pulling from \$TESTTMP/.*[^)]$')
800 ]
799 ]
801
800
802 class TTest(Test):
801 class TTest(Test):
803 """A "t test" is a test backed by a .t file."""
802 """A "t test" is a test backed by a .t file."""
804
803
805 SKIPPED_PREFIX = 'skipped: '
804 SKIPPED_PREFIX = 'skipped: '
806 FAILED_PREFIX = 'hghave check failed: '
805 FAILED_PREFIX = 'hghave check failed: '
807 NEEDESCAPE = re.compile(r'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
806 NEEDESCAPE = re.compile(r'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
808
807
809 ESCAPESUB = re.compile(r'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
808 ESCAPESUB = re.compile(r'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
810 ESCAPEMAP = dict((chr(i), r'\x%02x' % i) for i in range(256))
809 ESCAPEMAP = dict((chr(i), r'\x%02x' % i) for i in range(256))
811 ESCAPEMAP.update({'\\': '\\\\', '\r': r'\r'})
810 ESCAPEMAP.update({'\\': '\\\\', '\r': r'\r'})
812
811
813 @property
812 @property
814 def refpath(self):
813 def refpath(self):
815 return os.path.join(self._testdir, self.name)
814 return os.path.join(self._testdir, self.name)
816
815
817 def _run(self, replacements, env):
816 def _run(self, env):
818 f = open(self.path, 'rb')
817 f = open(self.path, 'rb')
819 lines = f.readlines()
818 lines = f.readlines()
820 f.close()
819 f.close()
821
820
822 salt, script, after, expected = self._parsetest(lines)
821 salt, script, after, expected = self._parsetest(lines)
823
822
824 # Write out the generated script.
823 # Write out the generated script.
825 fname = '%s.sh' % self._testtmp
824 fname = '%s.sh' % self._testtmp
826 f = open(fname, 'wb')
825 f = open(fname, 'wb')
827 for l in script:
826 for l in script:
828 f.write(l)
827 f.write(l)
829 f.close()
828 f.close()
830
829
831 cmd = '%s "%s"' % (self._shell, fname)
830 cmd = '%s "%s"' % (self._shell, fname)
832 vlog("# Running", cmd)
831 vlog("# Running", cmd)
833
832
834 exitcode, output = self._runcommand(cmd, replacements, env)
833 exitcode, output = self._runcommand(cmd, env)
835
834
836 if self._aborted:
835 if self._aborted:
837 raise KeyboardInterrupt()
836 raise KeyboardInterrupt()
838
837
839 # Do not merge output if skipped. Return hghave message instead.
838 # Do not merge output if skipped. Return hghave message instead.
840 # Similarly, with --debug, output is None.
839 # Similarly, with --debug, output is None.
841 if exitcode == self.SKIPPED_STATUS or output is None:
840 if exitcode == self.SKIPPED_STATUS or output is None:
842 return exitcode, output
841 return exitcode, output
843
842
844 return self._processoutput(exitcode, output, salt, after, expected)
843 return self._processoutput(exitcode, output, salt, after, expected)
845
844
846 def _hghave(self, reqs):
845 def _hghave(self, reqs):
847 # TODO do something smarter when all other uses of hghave are gone.
846 # TODO do something smarter when all other uses of hghave are gone.
848 tdir = self._testdir.replace('\\', '/')
847 tdir = self._testdir.replace('\\', '/')
849 proc = Popen4('%s -c "%s/hghave %s"' %
848 proc = Popen4('%s -c "%s/hghave %s"' %
850 (self._shell, tdir, ' '.join(reqs)),
849 (self._shell, tdir, ' '.join(reqs)),
851 self._testtmp, 0, self._getenv())
850 self._testtmp, 0, self._getenv())
852 stdout, stderr = proc.communicate()
851 stdout, stderr = proc.communicate()
853 ret = proc.wait()
852 ret = proc.wait()
854 if wifexited(ret):
853 if wifexited(ret):
855 ret = os.WEXITSTATUS(ret)
854 ret = os.WEXITSTATUS(ret)
856 if ret == 2:
855 if ret == 2:
857 print stdout
856 print stdout
858 sys.exit(1)
857 sys.exit(1)
859
858
860 return ret == 0
859 return ret == 0
861
860
862 def _parsetest(self, lines):
861 def _parsetest(self, lines):
863 # We generate a shell script which outputs unique markers to line
862 # We generate a shell script which outputs unique markers to line
864 # up script results with our source. These markers include input
863 # up script results with our source. These markers include input
865 # line number and the last return code.
864 # line number and the last return code.
866 salt = "SALT" + str(time.time())
865 salt = "SALT" + str(time.time())
867 def addsalt(line, inpython):
866 def addsalt(line, inpython):
868 if inpython:
867 if inpython:
869 script.append('%s %d 0\n' % (salt, line))
868 script.append('%s %d 0\n' % (salt, line))
870 else:
869 else:
871 script.append('echo %s %s $?\n' % (salt, line))
870 script.append('echo %s %s $?\n' % (salt, line))
872
871
873 script = []
872 script = []
874
873
875 # After we run the shell script, we re-unify the script output
874 # After we run the shell script, we re-unify the script output
876 # with non-active parts of the source, with synchronization by our
875 # with non-active parts of the source, with synchronization by our
877 # SALT line number markers. The after table contains the non-active
876 # SALT line number markers. The after table contains the non-active
878 # components, ordered by line number.
877 # components, ordered by line number.
879 after = {}
878 after = {}
880
879
881 # Expected shell script output.
880 # Expected shell script output.
882 expected = {}
881 expected = {}
883
882
884 pos = prepos = -1
883 pos = prepos = -1
885
884
886 # True or False when in a true or false conditional section
885 # True or False when in a true or false conditional section
887 skipping = None
886 skipping = None
888
887
889 # We keep track of whether or not we're in a Python block so we
888 # We keep track of whether or not we're in a Python block so we
890 # can generate the surrounding doctest magic.
889 # can generate the surrounding doctest magic.
891 inpython = False
890 inpython = False
892
891
893 if self._debug:
892 if self._debug:
894 script.append('set -x\n')
893 script.append('set -x\n')
895 if os.getenv('MSYSTEM'):
894 if os.getenv('MSYSTEM'):
896 script.append('alias pwd="pwd -W"\n')
895 script.append('alias pwd="pwd -W"\n')
897
896
898 for n, l in enumerate(lines):
897 for n, l in enumerate(lines):
899 if not l.endswith('\n'):
898 if not l.endswith('\n'):
900 l += '\n'
899 l += '\n'
901 if l.startswith('#require'):
900 if l.startswith('#require'):
902 lsplit = l.split()
901 lsplit = l.split()
903 if len(lsplit) < 2 or lsplit[0] != '#require':
902 if len(lsplit) < 2 or lsplit[0] != '#require':
904 after.setdefault(pos, []).append(' !!! invalid #require\n')
903 after.setdefault(pos, []).append(' !!! invalid #require\n')
905 if not self._hghave(lsplit[1:]):
904 if not self._hghave(lsplit[1:]):
906 script = ["exit 80\n"]
905 script = ["exit 80\n"]
907 break
906 break
908 after.setdefault(pos, []).append(l)
907 after.setdefault(pos, []).append(l)
909 elif l.startswith('#if'):
908 elif l.startswith('#if'):
910 lsplit = l.split()
909 lsplit = l.split()
911 if len(lsplit) < 2 or lsplit[0] != '#if':
910 if len(lsplit) < 2 or lsplit[0] != '#if':
912 after.setdefault(pos, []).append(' !!! invalid #if\n')
911 after.setdefault(pos, []).append(' !!! invalid #if\n')
913 if skipping is not None:
912 if skipping is not None:
914 after.setdefault(pos, []).append(' !!! nested #if\n')
913 after.setdefault(pos, []).append(' !!! nested #if\n')
915 skipping = not self._hghave(lsplit[1:])
914 skipping = not self._hghave(lsplit[1:])
916 after.setdefault(pos, []).append(l)
915 after.setdefault(pos, []).append(l)
917 elif l.startswith('#else'):
916 elif l.startswith('#else'):
918 if skipping is None:
917 if skipping is None:
919 after.setdefault(pos, []).append(' !!! missing #if\n')
918 after.setdefault(pos, []).append(' !!! missing #if\n')
920 skipping = not skipping
919 skipping = not skipping
921 after.setdefault(pos, []).append(l)
920 after.setdefault(pos, []).append(l)
922 elif l.startswith('#endif'):
921 elif l.startswith('#endif'):
923 if skipping is None:
922 if skipping is None:
924 after.setdefault(pos, []).append(' !!! missing #if\n')
923 after.setdefault(pos, []).append(' !!! missing #if\n')
925 skipping = None
924 skipping = None
926 after.setdefault(pos, []).append(l)
925 after.setdefault(pos, []).append(l)
927 elif skipping:
926 elif skipping:
928 after.setdefault(pos, []).append(l)
927 after.setdefault(pos, []).append(l)
929 elif l.startswith(' >>> '): # python inlines
928 elif l.startswith(' >>> '): # python inlines
930 after.setdefault(pos, []).append(l)
929 after.setdefault(pos, []).append(l)
931 prepos = pos
930 prepos = pos
932 pos = n
931 pos = n
933 if not inpython:
932 if not inpython:
934 # We've just entered a Python block. Add the header.
933 # We've just entered a Python block. Add the header.
935 inpython = True
934 inpython = True
936 addsalt(prepos, False) # Make sure we report the exit code.
935 addsalt(prepos, False) # Make sure we report the exit code.
937 script.append('%s -m heredoctest <<EOF\n' % PYTHON)
936 script.append('%s -m heredoctest <<EOF\n' % PYTHON)
938 addsalt(n, True)
937 addsalt(n, True)
939 script.append(l[2:])
938 script.append(l[2:])
940 elif l.startswith(' ... '): # python inlines
939 elif l.startswith(' ... '): # python inlines
941 after.setdefault(prepos, []).append(l)
940 after.setdefault(prepos, []).append(l)
942 script.append(l[2:])
941 script.append(l[2:])
943 elif l.startswith(' $ '): # commands
942 elif l.startswith(' $ '): # commands
944 if inpython:
943 if inpython:
945 script.append('EOF\n')
944 script.append('EOF\n')
946 inpython = False
945 inpython = False
947 after.setdefault(pos, []).append(l)
946 after.setdefault(pos, []).append(l)
948 prepos = pos
947 prepos = pos
949 pos = n
948 pos = n
950 addsalt(n, False)
949 addsalt(n, False)
951 cmd = l[4:].split()
950 cmd = l[4:].split()
952 if len(cmd) == 2 and cmd[0] == 'cd':
951 if len(cmd) == 2 and cmd[0] == 'cd':
953 l = ' $ cd %s || exit 1\n' % cmd[1]
952 l = ' $ cd %s || exit 1\n' % cmd[1]
954 script.append(l[4:])
953 script.append(l[4:])
955 elif l.startswith(' > '): # continuations
954 elif l.startswith(' > '): # continuations
956 after.setdefault(prepos, []).append(l)
955 after.setdefault(prepos, []).append(l)
957 script.append(l[4:])
956 script.append(l[4:])
958 elif l.startswith(' '): # results
957 elif l.startswith(' '): # results
959 # Queue up a list of expected results.
958 # Queue up a list of expected results.
960 expected.setdefault(pos, []).append(l[2:])
959 expected.setdefault(pos, []).append(l[2:])
961 else:
960 else:
962 if inpython:
961 if inpython:
963 script.append('EOF\n')
962 script.append('EOF\n')
964 inpython = False
963 inpython = False
965 # Non-command/result. Queue up for merged output.
964 # Non-command/result. Queue up for merged output.
966 after.setdefault(pos, []).append(l)
965 after.setdefault(pos, []).append(l)
967
966
968 if inpython:
967 if inpython:
969 script.append('EOF\n')
968 script.append('EOF\n')
970 if skipping is not None:
969 if skipping is not None:
971 after.setdefault(pos, []).append(' !!! missing #endif\n')
970 after.setdefault(pos, []).append(' !!! missing #endif\n')
972 addsalt(n + 1, False)
971 addsalt(n + 1, False)
973
972
974 return salt, script, after, expected
973 return salt, script, after, expected
975
974
976 def _processoutput(self, exitcode, output, salt, after, expected):
975 def _processoutput(self, exitcode, output, salt, after, expected):
977 # Merge the script output back into a unified test.
976 # Merge the script output back into a unified test.
978 warnonly = 1 # 1: not yet; 2: yes; 3: for sure not
977 warnonly = 1 # 1: not yet; 2: yes; 3: for sure not
979 if exitcode != 0:
978 if exitcode != 0:
980 warnonly = 3
979 warnonly = 3
981
980
982 pos = -1
981 pos = -1
983 postout = []
982 postout = []
984 for l in output:
983 for l in output:
985 lout, lcmd = l, None
984 lout, lcmd = l, None
986 if salt in l:
985 if salt in l:
987 lout, lcmd = l.split(salt, 1)
986 lout, lcmd = l.split(salt, 1)
988
987
989 if lout:
988 if lout:
990 if not lout.endswith('\n'):
989 if not lout.endswith('\n'):
991 lout += ' (no-eol)\n'
990 lout += ' (no-eol)\n'
992
991
993 # Find the expected output at the current position.
992 # Find the expected output at the current position.
994 el = None
993 el = None
995 if expected.get(pos, None):
994 if expected.get(pos, None):
996 el = expected[pos].pop(0)
995 el = expected[pos].pop(0)
997
996
998 r = TTest.linematch(el, lout)
997 r = TTest.linematch(el, lout)
999 if isinstance(r, str):
998 if isinstance(r, str):
1000 if r == '+glob':
999 if r == '+glob':
1001 lout = el[:-1] + ' (glob)\n'
1000 lout = el[:-1] + ' (glob)\n'
1002 r = '' # Warn only this line.
1001 r = '' # Warn only this line.
1003 elif r == '-glob':
1002 elif r == '-glob':
1004 lout = ''.join(el.rsplit(' (glob)', 1))
1003 lout = ''.join(el.rsplit(' (glob)', 1))
1005 r = '' # Warn only this line.
1004 r = '' # Warn only this line.
1006 else:
1005 else:
1007 log('\ninfo, unknown linematch result: %r\n' % r)
1006 log('\ninfo, unknown linematch result: %r\n' % r)
1008 r = False
1007 r = False
1009 if r:
1008 if r:
1010 postout.append(' ' + el)
1009 postout.append(' ' + el)
1011 else:
1010 else:
1012 if self.NEEDESCAPE(lout):
1011 if self.NEEDESCAPE(lout):
1013 lout = TTest._stringescape('%s (esc)\n' %
1012 lout = TTest._stringescape('%s (esc)\n' %
1014 lout.rstrip('\n'))
1013 lout.rstrip('\n'))
1015 postout.append(' ' + lout) # Let diff deal with it.
1014 postout.append(' ' + lout) # Let diff deal with it.
1016 if r != '': # If line failed.
1015 if r != '': # If line failed.
1017 warnonly = 3 # for sure not
1016 warnonly = 3 # for sure not
1018 elif warnonly == 1: # Is "not yet" and line is warn only.
1017 elif warnonly == 1: # Is "not yet" and line is warn only.
1019 warnonly = 2 # Yes do warn.
1018 warnonly = 2 # Yes do warn.
1020
1019
1021 if lcmd:
1020 if lcmd:
1022 # Add on last return code.
1021 # Add on last return code.
1023 ret = int(lcmd.split()[1])
1022 ret = int(lcmd.split()[1])
1024 if ret != 0:
1023 if ret != 0:
1025 postout.append(' [%s]\n' % ret)
1024 postout.append(' [%s]\n' % ret)
1026 if pos in after:
1025 if pos in after:
1027 # Merge in non-active test bits.
1026 # Merge in non-active test bits.
1028 postout += after.pop(pos)
1027 postout += after.pop(pos)
1029 pos = int(lcmd.split()[0])
1028 pos = int(lcmd.split()[0])
1030
1029
1031 if pos in after:
1030 if pos in after:
1032 postout += after.pop(pos)
1031 postout += after.pop(pos)
1033
1032
1034 if warnonly == 2:
1033 if warnonly == 2:
1035 exitcode = False # Set exitcode to warned.
1034 exitcode = False # Set exitcode to warned.
1036
1035
1037 return exitcode, postout
1036 return exitcode, postout
1038
1037
1039 @staticmethod
1038 @staticmethod
1040 def rematch(el, l):
1039 def rematch(el, l):
1041 try:
1040 try:
1042 # use \Z to ensure that the regex matches to the end of the string
1041 # use \Z to ensure that the regex matches to the end of the string
1043 if os.name == 'nt':
1042 if os.name == 'nt':
1044 return re.match(el + r'\r?\n\Z', l)
1043 return re.match(el + r'\r?\n\Z', l)
1045 return re.match(el + r'\n\Z', l)
1044 return re.match(el + r'\n\Z', l)
1046 except re.error:
1045 except re.error:
1047 # el is an invalid regex
1046 # el is an invalid regex
1048 return False
1047 return False
1049
1048
1050 @staticmethod
1049 @staticmethod
1051 def globmatch(el, l):
1050 def globmatch(el, l):
1052 # The only supported special characters are * and ? plus / which also
1051 # The only supported special characters are * and ? plus / which also
1053 # matches \ on windows. Escaping of these characters is supported.
1052 # matches \ on windows. Escaping of these characters is supported.
1054 if el + '\n' == l:
1053 if el + '\n' == l:
1055 if os.altsep:
1054 if os.altsep:
1056 # matching on "/" is not needed for this line
1055 # matching on "/" is not needed for this line
1057 for pat in checkcodeglobpats:
1056 for pat in checkcodeglobpats:
1058 if pat.match(el):
1057 if pat.match(el):
1059 return True
1058 return True
1060 return '-glob'
1059 return '-glob'
1061 return True
1060 return True
1062 i, n = 0, len(el)
1061 i, n = 0, len(el)
1063 res = ''
1062 res = ''
1064 while i < n:
1063 while i < n:
1065 c = el[i]
1064 c = el[i]
1066 i += 1
1065 i += 1
1067 if c == '\\' and el[i] in '*?\\/':
1066 if c == '\\' and el[i] in '*?\\/':
1068 res += el[i - 1:i + 1]
1067 res += el[i - 1:i + 1]
1069 i += 1
1068 i += 1
1070 elif c == '*':
1069 elif c == '*':
1071 res += '.*'
1070 res += '.*'
1072 elif c == '?':
1071 elif c == '?':
1073 res += '.'
1072 res += '.'
1074 elif c == '/' and os.altsep:
1073 elif c == '/' and os.altsep:
1075 res += '[/\\\\]'
1074 res += '[/\\\\]'
1076 else:
1075 else:
1077 res += re.escape(c)
1076 res += re.escape(c)
1078 return TTest.rematch(res, l)
1077 return TTest.rematch(res, l)
1079
1078
1080 @staticmethod
1079 @staticmethod
1081 def linematch(el, l):
1080 def linematch(el, l):
1082 if el == l: # perfect match (fast)
1081 if el == l: # perfect match (fast)
1083 return True
1082 return True
1084 if el:
1083 if el:
1085 if el.endswith(" (esc)\n"):
1084 if el.endswith(" (esc)\n"):
1086 el = el[:-7].decode('string-escape') + '\n'
1085 el = el[:-7].decode('string-escape') + '\n'
1087 if el == l or os.name == 'nt' and el[:-1] + '\r\n' == l:
1086 if el == l or os.name == 'nt' and el[:-1] + '\r\n' == l:
1088 return True
1087 return True
1089 if el.endswith(" (re)\n"):
1088 if el.endswith(" (re)\n"):
1090 return TTest.rematch(el[:-6], l)
1089 return TTest.rematch(el[:-6], l)
1091 if el.endswith(" (glob)\n"):
1090 if el.endswith(" (glob)\n"):
1092 # ignore '(glob)' added to l by 'replacements'
1091 # ignore '(glob)' added to l by 'replacements'
1093 if l.endswith(" (glob)\n"):
1092 if l.endswith(" (glob)\n"):
1094 l = l[:-8] + "\n"
1093 l = l[:-8] + "\n"
1095 return TTest.globmatch(el[:-8], l)
1094 return TTest.globmatch(el[:-8], l)
1096 if os.altsep and l.replace('\\', '/') == el:
1095 if os.altsep and l.replace('\\', '/') == el:
1097 return '+glob'
1096 return '+glob'
1098 return False
1097 return False
1099
1098
1100 @staticmethod
1099 @staticmethod
1101 def parsehghaveoutput(lines):
1100 def parsehghaveoutput(lines):
1102 '''Parse hghave log lines.
1101 '''Parse hghave log lines.
1103
1102
1104 Return tuple of lists (missing, failed):
1103 Return tuple of lists (missing, failed):
1105 * the missing/unknown features
1104 * the missing/unknown features
1106 * the features for which existence check failed'''
1105 * the features for which existence check failed'''
1107 missing = []
1106 missing = []
1108 failed = []
1107 failed = []
1109 for line in lines:
1108 for line in lines:
1110 if line.startswith(TTest.SKIPPED_PREFIX):
1109 if line.startswith(TTest.SKIPPED_PREFIX):
1111 line = line.splitlines()[0]
1110 line = line.splitlines()[0]
1112 missing.append(line[len(TTest.SKIPPED_PREFIX):])
1111 missing.append(line[len(TTest.SKIPPED_PREFIX):])
1113 elif line.startswith(TTest.FAILED_PREFIX):
1112 elif line.startswith(TTest.FAILED_PREFIX):
1114 line = line.splitlines()[0]
1113 line = line.splitlines()[0]
1115 failed.append(line[len(TTest.FAILED_PREFIX):])
1114 failed.append(line[len(TTest.FAILED_PREFIX):])
1116
1115
1117 return missing, failed
1116 return missing, failed
1118
1117
1119 @staticmethod
1118 @staticmethod
1120 def _escapef(m):
1119 def _escapef(m):
1121 return TTest.ESCAPEMAP[m.group(0)]
1120 return TTest.ESCAPEMAP[m.group(0)]
1122
1121
1123 @staticmethod
1122 @staticmethod
1124 def _stringescape(s):
1123 def _stringescape(s):
1125 return TTest.ESCAPESUB(TTest._escapef, s)
1124 return TTest.ESCAPESUB(TTest._escapef, s)
1126
1125
1127 iolock = threading.RLock()
1126 iolock = threading.RLock()
1128
1127
1129 class SkipTest(Exception):
1128 class SkipTest(Exception):
1130 """Raised to indicate that a test is to be skipped."""
1129 """Raised to indicate that a test is to be skipped."""
1131
1130
1132 class IgnoreTest(Exception):
1131 class IgnoreTest(Exception):
1133 """Raised to indicate that a test is to be ignored."""
1132 """Raised to indicate that a test is to be ignored."""
1134
1133
1135 class WarnTest(Exception):
1134 class WarnTest(Exception):
1136 """Raised to indicate that a test warned."""
1135 """Raised to indicate that a test warned."""
1137
1136
1138 class TestResult(unittest._TextTestResult):
1137 class TestResult(unittest._TextTestResult):
1139 """Holds results when executing via unittest."""
1138 """Holds results when executing via unittest."""
1140 # Don't worry too much about accessing the non-public _TextTestResult.
1139 # Don't worry too much about accessing the non-public _TextTestResult.
1141 # It is relatively common in Python testing tools.
1140 # It is relatively common in Python testing tools.
1142 def __init__(self, options, *args, **kwargs):
1141 def __init__(self, options, *args, **kwargs):
1143 super(TestResult, self).__init__(*args, **kwargs)
1142 super(TestResult, self).__init__(*args, **kwargs)
1144
1143
1145 self._options = options
1144 self._options = options
1146
1145
1147 # unittest.TestResult didn't have skipped until 2.7. We need to
1146 # unittest.TestResult didn't have skipped until 2.7. We need to
1148 # polyfill it.
1147 # polyfill it.
1149 self.skipped = []
1148 self.skipped = []
1150
1149
1151 # We have a custom "ignored" result that isn't present in any Python
1150 # We have a custom "ignored" result that isn't present in any Python
1152 # unittest implementation. It is very similar to skipped. It may make
1151 # unittest implementation. It is very similar to skipped. It may make
1153 # sense to map it into skip some day.
1152 # sense to map it into skip some day.
1154 self.ignored = []
1153 self.ignored = []
1155
1154
1156 # We have a custom "warned" result that isn't present in any Python
1155 # We have a custom "warned" result that isn't present in any Python
1157 # unittest implementation. It is very similar to failed. It may make
1156 # unittest implementation. It is very similar to failed. It may make
1158 # sense to map it into fail some day.
1157 # sense to map it into fail some day.
1159 self.warned = []
1158 self.warned = []
1160
1159
1161 self.times = []
1160 self.times = []
1162 # Data stored for the benefit of generating xunit reports.
1161 # Data stored for the benefit of generating xunit reports.
1163 self.successes = []
1162 self.successes = []
1164 self.faildata = {}
1163 self.faildata = {}
1165
1164
1166 def addFailure(self, test, reason):
1165 def addFailure(self, test, reason):
1167 self.failures.append((test, reason))
1166 self.failures.append((test, reason))
1168
1167
1169 if self._options.first:
1168 if self._options.first:
1170 self.stop()
1169 self.stop()
1171 else:
1170 else:
1172 iolock.acquire()
1171 iolock.acquire()
1173 if not self._options.nodiff:
1172 if not self._options.nodiff:
1174 self.stream.write('\nERROR: %s output changed\n' % test)
1173 self.stream.write('\nERROR: %s output changed\n' % test)
1175
1174
1176 self.stream.write('!')
1175 self.stream.write('!')
1177 self.stream.flush()
1176 self.stream.flush()
1178 iolock.release()
1177 iolock.release()
1179
1178
1180 def addSuccess(self, test):
1179 def addSuccess(self, test):
1181 iolock.acquire()
1180 iolock.acquire()
1182 super(TestResult, self).addSuccess(test)
1181 super(TestResult, self).addSuccess(test)
1183 iolock.release()
1182 iolock.release()
1184 self.successes.append(test)
1183 self.successes.append(test)
1185
1184
1186 def addError(self, test, err):
1185 def addError(self, test, err):
1187 super(TestResult, self).addError(test, err)
1186 super(TestResult, self).addError(test, err)
1188 if self._options.first:
1187 if self._options.first:
1189 self.stop()
1188 self.stop()
1190
1189
1191 # Polyfill.
1190 # Polyfill.
1192 def addSkip(self, test, reason):
1191 def addSkip(self, test, reason):
1193 self.skipped.append((test, reason))
1192 self.skipped.append((test, reason))
1194 iolock.acquire()
1193 iolock.acquire()
1195 if self.showAll:
1194 if self.showAll:
1196 self.stream.writeln('skipped %s' % reason)
1195 self.stream.writeln('skipped %s' % reason)
1197 else:
1196 else:
1198 self.stream.write('s')
1197 self.stream.write('s')
1199 self.stream.flush()
1198 self.stream.flush()
1200 iolock.release()
1199 iolock.release()
1201
1200
1202 def addIgnore(self, test, reason):
1201 def addIgnore(self, test, reason):
1203 self.ignored.append((test, reason))
1202 self.ignored.append((test, reason))
1204 iolock.acquire()
1203 iolock.acquire()
1205 if self.showAll:
1204 if self.showAll:
1206 self.stream.writeln('ignored %s' % reason)
1205 self.stream.writeln('ignored %s' % reason)
1207 else:
1206 else:
1208 if reason != 'not retesting' and reason != "doesn't match keyword":
1207 if reason != 'not retesting' and reason != "doesn't match keyword":
1209 self.stream.write('i')
1208 self.stream.write('i')
1210 else:
1209 else:
1211 self.testsRun += 1
1210 self.testsRun += 1
1212 self.stream.flush()
1211 self.stream.flush()
1213 iolock.release()
1212 iolock.release()
1214
1213
1215 def addWarn(self, test, reason):
1214 def addWarn(self, test, reason):
1216 self.warned.append((test, reason))
1215 self.warned.append((test, reason))
1217
1216
1218 if self._options.first:
1217 if self._options.first:
1219 self.stop()
1218 self.stop()
1220
1219
1221 iolock.acquire()
1220 iolock.acquire()
1222 if self.showAll:
1221 if self.showAll:
1223 self.stream.writeln('warned %s' % reason)
1222 self.stream.writeln('warned %s' % reason)
1224 else:
1223 else:
1225 self.stream.write('~')
1224 self.stream.write('~')
1226 self.stream.flush()
1225 self.stream.flush()
1227 iolock.release()
1226 iolock.release()
1228
1227
1229 def addOutputMismatch(self, test, ret, got, expected):
1228 def addOutputMismatch(self, test, ret, got, expected):
1230 """Record a mismatch in test output for a particular test."""
1229 """Record a mismatch in test output for a particular test."""
1231 if self.shouldStop:
1230 if self.shouldStop:
1232 # don't print, some other test case already failed and
1231 # don't print, some other test case already failed and
1233 # printed, we're just stale and probably failed due to our
1232 # printed, we're just stale and probably failed due to our
1234 # temp dir getting cleaned up.
1233 # temp dir getting cleaned up.
1235 return
1234 return
1236
1235
1237 accepted = False
1236 accepted = False
1238 failed = False
1237 failed = False
1239 lines = []
1238 lines = []
1240
1239
1241 iolock.acquire()
1240 iolock.acquire()
1242 if self._options.nodiff:
1241 if self._options.nodiff:
1243 pass
1242 pass
1244 elif self._options.view:
1243 elif self._options.view:
1245 os.system("%s %s %s" %
1244 os.system("%s %s %s" %
1246 (self._options.view, test.refpath, test.errpath))
1245 (self._options.view, test.refpath, test.errpath))
1247 else:
1246 else:
1248 servefail, lines = getdiff(expected, got,
1247 servefail, lines = getdiff(expected, got,
1249 test.refpath, test.errpath)
1248 test.refpath, test.errpath)
1250 if servefail:
1249 if servefail:
1251 self.addFailure(
1250 self.addFailure(
1252 test,
1251 test,
1253 'server failed to start (HGPORT=%s)' % test._startport)
1252 'server failed to start (HGPORT=%s)' % test._startport)
1254 else:
1253 else:
1255 self.stream.write('\n')
1254 self.stream.write('\n')
1256 for line in lines:
1255 for line in lines:
1257 self.stream.write(line)
1256 self.stream.write(line)
1258 self.stream.flush()
1257 self.stream.flush()
1259
1258
1260 # handle interactive prompt without releasing iolock
1259 # handle interactive prompt without releasing iolock
1261 if self._options.interactive:
1260 if self._options.interactive:
1262 self.stream.write('Accept this change? [n] ')
1261 self.stream.write('Accept this change? [n] ')
1263 answer = sys.stdin.readline().strip()
1262 answer = sys.stdin.readline().strip()
1264 if answer.lower() in ('y', 'yes'):
1263 if answer.lower() in ('y', 'yes'):
1265 if test.name.endswith('.t'):
1264 if test.name.endswith('.t'):
1266 rename(test.errpath, test.path)
1265 rename(test.errpath, test.path)
1267 else:
1266 else:
1268 rename(test.errpath, '%s.out' % test.path)
1267 rename(test.errpath, '%s.out' % test.path)
1269 accepted = True
1268 accepted = True
1270 if not accepted and not failed:
1269 if not accepted and not failed:
1271 self.faildata[test.name] = ''.join(lines)
1270 self.faildata[test.name] = ''.join(lines)
1272 iolock.release()
1271 iolock.release()
1273
1272
1274 return accepted
1273 return accepted
1275
1274
1276 def startTest(self, test):
1275 def startTest(self, test):
1277 super(TestResult, self).startTest(test)
1276 super(TestResult, self).startTest(test)
1278
1277
1279 # os.times module computes the user time and system time spent by
1278 # os.times module computes the user time and system time spent by
1280 # child's processes along with real elapsed time taken by a process.
1279 # child's processes along with real elapsed time taken by a process.
1281 # This module has one limitation. It can only work for Linux user
1280 # This module has one limitation. It can only work for Linux user
1282 # and not for Windows.
1281 # and not for Windows.
1283 test.started = os.times()
1282 test.started = os.times()
1284
1283
1285 def stopTest(self, test, interrupted=False):
1284 def stopTest(self, test, interrupted=False):
1286 super(TestResult, self).stopTest(test)
1285 super(TestResult, self).stopTest(test)
1287
1286
1288 test.stopped = os.times()
1287 test.stopped = os.times()
1289
1288
1290 starttime = test.started
1289 starttime = test.started
1291 endtime = test.stopped
1290 endtime = test.stopped
1292 self.times.append((test.name, endtime[2] - starttime[2],
1291 self.times.append((test.name, endtime[2] - starttime[2],
1293 endtime[3] - starttime[3], endtime[4] - starttime[4]))
1292 endtime[3] - starttime[3], endtime[4] - starttime[4]))
1294
1293
1295 if interrupted:
1294 if interrupted:
1296 iolock.acquire()
1295 iolock.acquire()
1297 self.stream.writeln('INTERRUPTED: %s (after %d seconds)' % (
1296 self.stream.writeln('INTERRUPTED: %s (after %d seconds)' % (
1298 test.name, self.times[-1][3]))
1297 test.name, self.times[-1][3]))
1299 iolock.release()
1298 iolock.release()
1300
1299
1301 class TestSuite(unittest.TestSuite):
1300 class TestSuite(unittest.TestSuite):
1302 """Custom unittest TestSuite that knows how to execute Mercurial tests."""
1301 """Custom unittest TestSuite that knows how to execute Mercurial tests."""
1303
1302
1304 def __init__(self, testdir, jobs=1, whitelist=None, blacklist=None,
1303 def __init__(self, testdir, jobs=1, whitelist=None, blacklist=None,
1305 retest=False, keywords=None, loop=False, runs_per_test=1,
1304 retest=False, keywords=None, loop=False, runs_per_test=1,
1306 loadtest=None,
1305 loadtest=None,
1307 *args, **kwargs):
1306 *args, **kwargs):
1308 """Create a new instance that can run tests with a configuration.
1307 """Create a new instance that can run tests with a configuration.
1309
1308
1310 testdir specifies the directory where tests are executed from. This
1309 testdir specifies the directory where tests are executed from. This
1311 is typically the ``tests`` directory from Mercurial's source
1310 is typically the ``tests`` directory from Mercurial's source
1312 repository.
1311 repository.
1313
1312
1314 jobs specifies the number of jobs to run concurrently. Each test
1313 jobs specifies the number of jobs to run concurrently. Each test
1315 executes on its own thread. Tests actually spawn new processes, so
1314 executes on its own thread. Tests actually spawn new processes, so
1316 state mutation should not be an issue.
1315 state mutation should not be an issue.
1317
1316
1318 whitelist and blacklist denote tests that have been whitelisted and
1317 whitelist and blacklist denote tests that have been whitelisted and
1319 blacklisted, respectively. These arguments don't belong in TestSuite.
1318 blacklisted, respectively. These arguments don't belong in TestSuite.
1320 Instead, whitelist and blacklist should be handled by the thing that
1319 Instead, whitelist and blacklist should be handled by the thing that
1321 populates the TestSuite with tests. They are present to preserve
1320 populates the TestSuite with tests. They are present to preserve
1322 backwards compatible behavior which reports skipped tests as part
1321 backwards compatible behavior which reports skipped tests as part
1323 of the results.
1322 of the results.
1324
1323
1325 retest denotes whether to retest failed tests. This arguably belongs
1324 retest denotes whether to retest failed tests. This arguably belongs
1326 outside of TestSuite.
1325 outside of TestSuite.
1327
1326
1328 keywords denotes key words that will be used to filter which tests
1327 keywords denotes key words that will be used to filter which tests
1329 to execute. This arguably belongs outside of TestSuite.
1328 to execute. This arguably belongs outside of TestSuite.
1330
1329
1331 loop denotes whether to loop over tests forever.
1330 loop denotes whether to loop over tests forever.
1332 """
1331 """
1333 super(TestSuite, self).__init__(*args, **kwargs)
1332 super(TestSuite, self).__init__(*args, **kwargs)
1334
1333
1335 self._jobs = jobs
1334 self._jobs = jobs
1336 self._whitelist = whitelist
1335 self._whitelist = whitelist
1337 self._blacklist = blacklist
1336 self._blacklist = blacklist
1338 self._retest = retest
1337 self._retest = retest
1339 self._keywords = keywords
1338 self._keywords = keywords
1340 self._loop = loop
1339 self._loop = loop
1341 self._runs_per_test = runs_per_test
1340 self._runs_per_test = runs_per_test
1342 self._loadtest = loadtest
1341 self._loadtest = loadtest
1343
1342
1344 def run(self, result):
1343 def run(self, result):
1345 # We have a number of filters that need to be applied. We do this
1344 # We have a number of filters that need to be applied. We do this
1346 # here instead of inside Test because it makes the running logic for
1345 # here instead of inside Test because it makes the running logic for
1347 # Test simpler.
1346 # Test simpler.
1348 tests = []
1347 tests = []
1349 num_tests = [0]
1348 num_tests = [0]
1350 for test in self._tests:
1349 for test in self._tests:
1351 def get():
1350 def get():
1352 num_tests[0] += 1
1351 num_tests[0] += 1
1353 if getattr(test, 'should_reload', False):
1352 if getattr(test, 'should_reload', False):
1354 return self._loadtest(test.name, num_tests[0])
1353 return self._loadtest(test.name, num_tests[0])
1355 return test
1354 return test
1356 if not os.path.exists(test.path):
1355 if not os.path.exists(test.path):
1357 result.addSkip(test, "Doesn't exist")
1356 result.addSkip(test, "Doesn't exist")
1358 continue
1357 continue
1359
1358
1360 if not (self._whitelist and test.name in self._whitelist):
1359 if not (self._whitelist and test.name in self._whitelist):
1361 if self._blacklist and test.name in self._blacklist:
1360 if self._blacklist and test.name in self._blacklist:
1362 result.addSkip(test, 'blacklisted')
1361 result.addSkip(test, 'blacklisted')
1363 continue
1362 continue
1364
1363
1365 if self._retest and not os.path.exists(test.errpath):
1364 if self._retest and not os.path.exists(test.errpath):
1366 result.addIgnore(test, 'not retesting')
1365 result.addIgnore(test, 'not retesting')
1367 continue
1366 continue
1368
1367
1369 if self._keywords:
1368 if self._keywords:
1370 f = open(test.path, 'rb')
1369 f = open(test.path, 'rb')
1371 t = f.read().lower() + test.name.lower()
1370 t = f.read().lower() + test.name.lower()
1372 f.close()
1371 f.close()
1373 ignored = False
1372 ignored = False
1374 for k in self._keywords.lower().split():
1373 for k in self._keywords.lower().split():
1375 if k not in t:
1374 if k not in t:
1376 result.addIgnore(test, "doesn't match keyword")
1375 result.addIgnore(test, "doesn't match keyword")
1377 ignored = True
1376 ignored = True
1378 break
1377 break
1379
1378
1380 if ignored:
1379 if ignored:
1381 continue
1380 continue
1382 for _ in xrange(self._runs_per_test):
1381 for _ in xrange(self._runs_per_test):
1383 tests.append(get())
1382 tests.append(get())
1384
1383
1385 runtests = list(tests)
1384 runtests = list(tests)
1386 done = queue.Queue()
1385 done = queue.Queue()
1387 running = 0
1386 running = 0
1388
1387
1389 def job(test, result):
1388 def job(test, result):
1390 try:
1389 try:
1391 test(result)
1390 test(result)
1392 done.put(None)
1391 done.put(None)
1393 except KeyboardInterrupt:
1392 except KeyboardInterrupt:
1394 pass
1393 pass
1395 except: # re-raises
1394 except: # re-raises
1396 done.put(('!', test, 'run-test raised an error, see traceback'))
1395 done.put(('!', test, 'run-test raised an error, see traceback'))
1397 raise
1396 raise
1398
1397
1399 stoppedearly = False
1398 stoppedearly = False
1400
1399
1401 try:
1400 try:
1402 while tests or running:
1401 while tests or running:
1403 if not done.empty() or running == self._jobs or not tests:
1402 if not done.empty() or running == self._jobs or not tests:
1404 try:
1403 try:
1405 done.get(True, 1)
1404 done.get(True, 1)
1406 running -= 1
1405 running -= 1
1407 if result and result.shouldStop:
1406 if result and result.shouldStop:
1408 stoppedearly = True
1407 stoppedearly = True
1409 break
1408 break
1410 except queue.Empty:
1409 except queue.Empty:
1411 continue
1410 continue
1412 if tests and not running == self._jobs:
1411 if tests and not running == self._jobs:
1413 test = tests.pop(0)
1412 test = tests.pop(0)
1414 if self._loop:
1413 if self._loop:
1415 if getattr(test, 'should_reload', False):
1414 if getattr(test, 'should_reload', False):
1416 num_tests[0] += 1
1415 num_tests[0] += 1
1417 tests.append(
1416 tests.append(
1418 self._loadtest(test.name, num_tests[0]))
1417 self._loadtest(test.name, num_tests[0]))
1419 else:
1418 else:
1420 tests.append(test)
1419 tests.append(test)
1421 t = threading.Thread(target=job, name=test.name,
1420 t = threading.Thread(target=job, name=test.name,
1422 args=(test, result))
1421 args=(test, result))
1423 t.start()
1422 t.start()
1424 running += 1
1423 running += 1
1425
1424
1426 # If we stop early we still need to wait on started tests to
1425 # If we stop early we still need to wait on started tests to
1427 # finish. Otherwise, there is a race between the test completing
1426 # finish. Otherwise, there is a race between the test completing
1428 # and the test's cleanup code running. This could result in the
1427 # and the test's cleanup code running. This could result in the
1429 # test reporting incorrect.
1428 # test reporting incorrect.
1430 if stoppedearly:
1429 if stoppedearly:
1431 while running:
1430 while running:
1432 try:
1431 try:
1433 done.get(True, 1)
1432 done.get(True, 1)
1434 running -= 1
1433 running -= 1
1435 except queue.Empty:
1434 except queue.Empty:
1436 continue
1435 continue
1437 except KeyboardInterrupt:
1436 except KeyboardInterrupt:
1438 for test in runtests:
1437 for test in runtests:
1439 test.abort()
1438 test.abort()
1440
1439
1441 return result
1440 return result
1442
1441
1443 class TextTestRunner(unittest.TextTestRunner):
1442 class TextTestRunner(unittest.TextTestRunner):
1444 """Custom unittest test runner that uses appropriate settings."""
1443 """Custom unittest test runner that uses appropriate settings."""
1445
1444
1446 def __init__(self, runner, *args, **kwargs):
1445 def __init__(self, runner, *args, **kwargs):
1447 super(TextTestRunner, self).__init__(*args, **kwargs)
1446 super(TextTestRunner, self).__init__(*args, **kwargs)
1448
1447
1449 self._runner = runner
1448 self._runner = runner
1450
1449
1451 def run(self, test):
1450 def run(self, test):
1452 result = TestResult(self._runner.options, self.stream,
1451 result = TestResult(self._runner.options, self.stream,
1453 self.descriptions, self.verbosity)
1452 self.descriptions, self.verbosity)
1454
1453
1455 test(result)
1454 test(result)
1456
1455
1457 failed = len(result.failures)
1456 failed = len(result.failures)
1458 warned = len(result.warned)
1457 warned = len(result.warned)
1459 skipped = len(result.skipped)
1458 skipped = len(result.skipped)
1460 ignored = len(result.ignored)
1459 ignored = len(result.ignored)
1461
1460
1462 iolock.acquire()
1461 iolock.acquire()
1463 self.stream.writeln('')
1462 self.stream.writeln('')
1464
1463
1465 if not self._runner.options.noskips:
1464 if not self._runner.options.noskips:
1466 for test, msg in result.skipped:
1465 for test, msg in result.skipped:
1467 self.stream.writeln('Skipped %s: %s' % (test.name, msg))
1466 self.stream.writeln('Skipped %s: %s' % (test.name, msg))
1468 for test, msg in result.warned:
1467 for test, msg in result.warned:
1469 self.stream.writeln('Warned %s: %s' % (test.name, msg))
1468 self.stream.writeln('Warned %s: %s' % (test.name, msg))
1470 for test, msg in result.failures:
1469 for test, msg in result.failures:
1471 self.stream.writeln('Failed %s: %s' % (test.name, msg))
1470 self.stream.writeln('Failed %s: %s' % (test.name, msg))
1472 for test, msg in result.errors:
1471 for test, msg in result.errors:
1473 self.stream.writeln('Errored %s: %s' % (test.name, msg))
1472 self.stream.writeln('Errored %s: %s' % (test.name, msg))
1474
1473
1475 if self._runner.options.xunit:
1474 if self._runner.options.xunit:
1476 xuf = open(self._runner.options.xunit, 'wb')
1475 xuf = open(self._runner.options.xunit, 'wb')
1477 try:
1476 try:
1478 timesd = dict(
1477 timesd = dict(
1479 (test, real) for test, cuser, csys, real in result.times)
1478 (test, real) for test, cuser, csys, real in result.times)
1480 doc = minidom.Document()
1479 doc = minidom.Document()
1481 s = doc.createElement('testsuite')
1480 s = doc.createElement('testsuite')
1482 s.setAttribute('name', 'run-tests')
1481 s.setAttribute('name', 'run-tests')
1483 s.setAttribute('tests', str(result.testsRun))
1482 s.setAttribute('tests', str(result.testsRun))
1484 s.setAttribute('errors', "0") # TODO
1483 s.setAttribute('errors', "0") # TODO
1485 s.setAttribute('failures', str(failed))
1484 s.setAttribute('failures', str(failed))
1486 s.setAttribute('skipped', str(skipped + ignored))
1485 s.setAttribute('skipped', str(skipped + ignored))
1487 doc.appendChild(s)
1486 doc.appendChild(s)
1488 for tc in result.successes:
1487 for tc in result.successes:
1489 t = doc.createElement('testcase')
1488 t = doc.createElement('testcase')
1490 t.setAttribute('name', tc.name)
1489 t.setAttribute('name', tc.name)
1491 t.setAttribute('time', '%.3f' % timesd[tc.name])
1490 t.setAttribute('time', '%.3f' % timesd[tc.name])
1492 s.appendChild(t)
1491 s.appendChild(t)
1493 for tc, err in sorted(result.faildata.iteritems()):
1492 for tc, err in sorted(result.faildata.iteritems()):
1494 t = doc.createElement('testcase')
1493 t = doc.createElement('testcase')
1495 t.setAttribute('name', tc)
1494 t.setAttribute('name', tc)
1496 t.setAttribute('time', '%.3f' % timesd[tc])
1495 t.setAttribute('time', '%.3f' % timesd[tc])
1497 # createCDATASection expects a unicode or it will convert
1496 # createCDATASection expects a unicode or it will convert
1498 # using default conversion rules, which will fail if
1497 # using default conversion rules, which will fail if
1499 # string isn't ASCII.
1498 # string isn't ASCII.
1500 err = cdatasafe(err).decode('utf-8', 'replace')
1499 err = cdatasafe(err).decode('utf-8', 'replace')
1501 cd = doc.createCDATASection(err)
1500 cd = doc.createCDATASection(err)
1502 t.appendChild(cd)
1501 t.appendChild(cd)
1503 s.appendChild(t)
1502 s.appendChild(t)
1504 xuf.write(doc.toprettyxml(indent=' ', encoding='utf-8'))
1503 xuf.write(doc.toprettyxml(indent=' ', encoding='utf-8'))
1505 finally:
1504 finally:
1506 xuf.close()
1505 xuf.close()
1507
1506
1508 if self._runner.options.json:
1507 if self._runner.options.json:
1509 if json is None:
1508 if json is None:
1510 raise ImportError("json module not installed")
1509 raise ImportError("json module not installed")
1511 jsonpath = os.path.join(self._runner._testdir, 'report.json')
1510 jsonpath = os.path.join(self._runner._testdir, 'report.json')
1512 fp = open(jsonpath, 'w')
1511 fp = open(jsonpath, 'w')
1513 try:
1512 try:
1514 timesd = {}
1513 timesd = {}
1515 for test, cuser, csys, real in result.times:
1514 for test, cuser, csys, real in result.times:
1516 timesd[test] = (real, cuser, csys)
1515 timesd[test] = (real, cuser, csys)
1517
1516
1518 outcome = {}
1517 outcome = {}
1519 for tc in result.successes:
1518 for tc in result.successes:
1520 testresult = {'result': 'success',
1519 testresult = {'result': 'success',
1521 'time': ('%0.3f' % timesd[tc.name][0]),
1520 'time': ('%0.3f' % timesd[tc.name][0]),
1522 'cuser': ('%0.3f' % timesd[tc.name][1]),
1521 'cuser': ('%0.3f' % timesd[tc.name][1]),
1523 'csys': ('%0.3f' % timesd[tc.name][2])}
1522 'csys': ('%0.3f' % timesd[tc.name][2])}
1524 outcome[tc.name] = testresult
1523 outcome[tc.name] = testresult
1525
1524
1526 for tc, err in sorted(result.faildata.iteritems()):
1525 for tc, err in sorted(result.faildata.iteritems()):
1527 testresult = {'result': 'failure',
1526 testresult = {'result': 'failure',
1528 'time': ('%0.3f' % timesd[tc][0]),
1527 'time': ('%0.3f' % timesd[tc][0]),
1529 'cuser': ('%0.3f' % timesd[tc][1]),
1528 'cuser': ('%0.3f' % timesd[tc][1]),
1530 'csys': ('%0.3f' % timesd[tc][2])}
1529 'csys': ('%0.3f' % timesd[tc][2])}
1531 outcome[tc] = testresult
1530 outcome[tc] = testresult
1532
1531
1533 for tc, reason in result.skipped:
1532 for tc, reason in result.skipped:
1534 testresult = {'result': 'skip',
1533 testresult = {'result': 'skip',
1535 'time': ('%0.3f' % timesd[tc.name][0]),
1534 'time': ('%0.3f' % timesd[tc.name][0]),
1536 'cuser': ('%0.3f' % timesd[tc.name][1]),
1535 'cuser': ('%0.3f' % timesd[tc.name][1]),
1537 'csys': ('%0.3f' % timesd[tc.name][2])}
1536 'csys': ('%0.3f' % timesd[tc.name][2])}
1538 outcome[tc.name] = testresult
1537 outcome[tc.name] = testresult
1539
1538
1540 jsonout = json.dumps(outcome, sort_keys=True, indent=4)
1539 jsonout = json.dumps(outcome, sort_keys=True, indent=4)
1541 fp.writelines(("testreport =", jsonout))
1540 fp.writelines(("testreport =", jsonout))
1542 finally:
1541 finally:
1543 fp.close()
1542 fp.close()
1544
1543
1545 self._runner._checkhglib('Tested')
1544 self._runner._checkhglib('Tested')
1546
1545
1547 self.stream.writeln('# Ran %d tests, %d skipped, %d warned, %d failed.'
1546 self.stream.writeln('# Ran %d tests, %d skipped, %d warned, %d failed.'
1548 % (result.testsRun,
1547 % (result.testsRun,
1549 skipped + ignored, warned, failed))
1548 skipped + ignored, warned, failed))
1550 if failed:
1549 if failed:
1551 self.stream.writeln('python hash seed: %s' %
1550 self.stream.writeln('python hash seed: %s' %
1552 os.environ['PYTHONHASHSEED'])
1551 os.environ['PYTHONHASHSEED'])
1553 if self._runner.options.time:
1552 if self._runner.options.time:
1554 self.printtimes(result.times)
1553 self.printtimes(result.times)
1555
1554
1556 iolock.release()
1555 iolock.release()
1557
1556
1558 return result
1557 return result
1559
1558
1560 def printtimes(self, times):
1559 def printtimes(self, times):
1561 # iolock held by run
1560 # iolock held by run
1562 self.stream.writeln('# Producing time report')
1561 self.stream.writeln('# Producing time report')
1563 times.sort(key=lambda t: (t[3]))
1562 times.sort(key=lambda t: (t[3]))
1564 cols = '%7.3f %7.3f %7.3f %s'
1563 cols = '%7.3f %7.3f %7.3f %s'
1565 self.stream.writeln('%-7s %-7s %-7s %s' % ('cuser', 'csys', 'real',
1564 self.stream.writeln('%-7s %-7s %-7s %s' % ('cuser', 'csys', 'real',
1566 'Test'))
1565 'Test'))
1567 for test, cuser, csys, real in times:
1566 for test, cuser, csys, real in times:
1568 self.stream.writeln(cols % (cuser, csys, real, test))
1567 self.stream.writeln(cols % (cuser, csys, real, test))
1569
1568
1570 class TestRunner(object):
1569 class TestRunner(object):
1571 """Holds context for executing tests.
1570 """Holds context for executing tests.
1572
1571
1573 Tests rely on a lot of state. This object holds it for them.
1572 Tests rely on a lot of state. This object holds it for them.
1574 """
1573 """
1575
1574
1576 # Programs required to run tests.
1575 # Programs required to run tests.
1577 REQUIREDTOOLS = [
1576 REQUIREDTOOLS = [
1578 os.path.basename(sys.executable),
1577 os.path.basename(sys.executable),
1579 'diff',
1578 'diff',
1580 'grep',
1579 'grep',
1581 'unzip',
1580 'unzip',
1582 'gunzip',
1581 'gunzip',
1583 'bunzip2',
1582 'bunzip2',
1584 'sed',
1583 'sed',
1585 ]
1584 ]
1586
1585
1587 # Maps file extensions to test class.
1586 # Maps file extensions to test class.
1588 TESTTYPES = [
1587 TESTTYPES = [
1589 ('.py', PythonTest),
1588 ('.py', PythonTest),
1590 ('.t', TTest),
1589 ('.t', TTest),
1591 ]
1590 ]
1592
1591
1593 def __init__(self):
1592 def __init__(self):
1594 self.options = None
1593 self.options = None
1595 self._hgroot = None
1594 self._hgroot = None
1596 self._testdir = None
1595 self._testdir = None
1597 self._hgtmp = None
1596 self._hgtmp = None
1598 self._installdir = None
1597 self._installdir = None
1599 self._bindir = None
1598 self._bindir = None
1600 self._tmpbinddir = None
1599 self._tmpbinddir = None
1601 self._pythondir = None
1600 self._pythondir = None
1602 self._coveragefile = None
1601 self._coveragefile = None
1603 self._createdfiles = []
1602 self._createdfiles = []
1604 self._hgpath = None
1603 self._hgpath = None
1605
1604
1606 def run(self, args, parser=None):
1605 def run(self, args, parser=None):
1607 """Run the test suite."""
1606 """Run the test suite."""
1608 oldmask = os.umask(022)
1607 oldmask = os.umask(022)
1609 try:
1608 try:
1610 parser = parser or getparser()
1609 parser = parser or getparser()
1611 options, args = parseargs(args, parser)
1610 options, args = parseargs(args, parser)
1612 self.options = options
1611 self.options = options
1613
1612
1614 self._checktools()
1613 self._checktools()
1615 tests = self.findtests(args)
1614 tests = self.findtests(args)
1616 return self._run(tests)
1615 return self._run(tests)
1617 finally:
1616 finally:
1618 os.umask(oldmask)
1617 os.umask(oldmask)
1619
1618
1620 def _run(self, tests):
1619 def _run(self, tests):
1621 if self.options.random:
1620 if self.options.random:
1622 random.shuffle(tests)
1621 random.shuffle(tests)
1623 else:
1622 else:
1624 # keywords for slow tests
1623 # keywords for slow tests
1625 slow = 'svn gendoc check-code-hg'.split()
1624 slow = 'svn gendoc check-code-hg'.split()
1626 def sortkey(f):
1625 def sortkey(f):
1627 # run largest tests first, as they tend to take the longest
1626 # run largest tests first, as they tend to take the longest
1628 try:
1627 try:
1629 val = -os.stat(f).st_size
1628 val = -os.stat(f).st_size
1630 except OSError, e:
1629 except OSError, e:
1631 if e.errno != errno.ENOENT:
1630 if e.errno != errno.ENOENT:
1632 raise
1631 raise
1633 return -1e9 # file does not exist, tell early
1632 return -1e9 # file does not exist, tell early
1634 for kw in slow:
1633 for kw in slow:
1635 if kw in f:
1634 if kw in f:
1636 val *= 10
1635 val *= 10
1637 return val
1636 return val
1638 tests.sort(key=sortkey)
1637 tests.sort(key=sortkey)
1639
1638
1640 self._testdir = os.environ['TESTDIR'] = os.getcwd()
1639 self._testdir = os.environ['TESTDIR'] = os.getcwd()
1641
1640
1642 if 'PYTHONHASHSEED' not in os.environ:
1641 if 'PYTHONHASHSEED' not in os.environ:
1643 # use a random python hash seed all the time
1642 # use a random python hash seed all the time
1644 # we do the randomness ourself to know what seed is used
1643 # we do the randomness ourself to know what seed is used
1645 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
1644 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
1646
1645
1647 if self.options.tmpdir:
1646 if self.options.tmpdir:
1648 self.options.keep_tmpdir = True
1647 self.options.keep_tmpdir = True
1649 tmpdir = self.options.tmpdir
1648 tmpdir = self.options.tmpdir
1650 if os.path.exists(tmpdir):
1649 if os.path.exists(tmpdir):
1651 # Meaning of tmpdir has changed since 1.3: we used to create
1650 # Meaning of tmpdir has changed since 1.3: we used to create
1652 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
1651 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
1653 # tmpdir already exists.
1652 # tmpdir already exists.
1654 print "error: temp dir %r already exists" % tmpdir
1653 print "error: temp dir %r already exists" % tmpdir
1655 return 1
1654 return 1
1656
1655
1657 # Automatically removing tmpdir sounds convenient, but could
1656 # Automatically removing tmpdir sounds convenient, but could
1658 # really annoy anyone in the habit of using "--tmpdir=/tmp"
1657 # really annoy anyone in the habit of using "--tmpdir=/tmp"
1659 # or "--tmpdir=$HOME".
1658 # or "--tmpdir=$HOME".
1660 #vlog("# Removing temp dir", tmpdir)
1659 #vlog("# Removing temp dir", tmpdir)
1661 #shutil.rmtree(tmpdir)
1660 #shutil.rmtree(tmpdir)
1662 os.makedirs(tmpdir)
1661 os.makedirs(tmpdir)
1663 else:
1662 else:
1664 d = None
1663 d = None
1665 if os.name == 'nt':
1664 if os.name == 'nt':
1666 # without this, we get the default temp dir location, but
1665 # without this, we get the default temp dir location, but
1667 # in all lowercase, which causes troubles with paths (issue3490)
1666 # in all lowercase, which causes troubles with paths (issue3490)
1668 d = os.getenv('TMP')
1667 d = os.getenv('TMP')
1669 tmpdir = tempfile.mkdtemp('', 'hgtests.', d)
1668 tmpdir = tempfile.mkdtemp('', 'hgtests.', d)
1670 self._hgtmp = os.environ['HGTMP'] = os.path.realpath(tmpdir)
1669 self._hgtmp = os.environ['HGTMP'] = os.path.realpath(tmpdir)
1671
1670
1672 if self.options.with_hg:
1671 if self.options.with_hg:
1673 self._installdir = None
1672 self._installdir = None
1674 self._bindir = os.path.dirname(os.path.realpath(
1673 self._bindir = os.path.dirname(os.path.realpath(
1675 self.options.with_hg))
1674 self.options.with_hg))
1676 self._tmpbindir = os.path.join(self._hgtmp, 'install', 'bin')
1675 self._tmpbindir = os.path.join(self._hgtmp, 'install', 'bin')
1677 os.makedirs(self._tmpbindir)
1676 os.makedirs(self._tmpbindir)
1678
1677
1679 # This looks redundant with how Python initializes sys.path from
1678 # This looks redundant with how Python initializes sys.path from
1680 # the location of the script being executed. Needed because the
1679 # the location of the script being executed. Needed because the
1681 # "hg" specified by --with-hg is not the only Python script
1680 # "hg" specified by --with-hg is not the only Python script
1682 # executed in the test suite that needs to import 'mercurial'
1681 # executed in the test suite that needs to import 'mercurial'
1683 # ... which means it's not really redundant at all.
1682 # ... which means it's not really redundant at all.
1684 self._pythondir = self._bindir
1683 self._pythondir = self._bindir
1685 else:
1684 else:
1686 self._installdir = os.path.join(self._hgtmp, "install")
1685 self._installdir = os.path.join(self._hgtmp, "install")
1687 self._bindir = os.environ["BINDIR"] = \
1686 self._bindir = os.environ["BINDIR"] = \
1688 os.path.join(self._installdir, "bin")
1687 os.path.join(self._installdir, "bin")
1689 self._tmpbindir = self._bindir
1688 self._tmpbindir = self._bindir
1690 self._pythondir = os.path.join(self._installdir, "lib", "python")
1689 self._pythondir = os.path.join(self._installdir, "lib", "python")
1691
1690
1692 os.environ["BINDIR"] = self._bindir
1691 os.environ["BINDIR"] = self._bindir
1693 os.environ["PYTHON"] = PYTHON
1692 os.environ["PYTHON"] = PYTHON
1694
1693
1695 runtestdir = os.path.abspath(os.path.dirname(__file__))
1694 runtestdir = os.path.abspath(os.path.dirname(__file__))
1696 path = [self._bindir, runtestdir] + os.environ["PATH"].split(os.pathsep)
1695 path = [self._bindir, runtestdir] + os.environ["PATH"].split(os.pathsep)
1697 if self._tmpbindir != self._bindir:
1696 if self._tmpbindir != self._bindir:
1698 path = [self._tmpbindir] + path
1697 path = [self._tmpbindir] + path
1699 os.environ["PATH"] = os.pathsep.join(path)
1698 os.environ["PATH"] = os.pathsep.join(path)
1700
1699
1701 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
1700 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
1702 # can run .../tests/run-tests.py test-foo where test-foo
1701 # can run .../tests/run-tests.py test-foo where test-foo
1703 # adds an extension to HGRC. Also include run-test.py directory to
1702 # adds an extension to HGRC. Also include run-test.py directory to
1704 # import modules like heredoctest.
1703 # import modules like heredoctest.
1705 pypath = [self._pythondir, self._testdir, runtestdir]
1704 pypath = [self._pythondir, self._testdir, runtestdir]
1706 # We have to augment PYTHONPATH, rather than simply replacing
1705 # We have to augment PYTHONPATH, rather than simply replacing
1707 # it, in case external libraries are only available via current
1706 # it, in case external libraries are only available via current
1708 # PYTHONPATH. (In particular, the Subversion bindings on OS X
1707 # PYTHONPATH. (In particular, the Subversion bindings on OS X
1709 # are in /opt/subversion.)
1708 # are in /opt/subversion.)
1710 oldpypath = os.environ.get(IMPL_PATH)
1709 oldpypath = os.environ.get(IMPL_PATH)
1711 if oldpypath:
1710 if oldpypath:
1712 pypath.append(oldpypath)
1711 pypath.append(oldpypath)
1713 os.environ[IMPL_PATH] = os.pathsep.join(pypath)
1712 os.environ[IMPL_PATH] = os.pathsep.join(pypath)
1714
1713
1715 if self.options.pure:
1714 if self.options.pure:
1716 os.environ["HGTEST_RUN_TESTS_PURE"] = "--pure"
1715 os.environ["HGTEST_RUN_TESTS_PURE"] = "--pure"
1717
1716
1718 self._coveragefile = os.path.join(self._testdir, '.coverage')
1717 self._coveragefile = os.path.join(self._testdir, '.coverage')
1719
1718
1720 vlog("# Using TESTDIR", self._testdir)
1719 vlog("# Using TESTDIR", self._testdir)
1721 vlog("# Using HGTMP", self._hgtmp)
1720 vlog("# Using HGTMP", self._hgtmp)
1722 vlog("# Using PATH", os.environ["PATH"])
1721 vlog("# Using PATH", os.environ["PATH"])
1723 vlog("# Using", IMPL_PATH, os.environ[IMPL_PATH])
1722 vlog("# Using", IMPL_PATH, os.environ[IMPL_PATH])
1724
1723
1725 try:
1724 try:
1726 return self._runtests(tests) or 0
1725 return self._runtests(tests) or 0
1727 finally:
1726 finally:
1728 time.sleep(.1)
1727 time.sleep(.1)
1729 self._cleanup()
1728 self._cleanup()
1730
1729
1731 def findtests(self, args):
1730 def findtests(self, args):
1732 """Finds possible test files from arguments.
1731 """Finds possible test files from arguments.
1733
1732
1734 If you wish to inject custom tests into the test harness, this would
1733 If you wish to inject custom tests into the test harness, this would
1735 be a good function to monkeypatch or override in a derived class.
1734 be a good function to monkeypatch or override in a derived class.
1736 """
1735 """
1737 if not args:
1736 if not args:
1738 if self.options.changed:
1737 if self.options.changed:
1739 proc = Popen4('hg st --rev "%s" -man0 .' %
1738 proc = Popen4('hg st --rev "%s" -man0 .' %
1740 self.options.changed, None, 0)
1739 self.options.changed, None, 0)
1741 stdout, stderr = proc.communicate()
1740 stdout, stderr = proc.communicate()
1742 args = stdout.strip('\0').split('\0')
1741 args = stdout.strip('\0').split('\0')
1743 else:
1742 else:
1744 args = os.listdir('.')
1743 args = os.listdir('.')
1745
1744
1746 return [t for t in args
1745 return [t for t in args
1747 if os.path.basename(t).startswith('test-')
1746 if os.path.basename(t).startswith('test-')
1748 and (t.endswith('.py') or t.endswith('.t'))]
1747 and (t.endswith('.py') or t.endswith('.t'))]
1749
1748
1750 def _runtests(self, tests):
1749 def _runtests(self, tests):
1751 try:
1750 try:
1752 if self._installdir:
1751 if self._installdir:
1753 self._installhg()
1752 self._installhg()
1754 self._checkhglib("Testing")
1753 self._checkhglib("Testing")
1755 else:
1754 else:
1756 self._usecorrectpython()
1755 self._usecorrectpython()
1757
1756
1758 if self.options.restart:
1757 if self.options.restart:
1759 orig = list(tests)
1758 orig = list(tests)
1760 while tests:
1759 while tests:
1761 if os.path.exists(tests[0] + ".err"):
1760 if os.path.exists(tests[0] + ".err"):
1762 break
1761 break
1763 tests.pop(0)
1762 tests.pop(0)
1764 if not tests:
1763 if not tests:
1765 print "running all tests"
1764 print "running all tests"
1766 tests = orig
1765 tests = orig
1767
1766
1768 tests = [self._gettest(t, i) for i, t in enumerate(tests)]
1767 tests = [self._gettest(t, i) for i, t in enumerate(tests)]
1769
1768
1770 failed = False
1769 failed = False
1771 warned = False
1770 warned = False
1772
1771
1773 suite = TestSuite(self._testdir,
1772 suite = TestSuite(self._testdir,
1774 jobs=self.options.jobs,
1773 jobs=self.options.jobs,
1775 whitelist=self.options.whitelisted,
1774 whitelist=self.options.whitelisted,
1776 blacklist=self.options.blacklist,
1775 blacklist=self.options.blacklist,
1777 retest=self.options.retest,
1776 retest=self.options.retest,
1778 keywords=self.options.keywords,
1777 keywords=self.options.keywords,
1779 loop=self.options.loop,
1778 loop=self.options.loop,
1780 runs_per_test=self.options.runs_per_test,
1779 runs_per_test=self.options.runs_per_test,
1781 tests=tests, loadtest=self._gettest)
1780 tests=tests, loadtest=self._gettest)
1782 verbosity = 1
1781 verbosity = 1
1783 if self.options.verbose:
1782 if self.options.verbose:
1784 verbosity = 2
1783 verbosity = 2
1785 runner = TextTestRunner(self, verbosity=verbosity)
1784 runner = TextTestRunner(self, verbosity=verbosity)
1786 result = runner.run(suite)
1785 result = runner.run(suite)
1787
1786
1788 if result.failures:
1787 if result.failures:
1789 failed = True
1788 failed = True
1790 if result.warned:
1789 if result.warned:
1791 warned = True
1790 warned = True
1792
1791
1793 if self.options.anycoverage:
1792 if self.options.anycoverage:
1794 self._outputcoverage()
1793 self._outputcoverage()
1795 except KeyboardInterrupt:
1794 except KeyboardInterrupt:
1796 failed = True
1795 failed = True
1797 print "\ninterrupted!"
1796 print "\ninterrupted!"
1798
1797
1799 if failed:
1798 if failed:
1800 return 1
1799 return 1
1801 if warned:
1800 if warned:
1802 return 80
1801 return 80
1803
1802
1804 def _gettest(self, test, count):
1803 def _gettest(self, test, count):
1805 """Obtain a Test by looking at its filename.
1804 """Obtain a Test by looking at its filename.
1806
1805
1807 Returns a Test instance. The Test may not be runnable if it doesn't
1806 Returns a Test instance. The Test may not be runnable if it doesn't
1808 map to a known type.
1807 map to a known type.
1809 """
1808 """
1810 lctest = test.lower()
1809 lctest = test.lower()
1811 testcls = Test
1810 testcls = Test
1812
1811
1813 for ext, cls in self.TESTTYPES:
1812 for ext, cls in self.TESTTYPES:
1814 if lctest.endswith(ext):
1813 if lctest.endswith(ext):
1815 testcls = cls
1814 testcls = cls
1816 break
1815 break
1817
1816
1818 refpath = os.path.join(self._testdir, test)
1817 refpath = os.path.join(self._testdir, test)
1819 tmpdir = os.path.join(self._hgtmp, 'child%d' % count)
1818 tmpdir = os.path.join(self._hgtmp, 'child%d' % count)
1820
1819
1821 t = testcls(refpath, tmpdir,
1820 t = testcls(refpath, tmpdir,
1822 keeptmpdir=self.options.keep_tmpdir,
1821 keeptmpdir=self.options.keep_tmpdir,
1823 debug=self.options.debug,
1822 debug=self.options.debug,
1824 timeout=self.options.timeout,
1823 timeout=self.options.timeout,
1825 startport=self.options.port + count * 3,
1824 startport=self.options.port + count * 3,
1826 extraconfigopts=self.options.extra_config_opt,
1825 extraconfigopts=self.options.extra_config_opt,
1827 py3kwarnings=self.options.py3k_warnings,
1826 py3kwarnings=self.options.py3k_warnings,
1828 shell=self.options.shell)
1827 shell=self.options.shell)
1829 t.should_reload = True
1828 t.should_reload = True
1830 return t
1829 return t
1831
1830
1832 def _cleanup(self):
1831 def _cleanup(self):
1833 """Clean up state from this test invocation."""
1832 """Clean up state from this test invocation."""
1834
1833
1835 if self.options.keep_tmpdir:
1834 if self.options.keep_tmpdir:
1836 return
1835 return
1837
1836
1838 vlog("# Cleaning up HGTMP", self._hgtmp)
1837 vlog("# Cleaning up HGTMP", self._hgtmp)
1839 shutil.rmtree(self._hgtmp, True)
1838 shutil.rmtree(self._hgtmp, True)
1840 for f in self._createdfiles:
1839 for f in self._createdfiles:
1841 try:
1840 try:
1842 os.remove(f)
1841 os.remove(f)
1843 except OSError:
1842 except OSError:
1844 pass
1843 pass
1845
1844
1846 def _usecorrectpython(self):
1845 def _usecorrectpython(self):
1847 """Configure the environment to use the appropriate Python in tests."""
1846 """Configure the environment to use the appropriate Python in tests."""
1848 # Tests must use the same interpreter as us or bad things will happen.
1847 # Tests must use the same interpreter as us or bad things will happen.
1849 pyexename = sys.platform == 'win32' and 'python.exe' or 'python'
1848 pyexename = sys.platform == 'win32' and 'python.exe' or 'python'
1850 if getattr(os, 'symlink', None):
1849 if getattr(os, 'symlink', None):
1851 vlog("# Making python executable in test path a symlink to '%s'" %
1850 vlog("# Making python executable in test path a symlink to '%s'" %
1852 sys.executable)
1851 sys.executable)
1853 mypython = os.path.join(self._tmpbindir, pyexename)
1852 mypython = os.path.join(self._tmpbindir, pyexename)
1854 try:
1853 try:
1855 if os.readlink(mypython) == sys.executable:
1854 if os.readlink(mypython) == sys.executable:
1856 return
1855 return
1857 os.unlink(mypython)
1856 os.unlink(mypython)
1858 except OSError, err:
1857 except OSError, err:
1859 if err.errno != errno.ENOENT:
1858 if err.errno != errno.ENOENT:
1860 raise
1859 raise
1861 if self._findprogram(pyexename) != sys.executable:
1860 if self._findprogram(pyexename) != sys.executable:
1862 try:
1861 try:
1863 os.symlink(sys.executable, mypython)
1862 os.symlink(sys.executable, mypython)
1864 self._createdfiles.append(mypython)
1863 self._createdfiles.append(mypython)
1865 except OSError, err:
1864 except OSError, err:
1866 # child processes may race, which is harmless
1865 # child processes may race, which is harmless
1867 if err.errno != errno.EEXIST:
1866 if err.errno != errno.EEXIST:
1868 raise
1867 raise
1869 else:
1868 else:
1870 exedir, exename = os.path.split(sys.executable)
1869 exedir, exename = os.path.split(sys.executable)
1871 vlog("# Modifying search path to find %s as %s in '%s'" %
1870 vlog("# Modifying search path to find %s as %s in '%s'" %
1872 (exename, pyexename, exedir))
1871 (exename, pyexename, exedir))
1873 path = os.environ['PATH'].split(os.pathsep)
1872 path = os.environ['PATH'].split(os.pathsep)
1874 while exedir in path:
1873 while exedir in path:
1875 path.remove(exedir)
1874 path.remove(exedir)
1876 os.environ['PATH'] = os.pathsep.join([exedir] + path)
1875 os.environ['PATH'] = os.pathsep.join([exedir] + path)
1877 if not self._findprogram(pyexename):
1876 if not self._findprogram(pyexename):
1878 print "WARNING: Cannot find %s in search path" % pyexename
1877 print "WARNING: Cannot find %s in search path" % pyexename
1879
1878
1880 def _installhg(self):
1879 def _installhg(self):
1881 """Install hg into the test environment.
1880 """Install hg into the test environment.
1882
1881
1883 This will also configure hg with the appropriate testing settings.
1882 This will also configure hg with the appropriate testing settings.
1884 """
1883 """
1885 vlog("# Performing temporary installation of HG")
1884 vlog("# Performing temporary installation of HG")
1886 installerrs = os.path.join("tests", "install.err")
1885 installerrs = os.path.join("tests", "install.err")
1887 compiler = ''
1886 compiler = ''
1888 if self.options.compiler:
1887 if self.options.compiler:
1889 compiler = '--compiler ' + self.options.compiler
1888 compiler = '--compiler ' + self.options.compiler
1890 if self.options.pure:
1889 if self.options.pure:
1891 pure = "--pure"
1890 pure = "--pure"
1892 else:
1891 else:
1893 pure = ""
1892 pure = ""
1894 py3 = ''
1893 py3 = ''
1895 if sys.version_info[0] == 3:
1894 if sys.version_info[0] == 3:
1896 py3 = '--c2to3'
1895 py3 = '--c2to3'
1897
1896
1898 # Run installer in hg root
1897 # Run installer in hg root
1899 script = os.path.realpath(sys.argv[0])
1898 script = os.path.realpath(sys.argv[0])
1900 hgroot = os.path.dirname(os.path.dirname(script))
1899 hgroot = os.path.dirname(os.path.dirname(script))
1901 self._hgroot = hgroot
1900 self._hgroot = hgroot
1902 os.chdir(hgroot)
1901 os.chdir(hgroot)
1903 nohome = '--home=""'
1902 nohome = '--home=""'
1904 if os.name == 'nt':
1903 if os.name == 'nt':
1905 # The --home="" trick works only on OS where os.sep == '/'
1904 # The --home="" trick works only on OS where os.sep == '/'
1906 # because of a distutils convert_path() fast-path. Avoid it at
1905 # because of a distutils convert_path() fast-path. Avoid it at
1907 # least on Windows for now, deal with .pydistutils.cfg bugs
1906 # least on Windows for now, deal with .pydistutils.cfg bugs
1908 # when they happen.
1907 # when they happen.
1909 nohome = ''
1908 nohome = ''
1910 cmd = ('%(exe)s setup.py %(py3)s %(pure)s clean --all'
1909 cmd = ('%(exe)s setup.py %(py3)s %(pure)s clean --all'
1911 ' build %(compiler)s --build-base="%(base)s"'
1910 ' build %(compiler)s --build-base="%(base)s"'
1912 ' install --force --prefix="%(prefix)s"'
1911 ' install --force --prefix="%(prefix)s"'
1913 ' --install-lib="%(libdir)s"'
1912 ' --install-lib="%(libdir)s"'
1914 ' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
1913 ' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
1915 % {'exe': sys.executable, 'py3': py3, 'pure': pure,
1914 % {'exe': sys.executable, 'py3': py3, 'pure': pure,
1916 'compiler': compiler,
1915 'compiler': compiler,
1917 'base': os.path.join(self._hgtmp, "build"),
1916 'base': os.path.join(self._hgtmp, "build"),
1918 'prefix': self._installdir, 'libdir': self._pythondir,
1917 'prefix': self._installdir, 'libdir': self._pythondir,
1919 'bindir': self._bindir,
1918 'bindir': self._bindir,
1920 'nohome': nohome, 'logfile': installerrs})
1919 'nohome': nohome, 'logfile': installerrs})
1921
1920
1922 # setuptools requires install directories to exist.
1921 # setuptools requires install directories to exist.
1923 def makedirs(p):
1922 def makedirs(p):
1924 try:
1923 try:
1925 os.makedirs(p)
1924 os.makedirs(p)
1926 except OSError, e:
1925 except OSError, e:
1927 if e.errno != errno.EEXIST:
1926 if e.errno != errno.EEXIST:
1928 raise
1927 raise
1929 makedirs(self._pythondir)
1928 makedirs(self._pythondir)
1930 makedirs(self._bindir)
1929 makedirs(self._bindir)
1931
1930
1932 vlog("# Running", cmd)
1931 vlog("# Running", cmd)
1933 if os.system(cmd) == 0:
1932 if os.system(cmd) == 0:
1934 if not self.options.verbose:
1933 if not self.options.verbose:
1935 os.remove(installerrs)
1934 os.remove(installerrs)
1936 else:
1935 else:
1937 f = open(installerrs, 'rb')
1936 f = open(installerrs, 'rb')
1938 for line in f:
1937 for line in f:
1939 sys.stdout.write(line)
1938 sys.stdout.write(line)
1940 f.close()
1939 f.close()
1941 sys.exit(1)
1940 sys.exit(1)
1942 os.chdir(self._testdir)
1941 os.chdir(self._testdir)
1943
1942
1944 self._usecorrectpython()
1943 self._usecorrectpython()
1945
1944
1946 if self.options.py3k_warnings and not self.options.anycoverage:
1945 if self.options.py3k_warnings and not self.options.anycoverage:
1947 vlog("# Updating hg command to enable Py3k Warnings switch")
1946 vlog("# Updating hg command to enable Py3k Warnings switch")
1948 f = open(os.path.join(self._bindir, 'hg'), 'rb')
1947 f = open(os.path.join(self._bindir, 'hg'), 'rb')
1949 lines = [line.rstrip() for line in f]
1948 lines = [line.rstrip() for line in f]
1950 lines[0] += ' -3'
1949 lines[0] += ' -3'
1951 f.close()
1950 f.close()
1952 f = open(os.path.join(self._bindir, 'hg'), 'wb')
1951 f = open(os.path.join(self._bindir, 'hg'), 'wb')
1953 for line in lines:
1952 for line in lines:
1954 f.write(line + '\n')
1953 f.write(line + '\n')
1955 f.close()
1954 f.close()
1956
1955
1957 hgbat = os.path.join(self._bindir, 'hg.bat')
1956 hgbat = os.path.join(self._bindir, 'hg.bat')
1958 if os.path.isfile(hgbat):
1957 if os.path.isfile(hgbat):
1959 # hg.bat expects to be put in bin/scripts while run-tests.py
1958 # hg.bat expects to be put in bin/scripts while run-tests.py
1960 # installation layout put it in bin/ directly. Fix it
1959 # installation layout put it in bin/ directly. Fix it
1961 f = open(hgbat, 'rb')
1960 f = open(hgbat, 'rb')
1962 data = f.read()
1961 data = f.read()
1963 f.close()
1962 f.close()
1964 if '"%~dp0..\python" "%~dp0hg" %*' in data:
1963 if '"%~dp0..\python" "%~dp0hg" %*' in data:
1965 data = data.replace('"%~dp0..\python" "%~dp0hg" %*',
1964 data = data.replace('"%~dp0..\python" "%~dp0hg" %*',
1966 '"%~dp0python" "%~dp0hg" %*')
1965 '"%~dp0python" "%~dp0hg" %*')
1967 f = open(hgbat, 'wb')
1966 f = open(hgbat, 'wb')
1968 f.write(data)
1967 f.write(data)
1969 f.close()
1968 f.close()
1970 else:
1969 else:
1971 print 'WARNING: cannot fix hg.bat reference to python.exe'
1970 print 'WARNING: cannot fix hg.bat reference to python.exe'
1972
1971
1973 if self.options.anycoverage:
1972 if self.options.anycoverage:
1974 custom = os.path.join(self._testdir, 'sitecustomize.py')
1973 custom = os.path.join(self._testdir, 'sitecustomize.py')
1975 target = os.path.join(self._pythondir, 'sitecustomize.py')
1974 target = os.path.join(self._pythondir, 'sitecustomize.py')
1976 vlog('# Installing coverage trigger to %s' % target)
1975 vlog('# Installing coverage trigger to %s' % target)
1977 shutil.copyfile(custom, target)
1976 shutil.copyfile(custom, target)
1978 rc = os.path.join(self._testdir, '.coveragerc')
1977 rc = os.path.join(self._testdir, '.coveragerc')
1979 vlog('# Installing coverage rc to %s' % rc)
1978 vlog('# Installing coverage rc to %s' % rc)
1980 os.environ['COVERAGE_PROCESS_START'] = rc
1979 os.environ['COVERAGE_PROCESS_START'] = rc
1981 covdir = os.path.join(self._installdir, '..', 'coverage')
1980 covdir = os.path.join(self._installdir, '..', 'coverage')
1982 try:
1981 try:
1983 os.mkdir(covdir)
1982 os.mkdir(covdir)
1984 except OSError, e:
1983 except OSError, e:
1985 if e.errno != errno.EEXIST:
1984 if e.errno != errno.EEXIST:
1986 raise
1985 raise
1987
1986
1988 os.environ['COVERAGE_DIR'] = covdir
1987 os.environ['COVERAGE_DIR'] = covdir
1989
1988
1990 def _checkhglib(self, verb):
1989 def _checkhglib(self, verb):
1991 """Ensure that the 'mercurial' package imported by python is
1990 """Ensure that the 'mercurial' package imported by python is
1992 the one we expect it to be. If not, print a warning to stderr."""
1991 the one we expect it to be. If not, print a warning to stderr."""
1993 if ((self._bindir == self._pythondir) and
1992 if ((self._bindir == self._pythondir) and
1994 (self._bindir != self._tmpbindir)):
1993 (self._bindir != self._tmpbindir)):
1995 # The pythondir has been inferred from --with-hg flag.
1994 # The pythondir has been inferred from --with-hg flag.
1996 # We cannot expect anything sensible here.
1995 # We cannot expect anything sensible here.
1997 return
1996 return
1998 expecthg = os.path.join(self._pythondir, 'mercurial')
1997 expecthg = os.path.join(self._pythondir, 'mercurial')
1999 actualhg = self._gethgpath()
1998 actualhg = self._gethgpath()
2000 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
1999 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
2001 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
2000 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
2002 ' (expected %s)\n'
2001 ' (expected %s)\n'
2003 % (verb, actualhg, expecthg))
2002 % (verb, actualhg, expecthg))
2004 def _gethgpath(self):
2003 def _gethgpath(self):
2005 """Return the path to the mercurial package that is actually found by
2004 """Return the path to the mercurial package that is actually found by
2006 the current Python interpreter."""
2005 the current Python interpreter."""
2007 if self._hgpath is not None:
2006 if self._hgpath is not None:
2008 return self._hgpath
2007 return self._hgpath
2009
2008
2010 cmd = '%s -c "import mercurial; print (mercurial.__path__[0])"'
2009 cmd = '%s -c "import mercurial; print (mercurial.__path__[0])"'
2011 pipe = os.popen(cmd % PYTHON)
2010 pipe = os.popen(cmd % PYTHON)
2012 try:
2011 try:
2013 self._hgpath = pipe.read().strip()
2012 self._hgpath = pipe.read().strip()
2014 finally:
2013 finally:
2015 pipe.close()
2014 pipe.close()
2016
2015
2017 return self._hgpath
2016 return self._hgpath
2018
2017
2019 def _outputcoverage(self):
2018 def _outputcoverage(self):
2020 """Produce code coverage output."""
2019 """Produce code coverage output."""
2021 from coverage import coverage
2020 from coverage import coverage
2022
2021
2023 vlog('# Producing coverage report')
2022 vlog('# Producing coverage report')
2024 # chdir is the easiest way to get short, relative paths in the
2023 # chdir is the easiest way to get short, relative paths in the
2025 # output.
2024 # output.
2026 os.chdir(self._hgroot)
2025 os.chdir(self._hgroot)
2027 covdir = os.path.join(self._installdir, '..', 'coverage')
2026 covdir = os.path.join(self._installdir, '..', 'coverage')
2028 cov = coverage(data_file=os.path.join(covdir, 'cov'))
2027 cov = coverage(data_file=os.path.join(covdir, 'cov'))
2029
2028
2030 # Map install directory paths back to source directory.
2029 # Map install directory paths back to source directory.
2031 cov.config.paths['srcdir'] = ['.', self._pythondir]
2030 cov.config.paths['srcdir'] = ['.', self._pythondir]
2032
2031
2033 cov.combine()
2032 cov.combine()
2034
2033
2035 omit = [os.path.join(x, '*') for x in [self._bindir, self._testdir]]
2034 omit = [os.path.join(x, '*') for x in [self._bindir, self._testdir]]
2036 cov.report(ignore_errors=True, omit=omit)
2035 cov.report(ignore_errors=True, omit=omit)
2037
2036
2038 if self.options.htmlcov:
2037 if self.options.htmlcov:
2039 htmldir = os.path.join(self._testdir, 'htmlcov')
2038 htmldir = os.path.join(self._testdir, 'htmlcov')
2040 cov.html_report(directory=htmldir, omit=omit)
2039 cov.html_report(directory=htmldir, omit=omit)
2041 if self.options.annotate:
2040 if self.options.annotate:
2042 adir = os.path.join(self._testdir, 'annotated')
2041 adir = os.path.join(self._testdir, 'annotated')
2043 if not os.path.isdir(adir):
2042 if not os.path.isdir(adir):
2044 os.mkdir(adir)
2043 os.mkdir(adir)
2045 cov.annotate(directory=adir, omit=omit)
2044 cov.annotate(directory=adir, omit=omit)
2046
2045
2047 def _findprogram(self, program):
2046 def _findprogram(self, program):
2048 """Search PATH for a executable program"""
2047 """Search PATH for a executable program"""
2049 for p in os.environ.get('PATH', os.defpath).split(os.pathsep):
2048 for p in os.environ.get('PATH', os.defpath).split(os.pathsep):
2050 name = os.path.join(p, program)
2049 name = os.path.join(p, program)
2051 if os.name == 'nt' or os.access(name, os.X_OK):
2050 if os.name == 'nt' or os.access(name, os.X_OK):
2052 return name
2051 return name
2053 return None
2052 return None
2054
2053
2055 def _checktools(self):
2054 def _checktools(self):
2056 """Ensure tools required to run tests are present."""
2055 """Ensure tools required to run tests are present."""
2057 for p in self.REQUIREDTOOLS:
2056 for p in self.REQUIREDTOOLS:
2058 if os.name == 'nt' and not p.endswith('.exe'):
2057 if os.name == 'nt' and not p.endswith('.exe'):
2059 p += '.exe'
2058 p += '.exe'
2060 found = self._findprogram(p)
2059 found = self._findprogram(p)
2061 if found:
2060 if found:
2062 vlog("# Found prerequisite", p, "at", found)
2061 vlog("# Found prerequisite", p, "at", found)
2063 else:
2062 else:
2064 print "WARNING: Did not find prerequisite tool: %s " % p
2063 print "WARNING: Did not find prerequisite tool: %s " % p
2065
2064
2066 if __name__ == '__main__':
2065 if __name__ == '__main__':
2067 runner = TestRunner()
2066 runner = TestRunner()
2068
2067
2069 try:
2068 try:
2070 import msvcrt
2069 import msvcrt
2071 msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
2070 msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
2072 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
2071 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
2073 msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
2072 msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
2074 except ImportError:
2073 except ImportError:
2075 pass
2074 pass
2076
2075
2077 sys.exit(runner.run(sys.argv[1:]))
2076 sys.exit(runner.run(sys.argv[1:]))
General Comments 0
You need to be logged in to leave comments. Login now