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.
Revision history for this message
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

Rename to just 'Timeout'; other review cleanups

45. By Martin Pool

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

Revision history for this message
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