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