summaryrefslogtreecommitdiffstats
path: root/tests/lint_tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests/lint_tests')
-rw-r--r--tests/lint_tests/__init__.py0
-rw-r--r--tests/lint_tests/conftest.py30
-rw-r--r--tests/lint_tests/lint_tutil.py71
-rw-r--r--tests/lint_tests/test_lint_dctrl.py117
-rw-r--r--tests/lint_tests/test_lint_debputy.py181
5 files changed, 399 insertions, 0 deletions
diff --git a/tests/lint_tests/__init__.py b/tests/lint_tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/lint_tests/__init__.py
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 <jane@example.com>
+ # 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"