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