add ReportingEventStack

This adds a new class ReportingEvent Stack for using
report_start_event and report_finish_event easily with a context
handler.

It also modifies FinishReportingEvent (and finish_event) accordingly
to take a status rather than simply a boolean successful.  The intent
is that WARN is provided when a non-desireable result occurred but it
is non-fatal.

Change-Id: I978c76e429790036f8740d7eb7279e925a1e74d0
This commit is contained in:
Scott Moser 2015-08-04 07:29:22 -05:00
parent 628e1a2fb0
commit 735f0ac4ad
2 changed files with 272 additions and 22 deletions

View File

@ -23,6 +23,16 @@ DEFAULT_CONFIG = {
instantiated_handler_registry = DictRegistry()
class _nameset(set):
def __getattr__(self, name):
if name in self:
return name
raise AttributeError("%s not a valid value" % name)
status = _nameset(("SUCCESS", "WARN", "FAIL"))
class ReportingEvent(object):
"""Encapsulation of event formatting."""
@ -39,17 +49,16 @@ class ReportingEvent(object):
class FinishReportingEvent(ReportingEvent):
def __init__(self, name, description, successful=None):
def __init__(self, name, description, result=status.SUCCESS):
super(FinishReportingEvent, self).__init__(
FINISH_EVENT_TYPE, name, description)
self.successful = successful
self.result = result
if result not in status:
raise ValueError("Invalid result: %s" % result)
def as_string(self):
if self.successful is None:
return super(FinishReportingEvent, self).as_string()
success_string = 'success' if self.successful else 'fail'
return '{0}: {1}: {2}: {3}'.format(
self.event_type, self.name, success_string, self.description)
self.event_type, self.name, self.result, self.description)
def add_configuration(config):
@ -74,12 +83,13 @@ def report_event(event):
handler.publish_event(event)
def report_finish_event(event_name, event_description, successful=None):
def report_finish_event(event_name, event_description,
result=status.SUCCESS):
"""Report a "finish" event.
See :py:func:`.report_event` for parameter details.
"""
event = FinishReportingEvent(event_name, event_description, successful)
event = FinishReportingEvent(event_name, event_description, result)
return report_event(event)
@ -97,4 +107,111 @@ def report_start_event(event_name, event_description):
return report_event(event)
class ReportEventStack(object):
"""Context Manager for using :py:func:`report_event`
This enables calling :py:func:`report_start_event` and
:py:func:`report_finish_event` through a context manager.
:param name:
the name of the event
:param description:
the event's description, passed on to :py:func:`report_start_event`
:param message:
the description to use for the finish event. defaults to
:param:description.
:param parent:
:type parent: :py:class:ReportEventStack or None
The parent of this event. The parent is populated with
results of all its children. The name used in reporting
is <parent.name>/<name>
:param reporting_enabled:
Indicates if reporting events should be generated.
If not provided, defaults to the parent's value, or True if no parent
is provided.
:param result_on_exception:
The result value to set if an exception is caught. default
value is FAIL.
"""
def __init__(self, name, description, message=None, parent=None,
reporting_enabled=None, result_on_exception=status.FAIL):
self.parent = parent
self.name = name
self.description = description
self.message = message
self.result_on_exception = result_on_exception
self.result = status.SUCCESS
# use parents reporting value if not provided
if reporting_enabled is None:
if parent:
reporting_enabled = parent.reporting_enabled
else:
reporting_enabled = True
self.reporting_enabled = reporting_enabled
if parent:
self.fullname = '/'.join((parent.fullname, name,))
else:
self.fullname = self.name
self.children = {}
def __repr__(self):
return ("ReportEventStack(%s, %s, reporting_enabled=%s)" %
(self.name, self.description, self.reporting_enabled))
def __enter__(self):
self.result = status.SUCCESS
if self.reporting_enabled:
report_start_event(self.fullname, self.description)
if self.parent:
self.parent.children[self.name] = (None, None)
return self
def _childrens_finish_info(self):
for cand_result in (status.FAIL, status.WARN):
for name, (value, msg) in self.children.items():
if value == cand_result:
return (value, self.message)
return (self.result, self.message)
@property
def result(self):
return self._result
@result.setter
def result(self, value):
if value not in status:
raise ValueError("'%s' not a valid result" % value)
self._result = value
@property
def message(self):
if self._message is not None:
return self._message
return self.description
@message.setter
def message(self, value):
self._message = value
def _finish_info(self, exc):
# return tuple of description, and value
if exc:
return (self.result_on_exception, self.message)
return self._childrens_finish_info()
def __exit__(self, exc_type, exc_value, traceback):
(result, msg) = self._finish_info(exc_value)
if self.parent:
self.parent.children[self.name] = (result, msg)
if self.reporting_enabled:
report_finish_event(self.fullname, msg, result)
add_configuration(DEFAULT_CONFIG)

