From 664240e13c71f0e63a3cd18e5c9d3134fb9ca381 Mon Sep 17 00:00:00 2001 From: ewuerger Date: Tue, 21 Feb 2023 13:35:25 +0100 Subject: [PATCH 1/7] feat: Use identifier in `data_types` and `requirement_types` Before this change the procedure used `long_name`s to match any `attribute_definition` found under a `requirement_type`. The snapshot disallows this by using only identifiers. --- capella_rm_bridge/changeset/actiontypes.py | 43 +++- capella_rm_bridge/changeset/change.py | 281 ++++++++++++--------- capella_rm_bridge/changeset/find.py | 17 +- tests/data/changesets/create.yaml | 102 ++++---- tests/data/changesets/delete.yaml | 4 +- tests/data/changesets/mod.yaml | 24 +- tests/data/model/RM Bridge.capella | 29 ++- tests/data/snapshots/snapshot.yaml | 58 +++-- tests/data/snapshots/snapshot1.yaml | 67 +++-- tests/data/snapshots/snapshot2.yaml | 32 ++- tests/test_changeset.py | 101 +++++--- 11 files changed, 453 insertions(+), 305 deletions(-) diff --git a/capella_rm_bridge/changeset/actiontypes.py b/capella_rm_bridge/changeset/actiontypes.py index 7c1cb7a..8f082b7 100644 --- a/capella_rm_bridge/changeset/actiontypes.py +++ b/capella_rm_bridge/changeset/actiontypes.py @@ -10,9 +10,12 @@ import typing_extensions as te Primitive = t.Union[int, float, str, list[str], bool, datetime.datetime] +"""Type alias for primitive values.""" class WorkitemTypeConfig(te.TypedDict): + """A configeration for workitem types.""" + name: str fields: te.NotRequired[cabc.Sequence[str]] @@ -25,9 +28,12 @@ class WorkitemTypeConfig(te.TypedDict): "id": str, }, ) +"""A configuration of an RM module.""" class Config(te.TypedDict): + """A configuration of an RM synchronization plan.""" + modules: cabc.Sequence[TrackerConfig] trackers: cabc.Sequence[TrackerConfig] @@ -37,6 +43,8 @@ class InvalidTrackerConfig(Exception): class WorkItem(te.TypedDict, total=False): + """A workitem from the snapshot.""" + id: te.Required[int] long_name: str text: str @@ -45,13 +53,31 @@ class WorkItem(te.TypedDict, total=False): children: cabc.Sequence[WorkItem] +class DataType(te.TypedDict): + """A data_type from the snapshot.""" + + long_name: str + values: list[DataTypeValue] + + +class DataTypeValue(te.TypedDict): + """An enum value/option from the snapshot.""" + + id: str + long_name: str + + class MetaData(te.TypedDict): + """Metadata of a snapshot.""" + tool: str revision: str connector: str class TrackerSnapshot(te.TypedDict): + """A snapshot of a whole module from the RM tool.""" + id: int version: int | float data_types: cabc.Mapping[str, cabc.Sequence[str]] @@ -60,22 +86,31 @@ class TrackerSnapshot(te.TypedDict): class Snapshot(te.TypedDict): + """A whole snapshot from the RM tool that may have multiple modules.""" + metadata: MetaData modules: cabc.Sequence[TrackerSnapshot] -class AttributeDefinition(te.TypedDict): - type: str - - class RequirementType(t.TypedDict): + """A requirement type from the snapshot.""" + long_name: str attributes: cabc.Mapping[ str, t.Union[AttributeDefinition, EnumAttributeDefinition] ] +class AttributeDefinition(te.TypedDict): + """An attribute definition from the snapshot.""" + + long_name: str + type: str + + class EnumAttributeDefinition(AttributeDefinition, total=False): + """An attribute definition with `type == enum` from the snapshot.""" + values: list[str] multi_values: bool diff --git a/capella_rm_bridge/changeset/change.py b/capella_rm_bridge/changeset/change.py index 90a9cb7..9014b49 100644 --- a/capella_rm_bridge/changeset/change.py +++ b/capella_rm_bridge/changeset/change.py @@ -42,7 +42,7 @@ class _AttributeValueBuilder(t.NamedTuple): deftype: str key: str - value: act.Primitive | None + value: act.Primitive | RMIdentifier | None class MissingCapellaModule(Exception): @@ -77,7 +77,7 @@ class TrackerChange: """A mapping for whitelisted fieldnames per RequirementType.""" actions: list[dict[str, t.Any]] """List of action requests for the tracker sync.""" - data_type_definitions: cabc.Mapping[str, list[str]] + data_type_definitions: cabc.Mapping[str, act.DataType] """A lookup for DataTypeDefinitions from the tracker snapshot.""" requirement_types: cabc.Mapping[RMIdentifier, act.RequirementType] """A lookup for RequirementTypes from the tracker snapshot.""" @@ -319,12 +319,12 @@ def yield_data_type_definition_create_actions( self, ) -> cabc.Iterator[dict[str, t.Any]]: r"""Yield actions for creating ``EnumDataTypeDefinition``\ s.""" - for name, values in self.data_type_definitions.items(): - yield self.data_type_create_action(name, values) + for id, ddef in self.data_type_definitions.items(): + yield self.data_type_create_action(id, ddef) # pylint: disable=line-too-long def data_type_create_action( - self, name: str, values: cabc.Iterable[str] + self, data_type_id: str, data_type_definition: act.DataType ) -> dict[str, t.Any]: r"""Return an action for creating an ``EnumDataTypeDefinition``. @@ -337,13 +337,18 @@ def data_type_create_action( """ type = "EnumerationDataTypeDefinition" enum_values = [ - {"long_name": value, "promise_id": f"EnumValue {name} {value}"} - for value in values + { + "identifier": value["id"], + "long_name": value["long_name"], + "promise_id": f"EnumValue {data_type_id} {value['id']}", + } + for value in data_type_definition["values"] ] return { - "long_name": name, + "identifier": data_type_id, + "long_name": data_type_definition["long_name"], "values": enum_values, - "promise_id": f"{type} {name}", + "promise_id": f"{type} {data_type_id}", "_type": type, } @@ -369,10 +374,10 @@ def requirement_type_create_action( attribute. """ attribute_definitions = list[dict[str, t.Any]]() - for name, adef in req_type.get("attributes", {}).items(): + for id, adef in req_type.get("attributes", {}).items(): try: attr_def = self.attribute_definition_create_action( - name, adef, identifier + id, adef, identifier ) attribute_definitions.append(attr_def) except act.InvalidAttributeDefinition as error: @@ -390,14 +395,14 @@ def requirement_type_create_action( def attribute_definition_create_action( self, - name: str, + id: str, item: act.AttributeDefinition | act.EnumAttributeDefinition, req_type_id: str, ) -> dict[str, t.Any]: r"""Return an action for creating ``AttributeDefinition``\ s. In case of an ``AttributeDefinitionEnumeration`` requires - ``name`` of possibly promised ``EnumerationDataTypeDefinition``. + ``id`` of possibly promised ``EnumerationDataTypeDefinition``. See Also -------- @@ -405,20 +410,23 @@ def attribute_definition_create_action( capellambse.extensions.reqif.AttributeDefinition capellambse.extensions.reqif.AttributeDefinitionEnumeration """ - identifier = f"{name} {req_type_id}" - base: dict[str, t.Any] = {"long_name": name, "identifier": identifier} + identifier = f"{id} {req_type_id}" + base: dict[str, t.Any] = { + "identifier": identifier, + "long_name": item["long_name"], + } cls = reqif.AttributeDefinition if item["type"] == "Enum": cls = reqif.AttributeDefinitionEnumeration - etdef = self.reqfinder.enum_data_type_definition_by_long_name( - name, below=self.reqt_folder + etdef = self.reqfinder.enum_data_type_definition_by_identifier( + id, below=self.reqt_folder ) if etdef is None: - promise_id = f"EnumerationDataTypeDefinition {name}" - if name not in self.data_type_definitions: + promise_id = f"EnumerationDataTypeDefinition {id}" + if id not in self.data_type_definitions: self._faulty_attribute_definitions.add(promise_id) raise act.InvalidAttributeDefinition( - f"Invalid {cls.__name__} found: {name!r}. Missing its " + f"Invalid {cls.__name__} found: {id!r}. Missing its " "datatype definition in `data_types`." ) @@ -451,15 +459,15 @@ def yield_requirements_create_actions( iid = item["id"] attributes = list[dict[str, t.Any]]() req_type_id = RMIdentifier(item.get("type", "")) - for name, value in item.get("attributes", {}).items(): - check = self._check_attribute((name, value), (req_type_id, iid)) + for attr_id, value in item.get("attributes", {}).items(): + check = self._check_attribute((attr_id, value), (req_type_id, iid)) if check == "break": break elif check == "continue": continue self._try_create_attribute_value( - (name, value), (req_type_id, iid), attributes + (attr_id, value), (req_type_id, iid), attributes ) identifier = RMIdentifier(str(item["id"])) @@ -523,7 +531,7 @@ def _check_attribute( attribute: tuple[t.Any, t.Any], identifiers: tuple[RMIdentifier, t.Any], ) -> str | None: - name, value = attribute + id, value = attribute req_type_id, iitem_id = identifiers if not req_type_id: self._handle_user_error( @@ -532,14 +540,14 @@ def _check_attribute( ) return "break" - if _blacklisted(name, value): + if _blacklisted(id, value): return "continue" reqtype_defs = self.requirement_types.get(req_type_id) - if reqtype_defs and name not in reqtype_defs["attributes"]: + if reqtype_defs and id not in reqtype_defs["attributes"]: self._handle_user_error( f"Invalid workitem '{iitem_id}'. " - f"Invalid field found: field name '{name}' not defined in " + f"Invalid field found: field identifier '{id}' not defined in " f"attributes of requirement type '{req_type_id}'" ) return "continue" @@ -551,12 +559,10 @@ def _try_create_attribute_value( identifiers: tuple[RMIdentifier, t.Any], actions: list[dict[str, t.Any]], ) -> None: - name, value = attribute + id, value = attribute req_type_id, iitem_id = identifiers try: - action = self.attribute_value_create_action( - name, value, req_type_id - ) + action = self.attribute_value_create_action(id, value, req_type_id) actions.append(action) except act.InvalidFieldValue as error: self._handle_user_error( @@ -564,17 +570,19 @@ def _try_create_attribute_value( ) def attribute_value_create_action( - self, name: str, value: str | list[str], req_type_id: RMIdentifier + self, + id: str, + value: act.Primitive | RMIdentifier, + req_type_id: RMIdentifier, ) -> dict[str, t.Any]: """Return an action for creating an ``AttributeValue``. - Requires ``name`` of possibly promised + Requires ``id`` of possibly promised ``AttributeDefinition/AttributeDefinitionEnumeration`` and checks given ``value`` to be of the correct types. See Also -------- - patch_faulty_attribute_value capellambse.extension.reqif.IntegerValueAttribute capellambse.extension.reqif.StringValueAttribute capellambse.extension.reqif.RealValueAttribute @@ -582,29 +590,29 @@ def attribute_value_create_action( capellambse.extension.reqif.BooleanValueAttribute capellambse.extension.reqif.EnumerationValueAttribute """ - builder = self.check_attribute_value_is_valid(name, value, req_type_id) + builder = self.check_attribute_value_is_valid(id, value, req_type_id) deftype = "AttributeDefinition" values: list[decl.UUIDReference | decl.Promise] = [] if builder.deftype == "Enum": deftype += "Enumeration" assert isinstance(builder.value, list) - edtdef = self.reqfinder.enum_data_type_definition_by_long_name( - name, below=self.reqt_folder + edtdef = self.reqfinder.enum_data_type_definition_by_identifier( + id, below=self.reqt_folder ) - for enum_name in builder.value: - enumvalue = self.reqfinder.enum_value_by_long_name( - enum_name, below=edtdef or self.reqt_folder + for evid in builder.value: + enumvalue = self.reqfinder.enum_value_by_identifier( + evid, below=edtdef or self.reqt_folder ) if enumvalue is None: - ev_ref = decl.Promise(f"EnumValue {name} {enum_name}") + ev_ref = decl.Promise(f"EnumValue {id} {evid}") assert ev_ref is not None else: ev_ref = decl.UUIDReference(enumvalue.uuid) values.append(ev_ref) - attr_def_id = f"{name} {req_type_id}" + attr_def_id = f"{id} {req_type_id}" definition = self.reqfinder.attribute_definition_by_identifier( deftype, attr_def_id, below=self.reqt_folder ) @@ -612,7 +620,7 @@ def attribute_value_create_action( promise_id = f"{deftype} {attr_def_id}" if promise_id in self._faulty_attribute_definitions: raise act.InvalidFieldValue( - f"Invalid field found: No AttributeDefinition {name!r} " + f"Invalid field found: No AttributeDefinition {id!r} " "promised." ) else: @@ -627,37 +635,60 @@ def attribute_value_create_action( } def check_attribute_value_is_valid( - self, name: str, value: str | list[str], req_type_id: RMIdentifier + self, id: str, value: act.Primitive, req_type_id: RMIdentifier ) -> _AttributeValueBuilder: - """Raise a if .""" + """Perform various integrity checks on the given attribute value. + + Raises + ------ + capella_rm_bridge.actiontypes.InvalidFieldValue + If the types are not matching, if the used data_type + definition is missing in the snapshot or the used options + on an `EnumerationAttributeValue` are missing in the + snapshot. + + Returns + ------- + builder + A data-class that gathers all needed data for creating the + `(Enumeration)AttributeValue`. + """ reqtype_attr_defs = self.requirement_types[req_type_id]["attributes"] - deftype = reqtype_attr_defs[name]["type"] + deftype = reqtype_attr_defs[id]["type"] if default_type := _ATTR_VALUE_DEFAULT_MAP.get(deftype): matches_type = isinstance(value, default_type) else: matches_type = True LOGGER.warning( - "Unknown field type '%s' for %s: %r", deftype, name, value + "Unknown field type '%s' for %s: %r", deftype, id, value + ) + + if not matches_type: + assert default_type is not None + raise act.InvalidFieldValue( + f"Invalid field found: {id!r}. Not matching expected types: " + f"{value!r} should be of type {default_type.__name__!r}" ) + if deftype == "Enum": - options = self.data_type_definitions.get(name) - if options is None: + datatype = self.data_type_definitions.get(id) + if datatype is None: raise act.InvalidFieldValue( - f"Invalid field found: {name!r}. Missing its " + f"Invalid field found: {id!r}. Missing its " "datatype definition in `data_types`." ) - is_faulty = not matches_type or not set(value) & set(options) + assert isinstance(value, cabc.Iterable) + assert not isinstance(value, str) + options = (value["id"] for value in datatype["values"]) key = "values" + if not set(value) & set(options): + raise act.InvalidFieldValue( + f"Invalid field found: {key} {value!r} for {id!r}" + ) else: - is_faulty = not matches_type key = "value" - if is_faulty: - raise act.InvalidFieldValue( - f"Invalid field found: {key} {value!r} for {name!r}" - ) - return _AttributeValueBuilder(deftype, key, value) def data_type_definition_mod_actions(self) -> None: @@ -673,13 +704,13 @@ def data_type_definition_mod_actions(self) -> None: dt_defs_deletions: list[decl.UUIDReference] = [ decl.UUIDReference(dtdef.uuid) for dtdef in self.reqt_folder.data_type_definitions - if dtdef.long_name not in self.data_type_definitions + if dtdef.identifier not in self.data_type_definitions ] dt_defs_creations = list[dict[str, t.Any]]() dt_defs_modifications = list[dict[str, t.Any]]() - for name, values in self.data_type_definitions.items(): - action = self.data_type_mod_action(name, values) + for id, ddef in self.data_type_definitions.items(): + action = self.data_type_mod_action(id, ddef) if action is None: continue @@ -701,15 +732,15 @@ def data_type_definition_mod_actions(self) -> None: self.actions.extend(dt_defs_modifications) def data_type_mod_action( - self, name: str, values: cabc.Sequence[str] + self, id: str, ddef: act.DataType ) -> dict[str, t.Any] | None: """Return an Action for creating or modifying a DataTypeDefinition. - If a :class:`reqif.DataTypeDefinition` can be found via - ``long_name`` and it differs against the snapshot an action for - modification is returned. If it doesn't differ the returned - ``action`` is ``None``. If the definition can't be found an - action for creating an + If a :class:`reqif.DataTypeDefinition` can be found via ``id`` + and it differs against the snapshot an action for modification + is returned. If it doesn't differ the returned ``action`` is + ``None``. If the definition can't be found an action for + creating an :class:`~capellambse.extensions.reqif.EnumerationDataTypeDefinition`` is returned. @@ -721,32 +752,42 @@ def data_type_mod_action( """ assert self.reqt_folder try: - dtdef = self.reqt_folder.data_type_definitions.by_long_name( - name, single=True + dtdef = self.reqt_folder.data_type_definitions.by_identifier( + id, single=True ) base = {"parent": decl.UUIDReference(dtdef.uuid)} mods = dict[str, t.Any]() - if dtdef.long_name != name: - mods["long_name"] = name + if dtdef.long_name != ddef["long_name"]: + mods["long_name"] = ddef["long_name"] - current = set(values) - creations = current - set(dtdef.values.by_long_name) - action = self.data_type_create_action(dtdef.long_name, creations) - deletions = set(dtdef.values.by_long_name) - current - if creations: - base["extend"] = {"values": action["values"]} if mods: base["modify"] = mods + + creations = [ + value + for value in ddef["values"] + if value["id"] not in dtdef.values.by_identifier + ] + action = self.data_type_create_action( + id, {"long_name": ddef["long_name"], "values": creations} + ) + if creations: + base["extend"] = {"values": action["values"]} + + deletions = set(dtdef.values.by_identifier) - set( + value["id"] for value in ddef["values"] + ) if deletions: - evs = dtdef.values.by_long_name(*deletions) + evs = dtdef.values.by_identifier(*deletions) base["delete"] = { "values": [decl.UUIDReference(ev.uuid) for ev in evs] } + if set(base) == {"parent"}: return None return base except KeyError: - return self.data_type_create_action(name, values) + return self.data_type_create_action(id, ddef) def requirement_type_delete_actions(self) -> None: r"""Populate actions for deleting ``RequirementType``\ s.""" @@ -783,17 +824,20 @@ def requirement_type_mod_action( f"Invalid workitem '{identifier}'. {error.args[0]}" ) + attribute_definition_ids = ( + f"{id} {reqtype.identifier}" for id in item["attributes"] + ) attr_defs_deletions: list[decl.UUIDReference] = [ decl.UUIDReference(adef.uuid) for adef in reqtype.attribute_definitions - if adef.long_name not in item["attributes"] + if adef.identifier not in attribute_definition_ids ] attr_defs_creations = list[dict[str, t.Any]]() attr_defs_modifications = list[dict[str, t.Any]]() - for name, data in item["attributes"].items(): + for id, data in item["attributes"].items(): action = self.attribute_definition_mod_action( - reqtype, name, data + reqtype, id, data ) if action is None: continue @@ -875,8 +919,8 @@ def yield_requirements_mod_actions( item_attributes = item.get("attributes", {}) attributes_creations = list[dict[str, t.Any]]() attributes_modifications = list[dict[str, t.Any]]() - for name, value in item_attributes.items(): - check = self._check_attribute((name, value), (req_type_id, iid)) + for id, value in item_attributes.items(): + check = self._check_attribute((id, value), (req_type_id, iid)) if check == "break": break elif check == "continue": @@ -885,12 +929,12 @@ def yield_requirements_mod_actions( action: act.Primitive | dict[str, t.Any] | None if mods.get("type"): self._try_create_attribute_value( - (name, value), (req_type_id, iid), attributes_creations + (id, value), (req_type_id, iid), attributes_creations ) else: try: action = self.attribute_value_mod_action( - req, name, value, req_type_id + req, id, value, req_type_id ) if action is None: continue @@ -898,18 +942,21 @@ def yield_requirements_mod_actions( attributes_modifications.append(action) except KeyError: self._try_create_attribute_value( - (name, value), (req_type_id, iid), attributes_creations + (id, value), (req_type_id, iid), attributes_creations ) except act.InvalidFieldValue as error: self._handle_user_error( f"Invalid workitem '{iid}'. {error.args[0]}" ) + attribute_definition_ids = { + f"{attr} {req_type_id}" for attr in item_attributes + } if not attributes_deletions: attributes_deletions = [ decl.UUIDReference(attr.uuid) for attr in req.attributes - if attr.definition.long_name not in item_attributes + if attr.definition.identifier not in attribute_definition_ids ] if mods: @@ -1006,15 +1053,15 @@ def yield_requirements_mod_actions( def attribute_value_mod_action( self, req: reqif.RequirementsModule | WorkItem, - name: str, - value: str | list[str], + id: str, + valueid: str | list[str], req_type_id: RMIdentifier, ) -> dict[str, t.Any] | None: """Return an action for modifying an ``AttributeValue``. If an ``AttributeValue`` can be found via - ``definition.long_name`` it is compared against the snapshot. If - any changes to its ``value/s`` are identified a tuple of the + ``definition.identifier`` it is compared against the snapshot. + If any changes to its ``value/s`` are identified a tuple of the name and value is returned else None. If the attribute can't be found an action for creation is returned. @@ -1023,12 +1070,12 @@ def attribute_value_mod_action( req The ReqIFElement under which the attribute value modification takes place. - name - The name of the definition for the attribute value. - value - The value from the snapshot that is compared against the - value on the found attribute if it exists. Else a new - ``AttributeValue`` with this value is created. + id + The identifier of the definition for the attribute value. + valueid + The value identifier from the snapshot that is compared + against the value on the found attribute if it exists. Else + a new ``AttributeValue`` with this value is created. req_type_id : optional The identifier of ``RequirementType`` for given ``req`` if it was changed. If not given or ``None`` the identifier @@ -1040,51 +1087,51 @@ def attribute_value_mod_action( Either a create-action, the value to modify or ``None`` if nothing changed. """ - builder = self.check_attribute_value_is_valid(name, value, req_type_id) + builder = self.check_attribute_value_is_valid(id, valueid, req_type_id) deftype = "AttributeDefinition" if builder.deftype == "Enum": deftype += "Enumeration" attrdef = self.reqfinder.attribute_definition_by_identifier( - deftype, f"{name} {req_type_id}", self.reqt_folder + deftype, f"{id} {req_type_id}", self.reqt_folder ) attr = req.attributes.by_definition(attrdef, single=True) if isinstance(attr, reqif.EnumerationValueAttribute): - assert isinstance(value, list) - actual = set(attr.values.by_long_name) - delete = actual - set(value) - create = set(value) - actual + assert isinstance(valueid, list) + actual = set(attr.values.by_identifier) + delete = actual - set(valueid) + create = set(valueid) - actual differ = bool(create) or bool(delete) - options = attrdef.data_type.values.by_long_name - value = [ - decl.Promise(f"EnumValue {name} {v}") + options = attrdef.data_type.values.by_identifier + valueid = [ + decl.Promise(f"EnumValue {id} {v}") if v not in options else decl.UUIDReference(options(v, single=True).uuid) for v in create | (actual - delete) ] key = "values" else: - differ = bool(attr.value != value) + differ = bool(attr.value != valueid) key = "value" if differ: return { "parent": decl.UUIDReference(attr.uuid), - "modify": {key: value}, + "modify": {key: valueid}, } return None def attribute_definition_mod_action( self, reqtype: reqif.RequirementType, - name: str, + identifier: str, data: act.AttributeDefinition | act.EnumAttributeDefinition, ) -> dict[str, t.Any] | None: """Return an action for an ``AttributeDefinition``. If a :class:`capellambse.extensions.reqif.AttributeDefinition` or :class:`capellambse.extensions.reqif.AttributeDefinitionEnumeration` - can be found via ``long_name`` it is compared against the + can be found via its ``identifier``, it is compared against the snapshot. If any changes are identified an action for modification is returned else None. If the definition can't be found an action for creation is returned. @@ -1095,16 +1142,16 @@ def attribute_definition_mod_action( Either a create-, mod-action or ``None`` if nothing changed. """ try: - attrdef = reqtype.attribute_definitions.by_long_name( - name, single=True + attrdef = reqtype.attribute_definitions.by_identifier( + f"{identifier} {reqtype.identifier}", single=True ) mods = dict[str, t.Any]() - if attrdef.long_name != name: - mods["long_name"] = name + if attrdef.long_name != data["long_name"]: + mods["long_name"] = data["long_name"] if data["type"] == "Enum": dtype = attrdef.data_type - if dtype is None or dtype.long_name != name: - mods["data_type"] = name + if dtype is None or dtype.identifier != identifier: + mods["data_type"] = identifier if ( attrdef.multi_valued != data.get("multi_values") @@ -1119,7 +1166,7 @@ def attribute_definition_mod_action( except KeyError: try: return self.attribute_definition_create_action( - name, data, reqtype.identifier + identifier, data, reqtype.identifier ) except act.InvalidAttributeDefinition as error: self._handle_user_error( diff --git a/capella_rm_bridge/changeset/find.py b/capella_rm_bridge/changeset/find.py index a0f93ad..fb63ae5 100644 --- a/capella_rm_bridge/changeset/find.py +++ b/capella_rm_bridge/changeset/find.py @@ -79,27 +79,22 @@ def requirement_by_identifier( str(identifier), reqif.Requirement.__name__, below=below ) - def enum_data_type_definition_by_long_name( - self, long_name: str, below: reqif.ReqIFElement | None + def enum_data_type_definition_by_identifier( + self, id: str, below: reqif.ReqIFElement | None ) -> reqif.EnumerationDataTypeDefinition | None: """Try to return an ``EnumerationDataTypeDefinition``. The object is matched with given ``long_name``. """ return self._get( - long_name, - reqif.EnumerationDataTypeDefinition.__name__, - attr="long_name", - below=below, + id, reqif.EnumerationDataTypeDefinition.__name__, below=below ) - def enum_value_by_long_name( - self, long_name: str, below: reqif.ReqIFElement | None = None + def enum_value_by_identifier( + self, id: str, below: reqif.ReqIFElement | None = None ) -> reqif.EnumValue | None: """Try to return an ``EnumValue``. The object is matched with given ``long_name``. """ - return self._get( - long_name, reqif.EnumValue.__name__, attr="long_name", below=below - ) + return self._get(id, reqif.EnumValue.__name__, below=below) diff --git a/tests/data/changesets/create.yaml b/tests/data/changesets/create.yaml index c7b49da..afe9c47 100644 --- a/tests/data/changesets/create.yaml +++ b/tests/data/changesets/create.yaml @@ -10,72 +10,78 @@ - long_name: Types identifier: "-2" data_type_definitions: - - long_name: Type + - identifier: type + long_name: Type values: - - long_name: Unset - promise_id: EnumValue Type Unset - - long_name: Functional - promise_id: EnumValue Type Functional - promise_id: EnumerationDataTypeDefinition Type + - identifier: unset + long_name: Unset + promise_id: EnumValue type unset + - identifier: functional + long_name: Functional + promise_id: EnumValue type functional + promise_id: EnumerationDataTypeDefinition type _type: EnumerationDataTypeDefinition - - long_name: Release + - identifier: release + long_name: Release values: - - long_name: Feature Rel. 1 - promise_id: EnumValue Release Feature Rel. 1 - - long_name: Feature Rel. 2 - promise_id: EnumValue Release Feature Rel. 2 - promise_id: EnumerationDataTypeDefinition Release + - identifier: featureRel.1 + long_name: Feature Rel. 1 + promise_id: EnumValue release featureRel.1 + - identifier: featureRel.2 + long_name: Feature Rel. 2 + promise_id: EnumValue release featureRel.2 + promise_id: EnumerationDataTypeDefinition release _type: EnumerationDataTypeDefinition requirement_types: - identifier: system_requirement long_name: System Requirement promise_id: RequirementType system_requirement attribute_definitions: - - long_name: Capella ID - identifier: Capella ID system_requirement - promise_id: AttributeDefinition Capella ID system_requirement + - identifier: capellaID system_requirement + long_name: Capella ID + promise_id: AttributeDefinition capellaID system_requirement _type: AttributeDefinition - - long_name: Type - identifier: Type system_requirement - data_type: !promise EnumerationDataTypeDefinition Type + - identifier: type system_requirement + long_name: Type + data_type: !promise EnumerationDataTypeDefinition type multi_valued: false - promise_id: AttributeDefinitionEnumeration Type system_requirement + promise_id: AttributeDefinitionEnumeration type system_requirement _type: AttributeDefinitionEnumeration - - long_name: Submitted at - identifier: Submitted at system_requirement - promise_id: AttributeDefinition Submitted at system_requirement + - identifier: submittedAt system_requirement + long_name: Submitted at + promise_id: AttributeDefinition submittedAt system_requirement _type: AttributeDefinition - - long_name: Release - identifier: Release system_requirement - data_type: !promise EnumerationDataTypeDefinition Release + - identifier: release system_requirement + long_name: Release + data_type: !promise EnumerationDataTypeDefinition release multi_valued: true - promise_id: AttributeDefinitionEnumeration Release system_requirement + promise_id: AttributeDefinitionEnumeration release system_requirement _type: AttributeDefinitionEnumeration - identifier: software_requirement long_name: Software Requirement promise_id: RequirementType software_requirement attribute_definitions: - - long_name: Capella ID - identifier: Capella ID software_requirement - promise_id: AttributeDefinition Capella ID software_requirement + - identifier: capellaID software_requirement + long_name: Capella ID + promise_id: AttributeDefinition capellaID software_requirement _type: AttributeDefinition - - long_name: Type - identifier: Type software_requirement - data_type: !promise EnumerationDataTypeDefinition Type + - identifier: type software_requirement + long_name: Type + data_type: !promise EnumerationDataTypeDefinition type multi_valued: false - promise_id: AttributeDefinitionEnumeration Type software_requirement + promise_id: AttributeDefinitionEnumeration type software_requirement _type: AttributeDefinitionEnumeration - - long_name: Submitted at - identifier: Submitted at software_requirement - promise_id: AttributeDefinition Submitted at software_requirement + - identifier: submittedAt software_requirement + long_name: Submitted at + promise_id: AttributeDefinition submittedAt software_requirement _type: AttributeDefinition - identifier: stakeholder_requirement long_name: Stakeholder Requirement promise_id: RequirementType stakeholder_requirement attribute_definitions: - - long_name: Capella ID - identifier: Capella ID stakeholder_requirement - promise_id: AttributeDefinition Capella ID stakeholder_requirement + - identifier: capellaID stakeholder_requirement + long_name: Capella ID + promise_id: AttributeDefinition capellaID stakeholder_requirement _type: AttributeDefinition folders: - long_name: Functional Requirements @@ -91,7 +97,7 @@ identifier: REQ-004 type: !promise RequirementType stakeholder_requirement attributes: - - definition: !promise AttributeDefinition Capella ID stakeholder_requirement + - definition: !promise AttributeDefinition capellaID stakeholder_requirement value: R-FNC-00002 _type: string requirements: @@ -100,18 +106,18 @@ text: "..." type: !promise RequirementType system_requirement attributes: - - definition: !promise AttributeDefinition Capella ID system_requirement + - definition: !promise AttributeDefinition capellaID system_requirement value: R-FNC-00001 _type: string - - definition: !promise AttributeDefinitionEnumeration Type system_requirement + - definition: !promise AttributeDefinitionEnumeration type system_requirement values: - - !promise EnumValue Type Functional + - !promise EnumValue type functional _type: enum - - definition: !promise AttributeDefinitionEnumeration Release system_requirement + - definition: !promise AttributeDefinitionEnumeration release system_requirement values: - - !promise EnumValue Release Feature Rel. 1 - - !promise EnumValue Release Feature Rel. 2 + - !promise EnumValue release featureRel.1 + - !promise EnumValue release featureRel.2 _type: enum - - definition: !promise AttributeDefinition Submitted at system_requirement - value: 2022-06-30 15:07:18.664000+00:00 + - definition: !promise AttributeDefinition submittedAt system_requirement + value: 2022-06-30 17:07:18.664000+02:00 _type: date diff --git a/tests/data/changesets/delete.yaml b/tests/data/changesets/delete.yaml index a92d5f4..5eb6fbc 100644 --- a/tests/data/changesets/delete.yaml +++ b/tests/data/changesets/delete.yaml @@ -15,11 +15,11 @@ delete: attribute_definitions: - !uuid e46d4629-da10-4f7f-954e-146bd2697638 - - long_name Test after migration + - long_name Test after migration # dynamically generated object - parent: !uuid b2c39449-ebbe-43cb-a712-22c740707a8b delete: attributes: - - definition.long_name Release + - definition.long_name Release # dynamically generated object requirements: - !uuid 25ccf941-17ed-4226-847b-040575922283 - parent: !uuid 3be8d0fc-c693-4b9b-8fa1-d59a9eec6ea4 diff --git a/tests/data/changesets/mod.yaml b/tests/data/changesets/mod.yaml index 95befa4..36b54d6 100644 --- a/tests/data/changesets/mod.yaml +++ b/tests/data/changesets/mod.yaml @@ -4,13 +4,15 @@ - parent: !uuid 686e198b-8baf-49f9-9d85-24571bd05d93 extend: values: - - long_name: Non-Functional - promise_id: EnumValue Type Non-Functional + - identifier: nonFunctional + long_name: Non-Functional + promise_id: EnumValue type nonFunctional - parent: !uuid 98aaca05-d47f-4916-a9f9-770dc60aa04f extend: values: - - long_name: Rel. 1 - promise_id: EnumValue Release Rel. 1 + - identifier: rel.1 + long_name: Rel. 1 + promise_id: EnumValue release rel.1 delete: values: - !uuid e91aa844-584d-451d-a201-2bb7bf13dcb0 @@ -18,9 +20,9 @@ - parent: !uuid 02bb4cdd-52cc-4fc3-af1c-e57dad8c51de extend: attribute_definitions: - - long_name: Test after migration - identifier: Test after migration system_requirement - promise_id: AttributeDefinition Test after migration system_requirement + - identifier: testAfterMigration system_requirement + long_name: Test after migration + promise_id: AttributeDefinition testAfterMigration system_requirement _type: AttributeDefinition - parent: !uuid cbc96c24-58c9-4292-9dfa-1ecb5ee16a82 modify: @@ -36,18 +38,18 @@ _type: string - definition: !uuid 12d97ef4-8b6e-4d45-8b3d-a1b6fe5b8aed values: - - !promise EnumValue Type Non-Functional + - !promise EnumValue type nonFunctional _type: enum - definition: !uuid e46d4629-da10-4f7f-954e-146bd2697638 values: - - !promise EnumValue Release Rel. 1 + - !promise EnumValue release rel.1 _type: enum - parent: !uuid 25ccf941-17ed-4226-847b-040575922283 modify: long_name: Non-Function Requirement extend: attributes: - - definition: !promise AttributeDefinition Test after migration system_requirement + - definition: !promise AttributeDefinition testAfterMigration system_requirement value: New _type: string delete: @@ -59,7 +61,7 @@ - parent: !uuid 6708cf60-2f06-4ccf-9973-21a035415ccb modify: values: - - !promise EnumValue Type Non-Functional + - !promise EnumValue type nonFunctional - parent: !uuid 9a9b5a8f-a6ad-4610-9e88-3b5e9c943c19 modify: long_name: Functional Requirements diff --git a/tests/data/model/RM Bridge.capella b/tests/data/model/RM Bridge.capella index 7de9981..6cba1b4 100644 --- a/tests/data/model/RM Bridge.capella +++ b/tests/data/model/RM Bridge.capella @@ -1836,47 +1836,48 @@ The predator is far away + id="686e198b-8baf-49f9-9d85-24571bd05d93" ReqIFIdentifier="type" ReqIFLongName="Type"> + ReqIFIdentifier="unset" ReqIFLongName="Unset"/> + ReqIFIdentifier="functional" ReqIFLongName="Functional"/> + id="98aaca05-d47f-4916-a9f9-770dc60aa04f" ReqIFIdentifier="release" + ReqIFLongName="Release"> + ReqIFIdentifier="featureRel.1" ReqIFLongName="Feature Rel. 1"/> + ReqIFIdentifier="featureRel.2" ReqIFLongName="Feature Rel. 2"/> + ReqIFIdentifier="capellaID system_requirement" ReqIFLongName="Capella ID"/> + ReqIFIdentifier="submittedAt system_requirement" ReqIFLongName="Submitted at"/> + ReqIFIdentifier="capellaID software_requirement" ReqIFLongName="Capella ID"/> + ReqIFIdentifier="submittedAt software_requirement" ReqIFLongName="Submitted at"/> + ReqIFIdentifier="capellaID stakeholder_requirement" ReqIFLongName="Capella ID"/> diff --git a/tests/data/snapshots/snapshot.yaml b/tests/data/snapshots/snapshot.yaml index 49368b6..a491f61 100644 --- a/tests/data/snapshots/snapshot.yaml +++ b/tests/data/snapshots/snapshot.yaml @@ -10,39 +10,55 @@ modules: - id: project/space/example title long_name: example title data_types: # Enumeration Data Type Definitions - Type: - - Unset - - Functional - Release: - - Feature Rel. 1 - - Feature Rel. 2 + type: + long_name: Type + values: + - id: unset + long_name: Unset + - id: functional + long_name: Functional + release: + long_name: Release + values: + - id: featureRel.1 + long_name: Feature Rel. 1 + - id: featureRel.2 + long_name: Feature Rel. 2 requirement_types: # WorkItemTypes system_requirement: long_name: System Requirement attributes: # Field Definitions, we don't need the IDs - Capella ID: # Field name + capellaID: # Field name + long_name: Capella ID type: String # -> AttributeDefinition - Type: + type: + long_name: Type type: Enum - Submitted at: + submittedAt: + long_name: Submitted at type: Date # -> AttributeDefinition - Release: + release: + long_name: Release type: Enum multi_values: true software_requirement: long_name: Software Requirement attributes: - Capella ID: + capellaID: + long_name: Capella ID type: String - Type: + type: + long_name: Type type: Enum - Submitted at: + submittedAt: + long_name: Submitted at type: Date stakeholder_requirement: long_name: Stakeholder Requirement attributes: - Capella ID: + capellaID: + long_name: Capella ID type: String items: # WorkItems @@ -57,12 +73,12 @@ modules: text: ... type: system_requirement attributes: # Fields - Capella ID: R-FNC-00001 # name, value pair - Type: [Functional] # values in a list for enum fields - Release: - - Feature Rel. 1 - - Feature Rel. 2 - Submitted at: 2022-06-30 17:07:18.664000+02:00 # datetime.datetime for dates + capellaID: R-FNC-00001 # name, value pair + type: [functional] # values in a list for enum fields + release: + - featureRel.1 + - featureRel.2 + submittedAt: 2022-06-30 17:07:18.664000+02:00 # datetime.datetime for dates - id: REQ-003 long_name: Kinds type: software_requirement @@ -71,4 +87,4 @@ modules: long_name: Kind Requirement type: stakeholder_requirement attributes: - Capella ID: R-FNC-00002 + capellaID: R-FNC-00002 diff --git a/tests/data/snapshots/snapshot1.yaml b/tests/data/snapshots/snapshot1.yaml index b981e46..3543467 100644 --- a/tests/data/snapshots/snapshot1.yaml +++ b/tests/data/snapshots/snapshot1.yaml @@ -10,40 +10,57 @@ modules: - id: project/space/example title long_name: example title data_types: - Type: - - Unset - - Functional - - Non-Functional - Release: - - Rel. 1 + type: + long_name: Type + values: + - id: unset + long_name: Unset + - id: functional + long_name: Functional + - id: nonFunctional + long_name: Non-Functional + release: + long_name: Release + values: + - id: rel.1 + long_name: Rel. 1 requirement_types: system_requirement: long_name: System Requirement attributes: - Capella ID: + capellaID: + long_name: Capella ID type: String - Type: + type: + long_name: Type type: Enum - Submitted at: + submittedAt: + long_name: Submitted at type: Date - Release: + release: + long_name: Release type: Enum multi_values: true - Test after migration: + testAfterMigration: + long_name: Test after migration type: String software_requirement: long_name: Software Requirement attributes: - Capella ID: + capellaID: + long_name: Capella ID type: String - Type: + type: + long_name: Type type: Enum - Submitted at: + submittedAt: + long_name: Submitted at type: Date stakeholder_requirement: long_name: Stakeholders Requirement attributes: - Capella ID: + capellaID: + long_name: Capella ID type: String items: - id: REQ-001 @@ -51,20 +68,20 @@ modules: text:

