Merge lp:~gz/juju-ci-tools/utility_logged_exception into lp:juju-ci-tools

Proposed by Martin Packman
Status: Merged
Approved by: Martin Packman
Approved revision: 1737
Merged at revision: 1736
Proposed branch: lp:~gz/juju-ci-tools/utility_logged_exception
Merge into: lp:juju-ci-tools
Diff against target: 171 lines (+81/-20)
3 files modified
deploy_stack.py (+4/-20)
tests/test_utility.py (+49/-0)
utility.py (+28/-0)
To merge this branch: bzr merge lp:~gz/juju-ci-tools/utility_logged_exception
Reviewer Review Type Date Requested Status
Aaron Bentley (community) Approve
Review via email: mp+311197@code.launchpad.net

Commit message

Add logged_exception helper to utility factored out from deploy_stack

Description of the change

Pulls the LoggedException code from deploy_stack to a more reusable location.

There is one relevent functional change here, the call site in BoostrapManager.runtime_context now does not wrap SystemExit or KeyboardInterrupt. This feels correct to me, but I am perhaps missing something. We may want specific KeyboardInterrupt handling given SIGINT is our top level timeout method.

Not done in this branch, switching deploy_stack to using a named logger rather than logging directly, and using the new functions in other tests.

To post a comment you must log in.
1737. By Martin Packman

Also wrap KeyboardInterrupt as SIGINT is used for timeout

Revision history for this message
Aaron Bentley (abentley) wrote :

Thanks.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'deploy_stack.py'
2--- deploy_stack.py 2016-11-14 22:18:53 +0000
3+++ deploy_stack.py 2016-11-17 18:55:17 +0000
4@@ -65,6 +65,7 @@
5 configure_logging,
6 ensure_deleted,
7 ensure_dir,
8+ logged_exception,
9 LoggedException,
10 PortTimeoutError,
11 print_now,
12@@ -753,16 +754,6 @@
13 self.controller_strategy.tear_down()
14 self.has_controller = False
15
16- def _log_and_wrap_exception(self, exc):
17- logging.exception(exc)
18- stdout = getattr(exc, 'output', None)
19- stderr = getattr(exc, 'stderr', None)
20- if stdout or stderr:
21- logging.info(
22- 'Output from exception:\nstdout:\n%s\nstderr:\n%s',
23- stdout, stderr)
24- return LoggedException(exc)
25-
26 @contextmanager
27 def bootstrap_context(self, machines, omit_config=None):
28 """Context for bootstrapping a state server."""
29@@ -845,13 +836,11 @@
30 Tear down. (self.keep_env is ignored.)
31 """
32 try:
33- try:
34- yield
35 # If an exception is raised that indicates an error, log it
36 # before tearing down so that the error is closely tied to
37 # the failed operation.
38- except Exception as e:
39- raise self._log_and_wrap_exception(e)
40+ with logged_exception(logging):
41+ yield
42 except:
43 # If run from a windows machine may not have ssh to get
44 # logs
45@@ -873,18 +862,13 @@
46 control is yielded.
47 """
48 try:
49- try:
50+ with logged_exception(logging):
51 if len(self.known_hosts) == 0:
52 self.known_hosts.update(
53 self.controller_strategy.get_hosts())
54 if addable_machines is not None:
55 self.client.add_ssh_machines(addable_machines)
56 yield
57- # avoid logging GeneratorExit
58- except GeneratorExit:
59- raise
60- except BaseException as e:
61- raise self._log_and_wrap_exception(e)
62 except:
63 if self.has_controller:
64 safe_print_status(self.client)
65
66=== modified file 'tests/test_utility.py'
67--- tests/test_utility.py 2016-11-08 15:17:26 +0000
68+++ tests/test_utility.py 2016-11-17 18:55:17 +0000
69@@ -18,6 +18,7 @@
70
71 from mock import (
72 call,
73+ Mock,
74 patch,
75 )
76
77@@ -35,6 +36,9 @@
78 get_deb_arch,
79 get_winrm_certs,
80 is_ipv6_address,
81+ log_and_wrap_exception,
82+ logged_exception,
83+ LoggedException,
84 quote,
85 run_command,
86 scoped_environ,
87@@ -714,3 +718,48 @@
88 qualified_model_name('admin/default', 'admin'),
89 'admin/default'
90 )
91+
92+
93+class TestLoggedException(TestCase):
94+
95+ def test_no_error_no_log(self):
96+ mock_logger = Mock(spec_set=[])
97+ with logged_exception(mock_logger):
98+ pass
99+
100+ def test_exception_logged_and_wrapped(self):
101+ mock_logger = Mock(spec=['exception'])
102+ err = Exception('some error')
103+ with self.assertRaises(LoggedException) as ctx:
104+ with logged_exception(mock_logger):
105+ raise err
106+ self.assertIs(ctx.exception.exception, err)
107+ mock_logger.exception.assert_called_once_with(err)
108+
109+ def test_generator_exit_not_wrapped(self):
110+ mock_logger = Mock(spec_set=[])
111+ with self.assertRaises(GeneratorExit):
112+ with logged_exception(mock_logger):
113+ raise GeneratorExit
114+
115+ def test_keyboard_interrupt_wrapped(self):
116+ mock_logger = Mock(spec=['exception'])
117+ err = KeyboardInterrupt()
118+ with self.assertRaises(LoggedException) as ctx:
119+ with logged_exception(mock_logger):
120+ raise err
121+ self.assertIs(ctx.exception.exception, err)
122+ mock_logger.exception.assert_called_once_with(err)
123+
124+ def test_output_logged(self):
125+ mock_logger = Mock(spec=['exception', 'info'])
126+ err = Exception('some error')
127+ err.output = 'some output'
128+ with self.assertRaises(LoggedException) as ctx:
129+ with logged_exception(mock_logger):
130+ raise err
131+ self.assertIs(ctx.exception.exception, err)
132+ mock_logger.exception.assert_called_once_with(err)
133+ mock_logger.info.assert_called_once_with(
134+ 'Output from exception:\nstdout:\n%s\nstderr:\n%s', 'some output',
135+ None)
136
137=== modified file 'utility.py'
138--- utility.py 2016-11-16 20:28:18 +0000
139+++ utility.py 2016-11-17 18:55:17 +0000
140@@ -526,3 +526,31 @@
141 def get_unit_ipaddress(client, unit_name):
142 status = client.get_status()
143 return status.get_unit(unit_name)['public-address']
144+
145+
146+def log_and_wrap_exception(logger, exc):
147+ """"Record exc details to logger and return wrapped in LoggedException."""
148+ logger.exception(exc)
149+ stdout = getattr(exc, 'output', None)
150+ stderr = getattr(exc, 'stderr', None)
151+ if stdout or stderr:
152+ logger.info(
153+ 'Output from exception:\nstdout:\n%s\nstderr:\n%s',
154+ stdout, stderr)
155+ return LoggedException(exc)
156+
157+
158+@contextmanager
159+def logged_exception(logger):
160+ """
161+ Record exceptions in managed context to logger and reraise LoggedException.
162+
163+ Note that BaseException classes like SystemExit, KeyboardInterrupt, and
164+ GeneratorExit are not wrapped.
165+ """
166+ try:
167+ yield
168+ except LoggedException:
169+ raise
170+ except (Exception, KeyboardInterrupt) as e:
171+ raise log_and_wrap_exception(logger, e)

Subscribers

People subscribed via source and target branches