diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a7f4e02..d688993b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ ## Changelog +### 0.9.18-dev + +* The `lobster-codebeamer` tool now uses codebeamer api v3. + Please note that the api v3 returns the value "Unset" for a codebeamer + item status if the status is actually empty. The api v1 did not return + any value at all for an item with a missing status. This means that the + resulting lobster file will now contain "Unset" as status information, + too, instead of `Null`. ### 0.9.18-dev diff --git a/lobster/tools/codebeamer/codebeamer.py b/lobster/tools/codebeamer/codebeamer.py index f636b02c..a7e49f0b 100755 --- a/lobster/tools/codebeamer/codebeamer.py +++ b/lobster/tools/codebeamer/codebeamer.py @@ -40,9 +40,7 @@ import sys import argparse import netrc - -from copy import copy - +from urllib.parse import quote import requests from lobster.items import Tracing_Tag, Requirement @@ -84,27 +82,26 @@ def query_cb_single(cb_config, url): def get_single_item(cb_config, item_id): assert isinstance(item_id, int) and item_id > 0 - url = "%s/item/%u" % (cb_config["base"], - item_id) + url = "%s/items/%u" % (cb_config["base"], + item_id) data = query_cb_single(cb_config, url) return data -def get_many_items_maybe(cb_config, tracker_id, item_ids): - assert isinstance(tracker_id, int) +def get_many_items(cb_config, item_ids): assert isinstance(item_ids, set) rv = [] - base_url = "%s/tracker/%u/items/or/%s" % ( - cb_config["base"], - tracker_id, - ";".join("id=%u" % item_id - for item_id in item_ids)) page_id = 1 + query_string = quote(f"item.id IN " + f"({','.join(str(item_id) for item_id in item_ids)})") + while True: - data = query_cb_single(cb_config, "%s/page/%u" % (base_url, - page_id)) + base_url = "%s/items/query?page=%u&pageSize=%u&queryString=%s"\ + % (cb_config["base"], page_id, + cb_config["page_size"], query_string) + data = query_cb_single(cb_config, base_url) rv += data["items"] if len(rv) == data["total"]: break @@ -123,14 +120,13 @@ def get_query(mh, cb_config, query_id): while total_items is None or len(rv) < total_items: print("Fetching page %u of query..." % page_id) - url = "%s/query/%u/page/%u?pagesize=%u" % \ + 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) == 1 - data = data["trackerItems"] + assert len(data) == 4 if page_id == 1 and len(data["items"]) == 0: print("This query doesn't generate items. Please check:") @@ -145,7 +141,7 @@ def get_query(mh, cb_config, query_id): else: assert total_items == data["total"] - rv += [to_lobster(cb_config, cb_item) + rv += [to_lobster(cb_config, cb_item["item"]) for cb_item in data["items"]] page_id += 1 @@ -162,8 +158,9 @@ def to_lobster(cb_config, cb_item): # This looks like it's business logic, maybe we should make this # configurable? - if "type" in cb_item: - kind = cb_item["type"].get("name", "codebeamer item") + categories = cb_item.get("categories") + if categories: + kind = categories[0].get("name", "codebeamer item") else: kind = "codebeamer item" @@ -206,32 +203,12 @@ def import_tagged(mh, cb_config, items_to_import): assert isinstance(mh, Message_Handler) assert isinstance(cb_config, dict) assert isinstance(items_to_import, set) - work_list = copy(items_to_import) - rv = [] - - tracker_id = None - while work_list: - if tracker_id is None or len(work_list) < 3: - target = work_list.pop() - print("Fetching single item %u" % target) - - cb_item = get_single_item(cb_config, target) - l_item = to_lobster(cb_config, cb_item) - tracker_id = l_item.location.tracker - rv.append(l_item) - - else: - print("Attempting to fetch %u items from %s" % - (len(work_list), tracker_id)) - cb_items = get_many_items_maybe(cb_config, tracker_id, work_list) - - for cb_item in cb_items: - l_item = to_lobster(cb_config, cb_item) - assert tracker_id == l_item.location.tracker - rv.append(l_item) - work_list.remove(l_item.location.item) + rv = [] - tracker_id = None + cb_items = get_many_items(cb_config, items_to_import) + for cb_item in cb_items: + l_item = to_lobster(cb_config, cb_item) + rv.append(l_item) return rv @@ -272,7 +249,7 @@ def main(): cb_config = { "root" : options.cb_root, - "base" : "%s/cb/rest" % options.cb_root, + "base" : "%s/cb/api/v3" % options.cb_root, "user" : options.cb_user, "pass" : options.cb_pass, "verify_ssl" : not options.ignore_ssl_errors, diff --git a/test-unit/lobster-codebeamer/test_codebeamer.py b/test-unit/lobster-codebeamer/test_codebeamer.py new file mode 100644 index 00000000..9405acce --- /dev/null +++ b/test-unit/lobster-codebeamer/test_codebeamer.py @@ -0,0 +1,95 @@ +import unittest +from unittest.mock import Mock, patch + +from lobster.tools.codebeamer.codebeamer import get_single_item, get_many_items, to_lobster, \ + import_tagged +from lobster.errors import Message_Handler + +list_of_compared_attributes = ['name', 'kind', 'status', 'just_down', 'just_up', 'just_global'] + + +class QueryCodebeamerTest(unittest.TestCase): + def _assertListEqualByAttributes(self, list1, list2): + self.assertEqual(len(list1), len(list2), "Lists length are not the same") + for obj1, obj2 in zip(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_single_item(self, mock_get): + _item_id = 11693324 + _cb_config = {'base': 'https://test.com'} + _moch_response = Mock() + _expected_test_result = { + 'page': 1, + 'pageSize': 100, + 'total': 1, + 'items': [{'item': {'id': 11693324, 'name': 'test name'}}] + } + _moch_response.return_value = _expected_test_result + + mock_get.return_value = _moch_response + + query_result = get_single_item(_cb_config, _item_id) + self.assertEqual(query_result, _moch_response) + + @patch('lobster.tools.codebeamer.codebeamer.query_cb_single') + def test_get_many_items(self, mock_get): + _item_ids = {24406947, 21747817} + _cb_config = {'base': 'https://test.com', 'page_size': 100} + _response_items = [ + {'id': 24406947, 'name': 'Test name 1'}, + {'id': 21747817, 'name': 'Test name 2'} + ] + _moch_response = { + 'page': 1, + 'pageSize': 100, + 'total': 2, + 'items': _response_items + } + + mock_get.return_value = _moch_response + + query_result = get_many_items(_cb_config, _item_ids) + self.assertEqual(query_result, _response_items) + + @patch('lobster.tools.codebeamer.codebeamer.query_cb_single') + def test_import_tagged(self, mock_get): + _mh = Message_Handler() + _item_ids = {24406947, 21747817} + _cb_config = {'root': 'https://test.com/', 'base': 'https://test.com/base', 'page_size': 100} + _response_items = [ + { + 'id': 24406947, + 'name': 'Test name 1', + 'categories': [{'name': 'Folder'}], + 'version': 7, + 'status': {'name': 'status'}, + 'tracker': {'id': 123} + }, + { + 'id': 21747817, + 'name': 'Test name 2', + 'categories': [{'name': 'Folder'}], + 'version': 10, + 'status': {'name': 'status'}, + 'tracker': {'id': 123} + } + ] + _mock_response = { + 'page': 1, + 'pageSize': 100, + 'total': 2, + 'items': _response_items + } + mock_get.return_value = _mock_response + + _expected_result = [to_lobster(_cb_config, items) for items in _response_items] + + import_tagged_result = import_tagged(_mh, _cb_config, _item_ids) + + self._assertListEqualByAttributes(import_tagged_result, _expected_result) + + +if __name__ == '__main__': + unittest.main()