From 2d2e2f4556a5d0b6c15d25ccc6f5400e740f4bc4 Mon Sep 17 00:00:00 2001 From: jce <28319872+JasperCraeghs@users.noreply.github.com> Date: Wed, 24 Jul 2024 21:54:25 +0200 Subject: [PATCH 01/10] Implement option 'recursiveintermediates' --- .../directives/item_matrix_directive.py | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/mlx/traceability/directives/item_matrix_directive.py b/mlx/traceability/directives/item_matrix_directive.py index e5247145..a8275825 100644 --- a/mlx/traceability/directives/item_matrix_directive.py +++ b/mlx/traceability/directives/item_matrix_directive.py @@ -352,16 +352,17 @@ def linking_via_intermediate(self, source_ids, targets_with_ids, collection): source_to_links_map = {source_id: {} for source_id in source_ids} for intermediate_id in collection.get_items(self['intermediate'], sort=bool(self['intermediatetitle'])): intermediate_item = collection.get_item(intermediate_id) - potential_source_ids = set() for reverse_rel in links_with_relationships[0]: potential_source_ids.update(intermediate_item.yield_targets(reverse_rel)) # apply :source: filter potential_source_ids = potential_source_ids.intersection(source_ids) + if not potential_source_ids: # move to the next intermediate candidate to save resources + continue potential_target_ids = set() for forward_rel in links_with_relationships[1]: - potential_target_ids.update(intermediate_item.yield_targets(forward_rel)) + potential_target_ids.update(self._determine_targets(intermediate_item, forward_rel, collection)) # apply :target: filter actual_targets = [] for target_ids in targets_with_ids: @@ -371,6 +372,20 @@ def linking_via_intermediate(self, source_ids, targets_with_ids, collection): self._store_targets(source_to_links_map, potential_source_ids, actual_targets, intermediate_item) return source_to_links_map + def _determine_targets(self, item, relationship, collection): + """ Determines all potential targets of a given intermediate item and forward relationship. + + Note: This function is recursively called when the option 'recursiveintermediates' is used and a suitable + intermediate item was found via the specified relationship for recursion. + """ + potential_target_ids = set(item.yield_targets(relationship)) + if self['recursiveintermediates']: + for nested_item_id in item.yield_targets(self['recursiveintermediates']): + nested_item = collection.items[nested_item_id] + if nested_item.is_match(self['intermediate']): + potential_target_ids.update(self._determine_targets(nested_item, relationship, collection)) + return potential_target_ids + @staticmethod def _store_targets(source_to_links_map, source_ids, targets, intermediate_item): """ Extends given mapping with target IDs per target as value for each source ID as key @@ -607,6 +622,7 @@ class ItemMatrixDirective(TraceableBaseDirective): 'onlycovered': directives.flag, 'onlyuncovered': directives.flag, 'coveredintermediates': directives.flag, + 'recursiveintermediates': directives.unchanged, 'stats': directives.flag, 'coverage': directives.unchanged, 'nocaptions': directives.flag, @@ -643,6 +659,7 @@ def run(self): 'intermediatetitle': {'default': ''}, 'type': {'default': ''}, 'sourcetype': {'default': []}, + 'recursiveintermediates': {'default': ''}, 'coverage': {'default': ''}, }, ) @@ -695,6 +712,10 @@ def run(self): raise TraceabilityException( "Item-matrix directive cannot combine 'onlycovered' with 'onlyuncovered' flag", docname=env.docname) + if node['intermediatetitle'] and node['recursiveintermediates']: + raise TraceabilityException( + "Item-matrix directive cannot combine 'intermediatetitle' with 'recursiveintermediates' flag", + docname=env.docname) if node['targetcolumns']: node['splittargets'] = True From 8abf3c4146cc8ad703f705e5cab5d446bbabf3ea Mon Sep 17 00:00:00 2001 From: jce <28319872+JasperCraeghs@users.noreply.github.com> Date: Wed, 24 Jul 2024 21:55:10 +0200 Subject: [PATCH 02/10] Test new option 'recursiveintermediates' --- doc/integration_test_report.rst | 18 ++++++++++++++++++ doc/requirements.rst | 4 ++++ 2 files changed, 22 insertions(+) diff --git a/doc/integration_test_report.rst b/doc/integration_test_report.rst index 8c291eb2..20cd6727 100755 --- a/doc/integration_test_report.rst +++ b/doc/integration_test_report.rst @@ -163,6 +163,9 @@ Integration tests .. item:: ITEST-r100 Test a requirement using the ``requirement`` type :validates: r100 +.. item:: ITEST-DUMMY_CHILD Test of dummy requirement + :validates: RQT-DUMMY_CHILD + Integration test reports ======================== @@ -506,6 +509,21 @@ Traceability via intermediate items :nocaptions: :stats: :coveredintermediates: + :coverage: <100 + +.. item-matrix:: Design to test via requirements recursively + :source: DESIGN- + :intermediate: RQT- + :target: UTEST ITEST + :sourcetitle: design items + :targettitle: unit tests, integration tests + :type: fulfills | validated_by + :group: top + :nocaptions: + :stats: + :coveredintermediates: + :recursiveintermediates: impacts_on + :coverage: ==100 Source and target columns ------------------------- diff --git a/doc/requirements.rst b/doc/requirements.rst index 206e0b86..88badb84 100755 --- a/doc/requirements.rst +++ b/doc/requirements.rst @@ -99,6 +99,10 @@ Requirements for mlx.traceability .. item:: RQT-DUMMY Dummy requirement that is not covered by a test :fulfilled_by: DESIGN-ATTRIBUTES DESIGN-ITEMIZE +.. item:: RQT-DUMMY_CHILD Child of the dummy requirement that is not covered by a test + :depends_on: RQT-DUMMY +.. :validated_by: ITEST-DUMMY_CHILD + ------------------- Traceability matrix ------------------- From 7109fc60eaa684bf1261f84caddee7a1585089d8 Mon Sep 17 00:00:00 2001 From: jce <28319872+JasperCraeghs@users.noreply.github.com> Date: Wed, 24 Jul 2024 22:07:44 +0200 Subject: [PATCH 03/10] Test combination of splitintermediates with recursiveintermediates --- doc/integration_test_report.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/integration_test_report.rst b/doc/integration_test_report.rst index 20cd6727..8e48a652 100755 --- a/doc/integration_test_report.rst +++ b/doc/integration_test_report.rst @@ -522,6 +522,7 @@ Traceability via intermediate items :nocaptions: :stats: :coveredintermediates: + :splitintermediates: :recursiveintermediates: impacts_on :coverage: ==100 From ef4d58e20151508cd17684ccbb70d4136c227189 Mon Sep 17 00:00:00 2001 From: jce <28319872+JasperCraeghs@users.noreply.github.com> Date: Thu, 25 Jul 2024 13:32:11 +0200 Subject: [PATCH 04/10] Clarify docstring --- mlx/traceability/directives/item_matrix_directive.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mlx/traceability/directives/item_matrix_directive.py b/mlx/traceability/directives/item_matrix_directive.py index a8275825..cbd3c5ff 100644 --- a/mlx/traceability/directives/item_matrix_directive.py +++ b/mlx/traceability/directives/item_matrix_directive.py @@ -376,7 +376,7 @@ def _determine_targets(self, item, relationship, collection): """ Determines all potential targets of a given intermediate item and forward relationship. Note: This function is recursively called when the option 'recursiveintermediates' is used and a suitable - intermediate item was found via the specified relationship for recursion. + nested intermediate item was found via the specified relationship for recursion. """ potential_target_ids = set(item.yield_targets(relationship)) if self['recursiveintermediates']: From 55b0c81776fc9c777a28e6b67a3b59938af8031b Mon Sep 17 00:00:00 2001 From: jce <28319872+JasperCraeghs@users.noreply.github.com> Date: Thu, 25 Jul 2024 17:32:25 +0200 Subject: [PATCH 05/10] All intermediates in the chain shall be covered for the source item to be covered --- .../directives/item_matrix_directive.py | 21 ++++++++++++------- mlx/traceability/traceable_item.py | 17 ++++++++------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/mlx/traceability/directives/item_matrix_directive.py b/mlx/traceability/directives/item_matrix_directive.py index cbd3c5ff..ab1b4ce0 100644 --- a/mlx/traceability/directives/item_matrix_directive.py +++ b/mlx/traceability/directives/item_matrix_directive.py @@ -361,8 +361,9 @@ def linking_via_intermediate(self, source_ids, targets_with_ids, collection): continue potential_target_ids = set() - for forward_rel in links_with_relationships[1]: - potential_target_ids.update(self._determine_targets(intermediate_item, forward_rel, collection)) + target_ids, uncovered_items = self._determine_targets(intermediate_item, links_with_relationships[1], + collection) + potential_target_ids.update(target_ids) # apply :target: filter actual_targets = [] for target_ids in targets_with_ids: @@ -370,21 +371,27 @@ def linking_via_intermediate(self, source_ids, targets_with_ids, collection): actual_targets.append(set(collection.get_item(id_) for id_ in linked_target_ids)) self._store_targets(source_to_links_map, potential_source_ids, actual_targets, intermediate_item) + for item in uncovered_items: + self._store_targets(source_to_links_map, potential_source_ids, [], item) return source_to_links_map - def _determine_targets(self, item, relationship, collection): - """ Determines all potential targets of a given intermediate item and forward relationship. + def _determine_targets(self, item, relationships, collection): + """ Determines all potential targets of a given intermediate item and forward relationships. Note: This function is recursively called when the option 'recursiveintermediates' is used and a suitable nested intermediate item was found via the specified relationship for recursion. """ - potential_target_ids = set(item.yield_targets(relationship)) + all_uncovered_items = set() + potential_target_ids = set(item.yield_targets(*relationships)) if self['recursiveintermediates']: for nested_item_id in item.yield_targets(self['recursiveintermediates']): nested_item = collection.items[nested_item_id] if nested_item.is_match(self['intermediate']): - potential_target_ids.update(self._determine_targets(nested_item, relationship, collection)) - return potential_target_ids + new_target_ids, _ = self._determine_targets(nested_item, relationships, collection) + potential_target_ids.update(new_target_ids) + if not new_target_ids: + all_uncovered_items.add(nested_item) + return potential_target_ids, all_uncovered_items @staticmethod def _store_targets(source_to_links_map, source_ids, targets, intermediate_item): diff --git a/mlx/traceability/traceable_item.py b/mlx/traceability/traceable_item.py index cede0d54..05f353c8 100644 --- a/mlx/traceability/traceable_item.py +++ b/mlx/traceability/traceable_item.py @@ -201,23 +201,24 @@ def iter_targets(self, relation, explicit=True, implicit=True, sort=True): return natsorted(targets) return targets - def yield_targets(self, relation, explicit=True, implicit=True): + def yield_targets(self, *relations, explicit=True, implicit=True): ''' Gets an iterable of targets to other traceable items. Args: - relation (str): Name of the relation. + relations (iter[str]): One or more names of relations. explicit (bool): If True, explicitly expressed relations are included. implicit (bool): If True, implicitly expressed relations are included. Returns: generator: Targets to other traceable items, unsorted ''' - if explicit and relation in self.explicit_relations: - for target in self.explicit_relations[relation]: - yield target - if implicit and relation in self.implicit_relations: - for target in self.implicit_relations[relation]: - yield target + for relation in relations: + if explicit and relation in self.explicit_relations: + for target in self.explicit_relations[relation]: + yield target + if implicit and relation in self.implicit_relations: + for target in self.implicit_relations[relation]: + yield target def yield_targets_sorted(self, *args, **kwargs): ''' Gets an iterable of targets to other traceable items, with natural sorting applied. ''' From 154e380c10af50383941070d8d41b5b885a3fe5a Mon Sep 17 00:00:00 2001 From: jce <28319872+JasperCraeghs@users.noreply.github.com> Date: Thu, 25 Jul 2024 17:35:16 +0200 Subject: [PATCH 06/10] Test effect of 55b0c81776fc9c777a28e6b67a3b59938af8031b by expecting additional Sphinx warning --- doc/integration_test_report.rst | 4 +++- tools/doc-warnings.json | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/doc/integration_test_report.rst b/doc/integration_test_report.rst index 8e48a652..2e7785f4 100755 --- a/doc/integration_test_report.rst +++ b/doc/integration_test_report.rst @@ -509,7 +509,9 @@ Traceability via intermediate items :nocaptions: :stats: :coveredintermediates: - :coverage: <100 + :coverage: <79 + +.. warning due to below item-matrix: coverage is not 100% due to child RQT not covered .. item-matrix:: Design to test via requirements recursively :source: DESIGN- diff --git a/tools/doc-warnings.json b/tools/doc-warnings.json index 5f1ec80c..46b628d5 100644 --- a/tools/doc-warnings.json +++ b/tools/doc-warnings.json @@ -1,8 +1,8 @@ { "sphinx": { "enabled": true, - "min": 29, - "max": 29, + "min": 30, + "max": 30, "exclude": [ "WARNING: the mlx.traceability extension is not safe for parallel reading", "WARNING: doing serial read" From 6c68d27983546a7a27aca447578234f39d9c0ad2 Mon Sep 17 00:00:00 2001 From: jce <28319872+JasperCraeghs@users.noreply.github.com> Date: Thu, 25 Jul 2024 17:44:45 +0200 Subject: [PATCH 07/10] Use the term 'relation' instead of 'relationship': preferred for abstract things --- .../directives/item_matrix_directive.py | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/mlx/traceability/directives/item_matrix_directive.py b/mlx/traceability/directives/item_matrix_directive.py index ab1b4ce0..c4f8a0a4 100644 --- a/mlx/traceability/directives/item_matrix_directive.py +++ b/mlx/traceability/directives/item_matrix_directive.py @@ -338,22 +338,22 @@ def linking_via_intermediate(self, source_ids, targets_with_ids, collection): dict: Mapping of source IDs as key with as value a mapping of intermediate items to the list of sets of target items per target """ - links_with_relationships = [] + links_with_relations = [] for relationships_str in self['type'].split(' | '): - links_with_relationships.append(relationships_str.split(' ')) - if len(links_with_relationships) > 2: + links_with_relations.append(relationships_str.split(' ')) + if len(links_with_relations) > 2: raise TraceabilityException("Type option of item-matrix must not contain more than one '|' " "character; got {}".format(self['type']), docname=self["document"]) # reverse relationship(s) specified for linking source to intermediate - for idx, rel in enumerate(links_with_relationships[0]): - links_with_relationships[0][idx] = collection.get_reverse_relation(rel) + for idx, rel in enumerate(links_with_relations[0]): + links_with_relations[0][idx] = collection.get_reverse_relation(rel) source_to_links_map = {source_id: {} for source_id in source_ids} for intermediate_id in collection.get_items(self['intermediate'], sort=bool(self['intermediatetitle'])): intermediate_item = collection.get_item(intermediate_id) potential_source_ids = set() - for reverse_rel in links_with_relationships[0]: + for reverse_rel in links_with_relations[0]: potential_source_ids.update(intermediate_item.yield_targets(reverse_rel)) # apply :source: filter potential_source_ids = potential_source_ids.intersection(source_ids) @@ -361,7 +361,7 @@ def linking_via_intermediate(self, source_ids, targets_with_ids, collection): continue potential_target_ids = set() - target_ids, uncovered_items = self._determine_targets(intermediate_item, links_with_relationships[1], + target_ids, uncovered_items = self._determine_targets(intermediate_item, links_with_relations[1], collection) potential_target_ids.update(target_ids) # apply :target: filter @@ -375,19 +375,19 @@ def linking_via_intermediate(self, source_ids, targets_with_ids, collection): self._store_targets(source_to_links_map, potential_source_ids, [], item) return source_to_links_map - def _determine_targets(self, item, relationships, collection): - """ Determines all potential targets of a given intermediate item and forward relationships. + def _determine_targets(self, item, relations, collection): + """ Determines all potential targets of a given intermediate item and forward relations. Note: This function is recursively called when the option 'recursiveintermediates' is used and a suitable - nested intermediate item was found via the specified relationship for recursion. + nested intermediate item was found via the specified relation for recursion. """ all_uncovered_items = set() - potential_target_ids = set(item.yield_targets(*relationships)) + potential_target_ids = set(item.yield_targets(*relations)) if self['recursiveintermediates']: for nested_item_id in item.yield_targets(self['recursiveintermediates']): nested_item = collection.items[nested_item_id] if nested_item.is_match(self['intermediate']): - new_target_ids, _ = self._determine_targets(nested_item, relationships, collection) + new_target_ids, _ = self._determine_targets(nested_item, relations, collection) potential_target_ids.update(new_target_ids) if not new_target_ids: all_uncovered_items.add(nested_item) From f067077c748abb374d160bd03d113e3abd5b8a48 Mon Sep 17 00:00:00 2001 From: jce <28319872+JasperCraeghs@users.noreply.github.com> Date: Thu, 25 Jul 2024 17:45:59 +0200 Subject: [PATCH 08/10] Document option 'recursiveintermediates' --- doc/usage.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/usage.rst b/doc/usage.rst index 9e78f8d1..36e37170 100644 --- a/doc/usage.rst +++ b/doc/usage.rst @@ -552,6 +552,12 @@ linked via the ``:intermediate:`` RQT-items: as the source item. In addition, all intermediates will be listed, regardless of their coverage status. This can be useful if you want to group target items per intermediate item *instead of per source item*. +:recursiveintermediates: *optional, *single argument* + + Expects a forward relation to recursively take nested intermediate items into account. The source item is only + covered if every single intermediate item in the chain is covered. This option is not compatible with the option + *intermediatetitle*. + .. _traceability_usage_2d_matrix: -------------------------------- From c9f9916d8189981636ed802fe4b9d72f07f03cab Mon Sep 17 00:00:00 2001 From: jce <28319872+JasperCraeghs@users.noreply.github.com> Date: Thu, 1 Aug 2024 12:05:15 +0200 Subject: [PATCH 09/10] Clarify how dummy requirements are covered --- doc/integration_test_report.rst | 3 +-- doc/requirements.rst | 8 ++++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/doc/integration_test_report.rst b/doc/integration_test_report.rst index 2e7785f4..961c249a 100755 --- a/doc/integration_test_report.rst +++ b/doc/integration_test_report.rst @@ -163,8 +163,7 @@ Integration tests .. item:: ITEST-r100 Test a requirement using the ``requirement`` type :validates: r100 -.. item:: ITEST-DUMMY_CHILD Test of dummy requirement - :validates: RQT-DUMMY_CHILD +.. item:: ITEST-DUMMY_CHILD Test of a child dummy requirement Integration test reports ======================== diff --git a/doc/requirements.rst b/doc/requirements.rst index 88badb84..48ed1678 100755 --- a/doc/requirements.rst +++ b/doc/requirements.rst @@ -96,12 +96,12 @@ Requirements for mlx.traceability The plugin shall be optimized for performance to minimize its impact on the documentation's build time. For example, unneeded sorting should be avoided. -.. item:: RQT-DUMMY Dummy requirement that is not covered by a test +.. item:: RQT-DUMMY_PARENT Dummy requirement that is not covered by a test :fulfilled_by: DESIGN-ATTRIBUTES DESIGN-ITEMIZE -.. item:: RQT-DUMMY_CHILD Child of the dummy requirement that is not covered by a test - :depends_on: RQT-DUMMY -.. :validated_by: ITEST-DUMMY_CHILD +.. item:: RQT-DUMMY_CHILD Child of the uncovered dummy requirement + :depends_on: RQT-DUMMY_PARENT + :validated_by: ITEST-DUMMY_CHILD ------------------- Traceability matrix From 6ff118d679ac3123ac394215a642b31149b499a5 Mon Sep 17 00:00:00 2001 From: jce <28319872+JasperCraeghs@users.noreply.github.com> Date: Thu, 1 Aug 2024 12:05:47 +0200 Subject: [PATCH 10/10] Improve wording --- doc/integration_test_report.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/integration_test_report.rst b/doc/integration_test_report.rst index 961c249a..a8c24287 100755 --- a/doc/integration_test_report.rst +++ b/doc/integration_test_report.rst @@ -510,7 +510,7 @@ Traceability via intermediate items :coveredintermediates: :coverage: <79 -.. warning due to below item-matrix: coverage is not 100% due to child RQT not covered +.. warning in below item-matrix: coverage is not 100% due to child RQT not covered .. item-matrix:: Design to test via requirements recursively :source: DESIGN-