[docs] 1# @Test suite for tree-sitter parsing utilities and language support, TEST_LANG_1, test, [IMPL_LANG_1, IMPL_EXTR_1, IMPL_RST_1]
2import shutil
3import subprocess
4from pathlib import Path
5
6import pytest
7import tree_sitter_bash
8import tree_sitter_c_sharp
9import tree_sitter_cpp
10import tree_sitter_go
11import tree_sitter_json
12import tree_sitter_python
13import tree_sitter_rust
14import tree_sitter_yaml
15from tree_sitter import Language, Parser, Query
16from tree_sitter import Node as TreeSitterNode
17
18from sphinx_codelinks.analyse import utils
19from sphinx_codelinks.config import UNIX_NEWLINE
20from sphinx_codelinks.source_discover.config import CommentType
21
22
23@pytest.fixture(scope="session")
24def init_cpp_tree_sitter() -> tuple[Parser, Query]:
25 parsed_language = Language(tree_sitter_cpp.language())
26 query = Query(parsed_language, utils.CPP_QUERY)
27 parser = Parser(parsed_language)
28 return parser, query
29
30
31@pytest.fixture(scope="session")
32def init_python_tree_sitter() -> tuple[Parser, Query]:
33 parsed_language = Language(tree_sitter_python.language())
34 query = Query(parsed_language, utils.PYTHON_QUERY)
35 parser = Parser(parsed_language)
36 return parser, query
37
38
39@pytest.fixture(scope="session")
40def init_csharp_tree_sitter() -> tuple[Parser, Query]:
41 parsed_language = Language(tree_sitter_c_sharp.language())
42 query = Query(parsed_language, utils.C_SHARP_QUERY)
43 parser = Parser(parsed_language)
44 return parser, query
45
46
47@pytest.fixture(scope="session")
48def init_yaml_tree_sitter() -> tuple[Parser, Query]:
49 parsed_language = Language(tree_sitter_yaml.language())
50 query = Query(parsed_language, utils.YAML_QUERY)
51 parser = Parser(parsed_language)
52 return parser, query
53
54
55@pytest.fixture(scope="session")
56def init_rust_tree_sitter() -> tuple[Parser, Query]:
57 parsed_language = Language(tree_sitter_rust.language())
58 query = Query(parsed_language, utils.RUST_QUERY)
59 parser = Parser(parsed_language)
60 return parser, query
61
62
63@pytest.fixture(scope="session")
64def init_go_tree_sitter() -> tuple[Parser, Query]:
65 parsed_language = Language(tree_sitter_go.language())
66 query = Query(parsed_language, utils.GO_QUERY)
67 parser = Parser(parsed_language)
68 return parser, query
69
70
71@pytest.fixture(scope="session")
72def init_jsonc_tree_sitter() -> tuple[Parser, Query]:
73 parsed_language = Language(tree_sitter_json.language())
74 query = Query(parsed_language, utils.JSONC_QUERY)
75 parser = Parser(parsed_language)
76 return parser, query
77
78
79@pytest.fixture(scope="session")
80def init_bash_tree_sitter() -> tuple[Parser, Query]:
81 parsed_language = Language(tree_sitter_bash.language())
82 query = Query(parsed_language, utils.BASH_QUERY)
83 parser = Parser(parsed_language)
84 return parser, query
85
86
87@pytest.mark.parametrize(
88 ("code", "result"),
89 [
90 (
91 b"""
92 // @req-id: need_001
93 void dummy_func1(){
94 }
95 """,
96 "void dummy_func1()",
97 ),
98 (
99 b"""
100 void dummy_func2(){
101 }
102 // @req-id: need_001
103 void dummy_func1(){
104 }
105 """,
106 "void dummy_func1()",
107 ),
108 (
109 b"""
110 void dummy_func1(){
111 a = 1;
112 /* @req-id: need_001 */
113 }
114 """,
115 "void dummy_func1()",
116 ),
117 (
118 b"""
119 void dummy_func1(){
120 // @req-id: need_001
121 a = 1;
122 }
123 void dummy_func2(){
124 }
125 """,
126 "void dummy_func1()",
127 ),
128 ],
129)
130def test_find_associated_scope_cpp(code, result, init_cpp_tree_sitter):
131 parser, query = init_cpp_tree_sitter
132 comments = utils.extract_comments(code, parser, query)
133 node: TreeSitterNode | None = utils.find_associated_scope(
134 comments[0], CommentType.cpp
135 )
136 assert node
137 assert node.text
138 func_def = node.text.decode("utf-8")
139 assert result in func_def
140
141
142@pytest.mark.parametrize(
143 ("code", "result"),
144 [
145 (
146 b"""
147 def dummy_func1():
148 # @req-id: need_001
149 pass
150 """,
151 "def dummy_func1()",
152 ),
153 (
154 b"""
155 def dummy_func1():
156 # @req-id: need_002
157 def dummy_func2():
158 pass
159 pass
160 """,
161 "def dummy_func2()",
162 ),
163 (
164 b"""
165 def dummy_func1():
166 '''@req-id: need_002'''
167 def nested_dummy_func():
168 pass
169 pass
170 """,
171 "def dummy_func1()",
172 ),
173 (
174 b"""
175 def dummy_func1():
176 def nested_dummy_func():
177 '''@req-id: need_002'''
178 pass
179 pass
180 """,
181 "def nested_dummy_func()",
182 ),
183 (
184 b"""
185 def dummy_func1():
186 def nested_dummy_func():
187 # @req-id: need_002
188 pass
189 pass
190 """,
191 "def nested_dummy_func()",
192 ),
193 (
194 b"""
195 def dummy_func1():
196 def nested_dummy_func():
197 pass
198 # @req-id: need_002
199 pass
200 """,
201 "def dummy_func1()",
202 ),
203 ],
204)
205def test_find_associated_scope_python(code, result, init_python_tree_sitter):
206 parser, query = init_python_tree_sitter
207 comments = utils.extract_comments(code, parser, query)
208 node: TreeSitterNode | None = utils.find_associated_scope(
209 comments[0], CommentType.python
210 )
211 assert node
212 assert node.text
213 func_def = node.text.decode("utf-8")
214 assert func_def.startswith(result)
215
216
217@pytest.mark.parametrize(
218 ("code", "result"),
219 [
220 (
221 b"""
222 // @req-id: need_001
223 public class DummyClass1
224 {
225 }
226 """,
227 "public class DummyClass1",
228 ),
229 (
230 b"""
231 public class DummyClass2
232 {
233 // @req-id: need_001
234 public void DummyFunc2()
235 {
236 }
237 }
238 """,
239 "public void DummyFunc2",
240 ),
241 (
242 b"""
243 public class DummyClass3
244 {
245 // @req-id: need_001
246 public string Property1 { get; set; }
247 }
248 """,
249 "public string Property1",
250 ),
251 ],
252)
253def test_find_associated_scope_csharp(code, result, init_csharp_tree_sitter):
254 parser, query = init_csharp_tree_sitter
255 comments = utils.extract_comments(code, parser, query)
256 node: TreeSitterNode | None = utils.find_associated_scope(
257 comments[0], CommentType.cs
258 )
259 assert node
260 assert node.text
261 func_def = node.text.decode("utf-8")
262 assert func_def.startswith(result)
263
264
265@pytest.mark.parametrize(
266 ("code", "result"),
267 [
268 (
269 b"""
270 # @req-id: need_001
271 database:
272 host: localhost
273 port: 5432
274 """,
275 "database:",
276 ),
277 (
278 b"""
279 services:
280 web:
281 # @req-id: need_002
282 image: nginx:latest
283 ports:
284 - "80:80"
285 """,
286 "image: nginx:latest",
287 ),
288 (
289 b"""
290 # @req-id: need_003
291 version: "3.8"
292 services:
293 app:
294 build: .
295 """,
296 "version:",
297 ),
298 (
299 b"""
300 items:
301 # @req-id: need_004
302 - name: item1
303 value: test
304 - name: item2
305 value: test2
306 """,
307 "- name: item1",
308 ),
309 ],
310)
311def test_find_associated_scope_yaml(code, result, init_yaml_tree_sitter):
312 parser, query = init_yaml_tree_sitter
313 comments = utils.extract_comments(code, parser, query)
314 node: TreeSitterNode | None = utils.find_associated_scope(
315 comments[0], CommentType.yaml
316 )
317 assert node
318 assert node.text
319 yaml_structure = node.text.decode("utf-8")
320 assert result in yaml_structure
321
322
323@pytest.mark.parametrize(
324 ("code", "result"),
325 [
326 (
327 b"""
328 // @req-id: need_001
329 fn dummy_func1() {
330 }
331 """,
332 "fn dummy_func1()",
333 ),
334 (
335 b"""
336 fn dummy_func2() {
337 }
338 // @req-id: need_001
339 fn dummy_func1() {
340 }
341 """,
342 "fn dummy_func1()",
343 ),
344 (
345 b"""
346 fn dummy_func1() {
347 let a = 1;
348 /* @req-id: need_001 */
349 }
350 """,
351 "fn dummy_func1()",
352 ),
353 (
354 b"""
355 fn dummy_func1() {
356 // @req-id: need_001
357 let a = 1;
358 }
359 fn dummy_func2() {
360 }
361 """,
362 "fn dummy_func1()",
363 ),
364 (
365 b"""
366 /// @req-id: need_001
367 fn dummy_func1() {
368 }
369 """,
370 "fn dummy_func1()",
371 ),
372 (
373 b"""
374 struct MyStruct {
375 // @req-id: need_001
376 field: i32,
377 }
378 """,
379 "struct MyStruct",
380 ),
381 ],
382)
383def test_find_associated_scope_rust(code, result, init_rust_tree_sitter):
384 parser, query = init_rust_tree_sitter
385 comments = utils.extract_comments(code, parser, query)
386 node: TreeSitterNode | None = utils.find_associated_scope(
387 comments[0], CommentType.rust
388 )
389 assert node
390 assert node.text
391 rust_def = node.text.decode("utf-8")
392 assert result in rust_def
393
394
395@pytest.mark.parametrize(
396 ("code", "result"),
397 [
398 # leading comment is associated with the following key/value pair
399 (
400 b'{\n // @req-id: need_001\n "alpha": 1\n}\n',
401 '"alpha": 1',
402 ),
403 # inline comment is associated with the array item on the same row
404 (
405 b'{\n "items": [\n "first", // @req-id: need_001\n "second"\n ]\n}\n',
406 '"first"',
407 ),
408 # inline comment is associated with the pair on the same row
409 (
410 b'{\n "alpha": 1, // @req-id: need_001\n "beta": 2\n}\n',
411 '"alpha": 1',
412 ),
413 # block comment is associated with the following pair
414 (
415 b'{\n /* @req-id: need_001 */\n "beta": 2\n}\n',
416 '"beta": 2',
417 ),
418 # trailing comment falls back to the enclosing object
419 (
420 b'{\n "alpha": 1\n // @req-id: need_001\n}\n',
421 '"alpha"',
422 ),
423 ],
424)
425def test_find_associated_scope_jsonc(code, result, init_jsonc_tree_sitter):
426 parser, query = init_jsonc_tree_sitter
427 comments = utils.extract_comments(code, parser, query)
428 node: TreeSitterNode | None = utils.find_associated_scope(
429 comments[0], CommentType.jsonc
430 )
431 assert node
432 assert node.text
433 jsonc_structure = node.text.decode("utf-8")
434 assert result in jsonc_structure
435
436
437@pytest.mark.parametrize(
438 ("code", "result"),
439 [
440 # comment above a POSIX-style function definition
441 (
442 b"""
443 # @req-id: need_001
444 greet() {
445 echo hi
446 }
447 """,
448 "greet()",
449 ),
450 # comment above the `function` keyword form
451 (
452 b"""
453 # @req-id: need_002
454 function greet {
455 echo hi
456 }
457 """,
458 "function greet",
459 ),
460 # comment inside a function body falls back to the enclosing function
461 (
462 b"""
463 greet() {
464 # @req-id: need_003
465 echo hi
466 }
467 """,
468 "greet()",
469 ),
470 ],
471)
472def test_find_associated_scope_bash(code, result, init_bash_tree_sitter):
473 parser, query = init_bash_tree_sitter
474 comments = utils.extract_comments(code, parser, query)
475 node: TreeSitterNode | None = utils.find_associated_scope(
476 comments[0], CommentType.bash
477 )
478 assert node
479 assert node.text
480 func_def = node.text.decode("utf-8")
481 assert func_def.startswith(result)
482
483
484@pytest.mark.parametrize(
485 ("code", "result"),
486 [
487 (
488 b"""
489 def dummy_func1():
490 # @req-id: need_001
491 pass
492 """,
493 "def dummy_func1()",
494 ),
495 (
496 b"""
497 def dummy_func1():
498 '''@req-id: need_001'''
499 pass
500 """,
501 "def dummy_func1()",
502 ),
503 (
504 b"""
505 def dummy_func1():
506 def nested_dummy_func1():
507 '''@req-id: need_001'''
508 pass
509 pass
510 """,
511 "def nested_dummy_func1()",
512 ),
513 (
514 b"""
515 def dummy_func1():
516 '''@req-id: need_001'''
517 def nested_dummy_func1():
518 pass
519 pass
520 """,
521 "def dummy_func1()",
522 ),
523 ],
524)
525def test_find_enclosing_scope_python(code, result, init_python_tree_sitter):
526 parser, query = init_python_tree_sitter
527 comments = utils.extract_comments(code, parser, query)
528 node: TreeSitterNode | None = utils.find_enclosing_scope(
529 comments[0], CommentType.python
530 )
531 assert node
532 assert node.text
533 func_def = node.text.decode("utf-8")
534 assert result in func_def
535
536
537@pytest.mark.parametrize(
538 ("code", "result"),
539 [
540 (
541 b"""
542 # @req-id: need_001
543 def dummy_func1():
544 pass
545 """,
546 "def dummy_func1()",
547 ),
548 (
549 b"""
550 # @req-id: need_001
551 # @req-id: need_002
552 def dummy_func1():
553 pass
554 """,
555 "def dummy_func1()",
556 ),
557 ],
558)
559def test_find_next_scope_python(code, result, init_python_tree_sitter):
560 parser, query = init_python_tree_sitter
561 comments = utils.extract_comments(code, parser, query)
562 node: TreeSitterNode | None = utils.find_next_scope(comments[0], CommentType.python)
563 assert node
564 assert node.text
565 func_def = node.text.decode("utf-8")
566 assert result in func_def
567
568
569@pytest.mark.parametrize(
570 ("code", "result"),
571 [
572 (
573 b"""
574 // @req-id: need_001
575 void dummy_func1(){
576 }
577 """,
578 "void dummy_func1()",
579 ),
580 (
581 b"""
582 /* @req-id: need_001 */
583 void dummy_func1(){
584 }
585 """,
586 "void dummy_func1()",
587 ),
588 ],
589)
590def test_find_next_scope_cpp(code, result, init_cpp_tree_sitter):
591 parser, query = init_cpp_tree_sitter
592 comments = utils.extract_comments(code, parser, query)
593 node: TreeSitterNode | None = utils.find_next_scope(comments[0], CommentType.cpp)
594 assert node
595 assert node.text
596 func_def = node.text.decode("utf-8")
597 assert result in func_def
598
599
600@pytest.mark.parametrize(
601 ("code", "result"),
602 [
603 (
604 b"""
605 // @req-id: need_001
606 public class DummyClass1
607 {
608 }
609 """,
610 "public class DummyClass1",
611 ),
612 (
613 b"""
614
615 public class DummyClass1
616 {
617 /* @req-id: need_001 */
618 /* @req-id: need_002 */
619 public void DummyFunc1()
620 {
621 }
622 }
623 """,
624 "public void DummyFunc1",
625 ),
626 ],
627)
628def test_find_next_scope_csharp(code, result, init_csharp_tree_sitter):
629 parser, query = init_csharp_tree_sitter
630 comments = utils.extract_comments(code, parser, query)
631 node: TreeSitterNode | None = utils.find_next_scope(comments[0], CommentType.cs)
632 assert node
633 assert node.text
634 func_def = node.text.decode("utf-8")
635 assert result in func_def
636
637
638@pytest.mark.parametrize(
639 ("code", "result"),
640 [
641 (
642 b"""
643 void dummy_func1(){
644 // @req-id: need_001
645 }
646 """,
647 "void dummy_func1()",
648 ),
649 (
650 b"""
651 void dummy_func1(){
652 /* @req-id: need_001 */
653 }
654 """,
655 "void dummy_func1()",
656 ),
657 ],
658)
659def test_find_enclosing_scope_cpp(code, result, init_cpp_tree_sitter):
660 parser, query = init_cpp_tree_sitter
661 comments = utils.extract_comments(code, parser, query)
662 node: TreeSitterNode | None = utils.find_enclosing_scope(
663 comments[0], CommentType.cpp
664 )
665 assert node
666 assert node.text
667 func_def = node.text.decode("utf-8")
668 assert result in func_def
669
670
671@pytest.mark.parametrize(
672 ("code", "result"),
673 [
674 (
675 b"""
676 public class DummyClass1
677 {
678 // @req-id: need_001
679 }
680 """,
681 "public class DummyClass1",
682 ),
683 (
684 b"""
685 public class DummyClass1
686 {
687 public void DummyFunc1()
688 {
689 /* @req-id: need_001 */
690 }
691 }
692 """,
693 "public void DummyFunc1()",
694 ),
695 (
696 b"""
697 public class DummyClass1
698 {
699 public string DummyProperty1
700 {
701 get
702 {
703 /* @req-id: need_001 */
704 return "dummy";
705 }
706 }
707 }
708 """,
709 "public string DummyProperty1",
710 ),
711 ],
712)
713def test_find_enclosing_scope_csharp(code, result, init_csharp_tree_sitter):
714 parser, query = init_csharp_tree_sitter
715 comments = utils.extract_comments(code, parser, query)
716 node: TreeSitterNode | None = utils.find_enclosing_scope(
717 comments[0], CommentType.cs
718 )
719 assert node
720 assert node.text
721 func_def = node.text.decode("utf-8")
722 assert result in func_def
723
724
725@pytest.mark.parametrize(
726 ("code", "num_comments", "result"),
727 [
728 (
729 b"""
730 // @req-id: need_001
731 void dummy_func1(){
732 }
733 """,
734 1,
735 "// @req-id: need_001",
736 ),
737 (
738 b"""
739 void dummy_func1(){
740 // @req-id: need_001
741 }
742 """,
743 1,
744 "// @req-id: need_001",
745 ),
746 (
747 b"""
748 /* @req-id: need_001 */
749 void dummy_func1(){
750 }
751 """,
752 1,
753 "/* @req-id: need_001 */",
754 ),
755 (
756 b"""
757 // @req-id: need_001
758 //
759 //
760 void dummy_func1(){
761 }
762 """,
763 3,
764 "// @req-id: need_001",
765 ),
766 ],
767)
768def test_cpp_comment(code, num_comments, result, init_cpp_tree_sitter):
769 parser, query = init_cpp_tree_sitter
770 comments = utils.extract_comments(code, parser, query)
771 assert len(comments) == num_comments
772 comments.sort(key=lambda x: x.start_point.row)
773 assert comments[0].text
774 assert comments[0].text.decode("utf-8") == result
775
776
777@pytest.mark.parametrize(
778 ("code", "num_comments", "result"),
779 [
780 (
781 b"""
782 # @req-id: need_001
783 def dummy_func1():
784 pass
785 """,
786 1,
787 "# @req-id: need_001",
788 ),
789 (
790 b"""
791 def dummy_func1():
792 # @req-id: need_001
793 pass
794 """,
795 1,
796 "# @req-id: need_001",
797 ),
798 (
799 b"""
800 # single line comment
801 # @req-id: need_001
802 def dummy_func1():
803 pass
804 """,
805 2,
806 "# single line comment",
807 ),
808 (
809 b"""
810 def dummy_func1():
811 '''
812 @req-id: need_001
813 '''
814 pass
815 """,
816 1,
817 "'''\n @req-id: need_001\n '''",
818 ),
819 (
820 b"""
821 def dummy_func1():
822 text = '''@req-id: need_001, need_002, this docstring shall not be extracted as comment'''
823 # @req-id: need_001
824 pass
825 """,
826 1,
827 "# @req-id: need_001",
828 ),
829 ],
830)
831def test_python_comment(code, num_comments, result, init_python_tree_sitter):
832 parser, query = init_python_tree_sitter
833 comments: list[TreeSitterNode] = utils.extract_comments(code, parser, query)
834 comments.sort(key=lambda x: x.start_point.row)
835 assert len(comments) == num_comments
836 assert comments[0].text
837 assert comments[0].text.decode("utf-8") == result
838
839
840@pytest.mark.parametrize(
841 ("code", "num_comments", "result"),
842 [
843 (
844 b"""
845 // @req-id: need_001
846 void DummyFunc1(){
847 }
848 """,
849 1,
850 "// @req-id: need_001",
851 ),
852 (
853 b"""
854 void DummyFunc1(){
855 // @req-id: need_001
856 }
857 """,
858 1,
859 "// @req-id: need_001",
860 ),
861 (
862 b"""
863 /* @req-id: need_001 */
864 void DummyFunc1(){
865 }
866 """,
867 1,
868 "/* @req-id: need_001 */",
869 ),
870 (
871 b"""
872 // @req-id: need_001
873 //
874 //
875 void DummyFunc1(){
876 }
877 """,
878 3,
879 "// @req-id: need_001",
880 ),
881 ],
882)
883def test_csharp_comment(code, num_comments, result, init_csharp_tree_sitter):
884 parser, query = init_csharp_tree_sitter
885 comments: list[TreeSitterNode] = utils.extract_comments(code, parser, query)
886 comments.sort(key=lambda x: x.start_point.row)
887 assert len(comments) == num_comments
888 assert comments[0].text
889 assert comments[0].text.decode("utf-8") == result
890
891
892@pytest.mark.parametrize(
893 ("code", "num_comments", "result"),
894 [
895 (
896 b"""
897 # @req-id: need_001
898 database:
899 host: localhost
900 """,
901 1,
902 "# @req-id: need_001",
903 ),
904 (
905 b"""
906 services:
907 web:
908 # @req-id: need_001
909 image: nginx:latest
910 """,
911 1,
912 "# @req-id: need_001",
913 ),
914 (
915 b"""
916 # Top level comment
917 # @req-id: need_001
918 version: "3.8"
919 """,
920 2,
921 "# Top level comment",
922 ),
923 ],
924)
925def test_yaml_comment(code, num_comments, result, init_yaml_tree_sitter):
926 parser, query = init_yaml_tree_sitter
927 comments: list[TreeSitterNode] = utils.extract_comments(code, parser, query)
928 comments.sort(key=lambda x: x.start_point.row)
929 assert len(comments) == num_comments
930 assert comments[0].text
931 assert comments[0].text.decode("utf-8") == result
932
933
934@pytest.mark.parametrize(
935 ("code", "num_comments", "result"),
936 [
937 (
938 b"""
939 // @req-id: need_001
940 func dummyFunc1() {
941 }
942 """,
943 1,
944 "// @req-id: need_001",
945 ),
946 (
947 b"""
948 func dummyFunc1() {
949 // @req-id: need_001
950 }
951 """,
952 1,
953 "// @req-id: need_001",
954 ),
955 (
956 b"""
957 /* @req-id: need_001 */
958 func dummyFunc1() {
959 }
960 """,
961 1,
962 "/* @req-id: need_001 */",
963 ),
964 (
965 b"""
966 // @req-id: need_001
967 //
968 //
969 func dummyFunc1() {
970 }
971 """,
972 3,
973 "// @req-id: need_001",
974 ),
975 ],
976)
977def test_go_comment(code, num_comments, result, init_go_tree_sitter):
978 parser, query = init_go_tree_sitter
979 comments: list[TreeSitterNode] = utils.extract_comments(code, parser, query)
980 comments.sort(key=lambda x: x.start_point.row)
981 assert len(comments) == num_comments
982 assert comments[0].text
983 assert comments[0].text.decode("utf-8") == result
984
985
986@pytest.mark.parametrize(
987 ("code", "result"),
988 [
989 (
990 b"""
991 // @req-id: need_001
992 func dummyFunc1() {
993 }
994 """,
995 "func dummyFunc1()",
996 ),
997 (
998 b"""
999 func dummyFunc2() {
1000 }
1001 // @req-id: need_001
1002 func dummyFunc1() {
1003 }
1004 """,
1005 "func dummyFunc1()",
1006 ),
1007 (
1008 b"""
1009 /* @req-id: need_001 */
1010 func dummyFunc1() {
1011 }
1012 """,
1013 "func dummyFunc1()",
1014 ),
1015 ],
1016)
1017def test_find_associated_scope_go(code, result, init_go_tree_sitter):
1018 parser, query = init_go_tree_sitter
1019 comments = utils.extract_comments(code, parser, query)
1020 node: TreeSitterNode | None = utils.find_associated_scope(
1021 comments[0], CommentType.go
1022 )
1023 assert node
1024 assert node.text
1025 go_def = node.text.decode("utf-8")
1026 assert result in go_def
1027
1028
1029@pytest.mark.parametrize(
1030 ("code", "result"),
1031 [
1032 (
1033 b"""
1034 // @req-id: need_001
1035 func dummyFunc1() {
1036 }
1037 """,
1038 "func dummyFunc1()",
1039 ),
1040 (
1041 b"""
1042 // @req-id: need_001
1043 type DummyStruct struct {
1044 Field int
1045 }
1046 """,
1047 "type DummyStruct struct",
1048 ),
1049 ],
1050)
1051def test_find_next_scope_go(code, result, init_go_tree_sitter):
1052 parser, query = init_go_tree_sitter
1053 comments = utils.extract_comments(code, parser, query)
1054 node: TreeSitterNode | None = utils.find_next_scope(comments[0], CommentType.go)
1055 assert node
1056 assert node.text
1057 go_def = node.text.decode("utf-8")
1058 assert result in go_def
1059
1060
1061@pytest.mark.parametrize(
1062 ("code", "result"),
1063 [
1064 (
1065 b"""
1066 func dummyFunc1() {
1067 // @req-id: need_001
1068 }
1069 """,
1070 "func dummyFunc1()",
1071 ),
1072 (
1073 b"""
1074 func dummyFunc1() {
1075 /* @req-id: need_001 */
1076 }
1077 """,
1078 "func dummyFunc1()",
1079 ),
1080 ],
1081)
1082def test_find_enclosing_scope_go(code, result, init_go_tree_sitter):
1083 parser, query = init_go_tree_sitter
1084 comments = utils.extract_comments(code, parser, query)
1085 node: TreeSitterNode | None = utils.find_enclosing_scope(
1086 comments[0], CommentType.go
1087 )
1088 assert node
1089 assert node.text
1090 go_def = node.text.decode("utf-8")
1091 assert result in go_def
1092
1093
1094@pytest.mark.parametrize(
1095 ("git_url", "rev", "project_path", "filepath", "lineno", "result"),
1096 [
1097 (
1098 "git@github.com:useblocks/sphinx-codelinks.git",
1099 "beef1234",
1100 Path(__file__).parent.parent,
1101 # ABSOLUTE, and under `project_path`. `form_https_url` resolves the
1102 # filepath with `.absolute()`, i.e. against the CWD, so a relative one
1103 # here only agrees with `project_path` when pytest happens to run from
1104 # the package directory -- which it does not when the whole workspace's
1105 # suite is invoked from the repository root
1106 Path(__file__).parent.parent / "example" / "to" / "here",
1107 3,
1108 "https://github.com/useblocks/sphinx-codelinks/blob/beef1234/example/to/here#L3",
1109 )
1110 ],
1111)
1112def test_form_https_url(
1113 git_url, rev, project_path, filepath, lineno, result
1114): # need to have these args
1115 url = utils.form_https_url(git_url, rev, project_path, filepath, lineno=lineno)
1116 assert url == result
1117
1118
1119def get_git_path() -> str:
1120 """Get the path to the git executable."""
1121 git_path = shutil.which("git")
1122 if not git_path:
1123 raise FileNotFoundError("Git executable not found")
1124 if not Path(git_path).is_file():
1125 raise FileNotFoundError("Git executable path is invalid")
1126 return git_path
1127
1128
1129def init_git_repo(repo_path: Path, remote_url: str) -> Path:
1130 """Initialize a git repository for testing."""
1131 git_dir = repo_path / "test_repo"
1132 src_dir = git_dir / "src"
1133 src_dir.mkdir(parents=True)
1134
1135 git_path = get_git_path()
1136 if not git_path:
1137 raise FileNotFoundError("Git executable not found")
1138 if not Path(git_path).is_file():
1139 raise FileNotFoundError("Git executable path is invalid")
1140
1141 # Initialize git repo
1142 subprocess.run([git_path, "init"], cwd=git_dir, check=True, capture_output=True)
1143 subprocess.run(
1144 [git_path, "config", "user.email", "test@example.com"], cwd=git_dir, check=True
1145 )
1146 subprocess.run(
1147 [git_path, "config", "user.name", "Test User"], cwd=git_dir, check=True
1148 )
1149
1150 # Create a test file and commit
1151 test_file = src_dir / "test_file.py"
1152 test_file.write_text("# Test file\nprint('hello')\n")
1153 subprocess.run([git_path, "add", "."], cwd=git_dir, check=True)
1154 subprocess.run(
1155 [git_path, "commit", "-m", "Initial commit"], cwd=git_dir, check=True
1156 )
1157
1158 # Add a remote
1159 subprocess.run(
1160 [git_path, "remote", "add", "origin", remote_url],
1161 cwd=git_dir,
1162 check=True,
1163 )
1164
1165 return git_dir
1166
1167
1168@pytest.fixture(
1169 params=[
1170 ("test_repo_git", "git@github.com:test-user/test-repo.git"),
1171 ("test_repo_https", "https://github.com/test-user/test-repo.git"),
1172 ]
1173)
1174def git_repo(tmp_path: str, request: pytest.FixtureRequest) -> tuple[Path, str]:
1175 """Create git repos for testing."""
1176 repo_name, remote_url = request.param
1177 repo_path = Path(tmp_path) / repo_name
1178 repo_path = init_git_repo(repo_path, remote_url)
1179 return repo_path, remote_url
1180
1181
1182def get_current_commit_hash(git_dir: Path) -> str:
1183 """Get the current commit hash of the git repository."""
1184 git_path = get_git_path()
1185 result = subprocess.run(
1186 [git_path, "rev-parse", "HEAD"],
1187 cwd=git_dir,
1188 check=True,
1189 capture_output=True,
1190 text=True,
1191 )
1192
1193 return str(result.stdout.strip())
1194
1195
1196def test_locate_git_root(git_repo: tuple[Path, str]) -> None:
1197 repo_path = git_repo[0]
1198 src_dir = repo_path / "src"
1199 git_root = utils.locate_git_root(src_dir)
1200 assert git_root == repo_path
1201
1202
1203def test_get_remote_url(git_repo: tuple[Path, str]) -> None:
1204 repo_path, expected_url = git_repo
1205 remote_url = utils.get_remote_url(repo_path)
1206 assert remote_url == expected_url
1207
1208
1209def test_get_current_rev(git_repo: tuple[Path, str]) -> None:
1210 repo_path, _ = git_repo
1211 current_rev = get_current_commit_hash(repo_path)
1212 assert current_rev == utils.get_current_rev(repo_path)
1213
1214
1215@pytest.fixture(params=["detached", "branch"])
1216def git_worktree(
1217 tmp_path: Path, request: pytest.FixtureRequest
1218) -> tuple[Path, str, str]:
1219 """A LINKED WORKTREE of a scratch repository, in both shapes git makes.
1220
1221 A worktree is not a checkout with a `.git` directory: `.git` is a FILE holding
1222 `gitdir: <path>`, `config` lives in the main repository's git directory (named by
1223 the worktree's `commondir`), and only per-worktree state -- `HEAD` among it -- is in
1224 the worktree's own. `detached` puts a SHA in `HEAD`; `branch` puts `ref:
1225 refs/heads/...`, whose target is a shared ref in the common directory.
1226
1227 This repository is worked in worktrees as a matter of routine, so a helper that
1228 demands a `.git` directory fails for every developer while staying green in CI
1229 (useblocks/sphinx-codelinks#106).
1230 """
1231 remote_url = "https://github.com/test-user/test-repo.git"
1232 repo_path = init_git_repo(tmp_path / "main", remote_url)
1233 git_path = get_git_path()
1234 worktree_path = tmp_path / "worktree"
1235 add = [git_path, "worktree", "add"]
1236 add += (
1237 ["--detach", str(worktree_path), "HEAD"]
1238 if request.param == "detached"
1239 else [str(worktree_path), "-b", "a-worktree-branch"]
1240 )
1241 subprocess.run(add, cwd=repo_path, check=True, capture_output=True)
1242 assert (worktree_path / ".git").is_file(), "a linked worktree's .git is a file"
1243 return worktree_path, remote_url, get_current_commit_hash(repo_path)
1244
1245
1246def test_locate_git_root_worktree(git_worktree: tuple[Path, str, str]) -> None:
1247 worktree_path, _, _ = git_worktree
1248 assert utils.locate_git_root(worktree_path / "src") == worktree_path
1249
1250
1251def test_get_remote_url_worktree(git_worktree: tuple[Path, str, str]) -> None:
1252 worktree_path, remote_url, _ = git_worktree
1253 assert utils.get_remote_url(worktree_path) == remote_url
1254
1255
1256def test_get_current_rev_worktree(git_worktree: tuple[Path, str, str]) -> None:
1257 worktree_path, _, rev = git_worktree
1258 assert utils.get_current_rev(worktree_path) == rev
1259
1260
1261def test_get_current_rev_packed_refs(git_worktree: tuple[Path, str, str]) -> None:
1262 """`git gc` packs the loose refs away, and the branch ref is then in `packed-refs`.
1263
1264 Nothing unusual has to happen for this: `git gc` runs unattended on any long-lived
1265 checkout, and afterwards there is no `refs/heads/<branch>` FILE in either the
1266 worktree's git directory or the common one. It is the same failure shape as
1267 useblocks/sphinx-codelinks#106 reached by a different route -- and CI never sees it,
1268 because `actions/checkout` and Read the Docs both leave a detached HEAD.
1269 """
1270 worktree_path, _, rev = git_worktree
1271 git_path = get_git_path()
1272 subprocess.run(
1273 [git_path, "pack-refs", "--all"],
1274 cwd=worktree_path,
1275 check=True,
1276 capture_output=True,
1277 )
1278 git_dir = utils._git_dir(worktree_path)
1279 assert git_dir is not None
1280 common = utils._git_common_dir(git_dir)
1281 assert not list((common / "refs" / "heads").glob("*")), (
1282 "the loose refs should be gone"
1283 )
1284
1285 assert utils.get_current_rev(worktree_path) == rev
1286
1287
1288def test_get_current_rev_detached_head(tmp_path: Path) -> None:
1289 """In a detached HEAD (e.g. CI checkouts) .git/HEAD holds the commit SHA
1290 directly; get_current_rev returns it rather than warning and giving up."""
1291 git_root = tmp_path / "repo"
1292 (git_root / ".git").mkdir(parents=True)
1293 sha = "a1b2c3d4e5f60718293a4b5c6d7e8f9012345678"
1294 (git_root / ".git" / "HEAD").write_text(f"{sha}\n")
1295
1296 assert utils.get_current_rev(git_root) == sha
1297
1298
1299@pytest.mark.parametrize(
1300 ("text", "leading_sequences", "result"),
1301 [
1302 (
1303 """
1304* some text in a comment
1305* some text in a comment
1306*
1307""",
1308 ["*"],
1309 """
1310 some text in a comment
1311 some text in a comment
1312
1313""",
1314 ),
1315 ],
1316)
1317def test_remove_leading_sequences(text, leading_sequences, result):
1318 clean_text = utils.remove_leading_sequences(text, leading_sequences)
1319 assert clean_text == result
1320
1321
1322@pytest.mark.parametrize(
1323 ("text", "rst_markers", "rst_text", "positions"),
1324 [
1325 (
1326 """
1327@rst
1328.. impl:: multiline rst text
1329 :id: IMPL_71
1330@endrst
1331""",
1332 ["@rst", "@endrst"],
1333 f""".. impl:: multiline rst text{UNIX_NEWLINE} :id: IMPL_71{UNIX_NEWLINE}""",
1334 {"row_offset": 1, "start_idx": 6, "end_idx": 51},
1335 ),
1336 (
1337 """
1338@rst.. impl:: oneline rst text@endrst
1339""",
1340 ["@rst", "@endrst"],
1341 """.. impl:: oneline rst text""",
1342 {"row_offset": 1, "start_idx": 5, "end_idx": 31},
1343 ),
1344 (
1345 # a one-line block after a comment prefix: the text before the start
1346 # marker holds no newline, so the block is on the comment's first row
1347 """// @rst .. impl:: oneline rst text @endrst""",
1348 ["@rst", "@endrst"],
1349 """ .. impl:: oneline rst text """,
1350 {"row_offset": 0, "start_idx": 7, "end_idx": 35},
1351 ),
1352 ],
1353)
1354def test_extract_rst(text, rst_markers, rst_text, positions):
1355 extracted_rst = utils.extract_rst(text, rst_markers[0], rst_markers[1])
1356 assert extracted_rst is not None
1357 assert extracted_rst["rst_text"] == rst_text
1358 assert extracted_rst["row_offset"] == positions["row_offset"]
1359 assert extracted_rst["start_idx"] == positions["start_idx"]
1360 assert extracted_rst["end_idx"] == positions["end_idx"]
1361
1362
1363# ========== YAML-specific tests ==========
1364
1365
1366@pytest.mark.parametrize(
1367 ("code", "expected_structure"),
1368 [
1369 # Basic key-value pair
1370 (
1371 b"""
1372 # Comment before key
1373 database:
1374 host: localhost
1375 """,
1376 "database:",
1377 ),
1378 # Comment in nested structure
1379 (
1380 b"""
1381 services:
1382 web:
1383 # Comment before image
1384 image: nginx:latest
1385 """,
1386 "image: nginx:latest",
1387 ),
1388 # Comment before list item
1389 (
1390 b"""
1391 items:
1392 # Comment before list item
1393 - name: item1
1394 value: test
1395 """,
1396 "- name: item1",
1397 ),
1398 # Comment in document structure
1399 (
1400 b"""---
1401# Comment in document
1402version: "3.8"
1403services:
1404 app:
1405 build: .
1406 """,
1407 "version:",
1408 ),
1409 # Flow mapping structure
1410 (
1411 b"""
1412 # Comment before flow mapping
1413 config: {debug: true, port: 8080}
1414 """,
1415 "config:",
1416 ),
1417 ],
1418)
1419def test_find_yaml_next_structure(code, expected_structure, init_yaml_tree_sitter):
1420 """Test the find_yaml_next_structure function."""
1421 parser, query = init_yaml_tree_sitter
1422 comments = utils.extract_comments(code, parser, query)
1423 assert comments, "No comments found in the code"
1424
1425 next_structure = utils.find_yaml_next_structure(comments[0])
1426 assert next_structure, "No next structure found"
1427 structure_text = next_structure.text.decode("utf-8")
1428 assert expected_structure in structure_text
1429
1430
1431@pytest.mark.parametrize(
1432 ("code", "expected_structure"),
1433 [
1434 # Comment associated with key-value pair
1435 (
1436 b"""
1437 # Database configuration
1438 database:
1439 host: localhost
1440 port: 5432
1441 """,
1442 "database:",
1443 ),
1444 # Comment associated with nested structure
1445 (
1446 b"""
1447 services:
1448 web:
1449 # Web service image
1450 image: nginx:latest
1451 ports:
1452 - "80:80"
1453 """,
1454 "image: nginx:latest",
1455 ),
1456 # Comment associated with list item
1457 (
1458 b"""
1459 dependencies:
1460 # First dependency
1461 - name: redis
1462 version: "6.0"
1463 - name: postgres
1464 version: "13"
1465 """,
1466 "- name: redis",
1467 ),
1468 # Comment inside parent structure
1469 (
1470 b"""
1471 app:
1472 # Internal comment
1473 name: myapp
1474 version: "1.0"
1475 """,
1476 "name: myapp",
1477 ),
1478 ],
1479)
1480def test_find_yaml_associated_structure(
1481 code, expected_structure, init_yaml_tree_sitter
1482):
1483 """Test the find_yaml_associated_structure function."""
1484 parser, query = init_yaml_tree_sitter
1485 comments = utils.extract_comments(code, parser, query)
1486 assert comments, "No comments found in the code"
1487
1488 associated_structure = utils.find_yaml_associated_structure(comments[0])
1489 assert associated_structure, "No associated structure found"
1490 structure_text = associated_structure.text.decode("utf-8")
1491 assert expected_structure in structure_text
1492
1493
1494@pytest.mark.parametrize(
1495 ("code", "expected_results"),
1496 [
1497 # Multiple comments in sequence
1498 (
1499 b"""
1500 # First comment
1501 # Second comment
1502 database:
1503 host: localhost
1504 """,
1505 ["database:", "database:"], # Both comments should associate with database
1506 ),
1507 # Comments at different nesting levels
1508 (
1509 b"""
1510 # Top level comment
1511 services:
1512 web:
1513 # Nested comment
1514 image: nginx:latest
1515 """,
1516 ["services:", "image: nginx:latest"],
1517 ),
1518 ],
1519)
1520def test_multiple_yaml_comments(code, expected_results, init_yaml_tree_sitter):
1521 """Test handling of multiple YAML comments in the same file."""
1522 parser, query = init_yaml_tree_sitter
1523 comments = utils.extract_comments(code, parser, query)
1524 comments.sort(key=lambda x: x.start_point.row)
1525
1526 assert len(comments) == len(expected_results), (
1527 f"Expected {len(expected_results)} comments, found {len(comments)}"
1528 )
1529
1530 for i, comment in enumerate(comments):
1531 associated_structure = utils.find_yaml_associated_structure(comment)
1532 assert associated_structure, f"No associated structure found for comment {i}"
1533 structure_text = associated_structure.text.decode("utf-8")
1534 assert expected_results[i] in structure_text
1535
1536
1537@pytest.mark.parametrize(
1538 ("code", "has_structure"),
1539 [
1540 # Comment at end of file with no following structure
1541 (
1542 b"""
1543database:
1544 host: localhost
1545# End of file comment
1546 """,
1547 True, # This will actually find the parent database structure
1548 ),
1549 # Comment with only whitespace after
1550 (
1551 b"""
1552 # Lonely comment
1553
1554
1555 """,
1556 False,
1557 ),
1558 # Comment before valid structure
1559 (
1560 b"""
1561 # Valid comment
1562 key: value
1563 """,
1564 True,
1565 ),
1566 ],
1567)
1568def test_yaml_edge_cases(code, has_structure, init_yaml_tree_sitter):
1569 """Test edge cases in YAML comment processing."""
1570 parser, query = init_yaml_tree_sitter
1571 comments = utils.extract_comments(code, parser, query)
1572
1573 if comments:
1574 structure = utils.find_yaml_associated_structure(comments[0])
1575 if has_structure:
1576 assert structure, "Expected to find associated structure"
1577 else:
1578 assert structure is None, "Expected no associated structure"
1579 else:
1580 assert not has_structure, "No comments found but structure was expected"
1581
1582
1583@pytest.mark.parametrize(
1584 ("code", "expected_structures"),
1585 [
1586 # Simpler nested YAML structure
1587 (
1588 b"""# Global configuration
1589version: "3.8"
1590
1591# Services section
1592services:
1593 web:
1594 image: nginx:latest
1595 # Port configuration
1596 ports:
1597 - "80:80"
1598 """,
1599 [
1600 "version:", # Global configuration
1601 "services:", # Services section
1602 '- "80:80"', # Port configuration
1603 ],
1604 ),
1605 ],
1606)
1607def test_complex_yaml_structure(code, expected_structures, init_yaml_tree_sitter):
1608 """Test complex nested YAML structures with multiple comments."""
1609 parser, query = init_yaml_tree_sitter
1610 comments = utils.extract_comments(code, parser, query)
1611 comments.sort(key=lambda x: x.start_point.row)
1612
1613 assert len(comments) == len(expected_structures), (
1614 f"Expected {len(expected_structures)} comments, found {len(comments)}"
1615 )
1616
1617 for i, comment in enumerate(comments):
1618 associated_structure = utils.find_yaml_associated_structure(comment)
1619 assert associated_structure, f"No associated structure found for comment {i}"
1620 structure_text = associated_structure.text.decode("utf-8")
1621 assert expected_structures[i] in structure_text, (
1622 f"Expected '{expected_structures[i]}' in structure text: '{structure_text}'"
1623 )
1624
1625
1626@pytest.mark.parametrize(
1627 ("code", "expected_type"),
1628 [
1629 # Block mapping pair
1630 (
1631 b"""
1632 # Comment
1633 key: value
1634 """,
1635 "block_mapping_pair",
1636 ),
1637 # Block sequence item
1638 (
1639 b"""
1640 items:
1641 # Comment
1642 - item1
1643 """,
1644 "block_sequence_item",
1645 ),
1646 # Nested block mapping
1647 (
1648 b"""
1649 services:
1650 # Comment
1651 web:
1652 image: nginx
1653 """,
1654 "block_mapping_pair",
1655 ),
1656 ],
1657)
1658def test_yaml_structure_types(code, expected_type, init_yaml_tree_sitter):
1659 """Test that YAML structures return the correct node types."""
1660 parser, query = init_yaml_tree_sitter
1661 comments = utils.extract_comments(code, parser, query)
1662 assert comments, "No comments found"
1663
1664 structure = utils.find_yaml_associated_structure(comments[0])
1665 assert structure, "No associated structure found"
1666 assert structure.type == expected_type, (
1667 f"Expected type {expected_type}, got {structure.type}"
1668 )
1669
1670
1671def test_yaml_document_structure(init_yaml_tree_sitter):
1672 """Test YAML document structure handling."""
1673 code = b"""---
1674# Document comment
1675apiVersion: v1
1676kind: ConfigMap
1677metadata:
1678 name: my-config
1679data:
1680 # Data comment
1681 config.yml: |
1682 setting: value
1683 """
1684
1685 parser, query = init_yaml_tree_sitter
1686 comments = utils.extract_comments(code, parser, query)
1687 comments.sort(key=lambda x: x.start_point.row)
1688
1689 # Should find both comments
1690 assert len(comments) >= 2, f"Expected at least 2 comments, found {len(comments)}"
1691
1692 # First comment should associate with apiVersion
1693 first_structure = utils.find_yaml_associated_structure(comments[0])
1694 assert first_structure, "No structure found for first comment"
1695 first_text = first_structure.text.decode("utf-8")
1696 assert "apiVersion:" in first_text
1697
1698 # Second comment should associate with config.yml
1699 second_structure = utils.find_yaml_associated_structure(comments[1])
1700 assert second_structure, "No structure found for second comment"
1701 second_text = second_structure.text.decode("utf-8")
1702 assert "config.yml:" in second_text
1703
1704
1705def test_yaml_inline_comments_current_behavior(init_yaml_tree_sitter):
1706 """Test improved behavior of inline comments in YAML after the fix."""
1707 code = b"""key1: value1 # inline comment about key1
1708key2: value2
1709key3: value3 # inline comment about key3
1710"""
1711
1712 parser, query = init_yaml_tree_sitter
1713 comments = utils.extract_comments(code, parser, query)
1714 comments.sort(key=lambda x: x.start_point.row)
1715
1716 assert len(comments) == 2, f"Expected 2 comments, found {len(comments)}"
1717
1718 # Fixed behavior: inline comment about key1 now correctly associates with key1
1719 first_structure = utils.find_yaml_associated_structure(comments[0])
1720 assert first_structure, "No structure found for first comment"
1721 first_text = first_structure.text.decode("utf-8")
1722 assert "key1:" in first_text, f"Expected 'key1:' in '{first_text}'"
1723
1724 # Fixed behavior: inline comment about key3 now correctly associates with key3
1725 second_structure = utils.find_yaml_associated_structure(comments[1])
1726 assert second_structure, "No structure found for second comment"
1727 second_text = second_structure.text.decode("utf-8")
1728 assert "key3:" in second_text, f"Expected 'key3:' in '{second_text}'"
1729
1730
1731@pytest.mark.parametrize(
1732 ("code", "expected_associations"),
1733 [
1734 # Basic inline comment case
1735 (
1736 b"""key1: value1 # comment about key1
1737key2: value2
1738 """,
1739 ["key1:"], # Now correctly associates with key1
1740 ),
1741 # Multiple inline comments
1742 (
1743 b"""database:
1744 host: localhost # production server
1745 port: 5432 # default postgres port
1746 user: admin
1747 """,
1748 [
1749 "host: localhost",
1750 "port: 5432",
1751 ], # Now correctly associates with the right structures
1752 ),
1753 ],
1754)
1755def test_yaml_inline_comments_fixed_behavior(
1756 code, expected_associations, init_yaml_tree_sitter
1757):
1758 """Test that inline comments now correctly associate with the structure they comment on."""
1759 parser, query = init_yaml_tree_sitter
1760 comments = utils.extract_comments(code, parser, query)
1761 comments.sort(key=lambda x: x.start_point.row)
1762
1763 assert len(comments) == len(expected_associations), (
1764 f"Expected {len(expected_associations)} comments, found {len(comments)}"
1765 )
1766
1767 for i, comment in enumerate(comments):
1768 structure = utils.find_yaml_associated_structure(comment)
1769 assert structure, f"No structure found for comment {i}"
1770 structure_text = structure.text.decode("utf-8")
1771 assert expected_associations[i] in structure_text, (
1772 f"Expected '{expected_associations[i]}' in structure text: '{structure_text}'"
1773 )
1774
1775
1776@pytest.mark.parametrize(
1777 ("code", "expected_associations"),
1778 [
1779 # Inline comments with list items
1780 (
1781 b"""items:
1782 - name: item1 # first item
1783 - name: item2 # second item
1784 """,
1785 [
1786 "name: item1",
1787 "name: item2",
1788 ], # The inline comment finds the key-value pair within the list item
1789 ),
1790 # Mixed inline and block comments
1791 (
1792 b"""# Block comment for database
1793database:
1794 host: localhost # inline comment for host
1795 port: 5432
1796 # Block comment for user
1797 user: admin
1798 """,
1799 ["database:", "host: localhost", "user: admin"],
1800 ),
1801 # Inline comments in nested structures
1802 (
1803 b"""services:
1804 web:
1805 image: nginx # web server image
1806 ports:
1807 - "80:80" # http port
1808 """,
1809 ["image: nginx", '- "80:80"'],
1810 ),
1811 ],
1812)
1813def test_yaml_inline_comments_comprehensive(
1814 code, expected_associations, init_yaml_tree_sitter
1815):
1816 """Comprehensive test for inline comment behavior in various YAML structures."""
1817 parser, query = init_yaml_tree_sitter
1818 comments = utils.extract_comments(code, parser, query)
1819 comments.sort(key=lambda x: x.start_point.row)
1820
1821 assert len(comments) == len(expected_associations), (
1822 f"Expected {len(expected_associations)} comments, found {len(comments)}"
1823 )
1824
1825 for i, comment in enumerate(comments):
1826 structure = utils.find_yaml_associated_structure(comment)
1827 assert structure, (
1828 f"No structure found for comment {i}: '{comment.text.decode('utf-8')}'"
1829 )
1830 structure_text = structure.text.decode("utf-8")
1831 assert expected_associations[i] in structure_text, (
1832 f"Comment {i} '{comment.text.decode('utf-8')}' -> Expected '{expected_associations[i]}' in '{structure_text}'"
1833 )