Changed Test Description

type: system_requirement attributes: - Capella ID: RF-NFNC-00001 - Type: [Non-Functional] - Release: - - Rel. 1 + capellaID: RF-NFNC-00001 + type: [nonFunctional] + release: + - rel.1 children: - id: REQ-002 long_name: Non-Function Requirement text: ... type: system_requirement attributes: - Capella ID: R-NFNC-00002 - Type: [Non-Functional] - Submitted at: 2022-06-30 17:07:18.664000+02:00 - Test after migration: New + capellaID: R-NFNC-00002 + type: [nonFunctional] + submittedAt: 2022-06-30 17:07:18.664000+02:00 + testAfterMigration: New - id: REQ-003 long_name: Functional Requirements text:

Brand new

@@ -74,5 +91,5 @@ modules: long_name: Function Requirement type: software_requirement attributes: - Capella ID: R-FNC-00001 - Type: [Unset] + capellaID: R-FNC-00001 + type: [unset] diff --git a/tests/data/snapshots/snapshot2.yaml b/tests/data/snapshots/snapshot2.yaml index 96279f9..6b1f2ff 100644 --- a/tests/data/snapshots/snapshot2.yaml +++ b/tests/data/snapshots/snapshot2.yaml @@ -10,27 +10,37 @@ modules: - id: project/space/example title long_name: example title data_types: - Type: - - Unset - - Non-Functional + type: + long_name: Type + values: + - id: unset + long_name: Unset + - id: nonFunctional + long_name: Non-Functional requirement_types: system_requirement: long_name: System Requirement attributes: - Capella ID: + capellaID: + long_name: Capella ID type: String - Type: + type: + long_name: Type type: Enum - Submitted at: + submittedAt: + long_name: Submitted at type: Date software_requirement: long_name: Software Requirement attributes: - Capella ID: + capellaID: + long_name: Capella ID type: String - Type: + type: + long_name: Type type: Enum - Submitted at: + submittedAt: + long_name: Submitted at type: Date items: - id: REQ-001 @@ -38,6 +48,6 @@ modules: text:

