From 228ba9ce6283cacec19d23c1b4e30222f8a7c9c9 Mon Sep 17 00:00:00 2001 From: Massimiliano Date: Thu, 15 Feb 2024 16:26:41 +0100 Subject: [PATCH] Fix quit option for manual jobs (bugfix) (#989) * Fix quit * Add new ui quit metabox test Minor: renamed urwidui -> ui * Test RemoteController change * Test also remote assistant remember_users_response --- .../checkbox_ng/launcher/controller.py | 6 + .../checkbox_ng/launcher/test_controller.py | 148 +++++++++++++++++- .../plainbox/impl/session/remote_assistant.py | 5 + .../impl/session/test_remote_assistant.py | 30 ++++ .../scenarios/{urwid => ui}/__init__.py | 0 metabox/metabox/scenarios/ui/interact_jobs.py | 56 +++++++ .../scenarios/{urwid => ui}/testplan.py | 0 7 files changed, 244 insertions(+), 1 deletion(-) rename metabox/metabox/scenarios/{urwid => ui}/__init__.py (100%) create mode 100644 metabox/metabox/scenarios/ui/interact_jobs.py rename metabox/metabox/scenarios/{urwid => ui}/testplan.py (100%) diff --git a/checkbox-ng/checkbox_ng/launcher/controller.py b/checkbox-ng/checkbox_ng/launcher/controller.py index c3aacfd5d8..5df25e5e2e 100644 --- a/checkbox-ng/checkbox_ng/launcher/controller.py +++ b/checkbox-ng/checkbox_ng/launcher/controller.py @@ -726,6 +726,9 @@ def _run_jobs(self, jobs_repr, total_num=0): ) if cmd == "skip": next_job = True + elif cmd == "quit": + self.sa.remember_users_response(cmd) + raise SystemExit("Session saved, exiting...") self.sa.remember_users_response(cmd) self.wait_for_job(dont_finish=True) elif interaction.kind in "steps": @@ -738,6 +741,9 @@ def _run_jobs(self, jobs_repr, total_num=0): ) if cmd == "skip": next_job = True + elif cmd == "quit": + self.sa.remember_users_response(cmd) + raise SystemExit("Session saved, exiting...") self.sa.remember_users_response(cmd) elif interaction.kind == "verification": self.wait_for_job(dont_finish=True) diff --git a/checkbox-ng/checkbox_ng/launcher/test_controller.py b/checkbox-ng/checkbox_ng/launcher/test_controller.py index e5fb8f3c1d..5cafe9f747 100644 --- a/checkbox-ng/checkbox_ng/launcher/test_controller.py +++ b/checkbox-ng/checkbox_ng/launcher/test_controller.py @@ -252,6 +252,152 @@ def test_resume_or_start_new_session_interactive(self): self.assertTrue(self_mock.interactively_choose_tp.called) + @mock.patch("checkbox_ng.launcher.controller.SimpleUI") + def test__run_jobs_description_command_none(self, simple_ui_mock): + self_mock = mock.MagicMock() + interaction_mock = mock.MagicMock() + interaction_mock.kind = "description" + + self_mock.sa.run_job.return_value = [interaction_mock] + jobs_repr_mock = { + "id": "id_mock", + "command": None, + "num": 0, + "name": "name", + "category_name": "category", + } + simple_ui_mock().wait_for_interaction_prompt.return_value = "skip" + + RemoteController._run_jobs(self_mock, [jobs_repr_mock]) + + @mock.patch("checkbox_ng.launcher.controller.SimpleUI") + def test__run_jobs_description_skip(self, simple_ui_mock): + self_mock = mock.MagicMock() + interaction_mock = mock.MagicMock() + interaction_mock.kind = "description" + + self_mock.sa.run_job.return_value = [interaction_mock] + jobs_repr_mock = { + "id": "id_mock", + "command": "skip_description", + "num": 0, + "name": "name", + "category_name": "category", + } + simple_ui_mock().wait_for_interaction_prompt.return_value = "skip" + + RemoteController._run_jobs(self_mock, [jobs_repr_mock]) + + @mock.patch("checkbox_ng.launcher.controller.SimpleUI") + def test__run_jobs_description_enter(self, simple_ui_mock): + self_mock = mock.MagicMock() + interaction_mock = mock.MagicMock() + interaction_mock.kind = "description" + + self_mock.sa.run_job.return_value = [interaction_mock] + jobs_repr_mock = { + "id": "id_mock", + "command": "skip_description", + "num": 0, + "name": "name", + "category_name": "category", + } + simple_ui_mock().wait_for_interaction_prompt.return_value = "" + + RemoteController._run_jobs(self_mock, [jobs_repr_mock]) + + @mock.patch("checkbox_ng.launcher.controller.SimpleUI") + def test__run_jobs_description_quit(self, simple_ui_mock): + self_mock = mock.MagicMock() + interaction_mock = mock.MagicMock() + interaction_mock.kind = "description" + + self_mock.sa.run_job.return_value = [interaction_mock] + jobs_repr_mock = { + "id": "id_mock", + "command": "quit_description", + "num": 0, + "name": "name", + "category_name": "category", + } + simple_ui_mock().wait_for_interaction_prompt.return_value = "quit" + + with self.assertRaises(SystemExit): + RemoteController._run_jobs(self_mock, [jobs_repr_mock]) + + @mock.patch("checkbox_ng.launcher.controller.SimpleUI") + def test__run_jobs_steps_run(self, simple_ui_mock): + self_mock = mock.MagicMock() + interaction_mock = mock.MagicMock() + interaction_mock.kind = "steps" + + self_mock.sa.run_job.return_value = [interaction_mock] + jobs_repr_mock = { + "id": "id_mock", + "command": None, + "num": 0, + "name": "name", + "category_name": "category", + } + + RemoteController._run_jobs(self_mock, [jobs_repr_mock]) + + @mock.patch("checkbox_ng.launcher.controller.SimpleUI") + def test__run_jobs_steps_enter(self, simple_ui_mock): + self_mock = mock.MagicMock() + interaction_mock = mock.MagicMock() + interaction_mock.kind = "steps" + + self_mock.sa.run_job.return_value = [interaction_mock] + jobs_repr_mock = { + "id": "id_mock", + "command": "skip_description", + "num": 0, + "name": "name", + "category_name": "category", + } + simple_ui_mock().wait_for_interaction_prompt.return_value = "" + + RemoteController._run_jobs(self_mock, [jobs_repr_mock]) + + @mock.patch("checkbox_ng.launcher.controller.SimpleUI") + def test__run_jobs_steps_skip(self, simple_ui_mock): + self_mock = mock.MagicMock() + interaction_mock = mock.MagicMock() + interaction_mock.kind = "steps" + + self_mock.sa.run_job.return_value = [interaction_mock] + jobs_repr_mock = { + "id": "id_mock", + "command": "skip_description", + "num": 0, + "name": "name", + "category_name": "category", + } + simple_ui_mock().wait_for_interaction_prompt.return_value = "skip" + + RemoteController._run_jobs(self_mock, [jobs_repr_mock]) + + @mock.patch("checkbox_ng.launcher.controller.SimpleUI") + def test__run_jobs_steps_quit(self, simple_ui_mock): + self_mock = mock.MagicMock() + interaction_mock = mock.MagicMock() + interaction_mock.kind = "steps" + + self_mock.sa.run_job.return_value = [interaction_mock] + jobs_repr_mock = { + "id": "id_mock", + "command": "quit_description", + "num": 0, + "name": "name", + "category_name": "category", + } + simple_ui_mock().wait_for_interaction_prompt.return_value = "quit" + + with self.assertRaises(SystemExit): + RemoteController._run_jobs(self_mock, [jobs_repr_mock]) + + class IsHostnameALoopbackTests(TestCase): @mock.patch("socket.gethostbyname") @mock.patch("ipaddress.ip_address") @@ -294,4 +440,4 @@ def test_is_hostname_a_loopback_socket_raises(self, gethostbyname_mock): when the socket.gethostname function raises an exception """ gethostbyname_mock.side_effect = socket.gaierror - self.assertFalse(is_hostname_a_loopback("foobar")) \ No newline at end of file + self.assertFalse(is_hostname_a_loopback("foobar")) diff --git a/checkbox-ng/plainbox/impl/session/remote_assistant.py b/checkbox-ng/plainbox/impl/session/remote_assistant.py index c637f665e4..1785f67dda 100644 --- a/checkbox-ng/plainbox/impl/session/remote_assistant.py +++ b/checkbox-ng/plainbox/impl/session/remote_assistant.py @@ -213,6 +213,11 @@ def remember_users_response(self, response): self._current_comments = "" self._state = TestsSelected return + elif response == "quit": + self._last_response = response + self._state = Idle + self.finalize_session() + return self._last_response = response self._state = Running diff --git a/checkbox-ng/plainbox/impl/session/test_remote_assistant.py b/checkbox-ng/plainbox/impl/session/test_remote_assistant.py index 62a7b5eef9..760eeaa054 100644 --- a/checkbox-ng/plainbox/impl/session/test_remote_assistant.py +++ b/checkbox-ng/plainbox/impl/session/test_remote_assistant.py @@ -273,6 +273,36 @@ def test_resume_by_id_with_result_file_not_json(self, mock_load_configs): rsa._sa.use_job_result.assert_called_with(rsa._last_job, mjr, True) + def test_remember_users_response_quit(self): + self_mock = mock.MagicMock() + self_mock._state = remote_assistant.Interacting + + remote_assistant.RemoteSessionAssistant.remember_users_response( + self_mock, "quit" + ) + + self.assertEqual(self_mock._state, remote_assistant.Idle) + self.assertTrue(self_mock.finalize_session.called) + + def test_remember_users_response_rollback(self): + self_mock = mock.MagicMock() + self_mock._state = remote_assistant.Interacting + + remote_assistant.RemoteSessionAssistant.remember_users_response( + self_mock, "rollback" + ) + + self.assertEqual(self_mock._state, remote_assistant.TestsSelected) + + def test_remember_users_response_run(self): + self_mock = mock.MagicMock() + self_mock._state = remote_assistant.Interacting + + remote_assistant.RemoteSessionAssistant.remember_users_response( + self_mock, "run" + ) + + self.assertEqual(self_mock._state, remote_assistant.Running) class RemoteAssistantFinishJobTests(TestCase): def setUp(self): diff --git a/metabox/metabox/scenarios/urwid/__init__.py b/metabox/metabox/scenarios/ui/__init__.py similarity index 100% rename from metabox/metabox/scenarios/urwid/__init__.py rename to metabox/metabox/scenarios/ui/__init__.py diff --git a/metabox/metabox/scenarios/ui/interact_jobs.py b/metabox/metabox/scenarios/ui/interact_jobs.py new file mode 100644 index 0000000000..f8dd646ec9 --- /dev/null +++ b/metabox/metabox/scenarios/ui/interact_jobs.py @@ -0,0 +1,56 @@ +# This file is part of Checkbox. +# +# Copyright 2024 Canonical Ltd. +# Written by: +# Massimiliano Girardi +# +# Checkbox is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3, +# as published by the Free Software Foundation. + +# +# Checkbox is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Checkbox. If not, see . +import textwrap + +import metabox.core.keys as keys +from metabox.core.actions import ( + Expect, + Send, + Start, + ExpectNot +) +from metabox.core.scenario import Scenario +from metabox.core.utils import tag + + +@tag("manual", "interact") +class ManualInteractQuit(Scenario): + launcher = textwrap.dedent( + """ + [launcher] + launcher_version = 1 + stock_reports = text + [test plan] + unit = 2021.com.canonical.certification::cert-blocker-manual-resume + forced = yes + [test selection] + forced = yes + """ + ) + + steps = [ + Start(), + Expect("Pick an action"), + Send("p" + keys.KEY_ENTER), + Expect("save the session and quit"), + Send("q" + keys.KEY_ENTER), + # if q is pressed, checkbox should exit instead of going ahead printing + # results + ExpectNot("Results") + ] diff --git a/metabox/metabox/scenarios/urwid/testplan.py b/metabox/metabox/scenarios/ui/testplan.py similarity index 100% rename from metabox/metabox/scenarios/urwid/testplan.py rename to metabox/metabox/scenarios/ui/testplan.py