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