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