summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMartin Pool <mbp@canonical.com>2011-11-29 12:56:44 +1100
committerMartin Pool <mbp@canonical.com>2011-11-29 12:56:44 +1100
commit3e502a29cc859f5c6b74827f5f321dfb5a193e3c (patch)
tree9dad8e9431ba8987f4dd8e0edf1456933d40fb17
parent5c1a5db9a6e06ff1fa2f816e99b606d3622fc789 (diff)
downloadfixtures-3e502a29cc859f5c6b74827f5f321dfb5a193e3c.tar.gz
Add TestTimeout fixture
-rw-r--r--NEWS2
-rw-r--r--README14
-rw-r--r--lib/fixtures/__init__.py2
-rw-r--r--lib/fixtures/_fixtures/__init__.py2
-rw-r--r--lib/fixtures/_fixtures/timeout.py45
-rw-r--r--lib/fixtures/tests/_fixtures/__init__.py1
-rw-r--r--lib/fixtures/tests/_fixtures/test_timeout.py76
7 files changed, 142 insertions, 0 deletions
diff --git a/NEWS b/NEWS
index 550c979..005e674 100644
--- a/NEWS
+++ b/NEWS
@@ -11,6 +11,8 @@ CHANGES:
* EnvironmentVariableFixture now upcalls via super().
(Jonathan Lange, #881120)
+* New TimeoutFixture. (Martin Pool)
+
0.3.7
~~~~~
diff --git a/README b/README
index e00bcd8..574649f 100644
--- a/README
+++ b/README
@@ -335,3 +335,17 @@ Create a temporary directory and clean it up later.
The created directory is stored in the ``path`` attribute of the fixture after
setUp.
+
+TimeoutFixture
+++++++++++++++
+
+Interrupts tests if they take more than a specified number of whole wall-clock
+seconds.
+
+There are two possibilities, controlled by the 'gentle' argument: when gentle,
+an exception will be raised and the test will fail. When not gentle, the
+entire test process will be terminated, which is less clean, but more likely to
+break hangs where no Python code is running. You can only use one in any given
+test.
+
+Currently supported only on Unix because it relies on the ``alarm`` system call.
diff --git a/lib/fixtures/__init__.py b/lib/fixtures/__init__.py
index 1d1948b..9e54aef 100644
--- a/lib/fixtures/__init__.py
+++ b/lib/fixtures/__init__.py
@@ -51,6 +51,7 @@ __all__ = [
'PythonPathEntry',
'TempDir',
'TestWithFixtures',
+ 'TestTimeout',
]
@@ -64,6 +65,7 @@ from fixtures._fixtures import (
PythonPackage,
PythonPathEntry,
TempDir,
+ TestTimeout,
)
from fixtures.testcase import TestWithFixtures
diff --git a/lib/fixtures/_fixtures/__init__.py b/lib/fixtures/_fixtures/__init__.py
index 082c936..20285ef 100644
--- a/lib/fixtures/_fixtures/__init__.py
+++ b/lib/fixtures/_fixtures/__init__.py
@@ -25,6 +25,7 @@ __all__ = [
'PythonPackage',
'PythonPathEntry',
'TempDir',
+ 'TestTimeout',
]
@@ -36,3 +37,4 @@ from fixtures._fixtures.packagepath import PackagePathEntry
from fixtures._fixtures.pythonpackage import PythonPackage
from fixtures._fixtures.pythonpath import PythonPathEntry
from fixtures._fixtures.tempdir import TempDir
+from fixtures._fixtures.timeout import TestTimeout
diff --git a/lib/fixtures/_fixtures/timeout.py b/lib/fixtures/_fixtures/timeout.py
new file mode 100644
index 0000000..b1d133b
--- /dev/null
+++ b/lib/fixtures/_fixtures/timeout.py
@@ -0,0 +1,45 @@
+# fixtures: Fixtures with cleanups for testing and convenience.
+#
+# Copyright (C) 2011, Martin Pool <mbp@sourcefrog.net>
+#
+# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause
+# license at the users choice. A copy of both licenses are available in the
+# project source as Apache-2.0 and BSD. You may not use this file except in
+# compliance with one of these two licences.
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# license you chose for the specific language governing permissions and
+# limitations under that license.
+
+
+import signal
+
+import fixtures
+
+
+class TestTimeoutException(Exception):
+ """Test timed out"""
+
+
+class TestTimeout(fixtures.Fixture):
+
+ def __init__(self, timeout_secs, gentle):
+ self.timeout_secs = timeout_secs
+ self.alarm_fn = getattr(signal, 'alarm', None)
+ self.gentle = gentle
+
+ def signal_handler(self):
+ raise TestTimeoutException()
+
+ def setUp(self):
+ super(TestTimeout, self).setUp()
+ if self.alarm_fn is None:
+ return # Can't run on Windows
+ self.alarm_fn(self.timeout_secs)
+ self.addCleanup(lambda: self.alarm_fn(0))
+ if self.gentle:
+ saved_handler = signal.signal(signal.SIGALRM, self.signal_handler)
+ self.addCleanup(lambda: signal.signal(signal.SIGALRM, saved_handler))
+ # Otherwise, the SIGALRM will probably kill the process.
diff --git a/lib/fixtures/tests/_fixtures/__init__.py b/lib/fixtures/tests/_fixtures/__init__.py
index b4d4be0..8e817a0 100644
--- a/lib/fixtures/tests/_fixtures/__init__.py
+++ b/lib/fixtures/tests/_fixtures/__init__.py
@@ -23,6 +23,7 @@ def load_tests(loader, standard_tests, pattern):
'pythonpackage',
'pythonpath',
'tempdir',
+ 'timeout',
]
prefix = "fixtures.tests._fixtures.test_"
test_mod_names = [prefix + test_module for test_module in test_modules]
diff --git a/lib/fixtures/tests/_fixtures/test_timeout.py b/lib/fixtures/tests/_fixtures/test_timeout.py
new file mode 100644
index 0000000..870f3bc
--- /dev/null
+++ b/lib/fixtures/tests/_fixtures/test_timeout.py
@@ -0,0 +1,76 @@
+# fixtures: Fixtures with cleanups for testing and convenience.
+#
+# Copyright (C) 2011, Martin Pool <mbp@sourcefrog.net>
+#
+# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause
+# license at the users choice. A copy of both licenses are available in the
+# project source as Apache-2.0 and BSD. You may not use this file except in
+# compliance with one of these two licences.
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# license you chose for the specific language governing permissions and
+# limitations under that license.
+
+import os
+import signal
+
+import testtools
+from testtools.testcase import (
+ TestSkipped,
+ )
+
+import fixtures
+from fixtures import EnvironmentVariableFixture, TestWithFixtures
+
+
+class ExampleTests(testtools.TestCase, TestWithFixtures):
+ """These are not intended to pass: they are sample data for the real tests"""
+
+ def sample_timeout_passes(self):
+ self.useFixture(fixtures.TestTimeout(100, gentle=True))
+ pass # Timeout shouldn't fire
+
+ def sample_long_delay_with_timeout(self):
+ self.useFixture(fixtures.TestTimeout(2, gentle=True))
+ time.sleep(100) # Expected to be killed here.
+
+ def sample_long_delay_with_harsh_timeout(self):
+ self.useFixture(fixtures.TestTimeout(2, gentle=False))
+ time.sleep(100) # Expected to be killed here.
+
+
+
+class TestTimeoutFixture(testtools.TestCase, TestWithFixtures):
+
+ def requireUnix(self):
+ if getattr(signal, 'alarm', None) is None:
+ raise TestSkipped("no alarm() function")
+
+ def test_timeout_passes(self):
+ # This can pass even on Windows - the test is skipped.
+ test = ExampleTests('sample_timeout_passes')
+ result = test.run()
+ self.assertTrue(result.wasSuccessful())
+
+ def test_timeout_gentle(self):
+ self.requireUnix()
+ test = ExampleTests('sample_long_delay_with_timeout')
+ result = test.run()
+ self.assertFalse(result.wasSuccessful())
+
+ def test_timeout_harsh(self):
+ self.requireUnix()
+ test = ExampleTests('sample_long_delay_with_harsh_timeout')
+ # This will normally kill the whole process, which would be
+ # inconvenient. Let's hook the alarm here so we can observe it.
+ got_alarm = False
+ def sigalrm_handler():
+ got_alarm = True
+ old_handler = signal.signal(signal.SIGALRM, sigalrm_handler)
+ self.addCleanup(signal.signal, signal.SIGALRM, old_handler)
+ import pdb;pdb.set_trace()
+ result = test.run()
+ self.assertFalse(result.wasSuccessful())
+ self.assertTrue(got_alarm)