From 980411a67043a0c5fffefc22d07b6db8e83fb3b3 Mon Sep 17 00:00:00 2001 From: Chris Peterson Date: Wed, 20 Mar 2024 16:03:11 -0700 Subject: [PATCH] reporting: add new logging type events Adds new reporting type events "INFO", "WARNING", and "ERROR" to be used for context logging. These can be invoked with the new `.info`, `.warning`, and `.error` methods on the context object accordingly. Useful for things like warning/errors on autoinstall configuartions. --- subiquity/server/controllers/reporting.py | 24 ++++- .../controllers/tests/test_reporting.py | 88 +++++++++++++++++++ subiquity/server/server.py | 22 ++++- subiquity/server/tests/test_server.py | 78 ++++++++++++++++ subiquitycore/context.py | 9 ++ subiquitycore/tests/mocks.py | 1 + 6 files changed, 218 insertions(+), 4 deletions(-) diff --git a/subiquity/server/controllers/reporting.py b/subiquity/server/controllers/reporting.py index 90ad1ee70..c4d8bdab5 100644 --- a/subiquity/server/controllers/reporting.py +++ b/subiquity/server/controllers/reporting.py @@ -17,10 +17,17 @@ import logging from curtin.reporter import available_handlers, update_configuration -from curtin.reporter.events import report_finish_event, report_start_event, status +from curtin.reporter.events import ( + ReportingEvent, + report_event, + report_finish_event, + report_start_event, + status, +) from curtin.reporter.handlers import LogHandler as CurtinLogHandler from subiquity.server.controller import NonInteractiveController +from subiquitycore.context import Context class LogHandler(CurtinLogHandler): @@ -76,3 +83,18 @@ def report_finish_event(self, context, description, result): report_finish_event( context.full_name(), description, result, level=context.level ) + + def report_info_event(self, context: Context, message: str): + """Report an "info" event.""" + event = ReportingEvent("info", context.full_name(), message, level="INFO") + report_event(event) + + def report_warning_event(self, context: Context, message: str): + """Report a "warning" event.""" + event = ReportingEvent("warning", context.full_name(), message, level="WARNING") + report_event(event) + + def report_error_event(self, context: Context, message: str): + """Report an "error" event.""" + event = ReportingEvent("error", context.full_name(), message, level="ERROR") + report_event(event) diff --git a/subiquity/server/controllers/tests/test_reporting.py b/subiquity/server/controllers/tests/test_reporting.py index 56ae4c196..aa692aa7f 100644 --- a/subiquity/server/controllers/tests/test_reporting.py +++ b/subiquity/server/controllers/tests/test_reporting.py @@ -13,11 +13,17 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from unittest.mock import Mock, patch + import jsonschema +from curtin.reporter.events import status as CurtinStatus from jsonschema.validators import validator_for from subiquity.server.controllers.reporting import ReportingController +from subiquitycore.context import Context +from subiquitycore.context import Status as ContextStatus from subiquitycore.tests import SubiTestCase +from subiquitycore.tests.mocks import MockedApplication, make_app class TestReportingController(SubiTestCase): @@ -29,3 +35,85 @@ def test_valid_schema(self): ) JsonValidator.check_schema(ReportingController.autoinstall_schema) + + +@patch("subiquity.server.controllers.reporting.report_event") +class TestReportingCurtinCalls(SubiTestCase): + def setUp(self): + app: MockedApplication = make_app() + self.controller: ReportingController = ReportingController(app) + self.context: Context = app.context + + @patch("subiquity.server.controllers.reporting.report_start_event") + def test_start_event(self, report_start_event, report_event): + self.controller.report_start_event(self.context, "description") + + # Calls specific start event method + report_start_event.assert_called_with( + self.context.full_name(), "description", level=self.context.level + ) + + # Not the generic one + report_event.assert_not_called() + + @patch("subiquity.server.controllers.reporting.report_finish_event") + def test_finish_event(self, report_finish_event, report_event): + self.controller.report_finish_event( + self.context, "description", ContextStatus.FAIL + ) + + # Calls specific finish event method + report_finish_event.assert_called_with( + self.context.full_name(), + "description", + CurtinStatus.FAIL, + level=self.context.level, + ) + + # Not the generic one + report_event.assert_not_called() + + # Test default WARN + status = Mock() + status.name = "NEW LEVEL" + self.controller.report_finish_event(self.context, "description", status) + + report_finish_event.assert_called_with( + self.context.full_name(), + "description", + CurtinStatus.WARN, + level=self.context.level, + ) + + @patch("subiquity.server.controllers.reporting.ReportingEvent") + def test_info_event(self, mock_class, report_event): + self.controller.report_info_event(self.context, "description") + + mock_class.assert_called_with( + "info", + self.context.full_name(), + "description", + level="INFO", + ) + + @patch("subiquity.server.controllers.reporting.ReportingEvent") + def test_warning_event(self, mock_class, report_event): + self.controller.report_warning_event(self.context, "description") + + mock_class.assert_called_with( + "warning", + self.context.full_name(), + "description", + level="WARNING", + ) + + @patch("subiquity.server.controllers.reporting.ReportingEvent") + def test_error_event(self, mock_class, report_event): + self.controller.report_error_event(self.context, "description") + + mock_class.assert_called_with( + "error", + self.context.full_name(), + "description", + level="ERROR", + ) diff --git a/subiquity/server/server.py b/subiquity/server/server.py index 424fdea11..cdee8ff0f 100644 --- a/subiquity/server/server.py +++ b/subiquity/server/server.py @@ -372,11 +372,14 @@ def _maybe_push_to_journal( # - special sections of the install, which set "is-install-context" # where we want to report the event anyways # + # - special event types: + # - warn + # - error # # For non-interactive installs (i.e., full autoinstall) we report # everything. - force_reporting: bool = install_context + force_reporting: bool = install_context or event_type in ["warning", "error"] # self.interactive=None could be an interactive install, we just # haven't found out yet @@ -388,8 +391,6 @@ def _maybe_push_to_journal( if controller is None or controller.interactive(): return - # Otherwise it came from the server - # Create the message out of the name of the reporter and optionally # the description name: str = context.full_name() @@ -432,6 +433,21 @@ def report_finish_event(self, context, description, status): listener.report_finish_event(context, description, status) self._maybe_push_to_journal("finish", context, description) + def report_info_event(self, context: Context, message: str) -> None: + for listener in self.event_listeners: + listener.report_info_event(context, message) + self._maybe_push_to_journal("info", context, message) + + def report_warning_event(self, context: Context, message: str) -> None: + for listener in self.event_listeners: + listener.report_warning_event(context, message) + self._maybe_push_to_journal("warning", context, message) + + def report_error_event(self, context: Context, message: str) -> None: + for listener in self.event_listeners: + listener.report_error_event(context, message) + self._maybe_push_to_journal("error", context, message) + @property def state(self): return self._state diff --git a/subiquity/server/tests/test_server.py b/subiquity/server/tests/test_server.py index d43035151..292980f57 100644 --- a/subiquity/server/tests/test_server.py +++ b/subiquity/server/tests/test_server.py @@ -378,3 +378,81 @@ async def test_maybe_push_to_journal( journal_send_mock.assert_called_once() else: journal_send_mock.assert_not_called() + + @parameterized.expand( + ( + # interactive, pushed to journal + (True, False), + (None, False), + (False, True), + ) + ) + def test_push_info_events(self, interactive, expect_pushed): + """Test info event publication""" + + context: Context = Context( + self.server, "MockContext", "description", None, "INFO" + ) + self.server.interactive = interactive + + with patch("subiquity.server.server.journal.send") as journal_send_mock: + self.server.report_info_event(context, "message") + + if not expect_pushed: + journal_send_mock.assert_not_called() + else: + journal_send_mock.assert_called_once() + # message is the only positional argument + (message,) = journal_send_mock.call_args.args + self.assertIn("message", message) + self.assertNotIn("description", message) + + @parameterized.expand( + ( + # interactive + (True,), + (None,), + (False,), + ) + ) + def test_push_warning_events(self, interactive): + """Test warning event publication""" + + context: Context = Context( + self.server, "MockContext", "description", None, "INFO" + ) + self.server.interactive = interactive + + with patch("subiquity.server.server.journal.send") as journal_send_mock: + self.server.report_warning_event(context, "message") + + journal_send_mock.assert_called_once() + # message is the only positional argument + (message,) = journal_send_mock.call_args.args + self.assertIn("message", message) + self.assertNotIn("description", message) + + @parameterized.expand( + ( + # interactive + (True,), + (None,), + (False,), + ) + ) + def test_push_error_events(self, interactive): + """Test error event publication""" + + context: Context = Context( + self.server, "MockContext", "description", None, "INFO" + ) + self.server.interactive = interactive + + with patch("subiquity.server.server.journal.send") as journal_send_mock: + self.server.report_error_event(context, "message") + + journal_send_mock.assert_called_once() + # message is the only positional argument + (message,) = journal_send_mock.call_args.args + self.assertIn("message", message) + self.assertNotIn("description", message) diff --git a/subiquitycore/context.py b/subiquitycore/context.py index 07c94d71c..bd1ca641a 100644 --- a/subiquitycore/context.py +++ b/subiquitycore/context.py @@ -118,6 +118,15 @@ def get(self, key, default=None): c = c.parent return default + def info(self, message: str) -> None: + self.app.report_info_event(self, message) + + def warning(self, message: str) -> None: + self.app.report_warning_event(self, message) + + def error(self, message: str) -> None: + self.app.report_error_event(self, message) + def with_context(name=None, description="", **context_kw): def decorate(meth): diff --git a/subiquitycore/tests/mocks.py b/subiquitycore/tests/mocks.py index 748b97b55..a936b15f9 100644 --- a/subiquitycore/tests/mocks.py +++ b/subiquitycore/tests/mocks.py @@ -36,6 +36,7 @@ def make_app(model=None): app.base_model = model else: app.base_model = mock.Mock() + app.add_event_listener = mock.Mock() app.controllers = mock.Mock() app.context = Context.new(app) app.exit = mock.Mock()