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