diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 9a3dad6..3827499 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -5,7 +5,7 @@ name: Docs
on:
push:
- branches: ["add-docs"]
+ branches: ["master"]
jobs:
sphinx:
diff --git a/capella_rm_bridge/__main__.py b/capella_rm_bridge/__main__.py
index 273c830..d86e04c 100644
--- a/capella_rm_bridge/__main__.py
+++ b/capella_rm_bridge/__main__.py
@@ -19,7 +19,8 @@
from . import auditing
-CHANGE_PATH = pathlib.Path("change-set.yaml")
+CHANGE_FOLDER_PATH = pathlib.Path("change-sets")
+CHANGE_FILENAME = "change-set.yaml"
CHANGE_HISTORY_PATH = pathlib.Path("change.history")
COMMIT_MSG_PATH = pathlib.Path("commit-message.txt")
ERROR_PATH = pathlib.Path("change-errors.txt")
@@ -31,6 +32,19 @@ def create_errors_statement(errors: cabc.Iterable[str]) -> str:
return "\n".join(errors)
+def write_change_set(change: str, module: dict[str, t.Any]) -> pathlib.Path:
+ """Create a change-set.yaml underneath the change-sets folder."""
+ CHANGE_FOLDER_PATH.mkdir(parents=True, exist_ok=True)
+ mid = module["id"].replace("/", "~")
+ path = CHANGE_FOLDER_PATH / f"{mid}-{CHANGE_FILENAME}"
+ if path.is_file():
+ path.unlink(missing_ok=True)
+
+ path.write_text(change, encoding="utf8")
+ LOGGER.info("Change-set file %s written.", str(path))
+ return path
+
+
@click.command()
@click.option(
"-c",
@@ -69,11 +83,12 @@ def create_errors_statement(errors: cabc.Iterable[str]) -> str:
help="Pull the latest changes from remote.",
)
@click.option(
- "--no-safe-mode",
+ "--force",
is_flag=True,
default=False,
- help="Modifications are still done to a RequirementModule if an error in "
- "another, independent module in the snapshot was identified.",
+ help="If a non RequirementModule-error was encountered only the object "
+ "and all related objects will be skipped. Intact objects will still be "
+ "synchronized",
)
@click.option(
"--gather-logs/--no-gather-logs",
@@ -104,7 +119,7 @@ def main(
dry_run: bool,
push: bool,
pull: bool,
- no_safe_mode: bool,
+ force: bool,
gather_logs: bool,
save_change_history: bool,
save_error_log: bool,
@@ -144,34 +159,35 @@ def main(
model,
tconfig,
module,
- safe_mode=not no_safe_mode,
+ force=force,
gather_logs=gather_logs,
)
if change_set:
change = decl.dump(change_set)
- CHANGE_PATH.write_text(change, encoding="utf8")
+ change_path = write_change_set(change, module)
with auditing.ChangeAuditor(model) as changed_objs:
- decl.apply(model, CHANGE_PATH)
+ decl.apply(model, change_path)
reporter.store_change(
changed_objs, module["id"], module["category"]
)
+ if force or not errors:
+ commit_message = reporter.create_commit_message(snapshot["metadata"])
+ print(commit_message)
+ if reporter.store and not dry_run:
+ model.save(
+ push=push, commit_msg=commit_message, push_options=["skip.ci"]
+ )
+
if errors:
error_statement = create_errors_statement(errors)
print(error_statement)
if save_error_log:
ERROR_PATH.write_text(error_statement, encoding="utf8")
LOGGER.info("Change-errors file %s written.", ERROR_PATH)
-
sys.exit(1)
- else:
- commit_message = reporter.create_commit_message(snapshot["metadata"])
- COMMIT_MSG_PATH.write_text(commit_message, encoding="utf8")
- print(commit_message)
- if reporter.store and not dry_run:
- model.save(push=push, commit_msg=commit_message)
report = reporter.get_change_report()
if report and save_change_history:
diff --git a/capella_rm_bridge/auditing.py b/capella_rm_bridge/auditing.py
index 8647531..f0cad50 100644
--- a/capella_rm_bridge/auditing.py
+++ b/capella_rm_bridge/auditing.py
@@ -232,14 +232,14 @@ def __audit(self, event: str, args: tuple[t.Any, ...]) -> None:
oval = getattr(obj, attr_name)
nrepr = self._get_value_repr(value)
orepr = self._get_value_repr(oval)
- params = (obj.uuid, attr_name, nrepr, orepr)
+ events = [EventType(obj.uuid, attr_name, nrepr, orepr)]
elif event.endswith("setitem"):
assert len(args) == 4
obj, attr_name, index, value = args
nrepr = self._get_value_repr(value)
oval = getattr(obj, attr_name)[index]
orepr = self._get_value_repr(oval)
- params = (obj.uuid, attr_name, nrepr, orepr)
+ events = [EventType(obj.uuid, attr_name, nrepr, orepr)]
elif event.endswith("delete"):
assert len(args) == 3
obj, attr_name, index = args
@@ -248,22 +248,34 @@ def __audit(self, event: str, args: tuple[t.Any, ...]) -> None:
if index is not None:
oval = oval[index]
- assert isinstance(oval, common.GenericElement)
- orepr = self._get_value_repr(oval)
- params = (obj.uuid, attr_name, orepr, oval.uuid)
+ if not isinstance(oval, common.GenericElement):
+ assert isinstance(oval, common.ElementList)
+ events = []
+ assert EventType is Deletion
+ for elt in oval:
+ event_type = EventType(
+ obj.uuid,
+ attr_name,
+ self._get_value_repr(elt),
+ elt.uuid,
+ )
+ events.append(event_type)
+ else:
+ orepr = self._get_value_repr(oval)
+ events = [EventType(obj.uuid, attr_name, orepr, oval.uuid)]
elif event.endswith("insert"):
assert len(args) == 4
obj, attr_name, _, value = args
nrepr = self._get_value_repr(value)
assert isinstance(value, common.GenericElement)
- params = (obj.uuid, attr_name, nrepr, value.uuid)
+ events = [EventType(obj.uuid, attr_name, nrepr, value.uuid)]
elif event.endswith("create"):
assert len(args) == 3
obj, attr_name, value = args
repr = self._get_value_repr(value)
- params = (obj.uuid, attr_name, repr, value.uuid)
+ events = [EventType(obj.uuid, attr_name, repr, value.uuid)]
- self.context.append(EventType(*params))
+ self.context.extend(events)
def _get_value_repr(self, value: t.Any) -> str | t.Any:
if hasattr(value, "_short_repr_"):
@@ -363,7 +375,7 @@ def store_change(
def _assign_module(self, change: Change) -> LiveDocID | TrackerID | None:
try:
obj = self.model.by_uuid(change.parent)
- while not isinstance(obj, reqif.RequirementsModule):
+ while not isinstance(obj, reqif.CapellaModule):
obj = obj.parent
return obj.identifier
except (KeyError, AttributeError):
@@ -408,7 +420,7 @@ def create_commit_message(self, tool_metadata: dict[str, str]) -> str:
main_message = generate_main_message(self.categories.items())
main = "\n".join((main_message, "\n".join(list_lines) + "\n"))
- summary = f"{summary} from rev.{tool_metadata['revision']} [skip ci]\n"
+ summary = f"{summary} from rev.{tool_metadata['revision']}\n"
rm_bridge_dependencies = get_dependencies()
dependencies = "\n".join(
(
@@ -439,20 +451,20 @@ def _count_changes(
return ext_count, mod_count, del_count, type_count
def _is_reqtype_change(self, change: Change) -> bool:
- if isinstance(change, Modification):
+ if isinstance(change, (Modification, Deletion)):
obj = self.model.by_uuid(change.parent)
- else:
+ elif isinstance(change, Extension):
obj = self.model.by_uuid(change.uuid)
return type(obj) in {
reqif.AttributeDefinition,
reqif.AttributeDefinitionEnumeration,
reqif.DataTypeDefinition,
- reqif.EnumDataTypeDefinition,
+ reqif.EnumerationDataTypeDefinition,
reqif.EnumValue,
reqif.ModuleType,
reqif.RelationType,
- reqif.RequirementsTypesFolder,
+ reqif.CapellaTypesFolder,
reqif.RequirementType,
}
diff --git a/capella_rm_bridge/changeset/__init__.py b/capella_rm_bridge/changeset/__init__.py
index d74aded..93b0d9e 100644
--- a/capella_rm_bridge/changeset/__init__.py
+++ b/capella_rm_bridge/changeset/__init__.py
@@ -24,8 +24,16 @@
ERROR_MESSAGE_PREFIX = "Skipping module: {module_id}"
-def _wrap_errors(module_id: int | str, errors: cabc.Sequence[str]) -> str:
- start = ERROR_MESSAGE_PREFIX.format(module_id=module_id)
+def _wrap_errors(
+ module_id: int | str,
+ errors: cabc.Sequence[str],
+ include_start: bool = True,
+) -> str:
+ if include_start:
+ start = ERROR_MESSAGE_PREFIX.format(module_id=module_id)
+ else:
+ start = f"Encountered error(s) in {module_id!r}"
+
first_sep = len(start) * "="
last_sep = len(errors[-1]) * "="
return "\n".join((start, first_sep, *errors, last_sep))
@@ -35,6 +43,7 @@ def calculate_change_set(
model: capellambse.MelodyModel,
config: actiontypes.TrackerConfig,
snapshot: actiontypes.TrackerSnapshot,
+ force: bool = False,
safe_mode: bool = True,
gather_logs: bool = True,
) -> tuple[list[dict[str, t.Any]], list[str]]:
@@ -54,6 +63,10 @@ def calculate_change_set(
calculated ``ChangeSet``.
snapshot
A snapshot of a tracker or live-document from the external tool.
+ force
+ If ``True`` a ``ChangeSet`` will be rendered even if an error
+ occurred during the change-calculation loop. All related objects
+ will be skipped.
safe_mode
If ``True`` no ``ChangeSet`` will be rendered iff atleast one
error occurred during the change-calculation loop. If ``False``
@@ -83,14 +96,21 @@ def calculate_change_set(
snapshot, model, config, gather_logs=gather_logs
)
if tchange.errors:
- message = _wrap_errors(module_id, tchange.errors)
+ message = _wrap_errors(
+ module_id, tchange.errors, include_start=not force
+ )
errors.append(message)
else:
actions.extend(tchange.actions)
+
+ if force and tchange.actions:
+ for action in tchange.actions:
+ if action not in actions:
+ actions.append(action)
except (
actiontypes.InvalidTrackerConfig,
actiontypes.InvalidSnapshotModule,
- change.MissingRequirementsModule,
+ change.MissingCapellaModule,
) as error:
if gather_logs:
message = _wrap_errors(module_id, [error.args[0]])
@@ -99,6 +119,8 @@ def calculate_change_set(
prefix = ERROR_MESSAGE_PREFIX.format(module_id=module_id)
LOGGER.error("%s. %s", prefix, error.args[0])
+ if force:
+ safe_mode = False
if safe_mode and any(errors):
actions = []
return actions, errors
diff --git a/capella_rm_bridge/changeset/change.py b/capella_rm_bridge/changeset/change.py
index 35bfefb..fc3f22e 100644
--- a/capella_rm_bridge/changeset/change.py
+++ b/capella_rm_bridge/changeset/change.py
@@ -35,7 +35,7 @@
}
-WorkItem = t.Union[reqif.Requirement, reqif.RequirementsFolder]
+WorkItem = t.Union[reqif.Requirement, reqif.Folder]
RMIdentifier = t.NewType("RMIdentifier", str)
@@ -45,8 +45,8 @@ class _AttributeValueBuilder(t.NamedTuple):
value: act.Primitive | None
-class MissingRequirementsModule(Exception):
- """A ``RequirementsModule`` with matching UUID could not be found."""
+class MissingCapellaModule(Exception):
+ """A ``CapellaModule`` with matching UUID could not be found."""
class TrackerChange:
@@ -55,6 +55,7 @@ class TrackerChange:
_location_changed: set[RMIdentifier]
_req_deletions: dict[helpers.UUIDString, dict[str, t.Any]]
_reqtype_ids: set[RMIdentifier]
+ _faulty_attribute_definitions: set[str]
errors: list[str]
tracker: cabc.Mapping[str, t.Any]
@@ -133,6 +134,7 @@ def __init__(
self._location_changed = set[RMIdentifier]()
self._req_deletions = {}
+ self._faulty_attribute_definitions = set[str]()
self.errors = []
self.__reqtype_action: dict[str, t.Any] | None = None
@@ -209,13 +211,13 @@ def check_requirements_module(self) -> dict[str, t.Any]:
self.req_module = self.reqfinder.reqmodule(module_uuid)
except KeyError as error:
raise act.InvalidTrackerConfig(
- "The given module configuration is missing 'UUID' of the "
+ "The given module configuration is missing UUID of the "
"target RequirementsModule"
) from error
if self.req_module is None:
- raise MissingRequirementsModule(
- f"No RequirementsModule with UUID '{module_uuid}' found in "
+ raise MissingCapellaModule(
+ f"No RequirementsModule with UUID {module_uuid!r} found in "
+ repr(self.model.info)
)
@@ -223,7 +225,7 @@ def check_requirements_module(self) -> dict[str, t.Any]:
identifier = self.tracker["id"]
except KeyError as error:
raise act.InvalidSnapshotModule(
- "In the snapshot the module is missing an 'id' key"
+ "In the snapshot the module is missing an id key"
) from error
base = {"parent": decl.UUIDReference(self.req_module.uuid)}
@@ -250,7 +252,7 @@ def invalidate_deletion(self, requirement: WorkItem) -> None:
delete action was removed.
"""
key = "requirements"
- if isinstance(requirement, reqif.RequirementsFolder):
+ if isinstance(requirement, reqif.Folder):
key = "folders"
try:
@@ -407,15 +409,15 @@ def attribute_definition_create_action(
name, below=self.reqt_folder
)
if etdef is None:
+ promise_id = f"EnumerationDataTypeDefinition {name}"
if name not in self.data_type_definitions:
+ self._faulty_attribute_definitions.add(promise_id)
raise act.InvalidAttributeDefinition(
- f"Invalid {cls.__name__} found: '{name}'. Missing its "
+ f"Invalid {cls.__name__} found: {name!r}. Missing its "
"datatype definition in `data_types`."
)
- data_type_ref = decl.Promise(
- f"EnumerationDataTypeDefinition {name}"
- )
+ data_type_ref = decl.Promise(promise_id)
else:
data_type_ref = decl.UUIDReference(etdef.uuid)
@@ -481,7 +483,7 @@ def yield_requirements_create_actions(
base["folders"] = []
child: act.WorkItem
for child in item["children"]:
- key = "folders" if child.get("children") else "requirements"
+ key = "folders" if "children" in child else "requirements"
creq = self.reqfinder.work_item_by_identifier(child["id"])
if creq is None:
child_actions = self.yield_requirements_create_actions(
@@ -591,7 +593,14 @@ def attribute_value_create_action(
deftype, attr_def_id, below=self.reqt_folder
)
if definition is None:
- definition_ref = decl.Promise(f"{deftype} {attr_def_id}")
+ 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} "
+ "promised."
+ )
+ else:
+ definition_ref = decl.Promise(promise_id)
else:
definition_ref = decl.UUIDReference(definition.uuid)
@@ -615,7 +624,13 @@ def check_attribute_value_is_valid(
"Unknown field type '%s' for %s: %r", deftype, name, value
)
if deftype == "Enum":
- options = self.data_type_definitions[name]
+ options = self.data_type_definitions.get(name)
+ if options is None:
+ raise act.InvalidFieldValue(
+ f"Invalid field found: {name!r}. Missing its "
+ "datatype definition in `data_types`."
+ )
+
is_faulty = not matches_type or not set(value) & set(options)
key = "values"
else:
@@ -624,7 +639,7 @@ def check_attribute_value_is_valid(
if is_faulty:
raise act.InvalidFieldValue(
- f"Invalid field found: {key} '{value}' for '{name}'"
+ f"Invalid field found: {key} {value!r} for {name!r}"
)
return _AttributeValueBuilder(deftype, key, value)
@@ -743,9 +758,14 @@ def requirement_type_mod_action(
)
assert isinstance(reqtype, reqif.RequirementType)
- mods = _compare_simple_attributes(
- reqtype, item, filter=("attributes",)
- )
+ try:
+ mods = _compare_simple_attributes(
+ reqtype, item, filter=("attributes",)
+ )
+ except AttributeError as error:
+ self._handle_user_error(
+ f"Invalid workitem '{identifier}'. {error.args[0]}"
+ )
attr_defs_deletions: list[decl.UUIDReference] = [
decl.UUIDReference(adef.uuid)
@@ -804,16 +824,23 @@ def yield_requirements_mod_actions(
from it.
"""
base = {"parent": decl.UUIDReference(req.uuid)}
- mods = _compare_simple_attributes(
- req, item, filter=("id", "type", "attributes", "children")
- )
+ try:
+ mods = _compare_simple_attributes(
+ req, item, filter=("id", "type", "attributes", "children")
+ )
+ except AttributeError as error:
+ self._handle_user_error(
+ f"Invalid workitem '{item['id']}'. {error.args[0]}"
+ )
+ return
+
req_type_id = RMIdentifier(item.get("type", ""))
attributes_deletions = list[dict[str, t.Any]]()
if req_type_id != req.type.identifier:
if req_type_id and req_type_id not in self.requirement_types:
raise act.InvalidWorkItemType(
"Faulty workitem in snapshot: "
- f"Unknown workitem-type '{req_type_id}'"
+ f"Unknown workitem-type {req_type_id!r}"
)
reqtype = self.reqfinder.reqtype_by_identifier(
@@ -888,12 +915,12 @@ def yield_requirements_mod_actions(
cf_creations: list[dict[str, t.Any] | decl.UUIDReference] = []
containers = [cr_creations, cf_creations]
child_mods: list[dict[str, t.Any]] = []
- if isinstance(req, reqif.RequirementsFolder):
+ if isinstance(req, reqif.Folder):
child_req_ids = set[RMIdentifier]()
child_folder_ids = set[RMIdentifier]()
for child in children:
cid = RMIdentifier(str(child["id"]))
- if child.get("children", []):
+ if "children" in child:
key = "folders"
child_folder_ids.add(cid)
else:
@@ -918,6 +945,7 @@ def yield_requirements_mod_actions(
f"Invalid workitem '{child['id']}'. "
+ error.args[0]
)
+ continue
if creq.parent != req:
container.append(decl.UUIDReference(creq.uuid))
@@ -1072,9 +1100,16 @@ def attribute_definition_mod_action(
return None
return {"parent": decl.UUIDReference(attrdef.uuid), "modify": mods}
except KeyError:
- return self.attribute_definition_create_action(
- name, data, reqtype.identifier
- )
+ try:
+ return self.attribute_definition_create_action(
+ name, data, reqtype.identifier
+ )
+ except act.InvalidAttributeDefinition as error:
+ self._handle_user_error(
+ f"In RequirementType {reqtype.long_name!r}: "
+ + error.args[0]
+ )
+ return None
def make_requirement_delete_actions(
@@ -1137,7 +1172,7 @@ def _compare_simple_attributes(
converter = type_conversion.get(name, lambda i: i)
converted_value = converter(value)
- if getattr(req, name, None) != converted_value:
+ if getattr(req, name) != converted_value:
mods[name] = value
return mods
diff --git a/capella_rm_bridge/changeset/find.py b/capella_rm_bridge/changeset/find.py
index 60a21ac..e88ef1b 100644
--- a/capella_rm_bridge/changeset/find.py
+++ b/capella_rm_bridge/changeset/find.py
@@ -35,25 +35,29 @@ def _get(
LOGGER.info("No %s found with %s: %r", types, attr, value)
return None
- def reqmodule(self, uuid: str) -> reqif.RequirementsModule | None:
- """Try to return the ``RequirementsModule``."""
- return self._get(uuid, reqif.XT_MODULE, attr="uuid")
+ 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.RequirementsModule
+ | reqif.CapellaModule
| None = None,
- ) -> reqif.RequirementsTypesFolder | None:
+ ) -> reqif.CapellaTypesFolder | None:
"""Try to return the ``RequirementTypesFolder``."""
- return self._get(str(identifier), reqif.XT_REQ_TYPES_F, below=below)
+ return self._get(
+ str(identifier), reqif.CapellaTypesFolder.__name__, below=below
+ )
def reqtype_by_identifier(
self, identifier: int | str, below: reqif.ReqIFElement | None = None
- ) -> reqif.RequirementsType | None:
+ ) -> reqif.RequirementType | None:
"""Try to return a ``RequirementType``."""
- return self._get(str(identifier), reqif.XT_REQ_TYPE, below=below)
+ return self._get(
+ str(identifier), reqif.RequirementType.__name__, below=below
+ )
def attribute_definition_by_identifier(
self, xtype: str, identifier: str, below: reqif.ReqIFElement | None
@@ -65,21 +69,27 @@ def work_item_by_identifier(
self,
identifier: int | str,
below: reqif.ReqIFElement | None = None,
- ) -> reqif.Requirement | reqif.RequirementsFolder | None:
+ ) -> reqif.Requirement | reqif.Folder | None:
"""Try to return a ``Requirement``/``RequirementsFolder``."""
return self._get(
- str(identifier), reqif.XT_REQUIREMENT, reqif.XT_FOLDER, below=below
+ str(identifier),
+ reqif.Requirement.__name__,
+ reqif.Folder.__name__,
+ below=below,
)
def enum_data_type_definition_by_long_name(
self, long_name: str, below: reqif.ReqIFElement | None
- ) -> reqif.EnumDataTypeDefinition | None:
- """Try to return an ``EnumDataTypeDefinition``.
+ ) -> reqif.EnumerationDataTypeDefinition | None:
+ """Try to return an ``EnumerationDataTypeDefinition``.
The object is matched with given ``long_name``.
"""
return self._get(
- long_name, reqif.XT_REQ_TYPE_ENUM, attr="long_name", below=below
+ long_name,
+ reqif.EnumerationDataTypeDefinition.__name__,
+ attr="long_name",
+ below=below,
)
def enum_value_by_long_name(
@@ -89,5 +99,6 @@ def enum_value_by_long_name(
The object is matched with given ``long_name``.
"""
- xt = reqif.XT_REQ_TYPE_ATTR_ENUM
- return self._get(long_name, xt, attr="long_name", below=below)
+ return self._get(
+ long_name, reqif.EnumValue.__name__, attr="long_name", below=below
+ )
diff --git a/tests/data/changesets/create.yaml b/tests/data/changesets/create.yaml
index df2a203..c7b49da 100644
--- a/tests/data/changesets/create.yaml
+++ b/tests/data/changesets/create.yaml
@@ -14,8 +14,6 @@
values:
- long_name: Unset
promise_id: EnumValue Type Unset
- - long_name: Folder
- promise_id: EnumValue Type Folder
- long_name: Functional
promise_id: EnumValue Type Functional
promise_id: EnumerationDataTypeDefinition Type
diff --git a/tests/data/model/RM Bridge.capella b/tests/data/model/RM Bridge.capella
index 1b7fd01..7de9981 100644
--- a/tests/data/model/RM Bridge.capella
+++ b/tests/data/model/RM Bridge.capella
@@ -1839,8 +1839,6 @@ The predator is far away
id="686e198b-8baf-49f9-9d85-24571bd05d93" ReqIFLongName="Type">
Test Description
type: system_requirement # WorkItemType ID NOT name - attributes: - Type: [Folder] # artifact from CODEBEAMER and cleaned in RM Bridge - children: # Folder b/c non-empty children - id: REQ-002 long_name: Function Requirement @@ -70,8 +66,6 @@ modules: - id: REQ-003 long_name: Kinds type: software_requirement - attributes: - Type: [Folder] children: - id: REQ-004 long_name: Kind Requirement diff --git a/tests/data/snapshots/snapshot1.yaml b/tests/data/snapshots/snapshot1.yaml index 263a3f8..b981e46 100644 --- a/tests/data/snapshots/snapshot1.yaml +++ b/tests/data/snapshots/snapshot1.yaml @@ -12,7 +12,6 @@ modules: data_types: Type: - Unset - - Folder - Functional - Non-Functional Release: @@ -70,8 +69,6 @@ modules: long_name: Functional Requirements text:Brand new
type: software_requirement - attributes: - Type: [Folder] children: - id: REQ-004 long_name: Function Requirement diff --git a/tests/data/snapshots/snapshot2.yaml b/tests/data/snapshots/snapshot2.yaml index a28d121..b661822 100644 --- a/tests/data/snapshots/snapshot2.yaml +++ b/tests/data/snapshots/snapshot2.yaml @@ -12,7 +12,6 @@ modules: data_types: Type: - Unset - - Folder - Non-Functional requirement_types: system_requirement: diff --git a/tests/test_auditing.py b/tests/test_auditing.py index 6d775c9..e10d92f 100644 --- a/tests/test_auditing.py +++ b/tests/test_auditing.py @@ -336,6 +336,8 @@ def test_create_commit_message(migration_model: capellambse.MelodyModel): req_uuid = "163394f5-c1ba-4712-a238-b0b143c66aed" reqtypesfolder_uuid = "a15e8b60-bf39-47ba-b7c7-74ceecb25c9c" dtdef_uuid = "686e198b-8baf-49f9-9d85-24571bd05d93" + req = migration_model.by_uuid(req_uuid) + req.parent.requirements.remove(req) changes: list[auditing.Change] = [ auditing.Deletion( parent="9a9b5a8f-a6ad-4610-9e88-3b5e9c943c19", @@ -372,7 +374,7 @@ def test_create_commit_message(migration_model: capellambse.MelodyModel): commit_message = reporter.create_commit_message(tool_metadata) assert commit_message.startswith( - "Updated model with RM content from rev.123 [skip ci]\n" + "Updated model with RM content from rev.123\n" "\n" "Synchronized 1 category1 and 1 category2:\n" "- 1: created: 1; updated: 1; deleted: 1; type-changes: 0\n" diff --git a/tests/test_changeset.py b/tests/test_changeset.py index cf1639b..740bdb5 100644 --- a/tests/test_changeset.py +++ b/tests/test_changeset.py @@ -115,7 +115,7 @@ def test_init_on_missing_module_raises_MissingRequirementsModule( """ del clean_model.la.requirement_modules[0] - with pytest.raises(change.MissingRequirementsModule): + with pytest.raises(change.MissingCapellaModule): self.tracker_change(clean_model, TEST_SNAPSHOT["modules"][0]) def test_init_on_missing_module_id_raises_InvalidSnapshotModule( @@ -207,7 +207,7 @@ def test_faulty_attribute_values_log_InvalidFieldValue_as_error( first_child = titem["children"][0] first_child["attributes"][attr] = faulty_value # type:ignore[index] message_end = ( - f"Invalid field found: {key} '{faulty_value}' for '{attr}'" + f"Invalid field found: {key} {faulty_value!r} for {attr!r}" ) with caplog.at_level(logging.ERROR): @@ -229,7 +229,7 @@ def test_InvalidFieldValue_errors_are_gathered( ] = faulty_value # type:ignore[index] messages.append( "Invalid workitem 'REQ-002'. " - f"Invalid field found: {key} '{faulty_value}' for '{attr}'" + f"Invalid field found: {key} {faulty_value!r} for {attr!r}" ) change = self.tracker_change(clean_model, tracker, gather_logs=True) @@ -268,6 +268,22 @@ def test_InvalidAttributeDefinition_errors_are_gathered( assert change.errors == [INVALID_ATTR_DEF_ERROR_MSG] + def test_requirements_with_empty_children_are_rendered_as_folders( + self, clean_model: capellambse.MelodyModel + ) -> None: + tracker = copy.deepcopy(self.tracker) + tracker["items"][0]["children"][1]["children"][0]["children"] = [] + + change_set = self.tracker_change( + clean_model, tracker, gather_logs=True + ) + + assert change_set and (change := change_set.actions[0]["extend"]) + assert ( + change["folders"][0]["folders"][0]["folders"][0]["identifier"] + == "REQ-004" + ) + @pytest.mark.integtest def test_calculate_change_sets( self, clean_model: capellambse.MelodyModel @@ -342,7 +358,7 @@ def test_faulty_attribute_values_log_InvalidFieldValue_as_error( first_child = titem["children"][0] first_child["attributes"][attr] = faulty_value message_end = ( - f"Invalid field found: {key} '{faulty_value}' for '{attr}'" + f"Invalid field found: {key} {faulty_value!r} for {attr!r}" ) with caplog.at_level(logging.ERROR): @@ -350,6 +366,72 @@ def test_faulty_attribute_values_log_InvalidFieldValue_as_error( assert caplog.messages[0].endswith(message_end) + def test_faulty_data_types_log_InvalidAttributeDefinition_as_error( + self, + migration_model: capellambse.MelodyModel, + caplog: pytest.LogCaptureFixture, + ) -> None: + tracker = copy.deepcopy(self.tracker) + reqtype = tracker["requirement_types"]["system_requirement"] + reqtype["attributes"]["Not-Defined"] = { # type: ignore[call-overload] + "type": "Enum", + "data_type": "Not-Defined", + } + + with caplog.at_level(logging.ERROR): + self.tracker_change(migration_model, tracker, gather_logs=False) + + assert caplog.messages[0].endswith(INVALID_ATTR_DEF_ERROR_MSG) + + def test_faulty_simple_attributes_log_AttributeError( + self, + migration_model: capellambse.MelodyModel, + caplog: pytest.LogCaptureFixture, + ) -> None: + """Test logging ``AttributeError`` on faulty simple attributes.""" + 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) + + def test_InvalidAttributeDefinition_errors_are_gathered( + self, migration_model: capellambse.MelodyModel + ) -> None: + """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] + "type": "Enum", + "data_type": "Not-Defined", + } + + change = self.tracker_change( + migration_model, tracker, gather_logs=True + ) + + assert change.errors == [INVALID_ATTR_DEF_ERROR_MSG] + + def test_requirements_with_empty_children_are_rendered_as_folders( + self, clean_model: capellambse.MelodyModel + ) -> None: + tracker = copy.deepcopy(self.tracker) + tracker["items"][1]["children"][0]["children"] = [] + + change_set = self.tracker_change( + clean_model, tracker, gather_logs=True + ) + + assert change_set and (change := change_set.actions[0]["extend"]) + assert change["folders"][1]["folders"][0]["identifier"] == "REQ-004" + @pytest.mark.integtest def test_calculate_change_sets( self, migration_model: capellambse.MelodyModel @@ -491,7 +573,7 @@ def test_missing_module_UUID_logs_InvalidTrackerConfig_error( config = copy.deepcopy(TEST_CONFIG["modules"][0]) del config["capella-uuid"] # type:ignore[misc] message = ( - "The given module configuration is missing 'UUID' of the target " + "The given module configuration is missing UUID of the target " "RequirementsModule" ) @@ -538,7 +620,7 @@ def test_missing_module_id_logs_InvalidSnapshotModule_error( tconfig = TEST_CONFIG["modules"][0] message = ( "Skipping module: MISSING ID. " - "In the snapshot the module is missing an 'id' key" + "In the snapshot the module is missing an id key" ) with caplog.at_level(logging.ERROR): @@ -555,7 +637,7 @@ def test_init_errors_are_gathered( config = copy.deepcopy(TEST_CONFIG["modules"][0]) del config["capella-uuid"] # type:ignore[misc] message = ( - "The given module configuration is missing 'UUID' of the target " + "The given module configuration is missing UUID of the target " "RequirementsModule" ) @@ -566,13 +648,51 @@ def test_init_errors_are_gathered( assert errors[0].startswith(self.SKIP_MESSAGE) assert message in errors[0] + def test_forced_calculation_produces_change_set_on_AttributeDefinition_error( + self, clean_model: capellambse.MelodyModel + ) -> None: + """Test that an invalid AttributeDefinition will not prohibit.""" + snapshot = copy.deepcopy(TEST_SNAPSHOT["modules"][0]) + 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 " + "datatype definition in `data_types`.\n" + "Invalid workitem 'REQ-002'. Invalid field found: 'Release'. " + "Missing its datatype definition in `data_types`." + ) + + change_sets, errors = calculate_change_set( + clean_model, tconfig, snapshot, gather_logs=True, force=True + ) + + assert (change_set := change_sets[0]) + for rtfolder in change_set["extend"]["requirement_types_folders"]: + for req_type in rtfolder["requirement_types"]: + for attr_def in req_type["attribute_definitions"]: + assert attr_def["long_name"] != missing_enumdt + + folder = change_set["extend"]["folders"][0] + for folder in folder["folders"]: + for attr_value in folder.get("attributes", []): + assert ( + missing_enumdt not in attr_value["definition"].identifier + ) + + assert errors[0].startswith( + "Encountered error(s) in 'project/space/example title'" + ) + assert message in errors[0] + def test_snapshot_errors_from_ChangeSet_calculation_are_gathered( self, clean_model: capellambse.MelodyModel ) -> None: snapshot = copy.deepcopy(TEST_SNAPSHOT["modules"][0]) titem = snapshot["items"][0] first_child = titem["children"][0] - titem["attributes"]["Test"] = 1 # type: ignore[index] + titem["attributes"] = {"Test": 1} # type: ignore[index] messages = [ "Invalid workitem 'REQ-001'. " "Invalid field found: field name 'Test' not defined in " @@ -584,9 +704,10 @@ def test_snapshot_errors_from_ChangeSet_calculation_are_gathered( ] = faulty_value # type:ignore[index] messages.append( "Invalid workitem 'REQ-002'. " - + f"Invalid field found: {key} '{faulty_value}' for '{attr}'" + + f"Invalid field found: {key} {faulty_value!r} for {attr!r}" ) del titem["children"][1]["type"] + titem["children"][1]["attributes"] = {"Test": 1} tconfig = TEST_CONFIG["modules"][0] messages.append( "Invalid workitem 'REQ-003'. Missing type but attributes found"