From 5d2e94ed3b9b2efbbad709ae6dffcbbee0ee2171 Mon Sep 17 00:00:00 2001 From: mugdhadhole1 Date: Fri, 6 Dec 2024 14:27:22 +0530 Subject: [PATCH 1/2] Feature: support query string as an argument in lobster codebeamer GitHub issue: #87 Query string (CBQL query) can be now passed as a command line argument to the lobster codebeamer tool --- CHANGELOG.md | 3 + lobster/tools/codebeamer/codebeamer.py | 60 +++++++---- packages/lobster-tool-codebeamer/README.md | 4 +- .../lobster-codebeamer/test_codebeamer.py | 100 +++++++++++++++++- 4 files changed, 146 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1a66fa3..367e03df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ ### 0.9.21-dev +* `lobster-codebeamer` now supports query string, query string (CBQL) can be passed + as a command line argument to the tool `lobster-codebeamer`. + * `lobster-html-report` has the following updates. * Filter items by status (Ok, Missing, Partial, Warning, Justified) * Hide/Unhide Issues. diff --git a/lobster/tools/codebeamer/codebeamer.py b/lobster/tools/codebeamer/codebeamer.py index fd2ad674..ec0ada24 100755 --- a/lobster/tools/codebeamer/codebeamer.py +++ b/lobster/tools/codebeamer/codebeamer.py @@ -152,23 +152,30 @@ def get_many_items(cb_config, item_ids): return rv -def get_query(mh, cb_config, query_id): +def get_query(mh, cb_config, query): assert isinstance(mh, Message_Handler) assert isinstance(cb_config, dict) - assert isinstance(query_id, int) + assert isinstance(query, (int, str)) rv = [] + url = "" page_id = 1 total_items = None while total_items is None or len(rv) < total_items: print("Fetching page %u of query..." % page_id) - url = "%s/reports/%u/items?page=%u&pageSize=%u" % \ - (cb_config["base"], - query_id, - page_id, - cb_config["page_size"]) - data = query_cb_single(cb_config, url) - assert len(data) == 4 + if query is not None: + if isinstance(query, int): + url = ("%s/reports/%u/items?page=%u&pageSize=%u" % + (cb_config["base"], + query, + page_id, + cb_config["page_size"])) + elif isinstance(query, str): + url = ("%s/items/query?queryString=%s" % + (cb_config["base"], + query)) + data = query_cb_single(cb_config, url) + assert len(data) == 4 if page_id == 1 and len(data["items"]) == 0: print("This query doesn't generate items. Please check:") @@ -183,8 +190,12 @@ def get_query(mh, cb_config, query_id): else: assert total_items == data["total"] - rv += [to_lobster(cb_config, cb_item["item"]) - for cb_item in data["items"]] + if query is not None and isinstance(query, int): + rv += [to_lobster(cb_config, cb_item["item"]) + for cb_item in data["items"]] + elif query is not None and isinstance(query, str): + rv += [to_lobster(cb_config, cb_item) + for cb_item in data["items"]] page_id += 1 @@ -416,7 +427,7 @@ def main(): default=None) modes.add_argument("--import-query", - metavar="CB_QUERY_ID", + metavar="CB_QUERY", default=None) ap.add_argument("--config", @@ -525,17 +536,30 @@ def main(): elif options.import_query: try: - query_id = int(options.import_query) - except ValueError: - ap.error("query-id must be an integer") - if query_id < 1: - ap.error("query-id must be a positive") + if isinstance(options.import_query, int): + query = int(options.import_query) + if query < 1: + raise ValueError("query_string must be a positive integer" + "or valid string") + elif isinstance(options.import_query, str): + if options.import_query.startswith("-"): + raise ValueError("query_string must be a positive integer" + "or valid string") + elif options.import_query.isdigit(): + query = int(options.import_query) + if query < 1: + raise ValueError("query_string must be a positive integer" + "or valid string") + elif isinstance(options.import_query, str): + query = str(options.import_query) + except ValueError as e: + ap.error(str(e)) try: if options.import_tagged: items = import_tagged(mh, cb_config, items_to_import) elif options.import_query: - items = get_query(mh, cb_config, query_id) + items = get_query(mh, cb_config, query) except LOBSTER_Error: return 1 diff --git a/packages/lobster-tool-codebeamer/README.md b/packages/lobster-tool-codebeamer/README.md index 216a426d..8794124e 100644 --- a/packages/lobster-tool-codebeamer/README.md +++ b/packages/lobster-tool-codebeamer/README.md @@ -120,8 +120,8 @@ There are two ways you can use this tool: 1. Download all requirements mentioned by another lobster trace (this way you do not get a completeness check) (using `--import-tagged`) -2. Download all requirements generated by a saved codebeamer query - (using `--import-query`) +2. Download all requirements generated by a saved codebeamer query ID OR a codebeamer query string (cbQL query) + (using `--import-query` - accepts the query ID as well as query string) * Configure the 'refs' upstream reference (this argument is optional) (using `--config`) diff --git a/tests-unit/lobster-codebeamer/test_codebeamer.py b/tests-unit/lobster-codebeamer/test_codebeamer.py index 694c3135..a53c46f7 100644 --- a/tests-unit/lobster-codebeamer/test_codebeamer.py +++ b/tests-unit/lobster-codebeamer/test_codebeamer.py @@ -1,7 +1,7 @@ import unittest from unittest.mock import Mock, patch -from lobster.tools.codebeamer.codebeamer import get_single_item, get_many_items, to_lobster, \ +from lobster.tools.codebeamer.codebeamer import get_query, get_single_item, get_many_items, to_lobster, \ import_tagged from lobster.errors import Message_Handler @@ -15,6 +15,104 @@ def _assertListEqualByAttributes(self, list1, list2): for attr in list_of_compared_attributes: self.assertEqual(getattr(obj1, attr), getattr(obj2, attr), f"{obj1} is not like {obj2} in {attr}") + @patch('lobster.tools.codebeamer.codebeamer.query_cb_single') + def test_get_query_with_ID(self, mock_query_cb_single): + mh = Message_Handler() + mock_cb_config = { + "root" : "https://codebeamer.bmwgroup.net", + "base": "https://test.com", + "page_size": 10 + } + mock_query = 171619121 + item_data = [ + { + "item": { + "id": 34886222, + "name" : "mock item", + "version": 8, + "tracker": { + "id": 34158092 + }, + "categories": [{"name": "Requirement"}], + "status": {"name": "Content Review",} + } + } + ] + mock_query_cb_single.return_value = { + "page": 1, + "pageSize": 100, + "total": 1, + "items": item_data + } + + result = get_query(mh, mock_cb_config, mock_query) + self.assertEqual(len(result), 1) + + self.assertEqual(result[0].kind, item_data[0]["item"]["categories"][0]["name"]) + self.assertEqual(result[0].status, item_data[0]["item"]["status"]["name"]) + self.assertEqual(result[0].name, item_data[0]["item"]["name"]) + self.assertEqual(result[0].tag.tag, str(item_data[0]["item"]["id"])) + self.assertEqual(result[0].location.item, item_data[0]["item"]["id"]) + self.assertEqual(result[0].location.tracker, item_data[0]["item"]["tracker"]["id"]) + self.assertEqual(result[0].location.version, item_data[0]["item"]["version"]) + + @patch('lobster.tools.codebeamer.codebeamer.query_cb_single') + def test_get_query_with_query(self, mock_query_cb_single): + mh = Message_Handler() + mock_cb_config = { + "root" : "https://codebeamer.bmwgroup.net", + "base": "https://test.com", + "page_size": 10 + } + mock_query = "TeamID IN (10833708) AND workItemStatus IN ('InProgress') AND summary LIKE 'Vulnerable Road User'" + item_data = [ + { + "id": 34886222, + "name" : "mock item", + "version": 8, + "tracker": { + "id": 34158092 + }, + "categories": [{"name": "Requirement"}], + "status": {"name": "Content Review",} + } + ] + mock_query_cb_single.return_value = { + "page": 1, + "pageSize": 100, + "total": 1, + "items": item_data + } + + result = get_query(mh, mock_cb_config, mock_query) + self.assertEqual(len(result), 1) + + self.assertEqual(result[0].kind, item_data[0]["categories"][0]["name"]) + self.assertEqual(result[0].status, item_data[0]["status"]["name"]) + self.assertEqual(result[0].name, item_data[0]["name"]) + self.assertEqual(result[0].tag.tag, str(item_data[0]["id"])) + self.assertEqual(result[0].location.item, item_data[0]["id"]) + self.assertEqual(result[0].location.tracker, item_data[0]["tracker"]["id"]) + self.assertEqual(result[0].location.version, item_data[0]["version"]) + + @patch('lobster.tools.codebeamer.codebeamer.query_cb_single') + def test_get_query_with_invalid_data(self, mock_query_cb_single): + mock_query = 789 + mh = Message_Handler() + mock_cb_config = { + "root" : "https://codebeamer.bmwgroup.net", + "base": "https://test.com", + "page_size": 10 + } + mock_query_cb_single.return_value = { + "page": 1, + "pageSize": 100, + "total": 1, + "items": [] + } + with self.assertRaises(SystemExit): + get_query(mh, mock_cb_config, mock_query) + @patch('lobster.tools.codebeamer.codebeamer.query_cb_single') def test_get_single_item(self, mock_get): _item_id = 11693324 From a0bbc2d7ef12577780951f1d265f16d45aed04c5 Mon Sep 17 00:00:00 2001 From: mugdhadhole1 Date: Mon, 16 Dec 2024 16:35:04 +0530 Subject: [PATCH 2/2] Implemented the review changes --- lobster/tools/codebeamer/codebeamer.py | 37 ++++++++++++-------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/lobster/tools/codebeamer/codebeamer.py b/lobster/tools/codebeamer/codebeamer.py index ec0ada24..3dd65e5a 100755 --- a/lobster/tools/codebeamer/codebeamer.py +++ b/lobster/tools/codebeamer/codebeamer.py @@ -163,19 +163,18 @@ def get_query(mh, cb_config, query): while total_items is None or len(rv) < total_items: print("Fetching page %u of query..." % page_id) - if query is not None: - if isinstance(query, int): - url = ("%s/reports/%u/items?page=%u&pageSize=%u" % - (cb_config["base"], - query, - page_id, - cb_config["page_size"])) - elif isinstance(query, str): - url = ("%s/items/query?queryString=%s" % - (cb_config["base"], - query)) - data = query_cb_single(cb_config, url) - assert len(data) == 4 + if isinstance(query, int): + url = ("%s/reports/%u/items?page=%u&pageSize=%u" % + (cb_config["base"], + query, + page_id, + cb_config["page_size"])) + elif isinstance(query, str): + url = ("%s/items/query?queryString=%s" % + (cb_config["base"], + query)) + data = query_cb_single(cb_config, url) + assert len(data) == 4 if page_id == 1 and len(data["items"]) == 0: print("This query doesn't generate items. Please check:") @@ -539,18 +538,16 @@ def main(): if isinstance(options.import_query, int): query = int(options.import_query) if query < 1: - raise ValueError("query_string must be a positive integer" - "or valid string") + ap.error("query_string must be a positive integer") elif isinstance(options.import_query, str): if options.import_query.startswith("-"): - raise ValueError("query_string must be a positive integer" - "or valid string") + ap.error("query_string must be a positive integer" + " or valid string") elif options.import_query.isdigit(): query = int(options.import_query) if query < 1: - raise ValueError("query_string must be a positive integer" - "or valid string") - elif isinstance(options.import_query, str): + ap.error("query_string must be a positive integer") + else: query = str(options.import_query) except ValueError as e: ap.error(str(e))