diff --git a/strictdoc/backend/sdoc_source_code/marker_parser.py b/strictdoc/backend/sdoc_source_code/marker_parser.py index 0d7e33187..7798b11a5 100644 --- a/strictdoc/backend/sdoc_source_code/marker_parser.py +++ b/strictdoc/backend/sdoc_source_code/marker_parser.py @@ -20,6 +20,7 @@ RangeMarker, ) from strictdoc.backend.sdoc_source_code.models.requirement_marker import Req +from strictdoc.backend.sdoc_source_code.models.source_location import ByteRange from strictdoc.backend.sdoc_source_code.models.source_node import SourceNode @@ -31,6 +32,7 @@ def parse( line_start: int, line_end: int, comment_line_start: int, + comment_byte_range: Optional[ByteRange], entity_name: Optional[str] = None, col_offset: int = 0, custom_tags: Optional[set[str]] = None, @@ -50,8 +52,11 @@ def parse( """ node_fields: Dict[str, str] = {} - source_node: SourceNode = SourceNode(entity_name) + source_node: SourceNode = SourceNode( + entity_name=entity_name, + comment_byte_range=comment_byte_range, + ) input_string = preprocess_source_code_comment(input_string) tree: ParseTree = MarkerLexer.parse( @@ -78,6 +83,11 @@ def parse( element_, ) node_fields[node_name] = node_value + + source_node.fields_locations[node_name] = ( + element_.meta.start_pos, + element_.meta.end_pos - 1, + ) else: raise AssertionError diff --git a/strictdoc/backend/sdoc_source_code/marker_writer.py b/strictdoc/backend/sdoc_source_code/marker_writer.py new file mode 100644 index 000000000..6626040fa --- /dev/null +++ b/strictdoc/backend/sdoc_source_code/marker_writer.py @@ -0,0 +1,34 @@ +from typing import Any + +from strictdoc.backend.sdoc_source_code.models.source_node import SourceNode + + +class MarkerWriter: + def write( + self, + source_node: SourceNode, + rewrites: dict[Any, bytes], + comment_file_bytes: bytes, + ) -> bytes: + output = bytearray() + prev_end = 0 + + for field_name_ in source_node.fields.keys(): + if field_name_ not in rewrites: + continue + + rewrite = rewrites[field_name_] + location = source_node.fields_locations[field_name_] + + output += comment_file_bytes[prev_end : location[0]] + + output += bytes(field_name_, encoding="utf8") + b": " + output += rewrite + + prev_end = location[1] + + # Possible trailing whitespace after last token. + if prev_end < len(comment_file_bytes): + output += comment_file_bytes[prev_end:] + + return bytes(output) diff --git a/strictdoc/backend/sdoc_source_code/models/function.py b/strictdoc/backend/sdoc_source_code/models/function.py index 476d3c3eb..435262449 100644 --- a/strictdoc/backend/sdoc_source_code/models/function.py +++ b/strictdoc/backend/sdoc_source_code/models/function.py @@ -2,12 +2,13 @@ @relation(SDOC-SRS-142, scope=file) """ -from typing import Any, List, Set +from typing import Any, List, Optional, Set from strictdoc.backend.sdoc_source_code.constants import FunctionAttribute from strictdoc.backend.sdoc_source_code.models.function_range_marker import ( FunctionRangeMarker, ) +from strictdoc.backend.sdoc_source_code.models.source_location import ByteRange from strictdoc.helpers.auto_described import auto_described @@ -21,6 +22,7 @@ def __init__( display_name: str, line_begin: int, line_end: int, + code_byte_range: Optional[ByteRange], child_functions: List[Any], markers: List[FunctionRangeMarker], attributes: Set[FunctionAttribute], @@ -36,6 +38,11 @@ def __init__( self.markers: List[FunctionRangeMarker] = markers self.line_begin = line_begin self.line_end = line_end + + # Not all source code functions have ranges. + # Example: Robot framework files. + self.code_byte_range: Optional[ByteRange] = code_byte_range + self.attributes: Set[FunctionAttribute] = attributes def is_declaration(self) -> bool: diff --git a/strictdoc/backend/sdoc_source_code/models/source_location.py b/strictdoc/backend/sdoc_source_code/models/source_location.py new file mode 100644 index 000000000..f3a6e1f91 --- /dev/null +++ b/strictdoc/backend/sdoc_source_code/models/source_location.py @@ -0,0 +1,16 @@ +from dataclasses import dataclass + +from tree_sitter import Node + + +@dataclass +class ByteRange: + start: int + end: int + + @classmethod + def create_from_ts_node(cls, node: Node) -> "ByteRange": + return cls( + start=node.start_byte, + end=node.end_byte, + ) diff --git a/strictdoc/backend/sdoc_source_code/models/source_node.py b/strictdoc/backend/sdoc_source_code/models/source_node.py index 67dd8a980..936e50f29 100644 --- a/strictdoc/backend/sdoc_source_code/models/source_node.py +++ b/strictdoc/backend/sdoc_source_code/models/source_node.py @@ -13,16 +13,27 @@ from strictdoc.backend.sdoc_source_code.models.range_marker import ( RangeMarker, ) +from strictdoc.backend.sdoc_source_code.models.source_location import ByteRange from strictdoc.core.project_config import SourceNodesEntry -@dataclass +@dataclass(eq=False) class SourceNode: + """ + NOTE: eq=False is needed to make this dataclass support being a dictionary key. + + eq=False means that dictionaries will index by object identity. Copied + SourceNode objects will appear as two different SourceNodes. An alternative + could be to implement __eq__ and __hash__ so that they target byte_range. + """ + entity_name: Optional[str] + comment_byte_range: Optional[ByteRange] markers: List[Union[FunctionRangeMarker, RangeMarker, LineMarker]] = field( default_factory=list ) fields: dict[str, str] = field(default_factory=dict) + fields_locations: dict[str, tuple[int, int]] = field(default_factory=dict) function: Optional[Function] = None def get_sdoc_field( diff --git a/strictdoc/backend/sdoc_source_code/reader_c.py b/strictdoc/backend/sdoc_source_code/reader_c.py index 8b4408e3d..350265ca8 100644 --- a/strictdoc/backend/sdoc_source_code/reader_c.py +++ b/strictdoc/backend/sdoc_source_code/reader_c.py @@ -21,6 +21,7 @@ from strictdoc.backend.sdoc_source_code.models.source_file_info import ( SourceFileTraceabilityInfo, ) +from strictdoc.backend.sdoc_source_code.models.source_location import ByteRange from strictdoc.backend.sdoc_source_code.models.source_node import SourceNode from strictdoc.backend.sdoc_source_code.parse_context import ParseContext from strictdoc.backend.sdoc_source_code.processors.general_language_marker_processors import ( @@ -50,13 +51,6 @@ def read( ) -> SourceFileTraceabilityInfo: assert isinstance(input_buffer, bytes) - file_size = len(input_buffer) - - traceability_info = SourceFileTraceabilityInfo([]) - - if file_size == 0: - return traceability_info - file_stats = SourceFileStats.create(input_buffer) parse_context = ParseContext(file_path, file_stats) @@ -69,6 +63,8 @@ def read( tree = parser.parse(input_buffer) + traceability_info = SourceFileTraceabilityInfo([]) + nodes = traverse_tree(tree) source_node: Optional[SourceNode] @@ -93,6 +89,9 @@ def read( if input_buffer[-1] == 10 else node_.end_point[0] + 1, comment_line_start=node_.start_point[0] + 1, + comment_byte_range=ByteRange.create_from_ts_node( + comment_node + ), custom_tags=self.custom_tags, ) for marker_ in source_node.markers: @@ -192,6 +191,9 @@ def read( line_end=function_last_line, comment_line_start=function_comment_node.start_point[0] + 1, + comment_byte_range=ByteRange.create_from_ts_node( + function_comment_node + ), entity_name=function_display_name, custom_tags=self.custom_tags, ) @@ -216,6 +218,7 @@ def read( if function_comment_node is not None else node_.range.start_point[0] + 1, line_end=node_.range.end_point[0] + 1, + code_byte_range=ByteRange.create_from_ts_node(node_), child_functions=[], markers=function_markers, attributes=function_attributes, @@ -301,9 +304,13 @@ def read( line_end=function_last_line, comment_line_start=function_comment_node.start_point[0] + 1, + comment_byte_range=ByteRange.create_from_ts_node( + function_comment_node + ), entity_name=function_display_name, custom_tags=self.custom_tags, ) + traceability_info.source_nodes.append(source_node) for marker_ in source_node.markers: if isinstance(marker_, FunctionRangeMarker): @@ -322,6 +329,7 @@ def read( if function_comment_node is not None else node_.range.start_point[0] + 1, line_end=node_.range.end_point[0] + 1, + code_byte_range=ByteRange.create_from_ts_node(node_), child_functions=[], markers=function_markers, attributes={FunctionAttribute.DEFINITION}, @@ -358,6 +366,7 @@ def read( line_start=node_.start_point[0] + 1, line_end=node_.end_point[0] + 1, comment_line_start=node_.start_point[0] + 1, + comment_byte_range=ByteRange.create_from_ts_node(node_), custom_tags=None, ) diff --git a/strictdoc/backend/sdoc_source_code/reader_python.py b/strictdoc/backend/sdoc_source_code/reader_python.py index 37cf62e19..a26f20f36 100644 --- a/strictdoc/backend/sdoc_source_code/reader_python.py +++ b/strictdoc/backend/sdoc_source_code/reader_python.py @@ -21,6 +21,7 @@ RelationMarkerType, SourceFileTraceabilityInfo, ) +from strictdoc.backend.sdoc_source_code.models.source_location import ByteRange from strictdoc.backend.sdoc_source_code.parse_context import ParseContext from strictdoc.backend.sdoc_source_code.processors.general_language_marker_processors import ( function_range_marker_processor, @@ -72,6 +73,7 @@ def read( display_name="module", line_begin=node_.start_point[0] + 1, line_end=node_.end_point[0] + 1, + code_byte_range=ByteRange.create_from_ts_node(node_), child_functions=[], markers=[], attributes=set(), @@ -111,6 +113,9 @@ def read( else node_.end_point[0] + 1, comment_line_start=string_content.start_point[0] + 1, + comment_byte_range=ByteRange.create_from_ts_node( + string_content + ), ) for marker_ in source_node.markers: if isinstance(marker_, FunctionRangeMarker) and ( @@ -170,6 +175,9 @@ def read( line_end=node_.end_point[0] + 1, comment_line_start=string_content.start_point[0] + 1, + comment_byte_range=ByteRange.create_from_ts_node( + string_content + ), entity_name=function_name, ) for marker_ in source_node.markers: @@ -201,6 +209,7 @@ def read( display_name=function_name, line_begin=node_.range.start_point[0] + 1, line_end=node_.range.end_point[0] + 1, + code_byte_range=ByteRange.create_from_ts_node(node_), child_functions=[], # Python functions do not need to track markers. markers=[], @@ -247,6 +256,9 @@ def read( line_start=node_.start_point[0] + 1, line_end=last_comment.end_point[0] + 1, comment_line_start=node_.start_point[0] + 1, + comment_byte_range=ByteRange( + node_.start_byte, last_comment.end_byte + ), entity_name=None, ) for marker_ in source_node.markers: diff --git a/strictdoc/backend/sdoc_source_code/reader_robot.py b/strictdoc/backend/sdoc_source_code/reader_robot.py index c10fc5b70..920748791 100644 --- a/strictdoc/backend/sdoc_source_code/reader_robot.py +++ b/strictdoc/backend/sdoc_source_code/reader_robot.py @@ -98,6 +98,8 @@ def visit_TestCase(self, node: TestCase) -> None: input_string=token.value, line_start=token.lineno, line_end=token.lineno, + # FIXME: Byte range is currently not used for Robot framework. + comment_byte_range=None, comment_line_start=token.lineno, entity_name=node.name, col_offset=token.col_offset, @@ -125,6 +127,8 @@ def visit_TestCase(self, node: TestCase) -> None: display_name=node.name, line_begin=node.lineno, line_end=node.end_lineno - trailing_empty_lines, + # FIXME: Byte range is currently not used for Robot framework. + code_byte_range=None, child_functions=[], markers=function_markers, attributes={FunctionAttribute.DEFINITION}, @@ -139,6 +143,8 @@ def _visit_possibly_marked_node( input_string=token.value, line_start=node.lineno, line_end=node.lineno, + # FIXME: Byte range is currently not used for Robot framework. + comment_byte_range=None, comment_line_start=node.lineno, entity_name=None, col_offset=token.col_offset, diff --git a/strictdoc/backend/sdoc_source_code/source_writer.py b/strictdoc/backend/sdoc_source_code/source_writer.py new file mode 100644 index 000000000..f629a4e7e --- /dev/null +++ b/strictdoc/backend/sdoc_source_code/source_writer.py @@ -0,0 +1,37 @@ +from strictdoc.backend.sdoc_source_code.models.source_file_info import ( + SourceFileTraceabilityInfo, +) +from strictdoc.backend.sdoc_source_code.models.source_node import SourceNode + + +class SourceWriter: + def write( + self, + trace_info: SourceFileTraceabilityInfo, + rewrites: dict[SourceNode, bytes], + file_bytes: bytes, + ) -> bytes: + output = bytearray() + prev_end = 0 + + for source_node_ in trace_info.source_nodes: + if source_node_.comment_byte_range is None: + continue + + if source_node_ not in rewrites: + continue + + rewrite = rewrites[source_node_] + output += file_bytes[ + prev_end : source_node_.comment_byte_range.start + ] + + output += rewrite + + prev_end = source_node_.comment_byte_range.end + + # Possible trailing whitespace after last token. + if prev_end < len(file_bytes): + output += file_bytes[prev_end:] + + return bytes(output) diff --git a/strictdoc/cli/main.py b/strictdoc/cli/main.py index b13248048..1770bc766 100644 --- a/strictdoc/cli/main.py +++ b/strictdoc/cli/main.py @@ -6,6 +6,7 @@ import multiprocessing import os import sys +from pathlib import Path from typing import Optional strictdoc_root_path = os.path.abspath( @@ -115,9 +116,13 @@ def _main_internal(parallelizer: Parallelizer, parser: SDocArgsParser) -> None: project_config = ProjectConfigLoader.load_from_path_or_get_default( path_to_config=manage_config.get_path_to_config(), ) - # FIXME: This must be improved. + + # FIXME: Encapsulate all this in project_config.integrate_manage_autouid_config(), + # following the example of integrate_export_config(). project_config.input_paths = [manage_config.input_path] - # FIXME: This must be improved. + project_config.source_root_path = str( + Path(manage_config.input_path).resolve() + ) project_config.auto_uid_mode = True project_config.autouuid_include_sections = ( manage_config.include_sections diff --git a/strictdoc/commands/manage_autouid_command.py b/strictdoc/commands/manage_autouid_command.py index 496790bf7..299ff57a1 100644 --- a/strictdoc/commands/manage_autouid_command.py +++ b/strictdoc/commands/manage_autouid_command.py @@ -2,6 +2,11 @@ from strictdoc.backend.sdoc.errors.document_tree_error import DocumentTreeError from strictdoc.backend.sdoc.writer import SDWriter +from strictdoc.backend.sdoc_source_code.marker_writer import MarkerWriter +from strictdoc.backend.sdoc_source_code.models.source_file_info import ( + SourceFileTraceabilityInfo, +) +from strictdoc.backend.sdoc_source_code.source_writer import SourceWriter from strictdoc.core.analyzers.document_stats import ( DocumentStats, DocumentTreeStats, @@ -11,11 +16,30 @@ from strictdoc.core.traceability_index import TraceabilityIndex from strictdoc.core.traceability_index_builder import TraceabilityIndexBuilder from strictdoc.helpers.parallelizer import Parallelizer +from strictdoc.helpers.sha256 import get_random_sha256, get_sha256 from strictdoc.helpers.string import ( create_safe_acronym, ) +def generate_code_hash( + *, project: bytes, file_path: bytes, instance: bytes, code: bytes +) -> bytes: + """ + Generate hash for drift detection as suggested by Linux kernel requirements template: + + "${PROJECT}${FILE_PATH}${INSTANCE}${CODE}" | sha256sum". + """ + + assert isinstance(project, bytes) + assert isinstance(file_path, bytes) + assert isinstance(instance, bytes) + assert isinstance(code, bytes) + + hash_input = project + file_path + instance + code + return bytes(get_sha256(hash_input), encoding="utf8") + + class ManageAutoUIDCommand: @staticmethod def execute( @@ -91,3 +115,92 @@ def execute( document_meta.input_doc_full_path, "w", encoding="utf8" ) as output_file: output_file.write(document_content) + + for ( + trace_info_ + ) in traceability_index.get_file_traceability_index().trace_infos: + ManageAutoUIDCommand._rewrite_source_file( + trace_info_, project_config + ) + + @staticmethod + def _rewrite_source_file( + trace_info: SourceFileTraceabilityInfo, project_config: ProjectConfig + ) -> None: + """ + NOTE: This only updates the source code with the new calculated value. + All links in the graph database and links in the search index are + not modified for now. + """ + + assert trace_info.source_file is not None + # FIXME: These conditions for skipping the writes may be insufficient. + if ( + not trace_info.source_file.in_doctree_source_file_rel_path_posix.endswith( + ".c" + ) + or not trace_info.source_file.is_referenced + ): + return + + with open(trace_info.source_file.full_path, "rb") as source_file_: + file_bytes = source_file_.read() + + rewrites = {} + for source_node_ in trace_info.source_nodes: + function = source_node_.function + if function is None or function.code_byte_range is None: + continue + + # Not all source readers create rewritable byte ranges. There is + # nothing to rewrite for such nodes. Skipping them here. + if source_node_.comment_byte_range is None: + continue + + # FILE_PATH: The file the code resides in, relative to the root of the project repository. + file_path = bytes( + trace_info.source_file.in_doctree_source_file_rel_path_posix, + encoding="utf8", + ) + + # INSTANCE: The requirement template instance, minus tags with hash strings. + instance_bytes = bytearray() + for field_name_, field_value_ in source_node_.fields.items(): + if field_name_ in ("SPDX-Req-ID", "SPDX-Req-HKey"): + continue + instance_bytes += bytes(field_value_, encoding="utf8") + + # CODE: The code that the SPDX-Req applies to. + code = file_bytes[ + function.code_byte_range.start : function.code_byte_range.end + ] + + # This is important for Windows. Otherwise, the hash key will be calculated incorrectly. + code = code.replace(b"\r\n", b"\n") + + hash_spdx_id = bytes(get_random_sha256(), encoding="utf8") + hash_spdx_hash = generate_code_hash( + project=bytes(project_config.project_title, encoding="utf8"), + file_path=file_path, + instance=bytes(instance_bytes), + code=code, + ) + patched_node = MarkerWriter().write( + source_node_, + rewrites={ + "SPDX-Req-ID": hash_spdx_id, + "SPDX-Req-HKey": hash_spdx_hash, + }, + comment_file_bytes=file_bytes[ + source_node_.comment_byte_range.start : source_node_.comment_byte_range.end + ], + ) + rewrites[source_node_] = patched_node + + source_writer = SourceWriter() + output_string = source_writer.write( + trace_info, rewrites=rewrites, file_bytes=file_bytes + ) + + with open(trace_info.source_file.full_path, "wb") as source_file_: + source_file_.write(output_string) diff --git a/strictdoc/core/project_config.py b/strictdoc/core/project_config.py index 1aa0197d5..911ac9e88 100644 --- a/strictdoc/core/project_config.py +++ b/strictdoc/core/project_config.py @@ -193,6 +193,7 @@ def __init__( if source_root_path is not None: assert os.path.isdir(source_root_path), source_root_path assert os.path.isabs(source_root_path), source_root_path + self.source_root_path: Optional[str] = source_root_path self.include_source_paths: List[str] = ( @@ -463,8 +464,10 @@ def get_relevant_source_nodes_entry( Returns data for the first entry from source_nodes that is a parent path of path_to_file. If path_to_file is absolute, source node config entries are assumed to be in the source_root_path. """ - if self.source_root_path is None: - return None + + source_root_path = self.source_root_path + assert source_root_path is not None + assert os.path.exists(source_root_path), source_root_path source_file_path = Path(path_to_file) for sdoc_source_config_entry_ in self.source_nodes: @@ -472,7 +475,7 @@ def get_relevant_source_nodes_entry( # class when it is implemented. if sdoc_source_config_entry_.full_path is None: sdoc_source_config_entry_.full_path = Path( - self.source_root_path + source_root_path ) / Path(sdoc_source_config_entry_.path) if source_file_path.is_absolute(): diff --git a/strictdoc/helpers/sha256.py b/strictdoc/helpers/sha256.py index 615280bef..61889ed61 100644 --- a/strictdoc/helpers/sha256.py +++ b/strictdoc/helpers/sha256.py @@ -1,6 +1,13 @@ import hashlib +import uuid def get_sha256(byte_array: bytes) -> str: readable_hash = hashlib.sha256(byte_array).hexdigest() return readable_hash + + +def get_random_sha256() -> str: + uid = uuid.uuid4() + random_hash = hashlib.sha256(uid.bytes).hexdigest() + return random_hash diff --git a/tests/integration/features/source_code_traceability/_source_nodes/_auto_uuid/10_linux_spdx_req_id_autogen/parent.sdoc b/tests/integration/features/source_code_traceability/_source_nodes/_auto_uuid/10_linux_spdx_req_id_autogen/parent.sdoc new file mode 100644 index 000000000..278b98a77 --- /dev/null +++ b/tests/integration/features/source_code_traceability/_source_nodes/_auto_uuid/10_linux_spdx_req_id_autogen/parent.sdoc @@ -0,0 +1,12 @@ +[DOCUMENT] +TITLE: Hello world doc + +[REQUIREMENT] +UID: REQ-1 +TITLE: Requirement Title +STATEMENT: Requirement Statement + +[REQUIREMENT] +UID: REQ-2 +TITLE: Requirement Title #2 +STATEMENT: Requirement Statement #2 diff --git a/tests/integration/features/source_code_traceability/_source_nodes/_auto_uuid/10_linux_spdx_req_id_autogen/requirements.sdoc b/tests/integration/features/source_code_traceability/_source_nodes/_auto_uuid/10_linux_spdx_req_id_autogen/requirements.sdoc new file mode 100644 index 000000000..2f1fca39d --- /dev/null +++ b/tests/integration/features/source_code_traceability/_source_nodes/_auto_uuid/10_linux_spdx_req_id_autogen/requirements.sdoc @@ -0,0 +1,52 @@ +[DOCUMENT] +MID: c2d4542d5f1741c88dfcb4f68ad7dcbd +TITLE: SPDX requirements +UID: SPDX_DOC + +[GRAMMAR] +ELEMENTS: +- TAG: TEXT + FIELDS: + - TITLE: UID + TYPE: String + REQUIRED: False + - TITLE: STATEMENT + TYPE: String + REQUIRED: True +- TAG: SECTION + PROPERTIES: + IS_COMPOSITE: True + FIELDS: + - TITLE: UID + TYPE: String + REQUIRED: False + - TITLE: TITLE + TYPE: String + REQUIRED: True +- TAG: REQUIREMENT + PROPERTIES: + VIEW_STYLE: Narrative + FIELDS: + - TITLE: UID + TYPE: String + REQUIRED: False + - TITLE: TITLE + TYPE: String + REQUIRED: False + - TITLE: SPDX-Req-ID + TYPE: String + REQUIRED: False + - TITLE: SPDX-Req-HKey + TYPE: String + REQUIRED: False + - TITLE: SPDX-Text + TYPE: String + REQUIRED: False + RELATIONS: + - TYPE: Parent + - TYPE: File + +[[SECTION]] +TITLE: Introduction + +[[/SECTION]] diff --git a/tests/integration/features/source_code_traceability/_source_nodes/_auto_uuid/10_linux_spdx_req_id_autogen/src/example/example.c b/tests/integration/features/source_code_traceability/_source_nodes/_auto_uuid/10_linux_spdx_req_id_autogen/src/example/example.c new file mode 100644 index 000000000..43b853249 --- /dev/null +++ b/tests/integration/features/source_code_traceability/_source_nodes/_auto_uuid/10_linux_spdx_req_id_autogen/src/example/example.c @@ -0,0 +1,20 @@ +#include + +/** + * Some text. + * + * @relation(REQ-1, scope=function) + * + * SPDX-Req-ID: SRC-1 + * + * SPDX-Req-HKey: TBD + * + * SPDX-Text: This + * is + * a statement + * \n\n + * And this is the same statement's another paragraph. + */ +void example_1(void) { + print("hello world\n"); +} diff --git a/tests/integration/features/source_code_traceability/_source_nodes/_auto_uuid/10_linux_spdx_req_id_autogen/src/example/example_no_spdx.c b/tests/integration/features/source_code_traceability/_source_nodes/_auto_uuid/10_linux_spdx_req_id_autogen/src/example/example_no_spdx.c new file mode 100644 index 000000000..a340a01ec --- /dev/null +++ b/tests/integration/features/source_code_traceability/_source_nodes/_auto_uuid/10_linux_spdx_req_id_autogen/src/example/example_no_spdx.c @@ -0,0 +1,10 @@ +#include + +/** + * Some text. + * + * @relation(REQ-1, scope=function) + */ +void example_1(void) { + print("hello world\n"); +} diff --git a/tests/integration/features/source_code_traceability/_source_nodes/_auto_uuid/10_linux_spdx_req_id_autogen/strictdoc.toml b/tests/integration/features/source_code_traceability/_source_nodes/_auto_uuid/10_linux_spdx_req_id_autogen/strictdoc.toml new file mode 100644 index 000000000..58d15585c --- /dev/null +++ b/tests/integration/features/source_code_traceability/_source_nodes/_auto_uuid/10_linux_spdx_req_id_autogen/strictdoc.toml @@ -0,0 +1,14 @@ +[project] + +features = [ + "REQUIREMENT_TO_SOURCE_TRACEABILITY", + "SOURCE_FILE_LANGUAGE_PARSERS", +] + +source_nodes = [ + { "src/" = { uid = "SPDX_DOC", node_type = "REQUIREMENT" } } +] + +exclude_source_paths = [ + "test.itest" +] diff --git a/tests/integration/features/source_code_traceability/_source_nodes/_auto_uuid/10_linux_spdx_req_id_autogen/test.itest b/tests/integration/features/source_code_traceability/_source_nodes/_auto_uuid/10_linux_spdx_req_id_autogen/test.itest new file mode 100644 index 000000000..483c4e505 --- /dev/null +++ b/tests/integration/features/source_code_traceability/_source_nodes/_auto_uuid/10_linux_spdx_req_id_autogen/test.itest @@ -0,0 +1,17 @@ +# +# @relation(SDOC-SRS-141, scope=file) +# + +RUN: cp -r %S/* %T/ +RUN: cd %T/ +RUN: %strictdoc manage auto-uid %T | filecheck %s + +CHECK: Reading source: src{{.}}example{{.}}example.c +CHECK: Total execution time + +# File with SPDX fields gets a stable hash generated. +RUN: cat %T/src/example/example.c | filecheck %s --check-prefix CHECK-SOURCE-FILE +CHECK-SOURCE-FILE: SPDX-Req-HKey: e83a7c0e382f18238a3ce4e9ca14444db37955b8b2332756d0b2826eb39ed381 + +# File without SPDX fields is not modified in any way. +RUN: %diff %S/src/example/example_no_spdx.c %T/src/example/example_no_spdx.c diff --git a/tests/unit/strictdoc/backend/sdoc_source_code/readers/test_writer_c.py b/tests/unit/strictdoc/backend/sdoc_source_code/readers/test_writer_c.py new file mode 100644 index 000000000..bf241d037 --- /dev/null +++ b/tests/unit/strictdoc/backend/sdoc_source_code/readers/test_writer_c.py @@ -0,0 +1,77 @@ +""" +@relation(SDOC-SRS-146, scope=file) +""" + +import sys + +import pytest + +from strictdoc.backend.sdoc_source_code.models.source_file_info import ( + SourceFileTraceabilityInfo, +) +from strictdoc.backend.sdoc_source_code.reader_c import ( + SourceFileTraceabilityReader_C, +) +from strictdoc.backend.sdoc_source_code.source_writer import SourceWriter + +pytestmark = pytest.mark.skipif( + sys.version_info < (3, 9), reason="Requires Python 3.9 or higher" +) + + +def test_02_c_functions(): + input_string = b"""\ +#include + +/** + * Some text. + * + * @relation(REQ-1, scope=function) + */ +void hello_world(void) { + print("hello world\\n"); +} + +/** + * Some text. + * + * @relation(REQ-2, scope=function) + */ +void hello_world_2(void) { + print("hello world\\n"); +} +""" + + reader = SourceFileTraceabilityReader_C() + + info: SourceFileTraceabilityInfo = reader.read( + input_string, file_path="foo.c" + ) + + assert isinstance(info, SourceFileTraceabilityInfo) + assert len(info.markers) == 2 + + rewrites = {} + for source_node_ in info.source_nodes: + rewrites[source_node_] = b"" + + source_writer = SourceWriter() + output_string = source_writer.write( + info, rewrites=rewrites, file_bytes=input_string + ) + + expected_output_string = b"""\ +#include + + +void hello_world(void) { + print("hello world\\n"); +} + + +void hello_world_2(void) { + print("hello world\\n"); +} +""" + + assert output_string == expected_output_string diff --git a/tests/unit/strictdoc/backend/sdoc_source_code/test_marker_parser.py b/tests/unit/strictdoc/backend/sdoc_source_code/test_marker_parser.py index 03d4566c0..582322936 100644 --- a/tests/unit/strictdoc/backend/sdoc_source_code/test_marker_parser.py +++ b/tests/unit/strictdoc/backend/sdoc_source_code/test_marker_parser.py @@ -31,6 +31,7 @@ def test_01_basic_nominal(): line_start=1, line_end=1, comment_line_start=1, + comment_byte_range=None, ) function_range = source_node.markers[0] assert isinstance(function_range, FunctionRangeMarker) @@ -53,6 +54,7 @@ def test_10_parses_with_leading_newlines(): line_start=1, line_end=5, comment_line_start=1, + comment_byte_range=None, ) function_range = source_node.markers[0] @@ -76,6 +78,7 @@ def test_11_parses_with_leading_whitespace(): line_start=1, line_end=3, comment_line_start=1, + comment_byte_range=None, ) function_range = source_node.markers[0] @@ -101,6 +104,7 @@ def test_20_parses_within_doxygen_comment(): line_start=1, line_end=5, comment_line_start=1, + comment_byte_range=None, ) function_range = source_node.markers[0] @@ -127,6 +131,7 @@ def test_21_parses_within_doxygen_comment_two_markers(): line_start=1, line_end=6, comment_line_start=1, + comment_byte_range=None, ) function_range = source_node.markers[0] @@ -152,6 +157,7 @@ def test_22_parses_within_doxygen_comment_curly_braces(): line_start=1, line_end=5, comment_line_start=1, + comment_byte_range=None, ) function_range = source_node.markers[0] @@ -188,6 +194,7 @@ def test_23_parses_within_doxygen_comment(): line_start=1, line_end=16, comment_line_start=1, + comment_byte_range=None, ) function_range = source_node.markers[0] @@ -241,6 +248,7 @@ def test_24_parses_multiline_marker(): line_start=1, line_end=11, comment_line_start=1, + comment_byte_range=None, ) function_range = source_node.markers[0] @@ -283,6 +291,7 @@ def test_80_linux_spdx_example(): line_start=1, line_end=11, comment_line_start=1, + comment_byte_range=None, custom_tags={"SPDX-Req-ID", "SPDX-Req-HKey", "SPDX-Text"}, ) diff --git a/tests/unit/strictdoc/backend/sdoc_source_code/test_marker_writer.py b/tests/unit/strictdoc/backend/sdoc_source_code/test_marker_writer.py new file mode 100644 index 000000000..459856d70 --- /dev/null +++ b/tests/unit/strictdoc/backend/sdoc_source_code/test_marker_writer.py @@ -0,0 +1,72 @@ +""" +@relation(SDOC-SRS-34, SDOC-SRS-141, scope=file) +""" + +from strictdoc.backend.sdoc_source_code.marker_parser import MarkerParser +from strictdoc.backend.sdoc_source_code.marker_writer import MarkerWriter + + +def test_write_doxygen_comment(): + input_string = """\ +/** + * Some text. + * + * @relation( + * REQ-1, + * REQ-2, + * REQ-3, + * scope=function + * ) + * @relation( + * REQ-4, + * REQ-5, + * REQ-6, + * scope=function + * ) + */ +""" + + source_node = MarkerParser.parse( + input_string=input_string, + line_start=1, + line_end=16, + comment_line_start=1, + comment_byte_range=None, + ) + + output_string = MarkerWriter().write( + source_node, + rewrites={}, + comment_file_bytes=bytes(input_string, encoding="utf8"), + ) + assert output_string == bytes(input_string, encoding="utf8") + + +def test_write_node_fields(): + input_string = """\ +/** + * FOO: BAR + */ +""" + + source_node = MarkerParser.parse( + input_string=input_string, + line_start=1, + line_end=16, + comment_line_start=1, + comment_byte_range=None, + custom_tags={"FOO"}, + ) + + expected_output_string = b"""\ +/** + * FOO: + */ +""" + + output_string = MarkerWriter().write( + source_node, + rewrites={"FOO": b""}, + comment_file_bytes=bytes(input_string, encoding="utf8"), + ) + assert output_string == expected_output_string