From 1d0335499ea63a938c7c5f8ea3e614e10cbd1085 Mon Sep 17 00:00:00 2001 From: Pierre Equoy Date: Mon, 11 Mar 2024 15:12:43 +0800 Subject: [PATCH 1/4] Update comment in qualifier.select_jobs() function --- checkbox-ng/plainbox/impl/secure/qualifiers.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/checkbox-ng/plainbox/impl/secure/qualifiers.py b/checkbox-ng/plainbox/impl/secure/qualifiers.py index 7c4ab1f7cf..63b8d1df57 100644 --- a/checkbox-ng/plainbox/impl/secure/qualifiers.py +++ b/checkbox-ng/plainbox/impl/secure/qualifiers.py @@ -516,13 +516,12 @@ def _handle_vote(qualifier, job): excluded_set.add(job) for qualifier in flat_qualifier_list: + # optimize the super-common case where a qualifier refers to + # a specific job if (isinstance(qualifier, FieldQualifier) and qualifier.field == 'id' and isinstance(qualifier.matcher, OperatorMatcher) and qualifier.matcher.op == operator.eq): - # optimize the super-common case where a qualifier refers to - # a specific job by using the id_to_index_map to instantly - # perform the requested operation on a single job for job in job_list: if job.id == qualifier.matcher.value: _handle_vote(qualifier, job) From ff1f13da2d880f3d67f017219fddf81fa8238c5f Mon Sep 17 00:00:00 2001 From: Pierre Equoy Date: Mon, 11 Mar 2024 15:16:45 +0800 Subject: [PATCH 2/4] Introduce "list-testplan" subcommand The new subcommand works similarly to "list-bootstrapped" since it also bootstraps a given test plan. However, it filters out the instantiated jobs and replaces them with their original template. In the end, a list of jobs and templates used in this test plan are returned as JSON-formatted data. This is to be used by external tools, for instance to create a document listing information (summary, description...) about executed jobs and templates. Fix CHECKBOX-1263 --- .../checkbox_ng/launcher/checkbox_cli.py | 2 + .../checkbox_ng/launcher/subcommands.py | 42 ++++++++++ .../checkbox_ng/launcher/test_subcommands.py | 83 +++++++++++++++++++ 3 files changed, 127 insertions(+) diff --git a/checkbox-ng/checkbox_ng/launcher/checkbox_cli.py b/checkbox-ng/checkbox_ng/launcher/checkbox_cli.py index c16ab3fa63..ba7c6bb64e 100644 --- a/checkbox-ng/checkbox_ng/launcher/checkbox_cli.py +++ b/checkbox-ng/checkbox_ng/launcher/checkbox_cli.py @@ -37,6 +37,7 @@ StartProvider, Submit, ListBootstrapped, + ListTestplan, TestPlanExport, Show, ) @@ -72,6 +73,7 @@ def main(): "submit": Submit, "show": Show, "list-bootstrapped": ListBootstrapped, + "list-testplan": ListTestplan, "merge-reports": MergeReports, "merge-submissions": MergeSubmissions, "tp-export": TestPlanExport, diff --git a/checkbox-ng/checkbox_ng/launcher/subcommands.py b/checkbox-ng/checkbox_ng/launcher/subcommands.py index b137589a73..3145755374 100644 --- a/checkbox-ng/checkbox_ng/launcher/subcommands.py +++ b/checkbox-ng/checkbox_ng/launcher/subcommands.py @@ -1333,6 +1333,48 @@ def __missing__(self, key): print(job_id) +class ListTestplan: + @property + def sa(self): + return self.ctx.sa + + def register_arguments(self, parser): + parser.description = ( + "Outputs the list of jobs and templates that will be run by a " + "given test plan as JSON. Instantiated jobs are ignored." + ) + parser.add_argument("TEST_PLAN", help=_("test plan id to use")) + + def invoked(self, ctx): + self.ctx = ctx + self.sa.start_new_session("checkbox-listing-ephemeral") + tps = self.sa.get_test_plans() + if ctx.args.TEST_PLAN not in tps: + raise SystemExit("Test plan not found") + self.sa.select_test_plan(ctx.args.TEST_PLAN) + self.sa.bootstrap() + job_and_template_list = [] + all_template_map = { + unit.template_id: unit + for unit in self.sa._context.state.unit_list + if unit.unit == "template" + } + for job in self.sa.get_static_todo_list(): + job_unit = self.sa.get_job(job) + if job_unit.template_id: + template_unit = all_template_map[job_unit.template_id] + attrs = template_unit._raw_data.copy() + else: + attrs = job_unit._raw_data.copy() + attrs["unit"] = "job" + attrs["certification_status"] = self.ctx.sa.get_job_state( + job + ).effective_certification_status + if attrs not in job_and_template_list: + job_and_template_list.append(attrs) + print(json.dumps(job_and_template_list)) + + class TestPlanExport: @property def sa(self): diff --git a/checkbox-ng/checkbox_ng/launcher/test_subcommands.py b/checkbox-ng/checkbox_ng/launcher/test_subcommands.py index 7c01e0e715..01bb27bf31 100644 --- a/checkbox-ng/checkbox_ng/launcher/test_subcommands.py +++ b/checkbox-ng/checkbox_ng/launcher/test_subcommands.py @@ -27,12 +27,14 @@ from checkbox_ng.launcher.subcommands import ( Launcher, ListBootstrapped, + ListTestplan, IncompatibleJobError, ResumeInstead, IJobResult, request_comment, generate_resume_candidate_description, ) +from plainbox.impl.unit.template import TemplateUnit class TestLauncher(TestCase): @@ -682,6 +684,87 @@ def test_invoke_print_output_customized_format(self, stdout): self.assertEqual(stdout.getvalue(), expected_out) +class TestListTestplan(TestCase): + maxDiff = None + def setUp(self): + self.launcher = ListTestplan() + self.ctx = Mock() + self.ctx.args = Mock(TEST_PLAN="", format="") + test_template = TemplateUnit({ + "template-id": "test-template", + "id": "test-{res}", + }) + self.ctx.sa = Mock( + start_new_session=Mock(), + get_test_plans=Mock(return_value=["test-plan1", "test-plan2"]), + select_test_plan=Mock(), + bootstrap=Mock(), + get_static_todo_list=Mock(return_value=["test-job1", "test-job2"]), + get_job=Mock( + side_effect=[ + Mock( + _raw_data={ + "id": "namespace1::test-job1", + "summary": "fake-job1", + "plugin": "manual", + "description": "fake-description1", + "certification_status": "unspecified", + }, + id="namespace1::test-job1", + partial_id="test-job1", + template_id="test-template", + ), + Mock( + _raw_data={ + "id": "namespace2::test-job2", + "summary": "fake-job2", + "plugin": "shell", + "command": "ls", + "certification_status": "unspecified", + }, + id="namespace2::test-job2", + partial_id="test-job2", + template_id=None, + ), + ] + ), + get_job_state=Mock( + return_value=Mock(effective_certification_status="blocker") + ), + get_resumable_sessions=Mock(return_value=[]), + get_dynamic_todo_list=Mock(return_value=[]), + _context=Mock( + state=Mock( + unit_list=[test_template,] + ), + ), + ) + + def test_invoke_test_plan_not_found(self): + self.ctx.args.TEST_PLAN = "test-plan3" + + with self.assertRaisesRegex(SystemExit, "Test plan not found"): + self.launcher.invoked(self.ctx) + + @patch("sys.stdout", new_callable=StringIO) + def test_invoke_print_output_standard_format(self, stdout): + self.ctx.args.TEST_PLAN = "test-plan1" + + expected_out = ( + '[{"template-id": "test-template", ' + '"id": "test-{res}", ' + '"certification_status": "blocker"}, ' + '{"id": "namespace2::test-job2", ' + '"summary": "fake-job2", ' + '"plugin": "shell", ' + '"command": "ls", ' + '"certification_status": "blocker", ' + '"unit": "job"}]\n' + ) + self.launcher.invoked(self.ctx) + self.assertEqual(stdout.getvalue(), expected_out) + + class TestUtilsFunctions(TestCase): @patch("checkbox_ng.launcher.subcommands.Colorizer", new=MagicMock()) @patch("builtins.print") From 9c642faeb18707a5fd317a55c1a8a201eddf3b7f Mon Sep 17 00:00:00 2001 From: Pierre Equoy Date: Mon, 11 Mar 2024 15:48:08 +0800 Subject: [PATCH 3/4] Update ListTestplan unit test --- .../checkbox_ng/launcher/test_subcommands.py | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/checkbox-ng/checkbox_ng/launcher/test_subcommands.py b/checkbox-ng/checkbox_ng/launcher/test_subcommands.py index 01bb27bf31..a4aca2a5c0 100644 --- a/checkbox-ng/checkbox_ng/launcher/test_subcommands.py +++ b/checkbox-ng/checkbox_ng/launcher/test_subcommands.py @@ -693,6 +693,7 @@ def setUp(self): test_template = TemplateUnit({ "template-id": "test-template", "id": "test-{res}", + "template-summary": "Test Template Summary", }) self.ctx.sa = Mock( start_new_session=Mock(), @@ -747,22 +748,11 @@ def test_invoke_test_plan_not_found(self): self.launcher.invoked(self.ctx) @patch("sys.stdout", new_callable=StringIO) - def test_invoke_print_output_standard_format(self, stdout): + def test_invoke_print_output(self, stdout): self.ctx.args.TEST_PLAN = "test-plan1" - expected_out = ( - '[{"template-id": "test-template", ' - '"id": "test-{res}", ' - '"certification_status": "blocker"}, ' - '{"id": "namespace2::test-job2", ' - '"summary": "fake-job2", ' - '"plugin": "shell", ' - '"command": "ls", ' - '"certification_status": "blocker", ' - '"unit": "job"}]\n' - ) self.launcher.invoked(self.ctx) - self.assertEqual(stdout.getvalue(), expected_out) + self.assertIn("Test Template Summary", stdout.getvalue()) class TestUtilsFunctions(TestCase): From c40315e73ee3f1f738aa1810eccbf15b8b3b0f4e Mon Sep 17 00:00:00 2001 From: Pierre Equoy Date: Mon, 11 Mar 2024 17:42:30 +0800 Subject: [PATCH 4/4] Add unit test for ListTestplan.register_arguments() --- checkbox-ng/checkbox_ng/launcher/test_subcommands.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/checkbox-ng/checkbox_ng/launcher/test_subcommands.py b/checkbox-ng/checkbox_ng/launcher/test_subcommands.py index a4aca2a5c0..ef37747703 100644 --- a/checkbox-ng/checkbox_ng/launcher/test_subcommands.py +++ b/checkbox-ng/checkbox_ng/launcher/test_subcommands.py @@ -685,7 +685,6 @@ def test_invoke_print_output_customized_format(self, stdout): class TestListTestplan(TestCase): - maxDiff = None def setUp(self): self.launcher = ListTestplan() self.ctx = Mock() @@ -741,6 +740,11 @@ def setUp(self): ), ) + def test_register_arguments(self): + parser_mock = Mock() + self.launcher.register_arguments(parser_mock) + self.assertTrue(parser_mock.add_argument.called) + def test_invoke_test_plan_not_found(self): self.ctx.args.TEST_PLAN = "test-plan3"