StrictDoc Documentation
tests/unit/strictdoc/backend/sdoc_source_code/test_marker_parser.py
Source file coverage
Path:
tests/unit/strictdoc/backend/sdoc_source_code/test_marker_parser.py
Lines:
422
Non-empty lines:
368
Non-empty lines covered with requirements:
368 / 368 (100.0%)
Functions:
13
Functions covered by requirements:
13 / 13 (100.0%)
1
"""
2
@relation(SDOC-SRS-34, SDOC-SRS-141, scope=file)
3
"""
4
 
5
import pytest
6
 
7
from strictdoc.backend.sdoc.error_handling import StrictDocSemanticError
8
from strictdoc.backend.sdoc_source_code.marker_parser import MarkerParser
9
from strictdoc.backend.sdoc_source_code.models.language_item_marker import (
10
    LanguageItemMarker,
11
)
12
from strictdoc.backend.sdoc_source_code.models.line_marker import LineMarker
13
 
14
 
15
def test_01_basic_nominal():
16
    input_strings = [
17
        "@relation(REQ-1, scope=function)\n",
18
        "@relation(REQ_1, scope=function)\n",
19
        "@relation(REQ.1, scope=function)\n",
20
        "@relation(REQ/1, scope=function)\n",
21
    ]
22
 
23
    for input_string_ in input_strings:
24
        source_node = MarkerParser.parse(
25
            input_string=input_string_,
26
            line_start=1,
27
            line_end=1,
28
            comment_line_start=1,
29
            comment_byte_range=None,
30
        )
31
        function_range = source_node.markers[0]
32
        assert isinstance(function_range, LanguageItemMarker)
33
        assert function_range.ng_source_line_begin == 1
34
        assert function_range.ng_range_line_begin == 1
35
        assert function_range.ng_range_line_end == 1
36
        assert function_range.reqs_objs[0].ng_source_line == 1
37
        assert function_range.reqs_objs[0].ng_source_column == 11
38
 
39
 
40
def test_10_parses_with_leading_newlines():
41
    input_string = """\
42
 
43
 
44
@relation(REQ-1, scope=function)
45
"""
46
 
47
    source_node = MarkerParser.parse(
48
        input_string=input_string,
49
        line_start=1,
50
        line_end=5,
51
        comment_line_start=1,
52
        comment_byte_range=None,
53
    )
54
    function_range = source_node.markers[0]
55
 
56
    assert isinstance(function_range, LanguageItemMarker)
57
    assert function_range.ng_source_line_begin == 3
58
    assert function_range.ng_range_line_begin == 1
59
    assert function_range.ng_range_line_end == 5
60
    assert function_range.reqs_objs[0].ng_source_line == 3
61
    assert function_range.reqs_objs[0].ng_source_column == 11
62
 
63
 
64
def test_11_parses_with_leading_whitespace():
65
    input_string = """\
66
 
67
 
68
    @relation(REQ-1, scope=function)
69
"""
70
 
71
    source_node = MarkerParser.parse(
72
        input_string=input_string,
73
        line_start=1,
74
        line_end=3,
75
        comment_line_start=1,
76
        comment_byte_range=None,
77
    )
78
    function_range = source_node.markers[0]
79
 
80
    assert isinstance(function_range, LanguageItemMarker)
81
    assert function_range.ng_source_line_begin == 3
82
    assert function_range.ng_range_line_begin == 1
83
    assert function_range.ng_range_line_end == 3
84
    assert function_range.reqs_objs[0].ng_source_line == 3
85
    assert function_range.reqs_objs[0].ng_source_column == 15
86
 
87
 
88
def test_20_parses_within_doxygen_comment():
89
    input_string = """\
90
/**
91
 * Some text.
92
 *
93
 * @relation(REQ-1, scope=function)
94
 */
95
"""
96
 
