Skip to content

Commit 97af3e1

Browse files
committed
feat(backend/sdoc_source_code): Support merge by MID
Until now, finding a static merge candidate for source nodes relied on having a UID field set at source code side. Now we can also use MIDs for this purpose. Merge by MID gets priority over merge merge by UID. This requires thinking about several edge cases. See test desriptions for some of them.
1 parent 6b44b62 commit 97af3e1

File tree

25 files changed

+624
-34
lines changed

25 files changed

+624
-34
lines changed

strictdoc/core/file_traceability_index.py

Lines changed: 84 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
from strictdoc.helpers.cast import assert_cast
4848
from strictdoc.helpers.exception import StrictDocException
4949
from strictdoc.helpers.google_test import convert_function_name_to_gtest_macro
50+
from strictdoc.helpers.mid import MID
5051
from strictdoc.helpers.ordered_set import OrderedSet
5152

5253
if TYPE_CHECKING:
@@ -601,15 +602,36 @@ def validate_and_resolve(
601602
continue
602603

603604
assert source_node_.entity_name is not None
605+
sdoc_node = None
604606
sdoc_node_uid = source_node_.get_sdoc_field(
605607
"UID", relevant_source_node_entry
606608
)
607-
if sdoc_node_uid is None:
608-
sdoc_node_uid = f"{document_uid}/{path_to_source_file_}/{source_node_.entity_name}"
609-
sdoc_node = traceability_index.get_node_by_uid_weak(
610-
sdoc_node_uid
609+
mid = source_node_.get_sdoc_field(
610+
"MID", relevant_source_node_entry
611611
)
612612

613+
# First merge criterion: Merge if SDoc node with same MID exists.
614+
if mid is not None:
615+
sdoc_node_mid = MID(mid)
616+
merge_candidate_sdoc_node = (
617+
traceability_index.get_node_by_mid_weak(sdoc_node_mid)
618+
)
619+
if isinstance(merge_candidate_sdoc_node, SDocNode):
620+
sdoc_node = merge_candidate_sdoc_node
621+
sdoc_node_uid = sdoc_node.reserved_uid
622+
623+
if sdoc_node is None:
624+
# If no UID from source code field or merge-by-MID, create UID by conventional scheme.
625+
if sdoc_node_uid is None:
626+
sdoc_node_uid = f"{document_uid}/{path_to_source_file_}/{source_node_.entity_name}"
627+
# Second merge criterion: Merge if SDoc node with same UID exists.
628+
tmp_sdoc_node = traceability_index.get_node_by_uid_weak(
629+
sdoc_node_uid
630+
)
631+
if isinstance(tmp_sdoc_node, SDocNode):
632+
sdoc_node = tmp_sdoc_node
633+
634+
assert sdoc_node_uid is not None
613635
if sdoc_node is not None:
614636
sdoc_node = assert_cast(sdoc_node, SDocNode)
615637
self.merge_sdoc_node_with_source_node(
@@ -626,11 +648,6 @@ def validate_and_resolve(
626648
document,
627649
)
628650
sdoc_node_uid = assert_cast(sdoc_node.reserved_uid, str)
629-
traceability_index.graph_database.create_link(
630-
link_type=GraphLinkType.UID_TO_NODE,
631-
lhs_node=sdoc_node_uid,
632-
rhs_node=sdoc_node,
633-
)
634651
if current_top_node is None:
635652
current_top_node = (
636653
FileTraceabilityIndex.create_source_node_section(
@@ -998,6 +1015,25 @@ def merge_sdoc_node_with_source_node(
9981015
)
9991016
# Merge strategy: overwrite any field if there's a field with same name from custom tags.
10001017
sdoc_node_fields = source_node.get_sdoc_fields(source_node_config_entry)
1018+
1019+
# Sanity check: Nor UID neither MID must conflict (early auto-MID is allowed to be overwritten)
1020+
if (
1021+
"MID" in sdoc_node.ordered_fields_lookup
1022+
and "MID" in sdoc_node_fields
1023+
):
1024+
sdoc_mid_field = sdoc_node.get_field_by_name("MID").get_text_value()
1025+
if sdoc_mid_field != sdoc_node_fields["MID"]:
1026+
raise StrictDocException(
1027+
f"Can't merge node by UID {sdoc_node.reserved_uid}: "
1028+
f"Conflicting MID: {sdoc_mid_field} != {sdoc_node_fields['MID']}"
1029+
)
1030+
if sdoc_node.reserved_uid is not None and "UID" in sdoc_node_fields:
1031+
if sdoc_node.reserved_uid != sdoc_node_fields["UID"]:
1032+
raise StrictDocException(
1033+
f"Can't merge node by MID {sdoc_node.reserved_mid}: "
1034+
f"Conflicting UID: {sdoc_node.reserved_uid} != {sdoc_node_fields['UID']}"
1035+
)
1036+
10011037
FileTraceabilityIndex.set_sdoc_node_fields(sdoc_node, sdoc_node_fields)
10021038

10031039
@staticmethod
@@ -1081,6 +1117,45 @@ def connect_source_node_requirements(
10811117
10821118
Here we link REQ and sdoc_node bidirectional.
10831119
"""
1120+
if (
1121+
sdoc_node.reserved_uid is not None
1122+
and not traceability_index.graph_database.has_link(
1123+
link_type=GraphLinkType.UID_TO_NODE,
1124+
lhs_node=sdoc_node.reserved_uid,
1125+
rhs_node=sdoc_node,
1126+
)
1127+
):
1128+
traceability_index.graph_database.create_link(
1129+
link_type=GraphLinkType.UID_TO_NODE,
1130+
lhs_node=sdoc_node.reserved_uid,
1131+
rhs_node=sdoc_node,
1132+
)
1133+
1134+
# A merge procedure may have overwritten the MID,
1135+
# in which case the graph database and search index needs an update.
1136+
if "MID" in sdoc_node.ordered_fields_lookup != sdoc_node.reserved_mid:
1137+
sdoc_mid_field = sdoc_node.get_field_by_name("MID").get_text_value()
1138+
if sdoc_mid_field != sdoc_node.reserved_mid:
1139+
# TODO:
1140+
# If we really want to support changing the auto-assigned MID,
1141+
# at least the graph database and the document search index need an update (remove old MID, add new MID).
1142+
# I currently struggle to update the search index.
1143+
parent_document = sdoc_node.get_parent_or_including_document()
1144+
sdoc_node.reserved_mid = MID(sdoc_mid_field)
1145+
if parent_document.config.enable_mid:
1146+
sdoc_node.mid_permanent = True
1147+
1148+
if not traceability_index.graph_database.has_link(
1149+
link_type=GraphLinkType.MID_TO_NODE,
1150+
lhs_node=sdoc_node.reserved_mid,
1151+
rhs_node=sdoc_node,
1152+
):
1153+
traceability_index.graph_database.create_link(
1154+
link_type=GraphLinkType.MID_TO_NODE,
1155+
lhs_node=sdoc_node.reserved_mid,
1156+
rhs_node=sdoc_node,
1157+
)
1158+
10841159
for marker_ in source_node.markers:
10851160
if not isinstance(marker_, FunctionRangeMarker):
10861161
continue
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ ELEMENTS:
3939
TITLE: Merge example.c into static nodes
4040

4141
[REQUIREMENT]
42-
UID: SRC-NODES-BASE/src/example/example.c/example_1
42+
UID: SRC-NODES-BASE/src/example.c/example_1
4343
TITLE: TITLE from sdoc
4444
FOO: FOO text from sdoc
4545
BAR: BAR text from sdoc
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#
2+
# This test verifies that a source nodes is merged with a static SDoc node if
3+
# - the source nodes is not marked up with a UID field (i.e., default UID is effective), and
4+
# - static SDoc node was explicitly given the default UID.
5+
#
6+
# @relation(SDOC-SRS-141, scope=file)
7+
#
8+
9+
RUN: %strictdoc --debug export %S --output-dir %T | filecheck %s
10+
11+
CHECK: Published: Hello world doc
12+
13+
RUN: %check_exists --file "%T/html/_source_files/src/example.c.html"
14+
15+
RUN: %cat %T/html/%THIS_TEST_FOLDER/source_node_base.html | filecheck %s --check-prefix CHECK-HTML
16+
CHECK-HTML: Requirements from Source Nodes
17+
CHECK-HTML: SRC-NODES-BASE/src/example.c/example_1
18+
CHECK-HTML: TITLE from sdoc
19+
CHECK-HTML: class="requirement__link-parent" href="../30_merge_with_sdoc_by_default_uid/parent.html#REQ-1"
20+
CHECK-HTML: src/example.c, <i>lines: 3-14</i>, function example_1()
21+
CHECK-HTML-NOT: FOO text from sdoc
22+
CHECK-HTML: FOO text from example.c
23+
CHECK-HTML-NOT: BAR text from sdoc
24+
CHECK-HTML: BAR text from example.c
25+
26+
RUN: %cat %T/html/_source_files/src/example.c.html | filecheck %s --check-prefix CHECK-SOURCE-FILE
27+
CHECK-SOURCE-FILE: SRC-NODES-BASE/src/example.c/example_1
28+
29+
RUN: %cat %T/html/source_coverage.html | filecheck %s --check-prefix CHECK-SOURCE-COVERAGE
30+
CHECK-SOURCE-COVERAGE: 100.0

tests/integration/features/source_code_traceability/_source_nodes/30_merge_with_static_node/test.itest

Lines changed: 0 additions & 24 deletions
This file was deleted.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[DOCUMENT]
2+
TITLE: Hello world doc
3+
4+
[REQUIREMENT]
5+
UID: REQ-1
6+
TITLE: Requirement Title
7+
STATEMENT: Requirement Statement
8+
9+
[REQUIREMENT]
10+
UID: REQ-2
11+
TITLE: Requirement Title #2
12+
STATEMENT: Requirement Statement #2
13+
14+
[REQUIREMENT]
15+
UID: REQ-3
16+
TITLE: Requirement Title #3
17+
STATEMENT: Requirement Statement #3
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
[DOCUMENT]
2+
MID: c2d4542d5f1741c88dfcb4f68ad7dcbd
3+
TITLE: Requirements from Source Nodes
4+
UID: SRC-NODES-BASE
5+
6+
[GRAMMAR]
7+
ELEMENTS:
8+
- TAG: SECTION
9+
PROPERTIES:
10+
IS_COMPOSITE: True
11+
FIELDS:
12+
- TITLE: UID
13+
TYPE: String
14+
REQUIRED: False
15+
- TITLE: TITLE
16+
TYPE: String
17+
REQUIRED: True
18+
- TAG: REQUIREMENT
19+
PROPERTIES:
20+
VIEW_STYLE: Narrative
21+
FIELDS:
22+
- TITLE: UID
23+
TYPE: String
24+
REQUIRED: False
25+
- TITLE: MID
26+
TYPE: String
27+
REQUIRED: False
28+
- TITLE: TITLE
29+
TYPE: String
30+
REQUIRED: False
31+
- TITLE: FOO
32+
TYPE: String
33+
REQUIRED: False
34+
- TITLE: BAR
35+
TYPE: String
36+
REQUIRED: False
37+
RELATIONS:
38+
- TYPE: Parent
39+
- TYPE: File
40+
41+
[[SECTION]]
42+
TITLE: Merge example.c into static nodes
43+
44+
[REQUIREMENT]
45+
UID: REQ-SOURCE-1
46+
TITLE: TITLE1 from sdoc
47+
FOO: FOO1 text from sdoc
48+
BAR: BAR1 text from sdoc
49+
RELATIONS:
50+
- TYPE: Parent
51+
VALUE: REQ-1
52+
53+
[REQUIREMENT]
54+
UID: REQ-SOURCE-2
55+
MID: 80cd685d-0e18-44b8-9842-c1863a2eb9ec
56+
TITLE: TITLE2 from sdoc
57+
FOO: FOO2 text from sdoc
58+
BAR: BAR2 text from sdoc
59+
RELATIONS:
60+
- TYPE: Parent
61+
VALUE: REQ-2
62+
63+
[REQUIREMENT]
64+
UID: REQ-SOURCE-3
65+
TITLE: TITLE3 from sdoc
66+
FOO: FOO3 text from sdoc
67+
BAR: BAR3 text from sdoc
68+
RELATIONS:
69+
- TYPE: Parent
70+
VALUE: REQ-3
71+
72+
[[/SECTION]]
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
#include <stdio.h>
2+
3+
/**
4+
* Some text.
5+
*
6+
* @relation(REQ-1, scope=function)
7+
*
8+
* UID: REQ-SOURCE-1
9+
*
10+
* FOO: FOO1 text from example.c
11+
*
12+
* BAR: BAR1 text from example.c
13+
*/
14+
void example_1(void) {
15+
print("hello world\n");
16+
}
17+
18+
/**
19+
* Some text.
20+
*
21+
* @relation(REQ-2, scope=function)
22+
*
23+
* UID: REQ-SOURCE-2
24+
*
25+
* FOO: FOO2 text from example.c
26+
*
27+
* BAR: BAR2 text from example.c
28+
*/
29+
void example_2(void) {
30+
print("hello world\n");
31+
}
32+
33+
/**
34+
* Some text.
35+
*
36+
* @relation(REQ-3, scope=function)
37+
*
38+
* UID: REQ-SOURCE-3
39+
*
40+
* MID: 1973a567-a109-491d-b7f0-6bb22eafa6ab
41+
*
42+
* FOO: FOO3 text from example.c
43+
*
44+
* BAR: BAR3 text from example.c
45+
*/
46+
void example_3(void) {
47+
print("hello world\n");
48+
}

0 commit comments

Comments
 (0)