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:
parent
628e1a2fb0
commit
735f0ac4ad
@ -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)
|
||||
|
@ -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")
|
||||
|
Loading…
x
Reference in New Issue
Block a user