From 56ebeb706be64326d9d24d77454d1d780ada0f8b Mon Sep 17 00:00:00 2001 From: JWM Date: Wed, 21 Aug 2024 11:27:56 +0200 Subject: [PATCH 01/25] Use snapshot if defined --- example/.env.example | 1 + example/conf.py | 1 + mlx/coverity.py | 14 +++++++++++--- mlx/coverity_services.py | 25 ++++++++++++++++++------- 4 files changed, 31 insertions(+), 10 deletions(-) diff --git a/example/.env.example b/example/.env.example index 591d2cbb..c8cea9fe 100644 --- a/example/.env.example +++ b/example/.env.example @@ -1,4 +1,5 @@ COVERITY_USERNAME = 'yourusername' COVERITY_PASSWORD = 'yourpassword' COVERITY_STREAM = 'yourstream' +COVERITY_SNAPSHOT = '' diff --git a/example/conf.py b/example/conf.py index 8033ffa4..4b881d4b 100644 --- a/example/conf.py +++ b/example/conf.py @@ -313,6 +313,7 @@ "username": config("COVERITY_USERNAME"), "password": config("COVERITY_PASSWORD"), "stream": config("COVERITY_STREAM"), + "snapshot": config("COVERITY_SNAPSHOT") } TRACEABILITY_ITEM_ID_REGEX = r"([A-Z_]+-[A-Z0-9_]+)" diff --git a/mlx/coverity.py b/mlx/coverity.py index 5e198246..887c00e7 100644 --- a/mlx/coverity.py +++ b/mlx/coverity.py @@ -46,7 +46,7 @@ def initialize_environment(self, app): \\makeatother""" self.stream = app.config.coverity_credentials["stream"] - + self.snaphsot = app.config.coverity_credentials["snapshot"] # Login to Coverity and obtain stream information try: self.input_credentials(app.config.coverity_credentials) @@ -61,6 +61,10 @@ def initialize_environment(self, app): report_info("Verify the given stream name... ", True) self.coverity_service.validate_stream(self.stream) report_info("done") + if self.snaphsot: + report_info("Verify the given snapshot ID and obtain all enabled checkers... ", True) + self.coverity_service.validate_snapshot(self.snaphsot) + report_info("done") # Get all column keys report_info("obtaining all column keys... ", True) self.coverity_service.retrieve_column_keys() @@ -100,7 +104,11 @@ def process_coverity_nodes(self, app, doctree, fromdocname): # Get items from server try: defects = self.get_filtered_defects(node) - node.perform_replacement(defects, self, app, fromdocname) + if defects["totalRows"] == -1: + error_message = "There are no defects with the specified filters" + report_warning(error_message, fromdocname, lineno=node["line"]) + else: + node.perform_replacement(defects, self, app, fromdocname) except (URLError, AttributeError, Exception) as err: # pylint: disable=broad-except error_message = f"failed to process coverity-list with {err!r}" report_warning(error_message, fromdocname, lineno=node["line"]) @@ -142,7 +150,7 @@ def get_filtered_defects(self, node): column_names = set(node["col"]) if "chart_attribute" in node and node["chart_attribute"].upper() in node.column_map: column_names.add(node["chart_attribute"]) - defects = self.coverity_service.get_defects(self.stream, node["filters"], column_names) + defects = self.coverity_service.get_defects(self.stream, self.snaphsot, node["filters"], column_names) report_info("%d received" % (defects["totalRows"])) report_info("building defects table and/or chart... ", True) return defects diff --git a/mlx/coverity_services.py b/mlx/coverity_services.py index 498dbad7..c298e8b4 100644 --- a/mlx/coverity_services.py +++ b/mlx/coverity_services.py @@ -125,6 +125,16 @@ def validate_stream(self, stream): url = f"{self.api_endpoint}/streams/{stream}" self._request(url) + def validate_snapshot(self, snapshot): + """Validate snapshot by retrieving the specified snapshot. + When the request fails, the snapshot does not exist or the user does not have acces to it. + + Args: + snapshot (str): The snapshot ID + """ + url = f"{self.api_endpoint}/snapshots/{snapshot}" + self._request(url) + def retrieve_issues(self, filters): """Retrieve issues from the server (Coverity Connect). @@ -236,14 +246,16 @@ def assemble_query_filter(self, column_name, filter_values, matcher_type): "matchers": matchers } - def get_defects(self, stream, filters, column_names): - """Gets a list of defects for given stream, filters and column names. + def get_defects(self, stream, snapshot, filters, column_names): + """Gets a list of defects for given stream, snapshot ID, filters and column names. + If the snapshot is empty, the last snapshot is taken. If a column name does not match the name of the `columns` property, the column can not be obtained because it need the correct corresponding column key. Column key `cid` is always obtained to use later in other functions. Args: stream (str): Name of the stream to query + snapshot (str): The snapshot ID; If empty the last snapshot is taken. filters (dict): Dictionary with attribute names as keys and CSV lists of attribute values to query as values column_names (list[str]): The column names @@ -291,16 +303,15 @@ def get_defects(self, stream, filters, column_names): if (filter := filters["component"]) and (filter_values := self.handle_component_filter(filter)): query_filters.append(self.assemble_query_filter("Component", filter_values, "nameMatcher")) + scope = "last()" + if snapshot: + scope = snapshot data = { "filters": query_filters, "columns": list(self.column_keys(column_names)), "snapshotScope": { "show": { - "scope": "last()", - "includeOutdatedSnapshots": False - }, - "compareTo": { - "scope": "last()", + "scope": scope, "includeOutdatedSnapshots": False } } From a45550e78a666cf4ebad3eccd1332877ada016ac Mon Sep 17 00:00:00 2001 From: JWM Date: Wed, 21 Aug 2024 11:45:44 +0200 Subject: [PATCH 02/25] Test get_defects with snapshot ID --- tests/filters.py | 58 ++++++++++++++++++++++++++++++++++++------ tests/test_coverity.py | 46 ++++++++++++++++++++++++++++++--- 2 files changed, 93 insertions(+), 11 deletions(-) diff --git a/tests/filters.py b/tests/filters.py index b1b6dcac..89f0d293 100644 --- a/tests/filters.py +++ b/tests/filters.py @@ -27,8 +27,7 @@ ], "columns": ["cid"], "snapshotScope": { - "show": {"scope": "last()", "includeOutdatedSnapshots": False}, - "compareTo": {"scope": "last()", "includeOutdatedSnapshots": False}, + "show": {"scope": "last()", "includeOutdatedSnapshots": False} }, }, ) @@ -74,8 +73,7 @@ ], "columns": ["cid", "checker", "lastTriageComment", "classification"], "snapshotScope": { - "show": {"scope": "last()", "includeOutdatedSnapshots": False}, - "compareTo": {"scope": "last()", "includeOutdatedSnapshots": False}, + "show": {"scope": "last()", "includeOutdatedSnapshots": False} }, }, ) @@ -102,8 +100,7 @@ ], "columns": ["status", "cid", "checker", "lastTriageComment"], "snapshotScope": { - "show": {"scope": "last()", "includeOutdatedSnapshots": False}, - "compareTo": {"scope": "last()", "includeOutdatedSnapshots": False}, + "show": {"scope": "last()", "includeOutdatedSnapshots": False} }, }, ) @@ -135,8 +132,53 @@ ], "columns": ["cid", "classification", "action"], "snapshotScope": { - "show": {"scope": "last()", "includeOutdatedSnapshots": False}, - "compareTo": {"scope": "last()", "includeOutdatedSnapshots": False}, + "show": {"scope": "last()", "includeOutdatedSnapshots": False} + }, + }, +) + +test_snapshot = Filter( + { + "checker": "MISRA", + "impact": None, + "kind": None, + "classification": "Intentional,Bug,Pending,Unclassified", + "action": None, + "component": None, + "cwe": None, + "cid": None, + }, + ["CID", "Classification", "Checker", "Comment"], + { + "filters": [ + { + "columnKey": "streams", + "matchMode": "oneOrMoreMatch", + "matchers": [{"class": "Stream", "name": "test_stream", "type": "nameMatcher"}], + }, + { + "columnKey": "checker", + "matchMode": "oneOrMoreMatch", + "matchers": [ + {"type": "keyMatcher", "key": "MISRA 2"}, + {"type": "keyMatcher", "key": "MISRA 1"}, + {"type": "keyMatcher", "key": "MISRA 3"}, + ], + }, + { + "columnKey": "classification", + "matchMode": "oneOrMoreMatch", + "matchers": [ + {"type": "keyMatcher", "key": "Bug"}, + {"type": "keyMatcher", "key": "Pending"}, + {"type": "keyMatcher", "key": "Unclassified"}, + {"type": "keyMatcher", "key": "Intentional"}, + ], + }, + ], + "columns": ["cid", "checker", "lastTriageComment", "classification"], + "snapshotScope": { + "show": {"scope": "123", "includeOutdatedSnapshots": False} }, }, ) diff --git a/tests/test_coverity.py b/tests/test_coverity.py index e28b8d4a..056134a6 100644 --- a/tests/test_coverity.py +++ b/tests/test_coverity.py @@ -11,7 +11,11 @@ from mlx.coverity import SphinxCoverityConnector from mlx.coverity_services import CoverityDefectService -from .filters import test_defect_filter_0, test_defect_filter_1, test_defect_filter_2, test_defect_filter_3 +from .filters import (test_defect_filter_0, + test_defect_filter_1, + test_defect_filter_2, + test_defect_filter_3, + test_snapshot) TEST_FOLDER = Path(__file__).parent @@ -158,11 +162,43 @@ def test_get_defects(self, filters, column_names, request_data): coverity_service.retrieve_column_keys() # Get defects with patch.object(CoverityDefectService, "retrieve_issues") as mock_method: - coverity_service.get_defects(self.fake_stream, filters, column_names) + coverity_service.get_defects(self.fake_stream, "", filters, column_names) data = mock_method.call_args[0][0] mock_method.assert_called_once() assert ordered(data) == ordered(request_data) + def test_get_defects_with_snapshot(self): + with open(f"{TEST_FOLDER}/columns_keys.json", "r") as content: + column_keys = json.loads(content.read()) + self.fake_checkers = { + "checkerAttribute": {"name": "checker", "displayName": "Checker"}, + "checkerAttributedata": [ + {"key": "MISRA 1", "value": "MISRA 1"}, + {"key": "MISRA 2", "value": "MISRA 2"}, + {"key": "MISRA 3", "value": "MISRA 3"}, + {"key": "CHECKER 1", "value": "CHECKER 1"}, + {"key": "CHECKER 2", "value": "CHECKER 2"} + ], + } + self.fake_stream = "test_stream" + # initialize what needed for the REST API + coverity_service = self.initialize_coverity_service(login=True) + + with requests_mock.mock() as mocker: + mocker.get(self.column_keys_url, json=column_keys) + mocker.get(self.checkers_url, json=self.fake_checkers) + # Retrieve checkers; required for get_defects() + coverity_service.retrieve_checkers() + # Retreive columns; required for get_defects() + coverity_service.retrieve_column_keys() + # Get defects + with patch.object(CoverityDefectService, "retrieve_issues") as mock_method: + coverity_service.get_defects(self.fake_stream, "123", test_snapshot.filters, test_snapshot.column_names) + data = mock_method.call_args[0][0] + mock_method.assert_called_once() + assert ordered(data) == ordered(test_snapshot.request_data) + + def test_get_filtered_defects(self): with open(f"{TEST_FOLDER}/columns_keys.json", "r") as content: column_keys = json.loads(content.read()) @@ -192,6 +228,7 @@ def test_get_filtered_defects(self): ] } self.fake_stream = "test_stream" + self.fake_snapshot = "123" # initialize what needed for the REST API coverity_service = self.initialize_coverity_service(login=True) @@ -207,6 +244,7 @@ def test_get_filtered_defects(self): sphinx_coverity_connector = SphinxCoverityConnector() sphinx_coverity_connector.coverity_service = coverity_service sphinx_coverity_connector.stream = self.fake_stream + sphinx_coverity_connector.snaphsot = self.fake_snapshot node_filters = { "checker": "MISRA", "impact": None, "kind": None, "classification": "Intentional,Bug,Pending,Unclassified", "action": None, "component": None, @@ -218,7 +256,9 @@ def test_get_filtered_defects(self): with patch.object(CoverityDefectService, "get_defects") as mock_method: sphinx_coverity_connector.get_filtered_defects(fake_node) - mock_method.assert_called_once_with(self.fake_stream, fake_node["filters"], column_names) + mock_method.assert_called_once_with( + self.fake_stream, self.fake_snapshot, fake_node["filters"], column_names + ) def test_failed_login(self): fake_stream = "test_stream" From d01961bdd73ed6a06b8ffe91e73b65aa58cb9f1a Mon Sep 17 00:00:00 2001 From: JWM Date: Wed, 21 Aug 2024 11:53:07 +0200 Subject: [PATCH 03/25] Add COVERITY_SNAPSHOT to env --- .github/workflows/python-package.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 434441ac..124ca5c7 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -6,6 +6,7 @@ env: COVERITY_PASSWORD: dummy COVERITY_STREAM: dummy COVERITY_USERNAME: dummy + COVERITY_SNAPSHOT: dummy jobs: test: From be7f2df41fbff4ac9ae3b0d3249146e8b8f84095 Mon Sep 17 00:00:00 2001 From: JWM Date: Thu, 22 Aug 2024 09:44:51 +0200 Subject: [PATCH 04/25] Fix flake8 --- tests/test_coverity.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_coverity.py b/tests/test_coverity.py index 056134a6..566fd936 100644 --- a/tests/test_coverity.py +++ b/tests/test_coverity.py @@ -198,7 +198,6 @@ def test_get_defects_with_snapshot(self): mock_method.assert_called_once() assert ordered(data) == ordered(test_snapshot.request_data) - def test_get_filtered_defects(self): with open(f"{TEST_FOLDER}/columns_keys.json", "r") as content: column_keys = json.loads(content.read()) From a8b1bccada88559d6f7c635d166efe8757d47235 Mon Sep 17 00:00:00 2001 From: JWM Date: Thu, 22 Aug 2024 10:53:44 +0200 Subject: [PATCH 05/25] Use the same fake_checkers --- tests/filters.py | 2 +- tests/test_coverity.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/filters.py b/tests/filters.py index 1bc90e69..693d7dc8 100644 --- a/tests/filters.py +++ b/tests/filters.py @@ -160,7 +160,7 @@ "columnKey": "checker", "matchMode": "oneOrMoreMatch", "matchers": [ - {"type": "keyMatcher", "key": "MISRA 2"}, + {"type": "keyMatcher", "key": "MISRA 2 KEY"}, {"type": "keyMatcher", "key": "MISRA 1"}, {"type": "keyMatcher", "key": "MISRA 3"}, ], diff --git a/tests/test_coverity.py b/tests/test_coverity.py index 0a680ec3..74707f65 100644 --- a/tests/test_coverity.py +++ b/tests/test_coverity.py @@ -169,11 +169,11 @@ def test_get_defects_with_snapshot(self): self.fake_checkers = { "checkerAttribute": {"name": "checker", "displayName": "Checker"}, "checkerAttributedata": [ - {"key": "MISRA 1", "value": "MISRA 1"}, - {"key": "MISRA 2", "value": "MISRA 2"}, - {"key": "MISRA 3", "value": "MISRA 3"}, - {"key": "CHECKER 1", "value": "CHECKER 1"}, - {"key": "CHECKER 2", "value": "CHECKER 2"} + {"key": "MISRA 1", "value": "M 1"}, + {"key": "MISRA 2 KEY", "value": "MISRA 2 VALUE"}, + {"key": "MISRA 3", "value": "M 3"}, + {"key": "C 1", "value": "CHECKER 1"}, + {"key": "C 2", "value": "CHECKER 2"} ], } self.fake_stream = "test_stream" From 69431614dea4ecdf493f335211d1290defd8e6c2 Mon Sep 17 00:00:00 2001 From: JWM Date: Thu, 22 Aug 2024 10:54:04 +0200 Subject: [PATCH 06/25] Add fake_snapshot variable to test --- tests/test_coverity.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_coverity.py b/tests/test_coverity.py index 74707f65..6cea3286 100644 --- a/tests/test_coverity.py +++ b/tests/test_coverity.py @@ -195,9 +195,11 @@ def test_get_defects_with_snapshot(self): assert ordered(data) == ordered(test_snapshot.request_data) def test_get_filtered_defects(self): + fake_snapshot = "123" sphinx_coverity_connector = SphinxCoverityConnector() sphinx_coverity_connector.coverity_service = self.initialize_coverity_service(login=False) sphinx_coverity_connector.stream = self.fake_stream + sphinx_coverity_connector.snaphsot = fake_snapshot node_filters = { "checker": "MISRA", "impact": None, "kind": None, "classification": "Intentional,Bug,Pending,Unclassified", "action": None, "component": None, @@ -210,7 +212,7 @@ def test_get_filtered_defects(self): with patch.object(CoverityDefectService, "get_defects") as mock_method: sphinx_coverity_connector.get_filtered_defects(fake_node) mock_method.assert_called_once_with( - self.fake_stream, self.fake_snapshot, fake_node["filters"], column_names + self.fake_stream, fake_snapshot, fake_node["filters"], column_names ) def test_failed_login(self): From d392e5093c7e64f1864f6ba8bc7d151623e0f667 Mon Sep 17 00:00:00 2001 From: JWM Date: Thu, 22 Aug 2024 11:36:52 +0200 Subject: [PATCH 07/25] Make oneliner from multiliner --- tests/test_coverity.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_coverity.py b/tests/test_coverity.py index 6cea3286..868c817b 100644 --- a/tests/test_coverity.py +++ b/tests/test_coverity.py @@ -211,9 +211,7 @@ def test_get_filtered_defects(self): with patch.object(CoverityDefectService, "get_defects") as mock_method: sphinx_coverity_connector.get_filtered_defects(fake_node) - mock_method.assert_called_once_with( - self.fake_stream, fake_snapshot, fake_node["filters"], column_names - ) + mock_method.assert_called_once_with(self.fake_stream, fake_snapshot, fake_node["filters"], column_names) def test_failed_login(self): coverity_conf_service = CoverityDefectService("scan.coverity.com/") From bd6864503eb70da3fd47b051ca513ec7945c8a0b Mon Sep 17 00:00:00 2001 From: JWM Date: Thu, 22 Aug 2024 14:09:54 +0200 Subject: [PATCH 08/25] Set logging level to WARNING in conf.py --- example/conf.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/example/conf.py b/example/conf.py index 4b881d4b..42268fd4 100644 --- a/example/conf.py +++ b/example/conf.py @@ -17,6 +17,7 @@ import mlx.coverity import mlx.traceability from decouple import config +import logging from pkg_resources import get_distribution pkg_version = get_distribution("mlx.coverity").version @@ -318,3 +319,5 @@ TRACEABILITY_ITEM_ID_REGEX = r"([A-Z_]+-[A-Z0-9_]+)" TRACEABILITY_ITEM_RELINK = {} + +logging.basicConfig(level=logging.WARNING) From 1a141f44d7e1d7505da4175853d1ea58be09396e Mon Sep 17 00:00:00 2001 From: JWM Date: Fri, 23 Aug 2024 14:13:29 +0200 Subject: [PATCH 09/25] Use if-else statement to avoid problems with static analysis checker --- mlx/coverity_services.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mlx/coverity_services.py b/mlx/coverity_services.py index 6c7395e6..3eee99ce 100644 --- a/mlx/coverity_services.py +++ b/mlx/coverity_services.py @@ -303,9 +303,11 @@ def get_defects(self, stream, snapshot, filters, column_names): if (filter := filters["component"]) and (filter_values := self.handle_component_filter(filter)): query_filters.append(self.assemble_query_filter("Component", filter_values, "nameMatcher")) - scope = "last()" if snapshot: scope = snapshot + else: + scope = "last()" + data = { "filters": query_filters, "columns": list(self.column_keys(column_names)), From 72a6cb16eff49d16ab98c6d658ddd291e8a8431e Mon Sep 17 00:00:00 2001 From: JWM Date: Fri, 23 Aug 2024 14:17:34 +0200 Subject: [PATCH 10/25] Move new arguments to the end in case it is already used --- mlx/coverity.py | 2 +- mlx/coverity_services.py | 4 ++-- tests/test_coverity.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/mlx/coverity.py b/mlx/coverity.py index 887c00e7..7c66976d 100644 --- a/mlx/coverity.py +++ b/mlx/coverity.py @@ -150,7 +150,7 @@ def get_filtered_defects(self, node): column_names = set(node["col"]) if "chart_attribute" in node and node["chart_attribute"].upper() in node.column_map: column_names.add(node["chart_attribute"]) - defects = self.coverity_service.get_defects(self.stream, self.snaphsot, node["filters"], column_names) + defects = self.coverity_service.get_defects(self.stream, node["filters"], column_names, self.snaphsot) report_info("%d received" % (defects["totalRows"])) report_info("building defects table and/or chart... ", True) return defects diff --git a/mlx/coverity_services.py b/mlx/coverity_services.py index 3eee99ce..5b827152 100644 --- a/mlx/coverity_services.py +++ b/mlx/coverity_services.py @@ -246,7 +246,7 @@ def assemble_query_filter(self, column_name, filter_values, matcher_type): "matchers": matchers } - def get_defects(self, stream, snapshot, filters, column_names): + def get_defects(self, stream, filters, column_names, snapshot): """Gets a list of defects for given stream, snapshot ID, filters and column names. If the snapshot is empty, the last snapshot is taken. If a column name does not match the name of the `columns` property, the column can not be obtained because @@ -255,9 +255,9 @@ def get_defects(self, stream, snapshot, filters, column_names): Args: stream (str): Name of the stream to query - snapshot (str): The snapshot ID; If empty the last snapshot is taken. filters (dict): Dictionary with attribute names as keys and CSV lists of attribute values to query as values column_names (list[str]): The column names + snapshot (str): The snapshot ID; If empty the last snapshot is taken. Returns: dict: The content of the request. This has a structure like: diff --git a/tests/test_coverity.py b/tests/test_coverity.py index 6869ca12..f628330b 100644 --- a/tests/test_coverity.py +++ b/tests/test_coverity.py @@ -168,7 +168,7 @@ def test_get_defects(self, filters, column_names, request_data): coverity_service.retrieve_column_keys() # Get defects with patch.object(CoverityDefectService, "retrieve_issues") as mock_method: - coverity_service.get_defects(self.fake_stream, "", filters, column_names) + coverity_service.get_defects(self.fake_stream, filters, column_names, "") data = mock_method.call_args[0][0] mock_method.assert_called_once() assert ordered(data) == ordered(request_data) @@ -199,7 +199,7 @@ def test_get_defects_with_snapshot(self): coverity_service.retrieve_column_keys() # Get defects with patch.object(CoverityDefectService, "retrieve_issues") as mock_method: - coverity_service.get_defects(self.fake_stream, "123", test_snapshot.filters, test_snapshot.column_names) + coverity_service.get_defects(self.fake_stream, test_snapshot.filters, test_snapshot.column_names, "123") data = mock_method.call_args[0][0] mock_method.assert_called_once() assert ordered(data) == ordered(test_snapshot.request_data) From 43e2bdc20c4138272a3383af07485d3575c9aa70 Mon Sep 17 00:00:00 2001 From: JWM Date: Fri, 23 Aug 2024 14:20:41 +0200 Subject: [PATCH 11/25] Fix argument order of assert_called_with --- tests/test_coverity.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_coverity.py b/tests/test_coverity.py index f628330b..a537fbad 100644 --- a/tests/test_coverity.py +++ b/tests/test_coverity.py @@ -224,11 +224,11 @@ def test_get_filtered_defects(self): fake_node["filters"] = node_filters with patch.object(CoverityDefectService, "get_defects") as mock_method: sphinx_coverity_connector.get_filtered_defects(fake_node) - mock_method.assert_called_once_with(self.fake_stream, fake_snapshot, fake_node["filters"], column_names) + mock_method.assert_called_once_with(self.fake_stream, fake_node["filters"], column_names, fake_snapshot) fake_node["chart_attribute"] = "Checker" column_names.add("Checker") sphinx_coverity_connector.get_filtered_defects(fake_node) - mock_method.assert_called_with(self.fake_stream, fake_snapshot, fake_node["filters"], column_names) + mock_method.assert_called_with(self.fake_stream, fake_node["filters"], column_names, fake_snapshot) def test_failed_login(self): """Test a failed login by mocking the status code when validating the stream.""" From 0c9e1c24b784395d16af30f4e9852c1a08fc075a Mon Sep 17 00:00:00 2001 From: JWM Date: Mon, 2 Sep 2024 09:34:54 +0200 Subject: [PATCH 12/25] Use one-liner for if-else statement --- mlx/coverity/coverity_services.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/mlx/coverity/coverity_services.py b/mlx/coverity/coverity_services.py index bb1d95c5..4e2da8d8 100644 --- a/mlx/coverity/coverity_services.py +++ b/mlx/coverity/coverity_services.py @@ -303,10 +303,7 @@ def get_defects(self, stream, filters, column_names, snapshot): if (filter := filters["component"]) and (filter_values := self.handle_component_filter(filter)): query_filters.append(self.assemble_query_filter("Component", filter_values, "nameMatcher")) - if snapshot: - scope = snapshot - else: - scope = "last()" + scope = snapshot if snapshot else "last()" data = { "filters": query_filters, From aedbafcbca377b33613c532a63540e8053365a08 Mon Sep 17 00:00:00 2001 From: JWM Date: Tue, 3 Sep 2024 10:17:42 +0200 Subject: [PATCH 13/25] Fix typo --- mlx/coverity/coverity.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mlx/coverity/coverity.py b/mlx/coverity/coverity.py index 34a2f8e6..c33f42dc 100644 --- a/mlx/coverity/coverity.py +++ b/mlx/coverity/coverity.py @@ -46,7 +46,7 @@ def initialize_environment(self, app): \\makeatother""" self.stream = app.config.coverity_credentials["stream"] - self.snaphsot = app.config.coverity_credentials["snapshot"] + self.snapshot = app.config.coverity_credentials["snapshot"] # Login to Coverity and obtain stream information try: self.input_credentials(app.config.coverity_credentials) @@ -61,9 +61,9 @@ def initialize_environment(self, app): report_info("Verify the given stream name... ", True) self.coverity_service.validate_stream(self.stream) report_info("done") - if self.snaphsot: + if self.snapshot: report_info("Verify the given snapshot ID and obtain all enabled checkers... ", True) - self.coverity_service.validate_snapshot(self.snaphsot) + self.coverity_service.validate_snapshot(self.snapshot) report_info("done") # Get all column keys report_info("obtaining all column keys... ", True) @@ -150,7 +150,7 @@ def get_filtered_defects(self, node): column_names = set(node["col"]) if "chart_attribute" in node and node["chart_attribute"].upper() in node.column_map: column_names.add(node["chart_attribute"]) - defects = self.coverity_service.get_defects(self.stream, node["filters"], column_names, self.snaphsot) + defects = self.coverity_service.get_defects(self.stream, node["filters"], column_names, self.snapshot) report_info("%d received" % (defects["totalRows"])) report_info("building defects table and/or chart... ", True) return defects From cc4d1cc323fb44a8a71b0f3e3bfa4701c46a3836 Mon Sep 17 00:00:00 2001 From: JWM Date: Tue, 3 Sep 2024 11:22:31 +0200 Subject: [PATCH 14/25] Fix typo in test --- tests/test_coverity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_coverity.py b/tests/test_coverity.py index 639ebef1..d081a6c7 100644 --- a/tests/test_coverity.py +++ b/tests/test_coverity.py @@ -211,7 +211,7 @@ def test_get_filtered_defects(self): sphinx_coverity_connector = SphinxCoverityConnector() sphinx_coverity_connector.coverity_service = self.initialize_coverity_service(login=False) sphinx_coverity_connector.stream = self.fake_stream - sphinx_coverity_connector.snaphsot = fake_snapshot + sphinx_coverity_connector.snapshot = fake_snapshot node_filters = { "checker": "MISRA", "impact": None, "kind": None, "classification": "Intentional,Bug,Pending,Unclassified", "action": None, "component": None, From 7807ab4f7e38583ab42fdfdf049ded0e15a0595a Mon Sep 17 00:00:00 2001 From: JWM Date: Tue, 3 Sep 2024 14:56:56 +0200 Subject: [PATCH 15/25] Fix f-string --- mlx/coverity/coverity_services.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mlx/coverity/coverity_services.py b/mlx/coverity/coverity_services.py index 4e2da8d8..dd0b0089 100644 --- a/mlx/coverity/coverity_services.py +++ b/mlx/coverity/coverity_services.py @@ -336,7 +336,7 @@ def handle_attribute_filter(self, attribute_values, name, valid_attributes, allo filter_values = set() for field in attribute_values.split(","): if not valid_attributes or field in valid_attributes: - report_info("Classification [{field}] is valid") + report_info(f"Classification [{field}] is valid") filter_values.add(field) elif allow_regex: pattern = re.compile(field) From 1c82f68b06c6335aa7af6d988e1c97964b02cc5b Mon Sep 17 00:00:00 2001 From: JWM Date: Tue, 3 Sep 2024 14:59:01 +0200 Subject: [PATCH 16/25] Take global LOGGER from coverity_logging to set logging level --- example/conf.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/example/conf.py b/example/conf.py index 2ef6219e..28357045 100644 --- a/example/conf.py +++ b/example/conf.py @@ -15,11 +15,10 @@ import sys import mlx.coverity -from mlx.coverity import __version__ +from mlx.coverity import __version__, coverity_logging import mlx.traceability from decouple import config import logging -from sphinx.util.logging import getLogger pkg_version = __version__ @@ -325,8 +324,7 @@ if log_level: try: numeric_level = getattr(logging, log_level.upper(), None) - logger = getLogger("mlx.coverity_logging") - logger.setLevel(level=numeric_level) + coverity_logging.LOGGER.setLevel(level=numeric_level) except: raise ValueError(f"Invalid log level: {log_level}") From b08f5c883b8d2d2278a51d509c02a104f47d2416 Mon Sep 17 00:00:00 2001 From: JWM Date: Tue, 3 Sep 2024 14:59:36 +0200 Subject: [PATCH 17/25] Fix typo to set default value if not exists --- example/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/Makefile b/example/Makefile index 0e78d882..6cc49adb 100644 --- a/example/Makefile +++ b/example/Makefile @@ -10,7 +10,7 @@ BUILDDIR ?= _build # logging variables DEBUG ?= 0 -LOGLEVEL =? WARNING +LOGLEVEL ?= WARNING # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 From 26b647a1519cfdd304fb37c5c3441f3f7ee69db3 Mon Sep 17 00:00:00 2001 From: JWM <62558419+JokeWaumans@users.noreply.github.com> Date: Thu, 5 Sep 2024 10:38:51 +0200 Subject: [PATCH 18/25] Update docstring Co-authored-by: Jasper Craeghs <28319872+JasperCraeghs@users.noreply.github.com> --- mlx/coverity/coverity_services.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mlx/coverity/coverity_services.py b/mlx/coverity/coverity_services.py index dd0b0089..08e1a21c 100644 --- a/mlx/coverity/coverity_services.py +++ b/mlx/coverity/coverity_services.py @@ -247,8 +247,9 @@ def assemble_query_filter(self, column_name, filter_values, matcher_type): } def get_defects(self, stream, filters, column_names, snapshot): - """Gets a list of defects for given stream, snapshot ID, filters and column names. - If the snapshot is empty, the last snapshot is taken. + """Gets a list of defects for the given stream, filters and column names. + + If no snapshot ID is given, the last snapshot is taken. If a column name does not match the name of the `columns` property, the column can not be obtained because it need the correct corresponding column key. Column key `cid` is always obtained to use later in other functions. From cc38b898d11b701f23e9f9577927fc56b1f1fa78 Mon Sep 17 00:00:00 2001 From: JWM Date: Thu, 5 Sep 2024 10:57:26 +0200 Subject: [PATCH 19/25] Snapshot defaults to empty string for backwards compatibility --- mlx/coverity/coverity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mlx/coverity/coverity.py b/mlx/coverity/coverity.py index c33f42dc..3c100c0a 100644 --- a/mlx/coverity/coverity.py +++ b/mlx/coverity/coverity.py @@ -46,7 +46,7 @@ def initialize_environment(self, app): \\makeatother""" self.stream = app.config.coverity_credentials["stream"] - self.snapshot = app.config.coverity_credentials["snapshot"] + self.snapshot = app.config.coverity_credentials.get("snapshot", "") # Login to Coverity and obtain stream information try: self.input_credentials(app.config.coverity_credentials) From 3346010b30b811a07b424316ea53aef3d09603e0 Mon Sep 17 00:00:00 2001 From: JWM Date: Thu, 5 Sep 2024 11:09:40 +0200 Subject: [PATCH 20/25] Continue with the latest snapshot if the snapshot does not exist --- mlx/coverity/coverity_services.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/mlx/coverity/coverity_services.py b/mlx/coverity/coverity_services.py index 08e1a21c..607e7b3d 100644 --- a/mlx/coverity/coverity_services.py +++ b/mlx/coverity/coverity_services.py @@ -9,7 +9,7 @@ import requests from sphinx.util.logging import getLogger -from mlx.coverity import report_info +from mlx.coverity import report_info, report_warning # Coverity built in Impact statuses IMPACT_LIST = ["High", "Medium", "Low"] @@ -53,6 +53,7 @@ def __init__(self, hostname): self._checkers = [] self._columns = {} self.logger = getLogger("mlx.coverity_logging") + self.valid_snapshot = False @property def base_url(self): @@ -128,12 +129,19 @@ def validate_stream(self, stream): def validate_snapshot(self, snapshot): """Validate snapshot by retrieving the specified snapshot. When the request fails, the snapshot does not exist or the user does not have acces to it. + In this case a warning is logged and continues with the latest snapshot. Args: snapshot (str): The snapshot ID """ url = f"{self.api_endpoint}/snapshots/{snapshot}" - self._request(url) + response = self.session.get(url) + if response.ok: + self.valid_snapshot = True + report_info(f"Snapshot ID {snapshot} is valid") + else: + report_warning(f"No snapshot found for ID {snapshot}; Continue with using the latest snapshot.", "") + self.valid_snapshot = False def retrieve_issues(self, filters): """Retrieve issues from the server (Coverity Connect). @@ -304,7 +312,7 @@ def get_defects(self, stream, filters, column_names, snapshot): if (filter := filters["component"]) and (filter_values := self.handle_component_filter(filter)): query_filters.append(self.assemble_query_filter("Component", filter_values, "nameMatcher")) - scope = snapshot if snapshot else "last()" + scope = snapshot if snapshot and self.valid_snapshot else "last()" data = { "filters": query_filters, From 6eb40c2c6432fffe26e2c7d6fd56a372330bf235 Mon Sep 17 00:00:00 2001 From: JWM Date: Thu, 5 Sep 2024 11:49:42 +0200 Subject: [PATCH 21/25] Refactor loggings --- mlx/coverity/coverity.py | 13 +++++++------ mlx/coverity/coverity_services.py | 14 ++++++++------ 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/mlx/coverity/coverity.py b/mlx/coverity/coverity.py index 3c100c0a..cea35c20 100644 --- a/mlx/coverity/coverity.py +++ b/mlx/coverity/coverity.py @@ -58,19 +58,19 @@ def initialize_environment(self, app): app.config.coverity_credentials["username"], app.config.coverity_credentials["password"] ) report_info("done") - report_info("Verify the given stream name... ", True) + report_info("Verify the given stream name... ") self.coverity_service.validate_stream(self.stream) report_info("done") if self.snapshot: - report_info("Verify the given snapshot ID and obtain all enabled checkers... ", True) + report_info("Verify the given snapshot ID and obtain all enabled checkers... ") self.coverity_service.validate_snapshot(self.snapshot) report_info("done") # Get all column keys - report_info("obtaining all column keys... ", True) + report_info("obtaining all column keys... ") self.coverity_service.retrieve_column_keys() report_info("done") # Get all checkers - report_info("obtaining all checkers... ", True) + report_info("obtaining all checkers... ") self.coverity_service.retrieve_checkers() report_info("done") except (URLError, HTTPError, Exception, ValueError) as error_info: # pylint: disable=broad-except @@ -108,7 +108,9 @@ def process_coverity_nodes(self, app, doctree, fromdocname): error_message = "There are no defects with the specified filters" report_warning(error_message, fromdocname, lineno=node["line"]) else: + report_info("building defects table and/or chart... ", True) node.perform_replacement(defects, self, app, fromdocname) + report_info("done") except (URLError, AttributeError, Exception) as err: # pylint: disable=broad-except error_message = f"failed to process coverity-list with {err!r}" report_warning(error_message, fromdocname, lineno=node["line"]) @@ -146,13 +148,12 @@ def get_filtered_defects(self, node): "rows": [list of dictionaries {"key": , "value": }] } """ - report_info("obtaining defects... ", True) + report_info("obtaining defects... ") column_names = set(node["col"]) if "chart_attribute" in node and node["chart_attribute"].upper() in node.column_map: column_names.add(node["chart_attribute"]) defects = self.coverity_service.get_defects(self.stream, node["filters"], column_names, self.snapshot) report_info("%d received" % (defects["totalRows"])) - report_info("building defects table and/or chart... ", True) return defects diff --git a/mlx/coverity/coverity_services.py b/mlx/coverity/coverity_services.py index 607e7b3d..6b580a60 100644 --- a/mlx/coverity/coverity_services.py +++ b/mlx/coverity/coverity_services.py @@ -218,7 +218,7 @@ def _request(self, url, data=None): err_msg = response.json()["message"] except (requests.exceptions.JSONDecodeError, KeyError): err_msg = response.content.decode() - self.logger.warning(err_msg) + self.logger.error(err_msg) return response.raise_for_status() def assemble_query_filter(self, column_name, filter_values, matcher_type): @@ -277,7 +277,7 @@ def get_defects(self, stream, filters, column_names, snapshot): "rows": list of [list of dictionaries {"key": , "value": }] } """ - report_info(f"Querying Coverity for defects in stream [{stream}] ...",) + report_info(f"Querying Coverity for defects in stream [{stream}] ...") query_filters = [ { "columnKey": "streams", @@ -325,8 +325,10 @@ def get_defects(self, stream, filters, column_names, snapshot): } } - report_info("Running Coverity query...") - return self.retrieve_issues(data) + defects_data = self.retrieve_issues(data) + report_info("done") + + return defects_data def handle_attribute_filter(self, attribute_values, name, valid_attributes, allow_regex=False): """Process the given CSV list of attribute values by filtering out the invalid ones while logging an error. @@ -341,7 +343,7 @@ def handle_attribute_filter(self, attribute_values, name, valid_attributes, allo Returns: set[str]: The attributes values to query with """ - report_info(f"Using {name} filter [{attribute_values}]") + report_info(f"Using {name!r} filter [{attribute_values}]") filter_values = set() for field in attribute_values.split(","): if not valid_attributes or field in valid_attributes: @@ -365,7 +367,7 @@ def handle_component_filter(self, attribute_values): Returns: list[str]: The list of attributes """ - report_info(f"Using Component filter [{attribute_values}]") + report_info(f"Using 'Component' filter [{attribute_values}]") parser = csv.reader([attribute_values]) filter_values = [] for fields in parser: From b8793b86f543adfb26f3ef17f68fc9864b6c8a12 Mon Sep 17 00:00:00 2001 From: JWM Date: Thu, 5 Sep 2024 11:53:22 +0200 Subject: [PATCH 22/25] Delete done in perform_replacement --- mlx/coverity/coverity_directives/coverity_defect_list.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mlx/coverity/coverity_directives/coverity_defect_list.py b/mlx/coverity/coverity_directives/coverity_defect_list.py index 9cd54479..74f01b72 100644 --- a/mlx/coverity/coverity_directives/coverity_defect_list.py +++ b/mlx/coverity/coverity_directives/coverity_defect_list.py @@ -101,7 +101,6 @@ def perform_replacement(self, defects, connector, app, fromdocname): self._prepare_labels_and_values(combined_labels, defects["totalRows"]) top_node += self.build_pie_chart(env) - report_info("done") self.replace_self(top_node) def initialize_table(self): From f9b649189f2c8945b88514c4c54f1562a97ffe9e Mon Sep 17 00:00:00 2001 From: JWM Date: Mon, 9 Sep 2024 10:23:21 +0200 Subject: [PATCH 23/25] Return valid snapshot to use it later --- mlx/coverity/coverity.py | 4 +++- mlx/coverity/coverity_services.py | 11 +++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/mlx/coverity/coverity.py b/mlx/coverity/coverity.py index cea35c20..1796c0ea 100644 --- a/mlx/coverity/coverity.py +++ b/mlx/coverity/coverity.py @@ -63,8 +63,10 @@ def initialize_environment(self, app): report_info("done") if self.snapshot: report_info("Verify the given snapshot ID and obtain all enabled checkers... ") - self.coverity_service.validate_snapshot(self.snapshot) + self.snapshot = self.coverity_service.validate_snapshot(self.snapshot) report_info("done") + else: + self.snapshot = "last()" # Get all column keys report_info("obtaining all column keys... ") self.coverity_service.retrieve_column_keys() diff --git a/mlx/coverity/coverity_services.py b/mlx/coverity/coverity_services.py index 6b580a60..f526aac8 100644 --- a/mlx/coverity/coverity_services.py +++ b/mlx/coverity/coverity_services.py @@ -53,7 +53,6 @@ def __init__(self, hostname): self._checkers = [] self._columns = {} self.logger = getLogger("mlx.coverity_logging") - self.valid_snapshot = False @property def base_url(self): @@ -137,11 +136,13 @@ def validate_snapshot(self, snapshot): url = f"{self.api_endpoint}/snapshots/{snapshot}" response = self.session.get(url) if response.ok: - self.valid_snapshot = True report_info(f"Snapshot ID {snapshot} is valid") + valid_snapshot = snapshot else: report_warning(f"No snapshot found for ID {snapshot}; Continue with using the latest snapshot.", "") - self.valid_snapshot = False + valid_snapshot = "last()" + + return valid_snapshot def retrieve_issues(self, filters): """Retrieve issues from the server (Coverity Connect). @@ -312,14 +313,12 @@ def get_defects(self, stream, filters, column_names, snapshot): if (filter := filters["component"]) and (filter_values := self.handle_component_filter(filter)): query_filters.append(self.assemble_query_filter("Component", filter_values, "nameMatcher")) - scope = snapshot if snapshot and self.valid_snapshot else "last()" - data = { "filters": query_filters, "columns": list(self.column_keys(column_names)), "snapshotScope": { "show": { - "scope": scope, + "scope": snapshot, "includeOutdatedSnapshots": False } } From 40f075feae430a0b3236511940ea47939905bbe4 Mon Sep 17 00:00:00 2001 From: JWM <62558419+JokeWaumans@users.noreply.github.com> Date: Mon, 9 Sep 2024 10:25:30 +0200 Subject: [PATCH 24/25] Add trailing comma to add data easily Co-authored-by: Jasper Craeghs <28319872+JasperCraeghs@users.noreply.github.com> --- example/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/conf.py b/example/conf.py index 28357045..7530203f 100644 --- a/example/conf.py +++ b/example/conf.py @@ -314,7 +314,7 @@ "username": config("COVERITY_USERNAME"), "password": config("COVERITY_PASSWORD"), "stream": config("COVERITY_STREAM"), - "snapshot": config("COVERITY_SNAPSHOT") + "snapshot": config("COVERITY_SNAPSHOT"), } TRACEABILITY_ITEM_ID_REGEX = r"([A-Z_]+-[A-Z0-9_]+)" From eadbb08fb1c9f4f11948f65e909acb5a995675d7 Mon Sep 17 00:00:00 2001 From: JWM Date: Mon, 9 Sep 2024 10:44:04 +0200 Subject: [PATCH 25/25] Update test with snapshot="last()" if snapshot is not defined/valid --- tests/test_coverity.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_coverity.py b/tests/test_coverity.py index d081a6c7..fbaf5942 100644 --- a/tests/test_coverity.py +++ b/tests/test_coverity.py @@ -142,7 +142,8 @@ def test_get_defects(self, filters, column_names, request_data): """Check get defects with different filters. Check if the response of `get_defects` is the same as expected. The data is obtained from the filters.py file. Due to the usage of set in `get_defects` (column_keys), the function `ordered` is used to compare the returned - data of the request where order does not matter.""" + data of the request where order does not matter. + """ with open(f"{TEST_FOLDER}/columns_keys.json", "r") as content: column_keys = json.loads(content.read()) self.fake_checkers = { @@ -167,7 +168,7 @@ def test_get_defects(self, filters, column_names, request_data): coverity_service.retrieve_column_keys() # Get defects with patch.object(CoverityDefectService, "retrieve_issues") as mock_method: - coverity_service.get_defects(self.fake_stream, filters, column_names, "") + coverity_service.get_defects(self.fake_stream, filters, column_names, "last()") data = mock_method.call_args[0][0] mock_method.assert_called_once() assert ordered(data) == ordered(request_data)