Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

item-matrix: Support recursion via intermediates #383

Merged
merged 11 commits into from
Aug 1, 2024
Merged
20 changes: 20 additions & 0 deletions doc/integration_test_report.rst
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ Integration tests
.. item:: ITEST-r100 Test a requirement using the ``requirement`` type
:validates: r100

.. item:: ITEST-DUMMY_CHILD Test of a child dummy requirement

Integration test reports
========================

Expand Down Expand Up @@ -506,6 +508,24 @@ Traceability via intermediate items
:nocaptions:
:stats:
:coveredintermediates:
:coverage: <79

.. 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-
:intermediate: RQT-
:target: UTEST ITEST
:sourcetitle: design items
:targettitle: unit tests, integration tests
:type: fulfills | validated_by
:group: top
:nocaptions:
:stats:
:coveredintermediates:
:splitintermediates:
:recursiveintermediates: impacts_on
:coverage: ==100

Source and target columns
-------------------------
Expand Down
6 changes: 5 additions & 1 deletion doc/requirements.rst
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,13 @@ 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 uncovered dummy requirement
:depends_on: RQT-DUMMY_PARENT
:validated_by: ITEST-DUMMY_CHILD

-------------------
Traceability matrix
-------------------
Expand Down
6 changes: 6 additions & 0 deletions doc/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,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:

--------------------------------
Expand Down
46 changes: 37 additions & 9 deletions mlx/traceability/directives/item_matrix_directive.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,39 +344,61 @@ 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)
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))
target_ids, uncovered_items = self._determine_targets(intermediate_item, links_with_relations[1],
collection)
potential_target_ids.update(target_ids)
# apply :target: filter
actual_targets = []
for target_ids in targets_with_ids:
linked_target_ids = potential_target_ids.intersection(target_ids)
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, 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 relation for recursion.
"""
all_uncovered_items = set()
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, relations, 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):
""" Extends given mapping with target IDs per target as value for each source ID as key
Expand Down Expand Up @@ -613,6 +635,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,
Expand Down Expand Up @@ -649,6 +672,7 @@ def run(self):
'intermediatetitle': {'default': ''},
'type': {'default': ''},
'sourcetype': {'default': []},
'recursiveintermediates': {'default': ''},
'coverage': {'default': ''},
},
)
Expand Down Expand Up @@ -701,6 +725,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
Expand Down
17 changes: 9 additions & 8 deletions mlx/traceability/traceable_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -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. '''
Expand Down
4 changes: 2 additions & 2 deletions tools/doc-warnings.json
Original file line number Diff line number Diff line change
@@ -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"
Expand Down