From c2da8244dfd429c1737c029ed3993c15b4cd8c4f Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 14 Apr 2024 22:17:41 +0200 Subject: Merging upstream version 0.1.27. Signed-off-by: Daniel Baumann --- tests/lint_tests/__init__.py | 0 tests/lint_tests/conftest.py | 30 ++++++ tests/lint_tests/lint_tutil.py | 71 +++++++++++++ tests/lint_tests/test_lint_dctrl.py | 117 ++++++++++++++++++++++ tests/lint_tests/test_lint_debputy.py | 181 ++++++++++++++++++++++++++++++++++ 5 files changed, 399 insertions(+) create mode 100644 tests/lint_tests/__init__.py create mode 100644 tests/lint_tests/conftest.py create mode 100644 tests/lint_tests/lint_tutil.py create mode 100644 tests/lint_tests/test_lint_dctrl.py create mode 100644 tests/lint_tests/test_lint_debputy.py (limited to 'tests/lint_tests') diff --git a/tests/lint_tests/__init__.py b/tests/lint_tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/lint_tests/conftest.py b/tests/lint_tests/conftest.py new file mode 100644 index 0000000..d08f5ca --- /dev/null +++ b/tests/lint_tests/conftest.py @@ -0,0 +1,30 @@ +import pytest + +from debputy.lsp.lsp_features import lsp_set_plugin_features +from debputy.plugin.api.feature_set import PluginProvidedFeatureSet +from debputy.util import setup_logging + +try: + from lsprotocol.types import Diagnostic + + HAS_LSPROTOCOL = True +except ImportError: + HAS_LSPROTOCOL = False + + +@pytest.fixture(scope="session", autouse=True) +def enable_logging() -> None: + if not HAS_LSPROTOCOL: + pytest.skip("Missing python3-lsprotocol") + 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) diff --git a/tests/lint_tests/lint_tutil.py b/tests/lint_tests/lint_tutil.py new file mode 100644 index 0000000..d4f654c --- /dev/null +++ b/tests/lint_tests/lint_tutil.py @@ -0,0 +1,71 @@ +import collections +from typing import List, Optional, Mapping, Any + +import pytest + +from debputy.linting.lint_util import LinterImpl, LinterPositionCodec + +try: + from lsprotocol.types import Diagnostic, DiagnosticSeverity +except ImportError: + pass + + +try: + from Levenshtein import distance + + HAS_LEVENSHTEIN = True +except ImportError: + HAS_LEVENSHTEIN = False + + +LINTER_POSITION_CODEC = LinterPositionCodec() + + +def requires_levenshtein(func: Any) -> Any: + return pytest.mark.skipif( + not HAS_LEVENSHTEIN, reason="Missing python3-levenshtein" + )(func) + + +def _check_diagnostics( + diagnostics: Optional[List["Diagnostic"]], +) -> Optional[List["Diagnostic"]]: + if diagnostics: + for diagnostic in diagnostics: + assert diagnostic.severity is not None + 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 + return start_pos.line, start_pos.character, end_pos.line, end_pos.character + + +def group_diagnostics_by_severity( + diagnostics: Optional[List["Diagnostic"]], +) -> Mapping["DiagnosticSeverity", List["Diagnostic"]]: + if not diagnostics: + return {} + + by_severity = collections.defaultdict(list) + + for diagnostic in sorted(diagnostics, key=by_range_sort_key): + severity = diagnostic.severity + assert severity is not None + by_severity[severity].append(diagnostic) + + return by_severity diff --git a/tests/lint_tests/test_lint_dctrl.py b/tests/lint_tests/test_lint_dctrl.py new file mode 100644 index 0000000..cc2758e --- /dev/null +++ b/tests/lint_tests/test_lint_dctrl.py @@ -0,0 +1,117 @@ +import textwrap +from typing import List, Optional, Callable + +import pytest + +from debputy.lsp.lsp_debian_control import _lint_debian_control +from lint_tests.lint_tutil import ( + run_linter, + group_diagnostics_by_severity, + requires_levenshtein, + exactly_one_diagnostic, +) + +try: + from lsprotocol.types import Diagnostic, DiagnosticSeverity +except ImportError: + pass + + +TestLinter = Callable[[List[str]], Optional[List["Diagnostic"]]] + + +@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: + 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}" == "8:9-8:13" + + +@requires_levenshtein +def test_dctrl_lint_typos(line_linter: TestLinter) -> None: + lines = textwrap.dedent( + """\ + Source: foo + Standards-Version: 4.5.2 + 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) + diag = exactly_one_diagnostic(diagnostics) + + 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" diff --git a/tests/lint_tests/test_lint_debputy.py b/tests/lint_tests/test_lint_debputy.py new file mode 100644 index 0000000..74977d0 --- /dev/null +++ b/tests/lint_tests/test_lint_debputy.py @@ -0,0 +1,181 @@ +import textwrap +from typing import List, Optional, Callable + +import pytest + +from debputy.lsp.lsp_debian_debputy_manifest import _lint_debian_debputy_manifest +from lint_tests.lint_tutil import ( + run_linter, + group_diagnostics_by_severity, + requires_levenshtein, +) + +try: + from lsprotocol.types import Diagnostic, DiagnosticSeverity +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: + 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: TestLinter) -> 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_conflicting_keys(line_linter: TestLinter) -> None: + lines = textwrap.dedent( + """\ + manifest-version: 0.1 + installations: + - install-docs: + sources: + - foo + - bar + as: baz # Conflicts with "sources" (#85) + - install: + source: foo + sources: # Conflicts with "source" (#85) + - bar + - baz + """ + ).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 = 'The "sources" cannot be used with "as".' + assert first_error.message == msg + assert f"{first_error.range}" == "3:4-3:11" + + msg = 'The "as" cannot be used with "sources".' + assert second_error.message == msg + assert f"{second_error.range}" == "6:4-6:6" + + msg = 'The "source" cannot be used with "sources".' + assert third_error.message == msg + assert f"{third_error.range}" == "8:4-8:10" + + msg = 'The "sources" cannot be used with "source".' + assert fourth_error.message == msg + assert f"{fourth_error.range}" == "9:4-9:11" -- cgit v1.2.3