##// END OF EJS Templates
tests: make coverage run in parallel mode, clean up coverage code
Dirkjan Ochtman -
r8620:b38f275b default
parent child Browse files
Show More
@@ -1,706 +1,709 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, incorporated herein by reference.
8 # GNU General Public License version 2, incorporated herein by reference.
9
9
10 import difflib
10 import difflib
11 import errno
11 import errno
12 import optparse
12 import optparse
13 import os
13 import os
14 import subprocess
14 import subprocess
15 import shutil
15 import shutil
16 import signal
16 import signal
17 import sys
17 import sys
18 import tempfile
18 import tempfile
19 import time
19 import time
20
20
21 closefds = os.name == 'posix'
21 closefds = os.name == 'posix'
22 def Popen4(cmd, bufsize=-1):
22 def Popen4(cmd, bufsize=-1):
23 p = subprocess.Popen(cmd, shell=True, bufsize=bufsize,
23 p = subprocess.Popen(cmd, shell=True, bufsize=bufsize,
24 close_fds=closefds,
24 close_fds=closefds,
25 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
25 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
26 stderr=subprocess.STDOUT)
26 stderr=subprocess.STDOUT)
27 p.fromchild = p.stdout
27 p.fromchild = p.stdout
28 p.tochild = p.stdin
28 p.tochild = p.stdin
29 p.childerr = p.stderr
29 p.childerr = p.stderr
30 return p
30 return p
31
31
32 # reserved exit code to skip test (used by hghave)
32 # reserved exit code to skip test (used by hghave)
33 SKIPPED_STATUS = 80
33 SKIPPED_STATUS = 80
34 SKIPPED_PREFIX = 'skipped: '
34 SKIPPED_PREFIX = 'skipped: '
35 FAILED_PREFIX = 'hghave check failed: '
35 FAILED_PREFIX = 'hghave check failed: '
36 PYTHON = sys.executable
36 PYTHON = sys.executable
37 hgpkg = None
37 hgpkg = None
38
38
39 requiredtools = ["python", "diff", "grep", "unzip", "gunzip", "bunzip2", "sed"]
39 requiredtools = ["python", "diff", "grep", "unzip", "gunzip", "bunzip2", "sed"]
40
40
41 defaults = {
41 defaults = {
42 'jobs': ('HGTEST_JOBS', 1),
42 'jobs': ('HGTEST_JOBS', 1),
43 'timeout': ('HGTEST_TIMEOUT', 180),
43 'timeout': ('HGTEST_TIMEOUT', 180),
44 'port': ('HGTEST_PORT', 20059),
44 'port': ('HGTEST_PORT', 20059),
45 }
45 }
46
46
47 def parseargs():
47 def parseargs():
48 parser = optparse.OptionParser("%prog [options] [tests]")
48 parser = optparse.OptionParser("%prog [options] [tests]")
49 parser.add_option("-C", "--annotate", action="store_true",
49 parser.add_option("-C", "--annotate", action="store_true",
50 help="output files annotated with coverage")
50 help="output files annotated with coverage")
51 parser.add_option("--child", type="int",
51 parser.add_option("--child", type="int",
52 help="run as child process, summary to given fd")
52 help="run as child process, summary to given fd")
53 parser.add_option("-c", "--cover", action="store_true",
53 parser.add_option("-c", "--cover", action="store_true",
54 help="print a test coverage report")
54 help="print a test coverage report")
55 parser.add_option("-f", "--first", action="store_true",
55 parser.add_option("-f", "--first", action="store_true",
56 help="exit on the first test failure")
56 help="exit on the first test failure")
57 parser.add_option("-i", "--interactive", action="store_true",
57 parser.add_option("-i", "--interactive", action="store_true",
58 help="prompt to accept changed output")
58 help="prompt to accept changed output")
59 parser.add_option("-j", "--jobs", type="int",
59 parser.add_option("-j", "--jobs", type="int",
60 help="number of jobs to run in parallel"
60 help="number of jobs to run in parallel"
61 " (default: $%s or %d)" % defaults['jobs'])
61 " (default: $%s or %d)" % defaults['jobs'])
62 parser.add_option("--keep-tmpdir", action="store_true",
62 parser.add_option("--keep-tmpdir", action="store_true",
63 help="keep temporary directory after running tests"
63 help="keep temporary directory after running tests"
64 " (best used with --tmpdir)")
64 " (best used with --tmpdir)")
65 parser.add_option("-R", "--restart", action="store_true",
65 parser.add_option("-R", "--restart", action="store_true",
66 help="restart at last error")
66 help="restart at last error")
67 parser.add_option("-p", "--port", type="int",
67 parser.add_option("-p", "--port", type="int",
68 help="port on which servers should listen"
68 help="port on which servers should listen"
69 " (default: $%s or %d)" % defaults['port'])
69 " (default: $%s or %d)" % defaults['port'])
70 parser.add_option("-r", "--retest", action="store_true",
70 parser.add_option("-r", "--retest", action="store_true",
71 help="retest failed tests")
71 help="retest failed tests")
72 parser.add_option("-s", "--cover_stdlib", action="store_true",
72 parser.add_option("-s", "--cover_stdlib", action="store_true",
73 help="print a test coverage report inc. standard libraries")
73 help="print a test coverage report inc. standard libraries")
74 parser.add_option("-t", "--timeout", type="int",
74 parser.add_option("-t", "--timeout", type="int",
75 help="kill errant tests after TIMEOUT seconds"
75 help="kill errant tests after TIMEOUT seconds"
76 " (default: $%s or %d)" % defaults['timeout'])
76 " (default: $%s or %d)" % defaults['timeout'])
77 parser.add_option("--tmpdir", type="string",
77 parser.add_option("--tmpdir", type="string",
78 help="run tests in the given temporary directory")
78 help="run tests in the given temporary directory")
79 parser.add_option("-v", "--verbose", action="store_true",
79 parser.add_option("-v", "--verbose", action="store_true",
80 help="output verbose messages")
80 help="output verbose messages")
81 parser.add_option("-n", "--nodiff", action="store_true",
81 parser.add_option("-n", "--nodiff", action="store_true",
82 help="skip showing test changes")
82 help="skip showing test changes")
83 parser.add_option("--with-hg", type="string",
83 parser.add_option("--with-hg", type="string",
84 help="test existing install at given location")
84 help="test existing install at given location")
85 parser.add_option("--pure", action="store_true",
85 parser.add_option("--pure", action="store_true",
86 help="use pure Python code instead of C extensions")
86 help="use pure Python code instead of C extensions")
87
87
88 for option, default in defaults.items():
88 for option, default in defaults.items():
89 defaults[option] = int(os.environ.get(*default))
89 defaults[option] = int(os.environ.get(*default))
90 parser.set_defaults(**defaults)
90 parser.set_defaults(**defaults)
91 (options, args) = parser.parse_args()
91 (options, args) = parser.parse_args()
92
92
93 global vlog
93 global vlog
94 options.anycoverage = (options.cover or
94 options.anycoverage = (options.cover or
95 options.cover_stdlib or
95 options.cover_stdlib or
96 options.annotate)
96 options.annotate)
97
97
98 if options.verbose:
98 if options.verbose:
99 def vlog(*msg):
99 def vlog(*msg):
100 for m in msg:
100 for m in msg:
101 print m,
101 print m,
102 print
102 print
103 else:
103 else:
104 vlog = lambda *msg: None
104 vlog = lambda *msg: None
105
105
106 if options.jobs < 1:
106 if options.jobs < 1:
107 print >> sys.stderr, 'ERROR: -j/--jobs must be positive'
107 print >> sys.stderr, 'ERROR: -j/--jobs must be positive'
108 sys.exit(1)
108 sys.exit(1)
109 if options.interactive and options.jobs > 1:
109 if options.interactive and options.jobs > 1:
110 print '(--interactive overrides --jobs)'
110 print '(--interactive overrides --jobs)'
111 options.jobs = 1
111 options.jobs = 1
112
112
113 return (options, args)
113 return (options, args)
114
114
115 def rename(src, dst):
115 def rename(src, dst):
116 """Like os.rename(), trade atomicity and opened files friendliness
116 """Like os.rename(), trade atomicity and opened files friendliness
117 for existing destination support.
117 for existing destination support.
118 """
118 """
119 shutil.copy(src, dst)
119 shutil.copy(src, dst)
120 os.remove(src)
120 os.remove(src)
121
121
122 def splitnewlines(text):
122 def splitnewlines(text):
123 '''like str.splitlines, but only split on newlines.
123 '''like str.splitlines, but only split on newlines.
124 keep line endings.'''
124 keep line endings.'''
125 i = 0
125 i = 0
126 lines = []
126 lines = []
127 while True:
127 while True:
128 n = text.find('\n', i)
128 n = text.find('\n', i)
129 if n == -1:
129 if n == -1:
130 last = text[i:]
130 last = text[i:]
131 if last:
131 if last:
132 lines.append(last)
132 lines.append(last)
133 return lines
133 return lines
134 lines.append(text[i:n+1])
134 lines.append(text[i:n+1])
135 i = n + 1
135 i = n + 1
136
136
137 def parsehghaveoutput(lines):
137 def parsehghaveoutput(lines):
138 '''Parse hghave log lines.
138 '''Parse hghave log lines.
139 Return tuple of lists (missing, failed):
139 Return tuple of lists (missing, failed):
140 * the missing/unknown features
140 * the missing/unknown features
141 * the features for which existence check failed'''
141 * the features for which existence check failed'''
142 missing = []
142 missing = []
143 failed = []
143 failed = []
144 for line in lines:
144 for line in lines:
145 if line.startswith(SKIPPED_PREFIX):
145 if line.startswith(SKIPPED_PREFIX):
146 line = line.splitlines()[0]
146 line = line.splitlines()[0]
147 missing.append(line[len(SKIPPED_PREFIX):])
147 missing.append(line[len(SKIPPED_PREFIX):])
148 elif line.startswith(FAILED_PREFIX):
148 elif line.startswith(FAILED_PREFIX):
149 line = line.splitlines()[0]
149 line = line.splitlines()[0]
150 failed.append(line[len(FAILED_PREFIX):])
150 failed.append(line[len(FAILED_PREFIX):])
151
151
152 return missing, failed
152 return missing, failed
153
153
154 def showdiff(expected, output):
154 def showdiff(expected, output):
155 for line in difflib.unified_diff(expected, output,
155 for line in difflib.unified_diff(expected, output,
156 "Expected output", "Test output"):
156 "Expected output", "Test output"):
157 sys.stdout.write(line)
157 sys.stdout.write(line)
158
158
159 def findprogram(program):
159 def findprogram(program):
160 """Search PATH for a executable program"""
160 """Search PATH for a executable program"""
161 for p in os.environ.get('PATH', os.defpath).split(os.pathsep):
161 for p in os.environ.get('PATH', os.defpath).split(os.pathsep):
162 name = os.path.join(p, program)
162 name = os.path.join(p, program)
163 if os.access(name, os.X_OK):
163 if os.access(name, os.X_OK):
164 return name
164 return name
165 return None
165 return None
166
166
167 def checktools():
167 def checktools():
168 # Before we go any further, check for pre-requisite tools
168 # Before we go any further, check for pre-requisite tools
169 # stuff from coreutils (cat, rm, etc) are not tested
169 # stuff from coreutils (cat, rm, etc) are not tested
170 for p in requiredtools:
170 for p in requiredtools:
171 if os.name == 'nt':
171 if os.name == 'nt':
172 p += '.exe'
172 p += '.exe'
173 found = findprogram(p)
173 found = findprogram(p)
174 if found:
174 if found:
175 vlog("# Found prerequisite", p, "at", found)
175 vlog("# Found prerequisite", p, "at", found)
176 else:
176 else:
177 print "WARNING: Did not find prerequisite tool: "+p
177 print "WARNING: Did not find prerequisite tool: "+p
178
178
179 def cleanup(options):
179 def cleanup(options):
180 if not options.keep_tmpdir:
180 if not options.keep_tmpdir:
181 if options.verbose:
181 if options.verbose:
182 print "# Cleaning up HGTMP", HGTMP
182 print "# Cleaning up HGTMP", HGTMP
183 shutil.rmtree(HGTMP, True)
183 shutil.rmtree(HGTMP, True)
184
184
185 def usecorrectpython():
185 def usecorrectpython():
186 # some tests run python interpreter. they must use same
186 # some tests run python interpreter. they must use same
187 # interpreter we use or bad things will happen.
187 # interpreter we use or bad things will happen.
188 exedir, exename = os.path.split(sys.executable)
188 exedir, exename = os.path.split(sys.executable)
189 if exename == 'python':
189 if exename == 'python':
190 path = findprogram('python')
190 path = findprogram('python')
191 if os.path.dirname(path) == exedir:
191 if os.path.dirname(path) == exedir:
192 return
192 return
193 vlog('# Making python executable in test path use correct Python')
193 vlog('# Making python executable in test path use correct Python')
194 mypython = os.path.join(BINDIR, 'python')
194 mypython = os.path.join(BINDIR, 'python')
195 try:
195 try:
196 os.symlink(sys.executable, mypython)
196 os.symlink(sys.executable, mypython)
197 except AttributeError:
197 except AttributeError:
198 # windows fallback
198 # windows fallback
199 shutil.copyfile(sys.executable, mypython)
199 shutil.copyfile(sys.executable, mypython)
200 shutil.copymode(sys.executable, mypython)
200 shutil.copymode(sys.executable, mypython)
201
201
202 def installhg(options):
202 def installhg(options):
203 global PYTHON
203 global PYTHON
204 vlog("# Performing temporary installation of HG")
204 vlog("# Performing temporary installation of HG")
205 installerrs = os.path.join("tests", "install.err")
205 installerrs = os.path.join("tests", "install.err")
206 pure = options.pure and "--pure" or ""
206 pure = options.pure and "--pure" or ""
207
207
208 # Run installer in hg root
208 # Run installer in hg root
209 os.chdir(os.path.join(os.path.dirname(sys.argv[0]), '..'))
209 os.chdir(os.path.join(os.path.dirname(sys.argv[0]), '..'))
210 cmd = ('%s setup.py %s clean --all'
210 cmd = ('%s setup.py %s clean --all'
211 ' install --force --prefix="%s" --install-lib="%s"'
211 ' install --force --prefix="%s" --install-lib="%s"'
212 ' --install-scripts="%s" >%s 2>&1'
212 ' --install-scripts="%s" >%s 2>&1'
213 % (sys.executable, pure, INST, PYTHONDIR, BINDIR, installerrs))
213 % (sys.executable, pure, INST, PYTHONDIR, BINDIR, installerrs))
214 vlog("# Running", cmd)
214 vlog("# Running", cmd)
215 if os.system(cmd) == 0:
215 if os.system(cmd) == 0:
216 if not options.verbose:
216 if not options.verbose:
217 os.remove(installerrs)
217 os.remove(installerrs)
218 else:
218 else:
219 f = open(installerrs)
219 f = open(installerrs)
220 for line in f:
220 for line in f:
221 print line,
221 print line,
222 f.close()
222 f.close()
223 sys.exit(1)
223 sys.exit(1)
224 os.chdir(TESTDIR)
224 os.chdir(TESTDIR)
225
225
226 os.environ["PATH"] = "%s%s%s" % (BINDIR, os.pathsep, os.environ["PATH"])
226 os.environ["PATH"] = "%s%s%s" % (BINDIR, os.pathsep, os.environ["PATH"])
227
227
228 pydir = os.pathsep.join([PYTHONDIR, TESTDIR])
228 pydir = os.pathsep.join([PYTHONDIR, TESTDIR])
229 pythonpath = os.environ.get("PYTHONPATH")
229 pythonpath = os.environ.get("PYTHONPATH")
230 if pythonpath:
230 if pythonpath:
231 pythonpath = pydir + os.pathsep + pythonpath
231 pythonpath = pydir + os.pathsep + pythonpath
232 else:
232 else:
233 pythonpath = pydir
233 pythonpath = pydir
234 os.environ["PYTHONPATH"] = pythonpath
234 os.environ["PYTHONPATH"] = pythonpath
235
235
236 usecorrectpython()
236 usecorrectpython()
237 global hgpkg
237 global hgpkg
238 hgpkg = _hgpath()
238 hgpkg = _hgpath()
239
239
240 vlog("# Installing dummy diffstat")
240 vlog("# Installing dummy diffstat")
241 f = open(os.path.join(BINDIR, 'diffstat'), 'w')
241 f = open(os.path.join(BINDIR, 'diffstat'), 'w')
242 f.write('#!' + sys.executable + '\n'
242 f.write('#!' + sys.executable + '\n'
243 'import sys\n'
243 'import sys\n'
244 'files = 0\n'
244 'files = 0\n'
245 'for line in sys.stdin:\n'
245 'for line in sys.stdin:\n'
246 ' if line.startswith("diff "):\n'
246 ' if line.startswith("diff "):\n'
247 ' files += 1\n'
247 ' files += 1\n'
248 'sys.stdout.write("files patched: %d\\n" % files)\n')
248 'sys.stdout.write("files patched: %d\\n" % files)\n')
249 f.close()
249 f.close()
250 os.chmod(os.path.join(BINDIR, 'diffstat'), 0700)
250 os.chmod(os.path.join(BINDIR, 'diffstat'), 0700)
251
251
252 if options.anycoverage:
252 if options.anycoverage:
253 vlog("# Installing coverage wrapper")
253 vlog("# Installing coverage wrapper")
254 os.environ['COVERAGE_FILE'] = COVERAGE_FILE
254 os.environ['COVERAGE_FILE'] = COVERAGE_FILE
255 if os.path.exists(COVERAGE_FILE):
255 if os.path.exists(COVERAGE_FILE):
256 os.unlink(COVERAGE_FILE)
256 os.unlink(COVERAGE_FILE)
257 # Create a wrapper script to invoke hg via coverage.py
257 # Create a wrapper script to invoke hg via coverage.py
258 os.rename(os.path.join(BINDIR, "hg"), os.path.join(BINDIR, "_hg.py"))
258 os.rename(os.path.join(BINDIR, "hg"), os.path.join(BINDIR, "_hg.py"))
259 f = open(os.path.join(BINDIR, 'hg'), 'w')
259 f = open(os.path.join(BINDIR, 'hg'), 'w')
260 f.write('#!' + sys.executable + '\n')
260 f.write('#!' + sys.executable + '\n')
261 f.write('import sys, os; os.execv(sys.executable, [sys.executable, '
261 f.write('import sys, os; os.execv(sys.executable, [sys.executable, '
262 '"%s", "-x", "%s"] + sys.argv[1:])\n' %
262 '"%s", "-x", "-p", "%s"] + sys.argv[1:])\n' %
263 (os.path.join(TESTDIR, 'coverage.py'),
263 (os.path.join(TESTDIR, 'coverage.py'),
264 os.path.join(BINDIR, '_hg.py')))
264 os.path.join(BINDIR, '_hg.py')))
265 f.close()
265 f.close()
266 os.chmod(os.path.join(BINDIR, 'hg'), 0700)
266 os.chmod(os.path.join(BINDIR, 'hg'), 0700)
267 PYTHON = '"%s" "%s" -x' % (sys.executable,
267 PYTHON = '"%s" "%s" -x -p' % (sys.executable,
268 os.path.join(TESTDIR,'coverage.py'))
268 os.path.join(TESTDIR, 'coverage.py'))
269
269
270 def _hgpath():
270 def _hgpath():
271 cmd = '%s -c "import mercurial; print mercurial.__path__[0]"'
271 cmd = '%s -c "import mercurial; print mercurial.__path__[0]"'
272 hgpath = os.popen(cmd % PYTHON)
272 hgpath = os.popen(cmd % PYTHON)
273 path = hgpath.read().strip()
273 path = hgpath.read().strip()
274 hgpath.close()
274 hgpath.close()
275 return path
275 return path
276
276
277 def outputcoverage(options):
277 def outputcoverage(options):
278 vlog("# Producing coverage report")
278
279 vlog('# Producing coverage report')
280 os.chdir(PYTHONDIR)
281
282 def covrun(*args):
283 start = sys.executable, os.path.join(TESTDIR, 'coverage.py')
284 cmd = '"%s" "%s" %s' % (start[0], start[1], ' '.join(args))
285 vlog('# Running: %s' % cmd)
286 os.system(cmd)
287
279 omit = [BINDIR, TESTDIR, PYTHONDIR]
288 omit = [BINDIR, TESTDIR, PYTHONDIR]
280 if not options.cover_stdlib:
289 if not options.cover_stdlib:
281 # Exclude as system paths (ignoring empty strings seen on win)
290 # Exclude as system paths (ignoring empty strings seen on win)
282 omit += [x for x in sys.path if x != '']
291 omit += [x for x in sys.path if x != '']
283 omit = ','.join(omit)
292 omit = ','.join(omit)
284 os.chdir(PYTHONDIR)
293
285 cmd = '"%s" "%s" -i -r "--omit=%s"' % (
294 covrun('-c') # combine from parallel processes
286 sys.executable, os.path.join(TESTDIR, 'coverage.py'), omit)
295 covrun('-i', '-r', '"--omit=%s"' % omit) # report
287 vlog("# Running: "+cmd)
288 os.system(cmd)
289 if options.annotate:
296 if options.annotate:
290 adir = os.path.join(TESTDIR, 'annotated')
297 adir = os.path.join(TESTDIR, 'annotated')
291 if not os.path.isdir(adir):
298 if not os.path.isdir(adir):
292 os.mkdir(adir)
299 os.mkdir(adir)
293 cmd = '"%s" "%s" -i -a "--directory=%s" "--omit=%s"' % (
300 covrun('-i', '-a', '"--directory=%s"' % adir, '"--omit=%s"' % omit)
294 sys.executable, os.path.join(TESTDIR, 'coverage.py'),
295 adir, omit)
296 vlog("# Running: "+cmd)
297 os.system(cmd)
298
301
299 class Timeout(Exception):
302 class Timeout(Exception):
300 pass
303 pass
301
304
302 def alarmed(signum, frame):
305 def alarmed(signum, frame):
303 raise Timeout
306 raise Timeout
304
307
305 def run(cmd, options):
308 def run(cmd, options):
306 """Run command in a sub-process, capturing the output (stdout and stderr).
309 """Run command in a sub-process, capturing the output (stdout and stderr).
307 Return the exist code, and output."""
310 Return the exist code, and output."""
308 # TODO: Use subprocess.Popen if we're running on Python 2.4
311 # TODO: Use subprocess.Popen if we're running on Python 2.4
309 if os.name == 'nt' or sys.platform.startswith('java'):
312 if os.name == 'nt' or sys.platform.startswith('java'):
310 tochild, fromchild = os.popen4(cmd)
313 tochild, fromchild = os.popen4(cmd)
311 tochild.close()
314 tochild.close()
312 output = fromchild.read()
315 output = fromchild.read()
313 ret = fromchild.close()
316 ret = fromchild.close()
314 if ret == None:
317 if ret == None:
315 ret = 0
318 ret = 0
316 else:
319 else:
317 proc = Popen4(cmd)
320 proc = Popen4(cmd)
318 try:
321 try:
319 output = ''
322 output = ''
320 proc.tochild.close()
323 proc.tochild.close()
321 output = proc.fromchild.read()
324 output = proc.fromchild.read()
322 ret = proc.wait()
325 ret = proc.wait()
323 if os.WIFEXITED(ret):
326 if os.WIFEXITED(ret):
324 ret = os.WEXITSTATUS(ret)
327 ret = os.WEXITSTATUS(ret)
325 except Timeout:
328 except Timeout:
326 vlog('# Process %d timed out - killing it' % proc.pid)
329 vlog('# Process %d timed out - killing it' % proc.pid)
327 os.kill(proc.pid, signal.SIGTERM)
330 os.kill(proc.pid, signal.SIGTERM)
328 ret = proc.wait()
331 ret = proc.wait()
329 if ret == 0:
332 if ret == 0:
330 ret = signal.SIGTERM << 8
333 ret = signal.SIGTERM << 8
331 output += ("\n### Abort: timeout after %d seconds.\n"
334 output += ("\n### Abort: timeout after %d seconds.\n"
332 % options.timeout)
335 % options.timeout)
333 return ret, splitnewlines(output)
336 return ret, splitnewlines(output)
334
337
335 def runone(options, test, skips, fails):
338 def runone(options, test, skips, fails):
336 '''tristate output:
339 '''tristate output:
337 None -> skipped
340 None -> skipped
338 True -> passed
341 True -> passed
339 False -> failed'''
342 False -> failed'''
340
343
341 def skip(msg):
344 def skip(msg):
342 if not options.verbose:
345 if not options.verbose:
343 skips.append((test, msg))
346 skips.append((test, msg))
344 else:
347 else:
345 print "\nSkipping %s: %s" % (test, msg)
348 print "\nSkipping %s: %s" % (test, msg)
346 return None
349 return None
347
350
348 def fail(msg):
351 def fail(msg):
349 fails.append((test, msg))
352 fails.append((test, msg))
350 if not options.nodiff:
353 if not options.nodiff:
351 print "\nERROR: %s %s" % (test, msg)
354 print "\nERROR: %s %s" % (test, msg)
352 return None
355 return None
353
356
354 vlog("# Test", test)
357 vlog("# Test", test)
355
358
356 # create a fresh hgrc
359 # create a fresh hgrc
357 hgrc = file(HGRCPATH, 'w+')
360 hgrc = file(HGRCPATH, 'w+')
358 hgrc.write('[ui]\n')
361 hgrc.write('[ui]\n')
359 hgrc.write('slash = True\n')
362 hgrc.write('slash = True\n')
360 hgrc.write('[defaults]\n')
363 hgrc.write('[defaults]\n')
361 hgrc.write('backout = -d "0 0"\n')
364 hgrc.write('backout = -d "0 0"\n')
362 hgrc.write('commit = -d "0 0"\n')
365 hgrc.write('commit = -d "0 0"\n')
363 hgrc.write('tag = -d "0 0"\n')
366 hgrc.write('tag = -d "0 0"\n')
364 hgrc.close()
367 hgrc.close()
365
368
366 err = os.path.join(TESTDIR, test+".err")
369 err = os.path.join(TESTDIR, test+".err")
367 ref = os.path.join(TESTDIR, test+".out")
370 ref = os.path.join(TESTDIR, test+".out")
368 testpath = os.path.join(TESTDIR, test)
371 testpath = os.path.join(TESTDIR, test)
369
372
370 if os.path.exists(err):
373 if os.path.exists(err):
371 os.remove(err) # Remove any previous output files
374 os.remove(err) # Remove any previous output files
372
375
373 # Make a tmp subdirectory to work in
376 # Make a tmp subdirectory to work in
374 tmpd = os.path.join(HGTMP, test)
377 tmpd = os.path.join(HGTMP, test)
375 os.mkdir(tmpd)
378 os.mkdir(tmpd)
376 os.chdir(tmpd)
379 os.chdir(tmpd)
377
380
378 try:
381 try:
379 tf = open(testpath)
382 tf = open(testpath)
380 firstline = tf.readline().rstrip()
383 firstline = tf.readline().rstrip()
381 tf.close()
384 tf.close()
382 except:
385 except:
383 firstline = ''
386 firstline = ''
384 lctest = test.lower()
387 lctest = test.lower()
385
388
386 if lctest.endswith('.py') or firstline == '#!/usr/bin/env python':
389 if lctest.endswith('.py') or firstline == '#!/usr/bin/env python':
387 cmd = '%s "%s"' % (PYTHON, testpath)
390 cmd = '%s "%s"' % (PYTHON, testpath)
388 elif lctest.endswith('.bat'):
391 elif lctest.endswith('.bat'):
389 # do not run batch scripts on non-windows
392 # do not run batch scripts on non-windows
390 if os.name != 'nt':
393 if os.name != 'nt':
391 return skip("batch script")
394 return skip("batch script")
392 # To reliably get the error code from batch files on WinXP,
395 # To reliably get the error code from batch files on WinXP,
393 # the "cmd /c call" prefix is needed. Grrr
396 # the "cmd /c call" prefix is needed. Grrr
394 cmd = 'cmd /c call "%s"' % testpath
397 cmd = 'cmd /c call "%s"' % testpath
395 else:
398 else:
396 # do not run shell scripts on windows
399 # do not run shell scripts on windows
397 if os.name == 'nt':
400 if os.name == 'nt':
398 return skip("shell script")
401 return skip("shell script")
399 # do not try to run non-executable programs
402 # do not try to run non-executable programs
400 if not os.path.exists(testpath):
403 if not os.path.exists(testpath):
401 return fail("does not exist")
404 return fail("does not exist")
402 elif not os.access(testpath, os.X_OK):
405 elif not os.access(testpath, os.X_OK):
403 return skip("not executable")
406 return skip("not executable")
404 cmd = '"%s"' % testpath
407 cmd = '"%s"' % testpath
405
408
406 if options.timeout > 0:
409 if options.timeout > 0:
407 signal.alarm(options.timeout)
410 signal.alarm(options.timeout)
408
411
409 vlog("# Running", cmd)
412 vlog("# Running", cmd)
410 ret, out = run(cmd, options)
413 ret, out = run(cmd, options)
411 vlog("# Ret was:", ret)
414 vlog("# Ret was:", ret)
412
415
413 if options.timeout > 0:
416 if options.timeout > 0:
414 signal.alarm(0)
417 signal.alarm(0)
415
418
416 mark = '.'
419 mark = '.'
417
420
418 skipped = (ret == SKIPPED_STATUS)
421 skipped = (ret == SKIPPED_STATUS)
419 # If reference output file exists, check test output against it
422 # If reference output file exists, check test output against it
420 if os.path.exists(ref):
423 if os.path.exists(ref):
421 f = open(ref, "r")
424 f = open(ref, "r")
422 refout = splitnewlines(f.read())
425 refout = splitnewlines(f.read())
423 f.close()
426 f.close()
424 else:
427 else:
425 refout = []
428 refout = []
426 if skipped:
429 if skipped:
427 mark = 's'
430 mark = 's'
428 missing, failed = parsehghaveoutput(out)
431 missing, failed = parsehghaveoutput(out)
429 if not missing:
432 if not missing:
430 missing = ['irrelevant']
433 missing = ['irrelevant']
431 if failed:
434 if failed:
432 fail("hghave failed checking for %s" % failed[-1])
435 fail("hghave failed checking for %s" % failed[-1])
433 skipped = False
436 skipped = False
434 else:
437 else:
435 skip(missing[-1])
438 skip(missing[-1])
436 elif out != refout:
439 elif out != refout:
437 mark = '!'
440 mark = '!'
438 if ret:
441 if ret:
439 fail("output changed and returned error code %d" % ret)
442 fail("output changed and returned error code %d" % ret)
440 else:
443 else:
441 fail("output changed")
444 fail("output changed")
442 if not options.nodiff:
445 if not options.nodiff:
443 showdiff(refout, out)
446 showdiff(refout, out)
444 ret = 1
447 ret = 1
445 elif ret:
448 elif ret:
446 mark = '!'
449 mark = '!'
447 fail("returned error code %d" % ret)
450 fail("returned error code %d" % ret)
448
451
449 if not options.verbose:
452 if not options.verbose:
450 sys.stdout.write(mark)
453 sys.stdout.write(mark)
451 sys.stdout.flush()
454 sys.stdout.flush()
452
455
453 if ret != 0 and not skipped:
456 if ret != 0 and not skipped:
454 # Save errors to a file for diagnosis
457 # Save errors to a file for diagnosis
455 f = open(err, "wb")
458 f = open(err, "wb")
456 for line in out:
459 for line in out:
457 f.write(line)
460 f.write(line)
458 f.close()
461 f.close()
459
462
460 # Kill off any leftover daemon processes
463 # Kill off any leftover daemon processes
461 try:
464 try:
462 fp = file(DAEMON_PIDS)
465 fp = file(DAEMON_PIDS)
463 for line in fp:
466 for line in fp:
464 try:
467 try:
465 pid = int(line)
468 pid = int(line)
466 except ValueError:
469 except ValueError:
467 continue
470 continue
468 try:
471 try:
469 os.kill(pid, 0)
472 os.kill(pid, 0)
470 vlog('# Killing daemon process %d' % pid)
473 vlog('# Killing daemon process %d' % pid)
471 os.kill(pid, signal.SIGTERM)
474 os.kill(pid, signal.SIGTERM)
472 time.sleep(0.25)
475 time.sleep(0.25)
473 os.kill(pid, 0)
476 os.kill(pid, 0)
474 vlog('# Daemon process %d is stuck - really killing it' % pid)
477 vlog('# Daemon process %d is stuck - really killing it' % pid)
475 os.kill(pid, signal.SIGKILL)
478 os.kill(pid, signal.SIGKILL)
476 except OSError, err:
479 except OSError, err:
477 if err.errno != errno.ESRCH:
480 if err.errno != errno.ESRCH:
478 raise
481 raise
479 fp.close()
482 fp.close()
480 os.unlink(DAEMON_PIDS)
483 os.unlink(DAEMON_PIDS)
481 except IOError:
484 except IOError:
482 pass
485 pass
483
486
484 os.chdir(TESTDIR)
487 os.chdir(TESTDIR)
485 if not options.keep_tmpdir:
488 if not options.keep_tmpdir:
486 shutil.rmtree(tmpd, True)
489 shutil.rmtree(tmpd, True)
487 if skipped:
490 if skipped:
488 return None
491 return None
489 return ret == 0
492 return ret == 0
490
493
491 def runchildren(options, expecthg, tests):
494 def runchildren(options, expecthg, tests):
492 if not options.with_hg:
495 if not options.with_hg:
493 installhg(options)
496 installhg(options)
494 if hgpkg != expecthg:
497 if hgpkg != expecthg:
495 print '# Testing unexpected mercurial: %s' % hgpkg
498 print '# Testing unexpected mercurial: %s' % hgpkg
496
499
497 optcopy = dict(options.__dict__)
500 optcopy = dict(options.__dict__)
498 optcopy['jobs'] = 1
501 optcopy['jobs'] = 1
499 optcopy['with_hg'] = INST
502 optcopy['with_hg'] = INST
500 opts = []
503 opts = []
501 for opt, value in optcopy.iteritems():
504 for opt, value in optcopy.iteritems():
502 name = '--' + opt.replace('_', '-')
505 name = '--' + opt.replace('_', '-')
503 if value is True:
506 if value is True:
504 opts.append(name)
507 opts.append(name)
505 elif value is not None:
508 elif value is not None:
506 opts.append(name + '=' + str(value))
509 opts.append(name + '=' + str(value))
507
510
508 tests.reverse()
511 tests.reverse()
509 jobs = [[] for j in xrange(options.jobs)]
512 jobs = [[] for j in xrange(options.jobs)]
510 while tests:
513 while tests:
511 for job in jobs:
514 for job in jobs:
512 if not tests: break
515 if not tests: break
513 job.append(tests.pop())
516 job.append(tests.pop())
514 fps = {}
517 fps = {}
515 for j, job in enumerate(jobs):
518 for j, job in enumerate(jobs):
516 if not job:
519 if not job:
517 continue
520 continue
518 rfd, wfd = os.pipe()
521 rfd, wfd = os.pipe()
519 childopts = ['--child=%d' % wfd, '--port=%d' % (options.port + j * 3)]
522 childopts = ['--child=%d' % wfd, '--port=%d' % (options.port + j * 3)]
520 cmdline = [PYTHON, sys.argv[0]] + opts + childopts + job
523 cmdline = [PYTHON, sys.argv[0]] + opts + childopts + job
521 vlog(' '.join(cmdline))
524 vlog(' '.join(cmdline))
522 fps[os.spawnvp(os.P_NOWAIT, cmdline[0], cmdline)] = os.fdopen(rfd, 'r')
525 fps[os.spawnvp(os.P_NOWAIT, cmdline[0], cmdline)] = os.fdopen(rfd, 'r')
523 os.close(wfd)
526 os.close(wfd)
524 failures = 0
527 failures = 0
525 tested, skipped, failed = 0, 0, 0
528 tested, skipped, failed = 0, 0, 0
526 skips = []
529 skips = []
527 fails = []
530 fails = []
528 while fps:
531 while fps:
529 pid, status = os.wait()
532 pid, status = os.wait()
530 fp = fps.pop(pid)
533 fp = fps.pop(pid)
531 l = fp.read().splitlines()
534 l = fp.read().splitlines()
532 test, skip, fail = map(int, l[:3])
535 test, skip, fail = map(int, l[:3])
533 split = -fail or len(l)
536 split = -fail or len(l)
534 for s in l[3:split]:
537 for s in l[3:split]:
535 skips.append(s.split(" ", 1))
538 skips.append(s.split(" ", 1))
536 for s in l[split:]:
539 for s in l[split:]:
537 fails.append(s.split(" ", 1))
540 fails.append(s.split(" ", 1))
538 tested += test
541 tested += test
539 skipped += skip
542 skipped += skip
540 failed += fail
543 failed += fail
541 vlog('pid %d exited, status %d' % (pid, status))
544 vlog('pid %d exited, status %d' % (pid, status))
542 failures |= status
545 failures |= status
543 print
546 print
544 for s in skips:
547 for s in skips:
545 print "Skipped %s: %s" % (s[0], s[1])
548 print "Skipped %s: %s" % (s[0], s[1])
546 for s in fails:
549 for s in fails:
547 print "Failed %s: %s" % (s[0], s[1])
550 print "Failed %s: %s" % (s[0], s[1])
548
551
549 if hgpkg != expecthg:
552 if hgpkg != expecthg:
550 print '# Tested unexpected mercurial: %s' % hgpkg
553 print '# Tested unexpected mercurial: %s' % hgpkg
551 print "# Ran %d tests, %d skipped, %d failed." % (
554 print "# Ran %d tests, %d skipped, %d failed." % (
552 tested, skipped, failed)
555 tested, skipped, failed)
553 sys.exit(failures != 0)
556 sys.exit(failures != 0)
554
557
555 def runtests(options, expecthg, tests):
558 def runtests(options, expecthg, tests):
556 global DAEMON_PIDS, HGRCPATH
559 global DAEMON_PIDS, HGRCPATH
557 DAEMON_PIDS = os.environ["DAEMON_PIDS"] = os.path.join(HGTMP, 'daemon.pids')
560 DAEMON_PIDS = os.environ["DAEMON_PIDS"] = os.path.join(HGTMP, 'daemon.pids')
558 HGRCPATH = os.environ["HGRCPATH"] = os.path.join(HGTMP, '.hgrc')
561 HGRCPATH = os.environ["HGRCPATH"] = os.path.join(HGTMP, '.hgrc')
559
562
560 try:
563 try:
561 if not options.with_hg:
564 if not options.with_hg:
562 installhg(options)
565 installhg(options)
563
566
564 if hgpkg != expecthg:
567 if hgpkg != expecthg:
565 print '# Testing unexpected mercurial: %s' % hgpkg
568 print '# Testing unexpected mercurial: %s' % hgpkg
566
569
567 if options.timeout > 0:
570 if options.timeout > 0:
568 try:
571 try:
569 signal.signal(signal.SIGALRM, alarmed)
572 signal.signal(signal.SIGALRM, alarmed)
570 vlog('# Running tests with %d-second timeout' %
573 vlog('# Running tests with %d-second timeout' %
571 options.timeout)
574 options.timeout)
572 except AttributeError:
575 except AttributeError:
573 print 'WARNING: cannot run tests with timeouts'
576 print 'WARNING: cannot run tests with timeouts'
574 options.timeout = 0
577 options.timeout = 0
575
578
576 tested = 0
579 tested = 0
577 failed = 0
580 failed = 0
578 skipped = 0
581 skipped = 0
579
582
580 if options.restart:
583 if options.restart:
581 orig = list(tests)
584 orig = list(tests)
582 while tests:
585 while tests:
583 if os.path.exists(tests[0] + ".err"):
586 if os.path.exists(tests[0] + ".err"):
584 break
587 break
585 tests.pop(0)
588 tests.pop(0)
586 if not tests:
589 if not tests:
587 print "running all tests"
590 print "running all tests"
588 tests = orig
591 tests = orig
589
592
590 skips = []
593 skips = []
591 fails = []
594 fails = []
592 for test in tests:
595 for test in tests:
593 if options.retest and not os.path.exists(test + ".err"):
596 if options.retest and not os.path.exists(test + ".err"):
594 skipped += 1
597 skipped += 1
595 continue
598 continue
596 ret = runone(options, test, skips, fails)
599 ret = runone(options, test, skips, fails)
597 if ret is None:
600 if ret is None:
598 skipped += 1
601 skipped += 1
599 elif not ret:
602 elif not ret:
600 if options.interactive:
603 if options.interactive:
601 print "Accept this change? [n] ",
604 print "Accept this change? [n] ",
602 answer = sys.stdin.readline().strip()
605 answer = sys.stdin.readline().strip()
603 if answer.lower() in "y yes".split():
606 if answer.lower() in "y yes".split():
604 rename(test + ".err", test + ".out")
607 rename(test + ".err", test + ".out")
605 tested += 1
608 tested += 1
606 fails.pop()
609 fails.pop()
607 continue
610 continue
608 failed += 1
611 failed += 1
609 if options.first:
612 if options.first:
610 break
613 break
611 tested += 1
614 tested += 1
612
615
613 if options.child:
616 if options.child:
614 fp = os.fdopen(options.child, 'w')
617 fp = os.fdopen(options.child, 'w')
615 fp.write('%d\n%d\n%d\n' % (tested, skipped, failed))
618 fp.write('%d\n%d\n%d\n' % (tested, skipped, failed))
616 for s in skips:
619 for s in skips:
617 fp.write("%s %s\n" % s)
620 fp.write("%s %s\n" % s)
618 for s in fails:
621 for s in fails:
619 fp.write("%s %s\n" % s)
622 fp.write("%s %s\n" % s)
620 fp.close()
623 fp.close()
621 else:
624 else:
622 print
625 print
623 for s in skips:
626 for s in skips:
624 print "Skipped %s: %s" % s
627 print "Skipped %s: %s" % s
625 for s in fails:
628 for s in fails:
626 print "Failed %s: %s" % s
629 print "Failed %s: %s" % s
627 if hgpkg != expecthg:
630 if hgpkg != expecthg:
628 print '# Tested unexpected mercurial: %s' % hgpkg
631 print '# Tested unexpected mercurial: %s' % hgpkg
629 print "# Ran %d tests, %d skipped, %d failed." % (
632 print "# Ran %d tests, %d skipped, %d failed." % (
630 tested, skipped, failed)
633 tested, skipped, failed)
631
634
632 if options.anycoverage:
635 if options.anycoverage:
633 outputcoverage(options)
636 outputcoverage(options)
634 except KeyboardInterrupt:
637 except KeyboardInterrupt:
635 failed = True
638 failed = True
636 print "\ninterrupted!"
639 print "\ninterrupted!"
637
640
638 if failed:
641 if failed:
639 sys.exit(1)
642 sys.exit(1)
640
643
641 def main():
644 def main():
642 (options, args) = parseargs()
645 (options, args) = parseargs()
643 if not options.child:
646 if not options.child:
644 os.umask(022)
647 os.umask(022)
645
648
646 checktools()
649 checktools()
647
650
648 # Reset some environment variables to well-known values so that
651 # Reset some environment variables to well-known values so that
649 # the tests produce repeatable output.
652 # the tests produce repeatable output.
650 os.environ['LANG'] = os.environ['LC_ALL'] = 'C'
653 os.environ['LANG'] = os.environ['LC_ALL'] = 'C'
651 os.environ['TZ'] = 'GMT'
654 os.environ['TZ'] = 'GMT'
652 os.environ["EMAIL"] = "Foo Bar <foo.bar@example.com>"
655 os.environ["EMAIL"] = "Foo Bar <foo.bar@example.com>"
653 os.environ['CDPATH'] = ''
656 os.environ['CDPATH'] = ''
654
657
655 global TESTDIR, HGTMP, INST, BINDIR, PYTHONDIR, COVERAGE_FILE
658 global TESTDIR, HGTMP, INST, BINDIR, PYTHONDIR, COVERAGE_FILE
656 TESTDIR = os.environ["TESTDIR"] = os.getcwd()
659 TESTDIR = os.environ["TESTDIR"] = os.getcwd()
657 HGTMP = os.environ['HGTMP'] = os.path.realpath(tempfile.mkdtemp('', 'hgtests.',
660 HGTMP = os.environ['HGTMP'] = os.path.realpath(tempfile.mkdtemp('', 'hgtests.',
658 options.tmpdir))
661 options.tmpdir))
659 DAEMON_PIDS = None
662 DAEMON_PIDS = None
660 HGRCPATH = None
663 HGRCPATH = None
661
664
662 os.environ["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"'
665 os.environ["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"'
663 os.environ["HGMERGE"] = "internal:merge"
666 os.environ["HGMERGE"] = "internal:merge"
664 os.environ["HGUSER"] = "test"
667 os.environ["HGUSER"] = "test"
665 os.environ["HGENCODING"] = "ascii"
668 os.environ["HGENCODING"] = "ascii"
666 os.environ["HGENCODINGMODE"] = "strict"
669 os.environ["HGENCODINGMODE"] = "strict"
667 os.environ["HGPORT"] = str(options.port)
670 os.environ["HGPORT"] = str(options.port)
668 os.environ["HGPORT1"] = str(options.port + 1)
671 os.environ["HGPORT1"] = str(options.port + 1)
669 os.environ["HGPORT2"] = str(options.port + 2)
672 os.environ["HGPORT2"] = str(options.port + 2)
670
673
671 if options.with_hg:
674 if options.with_hg:
672 INST = options.with_hg
675 INST = options.with_hg
673 else:
676 else:
674 INST = os.path.join(HGTMP, "install")
677 INST = os.path.join(HGTMP, "install")
675 BINDIR = os.environ["BINDIR"] = os.path.join(INST, "bin")
678 BINDIR = os.environ["BINDIR"] = os.path.join(INST, "bin")
676 PYTHONDIR = os.path.join(INST, "lib", "python")
679 PYTHONDIR = os.path.join(INST, "lib", "python")
677 COVERAGE_FILE = os.path.join(TESTDIR, ".coverage")
680 COVERAGE_FILE = os.path.join(TESTDIR, ".coverage")
678
681
679 expecthg = os.path.join(HGTMP, 'install', 'lib', 'python', 'mercurial')
682 expecthg = os.path.join(HGTMP, 'install', 'lib', 'python', 'mercurial')
680
683
681 if len(args) == 0:
684 if len(args) == 0:
682 args = os.listdir(".")
685 args = os.listdir(".")
683 args.sort()
686 args.sort()
684
687
685 tests = []
688 tests = []
686 for test in args:
689 for test in args:
687 if (test.startswith("test-") and '~' not in test and
690 if (test.startswith("test-") and '~' not in test and
688 ('.' not in test or test.endswith('.py') or
691 ('.' not in test or test.endswith('.py') or
689 test.endswith('.bat'))):
692 test.endswith('.bat'))):
690 tests.append(test)
693 tests.append(test)
691 if not tests:
694 if not tests:
692 print "# Ran 0 tests, 0 skipped, 0 failed."
695 print "# Ran 0 tests, 0 skipped, 0 failed."
693 return
696 return
694
697
695 vlog("# Using TESTDIR", TESTDIR)
698 vlog("# Using TESTDIR", TESTDIR)
696 vlog("# Using HGTMP", HGTMP)
699 vlog("# Using HGTMP", HGTMP)
697
700
698 try:
701 try:
699 if len(tests) > 1 and options.jobs > 1:
702 if len(tests) > 1 and options.jobs > 1:
700 runchildren(options, expecthg, tests)
703 runchildren(options, expecthg, tests)
701 else:
704 else:
702 runtests(options, expecthg, tests)
705 runtests(options, expecthg, tests)
703 finally:
706 finally:
704 cleanup(options)
707 cleanup(options)
705
708
706 main()
709 main()
General Comments 0
You need to be logged in to leave comments. Login now