Merge lp:~vila/udd/no-shell-true into lp:udd
- no-shell-true
- Merge into import-scripts
Status: | Merged |
---|---|
Approved by: | Jelmer Vernooij |
Approved revision: | 439 |
Merged at revision: | 405 |
Proposed branch: | lp:~vila/udd/no-shell-true |
Merge into: | lp:udd |
Prerequisite: | lp:~vila/udd/589523-import-timeout |
Diff against target: |
380 lines (+128/-55) 3 files modified
icommon.py (+10/-18) mass_import.py (+18/-7) tests.py (+100/-30) |
To merge this branch: | bzr merge lp:~vila/udd/no-shell-true |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
James Westby | Approve | ||
Review via email: mp+50921@code.launchpad.net |
Commit message
Description of the change
As mentioned in previous submissions, using shell=True with subprocesses led to complications for handling kills.
In preparation for a next submission that will allow more than import_package.py to be invoked, I also want to get rid of shell=True for security concerns.
I also added a test to ensure that we can still capture the output/error of a python subprocess if it catch interrupting signals and handled them.
Note that we can't reliably guarantee what return code we will get when a process is killed and I've stopped cheating in this regard ;)
John A Meinel (jameinel) wrote : | # |
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
On 2/23/2011 10:01 AM, James Westby wrote:
> Review: Approve
> 94 + cmd = 'import time; while True: time.sleep(0.001)'
>
> Should this have a timeout?
>
> Other than that this looks good.
>
> Thanks,
>
> James
>
It intentionally doesn't, because it is testing the kill code. But you
have a point, setting it to:
for i in xrange(10000): time.sleep(0.001)
would give us 10s of a process running, and not leave something forever
if the test fails for some reason.
John
=:->
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (Cygwin)
Comment: Using GnuPG with Mozilla - http://
iEYEARECAAYFAk1
BckAoLeQrQTvbLH
=a/qV
-----END PGP SIGNATURE-----
Vincent Ladeuil (vila) wrote : | # |
> -----BEGIN PGP SIGNED MESSAGE-----
> Hash: SHA1
>
> On 2/23/2011 10:01 AM, James Westby wrote:
> > Review: Approve
> > 94 + cmd = 'import time; while True: time.sleep(0.001)'
> >
> > Should this have a timeout?
> It intentionally doesn't, because it is testing the kill code.
Yup.
> But you have a point, setting it to:
Yup.
There are addCleanup(t.kill) that *should* kill them but...
- 435. By Vincent Ladeuil
-
Merge 589523-
import- timeout into no-shell-true - 436. By Vincent Ladeuil
-
Merge 589523-
import- timeout into no-shell-true - 437. By Vincent Ladeuil
-
Merge 589523-
import- timeout into no-shell-true - 438. By Vincent Ladeuil
-
Make sure the sleepers die in the end (still peaceful, they sleep ;)
- 439. By Vincent Ladeuil
-
Oops, we don't rely on the shell to find executables now and we rely on the traceback being available to categorize failures.
- 440. By Vincent Ladeuil
-
Tests requiring commits are a pita when it comes to typos.
- 441. By Vincent Ladeuil
-
Fix bad parsing of max_threads files, die untested obvious fixes, die !
Preview Diff
1 | === modified file 'icommon.py' | |||
2 | --- icommon.py 2011-02-24 13:53:11 +0000 | |||
3 | +++ icommon.py 2011-02-24 13:53:11 +0000 | |||
4 | @@ -1940,10 +1940,10 @@ | |||
5 | 1940 | 1940 | ||
6 | 1941 | class SubprocessMonitor(CatchingExceptionThread): | 1941 | class SubprocessMonitor(CatchingExceptionThread): |
7 | 1942 | 1942 | ||
9 | 1943 | def __init__(self, cmd, max_duration=None): | 1943 | def __init__(self, args, max_duration=None): |
10 | 1944 | """Create a SubprocessMonitor. | 1944 | """Create a SubprocessMonitor. |
11 | 1945 | 1945 | ||
13 | 1946 | :param cmd: The command string to be run in the subprocess. | 1946 | :param args: A list of arguments to give to python. |
14 | 1947 | 1947 | ||
15 | 1948 | :param max_duration: The max number of seconds the command is expected | 1948 | :param max_duration: The max number of seconds the command is expected |
16 | 1949 | to be run (elapsed time). | 1949 | to be run (elapsed time). |
17 | @@ -1952,26 +1952,20 @@ | |||
18 | 1952 | self.stopped = threading.Event() | 1952 | self.stopped = threading.Event() |
19 | 1953 | super(SubprocessMonitor, self).__init__(target=self.spawn, | 1953 | super(SubprocessMonitor, self).__init__(target=self.spawn, |
20 | 1954 | sync_event=self.started) | 1954 | sync_event=self.started) |
22 | 1955 | self.cmd = cmd | 1955 | self.args = args |
23 | 1956 | self.proc_pid = None | 1956 | self.proc_pid = None |
24 | 1957 | self.retcode = None | 1957 | self.retcode = None |
25 | 1958 | self.out = self.err = None | ||
26 | 1958 | self.started_at = self.ended_at = None | 1959 | self.started_at = self.ended_at = None |
27 | 1959 | self.max_duration = max_duration | 1960 | self.max_duration = max_duration |
28 | 1960 | 1961 | ||
29 | 1961 | def spawn(self): | 1962 | def spawn(self): |
39 | 1962 | """Spawn the command in a subprocess.""" | 1963 | """Spawn the python command in a subprocess.""" |
40 | 1963 | def child_setup(): | 1964 | proc = subprocess.Popen(['python'] + self.args, |
41 | 1964 | # python installs a SIGPIPE handler by default and signal | 1965 | executable=sys.executable, |
33 | 1965 | # dispositions set to SIG_IGN (used in that case) are preserved | ||
34 | 1966 | # across fork and exceve. We don't want to propagate this to | ||
35 | 1967 | # non-python subprocesses. | ||
36 | 1968 | signal.signal(signal.SIGPIPE, signal.SIG_DFL) | ||
37 | 1969 | proc = subprocess.Popen(self.cmd, | ||
38 | 1970 | shell=True, | ||
42 | 1971 | stdout=subprocess.PIPE, | 1966 | stdout=subprocess.PIPE, |
43 | 1972 | stderr=subprocess.PIPE, | 1967 | stderr=subprocess.PIPE, |
46 | 1973 | stdin=subprocess.PIPE, | 1968 | stdin=subprocess.PIPE) |
45 | 1974 | preexec_fn=child_setup) | ||
47 | 1975 | self.proc_pid = proc.pid | 1969 | self.proc_pid = proc.pid |
48 | 1976 | self.started_at = time.time() | 1970 | self.started_at = time.time() |
49 | 1977 | self.switch_and_set(self.stopped) | 1971 | self.switch_and_set(self.stopped) |
50 | @@ -1981,9 +1975,8 @@ | |||
51 | 1981 | self.retcode = proc.returncode | 1975 | self.retcode = proc.returncode |
52 | 1982 | except OSError, e: | 1976 | except OSError, e: |
53 | 1983 | if e.errno in (errno.ECHILD, errno.ESRCH): | 1977 | if e.errno in (errno.ECHILD, errno.ESRCH): |
57 | 1984 | # The process doesn't exist anymore, pretend is has been | 1978 | # The process doesn't exist anymore |
58 | 1985 | # terminated | 1979 | pass |
56 | 1986 | self.retcode = -signal.SIGTERM | ||
59 | 1987 | else: | 1980 | else: |
60 | 1988 | raise | 1981 | raise |
61 | 1989 | self.stopped.set() | 1982 | self.stopped.set() |
62 | @@ -2011,7 +2004,6 @@ | |||
63 | 2011 | if signum is None: | 2004 | if signum is None: |
64 | 2012 | signum = signal.SIGTERM | 2005 | signum = signal.SIGTERM |
65 | 2013 | os.kill(self.proc_pid, signum) | 2006 | os.kill(self.proc_pid, signum) |
66 | 2014 | self.retcode = -signum | ||
67 | 2015 | killed = True | 2007 | killed = True |
68 | 2016 | except OSError, e: | 2008 | except OSError, e: |
69 | 2017 | if e.errno in (errno.ECHILD, errno.ESRCH): | 2009 | if e.errno in (errno.ECHILD, errno.ESRCH): |
70 | 2018 | 2010 | ||
71 | === modified file 'mass_import.py' | |||
72 | --- mass_import.py 2011-02-24 13:53:11 +0000 | |||
73 | +++ mass_import.py 2011-02-24 13:53:11 +0000 | |||
74 | @@ -103,6 +103,9 @@ | |||
75 | 103 | logger.addHandler(progress_handler) | 103 | logger.addHandler(progress_handler) |
76 | 104 | 104 | ||
77 | 105 | logger.info("Starting up") | 105 | logger.info("Starting up") |
78 | 106 | # Some context to ease debug | ||
79 | 107 | logger.info("PATH: %s" % os.environ.get('PATH', None)) | ||
80 | 108 | logger.info("CWD: %s" % os.getcwd()) | ||
81 | 106 | 109 | ||
82 | 107 | 110 | ||
83 | 108 | logger.debug("Getting Launchpad object") | 111 | logger.debug("Getting Launchpad object") |
84 | @@ -115,13 +118,13 @@ | |||
85 | 115 | 118 | ||
86 | 116 | class Importer(icommon.SubprocessMonitor): | 119 | class Importer(icommon.SubprocessMonitor): |
87 | 117 | 120 | ||
89 | 118 | def __init__(self, cmd, package_name, job_id): | 121 | def __init__(self, args, package_name, job_id): |
90 | 119 | # FIXME: We should put the max_duration in a config file when it | 122 | # FIXME: We should put the max_duration in a config file when it |
91 | 120 | # becomes available. Until then, during the wheezy import the longest | 123 | # becomes available. Until then, during the wheezy import the longest |
92 | 121 | # import took more than 2 hours, so 24 is generous enough. On the other | 124 | # import took more than 2 hours, so 24 is generous enough. On the other |
93 | 122 | # hand, we had evidence of imports hanging for weeks (at least a month | 125 | # hand, we had evidence of imports hanging for weeks (at least a month |
94 | 123 | # even in one case, so 24 hours will get rid of them) | 126 | # even in one case, so 24 hours will get rid of them) |
96 | 124 | super(Importer, self).__init__(cmd % (package_name,), | 127 | super(Importer, self).__init__(args, |
97 | 125 | # No more than a day | 128 | # No more than a day |
98 | 126 | max_duration=24*60*60) | 129 | max_duration=24*60*60) |
99 | 127 | self.package_name = package_name | 130 | self.package_name = package_name |
100 | @@ -133,7 +136,9 @@ | |||
101 | 133 | if self.retcode == icommon.no_lock_returncode: | 136 | if self.retcode == icommon.no_lock_returncode: |
102 | 134 | logger.info("Couldn't lock %s, skipping" % self.package_name) | 137 | logger.info("Couldn't lock %s, skipping" % self.package_name) |
103 | 135 | else: | 138 | else: |
105 | 136 | unicode_output = self.out.decode("utf-8", "replace") | 139 | # We mostly care about self.err which contains the traceback here |
106 | 140 | output = self.out + self.err | ||
107 | 141 | unicode_output = output.decode("utf-8", "replace") | ||
108 | 137 | ascii_output = unicode_output.encode("ascii", "replace") | 142 | ascii_output = unicode_output.encode("ascii", "replace") |
109 | 138 | success = self.retcode == 0 | 143 | success = self.retcode == 0 |
110 | 139 | if success: | 144 | if success: |
111 | @@ -174,7 +179,8 @@ | |||
112 | 174 | except Queue.Empty: | 179 | except Queue.Empty: |
113 | 175 | self.unfinished_tasks += 1 | 180 | self.unfinished_tasks += 1 |
114 | 176 | job_id, package_name = self.next_job() | 181 | job_id, package_name = self.next_job() |
116 | 177 | return Importer('import_package.py %s', | 182 | return Importer(['scripts/import_package.py', |
117 | 183 | package_name], | ||
118 | 178 | package_name, job_id) | 184 | package_name, job_id) |
119 | 179 | 185 | ||
120 | 180 | def next_job(self): | 186 | def next_job(self): |
121 | @@ -268,10 +274,13 @@ | |||
122 | 268 | # Then check the graceful stop file | 274 | # Then check the graceful stop file |
123 | 269 | if os.path.exists(icommon.stop_file): | 275 | if os.path.exists(icommon.stop_file): |
124 | 270 | self.driver.queue_closed.set() | 276 | self.driver.queue_closed.set() |
125 | 277 | logger.info('Driver would not process new requests') | ||
126 | 271 | continue | 278 | continue |
127 | 272 | # And the number of threads | 279 | # And the number of threads |
128 | 273 | nb_threads = self.get_max_threads() | 280 | nb_threads = self.get_max_threads() |
129 | 274 | if self.driver.max_threads != nb_threads: | 281 | if self.driver.max_threads != nb_threads: |
130 | 282 | logger.info('Read %d in %s' | ||
131 | 283 | % (nb_threads, icommon.max_threads_file)) | ||
132 | 275 | self.driver.max_threads = nb_threads | 284 | self.driver.max_threads = nb_threads |
133 | 276 | # Catch driver exception if any | 285 | # Catch driver exception if any |
134 | 277 | self.report_driver_exception() | 286 | self.report_driver_exception() |
135 | @@ -286,10 +295,12 @@ | |||
136 | 286 | if os.path.exists(icommon.max_threads_file): | 295 | if os.path.exists(icommon.max_threads_file): |
137 | 287 | f = open(icommon.max_threads_file) | 296 | f = open(icommon.max_threads_file) |
138 | 288 | try: | 297 | try: |
140 | 289 | nb_threads = int(f.readlines()[0].strip()) | 298 | line = f.readline().strip() |
141 | 299 | if line: | ||
142 | 300 | nb_threads = int(line) | ||
143 | 290 | except Exception, e: | 301 | except Exception, e: |
146 | 291 | logger.warning('Error reading max threads file %s: %s', | 302 | logger.warning('Error reading max threads file %s: %s' |
147 | 292 | (icommon.max_threads_file, str(e))) | 303 | % (icommon.max_threads_file, str(e))) |
148 | 293 | finally: | 304 | finally: |
149 | 294 | f.close() | 305 | f.close() |
150 | 295 | return nb_threads | 306 | return nb_threads |
151 | 296 | 307 | ||
152 | === modified file 'tests.py' | |||
153 | --- tests.py 2011-02-24 13:53:11 +0000 | |||
154 | +++ tests.py 2011-02-24 13:53:11 +0000 | |||
155 | @@ -381,50 +381,56 @@ | |||
156 | 381 | 381 | ||
157 | 382 | class TestSubprocessMonitor(tests.TestCase): | 382 | class TestSubprocessMonitor(tests.TestCase): |
158 | 383 | 383 | ||
160 | 384 | def run_in_subprocess(self, cmd, klass=None): | 384 | def run_in_subprocess(self, args, klass=None): |
161 | 385 | if klass is None: | 385 | if klass is None: |
162 | 386 | klass = icommon.SubprocessMonitor | 386 | klass = icommon.SubprocessMonitor |
164 | 387 | sub = klass(cmd) | 387 | sub = klass(args) |
165 | 388 | self.addCleanup(sub.join, 0) | 388 | self.addCleanup(sub.join, 0) |
166 | 389 | sub.start() | 389 | sub.start() |
167 | 390 | return sub | 390 | return sub |
168 | 391 | 391 | ||
169 | 392 | def run_sleeper_in_subprocess(self, klass=None): | ||
170 | 393 | """Run a sleeper in a subprocess. | ||
171 | 394 | |||
172 | 395 | This is used to simulate a never-ending process without consuming too | ||
173 | 396 | much CPU cycles. | ||
174 | 397 | """ | ||
175 | 398 | cmd = 'import time; while True: time.sleep(0.001)' | ||
176 | 399 | return self.run_in_subprocess(['-c', cmd], klass=klass) | ||
177 | 400 | |||
178 | 392 | def test_broken_command(self): | 401 | def test_broken_command(self): |
180 | 393 | sub = self.run_in_subprocess('this-is-not-a-program') | 402 | sub = self.run_in_subprocess(['this-is-not-a-python-script']) |
181 | 394 | sub.stopped.wait() | 403 | sub.stopped.wait() |
182 | 395 | self.assertEquals(True, sub.started.isSet()) | 404 | self.assertEquals(True, sub.started.isSet()) |
185 | 396 | self.assertEquals('/bin/sh: this-is-not-a-program: not found\n', | 405 | self.assertEquals( |
186 | 397 | sub.err) | 406 | "python: can't open file 'this-is-not-a-python-script':" |
187 | 407 | " [Errno 2] No such file or directory\n", | ||
188 | 408 | sub.err) | ||
189 | 398 | 409 | ||
190 | 399 | def test_success(self): | 410 | def test_success(self): |
192 | 400 | sub = self.run_in_subprocess('echo toto') | 411 | sub = self.run_in_subprocess(['-c', 'print "toto"']) |
193 | 401 | sub.stopped.wait() | 412 | sub.stopped.wait() |
194 | 402 | self.assertEquals('toto\n', sub.out) | 413 | self.assertEquals('toto\n', sub.out) |
195 | 403 | self.assertEquals('', sub.err) | 414 | self.assertEquals('', sub.err) |
196 | 404 | self.assertEquals(0, sub.retcode) | 415 | self.assertEquals(0, sub.retcode) |
197 | 405 | 416 | ||
198 | 406 | def test_failure(self): | 417 | def test_failure(self): |
200 | 407 | sub = self.run_in_subprocess('false') | 418 | sub = self.run_in_subprocess(['-c', 'import sys; sys.exit(1)']) |
201 | 408 | sub.stopped.wait() | 419 | sub.stopped.wait() |
202 | 409 | self.assertEquals('', sub.out) | 420 | self.assertEquals('', sub.out) |
203 | 410 | self.assertEquals('', sub.err) | 421 | self.assertEquals('', sub.err) |
204 | 411 | self.assertEquals(1, sub.retcode) | 422 | self.assertEquals(1, sub.retcode) |
205 | 412 | 423 | ||
206 | 413 | def test_kill(self): | 424 | def test_kill(self): |
208 | 414 | sub = self.run_in_subprocess('yes') | 425 | sub = self.run_sleeper_in_subprocess() |
209 | 415 | sub.started.wait() | 426 | sub.started.wait() |
210 | 416 | self.assertEquals(True, sub.kill()) | 427 | self.assertEquals(True, sub.kill()) |
211 | 417 | sub.stopped.wait() | 428 | sub.stopped.wait() |
212 | 418 | self.assertEquals(-signal.SIGTERM, sub.retcode) | 429 | self.assertEquals(-signal.SIGTERM, sub.retcode) |
213 | 419 | 430 | ||
214 | 420 | def test_kill_SIGKILL(self): | 431 | def test_kill_SIGKILL(self): |
216 | 421 | sub = self.run_in_subprocess('yes') | 432 | sub = self.run_sleeper_in_subprocess() |
217 | 422 | sub.started.wait() | 433 | sub.started.wait() |
218 | 423 | # For oscure reasons, using SIGABRT instead of SIGKILL and stopping | ||
219 | 424 | # below left the 'yes' process alive and the SubprocessMonitor happily | ||
220 | 425 | # consuming memroy (8GB mark reached while sitting under pdb, which is | ||
221 | 426 | # good enough to consider bug #589532 addressed. | ||
222 | 427 | # import pdb; pdb.set_trace() | ||
223 | 428 | self.assertEquals(True, sub.kill(signal.SIGKILL)) | 434 | self.assertEquals(True, sub.kill(signal.SIGKILL)) |
224 | 429 | sub.stopped.wait() | 435 | sub.stopped.wait() |
225 | 430 | self.assertEquals(-signal.SIGKILL, sub.retcode) | 436 | self.assertEquals(-signal.SIGKILL, sub.retcode) |
226 | @@ -432,17 +438,17 @@ | |||
227 | 432 | def test_kill_catch_zombie(self): | 438 | def test_kill_catch_zombie(self): |
228 | 433 | kill_me = threading.Event() | 439 | kill_me = threading.Event() |
229 | 434 | resume = threading.Event() | 440 | resume = threading.Event() |
231 | 435 | class TestSubprocessMonitor(icommon.SubprocessMonitor): | 441 | class TestMonitor(icommon.SubprocessMonitor): |
232 | 436 | 442 | ||
233 | 437 | def switch_and_set(self, new): | 443 | def switch_and_set(self, new): |
235 | 438 | super(TestSubprocessMonitor, self).switch_and_set(new) | 444 | super(TestMonitor, self).switch_and_set(new) |
236 | 439 | if new is self.stopped: | 445 | if new is self.stopped: |
237 | 440 | # The process is running but we haven't called | 446 | # The process is running but we haven't called |
238 | 441 | # proc.communicate yet. | 447 | # proc.communicate yet. |
239 | 442 | kill_me.set() | 448 | kill_me.set() |
240 | 443 | resume.wait() | 449 | resume.wait() |
241 | 444 | 450 | ||
243 | 445 | sub = self.run_in_subprocess('yes', TestSubprocessMonitor) | 451 | sub = self.run_sleeper_in_subprocess(klass=TestMonitor) |
244 | 446 | self.addCleanup(sub.join, 0) | 452 | self.addCleanup(sub.join, 0) |
245 | 447 | sub.started.wait() | 453 | sub.started.wait() |
246 | 448 | kill_me.wait() | 454 | kill_me.wait() |
247 | @@ -450,25 +456,90 @@ | |||
248 | 450 | os.kill(sub.proc_pid, signal.SIGTERM) | 456 | os.kill(sub.proc_pid, signal.SIGTERM) |
249 | 451 | pid, st = os.waitpid(sub.proc_pid, 0) | 457 | pid, st = os.waitpid(sub.proc_pid, 0) |
250 | 452 | self.assertEquals(sub.proc_pid, pid) | 458 | self.assertEquals(sub.proc_pid, pid) |
251 | 459 | # Only the low byte contains the signal and the high bit if for core | ||
252 | 460 | # dumps. | ||
253 | 461 | self.assertEquals(signal.SIGTERM, st & 0x7F) | ||
254 | 453 | # Release the thread so it can call proc.communicate | 462 | # Release the thread so it can call proc.communicate |
255 | 454 | resume.set() | 463 | resume.set() |
256 | 455 | # Now pretend we don't know the subprocess is dead | 464 | # Now pretend we don't know the subprocess is dead |
257 | 456 | self.assertEquals(False, sub.kill()) | 465 | self.assertEquals(False, sub.kill()) |
258 | 457 | sub.join() | 466 | sub.join() |
261 | 458 | self.assertEquals(-signal.SIGTERM, sub.retcode) | 467 | # No useful retcode available |
262 | 459 | 468 | self.assertEquals(None, sub.retcode) | |
263 | 469 | # Nothing either in stdout/stderr. This means that subprocesses should | ||
264 | 470 | # handled SIGTERM to allow us to collect their data. | ||
265 | 471 | self.assertIs(None, sub.out) | ||
266 | 472 | self.assertIs(None, sub.err) | ||
267 | 473 | |||
268 | 474 | |||
269 | 475 | class TestSubprocessMonitorWithScript(tests.TestCaseInTempDir): | ||
270 | 476 | |||
271 | 477 | def run_in_subprocess(self, args, klass=None): | ||
272 | 478 | if klass is None: | ||
273 | 479 | klass = icommon.SubprocessMonitor | ||
274 | 480 | sub = klass(args) | ||
275 | 481 | self.addCleanup(sub.join, 0) | ||
276 | 482 | sub.start() | ||
277 | 483 | return sub | ||
278 | 484 | |||
279 | 485 | def test_kill_caught(self): | ||
280 | 486 | self.build_tree_contents([('test.py', | ||
281 | 487 | """\ | ||
282 | 488 | class Killed(Exception): | ||
283 | 489 | |||
284 | 490 | def __str__(self): | ||
285 | 491 | |||
286 | 492 | return "I'm so dead" | ||
287 | 493 | |||
288 | 494 | def die(signum, frame): | ||
289 | 495 | raise Killed | ||
290 | 496 | import signal | ||
291 | 497 | signal.signal(signal.SIGTERM, die) | ||
292 | 498 | f = open('ready_to_die', 'w') | ||
293 | 499 | try: | ||
294 | 500 | f.write('Yes I am\\n') | ||
295 | 501 | finally: | ||
296 | 502 | f.close() | ||
297 | 503 | import time | ||
298 | 504 | slice = 0.001 # seconds | ||
299 | 505 | max = 120.0 # seconds | ||
300 | 506 | # We want to die long after the test fail (it it succeeds we're dead already) | ||
301 | 507 | while xrange(max/slice): | ||
302 | 508 | time.sleep(0.001) | ||
303 | 509 | """)]) | ||
304 | 510 | sub = self.run_in_subprocess(['test.py']) | ||
305 | 511 | sub.started.wait() | ||
306 | 512 | # Gross hack to ensure the script ran long enough to catch the signal | ||
307 | 513 | ready = False | ||
308 | 514 | while not ready: | ||
309 | 515 | try: | ||
310 | 516 | f = open('ready_to_die') | ||
311 | 517 | ready = True | ||
312 | 518 | except: | ||
313 | 519 | pass | ||
314 | 520 | self.assertEquals(True, sub.kill()) | ||
315 | 521 | sub.stopped.wait() | ||
316 | 522 | # The signal has been caught, we just get a failure return code, not | ||
317 | 523 | # funky stuff with the signal in the low byte and so on | ||
318 | 524 | self.assertEquals(1, sub.retcode) | ||
319 | 525 | # after the split, the last line is empty | ||
320 | 526 | self.assertEndsWith(sub.err.split('\n')[-2], "I'm so dead") | ||
321 | 460 | 527 | ||
322 | 461 | class Sleeper(icommon.SubprocessMonitor): | 528 | class Sleeper(icommon.SubprocessMonitor): |
323 | 462 | 529 | ||
324 | 463 | def __init__(self, sleep_time=0.001, max_duration=None): | 530 | def __init__(self, sleep_time=0.001, max_duration=None): |
325 | 464 | # sleep can't be killed (or interrupted) so we don't sleep for long but | 531 | # sleep can't be killed (or interrupted) so we don't sleep for long but |
326 | 465 | # keep sleeping until killed | 532 | # keep sleeping until killed |
333 | 466 | super(Sleeper, self).__init__('while true ; do sleep %f ; done' | 533 | super(Sleeper, self).__init__( |
334 | 467 | % (sleep_time,), | 534 | # Make sure we die long after the test finish (to avoid leaks) but |
335 | 468 | max_duration=max_duration) | 535 | # still use a value that allow *some* debug (you want more for |
336 | 469 | 536 | # *heavy* debug :) | |
337 | 470 | 537 | ['-c', 'import time; while xrange(120.0/%s): time.sleep(%s)' | |
338 | 471 | class TestDriverFeatures(tests.TestCase): | 538 | % (sleep_time, sleep_time)], |
339 | 539 | max_duration=max_duration) | ||
340 | 540 | |||
341 | 541 | |||
342 | 542 | class TestThreadDriver(tests.TestCase): | ||
343 | 472 | 543 | ||
344 | 473 | def get_driver(self, *args, **kwargs): | 544 | def get_driver(self, *args, **kwargs): |
345 | 474 | klass = kwargs.pop('klass', None) | 545 | klass = kwargs.pop('klass', None) |
346 | @@ -557,10 +628,9 @@ | |||
347 | 557 | 628 | ||
348 | 558 | def collect(thread): | 629 | def collect(thread): |
349 | 559 | self.assertIs(self.thread, thread) | 630 | self.assertIs(self.thread, thread) |
350 | 560 | # false returns 1 | ||
351 | 561 | self.assertEquals(1, thread.retcode) | 631 | self.assertEquals(1, thread.retcode) |
352 | 562 | self.terminated.set() | 632 | self.terminated.set() |
354 | 563 | self.thread = TestThread('false') | 633 | self.thread = TestThread(['-c', 'import sys; sys.exit(1)']) |
355 | 564 | self.addCleanup(self.thread.join, 0) | 634 | self.addCleanup(self.thread.join, 0) |
356 | 565 | q = Queue.Queue(1) | 635 | q = Queue.Queue(1) |
357 | 566 | q.put(self.thread) | 636 | q.put(self.thread) |
358 | @@ -583,7 +653,8 @@ | |||
359 | 583 | self.addCleanup(driver.stop.set) | 653 | self.addCleanup(driver.stop.set) |
360 | 584 | # Wait for the subprocess thread to start | 654 | # Wait for the subprocess thread to start |
361 | 585 | self.started.wait() | 655 | self.started.wait() |
363 | 586 | # the thread is waiting but not yet deleted from the driver | 656 | # the thread is started and at most wait for collection on |
364 | 657 | # self.terminated so it's still there | ||
365 | 587 | self.assertLength(1, driver.threads) | 658 | self.assertLength(1, driver.threads) |
366 | 588 | self.assertEquals(self.thread, driver.threads[0]) | 659 | self.assertEquals(self.thread, driver.threads[0]) |
367 | 589 | self.resumed.set() | 660 | self.resumed.set() |
368 | @@ -596,11 +667,10 @@ | |||
369 | 596 | class TestThread(icommon.SubprocessMonitor): | 667 | class TestThread(icommon.SubprocessMonitor): |
370 | 597 | 668 | ||
371 | 598 | def collect(thread): | 669 | def collect(thread): |
372 | 599 | # false returns 1 | ||
373 | 600 | self.assertEquals(1, thread.retcode) | 670 | self.assertEquals(1, thread.retcode) |
374 | 601 | raise ExceptionForTests() | 671 | raise ExceptionForTests() |
375 | 602 | q = Queue.Queue(1) | 672 | q = Queue.Queue(1) |
377 | 603 | t = TestThread('false') | 673 | t = TestThread(['-c', 'import sys; sys.exit(1)']) |
378 | 604 | self.addCleanup(t.join, 0) | 674 | self.addCleanup(t.join, 0) |
379 | 605 | q.put(t) | 675 | q.put(t) |
380 | 606 | driver = self.get_started_driver(queue=q, max_threads=1) | 676 | driver = self.get_started_driver(queue=q, max_threads=1) |
94 + cmd = 'import time; while True: time.sleep(0.001)'
Should this have a timeout?
Other than that this looks good.
Thanks,
James