View File

@ -33,10 +33,10 @@ class TestReportStartEvent(TestCase):
class TestReportFinishEvent(TestCase):
def _report_finish_event(self, successful=None):
def _report_finish_event(self, result=reporting.status.SUCCESS):
event_name, event_description = 'my_test_event', 'my description'
reporting.report_finish_event(
event_name, event_description, successful=successful)
event_name, event_description, result=result)
return event_name, event_description
def assertHandlersPassedObjectWithAsString(
@ -52,7 +52,8 @@ class TestReportFinishEvent(TestCase):
self, instantiated_handler_registry):
event_name, event_description = self._report_finish_event()
expected_string_representation = ': '.join(
['finish', event_name, event_description])
['finish', event_name, reporting.status.SUCCESS,
event_description])
self.assertHandlersPassedObjectWithAsString(
instantiated_handler_registry.registered_items,
expected_string_representation)
@ -62,9 +63,10 @@ class TestReportFinishEvent(TestCase):
def test_reporting_successful_finish_has_sensible_string_repr(
self, instantiated_handler_registry):
event_name, event_description = self._report_finish_event(
successful=True)
result=reporting.status.SUCCESS)
expected_string_representation = ': '.join(
['finish', event_name, 'success', event_description])
['finish', event_name, reporting.status.SUCCESS,
event_description])
self.assertHandlersPassedObjectWithAsString(
instantiated_handler_registry.registered_items,
expected_string_representation)
@ -74,13 +76,16 @@ class TestReportFinishEvent(TestCase):
def test_reporting_unsuccessful_finish_has_sensible_string_repr(
self, instantiated_handler_registry):
event_name, event_description = self._report_finish_event(
successful=False)
result=reporting.status.FAIL)
expected_string_representation = ': '.join(
['finish', event_name, 'fail', event_description])
['finish', event_name, reporting.status.FAIL, event_description])
self.assertHandlersPassedObjectWithAsString(
instantiated_handler_registry.registered_items,
expected_string_representation)
def test_invalid_result_raises_attribute_error(self):
self.assertRaises(ValueError, self._report_finish_event, ("BOGUS",))
class TestReportingEvent(TestCase):
@ -102,26 +107,26 @@ class TestBaseReportingHandler(TestCase):
class TestLogHandler(TestCase):
@mock.patch.object(handlers.logging, 'getLogger')
@mock.patch.object(reporting.handlers.logging, 'getLogger')
def test_appropriate_logger_used(self, getLogger):
event_type, event_name = 'test_type', 'test_name'
event = reporting.ReportingEvent(event_type, event_name, 'description')
handlers.LogHandler().publish_event(event)
reporting.handlers.LogHandler().publish_event(event)
self.assertEqual(
[mock.call(
'cloudinit.reporting.{0}.{1}'.format(event_type, event_name))],
getLogger.call_args_list)
@mock.patch.object(handlers.logging, 'getLogger')
@mock.patch.object(reporting.handlers.logging, 'getLogger')
def test_single_log_message_at_info_published(self, getLogger):
event = reporting.ReportingEvent('type', 'name', 'description')
handlers.LogHandler().publish_event(event)
reporting.handlers.LogHandler().publish_event(event)
self.assertEqual(1, getLogger.return_value.info.call_count)
@mock.patch.object(handlers.logging, 'getLogger')
@mock.patch.object(reporting.handlers.logging, 'getLogger')
def test_log_message_uses_event_as_string(self, getLogger):
event = reporting.ReportingEvent('type', 'name', 'description')
handlers.LogHandler().publish_event(event)
reporting.handlers.LogHandler().publish_event(event)
self.assertIn(event.as_string(),
getLogger.return_value.info.call_args[0][0])
@ -132,7 +137,7 @@ class TestDefaultRegisteredHandler(TestCase):
registered_items = (
reporting.instantiated_handler_registry.registered_items)
for _, item in registered_items.items():
if isinstance(item, handlers.LogHandler):
if isinstance(item, reporting.handlers.LogHandler):
break
else:
self.fail('No reporting LogHandler registered by default.')
@ -192,3 +197,131 @@ class TestReportingConfiguration(TestCase):
expected_handler_config = handler_config.copy()
reporting.add_configuration({'my_test_handler': handler_config})
self.assertEqual(expected_handler_config, handler_config)
class TestReportingEventStack(TestCase):
@mock.patch('cloudinit.reporting.report_finish_event')
@mock.patch('cloudinit.reporting.report_start_event')
def test_start_and_finish_success(self, report_start, report_finish):
with reporting.ReportEventStack(name="myname", description="mydesc"):
pass
self.assertEqual(
[mock.call('myname', 'mydesc')], report_start.call_args_list)
self.assertEqual(
[mock.call('myname', 'mydesc', reporting.status.SUCCESS)],
report_finish.call_args_list)
@mock.patch('cloudinit.reporting.report_finish_event')
@mock.patch('cloudinit.reporting.report_start_event')
def test_finish_exception_defaults_fail(self, report_start, report_finish):
name = "myname"
desc = "mydesc"
try:
with reporting.ReportEventStack(name, description=desc):
raise ValueError("This didnt work")
except ValueError:
pass
self.assertEqual([mock.call(name, desc)], report_start.call_args_list)
self.assertEqual(
[mock.call(name, desc, reporting.status.FAIL)],
report_finish.call_args_list)
@mock.patch('cloudinit.reporting.report_finish_event')
@mock.patch('cloudinit.reporting.report_start_event')
def test_result_on_exception_used(self, report_start, report_finish):
name = "myname"
desc = "mydesc"
try:
with reporting.ReportEventStack(
name, desc, result_on_exception=reporting.status.WARN):
raise ValueError("This didnt work")
except ValueError:
pass
self.assertEqual([mock.call(name, desc)], report_start.call_args_list)
self.assertEqual(
[mock.call(name, desc, reporting.status.WARN)],
report_finish.call_args_list)
@mock.patch('cloudinit.reporting.report_start_event')
def test_child_fullname_respects_parent(self, report_start):
parent_name = "topname"
c1_name = "c1name"
c2_name = "c2name"
c2_expected_fullname = '/'.join([parent_name, c1_name, c2_name])
c1_expected_fullname = '/'.join([parent_name, c1_name])
parent = reporting.ReportEventStack(parent_name, "topdesc")
c1 = reporting.ReportEventStack(c1_name, "c1desc", parent=parent)
c2 = reporting.ReportEventStack(c2_name, "c2desc", parent=c1)
with c1:
report_start.assert_called_with(c1_expected_fullname, "c1desc")
with c2:
report_start.assert_called_with(c2_expected_fullname, "c2desc")
@mock.patch('cloudinit.reporting.report_finish_event')
@mock.patch('cloudinit.reporting.report_start_event')
def test_child_result_bubbles_up(self, report_start, report_finish):
parent = reporting.ReportEventStack("topname", "topdesc")
child = reporting.ReportEventStack("c_name", "c_desc", parent=parent)
with parent:
with child:
child.result = reporting.status.WARN
report_finish.assert_called_with(
"topname", "topdesc", reporting.status.WARN)
@mock.patch('cloudinit.reporting.report_finish_event')
def test_message_used_in_finish(self, report_finish):
with reporting.ReportEventStack("myname", "mydesc",
message="mymessage"):
pass
self.assertEqual(
[mock.call("myname", "mymessage", reporting.status.SUCCESS)],
report_finish.call_args_list)
@mock.patch('cloudinit.reporting.report_finish_event')
def test_message_updatable(self, report_finish):
with reporting.ReportEventStack("myname", "mydesc") as c:
c.message = "all good"
self.assertEqual(
[mock.call("myname", "all good", reporting.status.SUCCESS)],
report_finish.call_args_list)
@mock.patch('cloudinit.reporting.report_start_event')
@mock.patch('cloudinit.reporting.report_finish_event')
def test_reporting_disabled_does_not_report_events(
self, report_start, report_finish):
with reporting.ReportEventStack("a", "b", reporting_enabled=False):
pass
self.assertEqual(report_start.call_count, 0)
self.assertEqual(report_finish.call_count, 0)
@mock.patch('cloudinit.reporting.report_start_event')
@mock.patch('cloudinit.reporting.report_finish_event')
def test_reporting_child_default_to_parent(
self, report_start, report_finish):
parent = reporting.ReportEventStack(
"pname", "pdesc", reporting_enabled=False)
child = reporting.ReportEventStack("cname", "cdesc", parent=parent)
with parent:
with child:
pass
pass
self.assertEqual(report_start.call_count, 0)
self.assertEqual(report_finish.call_count, 0)
def test_reporting_event_has_sane_repr(self):
myrep = reporting.ReportEventStack("fooname", "foodesc",
reporting_enabled=True).__repr__()
self.assertIn("fooname", myrep)
self.assertIn("foodesc", myrep)
self.assertIn("True", myrep)
def test_set_invalid_result_raises_value_error(self):
f = reporting.ReportEventStack("myname", "mydesc")
self.assertRaises(ValueError, setattr, f, "result", "BOGUS")
class TestStatusAccess(TestCase):
def test_invalid_status_access_raises_value_error(self):
self.assertRaises(AttributeError, getattr, reporting.status, "BOGUS")