import textwrap from typing import List, Optional import pytest from debputy.lsp.lsp_debian_control import _lint_debian_control from debputy.lsp.lsp_debian_control_reference_data import CURRENT_STANDARDS_VERSION from debputy.packages import DctrlParser from debputy.plugin.api import virtual_path_def from debputy.plugin.api.feature_set import PluginProvidedFeatureSet from debputy.plugin.api.test_api import build_virtual_file_system from lint_tests.lint_tutil import ( group_diagnostics_by_severity, requires_levenshtein, LintWrapper, ) from debputy.lsprotocol.types import Diagnostic, DiagnosticSeverity from tutil import build_time_only class DctrlLintWrapper(LintWrapper): def __call__(self, lines: List[str]) -> Optional[List["Diagnostic"]]: try: self.dctrl_lines = lines return super().__call__(lines) finally: self.dctrl_lines = None @pytest.fixture def line_linter( debputy_plugin_feature_set: PluginProvidedFeatureSet, lint_dctrl_parser: DctrlParser, ) -> LintWrapper: return DctrlLintWrapper( "/nowhere/debian/control", _lint_debian_control, debputy_plugin_feature_set, lint_dctrl_parser, ) def test_dctrl_lint(line_linter: LintWrapper) -> None: lines = textwrap.dedent( """\ Source: foo Some-Other-Field: bar Build-Depends: debhelper-compat (= 13) Package: foo Architecture: all # Unknown section Section: base """ ).splitlines(keepends=True) diagnostics = line_linter(lines) by_severity = group_diagnostics_by_severity(diagnostics) # This example triggers errors and warnings, but no hint of info assert DiagnosticSeverity.Error in by_severity assert DiagnosticSeverity.Warning in by_severity assert DiagnosticSeverity.Hint not in by_severity assert DiagnosticSeverity.Information not in by_severity errors = by_severity[DiagnosticSeverity.Error] print(errors) assert len(errors) == 3 first_error, second_error, third_error = errors msg = "Stanza is missing field Standards-Version" assert first_error.message == msg assert f"{first_error.range}" == "0:0-1:0" msg = "Stanza is missing field Maintainer" assert second_error.message == msg assert f"{second_error.range}" == "0:0-1:0" msg = "Stanza is missing field Priority" assert third_error.message == msg assert f"{third_error.range}" == "4:0-5:0" warnings = by_severity[DiagnosticSeverity.Warning] assert len(warnings) == 2 first_warn, second_warn = warnings msg = "Stanza is missing field Description" assert first_warn.message == msg assert f"{first_warn.range}" == "4:0-5:0" msg = 'The value "base" is not supported in Section.' assert second_warn.message == msg assert f"{second_warn.range}" == "7:9-7:13" @requires_levenshtein def test_dctrl_lint_typos(line_linter: LintWrapper) -> None: lines = textwrap.dedent( f"""\ Source: foo Standards-Version: {CURRENT_STANDARDS_VERSION} Priority: optional Section: devel Maintainer: Jane Developer # Typo Build-Dpends: debhelper-compat (= 13) Package: foo Architecture: all Description: Some very interesting synopsis A very interesting description that spans multiple lines . Just so be clear, this is for a test. """ ).splitlines(keepends=True) diagnostics = line_linter(lines) print(diagnostics) assert diagnostics and len(diagnostics) == 1 diag = diagnostics[0] msg = 'The "Build-Dpends" looks like a typo of "Build-Depends".' assert diag.message == msg assert diag.severity == DiagnosticSeverity.Warning assert f"{diag.range}" == "6:0-6:12" @requires_levenshtein def test_dctrl_lint_mx_value_with_typo(line_linter: LintWrapper) -> None: lines = textwrap.dedent( f"""\ Source: foo Standards-Version: {CURRENT_STANDARDS_VERSION} Priority: optional Section: devel Maintainer: Jane Developer Build-Depends: debhelper-compat (= 13) Package: foo # Typo of `all` Architecture: linux-any alle Description: Some very interesting synopsis A very interesting description that spans multiple lines . Just so be clear, this is for a test. """ ).splitlines(keepends=True) diagnostics = line_linter(lines) print(diagnostics) assert diagnostics is not None assert len(diagnostics) == 2 by_severity = group_diagnostics_by_severity(diagnostics) assert DiagnosticSeverity.Error in by_severity assert DiagnosticSeverity.Warning in by_severity typo_diag = by_severity[DiagnosticSeverity.Warning][0] mx_diag = by_severity[DiagnosticSeverity.Error][0] mx_msg = 'The value "all" cannot be used with other values.' typo_msg = 'It is possible that the value is a typo of "all".' assert mx_diag.message == mx_msg assert typo_diag.message == typo_msg assert f"{mx_diag.range}" == "9:24-9:28" assert f"{typo_diag.range}" == "9:24-9:28" def test_dctrl_lint_mx_value(line_linter: LintWrapper) -> None: lines = textwrap.dedent( f"""\ Source: foo Standards-Version: {CURRENT_STANDARDS_VERSION} Priority: optional Section: devel Maintainer: Jane Developer Build-Depends: debhelper-compat (= 13) Package: foo Architecture: all linux-any Description: Some very interesting synopsis A very interesting description that spans multiple lines . Just so be clear, this is for a test. """ ).splitlines(keepends=True) diagnostics = line_linter(lines) print(diagnostics) assert diagnostics and len(diagnostics) == 1 diag = diagnostics[0] msg = 'The value "all" cannot be used with other values.' assert diag.message == msg assert diag.severity == DiagnosticSeverity.Error assert f"{diag.range}" == "8:14-8:17" lines = textwrap.dedent( f"""\ Source: foo Standards-Version: {CURRENT_STANDARDS_VERSION} Priority: optional Section: devel Maintainer: Jane Developer Build-Depends: debhelper-compat (= 13) Package: foo Architecture: linux-any any Description: Some very interesting synopsis A very interesting description that spans multiple lines . Just so be clear, this is for a test. """ ).splitlines(keepends=True) diagnostics = line_linter(lines) print(diagnostics) assert diagnostics and len(diagnostics) == 1 diag = diagnostics[0] msg = 'The value "any" cannot be used with other values.' assert diag.message == msg assert diag.severity == DiagnosticSeverity.Error assert f"{diag.range}" == "8:24-8:27" def test_dctrl_lint_dup_sep(line_linter: LintWrapper) -> None: lines = textwrap.dedent( f"""\ Source: foo Section: devel Priority: optional Standards-Version: {CURRENT_STANDARDS_VERSION} Maintainer: Jane Developer Build-Depends: debhelper-compat (= 13) Package: foo Architecture: all Depends: bar, , baz Description: Some very interesting synopsis A very interesting description that spans multiple lines . Just so be clear, this is for a test. """ ).splitlines(keepends=True) diagnostics = line_linter(lines) print(diagnostics) assert diagnostics and len(diagnostics) == 1 error = diagnostics[0] msg = "Duplicate separator" assert error.message == msg assert f"{error.range}" == "10:1-10:2" assert error.severity == DiagnosticSeverity.Error def test_dctrl_lint_ma(line_linter: LintWrapper) -> None: lines = textwrap.dedent( f"""\ Source: foo Section: devel Priority: optional Standards-Version: {CURRENT_STANDARDS_VERSION} Maintainer: Jane Developer Build-Depends: debhelper-compat (= 13) Package: foo Architecture: all Multi-Arch: same Depends: bar, baz Description: Some very interesting synopsis A very interesting description that spans multiple lines . Just so be clear, this is for a test. """ ).splitlines(keepends=True) diagnostics = line_linter(lines) print(diagnostics) assert diagnostics and len(diagnostics) == 1 error = diagnostics[0] msg = "Multi-Arch: same is not valid for Architecture: all packages. Maybe you want foreign?" assert error.message == msg assert f"{error.range}" == "9:12-9:16" assert error.severity == DiagnosticSeverity.Error def test_dctrl_lint_udeb(line_linter: LintWrapper) -> None: lines = textwrap.dedent( f"""\ Source: foo Section: devel Priority: optional Standards-Version: {CURRENT_STANDARDS_VERSION} Maintainer: Jane Developer Build-Depends: debhelper-compat (= 13) Package: foo Architecture: all XB-Installer-Menu-Item: 1234 Depends: bar, baz Description: Some very interesting synopsis A very interesting description that spans multiple lines . Just so be clear, this is for a test. Package: bar-udeb Architecture: all Section: debian-installer Package-Type: udeb XB-Installer-Menu-Item: golf Description: Some very interesting synopsis A very interesting description that spans multiple lines . Just so be clear, this is for a test. """ ).splitlines(keepends=True) diagnostics = line_linter(lines) print(diagnostics) assert diagnostics and len(diagnostics) == 2 first, second = diagnostics msg = "The XB-Installer-Menu-Item field is only applicable to udeb packages (`Package-Type: udeb`)" assert first.message == msg assert f"{first.range}" == "9:0-9:22" assert first.severity == DiagnosticSeverity.Warning msg = r'The value "golf" does not match the regex ^[1-9]\d{3,4}$.' assert second.message == msg assert f"{second.range}" == "21:24-21:28" assert second.severity == DiagnosticSeverity.Error def test_dctrl_lint_arch_only_fields(line_linter: LintWrapper) -> None: lines = textwrap.dedent( f"""\ Source: foo Section: devel Priority: optional Standards-Version: {CURRENT_STANDARDS_VERSION} Maintainer: Jane Developer Build-Depends: debhelper-compat (= 13) Package: foo Architecture: all X-DH-Build-For-Type: target Depends: bar, baz Description: Some very interesting synopsis A very interesting description that spans multiple lines . Just so be clear, this is for a test. """ ).splitlines(keepends=True) diagnostics = line_linter(lines) print(diagnostics) assert diagnostics and len(diagnostics) == 1 issue = diagnostics[0] msg = "The X-DH-Build-For-Type field is not applicable to arch:all packages (`Architecture: all`)" assert issue.message == msg assert f"{issue.range}" == "9:0-9:19" assert issue.severity == DiagnosticSeverity.Warning def test_dctrl_lint_sv(line_linter: LintWrapper) -> None: lines = textwrap.dedent( f"""\ Source: foo Section: devel Priority: optional Standards-Version: 4.6.2 Maintainer: Jane Developer Build-Depends: debhelper-compat (= 13) Package: foo Architecture: all Depends: bar, baz Description: Some very interesting synopsis A very interesting description that spans multiple lines . Just so be clear, this is for a test. """ ).splitlines(keepends=True) diagnostics = line_linter(lines) print(diagnostics) assert diagnostics and len(diagnostics) == 1 issue = diagnostics[0] msg = f"Latest Standards-Version is {CURRENT_STANDARDS_VERSION}" assert issue.message == msg assert f"{issue.range}" == "3:19-3:24" assert issue.severity == DiagnosticSeverity.Information lines = textwrap.dedent( f"""\ Source: foo Section: devel Priority: optional Standards-Version: Golf Maintainer: Jane Developer Build-Depends: debhelper-compat (= 13) Package: foo Architecture: all Depends: bar, baz Description: Some very interesting synopsis A very interesting description that spans multiple lines . Just so be clear, this is for a test. """ ).splitlines(keepends=True) diagnostics = line_linter(lines) print(diagnostics) assert diagnostics and len(diagnostics) == 1 issue = diagnostics[0] msg = f'Not a valid version. Current version is "{CURRENT_STANDARDS_VERSION}"' assert issue.message == msg assert f"{issue.range}" == "3:19-3:23" assert issue.severity == DiagnosticSeverity.Warning lines = textwrap.dedent( f"""\ Source: foo Section: devel Priority: optional Standards-Version: {CURRENT_STANDARDS_VERSION}.0 Maintainer: Jane Developer Build-Depends: debhelper-compat (= 13) Package: foo Architecture: all Depends: bar, baz Description: Some very interesting synopsis A very interesting description that spans multiple lines . Just so be clear, this is for a test. """ ).splitlines(keepends=True) diagnostics = line_linter(lines) print(diagnostics) assert diagnostics and len(diagnostics) == 1 issue = diagnostics[0] msg = "Unnecessary version segment. This part of the version is only used for editorial changes" assert issue.message == msg assert f"{issue.range}" == "3:24-3:26" assert issue.severity == DiagnosticSeverity.Information def test_dctrl_lint_sv_udeb_only(line_linter: LintWrapper) -> None: lines = textwrap.dedent( f"""\ Source: foo Section: devel Priority: optional Maintainer: Jane Developer Build-Depends: debhelper-compat (= 13) Package: foo-udeb Architecture: all Package-Type: udeb Section: debian-installer Depends: bar, baz Description: Some very interesting synopsis A very interesting description that spans multiple lines . Just so be clear, this is for a test. """ ).splitlines(keepends=True) diagnostics = line_linter(lines) print(diagnostics) assert not diagnostics def test_dctrl_lint_udeb_menu_iten(line_linter: LintWrapper) -> None: lines = textwrap.dedent( """\ Source: foo Section: devel Priority: optional Maintainer: Jane Developer Build-Depends: debhelper-compat (= 13) Package: foo-udeb Architecture: all Package-Type: udeb Section: debian-installer XB-Installer-Menu-Item: 12345 Description: Some very interesting synopsis A very interesting description that spans multiple lines . Just so be clear, this is for a test. Package: bar-udeb Architecture: all Package-Type: udeb Section: debian-installer XB-Installer-Menu-Item: ${foo} Description: Some very interesting synopsis A very interesting description that spans multiple lines . Just so be clear, this is for a test. """ ).splitlines(keepends=True) diagnostics = line_linter(lines) print(diagnostics) assert not diagnostics def test_dctrl_lint_multiple_vcs(line_linter: LintWrapper) -> None: lines = textwrap.dedent( f"""\ Source: foo Section: devel Priority: optional Standards-Version: {CURRENT_STANDARDS_VERSION} Maintainer: Jane Developer Build-Depends: debhelper-compat (= 13) Vcs-Git: https://salsa.debian.org/debian/foo Vcs-Svn: https://svn.debian.org/debian/foo Vcs-Browser: https://salsa.debian.org/debian/foo Package: foo Architecture: all Depends: bar, baz Description: Some very interesting synopsis A very interesting description that spans multiple lines . Just so be clear, this is for a test. """ ).splitlines(keepends=True) diagnostics = line_linter(lines) print(diagnostics) assert diagnostics and len(diagnostics) == 2 first_issue, second_issue = diagnostics msg = f'Multiple Version Control fields defined ("Vcs-Git")' assert first_issue.message == msg assert f"{first_issue.range}" == "6:0-7:0" assert first_issue.severity == DiagnosticSeverity.Warning msg = f'Multiple Version Control fields defined ("Vcs-Svn")' assert second_issue.message == msg assert f"{second_issue.range}" == "7:0-8:0" assert second_issue.severity == DiagnosticSeverity.Warning def test_dctrl_lint_synopsis_empty(line_linter: LintWrapper) -> None: lines = textwrap.dedent( f"""\ Source: foo Section: devel Priority: optional Standards-Version: {CURRENT_STANDARDS_VERSION} Maintainer: Jane Developer Build-Depends: debhelper-compat (= 13) Package: foo Architecture: all Depends: bar, baz Description: A very interesting description without a synopsis . Just so be clear, this is for a test. """ ).splitlines(keepends=True) diagnostics = line_linter(lines) print(diagnostics) assert diagnostics and len(diagnostics) == 1 issue = diagnostics[0] msg = "Package synopsis is missing" assert issue.message == msg assert f"{issue.range}" == "10:0-10:11" assert issue.severity == DiagnosticSeverity.Warning def test_dctrl_lint_synopsis_basis(line_linter: LintWrapper) -> None: lines = textwrap.dedent( f"""\ Source: foo Section: devel Priority: optional Standards-Version: {CURRENT_STANDARDS_VERSION} Maintainer: Jane Developer Build-Depends: debhelper-compat (= 13) Package: foo Architecture: all Depends: bar, baz Description: The synopsis is not the best because it starts with an article and also the synopsis goes on and on A very interesting description with a poor synopsis . Just so be clear, this is for a test. """ ).splitlines(keepends=True) diagnostics = line_linter(lines) print(diagnostics) assert diagnostics and len(diagnostics) == 2 first_issue, second_issue = diagnostics msg = "Package synopsis starts with an article (a/an/the)." assert first_issue.message == msg assert f"{first_issue.range}" == "10:13-10:16" assert first_issue.severity == DiagnosticSeverity.Warning msg = "Package synopsis is too long." assert second_issue.message == msg assert f"{second_issue.range}" == "10:92-10:112" assert second_issue.severity == DiagnosticSeverity.Warning def test_dctrl_lint_synopsis_template(line_linter: LintWrapper) -> None: lines = textwrap.dedent( f"""\ Source: foo Section: devel Priority: optional Standards-Version: {CURRENT_STANDARDS_VERSION} Maintainer: Jane Developer Build-Depends: debhelper-compat (= 13) Package: foo Architecture: all Depends: bar, baz Description: A very interesting description with a poor synopsis . Just so be clear, this is for a test. """ ).splitlines(keepends=True) diagnostics = line_linter(lines) print(diagnostics) assert diagnostics and len(diagnostics) == 1 issue = diagnostics[0] msg = "Package synopsis is a placeholder" assert issue.message == msg assert f"{issue.range}" == "10:13-10:48" assert issue.severity == DiagnosticSeverity.Warning def test_dctrl_lint_synopsis_too_short(line_linter: LintWrapper) -> None: lines = textwrap.dedent( f"""\ Source: foo Section: devel Priority: optional Standards-Version: {CURRENT_STANDARDS_VERSION} Maintainer: Jane Developer Build-Depends: debhelper-compat (= 13) Package: foo Architecture: all Depends: bar, baz Description: short A very interesting description with a poor synopsis . Just so be clear, this is for a test. """ ).splitlines(keepends=True) diagnostics = line_linter(lines) print(diagnostics) assert diagnostics and len(diagnostics) == 1 issue = diagnostics[0] msg = "Package synopsis is too short" assert issue.message == msg assert f"{issue.range}" == "10:13-10:18" assert issue.severity == DiagnosticSeverity.Warning @build_time_only def test_dctrl_lint_ambiguous_pkgfile(line_linter: LintWrapper) -> None: lines = textwrap.dedent( f"""\ Source: foo Section: devel Priority: optional Standards-Version: {CURRENT_STANDARDS_VERSION} Maintainer: Jane Developer Build-Depends: debhelper-compat (= 13) Package: foo Architecture: all Depends: bar, baz Description: some short synopsis A very interesting description with a valid synopsis . Just so be clear, this is for a test. """ ).splitlines(keepends=True) # FIXME: This relies on "cwd" being a valid debian directory using debhelper. Fix and # remove the `build_time_only` restriction line_linter.source_root = build_virtual_file_system(["./debian/bar.install"]) diagnostics = line_linter(lines) print(diagnostics) assert diagnostics and len(diagnostics) == 1 issue = diagnostics[0] msg = ( 'Possible typo in "./debian/bar.install". Consider renaming the file to "debian/foo.bar.install"' ' or "debian/foo.install if it is intended for foo' ) assert issue.message == msg assert f"{issue.range}" == "7:0-8:0" assert issue.severity == DiagnosticSeverity.Warning diag_data = issue.data assert isinstance(diag_data, dict) assert diag_data.get("report_for_related_file") in ( "./debian/bar.install", "debian/bar.install", ) @requires_levenshtein @build_time_only def test_dctrl_lint_stem_typo_pkgfile(line_linter: LintWrapper) -> None: lines = textwrap.dedent( f"""\ Source: foo Section: devel Priority: optional Standards-Version: {CURRENT_STANDARDS_VERSION} Maintainer: Jane Developer Build-Depends: debhelper-compat (= 13) Package: foo Architecture: all Depends: bar, baz Description: some short synopsis A very interesting description with a valid synopsis . Just so be clear, this is for a test. """ ).splitlines(keepends=True) # FIXME: This relies on "cwd" being a valid debian directory using debhelper. Fix and # remove the `build_time_only` restriction line_linter.source_root = build_virtual_file_system(["./debian/foo.intsall"]) diagnostics = line_linter(lines) print(diagnostics) assert diagnostics and len(diagnostics) == 1 issue = diagnostics[0] msg = 'The file "./debian/foo.intsall" is likely a typo of "./debian/foo.install"' assert issue.message == msg assert f"{issue.range}" == "7:0-8:0" assert issue.severity == DiagnosticSeverity.Warning diag_data = issue.data assert isinstance(diag_data, dict) assert diag_data.get("report_for_related_file") in ( "./debian/foo.intsall", "debian/foo.intsall", ) @build_time_only def test_dctrl_lint_stem_inactive_pkgfile_fp(line_linter: LintWrapper) -> None: lines = textwrap.dedent( f"""\ Source: foo Section: devel Priority: optional Standards-Version: {CURRENT_STANDARDS_VERSION} Maintainer: Jane Developer Build-Depends: debhelper-compat (= 13), dh-sequence-zz-debputy, Package: foo Architecture: all Depends: bar, baz Description: some short synopsis A very interesting description with a valid synopsis . Just so be clear, this is for a test. """ ).splitlines(keepends=True) # FIXME: This relies on "cwd" being a valid debian directory using debhelper. Fix and # remove the `build_time_only` restriction # # Note: The "positive" test of this one is missing; suspect because it cannot (reliably) # load the `zz-debputy` sequence. line_linter.source_root = build_virtual_file_system( [ "./debian/foo.install", virtual_path_def( "./debian/rules", content=textwrap.dedent( """\ #! /usr/bin/make -f binary binary-arch binary-indep build build-arch build-indep clean: foo $@ """ ), ), ] ) diagnostics = line_linter(lines) print(diagnostics) # We should not emit diagnostics when the package is not using dh! assert not diagnostics