[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]
   2from pathlib import Path
   3import shutil
   4import subprocess
   5
   6import pytest
   7from tree_sitter import Language, Parser, Query
   8from tree_sitter import Node as TreeSitterNode
   9import tree_sitter_c_sharp
  10import tree_sitter_cpp
  11import tree_sitter_python
  12import tree_sitter_rust
  13import tree_sitter_yaml
  14
  15from sphinx_codelinks.analyse import utils
  16from sphinx_codelinks.config import UNIX_NEWLINE
  17from sphinx_codelinks.source_discover.config import CommentType
  18
  19
  20@pytest.fixture(scope="session")
  21def init_cpp_tree_sitter() -> tuple[Parser, Query]:
  22    parsed_language = Language(tree_sitter_cpp.language())
  23    query = Query(parsed_language, utils.CPP_QUERY)
  24    parser = Parser(parsed_language)
  25    return parser, query
  26
  27
  28@pytest.fixture(scope="session")
  29def init_python_tree_sitter() -> tuple[Parser, Query]:
  30    parsed_language = Language(tree_sitter_python.language())
  31    query = Query(parsed_language, utils.PYTHON_QUERY)
  32    parser = Parser(parsed_language)
  33    return parser, query
  34
  35
  36@pytest.fixture(scope="session")
  37def init_csharp_tree_sitter() -> tuple[Parser, Query]:
  38    parsed_language = Language(tree_sitter_c_sharp.language())
  39    query = Query(parsed_language, utils.C_SHARP_QUERY)
  40    parser = Parser(parsed_language)
  41    return parser, query
  42
  43
  44@pytest.fixture(scope="session")
  45def init_yaml_tree_sitter() -> tuple[Parser, Query]:
  46    parsed_language = Language(tree_sitter_yaml.language())
  47    query = Query(parsed_language, utils.YAML_QUERY)
  48    parser = Parser(parsed_language)
  49    return parser, query
  50
  51
  52@pytest.fixture(scope="session")
  53def init_rust_tree_sitter() -> tuple[Parser, Query]:
  54    parsed_language = Language(tree_sitter_rust.language())
  55    query = Query(parsed_language, utils.RUST_QUERY)
  56    parser = Parser(parsed_language)
  57    return parser, query
  58
  59
  60@pytest.mark.parametrize(
  61    ("code", "result"),
  62    [
  63        (
  64            b"""
  65                // @req-id: need_001
  66                void dummy_func1(){
  67                }
  68            """,
  69            "void dummy_func1()",
  70        ),
  71        (
  72            b"""
  73                void dummy_func2(){
  74                }
  75                // @req-id: need_001
  76                void dummy_func1(){
  77                }
  78            """,
  79            "void dummy_func1()",
  80        ),
  81        (
  82            b"""
  83                void dummy_func1(){
  84                    a = 1;
  85                    /* @req-id: need_001 */
  86                }
  87            """,
  88            "void dummy_func1()",
  89        ),
  90        (
  91            b"""
  92                void dummy_func1(){
  93                    // @req-id: need_001
  94                    a = 1;
  95                }
  96                void dummy_func2(){
  97                }
  98            """,
  99            "void dummy_func1()",
 100        ),
 101    ],
 102)
 103def test_find_associated_scope_cpp(code, result, init_cpp_tree_sitter):
 104    parser, query = init_cpp_tree_sitter
 105    comments = utils.extract_comments(code, parser, query)
 106    node: TreeSitterNode | None = utils.find_associated_scope(
 107        comments[0], CommentType.cpp
 108    )
 109    assert node
 110    assert node.text
 111    func_def = node.text.decode("utf-8")
 112    assert result in func_def
 113
 114
 115@pytest.mark.parametrize(
 116    ("code", "result"),
 117    [
 118        (
 119            b"""
 120                def dummy_func1():
 121                    # @req-id: need_001
 122                    pass
 123            """,
 124            "def dummy_func1()",
 125        ),
 126        (
 127            b"""
 128                def dummy_func1():
 129                    # @req-id: need_002
 130                    def dummy_func2():
 131                        pass
 132                    pass
 133            """,
 134            "def dummy_func2()",
 135        ),
 136        (
 137            b"""
 138                def dummy_func1():
 139                '''@req-id: need_002'''
 140                    def nested_dummy_func():
 141                        pass
 142                    pass
 143            """,
 144            "def dummy_func1()",
 145        ),
 146        (
 147            b"""
 148                def dummy_func1():
 149                    def nested_dummy_func():
 150                        '''@req-id: need_002'''
 151                        pass
 152                    pass
 153            """,
 154            "def nested_dummy_func()",
 155        ),
 156        (
 157            b"""
 158                def dummy_func1():
 159                    def nested_dummy_func():
 160                        # @req-id: need_002
 161                        pass
 162                    pass
 163            """,
 164            "def nested_dummy_func()",
 165        ),
 166        (
 167            b"""
 168                def dummy_func1():
 169                    def nested_dummy_func():
 170                        pass
 171                    # @req-id: need_002
 172                    pass
 173            """,
 174            "def dummy_func1()",
 175        ),
 176    ],
 177)
 178def test_find_associated_scope_python(code, result, init_python_tree_sitter):
 179    parser, query = init_python_tree_sitter
 180    comments = utils.extract_comments(code, parser, query)
 181    node: TreeSitterNode | None = utils.find_associated_scope(
 182        comments[0], CommentType.python
 183    )
 184    assert node
 185    assert node.text
 186    func_def = node.text.decode("utf-8")
 187    assert func_def.startswith(result)
 188
 189
 190@pytest.mark.parametrize(
 191    ("code", "result"),
 192    [
 193        (
 194            b"""
 195                // @req-id: need_001
 196                public class DummyClass1
 197                {
 198                }
 199            """,
 200            "public class DummyClass1",
 201        ),
 202        (
 203            b"""
 204                public class DummyClass2
 205                {
 206                    // @req-id: need_001
 207                    public void DummyFunc2()
 208                    {
 209                    }
 210                }
 211            """,
 212            "public void DummyFunc2",
 213        ),
 214        (
 215            b"""
 216                public class DummyClass3
 217                {
 218                    // @req-id: need_001
 219                    public string Property1 { get; set; }
 220                }
 221            """,
 222            "public string Property1",
 223        ),
 224    ],
 225)
 226def test_find_associated_scope_csharp(code, result, init_csharp_tree_sitter):
 227    parser, query = init_csharp_tree_sitter
 228    comments = utils.extract_comments(code, parser, query)
 229    node: TreeSitterNode | None = utils.find_associated_scope(
 230        comments[0], CommentType.cs
 231    )
 232    assert node
 233    assert node.text
 234    func_def = node.text.decode("utf-8")
 235    assert func_def.startswith(result)
 236
 237
 238@pytest.mark.parametrize(
 239    ("code", "result"),
 240    [
 241        (
 242            b"""
 243                # @req-id: need_001
 244                database:
 245                  host: localhost
 246                  port: 5432
 247            """,
 248            "database:",
 249        ),
 250        (
 251            b"""
 252                services:
 253                  web:
 254                    # @req-id: need_002
 255                    image: nginx:latest
 256                    ports:
 257                      - "80:80"
 258            """,
 259            "image: nginx:latest",
 260        ),
 261        (
 262            b"""
 263                # @req-id: need_003
 264                version: "3.8"
 265                services:
 266                  app:
 267                    build: .
 268            """,
 269            "version:",
 270        ),
 271        (
 272            b"""
 273                items:
 274                  # @req-id: need_004
 275                  - name: item1
 276                    value: test
 277                  - name: item2
 278                    value: test2
 279            """,
 280            "- name: item1",
 281        ),
 282    ],
 283)
 284def test_find_associated_scope_yaml(code, result, init_yaml_tree_sitter):
 285    parser, query = init_yaml_tree_sitter
 286    comments = utils.extract_comments(code, parser, query)
 287    node: TreeSitterNode | None = utils.find_associated_scope(
 288        comments[0], CommentType.yaml
 289    )
 290    assert node
 291    assert node.text
 292    yaml_structure = node.text.decode("utf-8")
 293    assert result in yaml_structure
 294
 295
 296@pytest.mark.parametrize(
 297    ("code", "result"),
 298    [
 299        (
 300            b"""
 301                // @req-id: need_001
 302                fn dummy_func1() {
 303                }
 304            """,
 305            "fn dummy_func1()",
 306        ),
 307        (
 308            b"""
 309                fn dummy_func2() {
 310                }
 311                // @req-id: need_001
 312                fn dummy_func1() {
 313                }
 314            """,
 315            "fn dummy_func1()",
 316        ),
 317        (
 318            b"""
 319                fn dummy_func1() {
 320                    let a = 1;
 321                    /* @req-id: need_001 */
 322                }
 323            """,
 324            "fn dummy_func1()",
 325        ),
 326        (
 327            b"""
 328                fn dummy_func1() {
 329                    // @req-id: need_001
 330                    let a = 1;
 331                }
 332                fn dummy_func2() {
 333                }
 334            """,
 335            "fn dummy_func1()",
 336        ),
 337        (
 338            b"""
 339                /// @req-id: need_001
 340                fn dummy_func1() {
 341                }
 342            """,
 343            "fn dummy_func1()",
 344        ),
 345        (
 346            b"""
 347                struct MyStruct {
 348                    // @req-id: need_001
 349                    field: i32,
 350                }
 351            """,
 352            "struct MyStruct",
 353        ),
 354    ],
 355)
 356def test_find_associated_scope_rust(code, result, init_rust_tree_sitter):
 357    parser, query = init_rust_tree_sitter
 358    comments = utils.extract_comments(code, parser, query)
 359    node: TreeSitterNode | None = utils.find_associated_scope(
 360        comments[0], CommentType.rust
 361    )
 362    assert node
 363    assert node.text
 364    rust_def = node.text.decode("utf-8")
 365    assert result in rust_def
 366
 367
 368@pytest.mark.parametrize(
 369    ("code", "result"),
 370    [
 371        (
 372            b"""
 373                def dummy_func1():
 374                    # @req-id: need_001
 375                    pass
 376            """,
 377            "def dummy_func1()",
 378        ),
 379        (
 380            b"""
 381                def dummy_func1():
 382                '''@req-id: need_001'''
 383                    pass
 384            """,
 385            "def dummy_func1()",
 386        ),
 387        (
 388            b"""
 389                def dummy_func1():
 390                    def nested_dummy_func1():
 391                        '''@req-id: need_001'''
 392                        pass
 393                    pass
 394            """,
 395            "def nested_dummy_func1()",
 396        ),
 397        (
 398            b"""
 399                def dummy_func1():
 400                    '''@req-id: need_001'''
 401                    def nested_dummy_func1():
 402                        pass
 403                    pass
 404            """,
 405            "def dummy_func1()",
 406        ),
 407    ],
 408)
 409def test_find_enclosing_scope_python(code, result, init_python_tree_sitter):
 410    parser, query = init_python_tree_sitter
 411    comments = utils.extract_comments(code, parser, query)
 412    node: TreeSitterNode | None = utils.find_enclosing_scope(
 413        comments[0], CommentType.python
 414    )
 415    assert node
 416    assert node.text
 417    func_def = node.text.decode("utf-8")
 418    assert result in func_def
 419
 420
 421@pytest.mark.parametrize(
 422    ("code", "result"),
 423    [
 424        (
 425            b"""
 426                # @req-id: need_001
 427                def dummy_func1():
 428                    pass
 429            """,
 430            "def dummy_func1()",
 431        ),
 432        (
 433            b"""
 434                # @req-id: need_001
 435                # @req-id: need_002
 436                def dummy_func1():
 437                    pass
 438            """,
 439            "def dummy_func1()",
 440        ),
 441    ],
 442)
 443def test_find_next_scope_python(code, result, init_python_tree_sitter):
 444    parser, query = init_python_tree_sitter
 445    comments = utils.extract_comments(code, parser, query)
 446    node: TreeSitterNode | None = utils.find_next_scope(comments[0], CommentType.python)
 447    assert node
 448    assert node.text
 449    func_def = node.text.decode("utf-8")
 450    assert result in func_def
 451
 452
 453@pytest.mark.parametrize(
 454    ("code", "result"),
 455    [
 456        (
 457            b"""
 458                // @req-id: need_001
 459                void dummy_func1(){
 460                }
 461            """,
 462            "void dummy_func1()",
 463        ),
 464        (
 465            b"""
 466                /* @req-id: need_001 */
 467                void dummy_func1(){
 468                }
 469            """,
 470            "void dummy_func1()",
 471        ),
 472    ],
 473)
 474def test_find_next_scope_cpp(code, result, init_cpp_tree_sitter):
 475    parser, query = init_cpp_tree_sitter
 476    comments = utils.extract_comments(code, parser, query)
 477    node: TreeSitterNode | None = utils.find_next_scope(comments[0], CommentType.cpp)
 478    assert node
 479    assert node.text
 480    func_def = node.text.decode("utf-8")
 481    assert result in func_def
 482
 483
 484@pytest.mark.parametrize(
 485    ("code", "result"),
 486    [
 487        (
 488            b"""
 489                // @req-id: need_001
 490                public class DummyClass1
 491                {
 492                }
 493            """,
 494            "public class DummyClass1",
 495        ),
 496        (
 497            b"""
 498
 499                public class DummyClass1
 500                {
 501                    /* @req-id: need_001 */
 502                    /* @req-id: need_002 */
 503                    public void DummyFunc1()
 504                    {
 505                    }
 506                }
 507            """,
 508            "public void DummyFunc1",
 509        ),
 510    ],
 511)
 512def test_find_next_scope_csharp(code, result, init_csharp_tree_sitter):
 513    parser, query = init_csharp_tree_sitter
 514    comments = utils.extract_comments(code, parser, query)
 515    node: TreeSitterNode | None = utils.find_next_scope(comments[0], CommentType.cs)
 516    assert node
 517    assert node.text
 518    func_def = node.text.decode("utf-8")
 519    assert result in func_def
 520
 521
 522@pytest.mark.parametrize(
 523    ("code", "result"),
 524    [
 525        (
 526            b"""
 527                void dummy_func1(){
 528                // @req-id: need_001
 529                }
 530            """,
 531            "void dummy_func1()",
 532        ),
 533        (
 534            b"""
 535                void dummy_func1(){
 536                /* @req-id: need_001 */
 537                }
 538            """,
 539            "void dummy_func1()",
 540        ),
 541    ],
 542)
 543def test_find_enclosing_scope_cpp(code, result, init_cpp_tree_sitter):
 544    parser, query = init_cpp_tree_sitter
 545    comments = utils.extract_comments(code, parser, query)
 546    node: TreeSitterNode | None = utils.find_enclosing_scope(
 547        comments[0], CommentType.cpp
 548    )
 549    assert node
 550    assert node.text
 551    func_def = node.text.decode("utf-8")
 552    assert result in func_def
 553
 554
 555@pytest.mark.parametrize(
 556    ("code", "result"),
 557    [
 558        (
 559            b"""
 560                public class DummyClass1
 561                {
 562                    // @req-id: need_001
 563                }
 564            """,
 565            "public class DummyClass1",
 566        ),
 567        (
 568            b"""
 569                public class DummyClass1
 570                {
 571                    public void DummyFunc1()
 572                    {
 573                        /* @req-id: need_001 */
 574                    }
 575                }
 576            """,
 577            "public void DummyFunc1()",
 578        ),
 579        (
 580            b"""
 581                public class DummyClass1
 582                {
 583                    public string DummyProperty1
 584                    {
 585                        get
 586                        {
 587                            /* @req-id: need_001 */
 588                            return "dummy";
 589                        }
 590                    }
 591                }
 592            """,
 593            "public string DummyProperty1",
 594        ),
 595    ],
 596)
 597def test_find_enclosing_scope_csharp(code, result, init_csharp_tree_sitter):
 598    parser, query = init_csharp_tree_sitter
 599    comments = utils.extract_comments(code, parser, query)
 600    node: TreeSitterNode | None = utils.find_enclosing_scope(
 601        comments[0], CommentType.cs
 602    )
 603    assert node
 604    assert node.text
 605    func_def = node.text.decode("utf-8")
 606    assert result in func_def
 607
 608
 609@pytest.mark.parametrize(
 610    ("code", "num_comments", "result"),
 611    [
 612        (
 613            b"""
 614                // @req-id: need_001
 615                void dummy_func1(){
 616                }
 617            """,
 618            1,
 619            "// @req-id: need_001",
 620        ),
 621        (
 622            b"""
 623                void dummy_func1(){
 624                // @req-id: need_001
 625                }
 626            """,
 627            1,
 628            "// @req-id: need_001",
 629        ),
 630        (
 631            b"""
 632                /* @req-id: need_001 */
 633                void dummy_func1(){
 634                }
 635            """,
 636            1,
 637            "/* @req-id: need_001 */",
 638        ),
 639        (
 640            b"""
 641                //  @req-id: need_001
 642                //
 643                //
 644                void dummy_func1(){
 645                }
 646            """,
 647            3,
 648            "//  @req-id: need_001",
 649        ),
 650    ],
 651)
 652def test_cpp_comment(code, num_comments, result, init_cpp_tree_sitter):
 653    parser, query = init_cpp_tree_sitter
 654    comments = utils.extract_comments(code, parser, query)
 655    assert len(comments) == num_comments
 656    comments.sort(key=lambda x: x.start_point.row)
 657    assert comments[0].text
 658    assert comments[0].text.decode("utf-8") == result
 659
 660
 661@pytest.mark.parametrize(
 662    ("code", "num_comments", "result"),
 663    [
 664        (
 665            b"""
 666                # @req-id: need_001
 667                def dummy_func1():
 668                    pass
 669            """,
 670            1,
 671            "# @req-id: need_001",
 672        ),
 673        (
 674            b"""
 675                def dummy_func1():
 676                    # @req-id: need_001
 677                    pass
 678            """,
 679            1,
 680            "# @req-id: need_001",
 681        ),
 682        (
 683            b"""
 684                # single line comment
 685                # @req-id: need_001
 686                def dummy_func1():
 687                    pass
 688            """,
 689            2,
 690            "# single line comment",
 691        ),
 692        (
 693            b"""
 694                def dummy_func1():
 695                    '''
 696                    @req-id: need_001
 697                    '''
 698                    pass
 699            """,
 700            1,
 701            "'''\n                    @req-id: need_001\n                    '''",
 702        ),
 703        (
 704            b"""
 705                def dummy_func1():
 706                    text = '''@req-id: need_001, need_002, this docstring shall not be extracted as comment'''
 707                    # @req-id: need_001
 708                    pass
 709            """,
 710            1,
 711            "# @req-id: need_001",
 712        ),
 713    ],
 714)
 715def test_python_comment(code, num_comments, result, init_python_tree_sitter):
 716    parser, query = init_python_tree_sitter
 717    comments: list[TreeSitterNode] = utils.extract_comments(code, parser, query)
 718    comments.sort(key=lambda x: x.start_point.row)
 719    assert len(comments) == num_comments
 720    assert comments[0].text
 721    assert comments[0].text.decode("utf-8") == result
 722
 723
 724@pytest.mark.parametrize(
 725    ("code", "num_comments", "result"),
 726    [
 727        (
 728            b"""
 729                // @req-id: need_001
 730                void DummyFunc1(){
 731                }
 732            """,
 733            1,
 734            "// @req-id: need_001",
 735        ),
 736        (
 737            b"""
 738                void DummyFunc1(){
 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 DummyFunc1(){
 749                }
 750            """,
 751            1,
 752            "/* @req-id: need_001 */",
 753        ),
 754        (
 755            b"""
 756                //  @req-id: need_001
 757                //
 758                //
 759                void DummyFunc1(){
 760                }
 761            """,
 762            3,
 763            "//  @req-id: need_001",
 764        ),
 765    ],
 766)
 767def test_csharp_comment(code, num_comments, result, init_csharp_tree_sitter):
 768    parser, query = init_csharp_tree_sitter
 769    comments: list[TreeSitterNode] = utils.extract_comments(code, parser, query)
 770    comments.sort(key=lambda x: x.start_point.row)
 771    assert len(comments) == num_comments
 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                database:
 783                  host: localhost
 784            """,
 785            1,
 786            "# @req-id: need_001",
 787        ),
 788        (
 789            b"""
 790                services:
 791                  web:
 792                    # @req-id: need_001
 793                    image: nginx:latest
 794            """,
 795            1,
 796            "# @req-id: need_001",
 797        ),
 798        (
 799            b"""
 800                # Top level comment
 801                # @req-id: need_001
 802                version: "3.8"
 803            """,
 804            2,
 805            "# Top level comment",
 806        ),
 807    ],
 808)
 809def test_yaml_comment(code, num_comments, result, init_yaml_tree_sitter):
 810    parser, query = init_yaml_tree_sitter
 811    comments: list[TreeSitterNode] = utils.extract_comments(code, parser, query)
 812    comments.sort(key=lambda x: x.start_point.row)
 813    assert len(comments) == num_comments
 814    assert comments[0].text
 815    assert comments[0].text.decode("utf-8") == result
 816
 817
 818@pytest.mark.parametrize(
 819    ("git_url", "rev", "project_path", "filepath", "lineno", "result"),
 820    [
 821        (
 822            "git@github.com:useblocks/sphinx-codelinks.git",
 823            "beef1234",
 824            Path(__file__).parent.parent,
 825            Path("example") / "to" / "here",
 826            3,
 827            "https://github.com/useblocks/sphinx-codelinks/blob/beef1234/example/to/here#L3",
 828        )
 829    ],
 830)
 831def test_form_https_url(git_url, rev, project_path, filepath, lineno, result):  # noqa: PLR0913  # need to have these args
 832    url = utils.form_https_url(git_url, rev, project_path, filepath, lineno=lineno)
 833    assert url == result
 834
 835
 836def get_git_path() -> str:
 837    """Get the path to the git executable."""
 838    git_path = shutil.which("git")
 839    if not git_path:
 840        raise FileNotFoundError("Git executable not found")
 841    if not Path(git_path).is_file():
 842        raise FileNotFoundError("Git executable path is invalid")
 843    return git_path
 844
 845
 846def init_git_repo(repo_path: Path, remote_url: str) -> Path:
 847    """Initialize a git repository for testing."""
 848    git_dir = repo_path / "test_repo"
 849    src_dir = git_dir / "src"
 850    src_dir.mkdir(parents=True)
 851
 852    git_path = get_git_path()
 853    if not git_path:
 854        raise FileNotFoundError("Git executable not found")
 855    if not Path(git_path).is_file():
 856        raise FileNotFoundError("Git executable path is invalid")
 857
 858    # Initialize git repo
 859    subprocess.run([git_path, "init"], cwd=git_dir, check=True, capture_output=True)  # noqa: S603
 860    subprocess.run(  # noqa: S603
 861        [git_path, "config", "user.email", "test@example.com"], cwd=git_dir, check=True
 862    )
 863    subprocess.run(  # noqa: S603
 864        [git_path, "config", "user.name", "Test User"], cwd=git_dir, check=True
 865    )
 866
 867    # Create a test file and commit
 868    test_file = src_dir / "test_file.py"
 869    test_file.write_text("# Test file\nprint('hello')\n")
 870    subprocess.run([git_path, "add", "."], cwd=git_dir, check=True)  # noqa: S603
 871    subprocess.run(  # noqa: S603
 872        [git_path, "commit", "-m", "Initial commit"], cwd=git_dir, check=True
 873    )
 874
 875    # Add a remote
 876    subprocess.run(  # noqa: S603
 877        [git_path, "remote", "add", "origin", remote_url],
 878        cwd=git_dir,
 879        check=True,
 880    )
 881
 882    return git_dir
 883
 884
 885@pytest.fixture(
 886    params=[
 887        ("test_repo_git", "git@github.com:test-user/test-repo.git"),
 888        ("test_repo_https", "https://github.com/test-user/test-repo.git"),
 889    ]
 890)
 891def git_repo(tmp_path: str, request: pytest.FixtureRequest) -> tuple[Path, str]:
 892    """Create git repos for testing."""
 893    repo_name, remote_url = request.param
 894    repo_path = Path(tmp_path) / repo_name
 895    repo_path = init_git_repo(repo_path, remote_url)
 896    return repo_path, remote_url
 897
 898
 899def get_current_commit_hash(git_dir: Path) -> str:
 900    """Get the current commit hash of the git repository."""
 901    git_path = get_git_path()
 902    result = subprocess.run(  # noqa: S603
 903        [git_path, "rev-parse", "HEAD"],
 904        cwd=git_dir,
 905        check=True,
 906        capture_output=True,
 907        text=True,
 908    )
 909
 910    return str(result.stdout.strip())
 911
 912
 913def test_locate_git_root(git_repo: tuple[Path, str]) -> None:
 914    repo_path = git_repo[0]
 915    src_dir = repo_path / "src"
 916    git_root = utils.locate_git_root(src_dir)
 917    assert git_root == repo_path
 918
 919
 920def test_get_remote_url(git_repo: tuple[Path, str]) -> None:
 921    repo_path, expected_url = git_repo
 922    remote_url = utils.get_remote_url(repo_path)
 923    assert remote_url == expected_url
 924
 925
 926def test_get_current_rev(git_repo: tuple[Path, str]) -> None:
 927    repo_path, _ = git_repo
 928    current_rev = get_current_commit_hash(repo_path)
 929    assert current_rev == utils.get_current_rev(repo_path)
 930
 931
 932@pytest.mark.parametrize(
 933    ("text", "leading_sequences", "result"),
 934    [
 935        (
 936            """
 937*    some text in a comment
 938*    some text in a comment
 939*
 940""",
 941            ["*"],
 942            """
 943    some text in a comment
 944    some text in a comment
 945
 946""",
 947        ),
 948    ],
 949)
 950def test_remove_leading_sequences(text, leading_sequences, result):
 951    clean_text = utils.remove_leading_sequences(text, leading_sequences)
 952    assert clean_text == result
 953
 954
 955@pytest.mark.parametrize(
 956    ("text", "rst_markers", "rst_text", "positions"),
 957    [
 958        (
 959            """
 960@rst
 961.. impl:: multiline rst text
 962   :id: IMPL_71
 963@endrst
 964""",
 965            ["@rst", "@endrst"],
 966            f""".. impl:: multiline rst text{UNIX_NEWLINE}   :id: IMPL_71{UNIX_NEWLINE}""",
 967            {"row_offset": 1, "start_idx": 6, "end_idx": 51},
 968        ),
 969        (
 970            """
 971@rst.. impl:: oneline rst text@endrst
 972""",
 973            ["@rst", "@endrst"],
 974            """.. impl:: oneline rst text""",
 975            {"row_offset": 0, "start_idx": 5, "end_idx": 31},
 976        ),
 977    ],
 978)
 979def test_extract_rst(text, rst_markers, rst_text, positions):
 980    extracted_rst = utils.extract_rst(text, rst_markers[0], rst_markers[1])
 981    assert extracted_rst is not None
 982    assert extracted_rst["rst_text"] == rst_text
 983    assert extracted_rst["start_idx"] == positions["start_idx"]
 984    assert extracted_rst["end_idx"] == positions["end_idx"]
 985
 986
 987# ========== YAML-specific tests ==========
 988
 989
 990@pytest.mark.parametrize(
 991    ("code", "expected_structure"),
 992    [
 993        # Basic key-value pair
 994        (
 995            b"""
 996                # Comment before key
 997                database:
 998                  host: localhost
 999            """,
1000            "database:",
1001        ),
1002        # Comment in nested structure
1003        (
1004            b"""
1005                services:
1006                  web:
1007                    # Comment before image
1008                    image: nginx:latest
1009            """,
1010            "image: nginx:latest",
1011        ),
1012        # Comment before list item
1013        (
1014            b"""
1015                items:
1016                  # Comment before list item
1017                  - name: item1
1018                    value: test
1019            """,
1020            "- name: item1",
1021        ),
1022        # Comment in document structure
1023        (
1024            b"""---
1025# Comment in document
1026version: "3.8"
1027services:
1028  app:
1029    build: .
1030            """,
1031            "version:",
1032        ),
1033        # Flow mapping structure
1034        (
1035            b"""
1036                # Comment before flow mapping
1037                config: {debug: true, port: 8080}
1038            """,
1039            "config:",
1040        ),
1041    ],
1042)
1043def test_find_yaml_next_structure(code, expected_structure, init_yaml_tree_sitter):
1044    """Test the find_yaml_next_structure function."""
1045    parser, query = init_yaml_tree_sitter
1046    comments = utils.extract_comments(code, parser, query)
1047    assert comments, "No comments found in the code"
1048
1049    next_structure = utils.find_yaml_next_structure(comments[0])
1050    assert next_structure, "No next structure found"
1051    structure_text = next_structure.text.decode("utf-8")
1052    assert expected_structure in structure_text
1053
1054
1055@pytest.mark.parametrize(
1056    ("code", "expected_structure"),
1057    [
1058        # Comment associated with key-value pair
1059        (
1060            b"""
1061                # Database configuration
1062                database:
1063                  host: localhost
1064                  port: 5432
1065            """,
1066            "database:",
1067        ),
1068        # Comment associated with nested structure
1069        (
1070            b"""
1071                services:
1072                  web:
1073                    # Web service image
1074                    image: nginx:latest
1075                    ports:
1076                      - "80:80"
1077            """,
1078            "image: nginx:latest",
1079        ),
1080        # Comment associated with list item
1081        (
1082            b"""
1083                dependencies:
1084                  # First dependency
1085                  - name: redis
1086                    version: "6.0"
1087                  - name: postgres
1088                    version: "13"
1089            """,
1090            "- name: redis",
1091        ),
1092        # Comment inside parent structure
1093        (
1094            b"""
1095                app:
1096                  # Internal comment
1097                  name: myapp
1098                  version: "1.0"
1099            """,
1100            "name: myapp",
1101        ),
1102    ],
1103)
1104def test_find_yaml_associated_structure(
1105    code, expected_structure, init_yaml_tree_sitter
1106):
1107    """Test the find_yaml_associated_structure function."""
1108    parser, query = init_yaml_tree_sitter
1109    comments = utils.extract_comments(code, parser, query)
1110    assert comments, "No comments found in the code"
1111
1112    associated_structure = utils.find_yaml_associated_structure(comments[0])
1113    assert associated_structure, "No associated structure found"
1114    structure_text = associated_structure.text.decode("utf-8")
1115    assert expected_structure in structure_text
1116
1117
1118@pytest.mark.parametrize(
1119    ("code", "expected_results"),
1120    [
1121        # Multiple comments in sequence
1122        (
1123            b"""
1124                # First comment
1125                # Second comment
1126                database:
1127                  host: localhost
1128            """,
1129            ["database:", "database:"],  # Both comments should associate with database
1130        ),
1131        # Comments at different nesting levels
1132        (
1133            b"""
1134                # Top level comment
1135                services:
1136                  web:
1137                    # Nested comment
1138                    image: nginx:latest
1139            """,
1140            ["services:", "image: nginx:latest"],
1141        ),
1142    ],
1143)
1144def test_multiple_yaml_comments(code, expected_results, init_yaml_tree_sitter):
1145    """Test handling of multiple YAML comments in the same file."""
1146    parser, query = init_yaml_tree_sitter
1147    comments = utils.extract_comments(code, parser, query)
1148    comments.sort(key=lambda x: x.start_point.row)
1149
1150    assert len(comments) == len(expected_results), (
1151        f"Expected {len(expected_results)} comments, found {len(comments)}"
1152    )
1153
1154    for i, comment in enumerate(comments):
1155        associated_structure = utils.find_yaml_associated_structure(comment)
1156        assert associated_structure, f"No associated structure found for comment {i}"
1157        structure_text = associated_structure.text.decode("utf-8")
1158        assert expected_results[i] in structure_text
1159
1160
1161@pytest.mark.parametrize(
1162    ("code", "has_structure"),
1163    [
1164        # Comment at end of file with no following structure
1165        (
1166            b"""
1167database:
1168  host: localhost
1169# End of file comment
1170            """,
1171            True,  # This will actually find the parent database structure
1172        ),
1173        # Comment with only whitespace after
1174        (
1175            b"""
1176                # Lonely comment
1177
1178
1179            """,
1180            False,
1181        ),
1182        # Comment before valid structure
1183        (
1184            b"""
1185                # Valid comment
1186                key: value
1187            """,
1188            True,
1189        ),
1190    ],
1191)
1192def test_yaml_edge_cases(code, has_structure, init_yaml_tree_sitter):
1193    """Test edge cases in YAML comment processing."""
1194    parser, query = init_yaml_tree_sitter
1195    comments = utils.extract_comments(code, parser, query)
1196
1197    if comments:
1198        structure = utils.find_yaml_associated_structure(comments[0])
1199        if has_structure:
1200            assert structure, "Expected to find associated structure"
1201        else:
1202            assert structure is None, "Expected no associated structure"
1203    else:
1204        assert not has_structure, "No comments found but structure was expected"
1205
1206
1207@pytest.mark.parametrize(
1208    ("code", "expected_structures"),
1209    [
1210        # Simpler nested YAML structure
1211        (
1212            b"""# Global configuration
1213version: "3.8"
1214
1215# Services section
1216services:
1217  web:
1218    image: nginx:latest
1219    # Port configuration
1220    ports:
1221      - "80:80"
1222            """,
1223            [
1224                "version:",  # Global configuration
1225                "services:",  # Services section
1226                '- "80:80"',  # Port configuration
1227            ],
1228        ),
1229    ],
1230)
1231def test_complex_yaml_structure(code, expected_structures, init_yaml_tree_sitter):
1232    """Test complex nested YAML structures with multiple comments."""
1233    parser, query = init_yaml_tree_sitter
1234    comments = utils.extract_comments(code, parser, query)
1235    comments.sort(key=lambda x: x.start_point.row)
1236
1237    assert len(comments) == len(expected_structures), (
1238        f"Expected {len(expected_structures)} comments, found {len(comments)}"
1239    )
1240
1241    for i, comment in enumerate(comments):
1242        associated_structure = utils.find_yaml_associated_structure(comment)
1243        assert associated_structure, f"No associated structure found for comment {i}"
1244        structure_text = associated_structure.text.decode("utf-8")
1245        assert expected_structures[i] in structure_text, (
1246            f"Expected '{expected_structures[i]}' in structure text: '{structure_text}'"
1247        )
1248
1249
1250@pytest.mark.parametrize(
1251    ("code", "expected_type"),
1252    [
1253        # Block mapping pair
1254        (
1255            b"""
1256                # Comment
1257                key: value
1258            """,
1259            "block_mapping_pair",
1260        ),
1261        # Block sequence item
1262        (
1263            b"""
1264                items:
1265                  # Comment
1266                  - item1
1267            """,
1268            "block_sequence_item",
1269        ),
1270        # Nested block mapping
1271        (
1272            b"""
1273                services:
1274                  # Comment
1275                  web:
1276                    image: nginx
1277            """,
1278            "block_mapping_pair",
1279        ),
1280    ],
1281)
1282def test_yaml_structure_types(code, expected_type, init_yaml_tree_sitter):
1283    """Test that YAML structures return the correct node types."""
1284    parser, query = init_yaml_tree_sitter
1285    comments = utils.extract_comments(code, parser, query)
1286    assert comments, "No comments found"
1287
1288    structure = utils.find_yaml_associated_structure(comments[0])
1289    assert structure, "No associated structure found"
1290    assert structure.type == expected_type, (
1291        f"Expected type {expected_type}, got {structure.type}"
1292    )
1293
1294
1295def test_yaml_document_structure(init_yaml_tree_sitter):
1296    """Test YAML document structure handling."""
1297    code = b"""---
1298# Document comment
1299apiVersion: v1
1300kind: ConfigMap
1301metadata:
1302  name: my-config
1303data:
1304  # Data comment
1305  config.yml: |
1306    setting: value
1307    """
1308
1309    parser, query = init_yaml_tree_sitter
1310    comments = utils.extract_comments(code, parser, query)
1311    comments.sort(key=lambda x: x.start_point.row)
1312
1313    # Should find both comments
1314    assert len(comments) >= 2, f"Expected at least 2 comments, found {len(comments)}"
1315
1316    # First comment should associate with apiVersion
1317    first_structure = utils.find_yaml_associated_structure(comments[0])
1318    assert first_structure, "No structure found for first comment"
1319    first_text = first_structure.text.decode("utf-8")
1320    assert "apiVersion:" in first_text
1321
1322    # Second comment should associate with config.yml
1323    second_structure = utils.find_yaml_associated_structure(comments[1])
1324    assert second_structure, "No structure found for second comment"
1325    second_text = second_structure.text.decode("utf-8")
1326    assert "config.yml:" in second_text
1327
1328
1329def test_yaml_inline_comments_current_behavior(init_yaml_tree_sitter):
1330    """Test improved behavior of inline comments in YAML after the fix."""
1331    code = b"""key1: value1  # inline comment about key1
1332key2: value2
1333key3: value3  # inline comment about key3
1334"""
1335
1336    parser, query = init_yaml_tree_sitter
1337    comments = utils.extract_comments(code, parser, query)
1338    comments.sort(key=lambda x: x.start_point.row)
1339
1340    assert len(comments) == 2, f"Expected 2 comments, found {len(comments)}"
1341
1342    # Fixed behavior: inline comment about key1 now correctly associates with key1
1343    first_structure = utils.find_yaml_associated_structure(comments[0])
1344    assert first_structure, "No structure found for first comment"
1345    first_text = first_structure.text.decode("utf-8")
1346    assert "key1:" in first_text, f"Expected 'key1:' in '{first_text}'"
1347
1348    # Fixed behavior: inline comment about key3 now correctly associates with key3
1349    second_structure = utils.find_yaml_associated_structure(comments[1])
1350    assert second_structure, "No structure found for second comment"
1351    second_text = second_structure.text.decode("utf-8")
1352    assert "key3:" in second_text, f"Expected 'key3:' in '{second_text}'"
1353
1354
1355@pytest.mark.parametrize(
1356    ("code", "expected_associations"),
1357    [
1358        # Basic inline comment case
1359        (
1360            b"""key1: value1  # comment about key1
1361key2: value2
1362            """,
1363            ["key1:"],  # Now correctly associates with key1
1364        ),
1365        # Multiple inline comments
1366        (
1367            b"""database:
1368  host: localhost  # production server
1369  port: 5432       # default postgres port
1370  user: admin
1371            """,
1372            [
1373                "host: localhost",
1374                "port: 5432",
1375            ],  # Now correctly associates with the right structures
1376        ),
1377    ],
1378)
1379def test_yaml_inline_comments_fixed_behavior(
1380    code, expected_associations, init_yaml_tree_sitter
1381):
1382    """Test that inline comments now correctly associate with the structure they comment on."""
1383    parser, query = init_yaml_tree_sitter
1384    comments = utils.extract_comments(code, parser, query)
1385    comments.sort(key=lambda x: x.start_point.row)
1386
1387    assert len(comments) == len(expected_associations), (
1388        f"Expected {len(expected_associations)} comments, found {len(comments)}"
1389    )
1390
1391    for i, comment in enumerate(comments):
1392        structure = utils.find_yaml_associated_structure(comment)
1393        assert structure, f"No structure found for comment {i}"
1394        structure_text = structure.text.decode("utf-8")
1395        assert expected_associations[i] in structure_text, (
1396            f"Expected '{expected_associations[i]}' in structure text: '{structure_text}'"
1397        )
1398
1399
1400@pytest.mark.parametrize(
1401    ("code", "expected_associations"),
1402    [
1403        # Inline comments with list items
1404        (
1405            b"""items:
1406  - name: item1  # first item
1407  - name: item2  # second item
1408            """,
1409            [
1410                "name: item1",
1411                "name: item2",
1412            ],  # The inline comment finds the key-value pair within the list item
1413        ),
1414        # Mixed inline and block comments
1415        (
1416            b"""# Block comment for database
1417database:
1418  host: localhost  # inline comment for host
1419  port: 5432
1420  # Block comment for user
1421  user: admin
1422            """,
1423            ["database:", "host: localhost", "user: admin"],
1424        ),
1425        # Inline comments in nested structures
1426        (
1427            b"""services:
1428  web:
1429    image: nginx  # web server image
1430    ports:
1431      - "80:80"   # http port
1432            """,
1433            ["image: nginx", '- "80:80"'],
1434        ),
1435    ],
1436)
1437def test_yaml_inline_comments_comprehensive(
1438    code, expected_associations, init_yaml_tree_sitter
1439):
1440    """Comprehensive test for inline comment behavior in various YAML structures."""
1441    parser, query = init_yaml_tree_sitter
1442    comments = utils.extract_comments(code, parser, query)
1443    comments.sort(key=lambda x: x.start_point.row)
1444
1445    assert len(comments) == len(expected_associations), (
1446        f"Expected {len(expected_associations)} comments, found {len(comments)}"
1447    )
1448
1449    for i, comment in enumerate(comments):
1450        structure = utils.find_yaml_associated_structure(comment)
1451        assert structure, (
1452            f"No structure found for comment {i}: '{comment.text.decode('utf-8')}'"
1453        )
1454        structure_text = structure.text.decode("utf-8")
1455        assert expected_associations[i] in structure_text, (
1456            f"Comment {i} '{comment.text.decode('utf-8')}' -> Expected '{expected_associations[i]}' in '{structure_text}'"
1457        )