[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            Path("example") / "to" / "here",
1102            3,
1103            "https://github.com/useblocks/sphinx-codelinks/blob/beef1234/example/to/here#L3",
1104        )
1105    ],
1106)
1107def test_form_https_url(
1108    git_url, rev, project_path, filepath, lineno, result
1109):  # need to have these args
1110    url = utils.form_https_url(git_url, rev, project_path, filepath, lineno=lineno)
1111    assert url == result
1112
1113
1114def get_git_path() -> str:
1115    """Get the path to the git executable."""
1116    git_path = shutil.which("git")
1117    if not git_path:
1118        raise FileNotFoundError("Git executable not found")
1119    if not Path(git_path).is_file():
1120        raise FileNotFoundError("Git executable path is invalid")
1121    return git_path
1122
1123
1124def init_git_repo(repo_path: Path, remote_url: str) -> Path:
1125    """Initialize a git repository for testing."""
1126    git_dir = repo_path / "test_repo"
1127    src_dir = git_dir / "src"
1128    src_dir.mkdir(parents=True)
1129
1130    git_path = get_git_path()
1131    if not git_path:
1132        raise FileNotFoundError("Git executable not found")
1133    if not Path(git_path).is_file():
1134        raise FileNotFoundError("Git executable path is invalid")
1135
1136    # Initialize git repo
1137    subprocess.run([git_path, "init"], cwd=git_dir, check=True, capture_output=True)
1138    subprocess.run(
1139        [git_path, "config", "user.email", "test@example.com"], cwd=git_dir, check=True
1140    )
1141    subprocess.run(
1142        [git_path, "config", "user.name", "Test User"], cwd=git_dir, check=True
1143    )
1144
1145    # Create a test file and commit
1146    test_file = src_dir / "test_file.py"
1147    test_file.write_text("# Test file\nprint('hello')\n")
1148    subprocess.run([git_path, "add", "."], cwd=git_dir, check=True)
1149    subprocess.run(
1150        [git_path, "commit", "-m", "Initial commit"], cwd=git_dir, check=True
1151    )
1152
1153    # Add a remote
1154    subprocess.run(
1155        [git_path, "remote", "add", "origin", remote_url],
1156        cwd=git_dir,
1157        check=True,
1158    )
1159
1160    return git_dir
1161
1162
1163@pytest.fixture(
1164    params=[
1165        ("test_repo_git", "git@github.com:test-user/test-repo.git"),
1166        ("test_repo_https", "https://github.com/test-user/test-repo.git"),
1167    ]
1168)
1169def git_repo(tmp_path: str, request: pytest.FixtureRequest) -> tuple[Path, str]:
1170    """Create git repos for testing."""
1171    repo_name, remote_url = request.param
1172    repo_path = Path(tmp_path) / repo_name
1173    repo_path = init_git_repo(repo_path, remote_url)
1174    return repo_path, remote_url
1175
1176
1177def get_current_commit_hash(git_dir: Path) -> str:
1178    """Get the current commit hash of the git repository."""
1179    git_path = get_git_path()
1180    result = subprocess.run(
1181        [git_path, "rev-parse", "HEAD"],
1182        cwd=git_dir,
1183        check=True,
1184        capture_output=True,
1185        text=True,
1186    )
1187
1188    return str(result.stdout.strip())
1189
1190
1191def test_locate_git_root(git_repo: tuple[Path, str]) -> None:
1192    repo_path = git_repo[0]
1193    src_dir = repo_path / "src"
1194    git_root = utils.locate_git_root(src_dir)
1195    assert git_root == repo_path
1196
1197
1198def test_get_remote_url(git_repo: tuple[Path, str]) -> None:
1199    repo_path, expected_url = git_repo
1200    remote_url = utils.get_remote_url(repo_path)
1201    assert remote_url == expected_url
1202
1203
1204def test_get_current_rev(git_repo: tuple[Path, str]) -> None:
1205    repo_path, _ = git_repo
1206    current_rev = get_current_commit_hash(repo_path)
1207    assert current_rev == utils.get_current_rev(repo_path)
1208
1209
1210def test_get_current_rev_detached_head(tmp_path: Path) -> None:
1211    """In a detached HEAD (e.g. CI checkouts) .git/HEAD holds the commit SHA
1212    directly; get_current_rev returns it rather than warning and giving up."""
1213    git_root = tmp_path / "repo"
1214    (git_root / ".git").mkdir(parents=True)
1215    sha = "a1b2c3d4e5f60718293a4b5c6d7e8f9012345678"
1216    (git_root / ".git" / "HEAD").write_text(f"{sha}\n")
1217
1218    assert utils.get_current_rev(git_root) == sha
1219
1220
1221@pytest.mark.parametrize(
1222    ("text", "leading_sequences", "result"),
1223    [
1224        (
1225            """
1226*    some text in a comment
1227*    some text in a comment
1228*
1229""",
1230            ["*"],
1231            """
1232    some text in a comment
1233    some text in a comment
1234
1235""",
1236        ),
1237    ],
1238)
1239def test_remove_leading_sequences(text, leading_sequences, result):
1240    clean_text = utils.remove_leading_sequences(text, leading_sequences)
1241    assert clean_text == result
1242
1243
1244@pytest.mark.parametrize(
1245    ("text", "rst_markers", "rst_text", "positions"),
1246    [
1247        (
1248            """
1249@rst
1250.. impl:: multiline rst text
1251   :id: IMPL_71
1252@endrst
1253""",
1254            ["@rst", "@endrst"],
1255            f""".. impl:: multiline rst text{UNIX_NEWLINE}   :id: IMPL_71{UNIX_NEWLINE}""",
1256            {"row_offset": 1, "start_idx": 6, "end_idx": 51},
1257        ),
1258        (
1259            """
1260@rst.. impl:: oneline rst text@endrst
1261""",
1262            ["@rst", "@endrst"],
1263            """.. impl:: oneline rst text""",
1264            {"row_offset": 0, "start_idx": 5, "end_idx": 31},
1265        ),
1266    ],
1267)
1268def test_extract_rst(text, rst_markers, rst_text, positions):
1269    extracted_rst = utils.extract_rst(text, rst_markers[0], rst_markers[1])
1270    assert extracted_rst is not None
1271    assert extracted_rst["rst_text"] == rst_text
1272    assert extracted_rst["start_idx"] == positions["start_idx"]
1273    assert extracted_rst["end_idx"] == positions["end_idx"]
1274
1275
1276# ========== YAML-specific tests ==========
1277
1278
1279@pytest.mark.parametrize(
1280    ("code", "expected_structure"),
1281    [
1282        # Basic key-value pair
1283        (
1284            b"""
1285                # Comment before key
1286                database:
1287                  host: localhost
1288            """,
1289            "database:",
1290        ),
1291        # Comment in nested structure
1292        (
1293            b"""
1294                services:
1295                  web:
1296                    # Comment before image
1297                    image: nginx:latest
1298            """,
1299            "image: nginx:latest",
1300        ),
1301        # Comment before list item
1302        (
1303            b"""
1304                items:
1305                  # Comment before list item
1306                  - name: item1
1307                    value: test
1308            """,
1309            "- name: item1",
1310        ),
1311        # Comment in document structure
1312        (
1313            b"""---
1314# Comment in document
1315version: "3.8"
1316services:
1317  app:
1318    build: .
1319            """,
1320            "version:",
1321        ),
1322        # Flow mapping structure
1323        (
1324            b"""
1325                # Comment before flow mapping
1326                config: {debug: true, port: 8080}
1327            """,
1328            "config:",
1329        ),
1330    ],
1331)
1332def test_find_yaml_next_structure(code, expected_structure, init_yaml_tree_sitter):
1333    """Test the find_yaml_next_structure function."""
1334    parser, query = init_yaml_tree_sitter
1335    comments = utils.extract_comments(code, parser, query)
1336    assert comments, "No comments found in the code"
1337
1338    next_structure = utils.find_yaml_next_structure(comments[0])
1339    assert next_structure, "No next structure found"
1340    structure_text = next_structure.text.decode("utf-8")
1341    assert expected_structure in structure_text
1342
1343
1344@pytest.mark.parametrize(
1345    ("code", "expected_structure"),
1346    [
1347        # Comment associated with key-value pair
1348        (
1349            b"""
1350                # Database configuration
1351                database:
1352                  host: localhost
1353                  port: 5432
1354            """,
1355            "database:",
1356        ),
1357        # Comment associated with nested structure
1358        (
1359            b"""
1360                services:
1361                  web:
1362                    # Web service image
1363                    image: nginx:latest
1364                    ports:
1365                      - "80:80"
1366            """,
1367            "image: nginx:latest",
1368        ),
1369        # Comment associated with list item
1370        (
1371            b"""
1372                dependencies:
1373                  # First dependency
1374                  - name: redis
1375                    version: "6.0"
1376                  - name: postgres
1377                    version: "13"
1378            """,
1379            "- name: redis",
1380        ),
1381        # Comment inside parent structure
1382        (
1383            b"""
1384                app:
1385                  # Internal comment
1386                  name: myapp
1387                  version: "1.0"
1388            """,
1389            "name: myapp",
1390        ),
1391    ],
1392)
1393def test_find_yaml_associated_structure(
1394    code, expected_structure, init_yaml_tree_sitter
1395):
1396    """Test the find_yaml_associated_structure function."""
1397    parser, query = init_yaml_tree_sitter
1398    comments = utils.extract_comments(code, parser, query)
1399    assert comments, "No comments found in the code"
1400
1401    associated_structure = utils.find_yaml_associated_structure(comments[0])
1402    assert associated_structure, "No associated structure found"
1403    structure_text = associated_structure.text.decode("utf-8")
1404    assert expected_structure in structure_text
1405
1406
1407@pytest.mark.parametrize(
1408    ("code", "expected_results"),
1409    [
1410        # Multiple comments in sequence
1411        (
1412            b"""
1413                # First comment
1414                # Second comment
1415                database:
1416                  host: localhost
1417            """,
1418            ["database:", "database:"],  # Both comments should associate with database
1419        ),
1420        # Comments at different nesting levels
1421        (
1422            b"""
1423                # Top level comment
1424                services:
1425                  web:
1426                    # Nested comment
1427                    image: nginx:latest
1428            """,
1429            ["services:", "image: nginx:latest"],
1430        ),
1431    ],
1432)
1433def test_multiple_yaml_comments(code, expected_results, init_yaml_tree_sitter):
1434    """Test handling of multiple YAML comments in the same file."""
1435    parser, query = init_yaml_tree_sitter
1436    comments = utils.extract_comments(code, parser, query)
1437    comments.sort(key=lambda x: x.start_point.row)
1438
1439    assert len(comments) == len(expected_results), (
1440        f"Expected {len(expected_results)} comments, found {len(comments)}"
1441    )
1442
1443    for i, comment in enumerate(comments):
1444        associated_structure = utils.find_yaml_associated_structure(comment)
1445        assert associated_structure, f"No associated structure found for comment {i}"
1446        structure_text = associated_structure.text.decode("utf-8")
1447        assert expected_results[i] in structure_text
1448
1449
1450@pytest.mark.parametrize(
1451    ("code", "has_structure"),
1452    [
1453        # Comment at end of file with no following structure
1454        (
1455            b"""
1456database:
1457  host: localhost
1458# End of file comment
1459            """,
1460            True,  # This will actually find the parent database structure
1461        ),
1462        # Comment with only whitespace after
1463        (
1464            b"""
1465                # Lonely comment
1466
1467
1468            """,
1469            False,
1470        ),
1471        # Comment before valid structure
1472        (
1473            b"""
1474                # Valid comment
1475                key: value
1476            """,
1477            True,
1478        ),
1479    ],
1480)
1481def test_yaml_edge_cases(code, has_structure, init_yaml_tree_sitter):
1482    """Test edge cases in YAML comment processing."""
1483    parser, query = init_yaml_tree_sitter
1484    comments = utils.extract_comments(code, parser, query)
1485
1486    if comments:
1487        structure = utils.find_yaml_associated_structure(comments[0])
1488        if has_structure:
1489            assert structure, "Expected to find associated structure"
1490        else:
1491            assert structure is None, "Expected no associated structure"
1492    else:
1493        assert not has_structure, "No comments found but structure was expected"
1494
1495
1496@pytest.mark.parametrize(
1497    ("code", "expected_structures"),
1498    [
1499        # Simpler nested YAML structure
1500        (
1501            b"""# Global configuration
1502version: "3.8"
1503
1504# Services section
1505services:
1506  web:
1507    image: nginx:latest
1508    # Port configuration
1509    ports:
1510      - "80:80"
1511            """,
1512            [
1513                "version:",  # Global configuration
1514                "services:",  # Services section
1515                '- "80:80"',  # Port configuration
1516            ],
1517        ),
1518    ],
1519)
1520def test_complex_yaml_structure(code, expected_structures, init_yaml_tree_sitter):
1521    """Test complex nested YAML structures with multiple comments."""
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_structures), (
1527        f"Expected {len(expected_structures)} 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_structures[i] in structure_text, (
1535            f"Expected '{expected_structures[i]}' in structure text: '{structure_text}'"
1536        )
1537
1538
1539@pytest.mark.parametrize(
1540    ("code", "expected_type"),
1541    [
1542        # Block mapping pair
1543        (
1544            b"""
1545                # Comment
1546                key: value
1547            """,
1548            "block_mapping_pair",
1549        ),
1550        # Block sequence item
1551        (
1552            b"""
1553                items:
1554                  # Comment
1555                  - item1
1556            """,
1557            "block_sequence_item",
1558        ),
1559        # Nested block mapping
1560        (
1561            b"""
1562                services:
1563                  # Comment
1564                  web:
1565                    image: nginx
1566            """,
1567            "block_mapping_pair",
1568        ),
1569    ],
1570)
1571def test_yaml_structure_types(code, expected_type, init_yaml_tree_sitter):
1572    """Test that YAML structures return the correct node types."""
1573    parser, query = init_yaml_tree_sitter
1574    comments = utils.extract_comments(code, parser, query)
1575    assert comments, "No comments found"
1576
1577    structure = utils.find_yaml_associated_structure(comments[0])
1578    assert structure, "No associated structure found"
1579    assert structure.type == expected_type, (
1580        f"Expected type {expected_type}, got {structure.type}"
1581    )
1582
1583
1584def test_yaml_document_structure(init_yaml_tree_sitter):
1585    """Test YAML document structure handling."""
1586    code = b"""---
1587# Document comment
1588apiVersion: v1
1589kind: ConfigMap
1590metadata:
1591  name: my-config
1592data:
1593  # Data comment
1594  config.yml: |
1595    setting: value
1596    """
1597
1598    parser, query = init_yaml_tree_sitter
1599    comments = utils.extract_comments(code, parser, query)
1600    comments.sort(key=lambda x: x.start_point.row)
1601
1602    # Should find both comments
1603    assert len(comments) >= 2, f"Expected at least 2 comments, found {len(comments)}"
1604
1605    # First comment should associate with apiVersion
1606    first_structure = utils.find_yaml_associated_structure(comments[0])
1607    assert first_structure, "No structure found for first comment"
1608    first_text = first_structure.text.decode("utf-8")
1609    assert "apiVersion:" in first_text
1610
1611    # Second comment should associate with config.yml
1612    second_structure = utils.find_yaml_associated_structure(comments[1])
1613    assert second_structure, "No structure found for second comment"
1614    second_text = second_structure.text.decode("utf-8")
1615    assert "config.yml:" in second_text
1616
1617
1618def test_yaml_inline_comments_current_behavior(init_yaml_tree_sitter):
1619    """Test improved behavior of inline comments in YAML after the fix."""
1620    code = b"""key1: value1  # inline comment about key1
1621key2: value2
1622key3: value3  # inline comment about key3
1623"""
1624
1625    parser, query = init_yaml_tree_sitter
1626    comments = utils.extract_comments(code, parser, query)
1627    comments.sort(key=lambda x: x.start_point.row)
1628
1629    assert len(comments) == 2, f"Expected 2 comments, found {len(comments)}"
1630
1631    # Fixed behavior: inline comment about key1 now correctly associates with key1
1632    first_structure = utils.find_yaml_associated_structure(comments[0])
1633    assert first_structure, "No structure found for first comment"
1634    first_text = first_structure.text.decode("utf-8")
1635    assert "key1:" in first_text, f"Expected 'key1:' in '{first_text}'"
1636
1637    # Fixed behavior: inline comment about key3 now correctly associates with key3
1638    second_structure = utils.find_yaml_associated_structure(comments[1])
1639    assert second_structure, "No structure found for second comment"
1640    second_text = second_structure.text.decode("utf-8")
1641    assert "key3:" in second_text, f"Expected 'key3:' in '{second_text}'"
1642
1643
1644@pytest.mark.parametrize(
1645    ("code", "expected_associations"),
1646    [
1647        # Basic inline comment case
1648        (
1649            b"""key1: value1  # comment about key1
1650key2: value2
1651            """,
1652            ["key1:"],  # Now correctly associates with key1
1653        ),
1654        # Multiple inline comments
1655        (
1656            b"""database:
1657  host: localhost  # production server
1658  port: 5432       # default postgres port
1659  user: admin
1660            """,
1661            [
1662                "host: localhost",
1663                "port: 5432",
1664            ],  # Now correctly associates with the right structures
1665        ),
1666    ],
1667)
1668def test_yaml_inline_comments_fixed_behavior(
1669    code, expected_associations, init_yaml_tree_sitter
1670):
1671    """Test that inline comments now correctly associate with the structure they comment on."""
1672    parser, query = init_yaml_tree_sitter
1673    comments = utils.extract_comments(code, parser, query)
1674    comments.sort(key=lambda x: x.start_point.row)
1675
1676    assert len(comments) == len(expected_associations), (
1677        f"Expected {len(expected_associations)} comments, found {len(comments)}"
1678    )
1679
1680    for i, comment in enumerate(comments):
1681        structure = utils.find_yaml_associated_structure(comment)
1682        assert structure, f"No structure found for comment {i}"
1683        structure_text = structure.text.decode("utf-8")
1684        assert expected_associations[i] in structure_text, (
1685            f"Expected '{expected_associations[i]}' in structure text: '{structure_text}'"
1686        )
1687
1688
1689@pytest.mark.parametrize(
1690    ("code", "expected_associations"),
1691    [
1692        # Inline comments with list items
1693        (
1694            b"""items:
1695  - name: item1  # first item
1696  - name: item2  # second item
1697            """,
1698            [
1699                "name: item1",
1700                "name: item2",
1701            ],  # The inline comment finds the key-value pair within the list item
1702        ),
1703        # Mixed inline and block comments
1704        (
1705            b"""# Block comment for database
1706database:
1707  host: localhost  # inline comment for host
1708  port: 5432
1709  # Block comment for user
1710  user: admin
1711            """,
1712            ["database:", "host: localhost", "user: admin"],
1713        ),
1714        # Inline comments in nested structures
1715        (
1716            b"""services:
1717  web:
1718    image: nginx  # web server image
1719    ports:
1720      - "80:80"   # http port
1721            """,
1722            ["image: nginx", '- "80:80"'],
1723        ),
1724    ],
1725)
1726def test_yaml_inline_comments_comprehensive(
1727    code, expected_associations, init_yaml_tree_sitter
1728):
1729    """Comprehensive test for inline comment behavior in various YAML structures."""
1730    parser, query = init_yaml_tree_sitter
1731    comments = utils.extract_comments(code, parser, query)
1732    comments.sort(key=lambda x: x.start_point.row)
1733
1734    assert len(comments) == len(expected_associations), (
1735        f"Expected {len(expected_associations)} comments, found {len(comments)}"
1736    )
1737
1738    for i, comment in enumerate(comments):
1739        structure = utils.find_yaml_associated_structure(comment)
1740        assert structure, (
1741            f"No structure found for comment {i}: '{comment.text.decode('utf-8')}'"
1742        )
1743        structure_text = structure.text.decode("utf-8")
1744        assert expected_associations[i] in structure_text, (
1745            f"Comment {i} '{comment.text.decode('utf-8')}' -> Expected '{expected_associations[i]}' in '{structure_text}'"
1746        )