diff options
Diffstat (limited to 'tests')
-rw-r--r-- | tests/lint_tests/lint_tutil.py | 30 | ||||
-rw-r--r-- | tests/lint_tests/test_lint_dctrl.py | 444 | ||||
-rw-r--r-- | tests/lsp_tests/test_lsp_debputy_manifest_completer.py | 4 | ||||
-rw-r--r-- | tests/test_debputy_plugin.py | 136 | ||||
-rw-r--r-- | tests/test_declarative_parser.py | 9 | ||||
-rw-r--r-- | tests/test_fs_metadata.py | 115 | ||||
-rw-r--r-- | tests/test_install_rules.py | 39 | ||||
-rw-r--r-- | tests/test_migrations.py | 13 | ||||
-rw-r--r-- | tests/test_parser.py | 108 | ||||
-rw-r--r-- | tests/test_style.py | 135 | ||||
-rw-r--r-- | tests/test_substitute.py | 3 | ||||
-rw-r--r-- | tests/test_utils.py | 20 |
12 files changed, 928 insertions, 128 deletions
diff --git a/tests/lint_tests/lint_tutil.py b/tests/lint_tests/lint_tutil.py index 267f669..c16fde3 100644 --- a/tests/lint_tests/lint_tutil.py +++ b/tests/lint_tests/lint_tutil.py @@ -1,5 +1,5 @@ import collections -from typing import List, Optional, Mapping, Any, Callable +from typing import List, Optional, Mapping, Any, Callable, Sequence import pytest @@ -9,11 +9,14 @@ from debputy.linting.lint_util import ( LintStateImpl, LintState, ) -from debputy.lsp.style_prefs import StylePreferenceTable, EffectivePreference +from debputy.lsp.maint_prefs import ( + MaintainerPreferenceTable, + EffectiveFormattingPreference, +) from debputy.packages import DctrlParser from debputy.plugin.api.feature_set import PluginProvidedFeatureSet -from debputy.lsprotocol.types import Diagnostic, DiagnosticSeverity +from debputy.lsprotocol.types import Diagnostic, DiagnosticSeverity, Range try: @@ -42,8 +45,8 @@ class LintWrapper: self.path = path self._dctrl_parser = dctrl_parser self.source_root: Optional[VirtualPathBase] = None - self.lint_style_preference_table = StylePreferenceTable({}, {}) - self.effective_preference: Optional[EffectivePreference] = None + self.lint_maint_preference_table = MaintainerPreferenceTable({}, {}) + self.effective_preference: Optional[EffectiveFormattingPreference] = None def __call__(self, lines: List[str]) -> Optional[List["Diagnostic"]]: source_package = None @@ -59,7 +62,7 @@ class LintWrapper: debian_dir = source_root.get("debian") if source_root is not None else None state = LintStateImpl( self._debputy_plugin_feature_set, - self.lint_style_preference_table, + self.lint_maint_preference_table, source_root, debian_dir, self.path, @@ -108,3 +111,18 @@ def group_diagnostics_by_severity( by_severity[severity].append(diagnostic) return by_severity + + +def diag_range_to_text(lines: Sequence[str], range_: "Range") -> str: + parts = [] + for line_no in range(range_.start.line, range_.end.line + 1): + line = lines[line_no] + chunk = line + if line_no == range_.start.line and line_no == range_.end.line: + chunk = line[range_.start.character : range_.end.character] + elif line_no == range_.start.line: + chunk = line[range_.start.character :] + elif line_no == range_.end.line: + chunk = line[: range_.end.character] + parts.append(chunk) + return "".join(parts) diff --git a/tests/lint_tests/test_lint_dctrl.py b/tests/lint_tests/test_lint_dctrl.py index 745c323..229acc1 100644 --- a/tests/lint_tests/test_lint_dctrl.py +++ b/tests/lint_tests/test_lint_dctrl.py @@ -13,6 +13,7 @@ from lint_tests.lint_tutil import ( group_diagnostics_by_severity, requires_levenshtein, LintWrapper, + diag_range_to_text, ) from debputy.lsprotocol.types import Diagnostic, DiagnosticSeverity @@ -732,7 +733,63 @@ def test_dctrl_lint_ambiguous_pkgfile(line_linter: LintWrapper) -> None: # 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"]) + line_linter.source_root = build_virtual_file_system( + [ + virtual_path_def(".", fs_path="."), + "./debian/bar.service", + ] + ) + + diagnostics = line_linter(lines) + print(diagnostics) + assert diagnostics and len(diagnostics) == 1 + issue = diagnostics[0] + + msg = ( + 'Possible typo in "./debian/bar.service". Consider renaming the file to "debian/foo.service"' + ' (or maybe "debian/foo.bar.service") 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.service", + "debian/bar.service", + ) + + +@build_time_only +def test_dctrl_lint_ambiguous_pkgfile_no_name_segment(line_linter: LintWrapper) -> None: + lines = textwrap.dedent( + f"""\ + Source: foo + Section: devel + Priority: optional + Standards-Version: {CURRENT_STANDARDS_VERSION} + Maintainer: Jane Developer <jane@example.com> + 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 + line_linter.source_root = build_virtual_file_system( + [ + virtual_path_def(".", fs_path="."), + "./debian/bar.alternatives", + ] + ) diagnostics = line_linter(lines) print(diagnostics) @@ -740,8 +797,8 @@ def test_dctrl_lint_ambiguous_pkgfile(line_linter: LintWrapper) -> None: 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' + 'Possible typo in "./debian/bar.alternatives". Consider renaming the file to "debian/foo.alternatives"' + " if it is intended for foo" ) assert issue.message == msg assert f"{issue.range}" == "7:0-8:0" @@ -749,8 +806,8 @@ def test_dctrl_lint_ambiguous_pkgfile(line_linter: LintWrapper) -> None: diag_data = issue.data assert isinstance(diag_data, dict) assert diag_data.get("report_for_related_file") in ( - "./debian/bar.install", - "debian/bar.install", + "./debian/bar.alternatives", + "debian/bar.alternatives", ) @@ -779,7 +836,12 @@ def test_dctrl_lint_stem_typo_pkgfile(line_linter: LintWrapper) -> None: # 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"]) + line_linter.source_root = build_virtual_file_system( + [ + virtual_path_def(".", fs_path="."), + "./debian/foo.intsall", + ] + ) diagnostics = line_linter(lines) print(diagnostics) @@ -827,6 +889,7 @@ def test_dctrl_lint_stem_inactive_pkgfile_fp(line_linter: LintWrapper) -> None: # load the `zz-debputy` sequence. line_linter.source_root = build_virtual_file_system( [ + virtual_path_def(".", fs_path="."), "./debian/foo.install", virtual_path_def( "./debian/rules", @@ -846,3 +909,372 @@ def test_dctrl_lint_stem_inactive_pkgfile_fp(line_linter: LintWrapper) -> None: print(diagnostics) # We should not emit diagnostics when the package is not using dh! assert not diagnostics + + +@requires_levenshtein +@build_time_only +def test_dctrl_lint_stem_typo_pkgfile_ignored_exts_or_files( + line_linter: LintWrapper, +) -> None: + lines = textwrap.dedent( + f"""\ + Source: foo + Section: devel + Priority: optional + Standards-Version: {CURRENT_STANDARDS_VERSION} + Maintainer: Jane Developer <jane@example.com> + 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( + [ + virtual_path_def(".", fs_path="."), + "debian/salsa-ci.yml", + "debian/gbp.conf", + "debian/foo.conf", + "debian/foo.sh", + "debian/foo.yml", + # One wrong one to ensure the test works. + "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", + ) + + +def test_dctrl_lint_dep_field_missing_sep( + line_linter: LintWrapper, +) -> None: + lines = textwrap.dedent( + f"""\ + Source: foo + Section: devel + Priority: optional + Standards-Version: {CURRENT_STANDARDS_VERSION} + Maintainer: Jane Developer <jane@example.com> + Build-Depends: debhelper-compat (= 13) + + Package: foo + Architecture: all + Depends: bar, baz + # Missing separator between baz and libfubar1 + libfubar1, + Description: some short synopsis + A very interesting description + with a valid 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 = ( + "Trailing data after a relationship that might be a second relationship." + " Is a separator missing before this part?" + ) + problem_text = diag_range_to_text(lines, issue.range) + assert issue.message == msg + assert problem_text == "libfubar1" + assert f"{issue.range}" == "11:1-11:10" + assert issue.severity == DiagnosticSeverity.Error + + +def test_dctrl_lint_dep_field_missing_sep_or_syntax_error( + line_linter: LintWrapper, +) -> None: + lines = textwrap.dedent( + f"""\ + Source: foo + Section: devel + Priority: optional + Standards-Version: {CURRENT_STANDARDS_VERSION} + Maintainer: Jane Developer <jane@example.com> + Build-Depends: debhelper-compat (= 13) + + Package: foo + Architecture: all + Depends: bar, baz + # Missing separator between baz and libfubar1 + _libfubar1, + Description: some short synopsis + A very interesting description + with a valid 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 = "Parse error of the relationship. Either a syntax error or a missing separator somewhere." + problem_text = diag_range_to_text(lines, issue.range) + assert issue.message == msg + assert problem_text == "_libfubar1" + assert f"{issue.range}" == "11:1-11:11" + assert issue.severity == DiagnosticSeverity.Error + + +def test_dctrl_lint_dep_field_completely_busted( + line_linter: LintWrapper, +) -> None: + lines = textwrap.dedent( + f"""\ + Source: foo + Section: devel + Priority: optional + Standards-Version: {CURRENT_STANDARDS_VERSION} + Maintainer: Jane Developer <jane@example.com> + Build-Depends: debhelper-compat (= 13) + + Package: foo + Architecture: all + Depends: bar, baz, _asd + # This is just busted + _libfubar1, + Description: some short synopsis + A very interesting description + with a valid 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 = 'Could not parse "_asd _libfubar1" as a dependency relation.' + problem_text = diag_range_to_text(lines, issue.range) + expected_problem_text = "\n".join((" _asd", "# This is just busted", " _libfubar1")) + assert issue.message == msg + assert problem_text == expected_problem_text + assert f"{issue.range}" == "9:18-11:11" + assert issue.severity == DiagnosticSeverity.Error + + +def test_dctrl_lint_dep_field_completely_busted_first_line( + line_linter: LintWrapper, +) -> None: + lines = textwrap.dedent( + f"""\ + Source: foo + Section: devel + Priority: optional + Standards-Version: {CURRENT_STANDARDS_VERSION} + Maintainer: Jane Developer <jane@example.com> + Build-Depends: debhelper-compat (= 13) + + Package: foo + Architecture: all + # A wild field comment appeared! + Depends: _bar, + asd, + # This is fine (but the _bar part is not) + libfubar1, + Description: some short synopsis + A very interesting description + with a valid 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 = 'Could not parse "_bar" as a dependency relation.' + problem_text = diag_range_to_text(lines, issue.range) + assert issue.message == msg + assert problem_text == " _bar" + assert f"{issue.range}" == "10:8-10:13" + assert issue.severity == DiagnosticSeverity.Error + + +def test_dctrl_lint_dep_field_restricted_operator( + line_linter: LintWrapper, +) -> None: + lines = textwrap.dedent( + f"""\ + Source: foo + Section: devel + Priority: optional + Standards-Version: {CURRENT_STANDARDS_VERSION} + Maintainer: Jane Developer <jane@example.com> + Build-Depends: debhelper-compat (= 13) + + Package: foo + Architecture: all + # Some random field comment + Provides: bar (>= 2), + bar + # Inline comment to spice up things + (<= 1), + # This one is valid + fubar (= 2), + Description: some short synopsis + A very interesting description + with a valid 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 = 'The version operator ">=" is not allowed in Provides' + problem_text = diag_range_to_text(lines, first_issue.range) + assert first_issue.message == msg + assert problem_text == ">=" + assert f"{first_issue.range}" == "10:15-10:17" + assert first_issue.severity == DiagnosticSeverity.Error + + msg = 'The version operator "<=" is not allowed in Provides' + problem_text = diag_range_to_text(lines, second_issue.range) + assert second_issue.message == msg + assert problem_text == "<=" + assert f"{second_issue.range}" == "13:2-13:4" + assert second_issue.severity == DiagnosticSeverity.Error + + +def test_dctrl_lint_dep_field_restricted_or_relations( + line_linter: LintWrapper, +) -> None: + lines = textwrap.dedent( + f"""\ + Source: foo + Section: devel + Priority: optional + Standards-Version: {CURRENT_STANDARDS_VERSION} + Maintainer: Jane Developer <jane@example.com> + Build-Depends: debhelper-compat (= 13) + + Package: foo + Architecture: all + Depends: pkg-a + | pkg-b + # What goes in Depends do not always work in Provides + Provides: foo-a + | foo-b + Description: some short synopsis + A very interesting description + with a valid 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 = 'The field Provides does not support "|" (OR) in relations.' + problem_text = diag_range_to_text(lines, issue.range) + assert issue.message == msg + assert problem_text == "|" + assert f"{issue.range}" == "13:1-13:2" + assert issue.severity == DiagnosticSeverity.Error + + +def test_dctrl_duplicate_key(line_linter: LintWrapper) -> None: + lines = textwrap.dedent( + f"""\ + Source: jquery-tablesorter + Section: javascript + Priority: optional + Maintainer: Debian Javascript Maintainers <pkg-javascript-devel@lists.alioth.de\ + bian.org> + Uploaders: Paul Gevers <elbrus@debian.org> + Build-Depends: + debhelper-compat (=13), + grunt, + libjs-qunit, + node-grunt-contrib-clean, + node-grunt-contrib-copy, + node-grunt-contrib-uglify, + node-grunt-contrib-concat, + Standards-Version: {CURRENT_STANDARDS_VERSION} + Homepage: https://github.com/Mottie/tablesorter + Vcs-Git: https://salsa.debian.org/js-team/jquery-tablesorter.git + Vcs-Browser: https://salsa.debian.org/js-team/jquery-tablesorter + Rules-Requires-Root: no + + Package: libjs-jquery-tablesorter + Architecture: all + Multi-Arch: foreign + Depends: + ${{misc:Depends}}, + libjs-jquery, + libjs-jquery-metadata, + Recommends: javascript-common + Multi-Arch: foreign + Description: jQuery flexible client-side table sorting plugin + Tablesorter is a jQuery plugin for turning a standard HTML table with THEAD + and TBODY tags into a sortable table without page refreshes. Tablesorter can + successfully parse and sort many types of data including linked data in a + cell. It has many useful features including: + . + * Multi-column alphanumeric sorting and filtering. + * Multi-tbody sorting + * Supports Bootstrap v2-4. + * Parsers for sorting text, alphanumeric text, URIs, integers, currency, + floats, IP addresses, dates (ISO, long and short formats) and time. + Add your own easily. + * Inline editing + * Support for ROWSPAN and COLSPAN on TH elements. + * Support secondary "hidden" sorting (e.g., maintain alphabetical sort when + sorting on other criteria). + * Extensibility via widget system. + * Cross-browser: IE 6.0+, FF 2+, Safari 2.0+, Opera 9.0+, Chrome 5.0+. + + """ + ).splitlines(keepends=True) + + diagnostics = line_linter(lines) + assert len(diagnostics) == 1 + + issue = diagnostics[0] + + msg = ( + "The Multi-Arch field name was used multiple times in this stanza." + " Please ensure the field is only used once per stanza. Note that Multi-Arch and" + " X[BCS]-Multi-Arch are considered the same field." + ) + assert issue.message == msg + assert f"{issue.range}" == "27:0-27:10" + assert issue.severity == DiagnosticSeverity.Error diff --git a/tests/lsp_tests/test_lsp_debputy_manifest_completer.py b/tests/lsp_tests/test_lsp_debputy_manifest_completer.py index f052164..92056ed 100644 --- a/tests/lsp_tests/test_lsp_debputy_manifest_completer.py +++ b/tests/lsp_tests/test_lsp_debputy_manifest_completer.py @@ -200,7 +200,7 @@ def test_basic_debputy_completer_manifest_variable_value( ) assert isinstance(completions, list) keywords = {m.label for m in completions} - assert "0.1" in keywords + assert "'0.1'" in keywords cursor_pos = put_doc_with_cursor( ls, @@ -219,7 +219,7 @@ def test_basic_debputy_completer_manifest_variable_value( ) assert isinstance(completions, list) keywords = {m.label for m in completions} - assert "0.1" in keywords + assert "'0.1'" in keywords def test_basic_debputy_completer_install_rule_dispatch_key( diff --git a/tests/test_debputy_plugin.py b/tests/test_debputy_plugin.py index dc60597..8a4cc59 100644 --- a/tests/test_debputy_plugin.py +++ b/tests/test_debputy_plugin.py @@ -19,6 +19,16 @@ from debputy.plugin.api.test_api import ( from debputy.plugin.api.test_api import manifest_variable_resolution_context from debputy.plugin.api.test_api.test_impl import initialize_plugin_under_test_preloaded from debputy.plugin.api.test_api.test_spec import DetectedService +from debputy.plugin.debputy.build_system_rules import ( + AutoconfBuildSystemRule, + MakefileBuildSystemRule, + PerlBuildBuildSystemRule, + PerlMakeMakerBuildSystemRule, + QmakeBuildSystemRule, + Qmake6BuildSystemRule, + CMakeBuildSystemRule, + MesonBuildSystemRule, +) from debputy.plugin.debputy.debputy_plugin import initialize_debputy_features from debputy.plugin.debputy.private_api import load_libcap from debputy.plugin.debputy.service_management import SystemdServiceContext @@ -1252,3 +1262,129 @@ def test_auto_depends_solink() -> None: context=context_too_many_matches, ) assert "misc:Depends" not in sodep_metadata.substvars + + +@pytest.mark.parametrize( + "filename,expected,mode,content", + [ + ("configure.ac", True, 0o0644, None), + ("configure.in", True, 0o0644, "AC_INIT"), + ("configure.in", True, 0o0644, "AC_PREREQ"), + ("configure.in", False, 0o0644, "None of the above"), + ("configure", True, 0o0755, "GNU Autoconf"), + ("configure", False, 0o0644, "GNU Autoconf"), + ("configure", False, 0o0755, "No magic keyword"), + ("random-file", False, 0o0644, "No configure at all"), + ], +) +def test_auto_detect_autoconf_build_system( + filename: str, + expected: bool, + mode: int, + content: Optional[str], +) -> None: + fs_root = build_virtual_file_system( + [virtual_path_def(filename, mode=mode, content=content)] + ) + detected = AutoconfBuildSystemRule.auto_detect_build_system(fs_root) + assert detected == expected + + +@pytest.mark.parametrize( + "filename,expected", + [ + ("GNUmakefile", True), + ("Makefile", True), + ("makefile", True), + ("random-file", False), + ], +) +def test_auto_detect_make_build_system( + filename: str, + expected: bool, +) -> None: + fs_root = build_virtual_file_system([filename]) + detected = MakefileBuildSystemRule.auto_detect_build_system(fs_root) + assert detected == expected + + +@pytest.mark.parametrize( + "filename,expected", + [ + ("Build.PL", True), + ("random-file", False), + ], +) +def test_auto_detect_perl_build_build_system( + filename: str, + expected: bool, +) -> None: + fs_root = build_virtual_file_system([filename]) + detected = PerlBuildBuildSystemRule.auto_detect_build_system(fs_root) + assert detected == expected + + +@pytest.mark.parametrize( + "filename,expected", + [ + ("Makefile.PL", True), + ("random-file", False), + ], +) +def test_auto_detect_perl_makemaker_build_system( + filename: str, + expected: bool, +) -> None: + fs_root = build_virtual_file_system([filename]) + detected = PerlMakeMakerBuildSystemRule.auto_detect_build_system(fs_root) + assert detected == expected + + +@pytest.mark.parametrize( + "filename,expected", + [ + ("foo.pro", True), + ("random-file", False), + ], +) +def test_auto_detect_qmake_build_systems( + filename: str, + expected: bool, +) -> None: + fs_root = build_virtual_file_system([filename]) + detected_qmake = QmakeBuildSystemRule.auto_detect_build_system(fs_root) + detected_qmake6 = Qmake6BuildSystemRule.auto_detect_build_system(fs_root) + assert detected_qmake == expected + assert detected_qmake6 == expected + + +@pytest.mark.parametrize( + "filename,expected", + [ + ("CMakeLists.txt", True), + ("random-file", False), + ], +) +def test_auto_detect_cmake_build_systems( + filename: str, + expected: bool, +) -> None: + fs_root = build_virtual_file_system([filename]) + detected = CMakeBuildSystemRule.auto_detect_build_system(fs_root) + assert detected == expected + + +@pytest.mark.parametrize( + "filename,expected", + [ + ("meson.build", True), + ("random-file", False), + ], +) +def test_auto_detect_meson_build_systems( + filename: str, + expected: bool, +) -> None: + fs_root = build_virtual_file_system([filename]) + detected = MesonBuildSystemRule.auto_detect_build_system(fs_root) + assert detected == expected diff --git a/tests/test_declarative_parser.py b/tests/test_declarative_parser.py index 26291dd..d52f1c3 100644 --- a/tests/test_declarative_parser.py +++ b/tests/test_declarative_parser.py @@ -10,11 +10,12 @@ from typing import ( import pytest from debputy.highlevel_manifest import PackageTransformationDefinition -from debputy.manifest_parser.base_types import DebputyParsedContent, TypeMapping -from debputy.manifest_parser.declarative_parser import ( - DebputyParseHint, - ParserGenerator, +from debputy.manifest_parser.tagging_types import ( + DebputyParsedContent, + TypeMapping, ) +from debputy.manifest_parser.parse_hints import DebputyParseHint +from debputy.manifest_parser.declarative_parser import ParserGenerator from debputy.manifest_parser.mapper_code import type_mapper_str2package from debputy.manifest_parser.parser_data import ParserContextData from debputy.manifest_parser.util import AttributePath diff --git a/tests/test_fs_metadata.py b/tests/test_fs_metadata.py index 7dd3d55..3cbbb03 100644 --- a/tests/test_fs_metadata.py +++ b/tests/test_fs_metadata.py @@ -132,7 +132,9 @@ def test_mtime_clamp_and_builtin_dir_mode( verify_paths(intermediate_manifest, path_defs) -def test_transformations_create_symlink(manifest_parser_pkg_foo): +def test_transformations_create_symlink( + manifest_parser_pkg_foo: YAMLManifestParser, +) -> None: content = textwrap.dedent( """\ manifest-version: '0.1' @@ -178,7 +180,9 @@ def test_transformations_create_symlink(manifest_parser_pkg_foo): verify_paths(intermediate_manifest, expected_results) -def test_transformations_create_symlink_replace_success(manifest_parser_pkg_foo): +def test_transformations_create_symlink_replace_success( + manifest_parser_pkg_foo: YAMLManifestParser, +) -> None: content = textwrap.dedent( """\ manifest-version: '0.1' @@ -230,8 +234,10 @@ def test_transformations_create_symlink_replace_success(manifest_parser_pkg_foo) ], ) def test_transformations_create_symlink_replace_failure( - manifest_parser_pkg_foo, replacement_rule, reason -): + manifest_parser_pkg_foo: YAMLManifestParser, + replacement_rule: str, + reason: str, +) -> None: content = textwrap.dedent( f"""\ manifest-version: '0.1' @@ -257,14 +263,15 @@ def test_transformations_create_symlink_replace_failure( f"Refusing to replace ./usr/share/foo with a symlink; {reason} and the active" f" replacement-rule was {replacement_rule}. You can set the replacement-rule to" ' "discard-existing", if you are not interested in the contents of ./usr/share/foo. This error' - " was triggered by packages.foo.transformations[0].create-symlink <Search for: usr/share/foo>." + # Ideally, this would be reported for line 5. + " was triggered by packages.foo.transformations[0].create-symlink [Line 6 column 18]." ) assert e_info.value.args[0] == msg def test_transformations_create_symlink_replace_with_explicit_remove( - manifest_parser_pkg_foo, -): + manifest_parser_pkg_foo: YAMLManifestParser, +) -> None: content = textwrap.dedent( """\ manifest-version: '0.1' @@ -299,8 +306,8 @@ def test_transformations_create_symlink_replace_with_explicit_remove( def test_transformations_create_symlink_replace_with_replacement_rule( - manifest_parser_pkg_foo, -): + manifest_parser_pkg_foo: YAMLManifestParser, +) -> None: content = textwrap.dedent( """\ manifest-version: '0.1' @@ -335,7 +342,9 @@ def test_transformations_create_symlink_replace_with_replacement_rule( verify_paths(intermediate_manifest, expected_results) -def test_transformations_path_metadata(manifest_parser_pkg_foo): +def test_transformations_path_metadata( + manifest_parser_pkg_foo: YAMLManifestParser, +) -> None: content = textwrap.dedent( """\ manifest-version: '0.1' @@ -382,7 +391,9 @@ def test_transformations_path_metadata(manifest_parser_pkg_foo): verify_paths(intermediate_manifest, expected_results) -def test_transformations_directories(manifest_parser_pkg_foo): +def test_transformations_directories( + manifest_parser_pkg_foo: YAMLManifestParser, +) -> None: content = textwrap.dedent( """\ manifest-version: '0.1' @@ -453,7 +464,9 @@ def test_transformations_directories(manifest_parser_pkg_foo): verify_paths(intermediate_manifest, expected_results) -def test_transformation_remove(manifest_parser_pkg_foo): +def test_transformation_remove( + manifest_parser_pkg_foo: YAMLManifestParser, +) -> None: content = textwrap.dedent( """\ manifest-version: '0.1' @@ -489,7 +502,9 @@ def test_transformation_remove(manifest_parser_pkg_foo): verify_paths(intermediate_manifest, expected_results) -def test_transformation_remove_keep_empty(manifest_parser_pkg_foo): +def test_transformation_remove_keep_empty( + manifest_parser_pkg_foo: YAMLManifestParser, +) -> None: content = textwrap.dedent( """\ manifest-version: '0.1' @@ -529,7 +544,9 @@ def test_transformation_remove_keep_empty(manifest_parser_pkg_foo): verify_paths(intermediate_manifest, expected_results) -def test_transformation_remove_glob(manifest_parser_pkg_foo): +def test_transformation_remove_glob( + manifest_parser_pkg_foo: YAMLManifestParser, +) -> None: content = textwrap.dedent( """\ manifest-version: '0.1' @@ -581,7 +598,9 @@ def test_transformation_remove_glob(manifest_parser_pkg_foo): verify_paths(intermediate_manifest, expected_results) -def test_transformation_remove_no_match(manifest_parser_pkg_foo): +def test_transformation_remove_no_match( + manifest_parser_pkg_foo: YAMLManifestParser, +) -> None: content = textwrap.dedent( """\ manifest-version: '0.1' @@ -612,13 +631,15 @@ def test_transformation_remove_no_match(manifest_parser_pkg_foo): manifest.apply_to_binary_staging_directory("foo", fs_root, claim_mtime_to) expected = ( 'The match rule "./some/non-existing-path" in transformation' - ' "packages.foo.transformations[0].remove <Search for: some/non-existing-path>" did not match any paths. Either' + ' "packages.foo.transformations[0].remove [Line 5 column 18]" did not match any paths. Either' " the definition is redundant (and can be omitted) or the match rule is incorrect." ) assert expected == e_info.value.args[0] -def test_transformation_move_basic(manifest_parser_pkg_foo): +def test_transformation_move_basic( + manifest_parser_pkg_foo: YAMLManifestParser, +) -> None: content = textwrap.dedent( """\ manifest-version: '0.1' @@ -682,7 +703,9 @@ def test_transformation_move_basic(manifest_parser_pkg_foo): verify_paths(intermediate_manifest, expected_results) -def test_transformation_move_no_match(manifest_parser_pkg_foo): +def test_transformation_move_no_match( + manifest_parser_pkg_foo: YAMLManifestParser, +) -> None: content = textwrap.dedent( """\ manifest-version: '0.1' @@ -715,13 +738,15 @@ def test_transformation_move_no_match(manifest_parser_pkg_foo): manifest.apply_to_binary_staging_directory("foo", fs_root, claim_mtime_to) expected = ( 'The match rule "./some/non-existing-path" in transformation' - ' "packages.foo.transformations[0].move <Search for: some/non-existing-path>" did not match any paths. Either' + ' "packages.foo.transformations[0].move [Line 6 column 12]" did not match any paths. Either' " the definition is redundant (and can be omitted) or the match rule is incorrect." ) assert expected == e_info.value.args[0] -def test_builtin_mode_normalization(manifest_parser_pkg_foo): +def test_builtin_mode_normalization_shell_scripts( + manifest_parser_pkg_foo: YAMLManifestParser, +) -> None: manifest = manifest_parser_pkg_foo.build_manifest() claim_mtime_to = 255 sh_script_content = "#!/bin/sh" @@ -773,3 +798,53 @@ def test_builtin_mode_normalization(manifest_parser_pkg_foo): print(intermediate_manifest) verify_paths(intermediate_manifest, expected_results) + + +def test_builtin_mode_normalization( + manifest_parser_pkg_foo: YAMLManifestParser, +) -> None: + manifest = manifest_parser_pkg_foo.build_manifest() + claim_mtime_to = 255 + + paths = [ + virtual_path_def("usr/", mode=0o755, mtime=10, fs_path="/nowhere/usr"), + virtual_path_def( + "usr/share/", mode=0o755, mtime=10, fs_path="/nowhere/usr/share" + ), + virtual_path_def( + "usr/share/perl5/", mode=0o755, mtime=10, fs_path="/nowhere/usr/share/perl5" + ), + virtual_path_def( + "usr/share/perl5/Foo.pm", + # #1076346 + mode=0o444, + mtime=10, + fs_path="/nowhere/Foo.pm", + ), + virtual_path_def( + "usr/share/perl5/Bar.pm", + mode=0o755, + mtime=10, + fs_path="/nowhere/Bar.pm", + ), + ] + + fs_root = build_virtual_fs(paths, read_write_fs=True) + assert [p.name for p in manifest.all_packages] == ["foo"] + + expected_results = [ + ("usr/", Expected(mode=0o755, mtime=10)), + ("usr/share/", Expected(mode=0o755, mtime=10)), + ("usr/share/perl5/", Expected(mode=0o755, mtime=10)), + ("usr/share/perl5/Bar.pm", Expected(mode=0o644, mtime=10)), + ("usr/share/perl5/Foo.pm", Expected(mode=0o644, mtime=10)), + ] + assert [p.name for p in manifest.all_packages] == ["foo"] + + intermediate_manifest = manifest.apply_to_binary_staging_directory( + "foo", fs_root, claim_mtime_to + ) + + print(intermediate_manifest) + + verify_paths(intermediate_manifest, expected_results) diff --git a/tests/test_install_rules.py b/tests/test_install_rules.py index a361864..ecc49fd 100644 --- a/tests/test_install_rules.py +++ b/tests/test_install_rules.py @@ -9,6 +9,7 @@ from debputy.installations import ( SearchDir, ) from debputy.plugin.api import virtual_path_def +from debputy.plugin.api.spec import INTEGRATION_MODE_DH_DEBPUTY from debputy.plugin.api.test_api import build_virtual_file_system @@ -118,13 +119,14 @@ def test_install_rules(manifest_parser_pkg_foo) -> None: all_pkgs = frozenset(manifest.all_packages) result = manifest.perform_installations( + INTEGRATION_MODE_DH_DEBPUTY, install_request_context=InstallSearchDirContext( [ SearchDir(debian_tmp_dir, all_pkgs), SearchDir(debian_source_root_dir, all_pkgs), ], [debian_tmp_dir], - ) + ), ) assert "foo" in result foo_fs_root = result["foo"].fs_root @@ -207,12 +209,13 @@ def test_multi_dest_install_rules(manifest_parser_pkg_foo) -> None: all_pkgs = frozenset(manifest.all_packages) result = manifest.perform_installations( + INTEGRATION_MODE_DH_DEBPUTY, install_request_context=InstallSearchDirContext( [ SearchDir(debian_source_root_dir, all_pkgs), ], [], - ) + ), ) assert "foo" in result foo_fs_root = result["foo"].fs_root @@ -300,13 +303,14 @@ def test_install_rules_with_glob(manifest_parser_pkg_foo) -> None: all_pkgs = frozenset(manifest.all_packages) result = manifest.perform_installations( + INTEGRATION_MODE_DH_DEBPUTY, install_request_context=InstallSearchDirContext( [ SearchDir(debian_tmp_dir, all_pkgs), SearchDir(debian_source_root_dir, all_pkgs), ], [debian_tmp_dir], - ) + ), ) assert "foo" in result foo_fs_root = result["foo"].fs_root @@ -365,13 +369,14 @@ def test_install_rules_auto_discard_rules_dir(manifest_parser_pkg_foo) -> None: all_pkgs = frozenset(manifest.all_packages) result = manifest.perform_installations( + INTEGRATION_MODE_DH_DEBPUTY, install_request_context=InstallSearchDirContext( [ SearchDir(debian_tmp_dir, all_pkgs), SearchDir(debian_source_root_dir, all_pkgs), ], [debian_tmp_dir], - ) + ), ) assert "foo" in result foo_fs_root = result["foo"].fs_root @@ -424,13 +429,14 @@ def test_install_rules_auto_discard_rules_glob(manifest_parser_pkg_foo) -> None: all_pkgs = frozenset(manifest.all_packages) result = manifest.perform_installations( + INTEGRATION_MODE_DH_DEBPUTY, install_request_context=InstallSearchDirContext( [ SearchDir(debian_tmp_dir, all_pkgs), SearchDir(debian_source_root_dir, all_pkgs), ], [debian_tmp_dir], - ) + ), ) assert "foo" in result foo_fs_root = result["foo"].fs_root @@ -490,13 +496,14 @@ def test_install_rules_auto_discard_rules_overruled_by_explicit_install_rule( all_pkgs = frozenset(manifest.all_packages) result = manifest.perform_installations( + INTEGRATION_MODE_DH_DEBPUTY, install_request_context=InstallSearchDirContext( [ SearchDir(debian_tmp_dir, all_pkgs), SearchDir(debian_source_root_dir, all_pkgs), ], [debian_tmp_dir], - ) + ), ) assert "foo" in result foo_fs_root = result["foo"].fs_root @@ -559,13 +566,14 @@ def test_install_rules_install_as_with_var(manifest_parser_pkg_foo) -> None: all_pkgs = frozenset(manifest.all_packages) result = manifest.perform_installations( + INTEGRATION_MODE_DH_DEBPUTY, install_request_context=InstallSearchDirContext( [ SearchDir(debian_tmp_dir, all_pkgs), SearchDir(debian_source_root_dir, all_pkgs), ], [debian_tmp_dir], - ) + ), ) assert "foo" in result foo_fs_root = result["foo"].fs_root @@ -610,17 +618,18 @@ def test_install_rules_no_matches(manifest_parser_pkg_foo) -> None: with pytest.raises(NoMatchForInstallPatternError) as e_info: manifest.perform_installations( + INTEGRATION_MODE_DH_DEBPUTY, install_request_context=InstallSearchDirContext( [ SearchDir(debian_tmp_dir, all_pkgs), SearchDir(debian_source_root_dir, all_pkgs), ], [debian_tmp_dir], - ) + ), ) expected_msg = ( "There were no matches for build/private-arch-tool in /nowhere/debian/tmp, /nowhere" - " (definition: installations[0].install <Search for: build/private-arch-tool>)." + " (definition: installations[0].install [Line 5 column 6])." " Match rule: ./build/private-arch-tool (the exact path / no globbing)" ) assert e_info.value.message == expected_msg @@ -719,6 +728,7 @@ def test_install_rules_per_package_search_dirs(manifest_parser_pkg_foo_w_udeb) - all_udeb_pkgs = frozenset({p for p in all_pkgs if p.is_udeb}) result = manifest.perform_installations( + INTEGRATION_MODE_DH_DEBPUTY, install_request_context=InstallSearchDirContext( [ SearchDir(debian_tmp_deb_dir, all_deb_pkgs), @@ -726,7 +736,7 @@ def test_install_rules_per_package_search_dirs(manifest_parser_pkg_foo_w_udeb) - SearchDir(debian_source_root_dir, all_pkgs), ], [debian_tmp_deb_dir], - ) + ), ) for pkg, ptype in [ ("foo", "deb"), @@ -820,12 +830,13 @@ def test_install_rules_multi_into(manifest_parser_pkg_foo_w_udeb) -> None: all_pkgs = frozenset(manifest.all_packages) result = manifest.perform_installations( + INTEGRATION_MODE_DH_DEBPUTY, install_request_context=InstallSearchDirContext( [ SearchDir(debian_source_root_dir, all_pkgs), ], [], - ) + ), ) for pkg in ["foo", "foo-udeb"]: assert pkg in result @@ -939,6 +950,7 @@ def test_auto_install_d_pkg(manifest_parser_pkg_foo_w_udeb) -> None: all_pkgs = frozenset(manifest.all_packages) result = manifest.perform_installations( + INTEGRATION_MODE_DH_DEBPUTY, install_request_context=InstallSearchDirContext( [ SearchDir(debian_source_root_dir, all_pkgs), @@ -948,7 +960,7 @@ def test_auto_install_d_pkg(manifest_parser_pkg_foo_w_udeb) -> None: "foo": debian_foo_dir, "foo-udeb": debian_foo_udeb_dir, }, - ) + ), ) for pkg in ["foo", "foo-udeb"]: assert pkg in result @@ -1024,12 +1036,13 @@ def test_install_doc_rules_ignore_udeb(manifest_parser_pkg_foo_w_udeb) -> None: all_pkgs = frozenset(manifest.all_packages) result = manifest.perform_installations( + INTEGRATION_MODE_DH_DEBPUTY, install_request_context=InstallSearchDirContext( [ SearchDir(debian_source_root_dir, all_pkgs), ], [], - ) + ), ) assert "foo" in result foo_fs_root = result["foo"].fs_root diff --git a/tests/test_migrations.py b/tests/test_migrations.py index f53c716..0704c60 100644 --- a/tests/test_migrations.py +++ b/tests/test_migrations.py @@ -4,6 +4,7 @@ from typing import Callable, Optional, List, Tuple, Sequence import pytest +from debputy.commands.debputy_cmd.output import no_fancy_output from debputy.dh_migration.migrators import Migrator from debputy.dh_migration.migrators_impl import ( migrate_tmpfile, @@ -15,7 +16,7 @@ from debputy.dh_migration.migrators_impl import ( migrate_install_file, migrate_maintscript, migrate_links_files, - detect_dh_addons, + detect_dh_addons_with_zz_integration, migrate_not_installed_file, migrate_installman_file, migrate_bash_completion, @@ -99,7 +100,7 @@ def run_migrator( *, migration_target=INTEGRATION_MODE_DH_DEBPUTY, ) -> FeatureMigration: - feature_migration = FeatureMigration(migrator.__name__) + feature_migration = FeatureMigration(migrator.__name__, no_fancy_output()) migrator( path, manifest, @@ -1308,7 +1309,7 @@ def test_detect_obsolete_substvars( ) msg = ( "The following relationship substitution variables can be removed from foo:" - " ${misc:Depends}, ${shlibs:Depends}, ${so:Depends}" + " ${misc:Depends}, ${shlibs:Depends}, ${so:Depends} (Note: https://bugs.debian.org/1067653)" ) assert migration.anything_to_do assert migration.warnings == [msg] @@ -1356,7 +1357,7 @@ def test_detect_obsolete_substvars_remove_field( ) msg = ( "The following relationship fields can be removed from foo: Pre-Depends." - " (The content in them would be applied automatically.)" + " (The content in them would be applied automatically. Note: https://bugs.debian.org/1067653)" ) assert migration.anything_to_do assert migration.warnings == [msg] @@ -1406,7 +1407,7 @@ def test_detect_obsolete_substvars_remove_field_essential( ) msg = ( "The following relationship fields can be removed from foo: Pre-Depends." - " (The content in them would be applied automatically.)" + " (The content in them would be applied automatically. Note: https://bugs.debian.org/1067653)" ) assert migration.anything_to_do assert migration.warnings == [msg] @@ -1465,7 +1466,7 @@ def test_detect_dh_addons( accept_no_migration_issues: AcceptableMigrationIssues, accept_any_migration_issues: AcceptableMigrationIssues, ) -> None: - migrator = detect_dh_addons + migrator = detect_dh_addons_with_zz_integration empty_fs = build_virtual_file_system([DEBIAN_DIR_ENTRY]) dctrl_no_addons_content = textwrap.dedent( diff --git a/tests/test_parser.py b/tests/test_parser.py index 4aee024..0b1ed56 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -124,7 +124,10 @@ def test_parsing_variables_reserved(manifest_parser_pkg_foo, varname): with pytest.raises(ManifestParseException) as e_info: manifest_parser_pkg_foo.parse_manifest(fd=content) - msg = f'The variable "{varname}" is already reserved/defined. Error triggered by definitions.variables.{varname}.' + msg = ( + f'The variable "{varname}" is already reserved/defined.' + f" Error triggered by definitions.variables.{varname} [Line 4 column 4]." + ) assert normalize_doc_link(e_info.value.args[0]) == msg @@ -163,7 +166,7 @@ def test_parsing_variables_unused(manifest_parser_pkg_foo): msg = ( 'The variable "UNUSED" is unused. Either use it or remove it.' - " The variable was declared at definitions.variables.UNUSED." + " The variable was declared at definitions.variables.UNUSED [Line 4 column 4]." ) assert normalize_doc_link(e_info.value.args[0]) == msg @@ -181,7 +184,7 @@ def test_parsing_package_foo_empty(manifest_parser_pkg_foo): manifest_parser_pkg_foo.parse_manifest(fd=content) msg = ( - "The attribute packages.foo must be a non-empty mapping. Please see" + "The attribute packages.foo [Line 3 column 4] must be a non-empty mapping. Please see" " {{DEBPUTY_DOC_ROOT_DIR}}/MANIFEST-FORMAT.md#binary-package-rules for the documentation." ) assert normalize_doc_link(e_info.value.args[0]) == msg @@ -238,8 +241,8 @@ def test_create_symlinks_missing_path(manifest_parser_pkg_foo): manifest_parser_pkg_foo.parse_manifest(fd=content) msg = ( - "The following keys were required but not present at packages.foo.transformations[0].create-symlink: 'path'" - " (Documentation: " + "The following keys were required but not present at packages.foo.transformations[0].create-symlink" + " [Line 5 column 12]: 'path' (Documentation: " "{{DEBPUTY_DOC_ROOT_DIR}}/MANIFEST-FORMAT.md#create-symlinks-transformation-rule-create-symlink)" ) assert normalize_doc_link(e_info.value.args[0]) == msg @@ -263,7 +266,7 @@ def test_create_symlinks_unknown_replacement_rule(manifest_parser_pkg_foo): manifest_parser_pkg_foo.parse_manifest(fd=content) msg = ( - 'The attribute "packages.foo.transformations[0].create-symlink.replacement-rule <Search for: usr/share/foo>"' + 'The attribute "packages.foo.transformations[0].create-symlink.replacement-rule [Line 8 column 32]"' " did not have a valid structure/type: Value (golf) must be one of the following literal values:" ' "error-if-exists", "error-if-directory", "abort-on-non-empty-directory", "discard-existing"' ) @@ -286,8 +289,8 @@ def test_create_symlinks_missing_target(manifest_parser_pkg_foo): manifest_parser_pkg_foo.parse_manifest(fd=content) msg = ( - "The following keys were required but not present at packages.foo.transformations[0].create-symlink: 'target'" - " (Documentation: " + "The following keys were required but not present at packages.foo.transformations[0].create-symlink" + " [Line 5 column 12]: 'target' (Documentation: " "{{DEBPUTY_DOC_ROOT_DIR}}/MANIFEST-FORMAT.md#create-symlinks-transformation-rule-create-symlink)" ) assert normalize_doc_link(e_info.value.args[0]) == msg @@ -310,7 +313,7 @@ def test_create_symlinks_not_normalized_path(manifest_parser_pkg_foo): manifest_parser_pkg_foo.parse_manifest(fd=content) expected = ( - 'The path "../bar" provided in packages.foo.transformations[0].create-symlink.path <Search for: ../bar>' + 'The path "../bar" provided in packages.foo.transformations[0].create-symlink.path [Line 6 column 20]' ' should be relative to the root of the package and not use any ".." or "." segments.' ) assert e_info.value.args[0] == expected @@ -332,7 +335,7 @@ def test_unresolvable_subst_in_source_context(manifest_parser_pkg_foo): expected = ( "The variable {{PACKAGE}} is not available while processing installations[0].install.as" - " <Search for: foo.sh>." + " [Line 5 column 7]." ) assert e_info.value.args[0] == expected @@ -397,7 +400,7 @@ def test_yaml_octal_mode_int(manifest_parser_pkg_foo): manifest_parser_pkg_foo.parse_manifest(fd=content) msg = ( - 'The attribute "packages.foo.transformations[0].path-metadata.mode <Search for: usr/share/bar>" did not' + 'The attribute "packages.foo.transformations[0].path-metadata.mode [Line 7 column 20]" did not' " have a valid structure/type: The attribute must be a FileSystemMode (string)" ) @@ -486,3 +489,86 @@ def test_yaml_clean_after_removal_unsafe_path( else: with pytest.raises(ManifestParseException) as e_info: manifest_parser_pkg_foo.parse_manifest(fd=content) + + +def test_yaml_build_environment_default( + manifest_parser_pkg_foo: YAMLManifestParser, +) -> None: + content = textwrap.dedent( + ( + """\ + + manifest-version: '0.1' + default-build-environment: + set: + FOO: "bar" + builds: + # FIXME: we should not require an empty dict here + - autoconf: {} + """ + ) + ) + manifest = manifest_parser_pkg_foo.parse_manifest(fd=content) + envs = manifest.build_environments + assert not envs.environments + base_env = {} + envs.default_environment.update_env(base_env) + assert "FOO" in base_env + build_rule = manifest.build_rules[0] + assert build_rule.environment is envs.default_environment + + +def test_yaml_build_environments_no_default( + manifest_parser_pkg_foo: YAMLManifestParser, +) -> None: + content = textwrap.dedent( + ( + f"""\ + + manifest-version: '0.1' + build-environments: + - name: custom-env + set: + FOO: "bar" + builds: + - autoconf: + environment: custom-env + """ + ) + ) + manifest = manifest_parser_pkg_foo.parse_manifest(fd=content) + envs = manifest.build_environments + assert "custom-env" in envs.environments + custom_env = envs.environments["custom-env"] + assert envs.default_environment is None + base_env = {} + custom_env.update_env(base_env) + assert "FOO" in base_env + build_rule = manifest.build_rules[0] + assert build_rule.environment is custom_env + + +def test_yaml_build_environments_no_default_error( + manifest_parser_pkg_foo: YAMLManifestParser, +) -> None: + content = textwrap.dedent( + ( + """\ + + manifest-version: '0.1' + build-environments: + - name: custom-env + set: + FOO: "bar" + builds: + # FIXME: we should not require an empty dict here + - autoconf: {} + """ + ) + ) + with pytest.raises(ManifestParseException) as e_info: + manifest_parser_pkg_foo.parse_manifest(fd=content) + + expected_msg = "The following named environments were never referenced: custom-env" + msg = e_info.value.args[0] + assert msg == expected_msg diff --git a/tests/test_style.py b/tests/test_style.py index ef6ddc4..8f5b6ca 100644 --- a/tests/test_style.py +++ b/tests/test_style.py @@ -4,51 +4,57 @@ import pytest from debian.deb822 import Deb822 from debputy.yaml.compat import CommentedMap -from debputy.lsp.style_prefs import ( - StylePreferenceTable, - determine_effective_style, - EffectivePreference, +from debputy.lsp.maint_prefs import ( + MaintainerPreferenceTable, + determine_effective_preference, + EffectiveFormattingPreference, _WAS_DEFAULTS, ) from debputy.packages import SourcePackage def test_load_styles() -> None: - styles = StylePreferenceTable.load_styles() + styles = MaintainerPreferenceTable.load_preferences() assert "niels@thykier.net" in styles.maintainer_preferences - nt_style = styles.maintainer_preferences["niels@thykier.net"] + nt_maint_pref = styles.maintainer_preferences["niels@thykier.net"] # Note this is data dependent; if it fails because the style changes, update the test - assert nt_style.canonical_name == "Niels Thykier" - assert not nt_style.is_packaging_team - assert nt_style.formatting_deb822_normalize_field_content - assert nt_style.formatting_deb822_short_indent - assert nt_style.formatting_deb822_always_wrap - assert nt_style.formatting_deb822_trailing_separator - assert nt_style.formatting_deb822_max_line_length == 79 - assert not nt_style.formatting_deb822_normalize_stanza_order + assert nt_maint_pref.canonical_name == "Niels Thykier" + assert not nt_maint_pref.is_packaging_team + black_style = styles.named_styles["black"] + nt_style = nt_maint_pref.formatting + assert nt_style is not None + assert black_style == black_style - # TODO: Not implemented yet - assert not nt_style.formatting_deb822_normalize_field_order + +def test_load_no_styles() -> None: + styles = MaintainerPreferenceTable.load_preferences() + assert "packages@qa.debian.org" in styles.maintainer_preferences + qa_maint_pref = styles.maintainer_preferences["packages@qa.debian.org"] + assert qa_maint_pref.canonical_name == "Debian QA Group" + assert qa_maint_pref.is_packaging_team + # Orphaned packages do not have a predefined style, since Debian (nor Debian QA) have + # one well-defined style. + assert qa_maint_pref.formatting is None def test_load_named_styles() -> None: - styles = StylePreferenceTable.load_styles() + styles = MaintainerPreferenceTable.load_preferences() assert "black" in styles.named_styles black_style = styles.named_styles["black"] # Note this is data dependent; if it fails because the style changes, update the test - assert black_style.formatting_deb822_normalize_field_content - assert black_style.formatting_deb822_short_indent - assert black_style.formatting_deb822_always_wrap - assert black_style.formatting_deb822_trailing_separator - assert black_style.formatting_deb822_max_line_length == 79 - assert not black_style.formatting_deb822_normalize_stanza_order + assert black_style.deb822_normalize_field_content + assert black_style.deb822_short_indent + assert black_style.deb822_always_wrap + assert black_style.deb822_trailing_separator + assert black_style.deb822_max_line_length == 79 + assert not black_style.deb822_normalize_stanza_order # TODO: Not implemented yet - assert not black_style.formatting_deb822_normalize_field_order + assert not black_style.deb822_normalize_field_order def test_compat_styles() -> None: - styles = StylePreferenceTable.load_styles() + styles = MaintainerPreferenceTable.load_preferences() # Data dependent; if it breaks, provide a stubbed style preference table assert "niels@thykier.net" in styles.maintainer_preferences @@ -56,11 +62,11 @@ def test_compat_styles() -> None: assert "random-package@packages.debian.org" not in styles.maintainer_preferences assert "random@example.org" not in styles.maintainer_preferences - nt_pref = styles.maintainer_preferences["niels@thykier.net"].as_effective_pref() - zeha_pref = styles.maintainer_preferences["zeha@debian.org"].as_effective_pref() + nt_style = styles.maintainer_preferences["niels@thykier.net"].formatting + zeha_style = styles.maintainer_preferences["zeha@debian.org"].formatting # Data dependency - assert nt_pref == zeha_pref + assert nt_style == zeha_style fields = Deb822( { @@ -71,30 +77,33 @@ def test_compat_styles() -> None: ) src = SourcePackage(fields) - effective_style, _ = determine_effective_style(styles, src, None) - assert effective_style == nt_pref + effective_style, tool, _ = determine_effective_preference(styles, src, None) + assert effective_style == nt_style + assert tool == "debputy reformat" fields["Uploaders"] = ( "Niels Thykier <niels@thykier.net>, Chris Hofstaedtler <zeha@debian.org>" ) src = SourcePackage(fields) - effective_style, _ = determine_effective_style(styles, src, None) - assert effective_style == nt_pref - assert effective_style == zeha_pref + effective_style, tool, _ = determine_effective_preference(styles, src, None) + assert effective_style == nt_style + assert effective_style == zeha_style + assert tool == "debputy reformat" fields["Uploaders"] = ( "Niels Thykier <niels@thykier.net>, Chris Hofstaedtler <zeha@debian.org>, Random Developer <random@example.org>" ) src = SourcePackage(fields) - effective_style, _ = determine_effective_style(styles, src, None) + effective_style, tool, _ = determine_effective_preference(styles, src, None) assert effective_style is None + assert tool is None @pytest.mark.xfail def test_compat_styles_team_maint() -> None: - styles = StylePreferenceTable.load_styles() + styles = MaintainerPreferenceTable.load_preferences() fields = Deb822( { "Package": "foo", @@ -108,12 +117,13 @@ def test_compat_styles_team_maint() -> None: assert "random@example.org" not in styles.maintainer_preferences team_style = styles.maintainer_preferences["team@lists.debian.org"] assert team_style.is_packaging_team - effective_style, _ = determine_effective_style(styles, src, None) - assert effective_style == team_style.as_effective_pref() + effective_style, tool, _ = determine_effective_preference(styles, src, None) + assert effective_style == team_style.formatting + assert tool is None def test_x_style() -> None: - styles = StylePreferenceTable.load_styles() + styles = MaintainerPreferenceTable.load_preferences() fields = Deb822( { "Package": "foo", @@ -125,12 +135,13 @@ def test_x_style() -> None: assert "random@example.org" not in styles.maintainer_preferences assert "black" in styles.named_styles black_style = styles.named_styles["black"] - effective_style, _ = determine_effective_style(styles, src, None) + effective_style, tool, _ = determine_effective_preference(styles, src, None) assert effective_style == black_style + assert tool == "debputy reformat" def test_was_from_salsa_ci_style() -> None: - styles = StylePreferenceTable.load_styles() + styles = MaintainerPreferenceTable.load_preferences() fields = Deb822( { "Package": "foo", @@ -139,20 +150,23 @@ def test_was_from_salsa_ci_style() -> None: ) src = SourcePackage(fields) assert "random@example.org" not in styles.maintainer_preferences - effective_style, _ = determine_effective_style(styles, src, None) + effective_style, tool, _ = determine_effective_preference(styles, src, None) assert effective_style is None + assert tool is None salsa_ci = CommentedMap( {"variables": CommentedMap({"SALSA_CI_DISABLE_WRAP_AND_SORT": "yes"})} ) - effective_style, _ = determine_effective_style(styles, src, salsa_ci) + effective_style, tool, _ = determine_effective_preference(styles, src, salsa_ci) assert effective_style is None + assert tool is None salsa_ci = CommentedMap( {"variables": CommentedMap({"SALSA_CI_DISABLE_WRAP_AND_SORT": "no"})} ) - effective_style, _ = determine_effective_style(styles, src, salsa_ci) - was_style = EffectivePreference(**_WAS_DEFAULTS) + effective_style, tool, _ = determine_effective_preference(styles, src, salsa_ci) + was_style = EffectiveFormattingPreference(**_WAS_DEFAULTS) assert effective_style == was_style + assert tool == "wrap-and-sort" @pytest.mark.parametrize( @@ -161,45 +175,46 @@ def test_was_from_salsa_ci_style() -> None: ( "-a", { - "formatting_deb822_always_wrap": True, + "deb822_always_wrap": True, }, ), ( "-sa", { - "formatting_deb822_always_wrap": True, - "formatting_deb822_short_indent": True, + "deb822_always_wrap": True, + "deb822_short_indent": True, }, ), ( "-sa --keep-first", { - "formatting_deb822_always_wrap": True, - "formatting_deb822_short_indent": True, + "deb822_always_wrap": True, + "deb822_short_indent": True, }, ), ( "-sab --keep-first", { - "formatting_deb822_always_wrap": True, - "formatting_deb822_short_indent": True, - "formatting_deb822_normalize_stanza_order": True, + "deb822_always_wrap": True, + "deb822_short_indent": True, + "deb822_normalize_stanza_order": True, }, ), ( "-sab --no-keep-first", { - "formatting_deb822_always_wrap": True, - "formatting_deb822_short_indent": True, - "formatting_deb822_normalize_stanza_order": False, + "deb822_always_wrap": True, + "deb822_short_indent": True, + "deb822_normalize_stanza_order": False, }, ), ], ) def test_was_from_salsa_ci_style_args( - was_args: str, style_delta: Optional[Mapping[str, Any]] + was_args: str, + style_delta: Optional[Mapping[str, Any]], ) -> None: - styles = StylePreferenceTable.load_styles() + styles = MaintainerPreferenceTable.load_preferences() fields = Deb822( { "Package": "foo", @@ -218,12 +233,14 @@ def test_was_from_salsa_ci_style_args( ) } ) - effective_style, _ = determine_effective_style(styles, src, salsa_ci) + effective_style, tool, _ = determine_effective_preference(styles, src, salsa_ci) if style_delta is None: assert effective_style is None + assert tool is None else: - was_style = EffectivePreference(**_WAS_DEFAULTS).replace( + was_style = EffectiveFormattingPreference(**_WAS_DEFAULTS).replace( **style_delta, ) assert effective_style == was_style + assert tool == f"wrap-and-sort {was_args}".strip() diff --git a/tests/test_substitute.py b/tests/test_substitute.py index a83cc7f..81eb2e0 100644 --- a/tests/test_substitute.py +++ b/tests/test_substitute.py @@ -1,6 +1,7 @@ import pytest from debputy.architecture_support import faked_arch_table +from debputy.commands.debputy_cmd.output import no_fancy_output from debputy.dh_migration.models import ( DHMigrationSubstitution, AcceptableMigrationIssues, @@ -55,7 +56,7 @@ def test_substitution_match(debputy_plugin_feature_set, value, expected) -> None def test_migrate_substitution() -> None: - feature_migration = FeatureMigration("test migration") + feature_migration = FeatureMigration("test migration", no_fancy_output()) subst = DHMigrationSubstitution( MOCK_DPKG_ARCH_TABLE, AcceptableMigrationIssues(frozenset()), diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..e1dc87e --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,20 @@ +from typing import Sequence, Union + +import pytest + +from debputy.util import escape_shell + + +@pytest.mark.parametrize( + "arg,expected", + [ + ("foo bar", '"foo bar"'), + ("a'b", r"""a\'b"""), + ("foo=bar and baz", 'foo="bar and baz"'), + ("--foo=bar and baz", '--foo="bar and baz"'), + ("--foo with spaces=bar and baz", '"--foo with spaces=bar and baz"'), + ], +) +def test_symlink_normalization(arg: Union[str, Sequence[str]], expected: str) -> None: + actual = escape_shell(arg) if isinstance(arg, str) else escape_shell(*arg) + assert actual == expected |