97
    source_node = MarkerParser.parse(
98
        input_string=input_string,
99
        line_start=1,
100
        line_end=5,
101
        comment_line_start=1,
102
        comment_byte_range=None,
103
    )
104
    function_range = source_node.markers[0]
105
 
106
    assert isinstance(function_range, LanguageItemMarker)
107
    assert function_range.ng_source_line_begin == 4
108
    assert function_range.ng_range_line_begin == 1
109
    assert function_range.ng_range_line_end == 5
110
    assert function_range.reqs_objs[0].ng_source_line == 4
111
    assert function_range.reqs_objs[0].ng_source_column == 14
112
 
113
 
114
def test_21_parses_within_doxygen_comment_two_markers():
115
    input_string = """\
116
/**
117
 * Some text.
118
 *
119
 * @relation(REQ-1, scope=function)
120
 * @relation(REQ-2, scope=function)
121
 */
122
"""
123
 
124
    source_node = MarkerParser.parse(
125
        input_string=input_string,
126
        line_start=1,
127
        line_end=6,
128
        comment_line_start=1,
129
        comment_byte_range=None,
130
    )
131
    function_range = source_node.markers[0]
132
 
133
    assert isinstance(function_range, LanguageItemMarker)
134
    assert function_range.ng_source_line_begin == 4
135
    assert function_range.ng_range_line_begin == 1
136
    assert function_range.ng_range_line_end == 6
137
    assert function_range.reqs_objs[0].ng_source_line == 4
138
    assert function_range.reqs_objs[0].ng_source_column == 14
139
 
140
 
141
def test_22_parses_within_doxygen_comment_curly_braces():
142
    input_string = """\
143
/**
144
 * Some text.
145
 *
146
 * @relation{REQ-1, scope=function}
147
 */
148
"""
149
 
150
    source_node = MarkerParser.parse(
151
        input_string=input_string,
152
        line_start=1,
153
        line_end=5,
154
        comment_line_start=1,
155
        comment_byte_range=None,
156
    )
157
    function_range = source_node.markers[0]
158
 
159
    assert isinstance(function_range, LanguageItemMarker)
160
    assert function_range.ng_source_line_begin == 4
161
    assert function_range.ng_range_line_begin == 1
162
    assert function_range.ng_range_line_end == 5
163
    assert function_range.reqs_objs[0].ng_source_line == 4
164
    assert function_range.reqs_objs[0].ng_source_column == 14
165
 
166
 
167
def test_23_parses_within_doxygen_comment():
168
    input_string = """\
169
/**
170
 * Some text.
171
 *
172
 * @relation(
173
 *     REQ-1,
174
 *     REQ-2,
175
 *     REQ-3,
176
 *     scope=function
177
 * )
178
 * @relation(
179
 *     REQ-4,
180
 *     REQ-5,
181
 *     REQ-6,
182
 *     scope=function
183
 * )
184
 */
185
"""
186
 
187
    source_node = MarkerParser.parse(
188
        input_string=input_string,
189
        line_start=1,
190
        line_end=16,
191
        comment_line_start=1,
192
        comment_byte_range=None,
193
    )
194
 
195
    function_range = source_node.markers[0]
196
    assert isinstance(function_range, LanguageItemMarker)
197
    assert function_range.ng_source_line_begin == 4
198
    assert function_range.ng_range_line_begin == 1
199
    assert function_range.ng_range_line_end == 16
200
    assert function_range.reqs_objs[0].uid == "REQ-1"
201
    assert function_range.reqs_objs[0].ng_source_line == 5
202
    assert function_range.reqs_objs[0].ng_source_column == 8
203
    assert function_range.reqs_objs[1].uid == "REQ-2"
204
    assert function_range.reqs_objs[1].ng_source_line == 6
205
    assert function_range.reqs_objs[1].ng_source_column == 8
206
    assert function_range.reqs_objs[2].uid == "REQ-3"
207
    assert function_range.reqs_objs[2].ng_source_line == 7
