import textwrap from typing import Optional import pytest from debputy.lsp.debputy_ls import DebputyLanguageServer try: from lsprotocol.types import ( CompletionParams, TextDocumentIdentifier, HoverParams, MarkupContent, SemanticTokensParams, ) from debputy.lsp.lsp_debian_control import ( _debian_control_completions, _debian_control_hover, _debian_control_semantic_tokens_full, ) from pygls.server import LanguageServer except ImportError: pass from lsp_tests.lsp_tutil import ( put_doc_with_cursor, put_doc_no_cursor, resolve_semantic_tokens, resolved_semantic_token, ) def test_dctrl_complete_field(ls: "DebputyLanguageServer") -> None: dctrl_uri = "file:///nowhere/debian/control" cursor_pos = put_doc_with_cursor( ls, dctrl_uri, "debian/control", textwrap.dedent( """\ Source: foo Package: foo """ ), ) matches = _debian_control_completions( ls, CompletionParams(TextDocumentIdentifier(dctrl_uri), cursor_pos), ) assert isinstance(matches, list) keywords = {m.label for m in matches} assert "Multi-Arch" in keywords assert "Architecture" in keywords # Already present or wrong section assert "Package" not in keywords assert "Source" not in keywords cursor_pos = put_doc_with_cursor( ls, dctrl_uri, "debian/control", textwrap.dedent( """\ Source: foo Package: foo Architecture: any """ ), ) matches = _debian_control_completions( ls, CompletionParams(TextDocumentIdentifier(dctrl_uri), cursor_pos), ) assert isinstance(matches, list) keywords = {m.label for m in matches} assert "Multi-Arch" in keywords # Should be considered present even though it is parsed as two stanzas with a space assert "Architecture" not in keywords # Already present or wrong section assert "Package" not in keywords assert "Source" not in keywords cursor_pos = put_doc_with_cursor( ls, dctrl_uri, "debian/control", textwrap.dedent( """\ Source: foo Package: foo Sec Architecture: any """ ), ) matches = _debian_control_completions( ls, CompletionParams(TextDocumentIdentifier(dctrl_uri), cursor_pos), ) assert isinstance(matches, list) keywords = {m.label for m in matches} # Included since we rely on client filtering (some clients let "RRR" match "R(ules-)R(equires-)R(oot), etc). assert "Multi-Arch" in keywords # Should be considered present even though it is parsed as two stanzas with an error assert "Architecture" not in keywords # Already present or wrong section assert "Package" not in keywords assert "Source" not in keywords @pytest.mark.parametrize( "case,is_arch_all", [ ("Architecture: any\n", False), ("Architecture: any\nM-A", False), ("\nArchitecture: any", False), ("M-A\nArchitecture: any", False), ("Architecture: all\n", True), ("Architecture: all\nM-A", True), ("\nArchitecture: all", True), ("M-A\nArchitecture: all", True), # Does not have architecture ("M-A", None), ], ) def test_dctrl_complete_field_context( ls: "DebputyLanguageServer", case: str, is_arch_all: Optional[bool], ) -> None: dctrl_uri = "file:///nowhere/debian/control" content = textwrap.dedent( """\ Source: foo Package: foo {CASE} """ ).format(CASE=case) cursor_pos = put_doc_with_cursor( ls, dctrl_uri, "debian/control", content, ) matches = _debian_control_completions( ls, CompletionParams(TextDocumentIdentifier(dctrl_uri), cursor_pos), ) assert isinstance(matches, list) keywords = {m.label for m in matches} # Missing Architecture counts as "arch:all" by the completion logic if is_arch_all is False: assert "X-DH-Build-For-Type" in keywords else: assert "X-DH-Build-For-Type" not in keywords def test_dctrl_complete_field_value_context(ls: "DebputyLanguageServer") -> None: dctrl_uri = "file:///nowhere/debian/control" content = textwrap.dedent( """\ Source: foo Package: foo Architecture: any Multi-Arch: """ ) cursor_pos = put_doc_with_cursor( ls, dctrl_uri, "debian/control", content, ) matches = _debian_control_completions( ls, CompletionParams(TextDocumentIdentifier(dctrl_uri), cursor_pos), ) assert isinstance(matches, list) keywords = {m.label for m in matches} assert keywords == {"no", "same", "foreign", "allowed"} content = textwrap.dedent( """\ Source: foo Package: foo Architecture: all Multi-Arch: """ ) cursor_pos = put_doc_with_cursor( ls, dctrl_uri, "debian/control", content, ) matches = _debian_control_completions( ls, CompletionParams(TextDocumentIdentifier(dctrl_uri), cursor_pos), ) assert isinstance(matches, list) keywords = {m.label for m in matches} assert keywords == {"no", "foreign", "allowed"} def test_dctrl_hover_doc_field(ls: "DebputyLanguageServer") -> None: dctrl_uri = "file:///nowhere/debian/control" cursor_pos = put_doc_with_cursor( ls, dctrl_uri, "debian/control", textwrap.dedent( """\ Source: foo Package: foo Architecture: any """ ), ) hover_doc = _debian_control_hover( ls, HoverParams(TextDocumentIdentifier(dctrl_uri), cursor_pos), ) assert hover_doc is not None and isinstance(hover_doc.contents, MarkupContent) assert "Determines which architecture" in hover_doc.contents.value def test_dctrl_hover_doc_synopsis(ls: "DebputyLanguageServer") -> None: dctrl_uri = "file:///nowhere/debian/control" cursor_pos = put_doc_with_cursor( ls, dctrl_uri, "debian/control", textwrap.dedent( """\ Source: foo Package: foo Architecture: any Description: super charged tool with batteries included """ ), ) hover_doc = _debian_control_hover( ls, HoverParams(TextDocumentIdentifier(dctrl_uri), cursor_pos), ) assert hover_doc is not None and isinstance(hover_doc.contents, MarkupContent) assert hover_doc.contents.value.startswith("# Package synopsis") assert "super charged tool with batteries included" in hover_doc.contents.value def test_dctrl_hover_doc_substvars(ls: "DebputyLanguageServer") -> None: dctrl_uri = "file:///nowhere/debian/control" matching_cases = [ "bar (= ${binary:Version})", "bar (= ${binary:Version})", "bar (= ${binary:Version})", ] for variant in matching_cases: cursor_pos = put_doc_with_cursor( ls, dctrl_uri, "debian/control", textwrap.dedent( f"""\ Source: foo Package: foo Architecture: any Depends: bar (= {variant}) Description: super charged tool with batteries included """ ), ) hover_doc = _debian_control_hover( ls, HoverParams(TextDocumentIdentifier(dctrl_uri), cursor_pos), ) assert hover_doc is not None and isinstance(hover_doc.contents, MarkupContent) assert hover_doc.contents.value.startswith("# Substvar `${binary:Version}`") non_matching_cases = [ "bar (= ${binary:Version})", "bar (= ${binary:Version})", ] for variant in non_matching_cases: cursor_pos = put_doc_with_cursor( ls, dctrl_uri, "debian/control", textwrap.dedent( f"""\ Source: foo Package: foo Architecture: any Depends: bar (= {variant}) Description: super charged tool with batteries included """ ), ) hover_doc = _debian_control_hover( ls, HoverParams(TextDocumentIdentifier(dctrl_uri), cursor_pos), ) provided_doc = "" if hover_doc is not None and isinstance(hover_doc.contents, MarkupContent): provided_doc = hover_doc.contents.value assert not provided_doc.startswith("# Substvar `${binary:Version}`") def test_dctrl_semantic_tokens(ls: "DebputyLanguageServer") -> None: dctrl_uri = "file:///nowhere/debian/control" put_doc_no_cursor( ls, dctrl_uri, "debian/control", textwrap.dedent( """\ # Some leading comment Source: foo # Comment between stanzas Package: foo # Comment before Architecture Architecture: any Depends: # Comment about bar bar (>= 1.0), baz [linux-any] Description: super charged tool with batteries included Unknown-Field: Some value # Comment in that field that we do not know about. """ ), ) semantic_tokens = _debian_control_semantic_tokens_full( ls, SemanticTokensParams(TextDocumentIdentifier(dctrl_uri)), ) resolved_semantic_tokens = resolve_semantic_tokens(semantic_tokens) assert resolved_semantic_tokens is not None assert resolved_semantic_tokens == [ resolved_semantic_token(0, 0, len("# Some leading comment"), "comment"), resolved_semantic_token(2, 0, len("Source"), "keyword"), resolved_semantic_token(4, 0, len("# Comment between stanzas"), "comment"), resolved_semantic_token(6, 0, len("Package"), "keyword"), resolved_semantic_token(7, 0, len("# Comment before Architecture"), "comment"), resolved_semantic_token(8, 0, len("Architecture"), "keyword"), resolved_semantic_token(8, len("Architecture: "), len("any"), "enumMember"), resolved_semantic_token(9, 0, len("Depends"), "keyword"), resolved_semantic_token(10, 0, len("# Comment about bar"), "comment"), resolved_semantic_token(13, 0, len("Description"), "keyword"), resolved_semantic_token(14, 0, len("Unknown-Field"), "keyword"), # TODO: resolved_semantic_token(15, 0, len("# Comment in that field"), "comment"), ]