From da6ae5805b42fa6e94449b27487e341977576f04 Mon Sep 17 00:00:00 2001 From: ewuerger Date: Sat, 2 Sep 2023 10:04:07 +0200 Subject: [PATCH 01/13] wip(ci-templates)!: Install packages on special tags This is just for the staging pipeline. --- .gitlab-ci.yml | 1 + ci-templates/gitlab/synchronise_elements.yml | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c217c946..f2dca510 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -8,6 +8,7 @@ stages: .patch-pyproject-toml: &patch-pyproject-toml - sed -i -e 's/\(^ "polarion-rest-api-client\).*",/\1",/' pyproject.toml + - pip install https://$PYPI_ARTIFACTORY_USERNAME:$PYPI_ARTIFACTORY_PASSWORD@$PYPI_ARTIFACTORY/polarion-rest-api-client/attachments/polarion_rest_api_client-attachments-py3-none-any.whl wheel: stage: build diff --git a/ci-templates/gitlab/synchronise_elements.yml b/ci-templates/gitlab/synchronise_elements.yml index a089ada8..bffb77dd 100644 --- a/ci-templates/gitlab/synchronise_elements.yml +++ b/ci-templates/gitlab/synchronise_elements.yml @@ -10,7 +10,8 @@ capella2polarion_synchronise_elements: artifacts: true script: - - pip install capella2polarion --pre + - pip install https://$PYPI_ARTIFACTORY_USERNAME:$PYPI_ARTIFACTORY_PASSWORD@$PYPI_ARTIFACTORY/polarion-rest-api-client/attachments/polarion_rest_api_client-attachments-py3-none-any.whl + - pip install https://$PYPI_ARTIFACTORY_USERNAME:$PYPI_ARTIFACTORY_PASSWORD@$PYPI_ARTIFACTORY/capella2polarion/less-requests/capella2polarion-less_requests-py3-none-any.whl - > python \ -m capella2polarion \ From 80ca21b053c51395ad5356c4a344563fa424e236 Mon Sep 17 00:00:00 2001 From: Michael Harbarth Date: Tue, 12 Sep 2023 16:49:48 +0200 Subject: [PATCH 02/13] fix: don't use get_linked_text for conditions as it does not work properly for deleted references --- capella2polarion/elements/serialize.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/capella2polarion/elements/serialize.py b/capella2polarion/elements/serialize.py index 13b73a96..3a50d308 100644 --- a/capella2polarion/elements/serialize.py +++ b/capella2polarion/elements/serialize.py @@ -186,7 +186,7 @@ def include_pre_and_post_condition( def get_condition(cap: PrePostConditionElement, name: str) -> str: if not (condition := getattr(cap, name)): return "" - return get_linked_text(condition, ctx) + return condition.specification["capella:linkedText"].striptags() def strike_through(string: str) -> str: if match := RE_DESCR_DELETED_PATTERN.match(string): From 91996a473cfdbf34285155034cfe3e8533c4a164 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernst=20W=C3=BCrger?= Date: Wed, 13 Sep 2023 13:21:07 +0200 Subject: [PATCH 03/13] fix: Prevent infinite SystemActor creation Picked changes from b09ce597. --- capella2polarion/elements/element.py | 20 +++++++++++++++++--- tests/test_elements.py | 2 ++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/capella2polarion/elements/element.py b/capella2polarion/elements/element.py index 516409c7..fe39b2ee 100644 --- a/capella2polarion/elements/element.py +++ b/capella2polarion/elements/element.py @@ -13,7 +13,7 @@ from capellambse.model import diagram as diag from capella2polarion import elements -from capella2polarion.elements import serialize +from capella2polarion.elements import helpers, serialize logger = logging.getLogger(__name__) @@ -28,11 +28,25 @@ def create_work_items( ) -> list[serialize.CapellaWorkItem]: """Create a set of work items in Polarion.""" objects = chain.from_iterable(ctx["ELEMENTS"].values()) - work_items = [ + _work_items = [ serialize.element(obj, ctx, serialize.generic_work_item) for obj in objects ] - return list(filter(None.__ne__, work_items)) # type: ignore[arg-type] + _work_items = list(filter(None.__ne__, _work_items)) + valid_types = set(map(helpers.resolve_element_type, set(ctx["ELEMENTS"]))) + work_items: list[polarion_api.CapellaWorkItem] = [] + missing_types: set[str] = set() + for work_item in _work_items: + assert work_item is not None + if work_item.type in valid_types: + work_items.append(work_item) + else: + missing_types.add(work_item.type) + logger.debug( + "%r are missing in the capella2polarion configuration", + ", ".join(missing_types), + ) + return work_items def create_links( diff --git a/tests/test_elements.py b/tests/test_elements.py index c0502c22..d83b2954 100644 --- a/tests/test_elements.py +++ b/tests/test_elements.py @@ -192,6 +192,8 @@ def test_create_work_items( monkeypatch: pytest.MonkeyPatch, context: dict[str, t.Any] ): del context["ELEMENTS"]["UnsupportedFakeModelObject"] + context["MODEL"] = model = mock.MagicMock() + model.by_uuid.side_effect = context["ELEMENTS"]["FakeModelObject"] monkeypatch.setattr( serialize, "generic_work_item", From 083065b221a72372290441a43ec7229d1b775dce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernst=20W=C3=BCrger?= <50786483+ewuerger@users.noreply.github.com> Date: Tue, 26 Sep 2023 16:59:57 +0200 Subject: [PATCH 04/13] fix: Apply suggestions from code review Co-authored-by: micha91 --- capella2polarion/elements/__init__.py | 12 ++++++------ capella2polarion/elements/serialize.py | 1 - 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/capella2polarion/elements/__init__.py b/capella2polarion/elements/__init__.py index c08f97ff..c8b161f5 100644 --- a/capella2polarion/elements/__init__.py +++ b/capella2polarion/elements/__init__.py @@ -103,13 +103,13 @@ def post_work_items(ctx: dict[str, t.Any]) -> None: ctx The context for the workitem operation to be processed. """ - work_items = [ - wi - for wi in ctx["WORK_ITEMS"].values() - if wi.uuid_capella not in ctx["POLARION_ID_MAP"] - ] - for work_item in work_items: + work_items: list[serialize.CapellaWorkItem] = [] + for work_item in ctx["WORK_ITEMS"].values(): + if work_item.uuid_capella not in ctx["POLARION_ID_MAP"]: + continue + assert work_item is not None + work_items.append(work_item) logger.info("Create work item for %r...", work_item.title) if work_items: try: diff --git a/capella2polarion/elements/serialize.py b/capella2polarion/elements/serialize.py index 3a50d308..674c3670 100644 --- a/capella2polarion/elements/serialize.py +++ b/capella2polarion/elements/serialize.py @@ -48,7 +48,6 @@ class Condition(t.TypedDict): uuid_capella: str | None preCondition: Condition | None postCondition: Condition | None - checksum: str | None def element( From 39475d5bca9f572cf5fe48e20de01862c79d49d9 Mon Sep 17 00:00:00 2001 From: ewuerger Date: Wed, 11 Oct 2023 16:33:45 +0200 Subject: [PATCH 05/13] feat(model-elements): Add `input|output_exchanges` link handler --- capella2polarion/elements/element.py | 41 ++++++++++++++++++++++---- capella2polarion/elements/serialize.py | 8 +++-- tests/test_elements.py | 24 +++++++++++++-- 3 files changed, 63 insertions(+), 10 deletions(-) diff --git a/capella2polarion/elements/element.py b/capella2polarion/elements/element.py index fe39b2ee..46b831e6 100644 --- a/capella2polarion/elements/element.py +++ b/capella2polarion/elements/element.py @@ -4,6 +4,7 @@ from __future__ import annotations import collections.abc as cabc +import functools import logging import typing as t from itertools import chain @@ -11,6 +12,7 @@ import polarion_rest_api_client as polarion_api from capellambse.model import common from capellambse.model import diagram as diag +from capellambse.model.crosslayer import fa from capella2polarion import elements from capella2polarion.elements import helpers, serialize @@ -80,7 +82,7 @@ def create_links( continue if isinstance(refs, common.ElementList): - new = refs.by_uuid + new: cabc.Iterable[str] = refs.by_uuid # type: ignore[assignment] else: assert hasattr(refs, "uuid") new = [refs.uuid] @@ -141,7 +143,9 @@ def _handle_diagram_reference_links( return ref_links -def _collect_uuids(nodes: list[common.GenericElement]) -> cabc.Iterator[str]: +def _collect_uuids( + nodes: cabc.Iterable[common.GenericElement], +) -> cabc.Iterator[str]: type_resolvers = TYPE_RESOLVERS for node in nodes: uuid = node.uuid @@ -171,7 +175,34 @@ def _create( return list(filter(None.__ne__, _new_links)) -CUSTOM_LINKS = { - "description_reference": _handle_description_reference_links, - "diagram_elements": _handle_diagram_reference_links, +def _handle_exchanges( + context: dict[str, t.Any], + obj: fa.Function, + role_id: str, + links: dict[str, polarion_api.WorkItemLink], + attr: str = "inputs", +) -> list[polarion_api.WorkItemLink]: + wid = context["POLARION_ID_MAP"][obj.uuid] + exchanges: list[str] = [] + for element in getattr(obj, attr): + uuids = element.exchanges.by_uuid + exs = _get_work_item_ids(context, wid, uuids, role_id) + exchanges.extend(set(exs)) + return _create(context, wid, role_id, exchanges, links) + + +CustomLinkMaker = cabc.Callable[ + [ + dict[str, t.Any], + diag.Diagram | common.GenericElement, + str, + dict[str, t.Any], + ], + list[polarion_api.WorkItemLink], +] +CUSTOM_LINKS: dict[str, CustomLinkMaker] = { + "description_reference": _handle_description_reference_links, # type: ignore[dict-item] + "diagram_elements": _handle_diagram_reference_links, # type: ignore[dict-item] + "input_exchanges": functools.partial(_handle_exchanges, attr="inputs"), + "output_exchanges": functools.partial(_handle_exchanges, attr="outputs"), } diff --git a/capella2polarion/elements/serialize.py b/capella2polarion/elements/serialize.py index 674c3670..5c01e6cd 100644 --- a/capella2polarion/elements/serialize.py +++ b/capella2polarion/elements/serialize.py @@ -173,8 +173,7 @@ def replace_markup( f'id="fake" data-item-id="{pid}" data-option-id="long">' "" ) - else: - return non_matcher(match.group(0)) + return non_matcher(match.group(0)) def include_pre_and_post_condition( @@ -262,7 +261,10 @@ def physical_component( return work_item -SERIALIZERS = { +Serializer = cabc.Callable[ + [common.GenericElement, dict[str, t.Any]], CapellaWorkItem +] +SERIALIZERS: dict[str, Serializer] = { "CapabilityRealization": include_pre_and_post_condition, "LogicalComponent": component_or_actor, "OperationalCapability": include_pre_and_post_condition, diff --git a/tests/test_elements.py b/tests/test_elements.py index d83b2954..4a070740 100644 --- a/tests/test_elements.py +++ b/tests/test_elements.py @@ -137,7 +137,7 @@ def __init__( self, uuid: str, name: str = "", - attribute: FakeModelObject | None = None, + attribute: t.Any | None = None, ): self.uuid = uuid self.name = name @@ -237,6 +237,26 @@ def test_create_links_custom_resolver(context: dict[str, t.Any]): assert links == [expected] + @staticmethod + def test_create_links_custom_exchanges_resolver(context: dict[str, t.Any]): + function_uuid = "ceffa011-7b66-4b3c-9885-8e075e312ffa" + obj = context["MODEL"].by_uuid(function_uuid) + context["POLARION_ID_MAP"][function_uuid] = "Obj-1" + context["POLARION_ID_MAP"][ + "1a414995-f4cd-488c-8152-486e459fb9de" + ] = "Obj-2" + context["ROLES"] = {"SystemFunction": ["input_exchanges"]} + expected = polarion_api.WorkItemLink( + "Obj-1", + "Obj-2", + "input_exchanges", + secondary_work_item_project="project_id", + ) + + links = element.create_links(obj, context) + + assert links == [expected] + @staticmethod def test_create_links_missing_attribute( context: dict[str, t.Any], caplog: pytest.LogCaptureFixture @@ -282,7 +302,7 @@ def test_create_links_from_ElementList(context: dict[str, t.Any]): secondary_work_item_project="project_id", ) - links = element.create_links(obj, context) + links = element.create_links(obj, context) # type: ignore[arg-type] assert expected_link in links assert expected_link1 in links From da6a5bc2c8eef9d0099207ce7a6f2ddbd6eabe03 Mon Sep 17 00:00:00 2001 From: ewuerger Date: Wed, 11 Oct 2023 17:04:35 +0200 Subject: [PATCH 06/13] fix!: Fix `post_work_items` --- capella2polarion/elements/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/capella2polarion/elements/__init__.py b/capella2polarion/elements/__init__.py index c8b161f5..a2899692 100644 --- a/capella2polarion/elements/__init__.py +++ b/capella2polarion/elements/__init__.py @@ -105,7 +105,7 @@ def post_work_items(ctx: dict[str, t.Any]) -> None: """ work_items: list[serialize.CapellaWorkItem] = [] for work_item in ctx["WORK_ITEMS"].values(): - if work_item.uuid_capella not in ctx["POLARION_ID_MAP"]: + if work_item.uuid_capella in ctx["POLARION_ID_MAP"]: continue assert work_item is not None From c7e88ea61339a0198fc1f2785251121bec79f8f5 Mon Sep 17 00:00:00 2001 From: ewuerger Date: Thu, 19 Oct 2023 16:38:30 +0200 Subject: [PATCH 07/13] fix: Fix various bugs - Fix debug log message for empty missing types - Merge diagram work item creator into generic work item creator - Handle all roles from the wildcard entry in the config. Also include `Diagram`. --- capella2polarion/__main__.py | 63 ++++++++++++++++++++++----- capella2polarion/elements/__init__.py | 18 ++++---- capella2polarion/elements/diagram.py | 24 ---------- capella2polarion/elements/element.py | 29 ++++++++---- tests/data/model_elements/config.yaml | 15 ++++--- tests/test_cli.py | 5 --- tests/test_elements.py | 8 ++-- 7 files changed, 94 insertions(+), 68 deletions(-) delete mode 100644 capella2polarion/elements/diagram.py diff --git a/capella2polarion/__main__.py b/capella2polarion/__main__.py index 46bd34a0..097ade32 100644 --- a/capella2polarion/__main__.py +++ b/capella2polarion/__main__.py @@ -79,13 +79,30 @@ def _get_roles_from_config(ctx: dict[str, t.Any]) -> dict[str, list[str]]: roles[key] = list(role_ids) else: roles[typ] = [] - roles["Diagram"] = ["diagram_elements"] return roles def _sanitize_config( - config: dict[str, list[str | dict[str, t.Any]]], special: dict[str, t.Any] + config: dict[str, list[str | dict[str, t.Any]]], + special: list[str | dict[str, t.Any]], ) -> dict[str, t.Any]: + special_config: dict[str, t.Any] = {} + for typ in special: + if isinstance(typ, str): + special_config[typ] = None + else: + special_config.update(typ) + + lookup: dict[str, dict[str, list[str]]] = {} + for layer, xtypes in config.items(): + for xt in xtypes: + if isinstance(xt, str): + item: dict[str, list[str]] = {xt: []} + else: + item = xt + + lookup.setdefault(layer, {}).update(item) + new_config: dict[str, t.Any] = {} for layer, xtypes in config.items(): new_entries: list[str | dict[str, t.Any]] = [] @@ -93,16 +110,36 @@ def _sanitize_config( if isinstance(xtype, dict): for sub_key, sub_value in xtype.items(): new_value = ( - special.get("*", []) - + special.get(sub_key, []) + special_config.get("*", []) + + special_config.get(sub_key, []) + sub_value ) new_entries.append({sub_key: new_value}) else: - if new_value := special.get("*", []) + special.get(xtype, []): + star = special_config.get("*", []) + special_xtype = special_config.get(xtype, []) + if new_value := star + special_xtype: new_entries.append({xtype: new_value}) else: new_entries.append(xtype) + + wildcard_values = special_config.get("*", []) + for key, value in special_config.items(): + if key == "*": + continue + + if isinstance(value, list): + new_value = ( + lookup.get(layer, {}).get(key, []) + + wildcard_values + + value + ) + new_entries.append({key: new_value}) + elif value is None and key not in [ + entry if isinstance(entry, str) else list(entry.keys())[0] + for entry in new_entries + ]: + new_entries.append({key: wildcard_values}) new_config[layer] = new_entries return new_config @@ -189,16 +226,20 @@ def model_elements( ctx.obj["POLARION_ID_MAP"] = { uuid: wi.id for uuid, wi in ctx.obj["POLARION_WI_MAP"].items() } - diagrams = ctx.obj["ELEMENTS"].pop("Diagram", []) - work_items = elements.element.create_work_items(ctx.obj) - ctx.obj["ELEMENTS"]["Diagram"] = diagrams - pdiagrams = elements.diagram.create_diagrams(ctx.obj) - ctx.obj["WORK_ITEMS"] = { - wi.uuid_capella: wi for wi in work_items + pdiagrams + duuids = { + diag["uuid"] for diag in ctx.obj["DIAGRAM_IDX"] if diag["success"] } + ctx.obj["ELEMENTS"]["Diagram"] = [ + diag for diag in ctx.obj["ELEMENTS"]["Diagram"] if diag.uuid in duuids + ] + work_items = elements.element.create_work_items(ctx.obj) + ctx.obj["WORK_ITEMS"] = {wi.uuid_capella: wi for wi in work_items} elements.delete_work_items(ctx.obj) elements.post_work_items(ctx.obj) + + work_items = elements.element.create_work_items(ctx.obj) + ctx.obj["WORK_ITEMS"] = {wi.uuid_capella: wi for wi in work_items} elements.patch_work_items(ctx.obj) elements.make_model_elements_index(ctx.obj) diff --git a/capella2polarion/elements/__init__.py b/capella2polarion/elements/__init__.py index a2899692..ab9c208a 100644 --- a/capella2polarion/elements/__init__.py +++ b/capella2polarion/elements/__init__.py @@ -114,6 +114,11 @@ def post_work_items(ctx: dict[str, t.Any]) -> None: if work_items: try: ctx["API"].create_work_items(work_items) + workitems = {wi.uuid_capella: wi for wi in work_items if wi.id} + ctx["POLARION_WI_MAP"].update(workitems) + ctx["POLARION_ID_MAP"] = { + uuid: wi.id for uuid, wi in ctx["POLARION_WI_MAP"].items() + } except polarion_api.PolarionApiException as error: logger.error("Creating work items failed. %s", error.args[0]) @@ -140,21 +145,16 @@ def add_content( setattr(work_item, key, value) return work_item - ctx["POLARION_WI_MAP"] = get_polarion_wi_map(ctx) ctx["POLARION_ID_MAP"] = uuids = { uuid: wi.id for uuid, wi in ctx["POLARION_WI_MAP"].items() - if wi.status == "open" and uuid in ctx["WORK_ITEMS"] + if wi.status == "open" and wi.uuid_capella and wi.id } for uuid in uuids: elements = ctx["MODEL"] if uuid.startswith("_"): elements = ctx["MODEL"].diagrams - try: - obj = elements.by_uuid(uuid) - except KeyError: - logger.error("Weird %r", uuid) - continue + obj = elements.by_uuid(uuid) links = element.create_links(obj, ctx) @@ -189,6 +189,9 @@ def get_elements_and_type_map( if isinstance(typ, dict): typ = list(typ.keys())[0] + if typ == "Diagram": + continue + xtype = convert_type.get(typ, typ) objects = ctx["MODEL"].search(xtype, below=below) elements.setdefault(typ, []).extend(objects) @@ -277,7 +280,6 @@ def make_model_elements_index(ctx: dict[str, t.Any]) -> None: from . import ( # pylint: disable=cyclic-import api_helper, - diagram, element, helpers, serialize, diff --git a/capella2polarion/elements/diagram.py b/capella2polarion/elements/diagram.py deleted file mode 100644 index 4c82436e..00000000 --- a/capella2polarion/elements/diagram.py +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright DB Netz AG and contributors -# SPDX-License-Identifier: Apache-2.0 -"""Objects for synchronization of Capella diagrams to polarion.""" -from __future__ import annotations - -import logging -import typing as t - -from capella2polarion.elements import serialize - -logger = logging.getLogger(__name__) - - -def create_diagrams(ctx: dict[str, t.Any]) -> list[serialize.CapellaWorkItem]: - """Return a set of new work items of type ``diagram``.""" - uuids = {diag["uuid"] for diag in ctx["DIAGRAM_IDX"] if diag["success"]} - diagrams = [ - diag for diag in ctx["ELEMENTS"]["Diagram"] if diag.uuid in uuids - ] - work_items = [ - serialize.element(diagram, ctx, serialize.diagram) - for diagram in diagrams - ] - return list(filter(None.__ne__, work_items)) # type:ignore[arg-type] diff --git a/capella2polarion/elements/element.py b/capella2polarion/elements/element.py index 46b831e6..84ae08d5 100644 --- a/capella2polarion/elements/element.py +++ b/capella2polarion/elements/element.py @@ -28,12 +28,21 @@ def create_work_items( ctx: dict[str, t.Any] ) -> list[serialize.CapellaWorkItem]: - """Create a set of work items in Polarion.""" + """Create a list of work items for Polarion.""" objects = chain.from_iterable(ctx["ELEMENTS"].values()) - _work_items = [ - serialize.element(obj, ctx, serialize.generic_work_item) - for obj in objects + _work_items = [] + serializer: cabc.Callable[ + [diag.Diagram | common.GenericElement, dict[str, t.Any]], + serialize.CapellaWorkItem, ] + for obj in objects: + if isinstance(obj, diag.Diagram): + serializer = serialize.diagram + else: + serializer = serialize.generic_work_item + + _work_items.append(serialize.element(obj, ctx, serializer)) + _work_items = list(filter(None.__ne__, _work_items)) valid_types = set(map(helpers.resolve_element_type, set(ctx["ELEMENTS"]))) work_items: list[polarion_api.CapellaWorkItem] = [] @@ -44,10 +53,12 @@ def create_work_items( work_items.append(work_item) else: missing_types.add(work_item.type) - logger.debug( - "%r are missing in the capella2polarion configuration", - ", ".join(missing_types), - ) + + if missing_types: + logger.debug( + "%r are missing in the capella2polarion configuration", + ", ".join(missing_types), + ) return work_items @@ -118,7 +129,7 @@ def _handle_description_reference_links( role_id: str, links: dict[str, polarion_api.WorkItemLink], ) -> list[polarion_api.WorkItemLink]: - refs = context["DESCR_REFERENCES"].get(obj.uuid) + refs = context["DESCR_REFERENCES"].get(obj.uuid, []) wid = context["POLARION_ID_MAP"][obj.uuid] refs = set(_get_work_item_ids(context, wid, refs, role_id)) return _create(context, wid, role_id, refs, links) diff --git a/tests/data/model_elements/config.yaml b/tests/data/model_elements/config.yaml index e8647854..e032dcbb 100644 --- a/tests/data/model_elements/config.yaml +++ b/tests/data/model_elements/config.yaml @@ -2,11 +2,14 @@ # SPDX-License-Identifier: Apache-2.0 "*": # All layers - "*": # All class types - - parent # Specify workitem links - - description_reference # Custom attribute - Class: - - state_machines + - "*": # All class types + - parent # Specify workitem links + - description_reference # Custom attribute + - Class: + - state_machines + - Diagram: + - diagram_elements + - Constraint oa: # Specify below - OperationalCapability: # Capella Type with references @@ -19,7 +22,6 @@ oa: # Specify below - CommunicationMean - Class - StateMachine - - Constraint sa: - SystemComponent: @@ -34,7 +36,6 @@ sa: - exchanged_items - ExchangeItem - Class - - Constraint pa: - PhysicalComponent: diff --git a/tests/test_cli.py b/tests/test_cli.py index a766ff82..0b752e17 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -67,10 +67,6 @@ def test_migrate_model_elements(monkeypatch: pytest.MonkeyPatch): {}, ), ) - mock_create_diagrams = mock.MagicMock() - monkeypatch.setattr( - elements.diagram, "create_diagrams", mock_create_diagrams - ) mock_delete_work_items = mock.MagicMock() monkeypatch.setattr(elements, "delete_work_items", mock_delete_work_items) mock_post_work_items = mock.MagicMock() @@ -93,5 +89,4 @@ def test_migrate_model_elements(monkeypatch: pytest.MonkeyPatch): assert mock_delete_work_items.call_count == 1 assert mock_patch_work_items.call_count == 1 assert mock_post_work_items.call_count == 1 - assert mock_create_diagrams.call_count == 1 assert ELEMENTS_IDX_PATH.exists() diff --git a/tests/test_elements.py b/tests/test_elements.py index 4a070740..de07e915 100644 --- a/tests/test_elements.py +++ b/tests/test_elements.py @@ -14,7 +14,7 @@ from capellambse.model import common from capella2polarion import elements -from capella2polarion.elements import diagram, element, helpers, serialize +from capella2polarion.elements import element, helpers, serialize # pylint: disable-next=relative-beyond-top-level, useless-suppression from .conftest import TEST_DIAGRAM_CACHE, TEST_HOST # type: ignore[import] @@ -90,7 +90,7 @@ def context( def test_create_diagrams(context: dict[str, t.Any]): context["ELEMENTS"] = {"Diagram": context["MODEL"].diagrams} - diagrams = diagram.create_diagrams(context) + diagrams = element.create_work_items(context) assert len(diagrams) == 1 work_item = diagrams[0] @@ -116,7 +116,7 @@ def test_create_diagrams_filters_non_diagram_elements( attributes.return_value = None monkeypatch.setattr(serialize, "element", attributes) - diagram.create_diagrams(context) + element.create_work_items(context) assert context["API"].create_work_items.call_count == 0 @@ -385,7 +385,7 @@ def test_update_work_items_filters_work_items_with_same_checksum( @staticmethod def test_update_links_with_no_elements(context: dict[str, t.Any]): - context["POLARION_ID_MAP"] = {} + context["POLARION_WI_MAP"] = {} elements.patch_work_items(context) From ece1cde8489c6eff36caaab307d0eb0801f2e577 Mon Sep 17 00:00:00 2001 From: ewuerger Date: Thu, 19 Oct 2023 16:47:08 +0200 Subject: [PATCH 08/13] refactor: Simplify high level calls --- capella2polarion/__main__.py | 7 +++---- capella2polarion/elements/element.py | 5 +++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/capella2polarion/__main__.py b/capella2polarion/__main__.py index 097ade32..23e0f2e8 100644 --- a/capella2polarion/__main__.py +++ b/capella2polarion/__main__.py @@ -232,14 +232,13 @@ def model_elements( ctx.obj["ELEMENTS"]["Diagram"] = [ diag for diag in ctx.obj["ELEMENTS"]["Diagram"] if diag.uuid in duuids ] - work_items = elements.element.create_work_items(ctx.obj) - ctx.obj["WORK_ITEMS"] = {wi.uuid_capella: wi for wi in work_items} + elements.element.create_work_items(ctx.obj) elements.delete_work_items(ctx.obj) elements.post_work_items(ctx.obj) - work_items = elements.element.create_work_items(ctx.obj) - ctx.obj["WORK_ITEMS"] = {wi.uuid_capella: wi for wi in work_items} + # Create missing links b/c of unresolved references + elements.element.create_work_items(ctx.obj) elements.patch_work_items(ctx.obj) elements.make_model_elements_index(ctx.obj) diff --git a/capella2polarion/elements/element.py b/capella2polarion/elements/element.py index 84ae08d5..c64ef780 100644 --- a/capella2polarion/elements/element.py +++ b/capella2polarion/elements/element.py @@ -59,6 +59,7 @@ def create_work_items( "%r are missing in the capella2polarion configuration", ", ".join(missing_types), ) + ctx["WORK_ITEMS"] = {wi.uuid_capella: wi for wi in work_items} return work_items @@ -212,8 +213,8 @@ def _handle_exchanges( list[polarion_api.WorkItemLink], ] CUSTOM_LINKS: dict[str, CustomLinkMaker] = { - "description_reference": _handle_description_reference_links, # type: ignore[dict-item] - "diagram_elements": _handle_diagram_reference_links, # type: ignore[dict-item] + "description_reference": _handle_description_reference_links, + "diagram_elements": _handle_diagram_reference_links, "input_exchanges": functools.partial(_handle_exchanges, attr="inputs"), "output_exchanges": functools.partial(_handle_exchanges, attr="outputs"), } From 6136835c2294097cb1f289d2da58c1aaf681dc1f Mon Sep 17 00:00:00 2001 From: ewuerger Date: Thu, 19 Oct 2023 16:56:05 +0200 Subject: [PATCH 09/13] fix: Prepare for production pipelines --- .gitlab-ci.yml | 1 - ci-templates/gitlab/synchronise_elements.yml | 3 +-- pyproject.toml | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f2dca510..c217c946 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -8,7 +8,6 @@ stages: .patch-pyproject-toml: &patch-pyproject-toml - sed -i -e 's/\(^ "polarion-rest-api-client\).*",/\1",/' pyproject.toml - - pip install https://$PYPI_ARTIFACTORY_USERNAME:$PYPI_ARTIFACTORY_PASSWORD@$PYPI_ARTIFACTORY/polarion-rest-api-client/attachments/polarion_rest_api_client-attachments-py3-none-any.whl wheel: stage: build diff --git a/ci-templates/gitlab/synchronise_elements.yml b/ci-templates/gitlab/synchronise_elements.yml index bffb77dd..a089ada8 100644 --- a/ci-templates/gitlab/synchronise_elements.yml +++ b/ci-templates/gitlab/synchronise_elements.yml @@ -10,8 +10,7 @@ capella2polarion_synchronise_elements: artifacts: true script: - - pip install https://$PYPI_ARTIFACTORY_USERNAME:$PYPI_ARTIFACTORY_PASSWORD@$PYPI_ARTIFACTORY/polarion-rest-api-client/attachments/polarion_rest_api_client-attachments-py3-none-any.whl - - pip install https://$PYPI_ARTIFACTORY_USERNAME:$PYPI_ARTIFACTORY_PASSWORD@$PYPI_ARTIFACTORY/capella2polarion/less-requests/capella2polarion-less_requests-py3-none-any.whl + - pip install capella2polarion --pre - > python \ -m capella2polarion \ diff --git a/pyproject.toml b/pyproject.toml index b9a600b9..0bb414cd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,7 @@ dependencies = [ "capellambse", "click", "PyYAML", - "polarion-rest-api-client @ git+https://github.com/DSD-DBS/polarion-rest-api-client.git@feat-add-attachments-workitem-relations", + "polarion-rest-api-client @ git+https://github.com/DSD-DBS/polarion-rest-api-client.git@v0.2.1", "requests", ] From fcf080f235a26aa57471d5d9c5ca691bb28f5a7c Mon Sep 17 00:00:00 2001 From: ewuerger Date: Fri, 20 Oct 2023 13:34:09 +0200 Subject: [PATCH 10/13] feat: Add requirement types custom fields to generic serializer --- capella2polarion/elements/serialize.py | 38 ++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/capella2polarion/elements/serialize.py b/capella2polarion/elements/serialize.py index 5c01e6cd..210cd525 100644 --- a/capella2polarion/elements/serialize.py +++ b/capella2polarion/elements/serialize.py @@ -4,6 +4,7 @@ from __future__ import annotations import base64 +import collections import collections.abc as cabc import logging import mimetypes @@ -113,6 +114,7 @@ def _generic_work_item( raw_description = getattr(obj, "description", markupsafe.Markup("")) uuids, value = _sanitize_description(raw_description, ctx) ctx.setdefault("DESCR_REFERENCES", {})[obj.uuid] = uuids + requirement_types = _get_requirement_types_text(obj) return CapellaWorkItem( type=helpers.resolve_element_type(xtype), title=obj.name, @@ -120,9 +122,45 @@ def _generic_work_item( description=value, status="open", uuid_capella=obj.uuid, + **requirement_types, ) +def _get_requirement_types_text( + obj: common.GenericElement, +) -> dict[str, dict[str, str]]: + type_texts = collections.defaultdict(list) + for req in obj.requirements: + if req is None: + logger.error( + "RequirementsRelation with broken target found %r", obj.name + ) + continue + + if not (req.type and req.text): + continue + + type_texts[req.type.long_name].append(req.text) + return _format_texts(type_texts) + + +def _format_texts( + type_texts: dict[str, list[str]] +) -> dict[str, dict[str, str]]: + def _format(texts: list[str]) -> dict[str, str]: + if len(texts) > 1: + items = "".join(f"
  • {text}
  • " for text in texts) + text = f"
      {items}
    " + else: + text = texts[0] + return {"type": "text/html", "value": text} + + requirement_types = {} + for typ, texts in type_texts.items(): + requirement_types[typ.lower()] = _format(texts) + return requirement_types + + def _sanitize_description( descr: markupsafe.Markup, ctx: dict[str, t.Any] ) -> tuple[list[str], markupsafe.Markup]: From 60b237ec3e125dadc3c6b5e5458382dc9afe52f5 Mon Sep 17 00:00:00 2001 From: ewuerger Date: Mon, 23 Oct 2023 16:15:36 +0200 Subject: [PATCH 11/13] test: Fix test with requirements field --- capella2polarion/elements/serialize.py | 3 +++ tests/test_elements.py | 37 +++++++++++++++++++------- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/capella2polarion/elements/serialize.py b/capella2polarion/elements/serialize.py index 210cd525..51f73534 100644 --- a/capella2polarion/elements/serialize.py +++ b/capella2polarion/elements/serialize.py @@ -138,6 +138,9 @@ def _get_requirement_types_text( continue if not (req.type and req.text): + logger.warning( + "Requirement without text or type found %r", req.name + ) continue type_texts[req.type.long_name].append(req.text) diff --git a/tests/test_elements.py b/tests/test_elements.py index de07e915..7d0f5800 100644 --- a/tests/test_elements.py +++ b/tests/test_elements.py @@ -62,6 +62,12 @@ TEST_WI_CHECKSUM = ( "73508ec0c3048c5b33316dfa56ef5e5f4179ff69efaa209e47ab65b111415e82" ) +TEST_REQ_TEXT = ( + "

    Test requirement 1 really l o n g text that is way too long to " + "display here as that

    \n\n

    < > " '

    \n\n
      \n\t
    • " + "This is a list
    • \n\t
    • an unordered one
    • \n
    \n\n
      \n\t" + "
    1. Ordered list
    2. \n\t
    3. Ok
    4. \n
    \n" +) class TestDiagramElements: @@ -478,7 +484,7 @@ def test__decode_diagram(): @pytest.mark.parametrize( "uuid,expected", [ - ( + pytest.param( TEST_ELEMENT_UUID, { "type": "logicalComponent", @@ -486,9 +492,14 @@ def test__decode_diagram(): "uuid_capella": TEST_ELEMENT_UUID, "description_type": "text/html", "description": markupsafe.Markup(TEST_DESCR), + "reqtype": { + "type": "text/html", + "value": markupsafe.Markup(TEST_REQ_TEXT), + }, }, + id="logicalComponent", ), - ( + pytest.param( TEST_OCAP_UUID, { "type": "operationalCapability", @@ -501,8 +512,9 @@ def test__decode_diagram(): "postCondition": {"type": "text/html", "value": ""}, }, }, + id="operationalCapability", ), - ( + pytest.param( TEST_WE_UUID, { "type": "entity", @@ -511,8 +523,9 @@ def test__decode_diagram(): "description_type": "text/html", "description": markupsafe.Markup(TEST_WE_DESCR), }, + id="entity", ), - ( + pytest.param( TEST_ACTOR_UUID, { "type": "logicalActor", @@ -524,8 +537,9 @@ def test__decode_diagram(): "and greatest mage of all time.

    \n" ), }, + id="logicalActor", ), - ( + pytest.param( TEST_PHYS_COMP, { "type": "physicalComponent", @@ -534,8 +548,9 @@ def test__decode_diagram(): "description_type": "text/html", "description": markupsafe.Markup(""), }, + id="physicalComponent", ), - ( + pytest.param( TEST_PHYS_NODE, { "type": "physicalComponentNode", @@ -544,8 +559,9 @@ def test__decode_diagram(): "description_type": "text/html", "description": markupsafe.Markup(""), }, + id="physicalComponentNode", ), - ( + pytest.param( TEST_SCENARIO, { "type": "scenario", @@ -558,8 +574,9 @@ def test__decode_diagram(): "postCondition": {"type": "text/html", "value": ""}, }, }, + id="scenario", ), - ( + pytest.param( TEST_CAP_REAL, { "type": "capabilityRealization", @@ -572,8 +589,9 @@ def test__decode_diagram(): "postCondition": {"type": "text/html", "value": ""}, }, }, + id="capabilityRealization", ), - ( + pytest.param( TEST_CONSTRAINT, { "type": "constraint", @@ -584,6 +602,7 @@ def test__decode_diagram(): "This is a test context.Make Food" ), }, + id="constraint", ), ], ) From 9f78cf2345da2b8e1b0860902c318bd1c41aa319 Mon Sep 17 00:00:00 2001 From: ewuerger Date: Fri, 20 Oct 2023 13:34:09 +0200 Subject: [PATCH 12/13] feat: Add requirement types custom fields to generic serializer --- capella2polarion/elements/serialize.py | 38 ++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/capella2polarion/elements/serialize.py b/capella2polarion/elements/serialize.py index 6a0bfcf8..1f192799 100644 --- a/capella2polarion/elements/serialize.py +++ b/capella2polarion/elements/serialize.py @@ -4,6 +4,7 @@ from __future__ import annotations import base64 +import collections import collections.abc as cabc import logging import mimetypes @@ -113,6 +114,7 @@ def _generic_work_item( raw_description = getattr(obj, "description", markupsafe.Markup("")) uuids, value = _sanitize_description(obj, raw_description, ctx) ctx.setdefault("DESCR_REFERENCES", {})[obj.uuid] = uuids + requirement_types = _get_requirement_types_text(obj) return CapellaWorkItem( type=helpers.resolve_element_type(xtype), title=obj.name, @@ -120,9 +122,45 @@ def _generic_work_item( description=value, status="open", uuid_capella=obj.uuid, + **requirement_types, ) +def _get_requirement_types_text( + obj: common.GenericElement, +) -> dict[str, dict[str, str]]: + type_texts = collections.defaultdict(list) + for req in obj.requirements: + if req is None: + logger.error( + "RequirementsRelation with broken target found %r", obj.name + ) + continue + + if not (req.type and req.text): + continue + + type_texts[req.type.long_name].append(req.text) + return _format_texts(type_texts) + + +def _format_texts( + type_texts: dict[str, list[str]] +) -> dict[str, dict[str, str]]: + def _format(texts: list[str]) -> dict[str, str]: + if len(texts) > 1: + items = "".join(f"
  • {text}
  • " for text in texts) + text = f"
      {items}
    " + else: + text = texts[0] + return {"type": "text/html", "value": text} + + requirement_types = {} + for typ, texts in type_texts.items(): + requirement_types[typ.lower()] = _format(texts) + return requirement_types + + def _sanitize_description( obj: common.GenericElement, descr: markupsafe.Markup, ctx: dict[str, t.Any] ) -> tuple[list[str], markupsafe.Markup]: From 2a3d26b432d7439d07c574c398b3c829bbdd3f0c Mon Sep 17 00:00:00 2001 From: ewuerger Date: Mon, 23 Oct 2023 16:15:36 +0200 Subject: [PATCH 13/13] test: Fix test with requirements field --- capella2polarion/elements/serialize.py | 3 +++ tests/test_elements.py | 37 +++++++++++++++++++------- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/capella2polarion/elements/serialize.py b/capella2polarion/elements/serialize.py index 1f192799..7a1ee35d 100644 --- a/capella2polarion/elements/serialize.py +++ b/capella2polarion/elements/serialize.py @@ -138,6 +138,9 @@ def _get_requirement_types_text( continue if not (req.type and req.text): + logger.warning( + "Requirement without text or type found %r", req.name + ) continue type_texts[req.type.long_name].append(req.text) diff --git a/tests/test_elements.py b/tests/test_elements.py index de07e915..7d0f5800 100644 --- a/tests/test_elements.py +++ b/tests/test_elements.py @@ -62,6 +62,12 @@ TEST_WI_CHECKSUM = ( "73508ec0c3048c5b33316dfa56ef5e5f4179ff69efaa209e47ab65b111415e82" ) +TEST_REQ_TEXT = ( + "

    Test requirement 1 really l o n g text that is way too long to " + "display here as that

    \n\n

    < > " '

    \n\n
      \n\t
    • " + "This is a list
    • \n\t
    • an unordered one
    • \n
    \n\n
      \n\t" + "
    1. Ordered list
    2. \n\t
    3. Ok
    4. \n
    \n" +) class TestDiagramElements: @@ -478,7 +484,7 @@ def test__decode_diagram(): @pytest.mark.parametrize( "uuid,expected", [ - ( + pytest.param( TEST_ELEMENT_UUID, { "type": "logicalComponent", @@ -486,9 +492,14 @@ def test__decode_diagram(): "uuid_capella": TEST_ELEMENT_UUID, "description_type": "text/html", "description": markupsafe.Markup(TEST_DESCR), + "reqtype": { + "type": "text/html", + "value": markupsafe.Markup(TEST_REQ_TEXT), + }, }, + id="logicalComponent", ), - ( + pytest.param( TEST_OCAP_UUID, { "type": "operationalCapability", @@ -501,8 +512,9 @@ def test__decode_diagram(): "postCondition": {"type": "text/html", "value": ""}, }, }, + id="operationalCapability", ), - ( + pytest.param( TEST_WE_UUID, { "type": "entity", @@ -511,8 +523,9 @@ def test__decode_diagram(): "description_type": "text/html", "description": markupsafe.Markup(TEST_WE_DESCR), }, + id="entity", ), - ( + pytest.param( TEST_ACTOR_UUID, { "type": "logicalActor", @@ -524,8 +537,9 @@ def test__decode_diagram(): "and greatest mage of all time.

    \n" ), }, + id="logicalActor", ), - ( + pytest.param( TEST_PHYS_COMP, { "type": "physicalComponent", @@ -534,8 +548,9 @@ def test__decode_diagram(): "description_type": "text/html", "description": markupsafe.Markup(""), }, + id="physicalComponent", ), - ( + pytest.param( TEST_PHYS_NODE, { "type": "physicalComponentNode", @@ -544,8 +559,9 @@ def test__decode_diagram(): "description_type": "text/html", "description": markupsafe.Markup(""), }, + id="physicalComponentNode", ), - ( + pytest.param( TEST_SCENARIO, { "type": "scenario", @@ -558,8 +574,9 @@ def test__decode_diagram(): "postCondition": {"type": "text/html", "value": ""}, }, }, + id="scenario", ), - ( + pytest.param( TEST_CAP_REAL, { "type": "capabilityRealization", @@ -572,8 +589,9 @@ def test__decode_diagram(): "postCondition": {"type": "text/html", "value": ""}, }, }, + id="capabilityRealization", ), - ( + pytest.param( TEST_CONSTRAINT, { "type": "constraint", @@ -584,6 +602,7 @@ def test__decode_diagram(): "This is a test context.Make Food" ), }, + id="constraint", ), ], )