208
    assert function_range.reqs_objs[2].ng_source_column == 8
209
 
210
    function_range = source_node.markers[1]
211
    assert isinstance(function_range, LanguageItemMarker)
212
    assert function_range.ng_source_line_begin == 10
213
    assert function_range.ng_range_line_begin == 1
214
    assert function_range.ng_range_line_end == 16
215
    assert function_range.reqs_objs[0].uid == "REQ-4"
216
    assert function_range.reqs_objs[0].ng_source_line == 11
217
    assert function_range.reqs_objs[0].ng_source_column == 8
218
    assert function_range.reqs_objs[1].uid == "REQ-5"
219
    assert function_range.reqs_objs[1].ng_source_line == 12
220
    assert function_range.reqs_objs[1].ng_source_column == 8
221
    assert function_range.reqs_objs[2].uid == "REQ-6"
222
    assert function_range.reqs_objs[2].ng_source_line == 13
223
    assert function_range.reqs_objs[2].ng_source_column == 8
224
 
225
 
226
def test_24_parses_multiline_marker():
227
    input_string = """\
228
/**
229
 * Some text.
230
 *
231
 * @relation(
232
 *     REQ-1,
233
 *     REQ-2,
234
 *     REQ-3,
235
 *     scope=line
236
 * )
237
 * HERE SOME LINE
238
 */
239
"""
240
 
241
    source_node = MarkerParser.parse(
242
        input_string=input_string,
243
        line_start=1,
244
        line_end=11,
245
        comment_line_start=1,
246
        comment_byte_range=None,
247
    )
248
 
249
    function_range = source_node.markers[0]
250
    assert isinstance(function_range, LineMarker)
251
    assert function_range.ng_source_line_begin == 4
252
    assert function_range.ng_range_line_begin == 4
253
    assert function_range.ng_range_line_end == 10
254
    assert function_range.reqs_objs[0].uid == "REQ-1"
255
    assert function_range.reqs_objs[0].ng_source_line == 5
256
    assert function_range.reqs_objs[0].ng_source_column == 8
257
    assert function_range.reqs_objs[1].uid == "REQ-2"
258
    assert function_range.reqs_objs[1].ng_source_line == 6
259
    assert function_range.reqs_objs[1].ng_source_column == 8
260
    assert function_range.reqs_objs[2].uid == "REQ-3"
261
    assert function_range.reqs_objs[2].ng_source_line == 7
262
    assert function_range.reqs_objs[2].ng_source_column == 8
263
 
264
 
265
def test_30_parser_dedents_field_lines():
266
    """
267
    Since source code fields will likely opt for text rendering (instead of RST),
268
    ensure that ASCII formating is preserved reasonably.
269
    """
270
    input_string = """\
271
    /**
272
     * FIELD1: Nothing to dedent here.
273
     *
274
     * FIELD2: Nothing to
275
     * dedent here.
276
     *
277
     * FIELD3: Dedent
278
     *         - this list
279
     *           - but keep
280
     *             - inner indent
281
     *
282
     * FIELD4:
283
     *        ASCII art
284
     *    ___           ___
285
     *  ( foo ) <---> ( bar )
286
     *    ‾‾‾           ‾‾‾
287
     */"""
288
 
289
    source_node = MarkerParser.parse(
290
        input_string=input_string,
291
        line_start=1,
292
        line_end=7,
293
        comment_line_start=1,
294
        comment_byte_range=None,
295
        custom_tags=["FIELD1", "FIELD2", "FIELD3", "FIELD4"],
296
    )
297
    assert source_node.fields["FIELD1"] == "Nothing to dedent here."
298
    assert source_node.fields["FIELD2"] == "Nothing to\ndedent here."
299
    assert source_node.fields["FIELD3"] == (
300
        "Dedent\n- this list\n  - but keep\n    - inner indent"
301
    )
