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