Path:
tests/unit/strictdoc/backend/sdoc_source_code/readers/test_reader_python.py
Lines:
624
Non-empty lines:
499
Non-empty lines covered with requirements:
499 / 499 (100.0%)
Functions:
20
Functions covered by requirements:
20 / 20 (100.0%)
1
"""2
@relation(SDOC-SRS-147, scope=file)3
"""4
5
# The tests in this file has strings with whitespace that must not be linted.6
# ruff: noqa: W291,W2937
from typing import List
8
9
import pytest
10
11
from strictdoc.backend.sdoc.error_handling import StrictDocSemanticError
12
from strictdoc.backend.sdoc_source_code.models.language import LanguageItem
13
from strictdoc.backend.sdoc_source_code.models.range_marker import RangeMarker
14
from strictdoc.backend.sdoc_source_code.models.source_file_info import (
15
SourceFileTraceabilityInfo,
16
)17
from strictdoc.backend.sdoc_source_code.reader_python import (
18
SourceFileTraceabilityReader_Python,
19
)20
21
22
def test_00_empty_file():
23
input_string = b""""""
24
25
reader = SourceFileTraceabilityReader_Python()
26
27
info = reader.read(input_string)
28
29
assert isinstance(info, SourceFileTraceabilityInfo)
30
assert len(info.markers) == 0
31
32
33
def test_01_single_string():
34
input_string = b"""\
35
# Hello36
"""37
38
reader = SourceFileTraceabilityReader_Python()
39
40
info = reader.read(input_string)
41
42
assert isinstance(info, SourceFileTraceabilityInfo)
43
assert len(info.functions) == 0
44
assert len(info.markers) == 0
45
46
47
def test_02_functions():
48
input_string = b"""\
49
def hello_1():50
print("1")51
def hello_1_1():52
print("1_1")53
def hello_1_1_1():54
print("1_1_1")55
print("1_1_1 E")56
print("1_1 E")57
print("1 E")58
def hello_2():59
print("2")60
def hello_2_1():61
print("2_1")62
def hello_2_1_1():63
print("2_1_1")64
print("2_1_1 E")65
print("2_1 E")66
print("2 E")67
def hello_3():68
print("3")69
def hello_3_1():70
print("3_1")71
def hello_3_1_1():72
print("3_1_1")73
print("3_1_1 E")74
print("3_1 E")75
print("3 E")76
"""77
78
reader = SourceFileTraceabilityReader_Python()
79
80
info: SourceFileTraceabilityInfo = reader.read(input_string)
81
82
assert isinstance(info, SourceFileTraceabilityInfo)
83
assert len(info.functions) == 9
84
85
function_1 = info.functions[0]
86
assert isinstance(function_1, LanguageItem)
87
assert function_1.name == "hello_1"
88
assert len(function_1.child_functions) == 1
89
90
function_1_1 = function_1.child_functions[0]
91
assert isinstance(function_1_1, LanguageItem)
92
assert function_1_1.name == "hello_1.hello_1_1"
93
assert len(function_1_1.child_functions) == 1
94
95
function_1_1_1 = function_1_1.child_functions[0]
96
assert isinstance(function_1_1_1, LanguageItem)
97
assert function_1_1_1.name == "hello_1.hello_1_1.hello_1_1_1"
98
assert len(function_1_1_1.child_functions) == 0
99
100
function_2 = info.functions[3]
101
assert isinstance(function_2, LanguageItem)
102
assert function_2.name == "hello_2"
103
assert len(function_2.child_functions) == 1
104
105
function_2_1 = function_2.child_functions[0]
106
assert isinstance(function_2_1, LanguageItem)
107
assert function_2_1.name == "hello_2.hello_2_1"
108
assert len(function_2_1.child_functions) == 1
109
110
function_2_1_1 = function_2_1.child_functions[0]
111
assert isinstance(function_2_1_1, LanguageItem)
112
assert function_2_1_1.name == "hello_2.hello_2_1.hello_2_1_1"
113
assert len(function_2_1_1.child_functions) == 0
114
115
function_3 = info.functions[6]
116
assert isinstance(function_3, LanguageItem)
117
assert function_3.name == "hello_3"
118
assert len(function_3.child_functions) == 1
119
120
function_3_1 = function_3.child_functions[0]
121
assert isinstance(function_3_1, LanguageItem)
122
assert function_3_1.name == "hello_3.hello_3_1"
123
assert len(function_3_1.child_functions) == 1
124
125
function_3_1_1 = function_3_1.child_functions[0]
126
assert isinstance(function_3_1_1, LanguageItem)
127
assert function_3_1_1.name == "hello_3.hello_3_1.hello_3_1_1"
128
assert len(function_3_1_1.child_functions) == 0
129
130
131
def test_02_nested_sibling_functions():
132
input_string = b"""\
133
def outer():134
def wrapper():135
def first():136
pass137
def second():138
pass139
def third():140
pass141
"""142
143
reader = SourceFileTraceabilityReader_Python()
144
145
info: SourceFileTraceabilityInfo = reader.read(input_string)
146
147
assert isinstance(info, SourceFileTraceabilityInfo)
148
assert len(info.functions) == 5
149
150
outer_function = info.functions[0]
151
assert outer_function.name == "outer"
152
assert len(outer_function.child_functions) == 1
153
154
wrapper_function = outer_function.child_functions[0]
155
assert wrapper_function.name == "outer.wrapper"
156
assert len(wrapper_function.child_functions) == 3
157
158
assert wrapper_function.child_functions[0].name == "outer.wrapper.first"
159
assert wrapper_function.child_functions[1].name == "outer.wrapper.second"
160
assert wrapper_function.child_functions[2].name == "outer.wrapper.third"
161
162
163
def test_001_one_range_marker():
164
source_input = b"""\
165
# @relation(REQ-001, REQ-002, REQ-003, scope=range_start)166
print("Hello world")167
# @relation(REQ-001, REQ-002, REQ-003, scope=range_end)168
""".lstrip()
169
170
reader = SourceFileTraceabilityReader_Python()
171
172
info: SourceFileTraceabilityInfo = reader.read(source_input)
173
markers = info.markers
174
175
assert markers[0].reqs == ["REQ-001", "REQ-002", "REQ-003"]
176
assert markers[0].is_begin()
177
assert markers[0].ng_source_line_begin == 1
178
assert markers[0].ng_source_column_begin == 3
179
assert markers[0].ng_range_line_begin == 1
180
assert markers[0].ng_range_line_end == 3
181
182
assert markers[1].reqs == ["REQ-001", "REQ-002", "REQ-003"]
183
assert markers[1].is_end()
184
assert markers[1].ng_source_line_begin == 3
185
assert markers[1].ng_source_column_begin == 3
186
assert markers[1].ng_range_line_begin == 1
187
assert markers[1].ng_range_line_end == 3
188
189
190
def test_002_two_range_markers():
191
source_input = b"""
192
# @relation(REQ-001, scope=range_start)193
CONTENT 1194
CONTENT 2195
CONTENT 3196
# @relation(REQ-001, scope=range_end)197
# @relation(REQ-002, scope=range_start)198
CONTENT 4199
CONTENT 5200
CONTENT 6201
# @relation(REQ-002, scope=range_end)202
""".lstrip()
203
204
reader = SourceFileTraceabilityReader_Python()
205
206
document = reader.read(source_input)
207
markers = document.markers
208
assert len(markers) == 4
209
marker_1 = markers[0]
210
marker_2 = markers[1]
211
marker_3 = markers[2]
212
marker_4 = markers[3]
213
assert marker_1.reqs == ["REQ-001"]
214
assert marker_2.reqs == ["REQ-001"]
215
assert marker_3.reqs == ["REQ-002"]
216
assert marker_4.reqs == ["REQ-002"]
217
218
assert marker_1.ng_source_line_begin == 1
219
assert marker_2.ng_source_line_begin == 5
220
assert marker_3.ng_source_line_begin == 6
221
assert marker_4.ng_source_line_begin == 10
222
223
assert marker_1.ng_range_line_begin == 1
224
assert marker_2.ng_range_line_begin == 1
225
assert marker_3.ng_range_line_begin == 6
226
assert marker_4.ng_range_line_begin == 6
227
228
229
def test_003_one_range_marker_with_offset():
230
source_input = b"""\
231
232
233
#234
# @relation(REQ-001, REQ-002, REQ-003, scope=range_start)235
print("Hello world")236
# @relation(REQ-001, REQ-002, REQ-003, scope=range_end)237
#238
"""239
240
reader = SourceFileTraceabilityReader_Python()
241
242
info: SourceFileTraceabilityInfo = reader.read(source_input)
243
markers = info.markers
244
245
assert markers[0].reqs == ["REQ-001", "REQ-002", "REQ-003"]
246
assert markers[0].is_begin()
247
assert markers[0].ng_source_line_begin == 4
248
assert markers[0].ng_range_line_begin == 4
249
assert markers[0].ng_range_line_end == 6
250
251
assert markers[1].reqs == ["REQ-001", "REQ-002", "REQ-003"]
252
assert markers[1].is_end()
253
assert markers[1].ng_source_line_begin == 6
254
assert markers[1].ng_range_line_begin == 4
255
assert markers[1].ng_range_line_end == 6
256
257
258
def test_008_three_nested_range_markers():
259
source_input = b"""
260
CONTENT 1261
# @relation(REQ-001, scope=range_start)262
CONTENT 2263
# @relation(REQ-002, scope=range_start)264
CONTENT 3265
# @relation(REQ-003, scope=range_start)266
CONTENT 4267
# @relation(REQ-003, scope=range_end)268
CONTENT 5269
# @relation(REQ-002, scope=range_end)270
CONTENT 6271
# @relation(REQ-001, scope=range_end)272
CONTENT 7273
# @relation(REQ-001, scope=range_start)274
CONTENT 8275
# @relation(REQ-001, scope=range_end)276
CONTENT 9277
""".lstrip()
278
279
reader = SourceFileTraceabilityReader_Python()
280
281
document = reader.read(source_input)
282
markers = document.markers
283
assert len(markers) == 8
284
marker_1 = markers[0]
285
marker_2 = markers[1]
286
marker_3 = markers[2]
287
marker_4 = markers[3]
288
marker_5 = markers[4]
289
marker_6 = markers[5]
290
marker_7 = markers[6]
291
marker_8 = markers[7]
292
assert marker_1.reqs == ["REQ-001"]
293
assert marker_2.reqs == ["REQ-002"]
294
assert marker_3.reqs == ["REQ-003"]
295
assert marker_4.reqs == ["REQ-003"]
296
assert marker_5.reqs == ["REQ-002"]
297
assert marker_6.reqs == ["REQ-001"]
298
assert marker_7.reqs == ["REQ-001"]
299
assert marker_8.reqs == ["REQ-001"]
300
301
assert marker_1.ng_source_line_begin == 2
302
assert marker_2.ng_source_line_begin == 4
303
assert marker_3.ng_source_line_begin == 6
304
assert marker_4.ng_source_line_begin == 8
305
assert marker_5.ng_source_line_begin == 10
306
assert marker_6.ng_source_line_begin == 12
307
assert marker_7.ng_source_line_begin == 14
308
assert marker_8.ng_source_line_begin == 16
309
310
assert marker_1.ng_range_line_begin == 2
311
assert marker_2.ng_range_line_begin == 4
312
assert marker_3.ng_range_line_begin == 6
313
assert marker_4.ng_range_line_begin == 6
314
assert marker_5.ng_range_line_begin == 4
315
assert marker_6.ng_range_line_begin == 2
316
assert marker_7.ng_range_line_begin == 14
317
assert marker_8.ng_range_line_begin == 14
318
319
320
def test_010_relation_skip_keyword():
321
source_input = b"""
322
# @relation(skip, scope=range_start)323
# @relation(REQ-001, scope=range_start)324
CONTENT 1325
CONTENT 2326
CONTENT 3327
# @relation(REQ-001, scope=range_end)328
# @relation(skip, scope=range_end)329
""".lstrip()
330
331
reader = SourceFileTraceabilityReader_Python()
332
333
document = reader.read(source_input)
334
assert len(document.markers) == 0
335
336
337
def test_011_relation_skip_keyword_then_normal_marker():
338
source_input = b"""
339
# @relation(skip, scope=range_start)340
# @relation(REQ-001, scope=range_start)341
CONTENT 1342
CONTENT 2343
CONTENT 3344
# @relation(REQ-001, scope=range_end)345
# @relation(skip, scope=range_end)346
# @relation(REQ-001, scope=range_start)347
CONTENT 1348
CONTENT 2349
CONTENT 3350
# @relation(REQ-001, scope=range_end)351
""".lstrip()
352
353
reader = SourceFileTraceabilityReader_Python()
354
355
document = reader.read(source_input)
356
assert len(document.markers) == 2
357
358
359
def test_011_relation_skip_keyword_then_normal_marker_4spaces_indent():
360
source_input = b"""
361
# @relation(skip, scope=range_start)362
# @relation(REQ-001, scope=range_start)363
CONTENT 1364
CONTENT 2365
CONTENT 3366
# @relation(REQ-001, scope=range_end)367
# @relation(skip, scope=range_end)368
# @relation(REQ-001, scope=range_start)369
CONTENT 1370
CONTENT 2371
CONTENT 3372
# @relation(REQ-001, scope=range_end)373
""".lstrip()
374
375
reader = SourceFileTraceabilityReader_Python()
376
377
document = reader.read(source_input)
378
assert len(document.markers) == 2
379
380
381
def test_011_relation_skip_file():
382
source_input = b"""
383
\"\"\"384
@relation(skip, scope=file)385
\"\"\"386
387
# @relation(REQ-001, scope=range_start)388
CONTENT 1389
CONTENT 2390
CONTENT 3391
# @relation(REQ-001, scope=range_end)392
""".lstrip()
393
394
reader = SourceFileTraceabilityReader_Python()
395
396
document = reader.read(source_input)
397
assert len(document.markers) == 0
398
399
400
# Testing that correct line location is assigned when the marker is not on the401
# first line.402
def test_012_marker_not_first_line():
403
source_input = b"""
404
405
406
# @relation(REQ-001, scope=range_start)407
# CONTENT 1408
# CONTENT 2409
# CONTENT 3410
# @relation(REQ-001, scope=range_end)411
412
413
414
"""415
416
reader = SourceFileTraceabilityReader_Python()
417
418
document = reader.read(source_input)
419
420
markers: List[RangeMarker] = document.markers
421
assert markers[0].reqs == ["REQ-001"]
422
assert markers[0].is_begin()
423
assert markers[0].ng_source_line_begin == 4
424
assert markers[0].ng_range_line_begin == 4
425
assert markers[0].ng_range_line_end == 8
426
427
assert markers[1].reqs == ["REQ-001"]
428
assert markers[1].is_end()
429
assert markers[1].ng_source_line_begin == 8
430
assert markers[1].ng_range_line_begin == 4
431
assert markers[1].ng_range_line_end == 8
432
433
434
#435
# LINE markers.436
#437
438
439
def test_050_line_marker():
440
source_input = b"""
441
# @relation(REQ-001, scope=line)442
CONTENT 1443
# @relation(REQ-002, scope=line)444
CONTENT 2445
# @relation(REQ-003, scope=line)446
CONTENT 3447
""".lstrip()
448
449
reader = SourceFileTraceabilityReader_Python()
450
451
document = reader.read(source_input)
452
markers = document.markers
453
assert markers[0].reqs == ["REQ-001"]
454
assert markers[0].ng_source_line_begin == 1
455
assert markers[0].ng_range_line_begin == 1
456
assert markers[0].ng_range_line_end == 2
457
assert markers[1].reqs == ["REQ-002"]
458
assert markers[1].ng_source_line_begin == 3
459
assert markers[1].ng_range_line_begin == 3
460
assert markers[1].ng_range_line_end == 4
461
assert markers[2].reqs == ["REQ-003"]
462
assert markers[2].ng_source_line_begin == 5
463
assert markers[2].ng_range_line_begin == 5
464
assert markers[2].ng_range_line_end == 6
465
466
467
def test_070_function_marker():
468
source_input = b"""\
469
# Some offset...470
# Some offset...471
def function_1():472
\"\"\"
473
@relation(REQ-001, scope=function)474
\"\"\"
475
pass476
"""477
478
reader = SourceFileTraceabilityReader_Python()
479
480
document = reader.read(source_input)
481
markers = document.markers
482
assert markers[0].reqs == ["REQ-001"]
483
assert markers[0].ng_source_line_begin == 5
484
assert markers[0].ng_range_line_begin == 3
485
assert markers[0].ng_range_line_end == 7
486
assert markers[0].reqs_objs[0].ng_source_line == 5
487
assert markers[0].reqs_objs[0].ng_source_column == 15
488
489
490
def test_071_function_marker_multiline():
491
source_input = b"""\
492
# Some offset...493
# Some offset...494
def function_1():495
\"\"\"
496
@relation(497
REQ-1A,498
REQ-1B,499
scope=function500
)501
@relation(502
REQ-2A,503
REQ-2B,504
scope=function505
) 506
\"\"\"
507
pass508
"""509
510
reader = SourceFileTraceabilityReader_Python()
511
512
document = reader.read(source_input)
513
markers = document.markers
514
marker = markers[0]
515
assert marker.reqs == ["REQ-1A", "REQ-1B"]
516
assert marker.ng_source_line_begin == 5
517
assert marker.ng_range_line_begin == 3
518
assert marker.ng_range_line_end == 16
519
assert marker.reqs_objs[0].ng_source_line == 6
520
assert marker.reqs_objs[0].ng_source_column == 9
521
assert marker.reqs_objs[1].ng_source_line == 7
522
assert marker.reqs_objs[1].ng_source_column == 9
523
524
marker = markers[1]
525
assert marker.reqs == ["REQ-2A", "REQ-2B"]
526
assert marker.ng_source_line_begin == 10
527
assert marker.ng_range_line_begin == 3
528
assert marker.ng_range_line_end == 16
529
assert marker.reqs_objs[0].ng_source_line == 11
530
assert marker.reqs_objs[0].ng_source_column == 9
531
assert marker.reqs_objs[1].ng_source_line == 12
532
assert marker.reqs_objs[1].ng_source_column == 9
533
534
535
def test_080_file_marker():
536
"""
537
Test a basic file range relation marker.538
"""539
540
source_input = b"""\
541
\"\"\"542
@relation(REQ-1, scope=file)543
\"\"\"544
545
@auto_described546
class SDocDocumentContext:547
def __init__(self) -> None:548
self.title_number_string: Optional[str] = None549
"""550
551
reader = SourceFileTraceabilityReader_Python()
552
553
document = reader.read(source_input)
554
markers = document.markers
555
marker = markers[0]
556
assert marker.reqs == ["REQ-1"]
557
assert marker.ng_source_line_begin == 2
558
assert marker.ng_range_line_begin == 1
559
assert marker.ng_range_line_end == 8
560
assert marker.reqs_objs[0].ng_source_line == 2
561
assert marker.reqs_objs[0].ng_source_column == 11
562
563
564
def test_validation_01_one_range_marker_begin_req_not_equal_to_end_req():
565
source_input = b"""
566
# @relation(REQ-001, scope=range_start)567
CONTENT 1568
CONTENT 2569
CONTENT 3570
# @relation(REQ-002, scope=range_end)571
""".lstrip()
572
573
reader = SourceFileTraceabilityReader_Python()
574
575
with pytest.raises(Exception) as exc_info:
576
_ = reader.read(source_input)
577
578
assert exc_info.type is StrictDocSemanticError
579
assert (
580
exc_info.value.args[0]
581
== "STRICTDOC RANGE: BEGIN and END requirements mismatch"
582
)583
584
585
def test_validation_02_one_range_marker_end_without_begin():
586
source_input = b"""
587
# @relation(REQ-002, scope=range_end)588
""".lstrip()
589
590
reader = SourceFileTraceabilityReader_Python()
591
592
with pytest.raises(Exception) as exc_info:
593
_ = reader.read(source_input)
594
595
assert exc_info.type is StrictDocSemanticError
596
assert (
597
exc_info.value.args[0]
598
== "STRICTDOC RANGE: END marker without preceding BEGIN marker"
599
)600
601
602
def test_validation_03_range_start_without_range_end():
603
source_input = b"""
604
# @relation(REQ-001, scope=range_start)605
# @relation(REQ-002, scope=range_start)606
CONTENT 1607
CONTENT 2608
CONTENT 3609
""".lstrip()
610
611
reader = SourceFileTraceabilityReader_Python()
612
613
with pytest.raises(Exception) as exc_info:
614
_ = reader.read(source_input)
615
616
assert exc_info.type is StrictDocSemanticError
617
assert (
618
exc_info.value.args[0]
619
== "Unmatched @relation keyword found in source file."
620
)621
assert (
622
exc_info.value.args[1]
623
== "The @relation keywords are also unmatched on lines: [(2, 3)]."
624
)