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