diff --git a/subiquity/server/controllers/reporting.py b/subiquity/server/controllers/reporting.py index 90ad1ee7..c4d8bdab 100644 --- a/subiquity/server/controllers/reporting.py +++ b/subiquity/server/controllers/reporting.py @@ -17,10 +17,17 @@ import copy 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 @@ class ReportingController(NonInteractiveController): 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 56ae4c19..aa692aa7 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 @@ class TestReportingController(SubiTestCase): ) 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 424fdea1..cdee8ff0 100644 --- a/subiquity/server/server.py +++ b/subiquity/server/server.py @@ -372,11 +372,14 @@ class SubiquityServer(Application): # - 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 @@ class SubiquityServer(Application): 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 @@ class SubiquityServer(Application): 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 d4303515..292980f5 100644 --- a/subiquity/server/tests/test_server.py +++ b/subiquity/server/tests/test_server.py @@ -378,3 +378,81 @@ class TestEventReporting(SubiTestCase): 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 07c94d71..bd1ca641 100644 --- a/subiquitycore/context.py +++ b/subiquitycore/context.py @@ -118,6 +118,15 @@ class Context: 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 748b97b5..a936b15f 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()