Merge lp:~mbp/python-fixtures/timeout into lp:~python-fixtures/python-fixtures/trunk

Proposed by Martin Pool
Status: Merged
Merged at revision: 43
Proposed branch: lp:~mbp/python-fixtures/timeout
Merge into: lp:~python-fixtures/python-fixtures/trunk
Diff against target: 234 lines (+162/-0)
7 files modified
NEWS (+2/-0)
README (+18/-0)
lib/fixtures/__init__.py (+4/-0)
lib/fixtures/_fixtures/__init__.py (+6/-0)
lib/fixtures/_fixtures/timeout.py (+64/-0)
lib/fixtures/tests/_fixtures/__init__.py (+1/-0)
lib/fixtures/tests/_fixtures/test_timeout.py (+67/-0)
To merge this branch: bzr merge lp:~mbp/python-fixtures/timeout
Reviewer Review Type Date Requested Status
python-fixtures committers Pending
Review via email: mp+83721@code.launchpad.net

Description of the change

This adds a new TestTimeout based on https://code.launchpad.net/~mbp/bzr/test-timeout/+merge/83559

There are two modes, because I suspect people will have different preferences for being relatively sure things will stop vs getting a clean error.

To post a comment you must log in.
Robert Collins (lifeless) wrote :

We discussed on IRC.

Could you please
rename to 'Timeout', and change from talking about use in tests (e.g. 1 per test) to use in general (can only use one of these at a time because it builds on SIGALARM).

lp:~mbp/python-fixtures/timeout updated
44. By Martin Pool on 2011-11-29

Rename to just 'Timeout'; other review cleanups

45. By Martin Pool on 2011-11-29

Rename to just TimeoutException, and remove more connections to Timeout only being used in tests

Robert Collins (lifeless) wrote :

