StrictDoc Documentation
tests/unit/strictdoc/backend/sdoc_source_code/readers/test_reader_python.py
Source file coverage
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,W293
7
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
# Hello
36
"""
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
            pass
137
        def second():
138
            pass
139
        def third():
140
            pass
141
"""
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 1
194
CONTENT 2
195
CONTENT 3
196
# @relation(REQ-001, scope=range_end)
197
# @relation(REQ-002, scope=range_start)
198
CONTENT 4
199
CONTENT 5
200
CONTENT 6
201
# @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 1
261
# @relation(REQ-001, scope=range_start)
262
CONTENT 2
263
# @relation(REQ-002, scope=range_start)
264
CONTENT 3
265
# @relation(REQ-003, scope=range_start)
266
CONTENT 4
267
# @relation(REQ-003, scope=range_end)
268
CONTENT 5
269
# @relation(REQ-002, scope=range_end)
270
CONTENT 6
271
# @relation(REQ-001, scope=range_end)
272
CONTENT 7
273
# @relation(REQ-001, scope=range_start)
274
CONTENT 8
275
# @relation(REQ-001, scope=range_end)
276
CONTENT 9
277
""".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 1
325
CONTENT 2
326
CONTENT 3
327
# @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 1
342
CONTENT 2
343
CONTENT 3
344
# @relation(REQ-001, scope=range_end)
345
# @relation(skip, scope=range_end)
346
# @relation(REQ-001, scope=range_start)
347
CONTENT 1
348
CONTENT 2
349
CONTENT 3
350
# @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 1
364
    CONTENT 2
365
    CONTENT 3
366
    # @relation(REQ-001, scope=range_end)
367
    # @relation(skip, scope=range_end)
368
    # @relation(REQ-001, scope=range_start)
369
    CONTENT 1
370
    CONTENT 2
371
    CONTENT 3
372
    # @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 1
389
CONTENT 2
390
CONTENT 3
391
# @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 the
401
# 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 1
408
# CONTENT 2
409
# CONTENT 3
410
# @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 1
443
# @relation(REQ-002, scope=line)
444
CONTENT 2
445
# @relation(REQ-003, scope=line)
446
CONTENT 3
447
""".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
    pass
476
"""
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=function
500
    )
501
    @relation(
502
        REQ-2A,
503
        REQ-2B,
504
        scope=function
505
    )    
506
    \"\"\"
507
    pass
508
"""
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_described
546
class SDocDocumentContext:
547
    def __init__(self) -> None:
548
        self.title_number_string: Optional[str] = None
549
"""
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 1
568
CONTENT 2
569
CONTENT 3
570
# @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 1
607
CONTENT 2
608
CONTENT 3
609
""".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
    )