|
| 1 | +import sys |
| 2 | +from typing import List, Union |
| 3 | + |
| 4 | +from robot.api.parsing import ( |
| 5 | + Comment, |
| 6 | + Documentation, |
| 7 | + EmptyLine, |
| 8 | + ModelVisitor, |
| 9 | + Tags, |
| 10 | + TestCase, |
| 11 | + Token, |
| 12 | + get_model, |
| 13 | +) |
| 14 | + |
| 15 | +from strictdoc.backend.sdoc.error_handling import StrictDocSemanticError |
| 16 | +from strictdoc.backend.sdoc_source_code.constants import FunctionAttribute |
| 17 | +from strictdoc.backend.sdoc_source_code.marker_parser import MarkerParser |
| 18 | +from strictdoc.backend.sdoc_source_code.models.function import Function |
| 19 | +from strictdoc.backend.sdoc_source_code.models.function_range_marker import ( |
| 20 | + FunctionRangeMarker, |
| 21 | + RangeMarkerType, |
| 22 | +) |
| 23 | +from strictdoc.backend.sdoc_source_code.models.range_marker import ( |
| 24 | + LineMarker, |
| 25 | + RangeMarker, |
| 26 | +) |
| 27 | +from strictdoc.backend.sdoc_source_code.models.source_file_info import ( |
| 28 | + SourceFileTraceabilityInfo, |
| 29 | +) |
| 30 | +from strictdoc.backend.sdoc_source_code.parse_context import ParseContext |
| 31 | +from strictdoc.backend.sdoc_source_code.processors.general_language_marker_processors import ( |
| 32 | + function_range_marker_processor, |
| 33 | + line_marker_processor, |
| 34 | + range_marker_processor, |
| 35 | + source_file_traceability_info_processor, |
| 36 | +) |
| 37 | +from strictdoc.helpers.file_stats import SourceFileStats |
| 38 | + |
| 39 | + |
| 40 | +class SdocRelationVisitor(ModelVisitor): |
| 41 | + """ |
| 42 | + Create functions from test cases in *.robot files and create markers. |
| 43 | +
|
| 44 | + Note: ModelVisitor reuses ast.NodeVisitor from Python. We rely on following |
| 45 | + behavior. |
| 46 | + - It traverses depth first. This order is important to get matching marker |
| 47 | + pairs on the ParsersContext.marker_stack. |
| 48 | + - It doesn't recurse into subtrees if a custom visit_* method is defined. |
| 49 | + This is important to avoid duplicated matches. |
| 50 | + """ |
| 51 | + |
| 52 | + def __init__( |
| 53 | + self, |
| 54 | + traceability_info: SourceFileTraceabilityInfo, |
| 55 | + parse_context: ParseContext, |
| 56 | + ): |
| 57 | + super().__init__() |
| 58 | + self.traceability_info = traceability_info |
| 59 | + self.parse_context = parse_context |
| 60 | + |
| 61 | + def visit_Comment(self, node: Comment) -> None: |
| 62 | + """Create non-function Marker from Comment outside TestCases.""" |
| 63 | + self._visit_possibly_marked_node(node) |
| 64 | + |
| 65 | + def visit_Documentation(self, node: Documentation) -> None: |
| 66 | + """Create non-function Marker from Documentation outside TestCases.""" |
| 67 | + self._visit_possibly_marked_node(node) |
| 68 | + |
| 69 | + def visit_Tags(self, node: Tags) -> None: |
| 70 | + """Create non-function Marker from Tags outside TestCases.""" |
| 71 | + self._visit_possibly_marked_node(node) |
| 72 | + |
| 73 | + def visit_TestCase(self, node: TestCase) -> None: |
| 74 | + """Create function and non-function Marker from TestCases.""" |
| 75 | + trailing_empty_lines = 0 |
| 76 | + tc_markers = [] |
| 77 | + for stmt in node.body: |
| 78 | + if isinstance(stmt, EmptyLine): |
| 79 | + # trim trailing newlines from test case range |
| 80 | + trailing_empty_lines += 1 |
| 81 | + else: |
| 82 | + trailing_empty_lines = 0 |
| 83 | + |
| 84 | + if isinstance(stmt, (Comment, Tags, Documentation)): |
| 85 | + for token in filter(self._token_filter, stmt.tokens): |
| 86 | + markers: List[ |
| 87 | + Union[FunctionRangeMarker, RangeMarker, LineMarker] |
| 88 | + ] = MarkerParser.parse( |
| 89 | + token.value, |
| 90 | + token.lineno, |
| 91 | + token.lineno, |
| 92 | + token.lineno, |
| 93 | + node.name, |
| 94 | + token.col_offset, |
| 95 | + ) |
| 96 | + tc_markers.extend(markers) |
| 97 | + |
| 98 | + function_markers = [] |
| 99 | + for marker_ in tc_markers: |
| 100 | + if isinstance(marker_, FunctionRangeMarker): |
| 101 | + marker_.ng_range_line_begin = node.lineno |
| 102 | + marker_.ng_range_line_end = ( |
| 103 | + node.end_lineno - trailing_empty_lines |
| 104 | + ) |
| 105 | + function_markers.append(marker_) |
| 106 | + function_range_marker_processor(marker_, self.parse_context) |
| 107 | + elif isinstance(marker_, RangeMarker): |
| 108 | + range_marker_processor(marker_, self.parse_context) |
| 109 | + elif isinstance(marker_, LineMarker): |
| 110 | + line_marker_processor(marker_, self.parse_context) |
| 111 | + |
| 112 | + self.traceability_info.markers.extend(function_markers) |
| 113 | + test_case = Function( |
| 114 | + parent=self.traceability_info, |
| 115 | + name=node.name, |
| 116 | + display_name=node.name, |
| 117 | + line_begin=node.lineno, |
| 118 | + line_end=node.end_lineno - trailing_empty_lines, |
| 119 | + child_functions=[], |
| 120 | + markers=function_markers, |
| 121 | + attributes={FunctionAttribute.DEFINITION}, |
| 122 | + ) |
| 123 | + self.traceability_info.functions.append(test_case) |
| 124 | + |
| 125 | + def _visit_possibly_marked_node( |
| 126 | + self, node: Union[Comment, Documentation, Tags] |
| 127 | + ) -> None: |
| 128 | + for token in filter(self._token_filter, node.tokens): |
| 129 | + markers: List[ |
| 130 | + Union[FunctionRangeMarker, RangeMarker, LineMarker] |
| 131 | + ] = MarkerParser.parse( |
| 132 | + token.value, |
| 133 | + node.lineno, |
| 134 | + node.lineno, |
| 135 | + node.lineno, |
| 136 | + None, |
| 137 | + token.col_offset, |
| 138 | + ) |
| 139 | + for marker_ in markers: |
| 140 | + if ( |
| 141 | + isinstance(marker_, FunctionRangeMarker) |
| 142 | + and marker_.scope is RangeMarkerType.FILE |
| 143 | + ): |
| 144 | + # Outside Test Cases only accept scope=file function markers |
| 145 | + marker_.ng_range_line_begin = 1 |
| 146 | + marker_.ng_range_line_end = ( |
| 147 | + self.parse_context.file_stats.lines_total |
| 148 | + ) |
| 149 | + function_range_marker_processor(marker_, self.parse_context) |
| 150 | + self.traceability_info.markers.append(marker_) |
| 151 | + elif isinstance(marker_, RangeMarker): |
| 152 | + range_marker_processor(marker_, self.parse_context) |
| 153 | + elif isinstance(marker_, LineMarker): |
| 154 | + line_marker_processor(marker_, self.parse_context) |
| 155 | + |
| 156 | + @staticmethod |
| 157 | + def _token_filter(token: Token) -> bool: |
| 158 | + if token.type in ("SEPARATOR", "EOL"): |
| 159 | + return False |
| 160 | + return True |
| 161 | + |
| 162 | + |
| 163 | +class SourceFileTraceabilityReader_Robot: |
| 164 | + def read( |
| 165 | + self, input_buffer: str, file_path: str |
| 166 | + ) -> SourceFileTraceabilityInfo: |
| 167 | + traceability_info = SourceFileTraceabilityInfo([]) |
| 168 | + if len(input_buffer) == 0: |
| 169 | + return traceability_info |
| 170 | + file_stats = SourceFileStats.create(input_buffer) |
| 171 | + parse_context = ParseContext(file_path, file_stats) |
| 172 | + robotfw_model = get_model(input_buffer, data_only=False) |
| 173 | + visitor = SdocRelationVisitor(traceability_info, parse_context) |
| 174 | + visitor.visit(robotfw_model) |
| 175 | + source_file_traceability_info_processor( |
| 176 | + traceability_info, parse_context |
| 177 | + ) |
| 178 | + return traceability_info |
| 179 | + |
| 180 | + def read_from_file(self, file_path: str) -> SourceFileTraceabilityInfo: |
| 181 | + try: |
| 182 | + with open(file_path) as file: |
| 183 | + sdoc_content = file.read() |
| 184 | + sdoc = self.read(sdoc_content, file_path=file_path) |
| 185 | + return sdoc |
| 186 | + except UnicodeDecodeError: |
| 187 | + raise |
| 188 | + except StrictDocSemanticError as exc: |
| 189 | + print(exc.to_print_message()) # noqa: T201 |
| 190 | + sys.exit(1) |
| 191 | + except Exception as exc: # pylint: disable=broad-except |
| 192 | + print( # noqa: T201 |
| 193 | + f"error: SourceFileTraceabilityReader_Robot: could not parse file: " |
| 194 | + f"{file_path}.\n{exc.__class__.__name__}: {exc}" |
| 195 | + ) |
| 196 | + # TODO: when --debug is provided |
| 197 | + # traceback.print_exc() # noqa: ERA001 |
| 198 | + sys.exit(1) |
0 commit comments