From 64d06926dd09e400dfb3c7e17254d910108ffa82 Mon Sep 17 00:00:00 2001 From: ewuerger Date: Thu, 12 Dec 2024 16:41:46 +0100 Subject: [PATCH 1/6] feat: Add `layer` custom field Added `layer` custom field to generic and diagram serializer. --- .../converters/element_converter.py | 19 ++++++ .../converters/model_converter.py | 31 ++++++++- tests/test_workitem_attachments.py | 67 ++++++++----------- 3 files changed, 76 insertions(+), 41 deletions(-) diff --git a/capella2polarion/converters/element_converter.py b/capella2polarion/converters/element_converter.py index 56b9409..476d691 100644 --- a/capella2polarion/converters/element_converter.py +++ b/capella2polarion/converters/element_converter.py @@ -1,6 +1,7 @@ # Copyright DB InfraGO AG and contributors # SPDX-License-Identifier: Apache-2.0 """Objects for serialization of capella objects to workitems.""" + from __future__ import annotations import collections @@ -33,6 +34,14 @@ logger = logging.getLogger(__name__) C2P_IMAGE_PREFIX = "__C2P__" JINJA_RENDERED_IMG_CLS = "jinja-rendered-image" +ARCHITECTURE_LAYERS: dict[str, str] = { + "common": "Common", + "oa": "Operational Analysis", + "sa": "System Analysis", + "la": "Logical Architecture", + "pa": "Physical Architecture", + "epbs": "EPBS", +} def resolve_element_type(type_: str) -> str: @@ -424,6 +433,10 @@ def __generic_work_item( obj, raw_description or markupsafe.Markup("") ) converter_data.description_references = uuids + layer = polarion_api.TextContent( + type="string", + value=ARCHITECTURE_LAYERS.get(converter_data.layer, "UNKNOWN"), + ) requirement_types = self._get_requirement_types_text(obj) converter_data.work_item = data_model.CapellaWorkItem( @@ -433,6 +446,7 @@ def __generic_work_item( uuid_capella=obj.uuid, description=polarion_api.HtmlContent(value), status="open", + layer=layer, **requirement_types, # type:ignore[arg-type] ) assert converter_data.work_item is not None @@ -451,6 +465,10 @@ def _diagram( assert converter_data.work_item is not None assert isinstance(diagram, m.Diagram) work_item_id = converter_data.work_item.id + layer = polarion_api.TextContent( + type="string", + value=ARCHITECTURE_LAYERS.get(converter_data.layer, "UNKNOWN"), + ) diagram_html, attachment = self._draw_diagram_svg( diagram, @@ -473,6 +491,7 @@ def _diagram( uuid_capella=diagram.uuid, description=polarion_api.HtmlContent(diagram_html), status="open", + layer=layer, ) if attachment: self._add_attachment(converter_data.work_item, attachment) diff --git a/capella2polarion/converters/model_converter.py b/capella2polarion/converters/model_converter.py index 07204c3..eb7d331 100644 --- a/capella2polarion/converters/model_converter.py +++ b/capella2polarion/converters/model_converter.py @@ -9,6 +9,7 @@ import capellambse import polarion_rest_api_client as polarion_api +from capellambse import model as m from capella2polarion import data_model from capella2polarion.connectors import polarion_repo @@ -63,8 +64,9 @@ def read_model( if config.diagram_config: for d in self.model.diagrams: + layer = get_layer_name(d) self.converter_session[d.uuid] = data_session.ConverterData( - "", config.diagram_config, d + layer, config.diagram_config, d ) if missing_types: @@ -176,3 +178,30 @@ def generate_work_item_links( link_serializer.create_grouped_back_link_fields( converter_data.work_item, local_back_links ) + + +def get_layer_name(diagram: m.Diagram) -> str: + """Return the layer name for a diagram.""" + match diagram.type.name: + case ( + "OEBD" + | "OAIB" + | "OAB" + | "OABD" + | "ORB" + | "OES" + | "OAS" + | "OPD" + | "OCB" + ): + return "oa" + case "CM" | "MB" | "CC" | "MCB" | "SFBD" | "SDFB" | "SAB" | "CSA": + return "sa" + case "LCBD" | "LFBD" | "LDFB" | "LAB" | "CRR": + return "la" + case "PFBD" | "PDFB" | "PCBD" | "PAB" | "PPD": + return "pa" + case "EAB" | "CIBD": + return "epbs" + case _: + return "common" diff --git a/tests/test_workitem_attachments.py b/tests/test_workitem_attachments.py index 4c19f99..5642c5c 100644 --- a/tests/test_workitem_attachments.py +++ b/tests/test_workitem_attachments.py @@ -98,34 +98,38 @@ def set_attachment_ids(attachments: list[polarion_api.WorkItemAttachment]): def test_diagram_no_attachments(model: capellambse.MelodyModel): + diag = model.diagrams.by_uuid(TEST_DIAG_UUID) converter = model_converter.ModelConverter(model, "TEST") converter.converter_session[TEST_DIAG_UUID] = data_session.ConverterData( - "", + model_converter.get_layer_name(diag), converter_config.CapellaTypeConfig("diagram", "diagram", []), - model.diagrams.by_uuid(TEST_DIAG_UUID), + diag, ) + converter.generate_work_items( polarion_repo.PolarionDataRepository(), False, False ) + work_item = converter.converter_session[TEST_DIAG_UUID].work_item assert work_item is not None assert work_item.attachments == [] def test_diagram_has_attachments(model: capellambse.MelodyModel): + diag = model.diagrams.by_uuid(TEST_DIAG_UUID) converter = model_converter.ModelConverter(model, "TEST") converter.converter_session[TEST_DIAG_UUID] = data_session.ConverterData( - "", + model_converter.get_layer_name(diag), converter_config.CapellaTypeConfig("diagram", "diagram", []), - model.diagrams.by_uuid(TEST_DIAG_UUID), + diag, ) + converter.generate_work_items( polarion_repo.PolarionDataRepository(), False, True ) work_item = converter.converter_session[TEST_DIAG_UUID].work_item assert work_item is not None - assert len(work_item.attachments) == 2 @@ -134,11 +138,11 @@ def test_diagram_attachments_new( model: capellambse.MelodyModel, worker: polarion_worker.CapellaPolarionWorker, ): + diag = model.diagrams.by_uuid(TEST_DIAG_UUID) converter = model_converter.ModelConverter(model, "TEST") worker.polarion_data_repo = polarion_repo.PolarionDataRepository( [data_model.CapellaWorkItem(WORKITEM_ID, uuid_capella=TEST_DIAG_UUID)] ) - worker.project_client.work_items.get.return_value = ( data_model.CapellaWorkItem(WORKITEM_ID, uuid_capella=TEST_DIAG_UUID) ) @@ -146,15 +150,13 @@ def test_diagram_attachments_new( worker.project_client.work_items.attachments.create.side_effect = ( set_attachment_ids ) - converter.converter_session[TEST_DIAG_UUID] = data_session.ConverterData( - "", + model_converter.get_layer_name(diag), converter_config.CapellaTypeConfig("diagram", "diagram", []), - model.diagrams.by_uuid(TEST_DIAG_UUID), + diag, ) converter.generate_work_items(worker.polarion_data_repo, False, True) - worker.compare_and_update_work_item( converter.converter_session[TEST_DIAG_UUID] ) @@ -162,21 +164,18 @@ def test_diagram_attachments_new( assert worker.project_client.work_items.update.call_count == 1 assert worker.project_client.work_items.attachments.create.call_count == 1 assert worker.project_client.work_items.attachments.get_all.call_count == 0 - created_attachments: list[polarion_api.WorkItemAttachment] = ( worker.project_client.work_items.attachments.create.call_args.args[0] ) work_item: data_model.CapellaWorkItem = ( worker.project_client.work_items.update.call_args.args[0] ) - assert len(created_attachments) == 2 assert created_attachments[0].title == created_attachments[1].title assert ( created_attachments[0].file_name[:3] == created_attachments[0].file_name[:3] ) - assert work_item.description.value == TEST_DIAG_DESCR.format( title="Diagram", attachment_id="1-__C2P__diagram.svg", @@ -191,10 +190,9 @@ def test_new_diagram( model: capellambse.MelodyModel, worker: polarion_worker.CapellaPolarionWorker, ): + diag = model.diagrams.by_uuid(TEST_DIAG_UUID) converter = model_converter.ModelConverter(model, "TEST") - checksum = json.dumps({"__C2P__WORK_ITEM": DIAGRAM_WI_CHECKSUM}) - worker.polarion_data_repo = polarion_repo.PolarionDataRepository( [ data_model.CapellaWorkItem( @@ -202,7 +200,6 @@ def test_new_diagram( ) ] ) - worker.project_client.work_items.get.return_value = ( data_model.CapellaWorkItem( WORKITEM_ID, uuid_capella=TEST_DIAG_UUID, checksum=checksum @@ -212,15 +209,13 @@ def test_new_diagram( worker.project_client.work_items.attachments.create.side_effect = ( set_attachment_ids ) - converter.converter_session[TEST_DIAG_UUID] = data_session.ConverterData( - "", + model_converter.get_layer_name(diag), converter_config.CapellaTypeConfig("diagram", "diagram", []), - model.diagrams.by_uuid(TEST_DIAG_UUID), + diag, ) converter.generate_work_items(worker.polarion_data_repo, False, True) - worker.compare_and_update_work_item( converter.converter_session[TEST_DIAG_UUID] ) @@ -241,6 +236,7 @@ def test_diagram_attachments_updated( model: capellambse.MelodyModel, worker: polarion_worker.CapellaPolarionWorker, ): + diag = model.diagrams.by_uuid(TEST_DIAG_UUID) converter = model_converter.ModelConverter(model, "TEST") worker.polarion_data_repo = polarion_repo.PolarionDataRepository( [data_model.CapellaWorkItem(WORKITEM_ID, uuid_capella=TEST_DIAG_UUID)] @@ -259,7 +255,6 @@ def test_diagram_attachments_updated( file_name="__C2P__diagram.png", ), ] - worker.project_client.work_items.get.return_value = ( data_model.CapellaWorkItem( WORKITEM_ID, @@ -267,20 +262,17 @@ def test_diagram_attachments_updated( attachments=existing_attachments, ) ) - worker.project_client.work_items.attachments.get_all = mock.MagicMock() worker.project_client.work_items.attachments.get_all.return_value = ( existing_attachments ) - converter.converter_session[TEST_DIAG_UUID] = data_session.ConverterData( - "", + model_converter.get_layer_name(diag), converter_config.CapellaTypeConfig("diagram", "diagram", []), - model.diagrams.by_uuid(TEST_DIAG_UUID), + diag, ) converter.generate_work_items(worker.polarion_data_repo, False, True) - worker.compare_and_update_work_item( converter.converter_session[TEST_DIAG_UUID] ) @@ -289,11 +281,9 @@ def test_diagram_attachments_updated( assert worker.project_client.work_items.attachments.create.call_count == 0 assert worker.project_client.work_items.attachments.update.call_count == 2 assert worker.project_client.work_items.attachments.get_all.call_count == 1 - work_item: data_model.CapellaWorkItem = ( worker.project_client.work_items.update.call_args.args[0] ) - assert work_item.description.value == TEST_DIAG_DESCR.format( title="Diagram", attachment_id="SVG-ATTACHMENT", @@ -306,6 +296,7 @@ def test_diagram_attachments_unchanged_work_item_changed( model: capellambse.MelodyModel, worker: polarion_worker.CapellaPolarionWorker, ): + diag = model.diagrams.by_uuid(TEST_DIAG_UUID) converter = model_converter.ModelConverter(model, "TEST") diagram_work_item = data_model.CapellaWorkItem( WORKITEM_ID, @@ -339,15 +330,13 @@ def test_diagram_attachments_unchanged_work_item_changed( worker.project_client.work_items.attachments.get_all.return_value = ( diagram_work_item.attachments ) - converter.converter_session[TEST_DIAG_UUID] = data_session.ConverterData( - "", + model_converter.get_layer_name(diag), converter_config.CapellaTypeConfig("diagram", "diagram", []), - model.diagrams.by_uuid(TEST_DIAG_UUID), + diag, ) converter.generate_work_items(worker.polarion_data_repo, False, True) - worker.compare_and_update_work_item( converter.converter_session[TEST_DIAG_UUID] ) @@ -357,11 +346,9 @@ def test_diagram_attachments_unchanged_work_item_changed( assert worker.project_client.work_items.attachments.get_all.call_count == 1 assert worker.project_client.work_items.attachments.create.call_count == 0 assert worker.project_client.work_items.attachments.update.call_count == 0 - work_item: data_model.CapellaWorkItem = ( worker.project_client.work_items.update.call_args.args[0] ) - assert work_item.description.value == TEST_DIAG_DESCR.format( title="Diagram", attachment_id="SVG-ATTACHMENT", @@ -374,6 +361,7 @@ def test_diagram_attachments_fully_unchanged( model: capellambse.MelodyModel, worker: polarion_worker.CapellaPolarionWorker, ): + diag = model.diagrams.by_uuid(TEST_DIAG_UUID) converter = model_converter.ModelConverter(model, "TEST") worker.polarion_data_repo = polarion_repo.PolarionDataRepository( [ @@ -384,15 +372,13 @@ def test_diagram_attachments_fully_unchanged( ) ] ) - converter.converter_session[TEST_DIAG_UUID] = data_session.ConverterData( - "", + model_converter.get_layer_name(diag), converter_config.CapellaTypeConfig("diagram", "diagram", []), - model.diagrams.by_uuid(TEST_DIAG_UUID), + diag, ) converter.generate_work_items(worker.polarion_data_repo, False, True) - worker.compare_and_update_work_item( converter.converter_session[TEST_DIAG_UUID] ) @@ -557,6 +543,7 @@ def test_diagram_delete_attachments( model: capellambse.MelodyModel, worker: polarion_worker.CapellaPolarionWorker, ): + diag = model.diagrams.by_uuid(TEST_DIAG_UUID) converter = model_converter.ModelConverter(model, "TEST") worker.polarion_data_repo = polarion_repo.PolarionDataRepository( [ @@ -596,9 +583,9 @@ def test_diagram_delete_attachments( ] converter.converter_session[TEST_DIAG_UUID] = data_session.ConverterData( - "", + model_converter.get_layer_name(diag), converter_config.CapellaTypeConfig("diagram", "diagram", []), - model.diagrams.by_uuid(TEST_DIAG_UUID), + diag, ) converter.generate_work_items(worker.polarion_data_repo, False, True) From ee26a4f936645a7e072b894f1ed6d8e9cc3e55e5 Mon Sep 17 00:00:00 2001 From: ewuerger Date: Thu, 12 Dec 2024 17:16:25 +0100 Subject: [PATCH 2/6] test: Improve tests --- tests/test_elements.py | 16 ++-- tests/test_workitem_attachments.py | 133 +++++++++++------------------ 2 files changed, 59 insertions(+), 90 deletions(-) diff --git a/tests/test_elements.py b/tests/test_elements.py index a0f5424..08672d6 100644 --- a/tests/test_elements.py +++ b/tests/test_elements.py @@ -50,6 +50,7 @@ TEST_ACTOR_UUID = "08e02248-504d-4ed8-a295-c7682a614f66" TEST_PHYS_COMP = "b9f9a83c-fb02-44f7-9123-9d86326de5f1" TEST_PHYS_NODE = "8a6d68c8-ac3d-4654-a07e-ada7adeed09f" +TEST_PHYS_FNC = "11906f7b-3ae9-4343-b998-95b170be2e2b" TEST_SCENARIO = "afdaa095-e2cd-4230-b5d3-6cb771a90f51" TEST_CAP_REAL = "b80b3141-a7fc-48c7-84b2-1467dcef5fce" TEST_CONSTRAINT = "95cbd4af-7224-43fe-98cb-f13dda540b8e" @@ -1861,7 +1862,6 @@ def test_generic_work_item( @staticmethod def test_add_context_diagram(model: capellambse.MelodyModel): - uuid = "11906f7b-3ae9-4343-b998-95b170be2e2b" type_config = converter_config.CapellaTypeConfig( "test", "add_context_diagram", [] ) @@ -1869,16 +1869,16 @@ def test_add_context_diagram(model: capellambse.MelodyModel): model, polarion_repo.PolarionDataRepository(), { - uuid: data_session.ConverterData( + TEST_PHYS_FNC: data_session.ConverterData( "pa", type_config, - model.by_uuid(uuid), + model.by_uuid(TEST_PHYS_FNC), ) }, True, ) - work_item = serializer.serialize(uuid) + work_item = serializer.serialize(TEST_PHYS_FNC) assert work_item is not None assert "context_diagram" in work_item.additional_attributes @@ -2154,7 +2154,6 @@ def test_read_config_tree_view_with_params( with mock.patch.object( context.ContextDiagram, "render" ) as wrapped_render: - wis = serializer.serialize_all() _ = wis[0].attachments[0].content_bytes @@ -2204,7 +2203,6 @@ def test_read_config_links(caplog: pytest.LogCaptureFixture): @staticmethod def test_add_context_diagram_with_caption(model: capellambse.MelodyModel): - uuid = "11906f7b-3ae9-4343-b998-95b170be2e2b" type_config = converter_config.CapellaTypeConfig( "test", "add_context_diagram", [] ) @@ -2212,17 +2210,17 @@ def test_add_context_diagram_with_caption(model: capellambse.MelodyModel): model, polarion_repo.PolarionDataRepository(), { - uuid: data_session.ConverterData( + TEST_PHYS_FNC: data_session.ConverterData( "pa", type_config, - model.by_uuid(uuid), + model.by_uuid(TEST_PHYS_FNC), ) }, True, generate_figure_captions=True, ) - work_item = serializer.serialize(uuid) + work_item = serializer.serialize(TEST_PHYS_FNC) assert work_item is not None assert "context_diagram" in work_item.additional_attributes diff --git a/tests/test_workitem_attachments.py b/tests/test_workitem_attachments.py index 5642c5c..d1a82e7 100644 --- a/tests/test_workitem_attachments.py +++ b/tests/test_workitem_attachments.py @@ -21,16 +21,16 @@ # pylint: disable=relative-beyond-top-level, useless-suppression from .conftest import TEST_DIAGRAM_CACHE -from .test_elements import TEST_DIAG_DESCR +from .test_elements import TEST_DIAG_DESCR, TEST_PHYS_FNC DIAGRAM_WI_CHECKSUM = ( - "76fc1f7e4b73891488de7e47de8ef75fc24e85fc3cdde80661503201e70b1733" + "1239ced17306dc92213cd1b729e3652a1cbc9b07997683ed36033a9d05adcb75" ) WI_CONTEXT_DIAGRAM_CHECKSUM = ( - "0ed1417e8e4717524bc91162dcf8633afca686e93f8b036d0bc48d81f0444f56" + "69d8b2ca4e690ccaf70fff16d2e42bbf2ecf434307d49e020d2138160ba35cb2" ) CONTEXT_DIAGRAM_CHECKSUM = ( - "2b86192f2f65353512e1b4af0e652577d0ca3d0cf8595f5dcfba7d52bcb6d702" + "572cb7ba53bcde56638a119fafc1304af294467d8c851f4b2cc35ce2f5d231eb" ) TEST_DIAG_UUID = "_APOQ0QPhEeynfbzU12yy7w" @@ -62,6 +62,21 @@ ) +# pylint: disable=redefined-outer-name +@pytest.fixture +def converter( + model: capellambse.MelodyModel, +) -> model_converter.ModelConverter: + diag = model.diagrams.by_uuid(TEST_DIAG_UUID) + converter = model_converter.ModelConverter(model, "TEST") + converter.converter_session[TEST_DIAG_UUID] = data_session.ConverterData( + model_converter.get_layer_name(diag), + converter_config.CapellaTypeConfig("diagram", "diagram", []), + diag, + ) + return converter + + @pytest.fixture def worker(monkeypatch: pytest.MonkeyPatch): mock_api_client = mock.MagicMock(spec=polarion_api.PolarionClient) @@ -97,17 +112,11 @@ def set_attachment_ids(attachments: list[polarion_api.WorkItemAttachment]): counter += 1 -def test_diagram_no_attachments(model: capellambse.MelodyModel): - diag = model.diagrams.by_uuid(TEST_DIAG_UUID) - converter = model_converter.ModelConverter(model, "TEST") - converter.converter_session[TEST_DIAG_UUID] = data_session.ConverterData( - model_converter.get_layer_name(diag), - converter_config.CapellaTypeConfig("diagram", "diagram", []), - diag, - ) - +def test_diagram_no_attachments(converter: model_converter.ModelConverter): converter.generate_work_items( - polarion_repo.PolarionDataRepository(), False, False + polarion_repo.PolarionDataRepository(), + generate_links=False, + generate_attachments=False, ) work_item = converter.converter_session[TEST_DIAG_UUID].work_item @@ -115,17 +124,11 @@ def test_diagram_no_attachments(model: capellambse.MelodyModel): assert work_item.attachments == [] -def test_diagram_has_attachments(model: capellambse.MelodyModel): - diag = model.diagrams.by_uuid(TEST_DIAG_UUID) - converter = model_converter.ModelConverter(model, "TEST") - converter.converter_session[TEST_DIAG_UUID] = data_session.ConverterData( - model_converter.get_layer_name(diag), - converter_config.CapellaTypeConfig("diagram", "diagram", []), - diag, - ) - +def test_diagram_has_attachments(converter: model_converter.ModelConverter): converter.generate_work_items( - polarion_repo.PolarionDataRepository(), False, True + polarion_repo.PolarionDataRepository(), + generate_links=False, + generate_attachments=True, ) work_item = converter.converter_session[TEST_DIAG_UUID].work_item @@ -135,11 +138,9 @@ def test_diagram_has_attachments(model: capellambse.MelodyModel): # pylint: disable=redefined-outer-name def test_diagram_attachments_new( - model: capellambse.MelodyModel, + converter: model_converter.ModelConverter, worker: polarion_worker.CapellaPolarionWorker, ): - diag = model.diagrams.by_uuid(TEST_DIAG_UUID) - converter = model_converter.ModelConverter(model, "TEST") worker.polarion_data_repo = polarion_repo.PolarionDataRepository( [data_model.CapellaWorkItem(WORKITEM_ID, uuid_capella=TEST_DIAG_UUID)] ) @@ -150,11 +151,6 @@ def test_diagram_attachments_new( worker.project_client.work_items.attachments.create.side_effect = ( set_attachment_ids ) - converter.converter_session[TEST_DIAG_UUID] = data_session.ConverterData( - model_converter.get_layer_name(diag), - converter_config.CapellaTypeConfig("diagram", "diagram", []), - diag, - ) converter.generate_work_items(worker.polarion_data_repo, False, True) worker.compare_and_update_work_item( @@ -187,11 +183,9 @@ def test_diagram_attachments_new( # pylint: disable=redefined-outer-name def test_new_diagram( - model: capellambse.MelodyModel, + converter: model_converter.ModelConverter, worker: polarion_worker.CapellaPolarionWorker, ): - diag = model.diagrams.by_uuid(TEST_DIAG_UUID) - converter = model_converter.ModelConverter(model, "TEST") checksum = json.dumps({"__C2P__WORK_ITEM": DIAGRAM_WI_CHECKSUM}) worker.polarion_data_repo = polarion_repo.PolarionDataRepository( [ @@ -209,11 +203,6 @@ def test_new_diagram( worker.project_client.work_items.attachments.create.side_effect = ( set_attachment_ids ) - converter.converter_session[TEST_DIAG_UUID] = data_session.ConverterData( - model_converter.get_layer_name(diag), - converter_config.CapellaTypeConfig("diagram", "diagram", []), - diag, - ) converter.generate_work_items(worker.polarion_data_repo, False, True) worker.compare_and_update_work_item( @@ -233,11 +222,9 @@ def test_new_diagram( def test_diagram_attachments_updated( - model: capellambse.MelodyModel, + converter: model_converter.ModelConverter, worker: polarion_worker.CapellaPolarionWorker, ): - diag = model.diagrams.by_uuid(TEST_DIAG_UUID) - converter = model_converter.ModelConverter(model, "TEST") worker.polarion_data_repo = polarion_repo.PolarionDataRepository( [data_model.CapellaWorkItem(WORKITEM_ID, uuid_capella=TEST_DIAG_UUID)] ) @@ -266,11 +253,6 @@ def test_diagram_attachments_updated( worker.project_client.work_items.attachments.get_all.return_value = ( existing_attachments ) - converter.converter_session[TEST_DIAG_UUID] = data_session.ConverterData( - model_converter.get_layer_name(diag), - converter_config.CapellaTypeConfig("diagram", "diagram", []), - diag, - ) converter.generate_work_items(worker.polarion_data_repo, False, True) worker.compare_and_update_work_item( @@ -293,11 +275,9 @@ def test_diagram_attachments_updated( def test_diagram_attachments_unchanged_work_item_changed( - model: capellambse.MelodyModel, + converter: model_converter.ModelConverter, worker: polarion_worker.CapellaPolarionWorker, ): - diag = model.diagrams.by_uuid(TEST_DIAG_UUID) - converter = model_converter.ModelConverter(model, "TEST") diagram_work_item = data_model.CapellaWorkItem( WORKITEM_ID, uuid_capella=TEST_DIAG_UUID, @@ -330,11 +310,6 @@ def test_diagram_attachments_unchanged_work_item_changed( worker.project_client.work_items.attachments.get_all.return_value = ( diagram_work_item.attachments ) - converter.converter_session[TEST_DIAG_UUID] = data_session.ConverterData( - model_converter.get_layer_name(diag), - converter_config.CapellaTypeConfig("diagram", "diagram", []), - diag, - ) converter.generate_work_items(worker.polarion_data_repo, False, True) worker.compare_and_update_work_item( @@ -358,11 +333,9 @@ def test_diagram_attachments_unchanged_work_item_changed( def test_diagram_attachments_fully_unchanged( - model: capellambse.MelodyModel, + converter: model_converter.ModelConverter, worker: polarion_worker.CapellaPolarionWorker, ): - diag = model.diagrams.by_uuid(TEST_DIAG_UUID) - converter = model_converter.ModelConverter(model, "TEST") worker.polarion_data_repo = polarion_repo.PolarionDataRepository( [ data_model.CapellaWorkItem( @@ -372,11 +345,6 @@ def test_diagram_attachments_fully_unchanged( ) ] ) - converter.converter_session[TEST_DIAG_UUID] = data_session.ConverterData( - model_converter.get_layer_name(diag), - converter_config.CapellaTypeConfig("diagram", "diagram", []), - diag, - ) converter.generate_work_items(worker.polarion_data_repo, False, True) worker.compare_and_update_work_item( @@ -393,16 +361,15 @@ def test_add_context_diagram( model: capellambse.MelodyModel, worker: polarion_worker.CapellaPolarionWorker, ): - uuid = "11906f7b-3ae9-4343-b998-95b170be2e2b" converter = model_converter.ModelConverter(model, "TEST") worker.polarion_data_repo = polarion_repo.PolarionDataRepository( - [data_model.CapellaWorkItem(WORKITEM_ID, uuid_capella=uuid)] + [data_model.CapellaWorkItem(WORKITEM_ID, uuid_capella=TEST_PHYS_FNC)] ) - converter.converter_session[uuid] = data_session.ConverterData( - "", + converter.converter_session[TEST_PHYS_FNC] = data_session.ConverterData( + "pa", converter_config.CapellaTypeConfig("test", "add_context_diagram", []), - model.by_uuid(uuid), + model.by_uuid(TEST_PHYS_FNC), ) worker.project_client.work_items.attachments.create = mock.MagicMock() @@ -412,7 +379,9 @@ def test_add_context_diagram( converter.generate_work_items(worker.polarion_data_repo, False, True) - worker.compare_and_update_work_item(converter.converter_session[uuid]) + worker.compare_and_update_work_item( + converter.converter_session[TEST_PHYS_FNC] + ) assert worker.project_client.work_items.update.call_count == 1 assert worker.project_client.work_items.attachments.create.call_count == 1 @@ -445,13 +414,12 @@ def test_update_context_diagram_no_changes( model: capellambse.MelodyModel, worker: polarion_worker.CapellaPolarionWorker, ): - uuid = "11906f7b-3ae9-4343-b998-95b170be2e2b" converter = model_converter.ModelConverter(model, "TEST") worker.polarion_data_repo = polarion_repo.PolarionDataRepository( [ data_model.CapellaWorkItem( WORKITEM_ID, - uuid_capella=uuid, + uuid_capella=TEST_PHYS_FNC, checksum=json.dumps( { "__C2P__WORK_ITEM": WI_CONTEXT_DIAGRAM_CHECKSUM, @@ -462,15 +430,17 @@ def test_update_context_diagram_no_changes( ] ) - converter.converter_session[uuid] = data_session.ConverterData( - "", + converter.converter_session[TEST_PHYS_FNC] = data_session.ConverterData( + "pa", converter_config.CapellaTypeConfig("test", "add_context_diagram", []), - model.by_uuid(uuid), + model.by_uuid(TEST_PHYS_FNC), ) with mock.patch.object(context.ContextDiagram, "render") as wrapped_render: converter.generate_work_items(worker.polarion_data_repo, False, True) - worker.compare_and_update_work_item(converter.converter_session[uuid]) + worker.compare_and_update_work_item( + converter.converter_session[TEST_PHYS_FNC] + ) assert worker.project_client.work_items.update.call_count == 0 assert worker.project_client.work_items.attachments.update.call_count == 0 @@ -481,13 +451,12 @@ def test_update_context_diagram_with_changes( model: capellambse.MelodyModel, worker: polarion_worker.CapellaPolarionWorker, ): - uuid = "11906f7b-3ae9-4343-b998-95b170be2e2b" converter = model_converter.ModelConverter(model, "TEST") worker.polarion_data_repo = polarion_repo.PolarionDataRepository( [ data_model.CapellaWorkItem( WORKITEM_ID, - uuid_capella=uuid, + uuid_capella=TEST_PHYS_FNC, checksum=json.dumps( { "__C2P__WORK_ITEM": WI_CONTEXT_DIAGRAM_CHECKSUM, @@ -498,10 +467,10 @@ def test_update_context_diagram_with_changes( ] ) - converter.converter_session[uuid] = data_session.ConverterData( + converter.converter_session[TEST_PHYS_FNC] = data_session.ConverterData( "", converter_config.CapellaTypeConfig("test", "add_context_diagram", []), - model.by_uuid(uuid), + model.by_uuid(TEST_PHYS_FNC), ) worker.project_client.work_items.attachments.get_all.return_value = [ polarion_api.WorkItemAttachment( @@ -532,7 +501,9 @@ def test_update_context_diagram_with_changes( 'width="100" height="100">' ) converter.generate_work_items(worker.polarion_data_repo, False, True) - worker.compare_and_update_work_item(converter.converter_session[uuid]) + worker.compare_and_update_work_item( + converter.converter_session[TEST_PHYS_FNC] + ) assert worker.project_client.work_items.update.call_count == 1 assert worker.project_client.work_items.attachments.update.call_count == 2 From fac39491757d48936a957318626b7f2f0d4d4962 Mon Sep 17 00:00:00 2001 From: ewuerger Date: Thu, 12 Dec 2024 19:16:01 +0100 Subject: [PATCH 3/6] feat(serializer): Add `add_attributes` serializer For now this serializer is only supports adding enum attributes as custom fields. --- .../converters/converter_config.py | 89 ++++++++++--------- .../converters/element_converter.py | 37 ++++++++ docs/source/features/sync.rst | 4 + tests/test_elements.py | 29 ++++++ 4 files changed, 115 insertions(+), 44 deletions(-) diff --git a/capella2polarion/converters/converter_config.py b/capella2polarion/converters/converter_config.py index 5d5aff1..af08fd6 100644 --- a/capella2polarion/converters/converter_config.py +++ b/capella2polarion/converters/converter_config.py @@ -52,7 +52,50 @@ class CapellaTypeConfig: def __post_init__(self): """Post processing for the initialization.""" - self.converters = _force_dict(self.converters) + self.converters = self._force_dict() + + def _force_dict(self) -> dict[str, dict[str, t.Any]]: + match self.converters: + case None: + return {} + case str(): + return {self.converters: {}} + case list(): + return {c: {} for c in self.converters} + case dict(): + return self._filter_converter_config() + case _: + raise TypeError("Unsupported Type") + + def _filter_converter_config(self) -> dict[str, dict[str, t.Any]]: + custom_converters = ( + "include_pre_and_post_condition", + "linked_text_as_description", + "add_attributes", + "add_context_diagram", + "add_tree_diagram", + "add_jinja_fields", + "jinja_as_description", + ) + filtered_config = {} + assert isinstance(self.converters, dict) + for name, params in self.converters.items(): + params = params or {} + if name not in custom_converters: + logger.error("Unknown converter in config %r", name) + continue + + if name in ("add_context_diagram", "add_tree_diagram"): + assert isinstance(params, dict) + params = _filter_context_diagram_config(params) + + if name in ("add_attributes"): + assert isinstance(params, list) # type: ignore[unreachable] + params = {"attributes": params} # type: ignore[unreachable] + + filtered_config[name] = params + + return filtered_config def _default_type_conversion(c_type: str) -> str: @@ -283,7 +326,7 @@ def config_matches(config: CapellaTypeConfig | None, **kwargs: t.Any) -> bool: def _read_capella_type_configs( - conf: dict[str, t.Any] | list[dict[str, t.Any]] | None + conf: dict[str, t.Any] | list[dict[str, t.Any]] | None, ) -> list[dict]: if conf is None: return [{}] @@ -299,22 +342,6 @@ def _read_capella_type_configs( ) -def _force_dict( - config: str | list[str] | dict[str, dict[str, t.Any]] | None -) -> dict[str, dict[str, t.Any]]: - match config: - case None: - return {} - case str(): - return {config: {}} - case list(): - return {c: {} for c in config} - case dict(): - return _filter_converter_config(config) - case _: - raise TypeError("Unsupported Type") - - def add_prefix(polarion_type: str, prefix: str) -> str: """Add a prefix to the given ``polarion_type``.""" if prefix: @@ -322,32 +349,6 @@ def add_prefix(polarion_type: str, prefix: str) -> str: return polarion_type -def _filter_converter_config( - config: dict[str, dict[str, t.Any]] -) -> dict[str, dict[str, t.Any]]: - custom_converters = ( - "include_pre_and_post_condition", - "linked_text_as_description", - "add_context_diagram", - "add_tree_diagram", - "add_jinja_fields", - "jinja_as_description", - ) - filtered_config = {} - for name, params in config.items(): - params = params or {} - if name not in custom_converters: - logger.error("Unknown converter in config %r", name) - continue - - if name in ("add_context_diagram", "add_tree_diagram"): - params = _filter_context_diagram_config(params) - - filtered_config[name] = params - - return filtered_config - - def _filter_context_diagram_config( config: dict[str, t.Any] ) -> dict[str, t.Any]: diff --git a/capella2polarion/converters/element_converter.py b/capella2polarion/converters/element_converter.py index 476d691..90eeb92 100644 --- a/capella2polarion/converters/element_converter.py +++ b/capella2polarion/converters/element_converter.py @@ -5,6 +5,7 @@ from __future__ import annotations import collections +import enum import hashlib import logging import mimetypes @@ -66,6 +67,16 @@ def _format(texts: list[str]) -> dict[str, str]: return requirement_types +def _resolve_capella_attribute( + element: m.ModelElement | m.Diagram, attribute: str +) -> polarion_api.TextContent: + value = getattr(element, attribute) + if isinstance(value, enum.Enum): + return polarion_api.TextContent(type="string", value=value.name) + + raise ValueError("Unsupported attribute type: %r", value) + + class CapellaWorkItemSerializer(polarion_html_helper.JinjaRendererMixin): """The general serializer class for CapellaWorkItems.""" @@ -455,6 +466,32 @@ def __generic_work_item( return converter_data.work_item + def _add_attributes( + self, + converter_data: data_session.ConverterData, + attributes: list[dict[str, t.Any]], + ): + assert converter_data.work_item is not None + for attribute in attributes: + try: + value = _resolve_capella_attribute( + converter_data.capella_element, attribute["capella_attr"] + ) + setattr( + converter_data.work_item, attribute["polarion_id"], value + ) + except AttributeError: + logger.error( + "Attribute %r not found on %r", + attribute["capella_attr"], + converter_data.type_config.p_type, + ) + continue + except ValueError as error: + logger.error(error.args[0]) + + return converter_data.work_item + def _diagram( self, converter_data: data_session.ConverterData, diff --git a/docs/source/features/sync.rst b/docs/source/features/sync.rst index 1c2c247..24ce3a9 100644 --- a/docs/source/features/sync.rst +++ b/docs/source/features/sync.rst @@ -59,6 +59,10 @@ specific serializer alone: | linked_text_as_description | A serializer resolving ``Constraint`` s and their | | | linked text. | +--------------------------------------+------------------------------------------------------+ +| add_attributes | A serializer adding arbitrary attributes as custom | +| | fields to the work item. For now only supports enum | +| | attributes! | ++--------------------------------------+------------------------------------------------------+ | add_context_diagram | A serializer adding a context diagram to the work | | | item. This requires node.js to be installed. | | | The Capella objects where ``context_diagram`` is | diff --git a/tests/test_elements.py b/tests/test_elements.py index 08672d6..74c5203 100644 --- a/tests/test_elements.py +++ b/tests/test_elements.py @@ -1860,6 +1860,35 @@ def test_generic_work_item( assert work_item == data_model.CapellaWorkItem(**expected) assert status == "open" + def test_add_attributes(self, model: capellambse.MelodyModel): + converters = { + "add_attributes": [ + {"capella_attr": "nature", "polarion_id": "nature"}, + {"capella_attr": "kind", "polarion_id": "kind"}, + ] + } + type_config = converter_config.CapellaTypeConfig( + "PhysicalComponent", converters, [] + ) + serializer = element_converter.CapellaWorkItemSerializer( + model, + polarion_repo.PolarionDataRepository(), + { + TEST_PHYS_COMP: data_session.ConverterData( + "pa", + type_config, + model.by_uuid(TEST_PHYS_COMP), + ) + }, + True, + ) + + work_item = serializer.serialize(TEST_PHYS_COMP) + + assert work_item is not None + assert work_item.nature == {"type": "string", "value": "UNSET"} + assert work_item.kind == {"type": "string", "value": "UNSET"} + @staticmethod def test_add_context_diagram(model: capellambse.MelodyModel): type_config = converter_config.CapellaTypeConfig( From f9a300bd1d1990cb296b20ce5ef9782c1077c12e Mon Sep 17 00:00:00 2001 From: ewuerger Date: Fri, 13 Dec 2024 12:20:59 +0100 Subject: [PATCH 4/6] fix: Remove obvious error --- capella2polarion/converters/converter_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/capella2polarion/converters/converter_config.py b/capella2polarion/converters/converter_config.py index af08fd6..f63c9ff 100644 --- a/capella2polarion/converters/converter_config.py +++ b/capella2polarion/converters/converter_config.py @@ -89,7 +89,7 @@ def _filter_converter_config(self) -> dict[str, dict[str, t.Any]]: assert isinstance(params, dict) params = _filter_context_diagram_config(params) - if name in ("add_attributes"): + if name in ("add_attributes",): assert isinstance(params, list) # type: ignore[unreachable] params = {"attributes": params} # type: ignore[unreachable] From d0a31553a8ab8039abd5b3759099d88f30f91dfd Mon Sep 17 00:00:00 2001 From: ewuerger Date: Fri, 13 Dec 2024 12:27:26 +0100 Subject: [PATCH 5/6] ci: Please pylint --- capella2polarion/converters/element_converter.py | 2 +- pyproject.toml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/capella2polarion/converters/element_converter.py b/capella2polarion/converters/element_converter.py index 90eeb92..1e09f11 100644 --- a/capella2polarion/converters/element_converter.py +++ b/capella2polarion/converters/element_converter.py @@ -74,7 +74,7 @@ def _resolve_capella_attribute( if isinstance(value, enum.Enum): return polarion_api.TextContent(type="string", value=value.name) - raise ValueError("Unsupported attribute type: %r", value) + raise ValueError(f"Unsupported attribute type: {value!r}") class CapellaWorkItemSerializer(polarion_html_helper.JinjaRendererMixin): diff --git a/pyproject.toml b/pyproject.toml index 14f5b64..dfe0262 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -129,6 +129,7 @@ disable = [ "too-many-public-methods", "too-many-return-statements", "too-many-statements", + "too-many-positional-arguments", # Auto-formatting "bad-indentation", From 20da2f4d7dcb5bcbe22802a0ca65a0f1d7a98f8a Mon Sep 17 00:00:00 2001 From: ewuerger Date: Fri, 13 Dec 2024 16:20:28 +0100 Subject: [PATCH 6/6] test: Revert context diagram checksum --- tests/test_workitem_attachments.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_workitem_attachments.py b/tests/test_workitem_attachments.py index d1a82e7..a9837ae 100644 --- a/tests/test_workitem_attachments.py +++ b/tests/test_workitem_attachments.py @@ -30,7 +30,7 @@ "69d8b2ca4e690ccaf70fff16d2e42bbf2ecf434307d49e020d2138160ba35cb2" ) CONTEXT_DIAGRAM_CHECKSUM = ( - "572cb7ba53bcde56638a119fafc1304af294467d8c851f4b2cc35ce2f5d231eb" + "2b86192f2f65353512e1b4af0e652577d0ca3d0cf8595f5dcfba7d52bcb6d702" ) TEST_DIAG_UUID = "_APOQ0QPhEeynfbzU12yy7w"