From 11180d7be49c1912b7bdcba23d932747041e5b5d Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 14 Apr 2024 22:18:23 +0200 Subject: Merging upstream version 0.1.28. Signed-off-by: Daniel Baumann --- tests/lint_tests/conftest.py | 41 +++++-- tests/lint_tests/lint_tutil.py | 60 ++++++--- tests/lint_tests/test_lint_changelog.py | 90 ++++++++++++++ tests/lint_tests/test_lint_dctrl.py | 143 +++++++++++++++++++--- tests/lint_tests/test_lint_debputy.py | 207 +++++++++++++++++++++++++++++--- 5 files changed, 480 insertions(+), 61 deletions(-) create mode 100644 tests/lint_tests/test_lint_changelog.py (limited to 'tests/lint_tests') diff --git a/tests/lint_tests/conftest.py b/tests/lint_tests/conftest.py index d08f5ca..2c54eb7 100644 --- a/tests/lint_tests/conftest.py +++ b/tests/lint_tests/conftest.py @@ -1,16 +1,22 @@ import pytest +from debian.debian_support import DpkgArchTable -from debputy.lsp.lsp_features import lsp_set_plugin_features -from debputy.plugin.api.feature_set import PluginProvidedFeatureSet +from debputy._deb_options_profiles import DebBuildOptionsAndProfiles +from debputy.architecture_support import DpkgArchitectureBuildProcessValuesTable +from debputy.packages import DctrlParser from debputy.util import setup_logging try: from lsprotocol.types import Diagnostic + from debputy.lsp.spellchecking import disable_spellchecking HAS_LSPROTOCOL = True except ImportError: HAS_LSPROTOCOL = False + def disable_spellchecking() -> None: + pass + @pytest.fixture(scope="session", autouse=True) def enable_logging() -> None: @@ -19,12 +25,25 @@ def enable_logging() -> None: setup_logging(reconfigure_logging=True) -@pytest.fixture(autouse=True) -def setup_feature_set( - debputy_plugin_feature_set: PluginProvidedFeatureSet, -) -> None: - lsp_set_plugin_features(debputy_plugin_feature_set) - try: - yield - finally: - lsp_set_plugin_features(None) +@pytest.fixture(scope="session", autouse=True) +def disable_spellchecking_fixture() -> None: + # CI/The buildd does not install relevant, so this is mostly about ensuring + # consistent behavior between clean and "unclean" build/test environments + disable_spellchecking() + + +@pytest.fixture +def lint_dctrl_parser( + dpkg_arch_query: DpkgArchTable, + amd64_dpkg_architecture_variables: DpkgArchitectureBuildProcessValuesTable, + no_profiles_or_build_options: DebBuildOptionsAndProfiles, +) -> DctrlParser: + return DctrlParser( + frozenset(), + frozenset(), + True, + True, + amd64_dpkg_architecture_variables, + dpkg_arch_query, + no_profiles_or_build_options, + ) diff --git a/tests/lint_tests/lint_tutil.py b/tests/lint_tests/lint_tutil.py index d4f654c..83b69fd 100644 --- a/tests/lint_tests/lint_tutil.py +++ b/tests/lint_tests/lint_tutil.py @@ -1,9 +1,16 @@ import collections -from typing import List, Optional, Mapping, Any +from typing import List, Optional, Mapping, Any, Callable import pytest -from debputy.linting.lint_util import LinterImpl, LinterPositionCodec +from debputy.linting.lint_util import ( + LinterImpl, + LinterPositionCodec, + LintStateImpl, + LintState, +) +from debputy.packages import DctrlParser +from debputy.plugin.api.feature_set import PluginProvidedFeatureSet try: from lsprotocol.types import Diagnostic, DiagnosticSeverity @@ -22,13 +29,48 @@ except ImportError: LINTER_POSITION_CODEC = LinterPositionCodec() +class LintWrapper: + + def __init__( + self, + path: str, + handler: Callable[[LintState], Optional[List[Diagnostic]]], + debputy_plugin_feature_set: PluginProvidedFeatureSet, + dctrl_parser: DctrlParser, + ) -> None: + self._debputy_plugin_feature_set = debputy_plugin_feature_set + self._handler = handler + self.dctrl_lines: Optional[List[str]] = None + self.path = path + self._dctrl_parser = dctrl_parser + + def __call__(self, lines: List[str]) -> Optional[List["Diagnostic"]]: + source_package = None + binary_packages = None + dctrl_lines = self.dctrl_lines + if dctrl_lines is not None: + source_package, binary_packages = ( + self._dctrl_parser.parse_source_debian_control( + dctrl_lines, ignore_errors=True + ) + ) + state = LintStateImpl( + self._debputy_plugin_feature_set, + self.path, + lines, + source_package, + binary_packages, + ) + return check_diagnostics(self._handler(state)) + + def requires_levenshtein(func: Any) -> Any: return pytest.mark.skipif( not HAS_LEVENSHTEIN, reason="Missing python3-levenshtein" )(func) -def _check_diagnostics( +def check_diagnostics( diagnostics: Optional[List["Diagnostic"]], ) -> Optional[List["Diagnostic"]]: if diagnostics: @@ -37,18 +79,6 @@ def _check_diagnostics( return diagnostics -def run_linter( - path: str, lines: List[str], linter: LinterImpl -) -> Optional[List["Diagnostic"]]: - uri = f"file://{path}" - return _check_diagnostics(linter(uri, path, lines, LINTER_POSITION_CODEC)) - - -def exactly_one_diagnostic(diagnostics: Optional[List["Diagnostic"]]) -> "Diagnostic": - assert diagnostics and len(diagnostics) == 1 - return diagnostics[0] - - def by_range_sort_key(diagnostic: Diagnostic) -> Any: start_pos = diagnostic.range.start end_pos = diagnostic.range.end diff --git a/tests/lint_tests/test_lint_changelog.py b/tests/lint_tests/test_lint_changelog.py new file mode 100644 index 0000000..25dac0e --- /dev/null +++ b/tests/lint_tests/test_lint_changelog.py @@ -0,0 +1,90 @@ +import textwrap +from typing import List, Optional + +import pytest + +from debputy.lsp.lsp_debian_changelog import _lint_debian_changelog +from debputy.lsp.lsp_debian_control import _lint_debian_control +from debputy.packages import DctrlParser +from debputy.plugin.api.feature_set import PluginProvidedFeatureSet +from lint_tests.lint_tutil import ( + group_diagnostics_by_severity, + requires_levenshtein, + LintWrapper, +) + +try: + from lsprotocol.types import Diagnostic, DiagnosticSeverity +except ImportError: + pass + + +@pytest.fixture +def line_linter( + debputy_plugin_feature_set: PluginProvidedFeatureSet, + lint_dctrl_parser: DctrlParser, +) -> LintWrapper: + return LintWrapper( + "/nowhere/debian/changelog", + _lint_debian_changelog, + debputy_plugin_feature_set, + lint_dctrl_parser, + ) + + +def test_dctrl_lint(line_linter: LintWrapper) -> None: + lines = textwrap.dedent( + """\ + foo (0.2) unstable; urgency=medium + + * Renamed to foo + + -- Niels Thykier Mon, 08 Apr 2024 16:00:00 +0000 + + bar (0.2) unstable; urgency=medium + + * Initial release + + -- Niels Thykier Mon, 01 Apr 2024 00:00:00 +0000 + """ + ).splitlines(keepends=True) + + diagnostics = line_linter(lines) + print(diagnostics) + # Without a control file, this is fine + assert not diagnostics + + line_linter.dctrl_lines = textwrap.dedent( + """\ + Source: foo + + Package: something-else + """ + ) + + diagnostics = line_linter(lines) + print(diagnostics) + # Also fine, because d/control and d/changelog agrees + assert not diagnostics + + line_linter.dctrl_lines = textwrap.dedent( + """\ + Source: bar + + Package: something-else + """ + ) + + diagnostics = line_linter(lines) + print(diagnostics) + # This should be problematic though + assert diagnostics and len(diagnostics) == 1 + diag = diagnostics[0] + + msg = ( + "The first entry must use the same source name as debian/control." + ' Changelog uses: "foo" while d/control uses: "bar"' + ) + assert diag.severity == DiagnosticSeverity.Error + assert diag.message == msg + assert f"{diag.range}" == "0:0-0:3" diff --git a/tests/lint_tests/test_lint_dctrl.py b/tests/lint_tests/test_lint_dctrl.py index cc2758e..e9a5756 100644 --- a/tests/lint_tests/test_lint_dctrl.py +++ b/tests/lint_tests/test_lint_dctrl.py @@ -1,14 +1,15 @@ import textwrap -from typing import List, Optional, Callable +from typing import List, Optional import pytest from debputy.lsp.lsp_debian_control import _lint_debian_control +from debputy.packages import DctrlParser +from debputy.plugin.api.feature_set import PluginProvidedFeatureSet from lint_tests.lint_tutil import ( - run_linter, group_diagnostics_by_severity, requires_levenshtein, - exactly_one_diagnostic, + LintWrapper, ) try: @@ -17,20 +18,30 @@ except ImportError: pass -TestLinter = Callable[[List[str]], Optional[List["Diagnostic"]]] +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() -> TestLinter: - path = "/nowhere/debian/control" - - def _linter(lines: List[str]) -> Optional[List["Diagnostic"]]: - return run_linter(path, lines, _lint_debian_control) - - return _linter - -def test_dctrl_lint(line_linter: TestLinter) -> 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 @@ -86,7 +97,7 @@ def test_dctrl_lint(line_linter: TestLinter) -> None: @requires_levenshtein -def test_dctrl_lint_typos(line_linter: TestLinter) -> None: +def test_dctrl_lint_typos(line_linter: LintWrapper) -> None: lines = textwrap.dedent( """\ Source: foo @@ -109,9 +120,109 @@ def test_dctrl_lint_typos(line_linter: TestLinter) -> None: diagnostics = line_linter(lines) print(diagnostics) - diag = exactly_one_diagnostic(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( + """\ + Source: foo + Standards-Version: 4.5.2 + 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 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}" == "10:24-10:28" + assert f"{typo_diag.range}" == "10:24-10:28" + + +def test_dctrl_lint_mx_value(line_linter: LintWrapper) -> None: + lines = textwrap.dedent( + """\ + Source: foo + Standards-Version: 4.5.2 + 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( + """\ + Source: foo + Standards-Version: 4.5.2 + 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" diff --git a/tests/lint_tests/test_lint_debputy.py b/tests/lint_tests/test_lint_debputy.py index 74977d0..9c30392 100644 --- a/tests/lint_tests/test_lint_debputy.py +++ b/tests/lint_tests/test_lint_debputy.py @@ -1,13 +1,12 @@ -import textwrap from typing import List, Optional, Callable import pytest -from debputy.lsp.lsp_debian_debputy_manifest import _lint_debian_debputy_manifest +from debputy.packages import DctrlParser +from debputy.plugin.api.feature_set import PluginProvidedFeatureSet from lint_tests.lint_tutil import ( - run_linter, - group_diagnostics_by_severity, requires_levenshtein, + LintWrapper, ) try: @@ -16,20 +15,20 @@ except ImportError: pass -TestLinter = Callable[[List[str]], Optional[List["Diagnostic"]]] - - @pytest.fixture -def line_linter() -> TestLinter: - path = "/nowhere/debian/debputy.manifest" - - def _linter(lines: List[str]) -> Optional[List["Diagnostic"]]: - return run_linter(path, lines, _lint_debian_debputy_manifest) - - return _linter - - -def test_debputy_lint_unknown_keys(line_linter: TestLinter) -> None: +def line_linter( + debputy_plugin_feature_set: PluginProvidedFeatureSet, + lint_dctrl_parser: DctrlParser, +) -> LintWrapper: + return LintWrapper( + "/nowhere/debian/debputy.manifest", + _lint_debian_debputy_manifest, + debputy_plugin_feature_set, + lint_dctrl_parser, + ) + + +def test_debputy_lint_unknown_keys(line_linter: LintWrapper) -> None: lines = textwrap.dedent( """\ manifest-version: 0.1 @@ -85,7 +84,7 @@ def test_debputy_lint_unknown_keys(line_linter: TestLinter) -> None: @requires_levenshtein -def test_debputy_lint_unknown_keys_spelling(line_linter: TestLinter) -> None: +def test_debputy_lint_unknown_keys_spelling(line_linter: LintWrapper) -> None: lines = textwrap.dedent( """\ manifest-version: 0.1 @@ -131,7 +130,7 @@ def test_debputy_lint_unknown_keys_spelling(line_linter: TestLinter) -> None: assert f"{third_error.range}" == "8:6-8:9" -def test_debputy_lint_conflicting_keys(line_linter: TestLinter) -> None: +def test_debputy_lint_conflicting_keys(line_linter: LintWrapper) -> None: lines = textwrap.dedent( """\ manifest-version: 0.1 @@ -179,3 +178,173 @@ def test_debputy_lint_conflicting_keys(line_linter: TestLinter) -> None: msg = 'The "sources" cannot be used with "source".' assert fourth_error.message == msg assert f"{fourth_error.range}" == "9:4-9:11" + + +import textwrap +from typing import List, Optional, Callable + +import pytest + +from debputy.lsp.lsp_debian_debputy_manifest import _lint_debian_debputy_manifest +from debputy.packages import DctrlParser +from debputy.plugin.api.feature_set import PluginProvidedFeatureSet +from lint_tests.lint_tutil import ( + group_diagnostics_by_severity, + requires_levenshtein, + LintWrapper, +) + +try: + from lsprotocol.types import Diagnostic, DiagnosticSeverity +except ImportError: + pass + +TestLintWrapper = Callable[[List[str]], Optional[List["Diagnostic"]]] + + +@pytest.fixture +def line_linter( + debputy_plugin_feature_set: PluginProvidedFeatureSet, + lint_dctrl_parser: DctrlParser, +) -> LintWrapper: + return LintWrapper( + "/nowhere/debian/debputy.manifest", + _lint_debian_debputy_manifest, + debputy_plugin_feature_set, + lint_dctrl_parser, + ) + + +def test_debputy_lint_unknown_keys(line_linter: LintWrapper) -> None: + lines = textwrap.dedent( + """\ + manifest-version: 0.1 + installations: + - install-something: + sources: + - abc + - def + - install-docs: + source: foo + puff: true # Unknown keyword (assuming install-docs) + when: + negated: cross-compiling + - install-docs: + source: bar + when: ross-compiling # Typo of "cross-compiling"; FIXME not caught + packages: + foo: + blah: qwe # Unknown keyword + """ + ).splitlines(keepends=True) + + diagnostics = line_linter(lines) + by_severity = group_diagnostics_by_severity(diagnostics) + # This example triggers errors only + assert DiagnosticSeverity.Error in by_severity + + assert DiagnosticSeverity.Warning not 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) == 4 + + first_error, second_error, third_error, fourth_error = errors + + msg = 'Unknown or unsupported key "install-something".' + assert first_error.message == msg + assert f"{first_error.range}" == "2:2-2:19" + + msg = 'Unknown or unsupported key "puff".' + assert second_error.message == msg + assert f"{second_error.range}" == "8:4-8:8" + + msg = 'Unknown or unsupported key "negated".' + assert third_error.message == msg + assert f"{third_error.range}" == "10:6-10:13" + + msg = 'Unknown or unsupported key "blah".' + assert fourth_error.message == msg + assert f"{fourth_error.range}" == "16:4-16:8" + + +@requires_levenshtein +def test_debputy_lint_unknown_keys_spelling(line_linter: LintWrapper) -> None: + lines = textwrap.dedent( + """\ + manifest-version: 0.1 + installations: + - install-dcoss: # typo + sources: + - abc + - def + puff: true # Unknown keyword (assuming install-docs) + when: + nut: cross-compiling # Typo of "not" + - install-docs: + source: bar + when: ross-compiling # Typo of "cross-compiling"; FIXME not caught + """ + ).splitlines(keepends=True) + + diagnostics = line_linter(lines) + by_severity = group_diagnostics_by_severity(diagnostics) + # This example triggers errors only + assert DiagnosticSeverity.Error in by_severity + + assert DiagnosticSeverity.Warning not 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 = 'Unknown or unsupported key "install-dcoss". It looks like a typo of "install-docs".' + assert first_error.message == msg + assert f"{first_error.range}" == "2:2-2:15" + + msg = 'Unknown or unsupported key "puff".' + assert second_error.message == msg + assert f"{second_error.range}" == "6:4-6:8" + + msg = 'Unknown or unsupported key "nut". It looks like a typo of "not".' + assert third_error.message == msg + assert f"{third_error.range}" == "8:6-8:9" + + +def test_debputy_lint_check_package_names(line_linter: LintWrapper) -> None: + lines = textwrap.dedent( + """\ + manifest-version: 0.1 + packages: + unknown-package: + binary-version: '1:{{DEB_VERSION_UPSTREAM_REVISION}}' + """ + ).splitlines(keepends=True) + + line_linter.dctrl_lines = None + diagnostics = line_linter(lines) + print(diagnostics) + # Does nothing without a control file + assert not diagnostics + + line_linter.dctrl_lines = textwrap.dedent( + """\ + Source: foo + + Package: foo + """ + ).splitlines(keepends=True) + + diagnostics = line_linter(lines) + assert diagnostics and len(diagnostics) == 1 + diag = diagnostics[0] + + msg = 'Unknown package "unknown-package".' + assert diag.message == msg + assert f"{diag.range}" == "2:4-2:19" -- cgit v1.2.3