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%)
- "7.6.1. Relation markers syntax" (REQUIREMENT)
- "7.7.1. Parse nodes from source code" (REQUIREMENT)
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=function177
* )178
* @relation(179
* REQ-4,180
* REQ-5,181
* REQ-6,182
* scope=function183
* )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=line236
* )237
* HERE SOME LINE238
*/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 to275
* dedent here.276
*277
* FIELD3: Dedent278
* - this list279
* - but keep280
* - inner indent281
*282
* FIELD4:283
* ASCII art284
* ___ ___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-1387
*388
* SPDX-Req-HKey: TBD389
*390
* SPDX-Text: This391
* is392
* a statement393
*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
This417
is418
a statement419
420
And this is the same statement's another paragraph.\
421
"""422
)