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