Changed Test Description

type: system_requirement attributes: - Capella ID: RF-NFNC-00001 - Type: [Non-Functional] + capellaID: RF-NFNC-00001 + type: [nonFunctional] children: [] diff --git a/tests/test_changeset.py b/tests/test_changeset.py index 65424b8..c634ce1 100644 --- a/tests/test_changeset.py +++ b/tests/test_changeset.py @@ -49,14 +49,37 @@ TEST_MODULE_CHANGE_2 = decl.load(TEST_DATA_PATH / "changesets" / "delete.yaml") INVALID_FIELD_VALUES = [ - ("Type", ["Not an option"], "values"), - ("Capella ID", None, "value"), - ("Type", None, "values"), - ("Submitted at", 1, "value"), + ( + "type", + ["Not an option"], + "values", + "Invalid field found: values ['Not an option'] for 'type'", + ), + ( + "capellaID", + None, + "value", + "Invalid field found: 'capellaID'. Not matching expected types: " + "None should be of type 'str'", + ), + ( + "type", + None, + "values", + "Invalid field found: 'type'. Not matching expected types: " + "None should be of type 'list'", + ), + ( + "submittedAt", + 1, + "value", + "Invalid field found: 'submittedAt'. Not matching expected types: " + "1 should be of type 'datetime'", + ), ] INVALID_ATTR_DEF_ERROR_MSG = ( "In RequirementType 'System Requirement': " - "Invalid AttributeDefinitionEnumeration found: 'Not-Defined'. " + "Invalid AttributeDefinitionEnumeration found: 'notDefined'. " "Missing its datatype definition in `data_types`." ) @@ -193,23 +216,24 @@ def test_create_requirements_actions( assert action == self.REQ_CHANGE - @pytest.mark.parametrize("attr,faulty_value,key", INVALID_FIELD_VALUES) + @pytest.mark.parametrize( + "attr,faulty_value,key,message_end", INVALID_FIELD_VALUES + ) def test_faulty_attribute_values_log_InvalidFieldValue_as_error( self, clean_model: capellambse.MelodyModel, attr: str, faulty_value: actiontypes.Primitive, key: str, + message_end: str, caplog: pytest.LogCaptureFixture, ) -> None: """Test logging ``InvalidFieldValue`` on faulty field data.""" + del key tracker = copy.deepcopy(self.tracker) titem = tracker["items"][0] first_child = titem["children"][0] first_child["attributes"][attr] = faulty_value # type:ignore[index] - message_end = ( - f"Invalid field found: {key} {faulty_value!r} for {attr!r}" - ) with caplog.at_level(logging.ERROR): self.tracker_change(clean_model, tracker, gather_logs=False) @@ -224,14 +248,11 @@ def test_InvalidFieldValue_errors_are_gathered( titem = tracker["items"][0] first_child = titem["children"][0] messages = list[str]() - for attr, faulty_value, key in INVALID_FIELD_VALUES[1:]: + for attr, faulty_value, key, msg in INVALID_FIELD_VALUES[1:]: first_child["attributes"][ attr ] = faulty_value # type:ignore[index] - messages.append( - "Invalid workitem 'REQ-002'. " - f"Invalid field found: {key} {faulty_value!r} for {attr!r}" - ) + messages.append(f"Invalid workitem 'REQ-002'. {msg}") tchange = self.tracker_change(clean_model, tracker, gather_logs=True) @@ -244,9 +265,9 @@ def test_faulty_data_types_log_InvalidAttributeDefinition_as_error( ) -> None: tracker = copy.deepcopy(self.tracker) reqtype = tracker["requirement_types"]["system_requirement"] - reqtype["attributes"]["Not-Defined"] = { # type: ignore[call-overload] + reqtype["attributes"]["notDefined"] = { # type: ignore[call-overload] + "long_name": "Not-Defined", "type": "Enum", - "data_type": "Not-Defined", } with caplog.at_level(logging.ERROR): @@ -260,9 +281,9 @@ def test_InvalidAttributeDefinition_errors_are_gathered( """Test faulty field data are gathered in errors.""" tracker = copy.deepcopy(self.tracker) reqtype = tracker["requirement_types"]["system_requirement"] - reqtype["attributes"]["Not-Defined"] = { # type: ignore[call-overload] + reqtype["attributes"]["notDefined"] = { # type: ignore[call-overload] + "long_name": "Not-Defined", "type": "Enum", - "data_type": "Not-Defined", } tchange = self.tracker_change(clean_model, tracker, gather_logs=True) @@ -291,12 +312,12 @@ def test_enum_value_long_name_collision_produces_no_Unfulffilled_Promises( tracker = copy.deepcopy(self.tracker) tracker["data_types"]["new"] = tracker[ # type: ignore[index] "data_types" - ]["Type"] + ]["type"] tracker["requirement_types"]["system_requirement"][ # type: ignore "attributes" - ]["new"] = {"type": "Enum"} + ]["new"] = {"long_name": "New", "type": "Enum"} req_item = tracker["items"][0]["children"][0] - req_item["attributes"]["new"] = ["Functional"] # type: ignore[index] + req_item["attributes"]["new"] = ["functional"] # type: ignore[index] change_set = self.tracker_change( clean_model, tracker, gather_logs=True @@ -364,23 +385,24 @@ def test_mod_requirements_actions( assert tchange.actions[4:] == self.REQ_CHANGE + [self.REQ_FOLDER_MOVE] - @pytest.mark.parametrize("attr,faulty_value,key", INVALID_FIELD_VALUES) + @pytest.mark.parametrize( + "attr,faulty_value,key,message_end", INVALID_FIELD_VALUES + ) def test_faulty_attribute_values_log_InvalidFieldValue_as_error( self, migration_model: capellambse.MelodyModel, attr: str, faulty_value: actiontypes.Primitive, key: str, + message_end: str, caplog: pytest.LogCaptureFixture, ) -> None: """Test logging ``InvalidFieldValue`` on faulty field data.""" + del key tracker = copy.deepcopy(self.tracker) titem = tracker["items"][0] first_child = titem["children"][0] first_child["attributes"][attr] = faulty_value - message_end = ( - f"Invalid field found: {key} {faulty_value!r} for {attr!r}" - ) with caplog.at_level(logging.ERROR): self.tracker_change(migration_model, tracker, gather_logs=False) @@ -394,9 +416,9 @@ def test_faulty_data_types_log_InvalidAttributeDefinition_as_error( ) -> None: tracker = copy.deepcopy(self.tracker) reqtype = tracker["requirement_types"]["system_requirement"] - reqtype["attributes"]["Not-Defined"] = { # type: ignore[call-overload] + reqtype["attributes"]["notDefined"] = { # type: ignore[call-overload] + "long_name": "Not-Defined", "type": "Enum", - "data_type": "Not-Defined", } with caplog.at_level(logging.ERROR): @@ -429,9 +451,9 @@ def test_InvalidAttributeDefinition_errors_are_gathered( """Test faulty field data are gathered in errors.""" tracker = copy.deepcopy(self.tracker) reqtype = tracker["requirement_types"]["system_requirement"] - reqtype["attributes"]["Not-Defined"] = { # type: ignore[call-overload] + reqtype["attributes"]["notDefined"] = { # type: ignore[call-overload] + "long_name": "Not-Defined", "type": "Enum", - "data_type": "Not-Defined", } tchange = self.tracker_change( @@ -471,12 +493,12 @@ def test_enum_value_long_name_collision_produces_no_Unfulffilled_Promises( self, migration_model: capellambse.MelodyModel ) -> None: tracker = copy.deepcopy(self.tracker) - tracker["data_types"]["new"] = tracker["data_types"]["Type"] + tracker["data_types"]["new"] = tracker["data_types"]["type"] tracker["requirement_types"]["system_requirement"]["attributes"][ "new" - ] = {"type": "Enum"} + ] = {"long_name": "New", "type": "Enum"} req_item = tracker["items"][0]["children"][0] - req_item["attributes"]["new"] = ["Functional"] + req_item["attributes"]["new"] = ["functional"] change_set = self.tracker_change( migration_model, tracker, gather_logs=True @@ -706,14 +728,14 @@ def test_forced_calculation_produces_change_set_on_AttributeDefinition_error( ) -> None: """Test that an invalid AttributeDefinition will not prohibit.""" snapshot = copy.deepcopy(TEST_SNAPSHOT["modules"][0]) - missing_enumdt = "Release" + missing_enumdt = "release" del snapshot["data_types"][missing_enumdt] # type: ignore[attr-defined] tconfig = TEST_CONFIG["modules"][0] message = ( "In RequirementType 'System Requirement': Invalid " - "AttributeDefinitionEnumeration found: 'Release'. Missing its " + "AttributeDefinitionEnumeration found: 'release'. Missing its " "datatype definition in `data_types`.\n" - "Invalid workitem 'REQ-002'. Invalid field found: 'Release'. " + "Invalid workitem 'REQ-002'. Invalid field found: 'release'. " "Missing its datatype definition in `data_types`." ) @@ -748,17 +770,14 @@ def test_snapshot_errors_from_ChangeSet_calculation_are_gathered( titem["attributes"] = {"Test": 1} # type: ignore[index] messages = [ "Invalid workitem 'REQ-001'. " - "Invalid field found: field name 'Test' not defined in " + "Invalid field found: field identifier 'Test' not defined in " "attributes of requirement type 'system_requirement'" ] - for attr, faulty_value, key in INVALID_FIELD_VALUES[1:]: + for attr, faulty_value, _, msg in INVALID_FIELD_VALUES[1:]: first_child["attributes"][ attr ] = faulty_value # type:ignore[index] - messages.append( - "Invalid workitem 'REQ-002'. " - + f"Invalid field found: {key} {faulty_value!r} for {attr!r}" - ) + messages.append(f"Invalid workitem 'REQ-002'. {msg}") del titem["children"][1]["type"] titem["children"][1]["attributes"] = {"Test": 1} tconfig = TEST_CONFIG["modules"][0] From 72de6dab6824eb6d597ba9377a09b99f54a6010f Mon Sep 17 00:00:00 2001 From: ewuerger Date: Tue, 21 Feb 2023 14:46:51 +0100 Subject: [PATCH 2/7] docs: Update documentation --- capella_rm_bridge/changeset/__init__.py | 3 +- capella_rm_bridge/changeset/change.py | 2 +- docs/source/snapshot.rst | 91 ++++++++++++++----------- 3 files changed, 53 insertions(+), 43 deletions(-) diff --git a/capella_rm_bridge/changeset/__init__.py b/capella_rm_bridge/changeset/__init__.py index 93b0d9e..9be8373 100644 --- a/capella_rm_bridge/changeset/__init__.py +++ b/capella_rm_bridge/changeset/__init__.py @@ -74,8 +74,7 @@ def calculate_change_set( dismissed. gather_logs If ``True`` all error messages are gathered in - :attribute:`~capella_rm_bridge.changeset.change.TrackerChange.errors` - instead of being immediately logged. + `TrackerChange.errors` instead of being immediately logged. Returns ------- diff --git a/capella_rm_bridge/changeset/change.py b/capella_rm_bridge/changeset/change.py index 9014b49..b1081f3 100644 --- a/capella_rm_bridge/changeset/change.py +++ b/capella_rm_bridge/changeset/change.py @@ -641,7 +641,7 @@ def check_attribute_value_is_valid( Raises ------ - capella_rm_bridge.actiontypes.InvalidFieldValue + capella_rm_bridge.changeset.actiontypes.InvalidFieldValue If the types are not matching, if the used data_type definition is missing in the snapshot or the used options on an `EnumerationAttributeValue` are missing in the diff --git a/docs/source/snapshot.rst b/docs/source/snapshot.rst index e3413be..e540e63 100644 --- a/docs/source/snapshot.rst +++ b/docs/source/snapshot.rst @@ -67,13 +67,22 @@ mapping from ``long_name`` to its values. .. code-block:: yaml data_types: # Enumeration Data Type Definitions - Type: - - Unset - - Folder - - Functional - Release: - - Feature Rel. 1 - - Feature Rel. 2 + type: + long_name: Type + values: + - id: unset + long_name: Unset + - id: folder + long_name: Folder + - id: functional + long_name: Functional + release: + long_name: Release + values: + - id: featureRel.1 + long_name: Feature Rel. 1 + - id: feature_rel.1 + long_name: Feature Rel. 2 In order to have a nice display of ``ValueAttribute``\ s for ``Requirement``\ s in Capella and also functioning ``.values`` for @@ -81,20 +90,11 @@ in Capella and also functioning ``.values`` for s, :external:class:`~capellambse.extensions.reqif.AttributeDefinition` and :external:class:`~capellambse.extensions.reqif.AttributeDefinitionEnumeration`\ -s are needed. The *data_types* subsection is a ``long_name`` to value (values) +s are needed. The *data_types* subsection maps ``identifier``\ s to a +:class:`~capella_rm_bridge.changeset.actiontypes.DataType`. These share the same mapping that are matched against the attribute-definitions (``attributes``) subsection in :ref:`requirement_types`. -.. warning:: - - The current format does not allow for equally named - ``EnumerationDataTypeDefinition``\ s such that - ``EnumerationAttributeValue``\ s on separate ``RequirementType``\ s have - different options available. For now there is only one shared DataType - exploiting the availability in the ``CapellaModule``. This makes it - possible to choose values which shouldn't be available on the respective - ValueAttribute. - .. _requirement_types: Requirement Types (``requirement_types``) @@ -106,39 +106,44 @@ Requirement Types (``requirement_types``) system_requirement: long_name: System Requirement attributes: # Field Definitions, we don't need the IDs - Capella ID: # Field name + capellaID: # Field identifier + long_name: Capella ID # Field name type: String # -> AttributeDefinition - Type: - type: Enum - Submitted at: + type: # type should also be declared under data_types! + long_name: Type + type: Enum # -> EnumerationAttributeDefinition + submitted_at: + long_name: Submitted At type: Date # -> AttributeDefinition - Release: + release: + long_name: Release type: Enum multi_values: true software_requirement: long_name: Software Requirement attributes: - Capella ID: + capellaID: + long_name: Capella ID type: String - Type: + type: + long_name: Type type: Enum - Submitted at: + submitted_at: + long_name: Submitted At type: Date stakeholder_requirement: long_name: Stakeholder Requirement attributes: - Capella ID: + capellaID: + long_name: Capella ID type: String Work item types are dealt by most RM tools as special fields. This section is therefore a mapping that describes ``RequirementType``\ s from a given -``identifier`` to its ``long_name`` and ``attribute_definitions`` (in short -``attributes``). Therein the keys are matched against the ``long_name`` of the -``EnumerationDataTypeDefinition`` defined in ``data_types`` if it is an -``AttributeDefinitionEnumeration``. Else an ``AttributeDefinition`` is meant -and for these a type-hint via ``type`` is needed. +``identifier`` to its +:class:`~capella_rm_bridge.changeset.actiontypes.RequirementType`. ``Requirement``\ s and ``RequirementFolder``\ s (``items``) =========================================================== @@ -152,13 +157,17 @@ and for these a type-hint via ``type`` is needed. type: system_requirement # WorkItemType ID attributes: # Fields for a Folder - Capella ID: R-FNC-00001 # String Attribute - Type: [Unset] # Enum Attribute - Submitted at: 2022-06-30 17:07:18.664000+02:00 + capellaID: R-FNC-00001 # String Attribute + type: [unset] # Enum Attribute value identifier + submitted_at: 2022-06-30 17:07:18.664000+02:00 - children: # Folder b/c non-empty children + children: # Folder b/c children key exists - id: REQ-002 long_name: Function Requirement + attributes: # Fields for a WorkItem + capellaID: R-FNC-00002 # String Attribute + type: [functional] # Enum Attribute value identifier + submitted_at: 2022-06-30 17:07:18.664000+02:00 # [...] - id: REQ-003 # [...] @@ -186,15 +195,17 @@ are supported: .. note:: During execution of - :py:meth:`~capella_rm_bridge.change_set.change.TrackerChange.calculate_change` the - integrity of the snapshot is checked. That means for example work items that - have ``type`` identifiers which are not defined in the + :py:meth:`~capella_rm_bridge.change_set.change.TrackerChange.calculate_change` + the integrity of the snapshot is checked. That means for example work items + that have ``type`` identifiers which are not defined in the :ref:`requirement_types` section will be skipped. In general there needs to be a ``type`` identifier exported in order to have fields maintained. Another example: If there are any options/values exported on an enum-field which are not defined in the respective enum definition under ``data_types``, - the field will be skipped. + the field will be skipped if the force mode is enabled. If force mode is + disabled (default) any error will result into cancelation of the ChangeSet + calculation. With the ``children`` key the hierarchical structure of the workitems is exported and empty children will result in a ``Requirement``. Conversely From da346b61ffa24cf13cb774885e73f08b9ab85590 Mon Sep 17 00:00:00 2001 From: ewuerger Date: Tue, 21 Feb 2023 15:56:32 +0100 Subject: [PATCH 3/7] refactor: Get rid of redundant `ReqFinder` and methods --- capella_rm_bridge/changeset/change.py | 77 ++++++++++++------- capella_rm_bridge/changeset/find.py | 106 +++++--------------------- 2 files changed, 71 insertions(+), 112 deletions(-) diff --git a/capella_rm_bridge/changeset/change.py b/capella_rm_bridge/changeset/change.py index b1081f3..eee10cd 100644 --- a/capella_rm_bridge/changeset/change.py +++ b/capella_rm_bridge/changeset/change.py @@ -66,8 +66,6 @@ class TrackerChange: """Config section for the tracker.""" gather_logs: bool """Collect error messages in ``errors`` instead of immediate logging.""" - reqfinder: find.ReqFinder - """Find ReqIF elements in the model.""" req_module: reqif.RequirementsModule """The corresponding ``reqif.RequirementsModule`` for the tracker.""" @@ -129,7 +127,6 @@ def __init__( self.model = model self.config = config self.gather_logs = gather_logs - self.reqfinder = find.ReqFinder(model) self.actions = [] self._location_changed = set[RMIdentifier]() @@ -152,8 +149,11 @@ def calculate_change(self) -> None: deletions. """ base = self.check_requirements_module() - self.reqt_folder = self.reqfinder.reqtypesfolder_by_identifier( - CACHEKEY_TYPES_FOLDER_IDENTIFIER, below=self.req_module + self.reqt_folder = find.find_by_identifier( + self.model, + CACHEKEY_TYPES_FOLDER_IDENTIFIER, + reqif.CapellaTypesFolder.__name__, + below=self.req_module, ) if self.reqt_folder is None: base = self.requirement_types_folder_create_action(base) @@ -167,10 +167,12 @@ def calculate_change(self) -> None: for item in self.tracker["items"]: if "children" in item: second_key = "folders" - req = self.reqfinder.folder_by_identifier(item["id"]) + req = find.find_by_identifier(self.model, item["id"], "Folder") else: second_key = "requirements" - req = self.reqfinder.requirement_by_identifier(item["id"]) + req = find.find_by_identifier( + self.model, item["id"], "Requirement" + ) if req is None: req_actions = self.yield_requirements_create_actions(item) @@ -213,7 +215,9 @@ def check_requirements_module(self) -> dict[str, t.Any]: """ try: module_uuid = self.config["capella-uuid"] - self.req_module = self.reqfinder.reqmodule(module_uuid) + self.req_module = find.find_by( + self.model, module_uuid, "CapellaModule", attr="uuid" + ) except KeyError as error: raise act.InvalidTrackerConfig( "The given module configuration is missing UUID of the " @@ -418,8 +422,11 @@ def attribute_definition_create_action( cls = reqif.AttributeDefinition if item["type"] == "Enum": cls = reqif.AttributeDefinitionEnumeration - etdef = self.reqfinder.enum_data_type_definition_by_identifier( - id, below=self.reqt_folder + etdef = find.find_by_identifier( + self.model, + id, + "EnumerationDataTypeDefinition", + below=self.reqt_folder, ) if etdef is None: promise_id = f"EnumerationDataTypeDefinition {id}" @@ -482,8 +489,11 @@ def yield_requirements_create_actions( base["attributes"] = attributes if req_type_id: - reqtype = self.reqfinder.reqtype_by_identifier( - req_type_id, below=self.reqt_folder + reqtype = find.find_by_identifier( + self.model, + req_type_id, + "RequirementType", + below=self.reqt_folder, ) if reqtype is None: base["type"] = decl.Promise(f"RequirementType {req_type_id}") @@ -498,11 +508,13 @@ def yield_requirements_create_actions( for child in item["children"]: if "children" in child: key = "folders" - creq = self.reqfinder.folder_by_identifier(child["id"]) + creq = find.find_by_identifier( + self.model, str(child["id"]), "Folder" + ) else: key = "requirements" - creq = self.reqfinder.requirement_by_identifier( - child["id"] + creq = find.find_by_identifier( + self.model, str(child["id"]), "Requirement" ) if creq is None: @@ -596,13 +608,19 @@ def attribute_value_create_action( if builder.deftype == "Enum": deftype += "Enumeration" assert isinstance(builder.value, list) - edtdef = self.reqfinder.enum_data_type_definition_by_identifier( - id, below=self.reqt_folder + edtdef = find.find_by_identifier( + self.model, + id, + "EnumerationDataTypeDefinition", + below=self.reqt_folder, ) for evid in builder.value: - enumvalue = self.reqfinder.enum_value_by_identifier( - evid, below=edtdef or self.reqt_folder + enumvalue = find.find_by_identifier( + self.model, + evid, + "EnumValue", + below=edtdef or self.reqt_folder, ) if enumvalue is None: ev_ref = decl.Promise(f"EnumValue {id} {evid}") @@ -613,8 +631,8 @@ def attribute_value_create_action( values.append(ev_ref) attr_def_id = f"{id} {req_type_id}" - definition = self.reqfinder.attribute_definition_by_identifier( - deftype, attr_def_id, below=self.reqt_folder + definition = find.find_by_identifier( + self.model, attr_def_id, deftype, below=self.reqt_folder ) if definition is None: promise_id = f"{deftype} {attr_def_id}" @@ -903,8 +921,11 @@ def yield_requirements_mod_actions( f"Unknown workitem-type {req_type_id!r}" ) - reqtype = self.reqfinder.reqtype_by_identifier( - req_type_id, below=self.reqt_folder + reqtype = find.find_by_identifier( + self.model, + req_type_id, + "RequirementType", + below=self.reqt_folder, ) if reqtype is None: mods["type"] = decl.Promise(req_type_id) @@ -986,11 +1007,13 @@ def yield_requirements_mod_actions( if "children" in child: key = "folders" child_folder_ids.add(cid) - creq = self.reqfinder.folder_by_identifier(cid) + creq = find.find_by_identifier(self.model, cid, "Folder") else: key = "requirements" child_req_ids.add(cid) - creq = self.reqfinder.requirement_by_identifier(cid) + creq = find.find_by_identifier( + self.model, cid, "Requirement" + ) container = containers[key == "folders"] if creq is None: @@ -1092,8 +1115,8 @@ def attribute_value_mod_action( if builder.deftype == "Enum": deftype += "Enumeration" - attrdef = self.reqfinder.attribute_definition_by_identifier( - deftype, f"{id} {req_type_id}", self.reqt_folder + attrdef = find.find_by_identifier( + self.model, f"{id} {req_type_id}", deftype, below=self.reqt_folder ) attr = req.attributes.by_definition(attrdef, single=True) if isinstance(attr, reqif.EnumerationValueAttribute): diff --git a/capella_rm_bridge/changeset/find.py b/capella_rm_bridge/changeset/find.py index fb63ae5..51f7214 100644 --- a/capella_rm_bridge/changeset/find.py +++ b/capella_rm_bridge/changeset/find.py @@ -9,92 +9,28 @@ import capellambse from capellambse.extensions import reqif -from capellambse.model import crosslayer LOGGER = logging.getLogger(__name__) -class ReqFinder: - """Find ReqIF elements in a ``MelodyModel`` easily and efficiently.""" - - def __init__(self, model: capellambse.MelodyModel) -> None: - self.model = model - - def _get( - self, - value: t.Any, - *xtypes: str, - attr: str = "identifier", - below: reqif.ReqIFElement | None = None, - ) -> reqif.ReqIFElement | None: - try: - objs = self.model.search(*xtypes, below=below) - return getattr(objs, f"by_{attr}")(value, single=True) - except KeyError: - types = " or ".join(xt.split(":")[-1] for xt in xtypes) - LOGGER.info("No %s found with %s: %r", types, attr, value) - return None - - def reqmodule(self, uuid: str) -> reqif.CapellaModule | None: - """Try to return the ``CapellaModule``.""" - return self._get(uuid, reqif.CapellaModule.__name__, attr="uuid") - - def reqtypesfolder_by_identifier( - self, - identifier: int | str, - below: crosslayer.BaseArchitectureLayer - | reqif.CapellaModule - | None = None, - ) -> reqif.CapellaTypesFolder | None: - """Try to return the ``RequirementTypesFolder``.""" - return self._get( - str(identifier), reqif.CapellaTypesFolder.__name__, below=below - ) - - def reqtype_by_identifier( - self, identifier: int | str, below: reqif.ReqIFElement | None = None - ) -> reqif.RequirementType | None: - """Try to return a ``RequirementType``.""" - return self._get( - str(identifier), reqif.RequirementType.__name__, below=below - ) - - def attribute_definition_by_identifier( - self, xtype: str, identifier: str, below: reqif.ReqIFElement | None - ) -> reqif.AttributeDefinition | reqif.AttributeDefinitionEnumeration: - """Try to return an ``AttributeDefinition``-/``Enumeration``.""" - return self._get(identifier, xtype, below=below) - - def folder_by_identifier( - self, identifier: int | str, below: reqif.ReqIFElement | None = None - ) -> reqif.Folder | None: - """Try to return a ``Folder``.""" - return self._get(str(identifier), reqif.Folder.__name__, below=below) - - def requirement_by_identifier( - self, identifier: int | str, below: reqif.ReqIFElement | None = None - ) -> reqif.Requirement | None: - """Try to return a ``Requirement``.""" - return self._get( - str(identifier), reqif.Requirement.__name__, below=below - ) - - def enum_data_type_definition_by_identifier( - self, id: str, below: reqif.ReqIFElement | None - ) -> reqif.EnumerationDataTypeDefinition | None: - """Try to return an ``EnumerationDataTypeDefinition``. - - The object is matched with given ``long_name``. - """ - return self._get( - id, reqif.EnumerationDataTypeDefinition.__name__, below=below - ) - - def enum_value_by_identifier( - self, id: str, below: reqif.ReqIFElement | None = None - ) -> reqif.EnumValue | None: - """Try to return an ``EnumValue``. - - The object is matched with given ``long_name``. - """ - return self._get(id, reqif.EnumValue.__name__, below=below) +def find_by( + model: capellambse.MelodyModel, + value: t.Any, + *xtypes: str, + attr: str = "identifier", + below: reqif.ReqIFElement | None = None, +) -> reqif.ReqIFElement | None: + try: + objs = model.search(*xtypes, below=below) + return getattr(objs, f"by_{attr}")(value, single=True) + except KeyError: + types = " or ".join(xt.split(":")[-1] for xt in xtypes) + LOGGER.info("No %s found with %s: %r", types, attr, value) + return None + + +def find_by_identifier( + model: capellambse.MelodyModel, id: str, *xtypes: str, **kw +) -> reqif.ReqIFElement: + """Try to return a model object by its ``identifier``.""" + return find_by(model, id, *xtypes, **kw) From 6108f527a3ff3dc3e46667e807dff348632a5c1b Mon Sep 17 00:00:00 2001 From: ewuerger Date: Tue, 21 Feb 2023 16:07:32 +0100 Subject: [PATCH 4/7] feat: Update the `JSON` schema --- .../changeset/snapshot_input.schema.json | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/capella_rm_bridge/changeset/snapshot_input.schema.json b/capella_rm_bridge/changeset/snapshot_input.schema.json index 9cb50ee..6bd05b7 100644 --- a/capella_rm_bridge/changeset/snapshot_input.schema.json +++ b/capella_rm_bridge/changeset/snapshot_input.schema.json @@ -59,9 +59,29 @@ "patternProperties": { ".": { "description": "The values that are available for this EnumerationDataTypeDefinition. They are sometimes called \"options\"", - "type": "array", - "items": { - "type": "string" + "type": "object", + "properties": { + "long_name": { + "description": "Value of the \"Long Name\" field in Capella. Here it is the long name of the EnumerationDataTypeDefinition.", + "type": "string" + }, + "values": { + "description": "EnumValues in Capella.", + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "description": "Value of the \"Identifier\" field in Capella. Here it is the identifier of the EnumValue.", + "type": "string" + }, + "long_name": { + "description": "Value of the \"Long Name\" field in Capella. Here it is the long name of the EnumValue.", + "type": "string" + } + } + } + } } } } @@ -93,6 +113,10 @@ "oneOf": [ { "properties": { + "long_name": { + "description": "Value of the \"Long Name\" field in Capella. Here it is the long name of the AttributeDefinition.", + "type": "string" + }, "type": { "type": "string", "enum": [ @@ -108,6 +132,10 @@ }, { "properties": { + "long_name": { + "description": "Value of the \"Long Name\" field in Capella. Here it is the long name of the EnumerationAttributeDefinition.", + "type": "string" + }, "type": { "type": "string", "const": "Enum" From 713b946f4fba51e72f1948f72cd3a4a60324fd31 Mon Sep 17 00:00:00 2001 From: ewuerger Date: Thu, 23 Feb 2023 13:40:27 +0100 Subject: [PATCH 5/7] fix: Rework ReqType creation and modification --- capella_rm_bridge/changeset/change.py | 60 +++++++++++++------------- tests/data/changesets/delete.yaml | 12 +++--- tests/test_changeset.py | 61 +++++++++++++++++++++------ 3 files changed, 82 insertions(+), 51 deletions(-) diff --git a/capella_rm_bridge/changeset/change.py b/capella_rm_bridge/changeset/change.py index eee10cd..fe84931 100644 --- a/capella_rm_bridge/changeset/change.py +++ b/capella_rm_bridge/changeset/change.py @@ -18,9 +18,7 @@ LOGGER = logging.getLogger(__name__) REQ_TYPES_FOLDER_NAME = "Types" -CACHEKEY_MODULE_IDENTIFIER = "-1" -CACHEKEY_TYPES_FOLDER_IDENTIFIER = "-2" -CACHEKEY_REQTYPE_IDENTIFIER = "-3" +TYPES_FOLDER_IDENTIFIER = "-2" REQ_TYPE_NAME = "Requirement" _ATTR_BLACKLIST = frozenset({("Type", "Folder")}) @@ -134,8 +132,6 @@ def __init__( self._faulty_attribute_definitions = set[str]() self.errors = [] - self.__reqtype_action: dict[str, t.Any] | None = None - self.calculate_change() def calculate_change(self) -> None: @@ -151,29 +147,41 @@ def calculate_change(self) -> None: base = self.check_requirements_module() self.reqt_folder = find.find_by_identifier( self.model, - CACHEKEY_TYPES_FOLDER_IDENTIFIER, + TYPES_FOLDER_IDENTIFIER, reqif.CapellaTypesFolder.__name__, below=self.req_module, ) if self.reqt_folder is None: base = self.requirement_types_folder_create_action(base) else: - self.data_type_definition_mod_actions() - self.requirement_type_delete_actions() + reqt_folder_action = self.data_type_definition_mod_actions() + if reqtype_deletions := self.requirement_type_delete_actions(): + dels = {"delete": {"requirement_types": reqtype_deletions}} + _deep_update(reqt_folder_action, dels) + + reqtype_creations = list[dict[str, t.Any]]() for id, reqtype in self.requirement_types.items(): - self.requirement_type_mod_action(RMIdentifier(id), reqtype) + id = RMIdentifier(id) + if new_rtype := self.requirement_type_mod_action(id, reqtype): + reqtype_creations.append(new_rtype) + + if reqtype_creations: + exts = {"extend": {"requirement_types": reqtype_creations}} + _deep_update(reqt_folder_action, exts) + + if set(reqt_folder_action) != {"parent"}: + self.actions.append(reqt_folder_action) visited = set[str]() for item in self.tracker["items"]: if "children" in item: + type = "Folder" second_key = "folders" - req = find.find_by_identifier(self.model, item["id"], "Folder") else: + type = "Requirement" second_key = "requirements" - req = find.find_by_identifier( - self.model, item["id"], "Requirement" - ) + req = find.find_by_identifier(self.model, item["id"], type) if req is None: req_actions = self.yield_requirements_create_actions(item) item_action = next(req_actions) @@ -312,7 +320,7 @@ def requirement_types_folder_create_action( req_types = self.yield_requirement_type_create_actions() reqt_folder = { "long_name": REQ_TYPES_FOLDER_NAME, - "identifier": CACHEKEY_TYPES_FOLDER_IDENTIFIER, + "identifier": TYPES_FOLDER_IDENTIFIER, "data_type_definitions": list(data_type_defs), "requirement_types": list(req_types), } @@ -709,7 +717,7 @@ def check_attribute_value_is_valid( return _AttributeValueBuilder(deftype, key, value) - def data_type_definition_mod_actions(self) -> None: + def data_type_definition_mod_actions(self) -> dict[str, t.Any]: r"""Populate with ModActions for the RequirementTypesFolder. The data type definitions and requirement type are checked via @@ -743,11 +751,8 @@ def data_type_definition_mod_actions(self) -> None: if dt_defs_deletions: base["delete"] = {"data_type_definitions": dt_defs_deletions} - if base.get("extend", {}) or base.get("delete", {}): - self.__reqtype_action = base - self.actions.append(base) - self.actions.extend(dt_defs_modifications) + return base def data_type_mod_action( self, id: str, ddef: act.DataType @@ -807,25 +812,19 @@ def data_type_mod_action( except KeyError: return self.data_type_create_action(id, ddef) - def requirement_type_delete_actions(self) -> None: + def requirement_type_delete_actions(self) -> list[decl.UUIDReference]: r"""Populate actions for deleting ``RequirementType``\ s.""" assert self.reqt_folder - parent_ref = decl.UUIDReference(self.reqt_folder.uuid) dels = [ decl.UUIDReference(reqtype.uuid) for reqtype in self.reqt_folder.requirement_types if RMIdentifier(reqtype.identifier) not in self.requirement_types ] - if dels: - delete = {"requirement_types": dels} - if self.__reqtype_action is None: - self.actions.append({"parent": parent_ref, "delete": delete}) - else: - _deep_update(self.__reqtype_action, {"delete": delete}) + return dels def requirement_type_mod_action( self, identifier: RMIdentifier, item: act.RequirementType - ) -> None: + ) -> None | dict[str, t.Any]: assert self.reqt_folder try: reqtype = self.reqt_folder.requirement_types.by_identifier( @@ -878,10 +877,9 @@ def requirement_type_mod_action( self.actions.append(base) self.actions.extend(attr_defs_modifications) + return None except KeyError: - self.actions.append( - self.requirement_type_create_action(identifier, item) - ) + return self.requirement_type_create_action(identifier, item) def yield_requirements_mod_actions( self, diff --git a/tests/data/changesets/delete.yaml b/tests/data/changesets/delete.yaml index 5eb6fbc..17b4000 100644 --- a/tests/data/changesets/delete.yaml +++ b/tests/data/changesets/delete.yaml @@ -1,12 +1,6 @@ # SPDX-FileCopyrightText: Copyright DB Netz AG and the capella-rm-bridge contributors # SPDX-License-Identifier: Apache-2.0 -- parent: !uuid a15e8b60-bf39-47ba-b7c7-74ceecb25c9c - delete: - data_type_definitions: - - !uuid 98aaca05-d47f-4916-a9f9-770dc60aa04f - requirement_types: - - !uuid cbc96c24-58c9-4292-9dfa-1ecb5ee16a82 - parent: !uuid 686e198b-8baf-49f9-9d85-24571bd05d93 delete: values: @@ -16,6 +10,12 @@ attribute_definitions: - !uuid e46d4629-da10-4f7f-954e-146bd2697638 - long_name Test after migration # dynamically generated object +- parent: !uuid a15e8b60-bf39-47ba-b7c7-74ceecb25c9c + delete: + data_type_definitions: + - !uuid 98aaca05-d47f-4916-a9f9-770dc60aa04f + requirement_types: + - !uuid cbc96c24-58c9-4292-9dfa-1ecb5ee16a82 - parent: !uuid b2c39449-ebbe-43cb-a712-22c740707a8b delete: attributes: diff --git a/tests/test_changeset.py b/tests/test_changeset.py index c634ce1..7c2096b 100644 --- a/tests/test_changeset.py +++ b/tests/test_changeset.py @@ -492,13 +492,46 @@ def test_requirements_with_empty_children_are_rendered_as_folders( def test_enum_value_long_name_collision_produces_no_Unfulffilled_Promises( self, migration_model: capellambse.MelodyModel ) -> None: - tracker = copy.deepcopy(self.tracker) - tracker["data_types"]["new"] = tracker["data_types"]["type"] - tracker["requirement_types"]["system_requirement"]["attributes"][ - "new" - ] = {"long_name": "New", "type": "Enum"} - req_item = tracker["items"][0]["children"][0] - req_item["attributes"]["new"] = ["functional"] + tracker_yaml = """\ + id: project/space/example title + long_name: example title + data_types: + new: + long_name: Type + values: + - id: unset + long_name: Unset + - id: functional + long_name: Functional + - id: nonFunctional + long_name: Non-Functional + release: + long_name: Release + values: + - id: rel.1 + long_name: Rel. 1 + type: + long_name: Type + values: + - id: unset + long_name: Unset + - id: functional + long_name: Functional + - id: nonFunctional + long_name: Non-Functional + requirement_types: + system_requirement: + long_name: System Requirement + attributes: + new: + long_name: New + type: Enum + type: + long_name: Type + type: Enum + items: []""" + + tracker = yaml.safe_load(tracker_yaml) change_set = self.tracker_change( migration_model, tracker, gather_logs=True @@ -528,9 +561,9 @@ class TestDeleteActions(ActionsTest): tracker = TEST_SNAPSHOT_2["modules"][0] titem = tracker["items"][0] - REQ_TYPE_FOLDER_DEL = TEST_MODULE_CHANGE_2[0] - ENUM_DATA_TYPE_DEL = TEST_MODULE_CHANGE_2[1] - ATTR_DEF_DEL = TEST_MODULE_CHANGE_2[2] + ENUM_DATA_TYPE_DEL = TEST_MODULE_CHANGE_2[0] + ATTR_DEF_DEL = TEST_MODULE_CHANGE_2[1] + REQ_TYPE_FOLDER_DEL = TEST_MODULE_CHANGE_2[2] REQ_DEL, FOLDER_DEL = TEST_MODULE_CHANGE_2[-2:] def resolve_ChangeSet( @@ -572,7 +605,7 @@ def test_delete_data_type_definition_actions( data_type_del = copy.deepcopy(self.REQ_TYPE_FOLDER_DEL) del data_type_del["delete"]["requirement_types"] data_type_del = self.resolve_ChangeSet(deletion_model, data_type_del) - expected_actions = [data_type_del, self.ENUM_DATA_TYPE_DEL] + expected_actions = [self.ENUM_DATA_TYPE_DEL, data_type_del] tchange = self.tracker_change(deletion_model, snapshot) enum_data_type_actions = tchange.actions[:2] @@ -593,7 +626,7 @@ def test_delete_requirement_type_actions( tchange = self.tracker_change(deletion_model, snapshot) - assert tchange.actions[:2] == [req_type_del, attr_def_del] + assert tchange.actions[:2] == [attr_def_del, req_type_del] def test_delete_requirements_actions( self, deletion_model: capellambse.MelodyModel @@ -616,8 +649,8 @@ def test_calculate_change_sets( ) -> None: """Test ChangeSet on clean model for first migration run.""" expected_change_set = copy.deepcopy(TEST_MODULE_CHANGE_2) - expected_change_set[2] = self.resolve_ChangeSet( - deletion_model, TEST_MODULE_CHANGE_2[2] + expected_change_set[1] = self.resolve_ChangeSet( + deletion_model, TEST_MODULE_CHANGE_2[1] ) expected_change_set[3] = self.resolve_ChangeSet( deletion_model, TEST_MODULE_CHANGE_2[3] From 7fe3963bdbc9e252374665c4e8f74cc589fca6b2 Mon Sep 17 00:00:00 2001 From: ewuerger Date: Fri, 3 Mar 2023 11:00:41 +0100 Subject: [PATCH 6/7] fix: Minor CLI bug --- capella_rm_bridge/__main__.py | 6 +++--- capella_rm_bridge/changeset/actiontypes.py | 2 +- capella_rm_bridge/changeset/change.py | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/capella_rm_bridge/__main__.py b/capella_rm_bridge/__main__.py index 6b6175a..6482968 100644 --- a/capella_rm_bridge/__main__.py +++ b/capella_rm_bridge/__main__.py @@ -79,7 +79,7 @@ def write_change_set(change: str, module: dict[str, t.Any]) -> pathlib.Path: @click.option( "--pull/--no-pull", is_flag=True, - default=True, + default=None, help="Pull the latest changes from remote.", ) @click.option( @@ -118,7 +118,7 @@ def main( snapshotfile: t.TextIO, dry_run: bool, push: bool, - pull: bool, + pull: bool | None, force: bool, gather_logs: bool, save_change_history: bool, @@ -147,7 +147,7 @@ def main( config = yaml.safe_load(conffile) params = config["model"] - if "git" in config["model"]["path"]: + if pull is not None: params["update_cache"] = pull model = capellambse.MelodyModel(**params) diff --git a/capella_rm_bridge/changeset/actiontypes.py b/capella_rm_bridge/changeset/actiontypes.py index 8f082b7..d49a035 100644 --- a/capella_rm_bridge/changeset/actiontypes.py +++ b/capella_rm_bridge/changeset/actiontypes.py @@ -45,7 +45,7 @@ class InvalidTrackerConfig(Exception): class WorkItem(te.TypedDict, total=False): """A workitem from the snapshot.""" - id: te.Required[int] + id: te.Required[str] long_name: str text: str type: str diff --git a/capella_rm_bridge/changeset/change.py b/capella_rm_bridge/changeset/change.py index fe84931..fd43e41 100644 --- a/capella_rm_bridge/changeset/change.py +++ b/capella_rm_bridge/changeset/change.py @@ -457,7 +457,7 @@ def attribute_definition_create_action( return base def yield_requirements_create_actions( - self, item: dict[str, t.Any] | act.WorkItem + self, item: act.WorkItem ) -> cabc.Iterator[dict[str, t.Any]]: """Yield actions for creating Requirements or Folders. @@ -517,12 +517,12 @@ def yield_requirements_create_actions( if "children" in child: key = "folders" creq = find.find_by_identifier( - self.model, str(child["id"]), "Folder" + self.model, child["id"], "Folder" ) else: key = "requirements" creq = find.find_by_identifier( - self.model, str(child["id"]), "Requirement" + self.model, child["id"], "Requirement" ) if creq is None: From 3cad88bd0265fe4c05f380d05298d308ac88ebb8 Mon Sep 17 00:00:00 2001 From: ewuerger Date: Fri, 3 Mar 2023 15:10:07 +0100 Subject: [PATCH 7/7] refactor: Apply changes from code review --- capella_rm_bridge/changeset/change.py | 1 + capella_rm_bridge/changeset/find.py | 2 +- docs/source/snapshot.rst | 5 +++-- tests/test_changeset.py | 9 ++++----- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/capella_rm_bridge/changeset/change.py b/capella_rm_bridge/changeset/change.py index fd43e41..0217ee7 100644 --- a/capella_rm_bridge/changeset/change.py +++ b/capella_rm_bridge/changeset/change.py @@ -1117,6 +1117,7 @@ def attribute_value_mod_action( self.model, f"{id} {req_type_id}", deftype, below=self.reqt_folder ) attr = req.attributes.by_definition(attrdef, single=True) + assert attrdef is not None if isinstance(attr, reqif.EnumerationValueAttribute): assert isinstance(valueid, list) actual = set(attr.values.by_identifier) diff --git a/capella_rm_bridge/changeset/find.py b/capella_rm_bridge/changeset/find.py index 51f7214..71cd727 100644 --- a/capella_rm_bridge/changeset/find.py +++ b/capella_rm_bridge/changeset/find.py @@ -31,6 +31,6 @@ def find_by( def find_by_identifier( model: capellambse.MelodyModel, id: str, *xtypes: str, **kw -) -> reqif.ReqIFElement: +) -> reqif.ReqIFElement | None: """Try to return a model object by its ``identifier``.""" return find_by(model, id, *xtypes, **kw) diff --git a/docs/source/snapshot.rst b/docs/source/snapshot.rst index e540e63..c190ad9 100644 --- a/docs/source/snapshot.rst +++ b/docs/source/snapshot.rst @@ -208,8 +208,9 @@ are supported: calculation. With the ``children`` key the hierarchical structure of the workitems is -exported and empty children will result in a ``Requirement``. Conversely -non-empty children will cause change action on a ``Folder``. +exported. The existance of a ``children`` key will result in a ``Folder``. +Conversely if there is no ``children`` key will cause change action on a +``Requirement``. Complete snapshot ================= diff --git a/tests/test_changeset.py b/tests/test_changeset.py index 7c2096b..1c5ab97 100644 --- a/tests/test_changeset.py +++ b/tests/test_changeset.py @@ -435,15 +435,14 @@ def test_faulty_simple_attributes_log_AttributeError( tracker = copy.deepcopy(self.tracker) titem = tracker["items"][0] titem["imagination"] = 1 - message_end = ( - "Invalid module 'project/space/example title'. Invalid " - "workitem 'REQ-001'. imagination isn't defined on Folder" - ) with caplog.at_level(logging.ERROR): self.tracker_change(migration_model, tracker, gather_logs=False) - assert caplog.messages[0].endswith(message_end) + assert "project/space/example title" in caplog.messages[0] + assert "REQ-001" in caplog.messages[0] + assert "imagination" in caplog.messages[0] + assert "Folder" in caplog.messages[0] def test_InvalidAttributeDefinition_errors_are_gathered( self, migration_model: capellambse.MelodyModel