I've merged this. I noted some race conditions while doing so, you may want to apply them to bzrlib's version (or start using fixtures :P).

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'NEWS'
2--- NEWS 2011-11-22 09:26:54 +0000
3+++ NEWS 2011-11-29 02:25:26 +0000
4@@ -11,6 +11,8 @@
5 * EnvironmentVariableFixture now upcalls via super().
6 (Jonathan Lange, #881120)
7
8+* New Timeout fixture. (Martin Pool)
9+
10 0.3.7
11 ~~~~~
12
13
14=== modified file 'README'
15--- README 2011-11-22 10:07:24 +0000
16+++ README 2011-11-29 02:25:26 +0000
17@@ -335,3 +335,21 @@
18
19 The created directory is stored in the ``path`` attribute of the fixture after
20 setUp.
21+
22+Timeout
23++++++++
24+
25+Aborts if the covered code takes more than a specified number of whole wall-clock
26+seconds.
27+
28+There are two possibilities, controlled by the 'gentle' argument: when gentle,
29+an exception will be raised and the test (or other covered code) will fail.
30+When not gentle, the entire process will be terminated, which is less clean,
31+but more likely to break hangs where no Python code is running.
32+
33+*Caution:* Only one timeout can be active at any time across all threads in a
34+single process. Using more than one has undefined results. (This could be
35+improved by chaining alarms.)
36+
37+*Note:* Currently supported only on Unix because it relies on the ``alarm``
38+system call.
39
40=== modified file 'lib/fixtures/__init__.py'
41--- lib/fixtures/__init__.py 2011-11-22 08:58:38 +0000
42+++ lib/fixtures/__init__.py 2011-11-29 02:25:26 +0000
43@@ -51,6 +51,8 @@
44 'PythonPathEntry',
45 'TempDir',
46 'TestWithFixtures',
47+ 'Timeout',
48+ 'TimeoutException',
49 ]
50
51
52@@ -64,6 +66,8 @@
53 PythonPackage,
54 PythonPathEntry,
55 TempDir,
56+ Timeout,
57+ TimeoutException,
58 )
59 from fixtures.testcase import TestWithFixtures
60
61
62=== modified file 'lib/fixtures/_fixtures/__init__.py'
63--- lib/fixtures/_fixtures/__init__.py 2011-10-26 15:10:31 +0000
64+++ lib/fixtures/_fixtures/__init__.py 2011-11-29 02:25:26 +0000
65@@ -25,6 +25,8 @@
66 'PythonPackage',
67 'PythonPathEntry',
68 'TempDir',
69+ 'Timeout',
70+ 'TimeoutException',
71 ]
72
73
74@@ -36,3 +38,7 @@
75 from fixtures._fixtures.pythonpackage import PythonPackage
76 from fixtures._fixtures.pythonpath import PythonPathEntry
77 from fixtures._fixtures.tempdir import TempDir
78+from fixtures._fixtures.timeout import (
79+ Timeout,
80+ TimeoutException,
81+ )
82
83=== added file 'lib/fixtures/_fixtures/timeout.py'
84--- lib/fixtures/_fixtures/timeout.py 1970-01-01 00:00:00 +0000
85+++ lib/fixtures/_fixtures/timeout.py 2011-11-29 02:25:26 +0000
86@@ -0,0 +1,64 @@
87+# fixtures: Fixtures with cleanups for testing and convenience.
88+#
89+# Copyright (C) 2011, Martin Pool <mbp@sourcefrog.net>
90+#
91+# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause
92+# license at the users choice. A copy of both licenses are available in the
93+# project source as Apache-2.0 and BSD. You may not use this file except in
94+# compliance with one of these two licences.
95+#
96+# Unless required by applicable law or agreed to in writing, software
97+# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT
98+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
99+# license you chose for the specific language governing permissions and
100+# limitations under that license.
101+
102+
103+"""Timeout fixture."""
104+
105+
106+import signal
107+
108+import fixtures
109+
110+__all__ = [
111+ 'Timeout',
112+ 'TimeoutException',
113+ ]
114+
115+
116+class TimeoutException(Exception):
117+ """Timeout expired"""
118+
119+
120+class Timeout(fixtures.Fixture):
121+ """Fixture that aborts the contained code after a number of seconds.
122+
123+ The interrupt can be either gentle, in which case TimeoutException is
124+ raised, or not gentle, in which case the process will typically be aborted
125+ by SIGALRM.
126+
127+ Cautions:
128+ * This has no effect on Windows.
129+ * Only one Timeout can be used at any time per process.
130+ """
131+
132+ def __init__(self, timeout_secs, gentle):
133+ self.timeout_secs = timeout_secs
134+ self.alarm_fn = getattr(signal, 'alarm', None)
135+ self.gentle = gentle
136+
137+ def signal_handler(self, signum, frame):
138+ raise TimeoutException()
139+
140+ def setUp(self):
141+ super(Timeout, self).setUp()
142+ if self.alarm_fn is None:
143+ return # Can't run on Windows
144+ self.alarm_fn(self.timeout_secs)
145+ self.addCleanup(lambda: self.alarm_fn(0))
146+ if self.gentle:
147+ saved_handler = signal.signal(signal.SIGALRM, self.signal_handler)
148+ self.addCleanup(lambda: signal.signal(
149+ signal.SIGALRM, saved_handler))
150+ # Otherwise, the SIGALRM will probably kill the process.
151
152=== modified file 'lib/fixtures/tests/_fixtures/__init__.py'
153--- lib/fixtures/tests/_fixtures/__init__.py 2011-10-27 11:58:24 +0000
154+++ lib/fixtures/tests/_fixtures/__init__.py 2011-11-29 02:25:26 +0000
155@@ -23,6 +23,7 @@
156 'pythonpackage',
157 'pythonpath',
158 'tempdir',
159+ 'timeout',
160 ]
161 prefix = "fixtures.tests._fixtures.test_"
162 test_mod_names = [prefix + test_module for test_module in test_modules]
163
164=== added file 'lib/fixtures/tests/_fixtures/test_timeout.py'
165--- lib/fixtures/tests/_fixtures/test_timeout.py 1970-01-01 00:00:00 +0000
166+++ lib/fixtures/tests/_fixtures/test_timeout.py 2011-11-29 02:25:26 +0000
167@@ -0,0 +1,67 @@
168+# fixtures: Fixtures with cleanups for testing and convenience.
169+#
170+# Copyright (C) 2011, Martin Pool <mbp@sourcefrog.net>
171+#
172+# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause
173+# license at the users choice. A copy of both licenses are available in the
174+# project source as Apache-2.0 and BSD. You may not use this file except in
175+# compliance with one of these two licences.
176+#
177+# Unless required by applicable law or agreed to in writing, software
178+# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT
179+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
180+# license you chose for the specific language governing permissions and
181+# limitations under that license.
182+
183+import os
184+import signal
185+import time
186+
187+import testtools
188+from testtools.testcase import (
189+ TestSkipped,
190+ )
191+
192+import fixtures
193+
194+
195+def sample_timeout_passes():
196+ with fixtures.Timeout(100, gentle=True):
197+ pass # Timeout shouldn't fire
198+
199+def sample_long_delay_with_gentle_timeout():
200+ with fixtures.Timeout(1, gentle=True):
201+ time.sleep(100) # Expected to be killed here.
202+
203+def sample_long_delay_with_harsh_timeout():
204+ with fixtures.Timeout(1, gentle=False):
205+ time.sleep(100) # Expected to be killed here.
206+
207+
208+class TestTimeout(testtools.TestCase, fixtures.TestWithFixtures):
209+
210+ def requireUnix(self):
211+ if getattr(signal, 'alarm', None) is None:
212+ raise TestSkipped("no alarm() function")
213+
214+ def test_timeout_passes(self):
215+ # This can pass even on Windows - the test is skipped.
216+ sample_timeout_passes()
217+
218+ def test_timeout_gentle(self):
219+ self.requireUnix()
220+ self.assertRaises(
221+ fixtures.TimeoutException,
222+ sample_long_delay_with_gentle_timeout)
223+
224+ def test_timeout_harsh(self):
225+ self.requireUnix()
226+ # This will normally kill the whole process, which would be
227+ # inconvenient. Let's hook the alarm here so we can observe it.
228+ self.got_alarm = False
229+ def sigalrm_handler(signum, frame):
230+ self.got_alarm = True
231+ old_handler = signal.signal(signal.SIGALRM, sigalrm_handler)
232+ self.addCleanup(signal.signal, signal.SIGALRM, old_handler)
233+ sample_long_delay_with_harsh_timeout()
234+ self.assertTrue(self.got_alarm)

Subscribers

People subscribed via source and target branches