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