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/__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/actiontypes.py b/capella_rm_bridge/changeset/actiontypes.py
index 7c1cb7a..d49a035 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,7 +43,9 @@ class InvalidTrackerConfig(Exception):
class WorkItem(te.TypedDict, total=False):
- id: te.Required[int]
+ """A workitem from the snapshot."""
+
+ id: te.Required[str]
long_name: str
text: str
type: 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..0217ee7 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")})
@@ -42,7 +40,7 @@
class _AttributeValueBuilder(t.NamedTuple):
deftype: str
key: str
- value: act.Primitive | None
+ value: act.Primitive | RMIdentifier | None
class MissingCapellaModule(Exception):
@@ -66,8 +64,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."""
@@ -77,7 +73,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."""
@@ -129,7 +125,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]()
@@ -137,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:
@@ -152,26 +145,43 @@ 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,
+ 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 = self.reqfinder.folder_by_identifier(item["id"])
else:
+ type = "Requirement"
second_key = "requirements"
- req = self.reqfinder.requirement_by_identifier(item["id"])
+ 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)
@@ -213,7 +223,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 "
@@ -308,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),
}
@@ -319,12 +331,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 +349,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 +386,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 +407,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 +422,26 @@ 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 = find.find_by_identifier(
+ self.model,
+ id,
+ "EnumerationDataTypeDefinition",
+ 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`."
)
@@ -434,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.
@@ -451,15 +474,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"]))
@@ -474,8 +497,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}")
@@ -490,11 +516,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, child["id"], "Folder"
+ )
else:
key = "requirements"
- creq = self.reqfinder.requirement_by_identifier(
- child["id"]
+ creq = find.find_by_identifier(
+ self.model, child["id"], "Requirement"
)
if creq is None:
@@ -523,7 +551,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 +560,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 +579,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 +590,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,37 +610,43 @@ 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 = find.find_by_identifier(
+ self.model,
+ id,
+ "EnumerationDataTypeDefinition",
+ 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 = find.find_by_identifier(
+ self.model,
+ evid,
+ "EnumValue",
+ 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}"
- definition = self.reqfinder.attribute_definition_by_identifier(
- deftype, attr_def_id, below=self.reqt_folder
+ attr_def_id = f"{id} {req_type_id}"
+ 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}"
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,40 +661,63 @@ 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.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
+ 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:
+ 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
@@ -673,13 +730,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
@@ -694,22 +751,19 @@ 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, 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,52 +775,56 @@ 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:
+ 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(
@@ -783,17 +841,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
@@ -816,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,
@@ -859,8 +919,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)
@@ -875,8 +938,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 +948,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 +961,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:
@@ -939,11 +1005,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:
@@ -1006,15 +1074,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 +1091,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 +1108,52 @@ 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
+ 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)
+ assert attrdef is not None
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 +1164,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 +1188,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..71cd727 100644
--- a/capella_rm_bridge/changeset/find.py
+++ b/capella_rm_bridge/changeset/find.py
@@ -9,97 +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_long_name(
- self, long_name: 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,
- )
-
- def enum_value_by_long_name(
- self, long_name: 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
- )
+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 | None:
+ """Try to return a model object by its ``identifier``."""
+ return find_by(model, id, *xtypes, **kw)
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"
diff --git a/docs/source/snapshot.rst b/docs/source/snapshot.rst
index e3413be..c190ad9 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,19 +195,22 @@ 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
-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/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..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:
@@ -15,11 +9,17 @@
delete:
attribute_definitions:
- !uuid e46d4629-da10-4f7f-954e-146bd2697638
- - long_name Test after migration
+ - 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:
- - 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
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..1c5ab97 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): @@ -413,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 @@ -429,9 +450,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( @@ -470,13 +491,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" - ] = {"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 @@ -506,9 +560,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( @@ -550,7 +604,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] @@ -571,7 +625,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 @@ -594,8 +648,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] @@ -706,14 +760,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 +802,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]