##// END OF EJS Templates
run-tests.py: use coverage.py with "#!/usr/bin/env python" tests
Alexis S. L. Carvalho -
r4321:99184c6f default
parent child Browse files
Show More
@@ -1,426 +1,432 b''
1 1 #!/usr/bin/env python
2 2 #
3 3 # run-tests.py - Run a set of tests on Mercurial
4 4 #
5 5 # Copyright 2006 Matt Mackall <mpm@selenic.com>
6 6 #
7 7 # This software may be used and distributed according to the terms
8 8 # of the GNU General Public License, incorporated herein by reference.
9 9
10 10 import difflib
11 11 import errno
12 12 import optparse
13 13 import os
14 14 import popen2
15 15 import re
16 16 import shutil
17 17 import signal
18 18 import sys
19 19 import tempfile
20 20 import time
21 21
22 22 required_tools = ["python", "diff", "grep", "unzip", "gunzip", "bunzip2", "sed", "merge"]
23 23
24 24 parser = optparse.OptionParser("%prog [options] [tests]")
25 25 parser.add_option("-v", "--verbose", action="store_true",
26 26 help="output verbose messages")
27 27 parser.add_option("-t", "--timeout", type="int",
28 28 help="kill errant tests after TIMEOUT seconds")
29 29 parser.add_option("-c", "--cover", action="store_true",
30 30 help="print a test coverage report")
31 31 parser.add_option("-s", "--cover_stdlib", action="store_true",
32 32 help="print a test coverage report inc. standard libraries")
33 33 parser.add_option("-C", "--annotate", action="store_true",
34 34 help="output files annotated with coverage")
35 35 parser.add_option("-r", "--retest", action="store_true",
36 36 help="retest failed tests")
37 37 parser.add_option("-f", "--first", action="store_true",
38 38 help="exit on the first test failure")
39 39 parser.add_option("-R", "--restart", action="store_true",
40 40 help="restart at last error")
41 41 parser.add_option("-i", "--interactive", action="store_true",
42 42 help="prompt to accept changed output")
43 43
44 44 parser.set_defaults(timeout=180)
45 45 (options, args) = parser.parse_args()
46 46 verbose = options.verbose
47 47 coverage = options.cover or options.cover_stdlib or options.annotate
48 48 python = sys.executable
49 49
50 50 def vlog(*msg):
51 51 if verbose:
52 52 for m in msg:
53 53 print m,
54 54 print
55 55
56 56 def splitnewlines(text):
57 57 '''like str.splitlines, but only split on newlines.
58 58 keep line endings.'''
59 59 i = 0
60 60 lines = []
61 61 while True:
62 62 n = text.find('\n', i)
63 63 if n == -1:
64 64 last = text[i:]
65 65 if last:
66 66 lines.append(last)
67 67 return lines
68 68 lines.append(text[i:n+1])
69 69 i = n + 1
70 70
71 71 def show_diff(expected, output):
72 72 for line in difflib.unified_diff(expected, output,
73 73 "Expected output", "Test output"):
74 74 sys.stdout.write(line)
75 75
76 76 def find_program(program):
77 77 """Search PATH for a executable program"""
78 78 for p in os.environ.get('PATH', os.defpath).split(os.pathsep):
79 79 name = os.path.join(p, program)
80 80 if os.access(name, os.X_OK):
81 81 return name
82 82 return None
83 83
84 84 def check_required_tools():
85 85 # Before we go any further, check for pre-requisite tools
86 86 # stuff from coreutils (cat, rm, etc) are not tested
87 87 for p in required_tools:
88 88 if os.name == 'nt':
89 89 p += '.exe'
90 90 found = find_program(p)
91 91 if found:
92 92 vlog("# Found prerequisite", p, "at", found)
93 93 else:
94 94 print "WARNING: Did not find prerequisite tool: "+p
95 95
96 96 def cleanup_exit():
97 97 if verbose:
98 98 print "# Cleaning up HGTMP", HGTMP
99 99 shutil.rmtree(HGTMP, True)
100 100
101 101 def use_correct_python():
102 102 # some tests run python interpreter. they must use same
103 103 # interpreter we use or bad things will happen.
104 104 exedir, exename = os.path.split(sys.executable)
105 105 if exename == 'python':
106 106 path = find_program('python')
107 107 if os.path.dirname(path) == exedir:
108 108 return
109 109 vlog('# Making python executable in test path use correct Python')
110 110 my_python = os.path.join(BINDIR, 'python')
111 111 try:
112 112 os.symlink(sys.executable, my_python)
113 113 except AttributeError:
114 114 # windows fallback
115 115 shutil.copyfile(sys.executable, my_python)
116 116 shutil.copymode(sys.executable, my_python)
117 117
118 118 def install_hg():
119 119 global python
120 120 vlog("# Performing temporary installation of HG")
121 121 installerrs = os.path.join("tests", "install.err")
122 122
123 123 os.chdir("..") # Get back to hg root
124 124 cmd = ('%s setup.py clean --all'
125 125 ' install --force --home="%s" --install-lib="%s" >%s 2>&1'
126 126 % (sys.executable, INST, PYTHONDIR, installerrs))
127 127 vlog("# Running", cmd)
128 128 if os.system(cmd) == 0:
129 129 if not verbose:
130 130 os.remove(installerrs)
131 131 else:
132 132 f = open(installerrs)
133 133 for line in f:
134 134 print line,
135 135 f.close()
136 136 sys.exit(1)
137 137 os.chdir(TESTDIR)
138 138
139 139 os.environ["PATH"] = "%s%s%s" % (BINDIR, os.pathsep, os.environ["PATH"])
140 140 os.environ["PYTHONPATH"] = PYTHONDIR
141 141
142 142 use_correct_python()
143 143
144 144 if coverage:
145 145 vlog("# Installing coverage wrapper")
146 146 os.environ['COVERAGE_FILE'] = COVERAGE_FILE
147 147 if os.path.exists(COVERAGE_FILE):
148 148 os.unlink(COVERAGE_FILE)
149 149 # Create a wrapper script to invoke hg via coverage.py
150 150 os.rename(os.path.join(BINDIR, "hg"), os.path.join(BINDIR, "_hg.py"))
151 151 f = open(os.path.join(BINDIR, 'hg'), 'w')
152 152 f.write('#!' + sys.executable + '\n')
153 153 f.write('import sys, os; os.execv(sys.executable, [sys.executable, '+ \
154 154 '"%s", "-x", "%s"] + sys.argv[1:])\n' % (
155 155 os.path.join(TESTDIR, 'coverage.py'),
156 156 os.path.join(BINDIR, '_hg.py')))
157 157 f.close()
158 158 os.chmod(os.path.join(BINDIR, 'hg'), 0700)
159 159 python = '"%s" "%s" -x' % (sys.executable,
160 160 os.path.join(TESTDIR,'coverage.py'))
161 161
162 162 def output_coverage():
163 163 vlog("# Producing coverage report")
164 164 omit = [BINDIR, TESTDIR, PYTHONDIR]
165 165 if not options.cover_stdlib:
166 166 # Exclude as system paths (ignoring empty strings seen on win)
167 167 omit += [x for x in sys.path if x != '']
168 168 omit = ','.join(omit)
169 169 os.chdir(PYTHONDIR)
170 170 cmd = '"%s" "%s" -i -r "--omit=%s"' % (
171 171 sys.executable, os.path.join(TESTDIR, 'coverage.py'), omit)
172 172 vlog("# Running: "+cmd)
173 173 os.system(cmd)
174 174 if options.annotate:
175 175 adir = os.path.join(TESTDIR, 'annotated')
176 176 if not os.path.isdir(adir):
177 177 os.mkdir(adir)
178 178 cmd = '"%s" "%s" -i -a "--directory=%s" "--omit=%s"' % (
179 179 sys.executable, os.path.join(TESTDIR, 'coverage.py'),
180 180 adir, omit)
181 181 vlog("# Running: "+cmd)
182 182 os.system(cmd)
183 183
184 184 class Timeout(Exception):
185 185 pass
186 186
187 187 def alarmed(signum, frame):
188 188 raise Timeout
189 189
190 190 def run(cmd):
191 191 """Run command in a sub-process, capturing the output (stdout and stderr).
192 192 Return the exist code, and output."""
193 193 # TODO: Use subprocess.Popen if we're running on Python 2.4
194 194 if os.name == 'nt':
195 195 tochild, fromchild = os.popen4(cmd)
196 196 tochild.close()
197 197 output = fromchild.read()
198 198 ret = fromchild.close()
199 199 if ret == None:
200 200 ret = 0
201 201 else:
202 202 proc = popen2.Popen4(cmd)
203 203 try:
204 204 output = ''
205 205 proc.tochild.close()
206 206 output = proc.fromchild.read()
207 207 ret = proc.wait()
208 208 except Timeout:
209 209 vlog('# Process %d timed out - killing it' % proc.pid)
210 210 os.kill(proc.pid, signal.SIGTERM)
211 211 ret = proc.wait()
212 212 if ret == 0:
213 213 ret = signal.SIGTERM << 8
214 214 return ret, splitnewlines(output)
215 215
216 216 def run_one(test):
217 217 '''tristate output:
218 218 None -> skipped
219 219 True -> passed
220 220 False -> failed'''
221 221
222 222 vlog("# Test", test)
223 223 if not verbose:
224 224 sys.stdout.write('.')
225 225 sys.stdout.flush()
226 226
227 227 # create a fresh hgrc
228 228 hgrc = file(HGRCPATH, 'w+')
229 229 hgrc.close()
230 230
231 231 err = os.path.join(TESTDIR, test+".err")
232 232 ref = os.path.join(TESTDIR, test+".out")
233 233 testpath = os.path.join(TESTDIR, test)
234 234
235 235 if os.path.exists(err):
236 236 os.remove(err) # Remove any previous output files
237 237
238 238 # Make a tmp subdirectory to work in
239 239 tmpd = os.path.join(HGTMP, test)
240 240 os.mkdir(tmpd)
241 241 os.chdir(tmpd)
242 242
243 try:
244 tf = open(testpath)
245 firstline = tf.readline().rstrip()
246 tf.close()
247 except:
248 firstline = ''
243 249 lctest = test.lower()
244 250
245 if lctest.endswith('.py'):
251 if lctest.endswith('.py') or firstline == '#!/usr/bin/env python':
246 252 cmd = '%s "%s"' % (python, testpath)
247 253 elif lctest.endswith('.bat'):
248 254 # do not run batch scripts on non-windows
249 255 if os.name != 'nt':
250 256 print '\nSkipping %s: batch script' % test
251 257 return None
252 258 # To reliably get the error code from batch files on WinXP,
253 259 # the "cmd /c call" prefix is needed. Grrr
254 260 cmd = 'cmd /c call "%s"' % testpath
255 261 else:
256 262 # do not run shell scripts on windows
257 263 if os.name == 'nt':
258 264 print '\nSkipping %s: shell script' % test
259 265 return None
260 266 # do not try to run non-executable programs
261 267 if not os.access(testpath, os.X_OK):
262 268 print '\nSkipping %s: not executable' % test
263 269 return None
264 270 cmd = '"%s"' % testpath
265 271
266 272 if options.timeout > 0:
267 273 signal.alarm(options.timeout)
268 274
269 275 vlog("# Running", cmd)
270 276 ret, out = run(cmd)
271 277 vlog("# Ret was:", ret)
272 278
273 279 if options.timeout > 0:
274 280 signal.alarm(0)
275 281
276 282 diffret = 0
277 283 # If reference output file exists, check test output against it
278 284 if os.path.exists(ref):
279 285 f = open(ref, "r")
280 286 ref_out = splitnewlines(f.read())
281 287 f.close()
282 288 else:
283 289 ref_out = []
284 290 if out != ref_out:
285 291 diffret = 1
286 292 print "\nERROR: %s output changed" % (test)
287 293 show_diff(ref_out, out)
288 294 if ret:
289 295 print "\nERROR: %s failed with error code %d" % (test, ret)
290 296 elif diffret:
291 297 ret = diffret
292 298
293 299 if ret != 0: # Save errors to a file for diagnosis
294 300 f = open(err, "wb")
295 301 for line in out:
296 302 f.write(line)
297 303 f.close()
298 304
299 305 # Kill off any leftover daemon processes
300 306 try:
301 307 fp = file(DAEMON_PIDS)
302 308 for line in fp:
303 309 try:
304 310 pid = int(line)
305 311 except ValueError:
306 312 continue
307 313 try:
308 314 os.kill(pid, 0)
309 315 vlog('# Killing daemon process %d' % pid)
310 316 os.kill(pid, signal.SIGTERM)
311 317 time.sleep(0.25)
312 318 os.kill(pid, 0)
313 319 vlog('# Daemon process %d is stuck - really killing it' % pid)
314 320 os.kill(pid, signal.SIGKILL)
315 321 except OSError, err:
316 322 if err.errno != errno.ESRCH:
317 323 raise
318 324 fp.close()
319 325 os.unlink(DAEMON_PIDS)
320 326 except IOError:
321 327 pass
322 328
323 329 os.chdir(TESTDIR)
324 330 shutil.rmtree(tmpd, True)
325 331 return ret == 0
326 332
327 333
328 334 os.umask(022)
329 335
330 336 check_required_tools()
331 337
332 338 # Reset some environment variables to well-known values so that
333 339 # the tests produce repeatable output.
334 340 os.environ['LANG'] = os.environ['LC_ALL'] = 'C'
335 341 os.environ['TZ'] = 'GMT'
336 342
337 343 os.environ["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"'
338 344 os.environ["HGMERGE"] = sys.executable + ' -c "import sys; sys.exit(0)"'
339 345 os.environ["HGUSER"] = "test"
340 346 os.environ["HGENCODING"] = "ascii"
341 347 os.environ["HGENCODINGMODE"] = "strict"
342 348
343 349 TESTDIR = os.environ["TESTDIR"] = os.getcwd()
344 350 HGTMP = os.environ["HGTMP"] = tempfile.mkdtemp("", "hgtests.")
345 351 DAEMON_PIDS = os.environ["DAEMON_PIDS"] = os.path.join(HGTMP, 'daemon.pids')
346 352 HGRCPATH = os.environ["HGRCPATH"] = os.path.join(HGTMP, '.hgrc')
347 353
348 354 vlog("# Using TESTDIR", TESTDIR)
349 355 vlog("# Using HGTMP", HGTMP)
350 356
351 357 INST = os.path.join(HGTMP, "install")
352 358 BINDIR = os.path.join(INST, "bin")
353 359 PYTHONDIR = os.path.join(INST, "lib", "python")
354 360 COVERAGE_FILE = os.path.join(TESTDIR, ".coverage")
355 361
356 362 try:
357 363 try:
358 364 install_hg()
359 365
360 366 if options.timeout > 0:
361 367 try:
362 368 signal.signal(signal.SIGALRM, alarmed)
363 369 vlog('# Running tests with %d-second timeout' %
364 370 options.timeout)
365 371 except AttributeError:
366 372 print 'WARNING: cannot run tests with timeouts'
367 373 options.timeout = 0
368 374
369 375 tested = 0
370 376 failed = 0
371 377 skipped = 0
372 378
373 379 if len(args) == 0:
374 380 args = os.listdir(".")
375 381 args.sort()
376 382
377 383
378 384 tests = []
379 385 for test in args:
380 386 if (test.startswith("test-") and '~' not in test and
381 387 ('.' not in test or test.endswith('.py') or
382 388 test.endswith('.bat'))):
383 389 tests.append(test)
384 390
385 391 if options.restart:
386 392 orig = list(tests)
387 393 while tests:
388 394 if os.path.exists(tests[0] + ".err"):
389 395 break
390 396 tests.pop(0)
391 397 if not tests:
392 398 print "running all tests"
393 399 tests = orig
394 400
395 401 for test in tests:
396 402 if options.retest and not os.path.exists(test + ".err"):
397 403 skipped += 1
398 404 continue
399 405 ret = run_one(test)
400 406 if ret is None:
401 407 skipped += 1
402 408 elif not ret:
403 409 if options.interactive:
404 410 print "Accept this change? [n] ",
405 411 answer = sys.stdin.readline().strip()
406 412 if answer.lower() in "y yes".split():
407 413 os.rename(test + ".err", test + ".out")
408 414 tested += 1
409 415 continue
410 416 failed += 1
411 417 if options.first:
412 418 break
413 419 tested += 1
414 420
415 421 print "\n# Ran %d tests, %d skipped, %d failed." % (tested, skipped,
416 422 failed)
417 423 if coverage:
418 424 output_coverage()
419 425 except KeyboardInterrupt:
420 426 failed = True
421 427 print "\ninterrupted!"
422 428 finally:
423 429 cleanup_exit()
424 430
425 431 if failed:
426 432 sys.exit(1)
General Comments 0
You need to be logged in to leave comments. Login now