diff --git a/capellambse/aird/capstyle.py b/capellambse/aird/capstyle.py index 008c95bde..9b38798ea 100644 --- a/capellambse/aird/capstyle.py +++ b/capellambse/aird/capstyle.py @@ -650,6 +650,24 @@ class in the form:: "stroke-dasharray": "5", }, }, + "Operational Activity Interaction Blank": { + "Box.OperationalActivity": { + "fill": COLORS["_CAP_Activity_Orange"], + "rx": "10px", + "ry": "10px", + "stroke": COLORS["_CAP_Activity_Border_Orange"], + "text_fill": COLORS["_CAP_xAB_Activity_Label_Orange"], + }, + "Box.OperationalProcess": { + "stroke": COLORS["black"], + "text_fill": COLORS["black"], + }, + "Edge.FunctionalExchange": { + "marker-end": "ArrowMark", + "stroke-width": 2, + "stroke": COLORS["_CAP_Activity_Border_Orange"], + }, + }, "Physical Architecture Blank": { **dict.fromkeys( ["Box.CP_IN", "Box.CP_OUT", "Box.CP_INOUT"], @@ -658,6 +676,14 @@ class in the form:: "stroke": COLORS["black"], }, ), + "Box.FIP": { + "fill": COLORS["dark_orange"], + "stroke-width": 0, + }, + "Box.FOP": { + "fill": COLORS["_CAP_xAB_Function_Border_Green"], + "stroke-width": 0, + }, "Box.PP": { "fill": COLORS["_CAP_PhysicalPort_Yellow"], "stroke": COLORS["_CAP_Class_Border_Brown"], @@ -670,12 +696,68 @@ class in the form:: "stroke": COLORS["_CAP_Node_Yellow_Border"], "text_fill": COLORS["_CAP_Node_Yellow_Label"], }, + "Box.PhysicalFunction": { + "fill": COLORS["_CAP_xAB_Function_Green"], + "stroke": COLORS["_CAP_xAB_Function_Border_Green"], + }, + "Edge.ComponentExchange": { + "stroke": COLORS["_CAP_Component_Border_Blue"], + "stroke-width": 2, + "text_fill": COLORS["_CAP_Component_Border_Blue"], + }, + "Edge.FunctionalExchange": { + "stroke": COLORS["_CAP_xAB_Function_Border_Green"], + "stroke-width": 2, + "text_fill": COLORS["_CAP_xAB_Function_Border_Green"], + }, "Edge.PhysicalLink": { "stroke": COLORS["red"], "stroke-width": 2, "text_fill": COLORS["red"], }, }, + "Physical Data Flow Blank": { + "Box.FIP": { + "fill": COLORS["dark_orange"], + "stroke-width": 0, + }, + "Box.FOP": { + "fill": COLORS["_CAP_xAB_Function_Border_Green"], + "stroke-width": 0, + }, + "Box.PhysicalFunction": { + "fill": COLORS["_CAP_xAB_Function_Green"], + "stroke": COLORS["_CAP_xAB_Function_Border_Green"], + }, + "Edge.FunctionalExchange": { + "stroke": COLORS["_CAP_xAB_Function_Border_Green"], + "stroke-width": 2, + "text_fill": COLORS["_CAP_xAB_Function_Border_Green"], + }, + }, + "System Architecture Blank": { + **dict.fromkeys( + ["Box.CP_IN", "Box.CP_OUT", "Box.CP_INOUT"], + { + "fill": COLORS["white"], + "stroke": COLORS["black"], + }, + ), + "Box.SystemComponent": { + "fill": [COLORS["_CAP_Actor_Blue_min"], COLORS["_CAP_Actor_Blue"]], + "stroke": COLORS["_CAP_Actor_Border_Blue"], + "text_fill": COLORS["_CAP_Actor_Blue_label"], + }, + "Box.SystemFunction": { + "fill": COLORS["_CAP_xAB_Function_Green"], + "stroke": COLORS["_CAP_xAB_Function_Border_Green"], + }, + "Edge.ComponentExchange": { + "stroke": COLORS["_CAP_Component_Border_Blue"], + "stroke-width": 2, + "text_fill": COLORS["_CAP_Component_Border_Blue"], + }, + }, "System Data Flow Blank": { "Box.FIP": { "fill": COLORS["dark_orange"], diff --git a/capellambse/aird/diagram.py b/capellambse/aird/diagram.py index 28194e1ef..f712f804a 100644 --- a/capellambse/aird/diagram.py +++ b/capellambse/aird/diagram.py @@ -190,36 +190,14 @@ def snap_to_parent(self) -> None: if self.port: padding = (self.parent.PORT_OVERHANG, self.parent.PORT_OVERHANG) - # Distances of all corner combinations - # (our/parent's top-left/bottom-right) - d_tl_tl = self.pos - self.parent.pos - d_br_tl = self.pos + self.size - self.parent.pos - d_tl_br = self.pos - self.parent.pos - self.parent.size - d_br_br = self.pos + self.size - self.parent.pos - self.parent.size - - # Find closest parent border (left vs. right and top vs. bottom) - d_tl = aird.Vector2D( - min(d_tl_tl.x, d_br_tl.x), min(d_tl_tl.y, d_br_tl.y) - ) - d_br = aird.Vector2D( - min(d_tl_br.x, d_br_br.x), min(d_tl_br.y, d_br_br.y) - ) - border = aird.Vector2D( - -1 if abs(d_tl.x) <= abs(d_br.x) else 1, - -1 if abs(d_tl.y) <= abs(d_br.y) else 1, - ) - - # Find out if horizontal or vertical border is closer - horiz = abs(d_tl.x if border.x < 0 else d_br.x) <= abs( - d_tl.y if border.y < 0 else d_br.y - ) - border = border @ (horiz, not horiz) - - self.pos = self.pos.boxsnap( - self.parent.pos - padding, - self.parent.pos + self.parent.size + padding - self.size, - border, + midbox_tl = self.parent.pos + self.size / 2 - padding + midbox_br = ( + self.parent.pos + self.parent.size - self.size / 2 + padding ) + midbox = aird.Box(midbox_tl, midbox_br - midbox_tl) + mid = self.pos + self.size / 2 + newmid = midbox.vector_snap(mid) + self.pos += newmid - mid else: padding = (self.parent.CHILD_MARGIN, self.parent.CHILD_MARGIN) minpos = self.parent.pos + padding @@ -253,7 +231,7 @@ def add_context(self, uuid: str) -> None: self.parent.add_context(uuid) def vector_snap( - self, vector: aird.Vec2ish, direction: aird.Vec2ish + self, vector: aird.Vec2ish, direction: aird.Vec2ish = (0, 0) ) -> aird.Vector2D: """Snap the ``vector`` into this Box, preferably into ``direction``.""" if not isinstance(vector, aird.Vector2D): diff --git a/capellambse/aird/vector2d.py b/capellambse/aird/vector2d.py index 9c0778984..bdae65af0 100644 --- a/capellambse/aird/vector2d.py +++ b/capellambse/aird/vector2d.py @@ -170,23 +170,35 @@ def boxsnap( corner2 A Vector2D describing the second corner of the target box. dirvec - A Vector2D pointing in the direction to snap towards. + Ignored. """ minx = min(corner1[0], corner2[0]) miny = min(corner1[1], corner2[1]) maxx = max(corner1[0], corner2[0]) maxy = max(corner1[1], corner2[1]) - if abs(dirvec[0]) >= abs(dirvec[1]): - # Snap horizontally - return Vector2D( - (minx, maxx)[dirvec[0] > 0], - min(maxy - 1, max(miny + 1, self[1])), - ) - # Snap vertically - return Vector2D( - min(maxx - 1, max(minx + 1, self[0])), (miny, maxy)[dirvec[1] > 0] - ) + x, y = self.x, self.y + if x < minx: + x = minx + elif x > maxx: + x = maxx + if y < miny: + y = miny + elif y > maxy: + y = maxy + + point = Vector2D(x, y) + if x != self.x or y != self.y: + return point + + distances = [ + Vector2D(self.x - minx, 0), + Vector2D(self.x - maxx, 0), + Vector2D(0, self.y - miny), + Vector2D(0, self.y - maxy), + ] + offset = min(distances, key=lambda i: i.sqlength) + return point + offset def __map( self, diff --git a/capellambse/model/crosslayer/cs.py b/capellambse/model/crosslayer/cs.py index dfb6bc7d5..b208bc09c 100644 --- a/capellambse/model/crosslayer/cs.py +++ b/capellambse/model/crosslayer/cs.py @@ -35,29 +35,6 @@ XT_PHYS_PATH_INV = "org.polarsys.capella.core.data.cs:PhysicalPathInvolvement" -class Component(c.GenericElement): - """A template class for components.""" - - is_abstract = xmltools.BooleanAttributeProperty( - "_element", - "abstract", - __doc__="Boolean flag for an abstract Component", - ) - is_human = xmltools.BooleanAttributeProperty( - "_element", "human", __doc__="Boolean flag for a human Component" - ) - is_actor = xmltools.BooleanAttributeProperty( - "_element", "actor", __doc__="Boolean flag for an actor Component" - ) - - owner = c.ParentAccessor(c.GenericElement) - state_machines = c.ProxyAccessor( - capellacommon.StateMachine, aslist=c.ElementList - ) - - parts: c.Accessor - - @c.xtype_handler(None) class Part(c.GenericElement): """A representation of a physical component""" @@ -113,6 +90,30 @@ class PhysicalLink(PhysicalPort): physical_paths: c.Accessor +class Component(c.GenericElement): + """A template class for components.""" + + is_abstract = xmltools.BooleanAttributeProperty( + "_element", + "abstract", + __doc__="Boolean flag for an abstract Component", + ) + is_human = xmltools.BooleanAttributeProperty( + "_element", "human", __doc__="Boolean flag for a human Component" + ) + is_actor = xmltools.BooleanAttributeProperty( + "_element", "actor", __doc__="Boolean flag for an actor Component" + ) + + owner = c.ParentAccessor(c.GenericElement) + state_machines = c.ProxyAccessor( + capellacommon.StateMachine, aslist=c.ElementList + ) + ports = c.ProxyAccessor(fa.ComponentPort, aslist=c.ElementList) + physical_ports = c.ProxyAccessor(PhysicalPort, aslist=c.ElementList) + parts = c.ReferenceSearchingAccessor(Part, "type", aslist=c.ElementList) + + @c.xtype_handler(None) class PhysicalPath(c.GenericElement): """A physical path.""" @@ -144,11 +145,6 @@ class ComponentRealization(c.GenericElement): _xmltag = "ownedComponentRealizations" -c.set_accessor( - Component, - "parts", - c.ReferenceSearchingAccessor(Part, "type", aslist=c.ElementList), -) c.set_accessor( InterfacePkg, "packages", diff --git a/capellambse/model/crosslayer/fa.py b/capellambse/model/crosslayer/fa.py index 05220ffc6..bacc009e6 100644 --- a/capellambse/model/crosslayer/fa.py +++ b/capellambse/model/crosslayer/fa.py @@ -55,8 +55,30 @@ class FunctionRealization(c.GenericElement): class AbstractExchange(c.GenericElement): """Common code for Exchanges.""" - source_port = c.AttrProxyAccessor(c.GenericElement, "source") - target_port = c.AttrProxyAccessor(c.GenericElement, "target") + source = c.AttrProxyAccessor(c.GenericElement, "source") + target = c.AttrProxyAccessor(c.GenericElement, "target") + + @property + def source_port(self) -> c.GenericElement: + import warnings + + warnings.warn( + "source_port is deprecated, use source instead", + FutureWarning, + stacklevel=2, + ) + return self.source + + @property + def target_port(self) -> c.GenericElement: + import warnings + + warnings.warn( + "target_port is deprecated, use target instead", + FutureWarning, + stacklevel=2, + ) + return self.target @c.xtype_handler(None) @@ -101,6 +123,18 @@ class FunctionOutputPort(FunctionPort): ) +class Function(AbstractFunction): + """Common Code for Function's.""" + + is_leaf = property(lambda self: not self.functions) + + inputs = c.ProxyAccessor(FunctionInputPort, aslist=c.ElementList) + outputs = c.ProxyAccessor(FunctionOutputPort, aslist=c.ElementList) + + functions: c.Accessor + packages: c.Accessor + + @c.xtype_handler(None) class FunctionalExchange(AbstractExchange): """A functional exchange.""" @@ -136,7 +170,7 @@ class ComponentExchange(AbstractExchange): _xmltag = "ownedComponentExchanges" - func_exchanges = c.ProxyAccessor( + allocated_functional_exchanges = c.ProxyAccessor( FunctionalExchange, XT_COMP_EX_FNC_EX_ALLOC, aslist=c.ElementList, @@ -148,12 +182,23 @@ class ComponentExchange(AbstractExchange): aslist=c.ElementList, ) + @property + def func_exchanges(self) -> c.ElementList[FunctionalExchange]: + import warnings + + warnings.warn( + "func_exchanges is deprecated, use allocated_functional_exchanges instead", + FutureWarning, + stacklevel=2, + ) + return self.allocated_functional_exchanges + @property def exchange_items( self, ) -> c.ElementList[information.ExchangeItem]: items = self.allocated_exchange_items - func_exchanges = self.func_exchanges + func_exchanges = self.allocated_functional_exchanges assert isinstance(func_exchanges, cabc.Iterable) for exchange in func_exchanges: items += exchange.exchange_items @@ -169,10 +214,7 @@ def exchange_items( _port, "exchanges", c.ReferenceSearchingAccessor( - _exchange, - "source_port", - "target_port", - aslist=c.ElementList, + _exchange, "source", "target", aslist=c.ElementList ), ) del _port, _exchange diff --git a/capellambse/model/layers/ctx.py b/capellambse/model/layers/ctx.py index 4902bfb04..5fb6753e7 100644 --- a/capellambse/model/layers/ctx.py +++ b/capellambse/model/layers/ctx.py @@ -30,14 +30,11 @@ @c.xtype_handler(XT_ARCH) -class SystemFunction(fa.AbstractFunction): +class SystemFunction(fa.Function): """A system function.""" _xmltag = "ownedFunctions" - inputs = c.ProxyAccessor(fa.FunctionInputPort, aslist=c.ElementList) - outputs = c.ProxyAccessor(fa.FunctionOutputPort, aslist=c.ElementList) - is_leaf = property(lambda self: not self.functions) realized_operational_activities = c.ProxyAccessor( oa.OperationalActivity, fa.FunctionRealization, @@ -46,8 +43,6 @@ class SystemFunction(fa.AbstractFunction): ) owner: c.Accessor - functions: c.Accessor - packages: c.Accessor @c.xtype_handler(XT_ARCH) diff --git a/capellambse/model/layers/la.py b/capellambse/model/layers/la.py index 7a71046c1..2c9394e77 100644 --- a/capellambse/model/layers/la.py +++ b/capellambse/model/layers/la.py @@ -28,14 +28,11 @@ @c.xtype_handler(XT_ARCH) -class LogicalFunction(fa.AbstractFunction): +class LogicalFunction(fa.Function): """A logical function on the Logical Architecture layer.""" _xmltag = "ownedLogicalFunctions" - inputs = c.ProxyAccessor(fa.FunctionInputPort, aslist=c.ElementList) - outputs = c.ProxyAccessor(fa.FunctionOutputPort, aslist=c.ElementList) - is_leaf = property(lambda self: not self.functions) realized_system_functions = c.ProxyAccessor( ctx.SystemFunction, fa.FunctionRealization, @@ -43,8 +40,6 @@ class LogicalFunction(fa.AbstractFunction): follow="targetElement", ) - functions: c.Accessor - @property def owner(self) -> LogicalComponent | None: try: @@ -86,7 +81,6 @@ class LogicalComponent(cs.Component): aslist=c.ElementList, follow="targetElement", ) - ports = c.ProxyAccessor(fa.ComponentPort, aslist=c.ElementList) components: c.Accessor @@ -251,6 +245,14 @@ class LogicalArchitecture(crosslayer.BaseArchitectureLayer): aslist=c.ElementList, ), ) +c.set_accessor( + LogicalFunction, + "packages", + c.ProxyAccessor( + LogicalFunctionPkg, + aslist=c.ElementList, + ), +) c.set_self_references( (LogicalComponent, "components"), (LogicalComponentPkg, "packages"), diff --git a/capellambse/model/layers/oa.py b/capellambse/model/layers/oa.py index d9952ecc1..194c22a4c 100644 --- a/capellambse/model/layers/oa.py +++ b/capellambse/model/layers/oa.py @@ -44,6 +44,18 @@ class OperationalActivity(fa.AbstractFunction): matchtransform=operator.attrgetter("activities"), ) + @property + def inputs(self) -> c.ElementList[fa.FunctionalExchange]: + return self._model.oa.all_activity_exchanges.by_target(self) + + @property + def outputs(self) -> c.ElementList[fa.FunctionalExchange]: + return self._model.oa.all_activity_exchanges.by_source(self) + + @property + def exchanges(self) -> c.ElementList[fa.FunctionalExchange]: + return self.inputs + self.outputs + @c.xtype_handler(XT_ARCH) class OperationalProcess(c.GenericElement): @@ -133,6 +145,18 @@ class Entity(AbstractEntity): entities: c.Accessor + @property + def inputs(self) -> c.ElementList[fa.FunctionalExchange]: + return self._model.oa.all_entity_exchanges.by_target(self) + + @property + def outputs(self) -> c.ElementList[fa.FunctionalExchange]: + return self._model.oa.all_entity_exchanges.by_source(self) + + @property + def exchanges(self) -> c.ElementList[fa.FunctionalExchange]: + return self.inputs + self.outputs + @c.xtype_handler(XT_ARCH) class OperationalActivityPkg(c.GenericElement): @@ -146,14 +170,11 @@ class OperationalActivityPkg(c.GenericElement): @c.xtype_handler(XT_ARCH) -class CommunicationMean(c.GenericElement): +class CommunicationMean(fa.AbstractExchange): """An operational entity exchange""" _xmltag = "ownedComponentExchanges" - source = c.AttrProxyAccessor(c.GenericElement, "source") - target = c.AttrProxyAccessor(c.GenericElement, "target") - allocated_interactions = c.ProxyAccessor( fa.FunctionalExchange, fa.XT_COMP_EX_FNC_EX_ALLOC, @@ -246,6 +267,14 @@ class OperationalAnalysis(crosslayer.BaseArchitectureLayer): aslist=c.ElementList, ), ) +c.set_accessor( + OperationalActivity, + "packages", + c.ProxyAccessor( + OperationalActivityPkg, + aslist=c.ElementList, + ), +) c.set_self_references( (OperationalActivityPkg, "packages"), (OperationalCapabilityPkg, "packages"), diff --git a/capellambse/model/layers/pa.py b/capellambse/model/layers/pa.py index 47cc8761c..0646cdf51 100644 --- a/capellambse/model/layers/pa.py +++ b/capellambse/model/layers/pa.py @@ -34,13 +34,11 @@ @c.xtype_handler(XT_ARCH) -class PhysicalFunction(fa.AbstractFunction): +class PhysicalFunction(fa.Function): """A physical function on the Physical Architecture layer.""" _xmltag = "ownedPhysicalFunctions" - inputs = c.ProxyAccessor(fa.FunctionInputPort, aslist=c.ElementList) - outputs = c.ProxyAccessor(fa.FunctionOutputPort, aslist=c.ElementList) owner = c.CustomAccessor( c.GenericElement, operator.attrgetter("_model.pa.all_components"), @@ -53,10 +51,6 @@ class PhysicalFunction(fa.AbstractFunction): follow="targetElement", ) - is_leaf = property(lambda self: not self.functions) - - functions: c.Accessor - @c.xtype_handler(XT_ARCH) class PhysicalFunctionPkg(c.GenericElement): @@ -94,7 +88,6 @@ class PhysicalComponent(cs.Component): aslist=c.ElementList, follow="targetElement", ) - ports = c.ProxyAccessor(cs.PhysicalPort, aslist=c.ElementList) owned_components: c.Accessor deploying_components: c.Accessor @@ -214,6 +207,14 @@ class PhysicalArchitecture(crosslayer.BaseArchitectureLayer): aslist=c.ElementList, ), ) +c.set_accessor( + PhysicalFunction, + "packages", + c.ProxyAccessor( + PhysicalFunctionPkg, + aslist=c.ElementList, + ), +) c.set_self_references( (PhysicalComponent, "owned_components"), (PhysicalComponentPkg, "packages"), diff --git a/capellambse/svg/decorations.py b/capellambse/svg/decorations.py index 8157afbaf..0f9465bc5 100644 --- a/capellambse/svg/decorations.py +++ b/capellambse/svg/decorations.py @@ -37,16 +37,16 @@ "LogicalHumanComponent", } only_icons = {"Requirement"} -# TODO: Instead of dirty-patching either rename FunctionalExchange in OA-Layer as -# ActivityExchange/OperationalExchange DiagramClass = str FaultyClass = str PatchClass = str - -needs_patch: dict[DiagramClass, dict[FaultyClass, PatchClass]] = { - "Operational Entity Blank": {"FunctionalExchange": "OperationalExchange"} +always_top_label = { + "Class", + "Enumeration", + "Note", + "OperationalActivity", + "PhysicalComponent", } -always_top_label = {"Note", "Class", "Enumeration"} needs_feature_line = {"Class", "Enumeration"} diff --git a/capellambse/svg/drawing.py b/capellambse/svg/drawing.py index f29932ab5..920a2385a 100644 --- a/capellambse/svg/drawing.py +++ b/capellambse/svg/drawing.py @@ -62,9 +62,6 @@ def __init__(self, metadata: generate.DiagramMetadata): self.diagram_class = metadata.class_ self.stylesheet = self.make_stylesheet() self.obj_cache: dict[str | None, t.Any] = {} - self.requires_deco_patch = decorations.needs_patch.get( - self.diagram_class, {} - ) self.add_backdrop(pos=metadata.pos, size=metadata.size) def add_backdrop( @@ -536,7 +533,13 @@ def _draw_symbol( if class_ in decorations.all_ports: grp = self.add_port( - pos, size, text_style, parent_, class_=class_, label=label_ + pos, + size, + text_style, + parent_, + class_=class_, + label=label_, + id_=id_, ) else: grp = self.g(class_=f"Box {class_}", id_=id_) @@ -684,7 +687,6 @@ def _draw_edge( # Received text space doesn't allow for anything else than the text for label_ in labels_: label_["class"] = "Annotation" - self._draw_label_bbox(label_, grp, "AnnotationBB") self._draw_edge_label( label_, @@ -707,9 +709,9 @@ def _draw_edge_label( text_anchor: str = "start", y_margin: int | float, ) -> container.Group: - if class_ in self.requires_deco_patch: - class_ = self.requires_deco_patch[class_] - + class_ = ( + style.get_symbol_styleclass(class_, self.diagram_class) or class_ + ) if f"{class_}Symbol" in decorations.deco_factories: additional_space = ( decorations.icon_size + 2 * decorations.icon_padding diff --git a/capellambse/svg/generate.py b/capellambse/svg/generate.py index c939a9eb0..ed8095938 100644 --- a/capellambse/svg/generate.py +++ b/capellambse/svg/generate.py @@ -16,6 +16,7 @@ import collections.abc as cabc import dataclasses import json +import os import pathlib import typing as t @@ -98,7 +99,7 @@ def from_json(cls, jsonstring: str) -> SVGDiagram: return cls(metadata, jsondict["contents"]) @classmethod - def from_json_path(cls, path: str) -> SVGDiagram: + def from_json_path(cls, path: str | os.PathLike) -> SVGDiagram: """Create an SVGDiagram from the given JSON file. Parameters diff --git a/capellambse/svg/style.py b/capellambse/svg/style.py index 6d56a90ac..2b5f9b6fa 100644 --- a/capellambse/svg/style.py +++ b/capellambse/svg/style.py @@ -95,6 +95,10 @@ "OrControlNodeSymbol", "OperationalExchangeSymbol", ), + "Operational Activity Interaction Blank": ( + "OperationalActivitySymbol", + "OperationalExchangeSymbol", + ), "Physical Architecture Blank": ( "PhysicalLinkSymbol", "ComponentExchangeSymbol", @@ -119,6 +123,28 @@ "SystemHumanActorSymbol", ), } +MODIFY_STYLECLASS = {"FunctionalExchange"} + + +def get_symbol_styleclass(style: str | None, dstyle: str) -> str | None: + if ( + style not in MODIFY_STYLECLASS + or dstyle not in STATIC_DECORATIONS + or style in STATIC_DECORATIONS[dstyle] + ): + return None + + capitals = dstyle.split(" ") + assert capitals + layer = capitals[0] + if style.startswith(layer): + return None + + scapitals = re.findall(r"[A-Z][^A-Z]*", style) + symbol = f'{layer}{"".join(scapitals[1:])}' + if f"{symbol}Symbol" in STATIC_DECORATIONS[dstyle]: + return symbol + return None class Styling: diff --git a/tests/data/melodymodel/5_0/Melody Model Test.aird b/tests/data/melodymodel/5_0/Melody Model Test.aird index 175a3a6dc..efb77d9e0 100644 --- a/tests/data/melodymodel/5_0/Melody Model Test.aird +++ b/tests/data/melodymodel/5_0/Melody Model Test.aird @@ -87,6 +87,10 @@ + + + + @@ -97,7 +101,7 @@ - + @@ -105,6 +109,10 @@ + + + + @@ -135,10 +143,14 @@ - + + + + + @@ -8081,6 +8093,17 @@ + + + + + + + + + + + @@ -8167,6 +8190,15 @@ + + + + + + + + + backgroundColor @@ -8805,6 +8837,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -8968,6 +9033,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + @@ -12607,4 +12696,1429 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + + + + + + + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + + + + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + + + + + + + + + routingStyle + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + borderColor + borderSize + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + + + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + borderColor + borderSize + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + borderColor + borderSize + + + + + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + borderColor + borderSize + + + + + + + + + borderColor + borderSize + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + + + + + + + + + + + + + + + + + strokeColor + size + + + + + + + + + + strokeColor + size + + + + + + + + + + strokeColor + size + + + + + + + + + + + + + + + + + + + strokeColor + size + + + + + + + + + + strokeColor + size + + + + + + + + + + strokeColor + size + + + + + + + + + + + + + + + + + + + color + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + color + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/data/melodymodel/5_0/Melody Model Test.capella b/tests/data/melodymodel/5_0/Melody Model Test.capella index 004f96101..a1a8743c3 100644 --- a/tests/data/melodymodel/5_0/Melody Model Test.capella +++ b/tests/data/melodymodel/5_0/Melody Model Test.capella @@ -1135,6 +1135,27 @@ The predator is far away + + + + + + + + + + + + id="fe4cdb52-906d-49f2-83c6-449e5c6642af" targetElement="#b8f34893-c50b-4b7d-9326-1d01990434d8" sourceElement="#1a414995-f4cd-488c-8152-486e459fb9de"/> + + + + name="HollyWood" abstractType="#d4a22478-5717-4ca7-bfc9-9a193e6218a8"/> + name="Affleck" abstractType="#da12377b-fb70-4441-8faa-3a5c153c5de2"/> + + + + human="true"> + + + + + + id="d4a22478-5717-4ca7-bfc9-9a193e6218a8" name="HollyWood" actor="true"> + + id="da12377b-fb70-4441-8faa-3a5c153c5de2" name="Affleck" actor="true"> + + @@ -2029,7 +2090,14 @@ The predator is far away id="145327ea-d6eb-4ad2-abe5-79876938b0a7" name="First Class"> + human="true"> + + + + + + + - - - - diff --git a/tests/test_model_layers.py b/tests/test_model_layers.py index eaaea23e5..b7aa9f26e 100644 --- a/tests/test_model_layers.py +++ b/tests/test_model_layers.py @@ -25,7 +25,8 @@ StateTransition, ) from capellambse.model.crosslayer.capellacore import Constraint -from capellambse.model.crosslayer.fa import FunctionOutputPort +from capellambse.model.crosslayer.cs import PhysicalPort +from capellambse.model.crosslayer.fa import ComponentPort from capellambse.model.crosslayer.information import Class from capellambse.model.layers.ctx import SystemComponentPkg from capellambse.model.layers.la import CapabilityRealization @@ -705,3 +706,37 @@ def test_pa_component_exchange_is_found(self, model: MelodyModel) -> None: "3aa006b1-f954-4e8f-a4e9-2e9cd38555de" ) assert expected_exchange in model.pa.all_component_exchanges + + @pytest.mark.parametrize( + "uuid,port_attr,ports,class_", + [ + pytest.param( + "b51ccc6f-5f96-4e28-b90e-72463a3b50cf", + "physical_ports", + 3, + PhysicalPort, + id="PP", + ), + pytest.param( + "c78b5d7c-be0c-4ed4-9d12-d447cb39304e", + "ports", + 3, + ComponentPort, + id="CP", + ), + ], + ) + def test_pa_component_finds_ports( + self, + model: MelodyModel, + uuid: str, + port_attr: str, + ports: int, + class_: type, + ) -> None: + comp = model.by_uuid(uuid) + port_list = getattr(comp, port_attr) + + assert ports == len(port_list) + for p in port_list: + assert isinstance(p, class_) diff --git a/tests/test_svg.py b/tests/test_svg.py index f57efafcf..536884e69 100644 --- a/tests/test_svg.py +++ b/tests/test_svg.py @@ -14,14 +14,12 @@ from __future__ import annotations import collections.abc as cabc -import io import json import logging import math import pathlib import random import string -import sys import cssutils import pytest @@ -38,8 +36,6 @@ symbols, ) -from . import TEST_MODEL, TEST_ROOT - cssutils.log.setLevel(logging.CRITICAL) TEST_LAB = "[LAB] Wizzard Education" @@ -67,15 +63,8 @@ } -@pytest.fixture -def model(monkeypatch) -> capellambse.MelodyModel: - """Return test model""" - monkeypatch.setattr(sys, "stderr", io.StringIO) - return capellambse.MelodyModel(TEST_ROOT / "5_0" / TEST_MODEL) - - -@pytest.fixture -def tmp_json( +@pytest.fixture(name="tmp_json") +def tmp_json_fixture( model: capellambse.MelodyModel, tmp_path: pathlib.Path ) -> pathlib.Path: """Return tmp path of diagram json file""" @@ -85,9 +74,10 @@ def tmp_json( return dest -@pytest.mark.usefixtures("tmp_json") class TestSVG: - def test_diagram_meta_data_attributes(self, tmp_json) -> None: + def test_diagram_meta_data_attributes( + self, tmp_json: pathlib.Path + ) -> None: diag_meta = generate.DiagramMetadata.from_dict( json.loads(tmp_json.read_text()) ) @@ -97,7 +87,9 @@ def test_diagram_meta_data_attributes(self, tmp_json) -> None: assert diag_meta.viewbox == "10 10 1162 611" assert diag_meta.class_ == "Logical Architecture Blank" - def test_diagram_from_json_path_componentports(self, tmp_json) -> None: + def test_diagram_from_json_path_componentports( + self, tmp_json: pathlib.Path + ) -> None: tree = etree.fromstring( SVGDiagram.from_json_path(tmp_json).to_string() ) @@ -136,7 +128,7 @@ def test_diagram_from_json_path_componentports(self, tmp_json) -> None: assert cp_reference_exists @pytest.fixture - def tmp_svg(self, tmp_path) -> SVGDiagram: + def tmp_svg(self, tmp_path: pathlib.Path) -> SVGDiagram: name = "Test svg" meta = generate.DiagramMetadata( pos=(0, 0), size=(1, 1), name=name, class_="TEST" @@ -150,7 +142,7 @@ def test_diagram_saves(self, tmp_svg: SVGDiagram) -> None: assert pathlib.Path(tmp_svg.drawing.filename).is_file() # FIXME: change this to a parametrized test, do not use if- or for-statements in a unit test - def test_css_colors(self, tmp_json) -> None: + def test_css_colors(self, tmp_json: pathlib.Path) -> None: COLORS_TO_CHECK = { ".LogicalArchitectureBlank g.Box.CP_IN > line": { "stroke": "#000000" @@ -370,12 +362,12 @@ def test_css_colors(self, tmp_json) -> None: tree = etree.fromstring( SVGDiagram.from_json_path(tmp_json).to_string() ) - style = tree.xpath( + style_ = tree.xpath( "/x:svg/x:defs/x:style", namespaces={"x": "http://www.w3.org/2000/svg"}, )[0] - stylesheet = cssutils.parseString(style.text) + stylesheet = cssutils.parseString(style_.text) for rule in stylesheet: for (element, prop) in COLORS_TO_CHECK.items(): if ( @@ -407,7 +399,7 @@ def test_diagram_decorations( ): """Test diagrams get rendered successfully""" diag = model.diagrams.by_name(diagram_name) - svg_string = diag.render("svg") + diag.render("svg") @pytest.mark.parametrize("diagram_type", TEST_DECO) @pytest.mark.parametrize( @@ -583,20 +575,43 @@ def check_label_for_containment_and_overlap( assert symbol.attribs["y"] + symbol.attribs["height"] <= lower_bound -class TestSVGStylesheet: - def test_svg_stylesheet_as_str(self, tmp_json) -> None: - svg = SVGDiagram.from_json_path(tmp_json) - for line in str(svg.drawing.stylesheet).splitlines(): - assert line.startswith(".LogicalArchitectureBlank") +class TestSVGStyling: + LAB = "Logical Architecture Blank" + FCD = "Functional Chain Description" + OEB = "Operational Entity Blank" + FEX = "FunctionalExchange" + OEX = "OperationalExchange" + BOX = "LogicalComponentSymbol" - def test_svg_stylesheet_builder_fails_when_no_class_was_given(self): - with pytest.raises(TypeError) as error: - style.SVGStylesheet(None) # type: ignore[arg-type] + class TestSVGStylesheet: + def test_svg_stylesheet_as_str(self, tmp_json) -> None: + svg = SVGDiagram.from_json_path(tmp_json) + for line in str(svg.drawing.stylesheet).splitlines(): + assert line.startswith(".LogicalArchitectureBlank") - assert ( - error.value.args[0] - == "Invalid type for class_ 'NoneType'. This needs to be a str." - ) + def test_svg_stylesheet_builder_fails_when_no_class_was_given(self): + with pytest.raises(TypeError) as error: + style.SVGStylesheet(None) # type: ignore[arg-type] + + assert ( + error.value.args[0] + == "Invalid type for class_ 'NoneType'. This needs to be a str." + ) + + @pytest.mark.parametrize( + "style_,diagstyle,expected", + [ + pytest.param(None, "None", None, id="No modify"), + pytest.param(FEX, "None", None, id="Unregistered diagramstyle"), + pytest.param(BOX, LAB, None, id="Found in deco"), + pytest.param(FEX, FCD, None, id="Layer is prefix"), + pytest.param(FEX, OEB, OEX, id="Fixed FExchange styleclass"), + ], + ) + def test_get_symbol_styleclass( + self, style_: str | None, diagstyle: str, expected: str | None + ) -> None: + assert style.get_symbol_styleclass(style_, diagstyle) == expected class TestDecoFactory: