Rev 5655: (vila) Moves CatchingExceptionThread out of the bzrlib.tests hierarchy. in file:///home/pqm/archives/thelove/bzr/%2Btrunk/
Canonical.com Patch Queue Manager
pqm at pqm.ubuntu.com
Wed Feb 9 10:44:20 UTC 2011
At file:///home/pqm/archives/thelove/bzr/%2Btrunk/
------------------------------------------------------------
revno: 5655 [merge]
revision-id: pqm at pqm.ubuntu.com-20110209104418-n3058l06ehubtl9a
parent: pqm at pqm.ubuntu.com-20110209100454-gb7liylp8ywpdrn2
parent: v.ladeuil+lp at free.fr-20110209091410-cubxgkmrfd0hu577
committer: Canonical.com Patch Queue Manager <pqm at pqm.ubuntu.com>
branch nick: +trunk
timestamp: Wed 2011-02-09 10:44:18 +0000
message:
(vila) Moves CatchingExceptionThread out of the bzrlib.tests hierarchy.
(Vincent Ladeuil)
added:
bzrlib/tests/test_thread.py test_thread.py-20110208152811-9ivenqn0omw1ev4d-1
bzrlib/thread.py thread.py-20110208152810-i29un3um8v6marcr-1
modified:
bzrlib/tests/__init__.py selftest.py-20050531073622-8d0e3c8845c97a64
bzrlib/tests/test_http.py testhttp.py-20051018020158-b2eef6e867c514d9
bzrlib/tests/test_server.py test_server.py-20100209163834-im1ozfuenfmqaa2m-1
bzrlib/tests/test_test_server.py test_test_server.py-20100601152414-r8rln0ok7514pcoz-1
doc/en/release-notes/bzr-2.4.txt bzr2.4.txt-20110114053217-k7ym9jfz243fddjm-1
=== modified file 'bzrlib/tests/__init__.py'
--- a/bzrlib/tests/__init__.py 2011-01-27 15:58:36 +0000
+++ b/bzrlib/tests/__init__.py 2011-02-08 15:54:40 +0000
@@ -3858,6 +3858,7 @@
'bzrlib.tests.test_testament',
'bzrlib.tests.test_textfile',
'bzrlib.tests.test_textmerge',
+ 'bzrlib.tests.test_thread',
'bzrlib.tests.test_timestamp',
'bzrlib.tests.test_trace',
'bzrlib.tests.test_transactions',
=== modified file 'bzrlib/tests/test_http.py'
--- a/bzrlib/tests/test_http.py 2011-02-03 09:07:25 +0000
+++ b/bzrlib/tests/test_http.py 2011-02-08 16:26:23 +0000
@@ -37,6 +37,7 @@
osutils,
remote as _mod_remote,
tests,
+ thread,
transport,
ui,
)
@@ -178,8 +179,8 @@
self._sock.bind(('127.0.0.1', 0))
self.host, self.port = self._sock.getsockname()
self._ready = threading.Event()
- self._thread = test_server.ThreadWithException(
- event=self._ready, target=self._accept_read_and_reply)
+ self._thread = test_server.TestThread(
+ sync_event=self._ready, target=self._accept_read_and_reply)
self._thread.start()
if 'threads' in tests.selftest_debug_flags:
sys.stderr.write('Thread started: %s\n' % (self._thread.ident,))
=== modified file 'bzrlib/tests/test_server.py'
--- a/bzrlib/tests/test_server.py 2011-01-14 22:36:45 +0000
+++ b/bzrlib/tests/test_server.py 2011-02-08 16:26:23 +0000
@@ -23,6 +23,7 @@
from bzrlib import (
osutils,
+ thread,
transport,
urlutils,
)
@@ -242,94 +243,15 @@
raise NotImplementedError
-class ThreadWithException(threading.Thread):
- """A catching exception thread.
-
- If an exception occurs during the thread execution, it's caught and
- re-raised when the thread is joined().
- """
-
- def __init__(self, *args, **kwargs):
- # There are cases where the calling thread must wait, yet, if an
- # exception occurs, the event should be set so the caller is not
- # blocked. The main example is a calling thread that want to wait for
- # the called thread to be in a given state before continuing.
- try:
- event = kwargs.pop('event')
- except KeyError:
- # If the caller didn't pass a specific event, create our own
- event = threading.Event()
- super(ThreadWithException, self).__init__(*args, **kwargs)
- self.set_ready_event(event)
- self.exception = None
- self.ignored_exceptions = None # see set_ignored_exceptions
-
- # compatibility thunk for python-2.4 and python-2.5...
- if sys.version_info < (2, 6):
- name = property(threading.Thread.getName, threading.Thread.setName)
-
- def set_ready_event(self, event):
- """Set the ``ready`` event used to synchronize exception catching.
-
- When the thread uses an event to synchronize itself with another thread
- (setting it when the other thread can wake up from a ``wait`` call),
- the event must be set after catching an exception or the other thread
- will hang.
-
- Some threads require multiple events and should set the relevant one
- when appropriate.
- """
- self.ready = event
-
- def set_ignored_exceptions(self, ignored):
- """Declare which exceptions will be ignored.
-
- :param ignored: Can be either:
- - None: all exceptions will be raised,
- - an exception class: the instances of this class will be ignored,
- - a tuple of exception classes: the instances of any class of the
- list will be ignored,
- - a callable: that will be passed the exception object
- and should return True if the exception should be ignored
- """
- if ignored is None:
- self.ignored_exceptions = None
- elif isinstance(ignored, (Exception, tuple)):
- self.ignored_exceptions = lambda e: isinstance(e, ignored)
- else:
- self.ignored_exceptions = ignored
-
- def run(self):
- """Overrides Thread.run to capture any exception."""
- self.ready.clear()
- try:
- try:
- super(ThreadWithException, self).run()
- except:
- self.exception = sys.exc_info()
- finally:
- # Make sure the calling thread is released
- self.ready.set()
-
+class TestThread(thread.CatchingExceptionThread):
def join(self, timeout=5):
- """Overrides Thread.join to raise any exception caught.
-
-
- Calling join(timeout=0) will raise the caught exception or return None
- if the thread is still alive.
+ """Overrides to use a default timeout.
The default timeout is set to 5 and should expire only when a thread
serving a client connection is hung.
"""
- super(ThreadWithException, self).join(timeout)
- if self.exception is not None:
- exc_class, exc_value, exc_tb = self.exception
- self.exception = None # The exception should be raised only once
- if (self.ignored_exceptions is None
- or not self.ignored_exceptions(exc_value)):
- # Raise non ignored exceptions
- raise exc_class, exc_value, exc_tb
+ super(TestThread, self).join(timeout)
if timeout and self.isAlive():
# The timeout expired without joining the thread, the thread is
# therefore stucked and that's a failure as far as the test is
@@ -342,13 +264,6 @@
sys.stderr.write('thread %s hung\n' % (self.name,))
#raise AssertionError('thread %s hung' % (self.name,))
- def pending_exception(self):
- """Raise the caught exception.
-
- This does nothing if no exception occurred.
- """
- self.join(timeout=0)
-
class TestingTCPServerMixin:
"""Mixin to support running SocketServer.TCPServer in a thread.
@@ -522,8 +437,8 @@
"""Start a new thread to process the request."""
started = threading.Event()
stopped = threading.Event()
- t = ThreadWithException(
- event=stopped,
+ t = TestThread(
+ sync_event=stopped,
name='%s -> %s' % (client_address, self.server_address),
target = self.process_request_thread,
args = (started, stopped, request, client_address))
@@ -589,8 +504,8 @@
def start_server(self):
self.server = self.create_server()
- self._server_thread = ThreadWithException(
- event=self.server.started,
+ self._server_thread = TestThread(
+ sync_event=self.server.started,
target=self.run_server)
self._server_thread.start()
# Wait for the server thread to start (i.e release the lock)
@@ -606,7 +521,7 @@
self._server_thread.pending_exception()
# From now on, we'll use a different event to ensure the server can set
# its exception
- self._server_thread.set_ready_event(self.server.stopped)
+ self._server_thread.set_sync_event(self.server.stopped)
def run_server(self):
self.server.serve()
=== modified file 'bzrlib/tests/test_test_server.py'
--- a/bzrlib/tests/test_test_server.py 2011-01-10 22:20:12 +0000
+++ b/bzrlib/tests/test_test_server.py 2011-02-08 16:26:23 +0000
@@ -30,46 +30,6 @@
load_tests = load_tests_apply_scenarios
-class TestThreadWithException(tests.TestCase):
-
- def test_start_and_join_smoke_test(self):
- def do_nothing():
- pass
-
- tt = test_server.ThreadWithException(target=do_nothing)
- tt.start()
- tt.join()
-
- def test_exception_is_re_raised(self):
- class MyException(Exception):
- pass
-
- def raise_my_exception():
- raise MyException()
-
- tt = test_server.ThreadWithException(target=raise_my_exception)
- tt.start()
- self.assertRaises(MyException, tt.join)
-
- def test_join_when_no_exception(self):
- resume = threading.Event()
- class MyException(Exception):
- pass
-
- def raise_my_exception():
- # Wait for the test to tell us to resume
- resume.wait()
- # Now we can raise
- raise MyException()
-
- tt = test_server.ThreadWithException(target=raise_my_exception)
- tt.start()
- tt.join(timeout=0)
- self.assertIs(None, tt.exception)
- resume.set()
- self.assertRaises(MyException, tt.join)
-
-
class TCPClient(object):
def __init__(self):
@@ -221,7 +181,7 @@
def handle_connection(self):
req = self.rfile.readline()
- threading.currentThread().set_ready_event(sync)
+ threading.currentThread().set_sync_event(sync)
raise FailToRespond()
server = self.get_server(
@@ -243,7 +203,7 @@
def handle(self):
# We want to sync with the thread that is serving the
# connection.
- threading.currentThread().set_ready_event(sync)
+ threading.currentThread().set_sync_event(sync)
raise CantServe()
server = self.get_server(
=== added file 'bzrlib/tests/test_thread.py'
--- a/bzrlib/tests/test_thread.py 1970-01-01 00:00:00 +0000
+++ b/bzrlib/tests/test_thread.py 2011-02-08 16:26:23 +0000
@@ -0,0 +1,64 @@
+# Copyright (C) 2010, 2011 Canonical Ltd
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+import threading
+
+from bzrlib import (
+ tests,
+ thread,
+ )
+
+
+class TestCatchingExceptionThread(tests.TestCase):
+
+ def test_start_and_join_smoke_test(self):
+ def do_nothing():
+ pass
+
+ tt = thread.CatchingExceptionThread(target=do_nothing)
+ tt.start()
+ tt.join()
+
+ def test_exception_is_re_raised(self):
+ class MyException(Exception):
+ pass
+
+ def raise_my_exception():
+ raise MyException()
+
+ tt = thread.CatchingExceptionThread(target=raise_my_exception)
+ tt.start()
+ self.assertRaises(MyException, tt.join)
+
+ def test_join_when_no_exception(self):
+ resume = threading.Event()
+ class MyException(Exception):
+ pass
+
+ def raise_my_exception():
+ # Wait for the test to tell us to resume
+ resume.wait()
+ # Now we can raise
+ raise MyException()
+
+ tt = thread.CatchingExceptionThread(target=raise_my_exception)
+ tt.start()
+ tt.join(timeout=0)
+ self.assertIs(None, tt.exception)
+ resume.set()
+ self.assertRaises(MyException, tt.join)
+
+
=== added file 'bzrlib/thread.py'
--- a/bzrlib/thread.py 1970-01-01 00:00:00 +0000
+++ b/bzrlib/thread.py 2011-02-08 16:26:23 +0000
@@ -0,0 +1,114 @@
+# Copyright (C) 2011 Canonical Ltd
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+import sys
+import threading
+
+
+class CatchingExceptionThread(threading.Thread):
+ """A thread that keeps track of exceptions.
+
+ If an exception occurs during the thread execution, it's caught and
+ re-raised when the thread is joined().
+ """
+
+ def __init__(self, *args, **kwargs):
+ # There are cases where the calling thread must wait, yet, if an
+ # exception occurs, the event should be set so the caller is not
+ # blocked. The main example is a calling thread that want to wait for
+ # the called thread to be in a given state before continuing.
+ try:
+ sync_event = kwargs.pop('sync_event')
+ except KeyError:
+ # If the caller didn't pass a specific event, create our own
+ sync_event = threading.Event()
+ super(CatchingExceptionThread, self).__init__(*args, **kwargs)
+ self.set_sync_event(sync_event)
+ self.exception = None
+ self.ignored_exceptions = None # see set_ignored_exceptions
+
+ # compatibility thunk for python-2.4 and python-2.5...
+ if sys.version_info < (2, 6):
+ name = property(threading.Thread.getName, threading.Thread.setName)
+
+ def set_sync_event(self, event):
+ """Set the ``sync_event`` event used to synchronize exception catching.
+
+ When the thread uses an event to synchronize itself with another thread
+ (setting it when the other thread can wake up from a ``wait`` call),
+ the event must be set after catching an exception or the other thread
+ will hang.
+
+ Some threads require multiple events and should set the relevant one
+ when appropriate.
+
+ Note that the event should be cleared so the caller can wait() on him
+ and be released when the thread set the event.
+ """
+ self.sync_event = event
+
+ def set_ignored_exceptions(self, ignored):
+ """Declare which exceptions will be ignored.
+
+ :param ignored: Can be either:
+ - None: all exceptions will be raised,
+ - an exception class: the instances of this class will be ignored,
+ - a tuple of exception classes: the instances of any class of the
+ list will be ignored,
+ - a callable: that will be passed the exception object
+ and should return True if the exception should be ignored
+ """
+ if ignored is None:
+ self.ignored_exceptions = None
+ elif isinstance(ignored, (Exception, tuple)):
+ self.ignored_exceptions = lambda e: isinstance(e, ignored)
+ else:
+ self.ignored_exceptions = ignored
+
+ def run(self):
+ """Overrides Thread.run to capture any exception."""
+ self.sync_event.clear()
+ try:
+ try:
+ super(CatchingExceptionThread, self).run()
+ except:
+ self.exception = sys.exc_info()
+ finally:
+ # Make sure the calling thread is released
+ self.sync_event.set()
+
+
+ def join(self, timeout=None):
+ """Overrides Thread.join to raise any exception caught.
+
+ Calling join(timeout=0) will raise the caught exception or return None
+ if the thread is still alive.
+ """
+ super(CatchingExceptionThread, self).join(timeout)
+ if self.exception is not None:
+ exc_class, exc_value, exc_tb = self.exception
+ self.exception = None # The exception should be raised only once
+ if (self.ignored_exceptions is None
+ or not self.ignored_exceptions(exc_value)):
+ # Raise non ignored exceptions
+ raise exc_class, exc_value, exc_tb
+
+ def pending_exception(self):
+ """Raise the caught exception.
+
+ This does nothing if no exception occurred.
+ """
+ self.join(timeout=0)
=== modified file 'doc/en/release-notes/bzr-2.4.txt'
--- a/doc/en/release-notes/bzr-2.4.txt 2011-02-09 09:01:33 +0000
+++ b/doc/en/release-notes/bzr-2.4.txt 2011-02-09 10:44:18 +0000
@@ -118,6 +118,12 @@
.. Major internal changes, unlikely to be visible to users or plugin
developers, but interesting for bzr developers.
+* ``CatchingExceptionThread`` (formerly ThreadWithException) has been moved
+ out of the ``bzrlib.tests`` hierarchy to make it clearer that it can be used
+ outside of tests. This class makes it easier to track exceptions in threads
+ by cacthing them so they can be re-raised in the controlling thread.
+ (Vincent Ladeuil)
+
Testing
*******
More information about the bazaar-commits
mailing list