302
    assert source_node.fields["FIELD4"] == (
303
        "\n"
304
        "      ASCII art\n"
305
        "  ___           ___\n"
306
        "( foo ) <---> ( bar )\n"
307
        "  ‾‾‾           ‾‾‾"
308
    )
309
 
310
 
311
@pytest.mark.parametrize(
312
    "input_string,default_scope,expected_type",
313
    [
314
        ("@relation(REQ-1)\n", "function", LanguageItemMarker),
315
        ("@relation(REQ-1, scope=line)\n", None, LineMarker),
316
        ("@relation(REQ-1, scope=line)\n", "function", LineMarker),
317
    ],
318
    ids=[
319
        "default, no user value",
320
        "no default, user value",
321
        "override default with user value",
322
    ],
323
)
324
def test_40_default_scope(input_string, default_scope, expected_type):
325
    source_node = MarkerParser.parse(
326
        input_string=input_string,
327
        line_start=1,
328
        line_end=1,
329
        comment_line_start=1,
330
        comment_byte_range=None,
331
        default_scope=default_scope,
332
    )
333
    assert isinstance(source_node.markers[0], expected_type)
334
 
335
 
336
def test_41_omitted_scope_with_role():
337
    input_string = "@relation(REQ-1, role=implementation)\n"
338
    source_node = MarkerParser.parse(
339
        input_string=input_string,
340
        line_start=1,
341
        line_end=1,
342
        comment_line_start=1,
343
        comment_byte_range=None,
344
        default_scope="function",
345
    )
346
    assert isinstance(source_node.markers[0], LanguageItemMarker)
347
    assert source_node.markers[0].role == "implementation"
348
 
349
 
350
def test_42_error_on_missing_scope():
351
    input_string = "some comment\n@relation(REQ-1)\n"
352
 
353
    excinfo: pytest.ExceptionInfo
354
    with pytest.raises(StrictDocSemanticError) as excinfo:
355
        MarkerParser.parse(
356
            input_string=input_string,
357
            line_start=1,
358
            line_end=1,
359
            comment_line_start=10,
360
            comment_byte_range=None,
361
            filename="main.py",
362
        )
363
    assert (
364
        excinfo.value.title
365
        == "@relation marker for requirements REQ-1 misses scope argument."
366
    )
367
    assert (
368
        excinfo.value.hint
369
        == "Scope can only be omitted if supported by language, as e.g. with Rust doc comments."
370
    )
371
    assert (
372
        excinfo.value.example
373
        == "Add a scope argument. Example:\n@relation(REQ-1, scope=function)"
374
    )
375
    assert excinfo.value.file_path == "main.py"
376
    assert excinfo.value.line == 11
377
 
378
 
379
def test_80_linux_spdx_example():
380
    input_string = """\
381
/**
382
 * Some text.
383
 *
384
 * @relation(REQ-1, scope=function)
385
 *
386
 * SPDX-Req-ID: SRC-1
387
 *
388
 * SPDX-Req-HKey: TBD
389
 *
390
 * SPDX-Text: This
391
 *            is
392
 *            a statement
393
 *
394
 *            And this is the same statement's another paragraph.
395
 */
396
"""
397
 
398
    source_node = MarkerParser.parse(
399
        input_string=input_string,
400
        line_start=1,
401
        line_end=11,
402
        comment_line_start=1,
403
        comment_byte_range=None,
404
        custom_tags={"SPDX-Req-ID", "SPDX-Req-HKey", "SPDX-Text"},
405
    )
406
 
407
    assert list(source_node.fields.keys()) == [
408
        "SPDX-Req-ID",
409
        "SPDX-Req-HKey",
410
        "SPDX-Text",
411
    ]
412
 
413
    assert (
414
        source_node.fields["SPDX-Text"]
415
        == """\
416
This
417
is
418
a statement
419
 
420
And this is the same statement's another paragraph.\
421
"""
422
    )