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