summaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/conftest.py167
-rw-r--r--tests/data/custom-plugin.json.in16
-rw-r--r--tests/data/custom_plugin.py25
-rw-r--r--tests/plugin_tests/conftest.py20
-rw-r--r--tests/plugin_tests/gnome_test.py45
-rw-r--r--tests/plugin_tests/numpy3_test.data4
-rw-r--r--tests/plugin_tests/numpy3_test.py38
-rw-r--r--tests/plugin_tests/perl-openssl_test.py33
-rwxr-xr-xtests/plugin_tests/perl-ssl_test.sh5
-rw-r--r--tests/test_apply_compression.py33
-rw-r--r--tests/test_architecture.py59
-rw-r--r--tests/test_cross_check_precheck.py124
-rw-r--r--tests/test_deb_packaging_support.py218
-rw-r--r--tests/test_debputy_plugin.py1246
-rw-r--r--tests/test_declarative_parser.py211
-rw-r--r--tests/test_fs_metadata.py770
-rw-r--r--tests/test_install_rules.py1059
-rw-r--r--tests/test_interpreter.py162
-rw-r--r--tests/test_migrations.py1767
-rw-r--r--tests/test_output_filename.py27
-rw-r--r--tests/test_packager_provided_files.py210
-rw-r--r--tests/test_packer_pack.py86
-rw-r--r--tests/test_parser.py473
-rw-r--r--tests/test_path.py47
-rw-r--r--tests/test_plugin_tester.py285
-rw-r--r--tests/test_substitute.py66
-rw-r--r--tests/test_symbolic_mode.py25
-rw-r--r--tests/test_symlink_normalization.py35
-rw-r--r--tests/tutil.py66
29 files changed, 7322 insertions, 0 deletions
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..1f2ed3d
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,167 @@
+import os
+from typing import Mapping
+
+import pytest
+from debian.deb822 import Deb822
+from debian.debian_support import DpkgArchTable
+
+from debputy._deb_options_profiles import DebBuildOptionsAndProfiles
+from debputy.architecture_support import (
+ DpkgArchitectureBuildProcessValuesTable,
+ faked_arch_table,
+)
+from debputy.filesystem_scan import FSROOverlay
+from debputy.manifest_parser.util import AttributePath
+from debputy.packages import BinaryPackage, SourcePackage
+from debputy.plugin.debputy.debputy_plugin import initialize_debputy_features
+from debputy.plugin.api.impl_types import (
+ DebputyPluginMetadata,
+)
+from debputy.plugin.api.feature_set import PluginProvidedFeatureSet
+from debputy.plugin.api.impl import DebputyPluginInitializerProvider
+from debputy.substitution import (
+ NULL_SUBSTITUTION,
+ Substitution,
+ SubstitutionImpl,
+ VariableContext,
+)
+
+# Disable dpkg's translation layer. It is very slow and disabling it makes it easier to debug
+# test-failure reports from systems with translations active.
+os.environ["DPKG_NLS"] = "0"
+
+
+@pytest.fixture(scope="session")
+def amd64_dpkg_architecture_variables() -> DpkgArchitectureBuildProcessValuesTable:
+ return faked_arch_table("amd64")
+
+
+@pytest.fixture(scope="session")
+def dpkg_arch_query() -> DpkgArchTable:
+ return DpkgArchTable.load_arch_table()
+
+
+@pytest.fixture()
+def source_package() -> SourcePackage:
+ return SourcePackage(
+ {
+ "Source": "foo",
+ }
+ )
+
+
+@pytest.fixture()
+def package_single_foo_arch_all_cxt_amd64(
+ amd64_dpkg_architecture_variables,
+ dpkg_arch_query,
+) -> Mapping[str, BinaryPackage]:
+ return {
+ p.name: p
+ for p in [
+ BinaryPackage(
+ Deb822(
+ {
+ "Package": "foo",
+ "Architecture": "all",
+ }
+ ),
+ amd64_dpkg_architecture_variables,
+ dpkg_arch_query,
+ is_main_package=True,
+ )
+ ]
+ }
+
+
+@pytest.fixture()
+def package_foo_w_udeb_arch_any_cxt_amd64(
+ amd64_dpkg_architecture_variables,
+ dpkg_arch_query,
+) -> Mapping[str, BinaryPackage]:
+ return {
+ p.name: p
+ for p in [
+ BinaryPackage(
+ Deb822(
+ {
+ "Package": "foo",
+ "Architecture": "any",
+ }
+ ),
+ amd64_dpkg_architecture_variables,
+ dpkg_arch_query,
+ is_main_package=True,
+ ),
+ BinaryPackage(
+ Deb822(
+ {
+ "Package": "foo-udeb",
+ "Architecture": "any",
+ "Package-Type": "udeb",
+ }
+ ),
+ amd64_dpkg_architecture_variables,
+ dpkg_arch_query,
+ ),
+ ]
+ }
+
+
+@pytest.fixture(scope="session")
+def null_substitution() -> Substitution:
+ return NULL_SUBSTITUTION
+
+
+@pytest.fixture(scope="session")
+def _empty_debputy_plugin_feature_set() -> PluginProvidedFeatureSet:
+ return PluginProvidedFeatureSet()
+
+
+@pytest.fixture(scope="session")
+def amd64_substitution(
+ amd64_dpkg_architecture_variables,
+ _empty_debputy_plugin_feature_set,
+) -> Substitution:
+ debian_dir = FSROOverlay.create_root_dir("debian", "debian")
+ variable_context = VariableContext(
+ debian_dir,
+ )
+ return SubstitutionImpl(
+ plugin_feature_set=_empty_debputy_plugin_feature_set,
+ dpkg_arch_table=amd64_dpkg_architecture_variables,
+ static_variables=None,
+ environment={},
+ unresolvable_substitutions=frozenset(["SOURCE_DATE_EPOCH", "PACKAGE"]),
+ variable_context=variable_context,
+ )
+
+
+@pytest.fixture(scope="session")
+def no_profiles_or_build_options() -> DebBuildOptionsAndProfiles:
+ return DebBuildOptionsAndProfiles(environ={})
+
+
+@pytest.fixture(scope="session")
+def debputy_plugin_feature_set(
+ _empty_debputy_plugin_feature_set, amd64_substitution
+) -> PluginProvidedFeatureSet:
+ plugin_metadata = DebputyPluginMetadata(
+ plugin_name="debputy",
+ api_compat_version=1,
+ plugin_initializer=initialize_debputy_features,
+ plugin_loader=None,
+ plugin_path="<loaded-via-test>",
+ )
+ feature_set = _empty_debputy_plugin_feature_set
+ api = DebputyPluginInitializerProvider(
+ plugin_metadata,
+ feature_set,
+ amd64_substitution,
+ )
+ api.load_plugin()
+ return feature_set
+
+
+@pytest.fixture
+def attribute_path(request) -> AttributePath:
+ return AttributePath.builtin_path()[request.node.nodeid]
diff --git a/tests/data/custom-plugin.json.in b/tests/data/custom-plugin.json.in
new file mode 100644
index 0000000..67ca847
--- /dev/null
+++ b/tests/data/custom-plugin.json.in
@@ -0,0 +1,16 @@
+{
+ "plugin-initializer": "custom_plugin_initializer",
+ "api-compat-version": 1,
+ "packager-provided-files": [
+ {
+ "stem": "test-file-from-json",
+ "installed-path": "/usr/share/test-files/{name}.test",
+ "default-mode": "0644",
+ "allow-name-segment": true,
+ "reference-documentation": {
+ "description": "test of loading PPFs from a JSON",
+ "format-documentation-uris": ["man:ls(1)"]
+ }
+ }
+ ]
+}
diff --git a/tests/data/custom_plugin.py b/tests/data/custom_plugin.py
new file mode 100644
index 0000000..41d9779
--- /dev/null
+++ b/tests/data/custom_plugin.py
@@ -0,0 +1,25 @@
+from debputy.plugin.api import (
+ DebputyPluginInitializer,
+ VirtualPath,
+ BinaryCtrlAccessor,
+ PackageProcessingContext,
+)
+
+
+def _udeb_metadata_detector(
+ _path: VirtualPath,
+ ctrl: BinaryCtrlAccessor,
+ _context: PackageProcessingContext,
+) -> None:
+ ctrl.substvars["Test:Udeb-Metadata-Detector"] = "was-run"
+
+
+def custom_plugin_initializer(api: DebputyPluginInitializer) -> None:
+ api.packager_provided_file(
+ "my-file",
+ "/no-where/this/is/a/test/plugin/{name}.conf",
+ post_formatting_rewrite=lambda x: x.replace("+", "_"),
+ )
+ api.metadata_or_maintscript_detector(
+ "udeb-only", _udeb_metadata_detector, package_type="udeb"
+ )
diff --git a/tests/plugin_tests/conftest.py b/tests/plugin_tests/conftest.py
new file mode 100644
index 0000000..f2a8aea
--- /dev/null
+++ b/tests/plugin_tests/conftest.py
@@ -0,0 +1,20 @@
+import os
+
+import pytest
+
+
+@pytest.fixture(autouse=True)
+def workaround_debputys_own_test_suite() -> None:
+ # This fixture is only required as long as the tests are run inside `debputy`'s
+ # own test suite. If you copy out a plugin + tests, you should *not* need this
+ # fixture.
+ #
+ # The problem appears because in the debputy source package, these plugins are
+ # always provided in their "installed" location.
+ orig = os.environ.get("DEBPUTY_TEST_PLUGIN_LOCATION")
+ os.environ["DEBPUTY_TEST_PLUGIN_LOCATION"] = "installed"
+ yield
+ if orig is None:
+ del os.environ["DEBPUTY_TEST_PLUGIN_LOCATION"]
+ else:
+ os.environ["DEBPUTY_TEST_PLUGIN_LOCATION"] = orig
diff --git a/tests/plugin_tests/gnome_test.py b/tests/plugin_tests/gnome_test.py
new file mode 100644
index 0000000..ec27a85
--- /dev/null
+++ b/tests/plugin_tests/gnome_test.py
@@ -0,0 +1,45 @@
+import pytest
+
+from debputy.plugin.api.test_api import (
+ initialize_plugin_under_test,
+ build_virtual_file_system,
+ package_metadata_context,
+)
+
+
+@pytest.mark.parametrize(
+ "version,expected_version,expected_next_version",
+ [
+ (
+ "1:3.36.1",
+ "1:3.36",
+ "1:3.38",
+ ),
+ (
+ "3.38.2",
+ "3.38",
+ "40",
+ ),
+ (
+ "40.2.0",
+ "40~",
+ "41~",
+ ),
+ (
+ "40",
+ "40~",
+ "41~",
+ ),
+ ],
+)
+def test_gnome_plugin(
+ version: str,
+ expected_version: str,
+ expected_next_version: str,
+) -> None:
+ plugin = initialize_plugin_under_test()
+ fs = build_virtual_file_system([])
+ context = package_metadata_context(binary_package_version=version)
+ metadata = plugin.run_metadata_detector("gnome-versions", fs, context)
+ assert metadata.substvars["gnome:Version"] == expected_version
+ assert metadata.substvars["gnome:NextVersion"] == expected_next_version
diff --git a/tests/plugin_tests/numpy3_test.data b/tests/plugin_tests/numpy3_test.data
new file mode 100644
index 0000000..22b65a2
--- /dev/null
+++ b/tests/plugin_tests/numpy3_test.data
@@ -0,0 +1,4 @@
+# Values taken from 1:1.24.2-1
+abi 9
+api 16
+api-min-version 1:1.22.0
diff --git a/tests/plugin_tests/numpy3_test.py b/tests/plugin_tests/numpy3_test.py
new file mode 100644
index 0000000..9b252fb
--- /dev/null
+++ b/tests/plugin_tests/numpy3_test.py
@@ -0,0 +1,38 @@
+import os
+
+import pytest
+
+from debputy.plugin.api.test_api import (
+ initialize_plugin_under_test,
+ build_virtual_file_system,
+ package_metadata_context,
+)
+
+DATA_FILE = os.path.join(os.path.dirname(__file__), "numpy3_test.data")
+
+
+@pytest.fixture(scope="session")
+def numpy3_stub_data_file() -> None:
+ os.environ["_NUMPY_TEST_PATH"] = DATA_FILE
+ yield
+ try:
+ del os.environ["_NUMPY_TEST_PATH"]
+ except KeyError:
+ pass
+
+
+def test_numpy3_plugin_arch_all(numpy3_stub_data_file) -> None:
+ plugin = initialize_plugin_under_test()
+ fs = build_virtual_file_system([])
+ context = package_metadata_context(package_fields={"Architecture": "all"})
+ metadata = plugin.run_metadata_detector("numpy-depends", fs, context)
+ assert metadata.substvars["python3:Depends"] == "python3-numpy"
+
+
+def test_numpy3_plugin_arch_any(numpy3_stub_data_file) -> None:
+ plugin = initialize_plugin_under_test()
+ fs = build_virtual_file_system([])
+ context = package_metadata_context(package_fields={"Architecture": "any"})
+ metadata = plugin.run_metadata_detector("numpy-depends", fs, context)
+ expected = "python3-numpy (>= 1:1.22.0), python3-numpy-abi9"
+ assert metadata.substvars["python3:Depends"] == expected
diff --git a/tests/plugin_tests/perl-openssl_test.py b/tests/plugin_tests/perl-openssl_test.py
new file mode 100644
index 0000000..37f2ba1
--- /dev/null
+++ b/tests/plugin_tests/perl-openssl_test.py
@@ -0,0 +1,33 @@
+import stat
+import os
+
+import pytest
+
+from debputy.plugin.api.test_api import (
+ initialize_plugin_under_test,
+ build_virtual_file_system,
+ package_metadata_context,
+)
+
+STUB_CMD = os.path.join(os.path.dirname(__file__), "perl-ssl_test.sh")
+
+
+@pytest.fixture(scope="session")
+def perl_ssl_stub_cmd() -> None:
+ os.environ["_PERL_SSL_DEFAULTS_TEST_PATH"] = STUB_CMD
+ mode = stat.S_IMODE(os.stat(STUB_CMD).st_mode)
+ if (mode & 0o500) != 0o500:
+ os.chmod(STUB_CMD, mode | 0o500)
+ yield
+ try:
+ del os.environ["_PERL_SSL_DEFAULTS_TEST_PATH"]
+ except KeyError:
+ pass
+
+
+def test_perl_openssl(perl_ssl_stub_cmd) -> None:
+ plugin = initialize_plugin_under_test()
+ fs = build_virtual_file_system([])
+ context = package_metadata_context(package_fields={"Architecture": "all"})
+ metadata = plugin.run_metadata_detector("perl-openssl-abi", fs, context)
+ assert metadata.substvars["perl:Depends"] == "perl-openssl-abi-3"
diff --git a/tests/plugin_tests/perl-ssl_test.sh b/tests/plugin_tests/perl-ssl_test.sh
new file mode 100755
index 0000000..5238dc7
--- /dev/null
+++ b/tests/plugin_tests/perl-ssl_test.sh
@@ -0,0 +1,5 @@
+#!/bin/sh
+
+# The original script looks up libssl.so and extract part of the SONAME,
+# which is all adequately stubbed by:
+echo 3
diff --git a/tests/test_apply_compression.py b/tests/test_apply_compression.py
new file mode 100644
index 0000000..70817f9
--- /dev/null
+++ b/tests/test_apply_compression.py
@@ -0,0 +1,33 @@
+from debputy.filesystem_scan import build_virtual_fs
+from debputy.plugin.api import virtual_path_def
+from debputy.plugin.debputy.package_processors import apply_compression
+
+
+def test_apply_compression():
+ # TODO: This test should be a proper plugin test
+ fs_root = build_virtual_fs(
+ [
+ virtual_path_def(
+ "./usr/share/man/man1/foo.1",
+ materialized_content="manpage content",
+ ),
+ virtual_path_def("./usr/share/man/man1/bar.1", link_target="foo.1"),
+ virtual_path_def(
+ "./usr/share/man/de/man1/bar.1", link_target="../../man1/foo.1"
+ ),
+ ],
+ read_write_fs=True,
+ )
+ apply_compression(fs_root, None, None)
+
+ assert fs_root.lookup("./usr/share/man/man1/foo.1") is None
+ assert fs_root.lookup("./usr/share/man/man1/foo.1.gz") is not None
+ assert fs_root.lookup("./usr/share/man/man1/bar.1") is None
+ bar_symlink = fs_root.lookup("./usr/share/man/man1/bar.1.gz")
+ assert bar_symlink is not None
+ assert bar_symlink.readlink() == "foo.1.gz"
+
+ assert fs_root.lookup("./usr/share/man/de/man1/bar.1") is None
+ de_bar_symlink = fs_root.lookup("./usr/share/man/de/man1/bar.1.gz")
+ assert de_bar_symlink is not None
+ assert de_bar_symlink.readlink() == "../../man1/foo.1.gz"
diff --git a/tests/test_architecture.py b/tests/test_architecture.py
new file mode 100644
index 0000000..b0431bc
--- /dev/null
+++ b/tests/test_architecture.py
@@ -0,0 +1,59 @@
+from debputy.architecture_support import faked_arch_table
+
+
+# Ensure our mocks seem to be working reasonably
+def test_mock_arch_table():
+ amd64_native_table = faked_arch_table("amd64")
+ amd64_cross_table = faked_arch_table("amd64", build_arch="i386")
+ amd64_cross_target_table = faked_arch_table("amd64", target_arch="arm64")
+ all_differ_table = faked_arch_table("amd64", build_arch="i386", target_arch="arm64")
+
+ for var_stem in ["ARCH", "MULTIARCH"]:
+ host_var = f"DEB_HOST_{var_stem}"
+ build_var = f"DEB_BUILD_{var_stem}"
+ target_var = f"DEB_TARGET_{var_stem}"
+
+ assert (
+ amd64_cross_table.current_host_arch == amd64_native_table.current_host_arch
+ )
+ assert amd64_native_table[host_var] == amd64_native_table[build_var]
+ assert amd64_native_table[host_var] == amd64_native_table[target_var]
+
+ # HOST_ARCH differ in a cross build, but the rest remain the same
+ assert amd64_cross_table[host_var] == amd64_native_table[host_var]
+ assert amd64_cross_table[target_var] == amd64_native_table[target_var]
+ assert amd64_cross_table[build_var] != amd64_native_table[build_var]
+ assert amd64_cross_table[target_var] == amd64_native_table[target_var]
+ assert (
+ amd64_cross_table.current_host_multiarch
+ == amd64_native_table.current_host_multiarch
+ )
+
+ # TARGET_ARCH differ in a cross-compiler build, but the rest remain the same
+ assert amd64_cross_target_table[host_var] == amd64_native_table[host_var]
+ assert amd64_cross_target_table[target_var] != amd64_native_table[target_var]
+ assert amd64_cross_target_table[build_var] == amd64_native_table[build_var]
+ assert (
+ amd64_cross_target_table.current_host_multiarch
+ == amd64_native_table.current_host_multiarch
+ )
+
+ # TARGET_ARCH differ in a cross-compiler build, but the rest remain the same
+ assert all_differ_table[host_var] == amd64_native_table[host_var]
+ assert all_differ_table[target_var] != amd64_native_table[target_var]
+ assert all_differ_table[build_var] != amd64_native_table[build_var]
+ assert all_differ_table[build_var] == amd64_cross_table[build_var]
+ assert all_differ_table[target_var] == amd64_cross_target_table[target_var]
+ assert (
+ all_differ_table.current_host_arch == amd64_native_table.current_host_arch
+ )
+ assert (
+ all_differ_table.current_host_multiarch
+ == amd64_native_table.current_host_multiarch
+ )
+
+ # Finally, check is_cross_compiling
+ assert not amd64_native_table.is_cross_compiling
+ assert amd64_cross_table.is_cross_compiling
+ assert not amd64_cross_target_table.is_cross_compiling
+ assert all_differ_table.is_cross_compiling
diff --git a/tests/test_cross_check_precheck.py b/tests/test_cross_check_precheck.py
new file mode 100644
index 0000000..41bf01c
--- /dev/null
+++ b/tests/test_cross_check_precheck.py
@@ -0,0 +1,124 @@
+from typing import Optional
+
+import pytest
+
+from debputy.plugin.api.test_api import package_metadata_context
+from debputy.util import package_cross_check_precheck
+
+
+@pytest.mark.parametrize(
+ "a_arch,b_arch,a_bp,b_bp,act_on_a,act_on_b,ex_res_a2b,ex_res_b2a",
+ [
+ # Both way OK
+ ("any", "any", None, None, True, True, True, True),
+ ("all", "all", None, None, True, True, True, True),
+ ("any", "any", "<!noudeb>", "<!noudeb>", True, True, True, True),
+ # OK as well. Same BPs just reordered
+ (
+ "any",
+ "any",
+ "<!noudeb !noinsttests>",
+ "<!noinsttests !noudeb>",
+ True,
+ True,
+ True,
+ True,
+ ),
+ (
+ "any",
+ "any",
+ "<!noudeb> <!noinsttests>",
+ "<!noinsttests> <!noudeb>",
+ True,
+ True,
+ True,
+ True,
+ ),
+ # One way OK
+ ("any", "any", None, "<!noudeb>", True, True, True, False),
+ ("any", "any", None, "<pkg.foo.positive-build>", True, True, True, False),
+ # One way OK - BP is clearly a subset of the other
+ (
+ "any",
+ "any",
+ "<!noudeb>",
+ "<!noudeb> <!noinsttests>",
+ True,
+ True,
+ True,
+ False,
+ ),
+ (
+ "any",
+ "any",
+ "<pos>",
+ "<pos> <pkg.foo.positive-build>",
+ True,
+ True,
+ True,
+ False,
+ ),
+ # Currently fails but should probably allow one way
+ (
+ "any",
+ "any",
+ "<!nopython>",
+ "<!noudeb> <!notestests>",
+ True,
+ True,
+ False,
+ False,
+ ),
+ (
+ "any",
+ "any",
+ "<!nopython>",
+ "<!noudeb> <pkg.foo.positive-build>",
+ True,
+ True,
+ False,
+ False,
+ ),
+ # Negative tests
+ ("any", "all", None, None, True, True, False, False),
+ ("all", "all", None, None, True, False, False, False),
+ ("any", "any", None, None, False, True, False, False),
+ ("i386", "amd64", None, None, True, True, False, False),
+ ],
+)
+def test_generate_deb_filename(
+ a_arch: str,
+ b_arch: str,
+ a_bp: Optional[str],
+ b_bp: Optional[str],
+ act_on_a: bool,
+ act_on_b: bool,
+ ex_res_a2b: bool,
+ ex_res_b2a: bool,
+):
+ pkg_a_fields = {
+ "Package": "pkg-a",
+ "Architecture": a_arch,
+ }
+ if a_bp is not None:
+ pkg_a_fields["Build-Profiles"] = a_bp
+
+ pkg_b_fields = {
+ "Package": "pkg-b",
+ "Architecture": b_arch,
+ }
+ if b_bp is not None:
+ pkg_b_fields["Build-Profiles"] = b_bp
+
+ pkg_a = package_metadata_context(
+ package_fields=pkg_a_fields,
+ should_be_acted_on=act_on_a,
+ ).binary_package
+ pkg_b = package_metadata_context(
+ package_fields=pkg_b_fields,
+ should_be_acted_on=act_on_b,
+ ).binary_package
+
+ assert package_cross_check_precheck(pkg_a, pkg_b) == (ex_res_a2b, ex_res_b2a)
+ # Inverted should functionally give the same answer
+ assert package_cross_check_precheck(pkg_b, pkg_a) == (ex_res_b2a, ex_res_a2b)
diff --git a/tests/test_deb_packaging_support.py b/tests/test_deb_packaging_support.py
new file mode 100644
index 0000000..d47526d
--- /dev/null
+++ b/tests/test_deb_packaging_support.py
@@ -0,0 +1,218 @@
+import pytest
+
+from debputy.deb_packaging_support import install_upstream_changelog
+from debputy.filesystem_scan import build_virtual_fs
+from debputy.plugin.api import virtual_path_def
+
+
+@pytest.mark.parametrize(
+ "upstream_changelog_name,other_files",
+ [
+ (
+ "changelog.txt",
+ [
+ "changelog.md",
+ "CHANGELOG.rst",
+ "random-file",
+ ],
+ ),
+ (
+ "CHANGELOG.rst",
+ [
+ "doc/CHANGELOG.txt",
+ "docs/CHANGELOG.md",
+ ],
+ ),
+ (
+ "docs/CHANGELOG.rst",
+ [
+ "docs/history.md",
+ ],
+ ),
+ (
+ "changelog",
+ [],
+ ),
+ ],
+)
+def test_upstream_changelog_from_source(
+ package_single_foo_arch_all_cxt_amd64,
+ upstream_changelog_name,
+ other_files,
+) -> None:
+ upstream_changelog_content = "Some upstream changelog"
+ dctrl = package_single_foo_arch_all_cxt_amd64["foo"]
+ data_fs_root = build_virtual_fs([], read_write_fs=True)
+ upstream_fs_contents = [
+ virtual_path_def("CHANGELOG", materialized_content="Some upstream changelog")
+ ]
+ upstream_fs_contents.extend(
+ virtual_path_def(x, materialized_content="Wrong file!") for x in other_files
+ )
+ source_fs_root = build_virtual_fs(upstream_fs_contents)
+
+ install_upstream_changelog(dctrl, data_fs_root, source_fs_root)
+
+ upstream_changelog = data_fs_root.lookup(f"usr/share/doc/{dctrl.name}/changelog")
+ assert upstream_changelog is not None
+ assert upstream_changelog.is_file
+ with upstream_changelog.open() as fd:
+ content = fd.read()
+ assert upstream_changelog_content == content
+
+
+@pytest.mark.parametrize(
+ "upstream_changelog_basename,other_data_files,other_source_files",
+ [
+ (
+ "CHANGELOG",
+ [
+ "history.txt",
+ "changes.md",
+ ],
+ [
+ "changelog",
+ "doc/CHANGELOG.txt",
+ "docs/CHANGELOG.md",
+ ],
+ ),
+ (
+ "changelog",
+ [
+ "history.txt",
+ "changes.md",
+ ],
+ [
+ "changelog",
+ "doc/CHANGELOG.txt",
+ "docs/CHANGELOG.md",
+ ],
+ ),
+ (
+ "changes.md",
+ [
+ "changelog.rst",
+ ],
+ ["changelog"],
+ ),
+ ],
+)
+def test_upstream_changelog_from_data_fs(
+ package_single_foo_arch_all_cxt_amd64,
+ upstream_changelog_basename,
+ other_data_files,
+ other_source_files,
+) -> None:
+ upstream_changelog_content = "Some upstream changelog"
+ dctrl = package_single_foo_arch_all_cxt_amd64["foo"]
+ doc_dir = f"./usr/share/doc/{dctrl.name}"
+ data_fs_contents = [
+ virtual_path_def(
+ f"{doc_dir}/{upstream_changelog_basename}",
+ materialized_content="Some upstream changelog",
+ )
+ ]
+ data_fs_contents.extend(
+ virtual_path_def(
+ f"{doc_dir}/{x}",
+ materialized_content="Wrong file!",
+ )
+ for x in other_data_files
+ )
+ data_fs_root = build_virtual_fs(data_fs_contents, read_write_fs=True)
+ source_fs_root = build_virtual_fs(
+ [
+ virtual_path_def(
+ x,
+ materialized_content="Wrong file!",
+ )
+ for x in other_source_files
+ ]
+ )
+
+ install_upstream_changelog(dctrl, data_fs_root, source_fs_root)
+
+ upstream_changelog = data_fs_root.lookup(f"usr/share/doc/{dctrl.name}/changelog")
+ assert upstream_changelog is not None
+ assert upstream_changelog.is_file
+ with upstream_changelog.open() as fd:
+ content = fd.read()
+ assert upstream_changelog_content == content
+
+
+def test_upstream_changelog_pre_installed_compressed(
+ package_single_foo_arch_all_cxt_amd64,
+) -> None:
+ dctrl = package_single_foo_arch_all_cxt_amd64["foo"]
+ changelog = f"./usr/share/doc/{dctrl.name}/changelog.gz"
+ data_fs_root = build_virtual_fs(
+ [virtual_path_def(changelog, fs_path="/nowhere/should/not/be/resolved")],
+ read_write_fs=True,
+ )
+ source_fs_root = build_virtual_fs(
+ [virtual_path_def("changelog", materialized_content="Wrong file!")]
+ )
+
+ install_upstream_changelog(dctrl, data_fs_root, source_fs_root)
+
+ upstream_ch_compressed = data_fs_root.lookup(
+ f"usr/share/doc/{dctrl.name}/changelog.gz"
+ )
+ assert upstream_ch_compressed is not None
+ assert upstream_ch_compressed.is_file
+ upstream_ch_uncompressed = data_fs_root.lookup(
+ f"usr/share/doc/{dctrl.name}/changelog"
+ )
+ assert upstream_ch_uncompressed is None
+
+
+def test_upstream_changelog_no_matches(
+ package_single_foo_arch_all_cxt_amd64,
+) -> None:
+ dctrl = package_single_foo_arch_all_cxt_amd64["foo"]
+ doc_dir = f"./usr/share/doc/{dctrl.name}"
+ data_fs_root = build_virtual_fs(
+ [
+ virtual_path_def(
+ f"{doc_dir}/random-file", materialized_content="Wrong file!"
+ ),
+ virtual_path_def(
+ f"{doc_dir}/changelog.Debian", materialized_content="Wrong file!"
+ ),
+ ],
+ read_write_fs=True,
+ )
+ source_fs_root = build_virtual_fs(
+ [virtual_path_def("some-random-file", materialized_content="Wrong file!")]
+ )
+
+ install_upstream_changelog(dctrl, data_fs_root, source_fs_root)
+
+ upstream_ch_compressed = data_fs_root.lookup(
+ f"usr/share/doc/{dctrl.name}/changelog.gz"
+ )
+ assert upstream_ch_compressed is None
+ upstream_ch_uncompressed = data_fs_root.lookup(
+ f"usr/share/doc/{dctrl.name}/changelog"
+ )
+ assert upstream_ch_uncompressed is None
+
+
+def test_upstream_changelog_salsa_issue_49(
+ package_single_foo_arch_all_cxt_amd64,
+) -> None:
+ # https://salsa.debian.org/debian/debputy/-/issues/49
+ dctrl = package_single_foo_arch_all_cxt_amd64["foo"]
+ doc_dir = f"./usr/share/doc/{dctrl.name}"
+ data_fs_root = build_virtual_fs(
+ [virtual_path_def(f"{doc_dir}", link_target="foo-data")], read_write_fs=True
+ )
+ source_fs_root = build_virtual_fs(
+ [virtual_path_def("changelog", materialized_content="Wrong file!")]
+ )
+
+ install_upstream_changelog(dctrl, data_fs_root, source_fs_root)
+
+ doc_dir = data_fs_root.lookup(f"usr/share/doc/{dctrl.name}")
+ assert doc_dir is not None
+ assert doc_dir.is_symlink
diff --git a/tests/test_debputy_plugin.py b/tests/test_debputy_plugin.py
new file mode 100644
index 0000000..a5d7758
--- /dev/null
+++ b/tests/test_debputy_plugin.py
@@ -0,0 +1,1246 @@
+import os
+import textwrap
+from typing import Sequence
+
+import pytest
+
+from debputy.exceptions import (
+ DebputyManifestVariableRequiresDebianDirError,
+ DebputySubstitutionError,
+)
+from debputy.manifest_parser.base_types import SymbolicMode
+from debputy.manifest_parser.util import AttributePath
+from debputy.plugin.api import virtual_path_def
+from debputy.plugin.api.spec import DSD
+from debputy.plugin.api.test_api import (
+ build_virtual_file_system,
+ package_metadata_context,
+)
+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.debputy_plugin import initialize_debputy_features
+from debputy.plugin.debputy.private_api import load_libcap
+from debputy.plugin.debputy.service_management import SystemdServiceContext
+from debputy.plugin.debputy.types import DebputyCapability
+
+
+def test_debputy_packager_provided_files():
+ plugin = initialize_plugin_under_test_preloaded(
+ 1,
+ initialize_debputy_features,
+ plugin_name="debputy",
+ load_debputy_plugin=False,
+ )
+ ppf_by_stem = plugin.packager_provided_files_by_stem()
+ # Verify that all the files are loaded
+ assert set(ppf_by_stem.keys()) == {
+ "tmpfiles",
+ "sysusers",
+ "bash-completion",
+ "pam",
+ "ppp.ip-up",
+ "ppp.ip-down",
+ "logrotate",
+ "logcheck.cracking",
+ "logcheck.violations",
+ "logcheck.violations.ignore",
+ "logcheck.ignore.workstation",
+ "logcheck.ignore.server",
+ "logcheck.ignore.paranoid",
+ "mime",
+ "sharedmimeinfo",
+ "if-pre-up",
+ "if-up",
+ "if-down",
+ "if-post-down",
+ "cron.hourly",
+ "cron.daily",
+ "cron.weekly",
+ "cron.monthly",
+ "cron.yearly",
+ "cron.d",
+ "initramfs-hook",
+ "modprobe",
+ "gsettings-override",
+ "lintian-overrides",
+ "bug-script",
+ "bug-control",
+ "bug-presubj",
+ "changelog",
+ "NEWS",
+ "copyright",
+ "README.Debian",
+ "TODO",
+ "doc-base",
+ "shlibs",
+ "symbols",
+ "alternatives",
+ "init",
+ "default",
+ "templates",
+ # dh_installsytemd
+ "mount",
+ "path",
+ "service",
+ "socket",
+ "target",
+ "timer",
+ "@path",
+ "@service",
+ "@socket",
+ "@target",
+ "@timer",
+ }
+ # Verify the post_rewrite_hook
+ assert (
+ ppf_by_stem["logcheck.ignore.paranoid"].compute_dest("foo.bar")[1] == "foo_bar"
+ )
+ # Verify custom formats work
+ assert ppf_by_stem["tmpfiles"].compute_dest("foo.bar")[1] == "foo.bar.conf"
+ assert ppf_by_stem["sharedmimeinfo"].compute_dest("foo.bar")[1] == "foo.bar.xml"
+ assert ppf_by_stem["modprobe"].compute_dest("foo.bar")[1] == "foo.bar.conf"
+ assert (
+ ppf_by_stem["gsettings-override"].compute_dest("foo.bar", assigned_priority=20)[
+ 1
+ ]
+ == "20_foo.bar.gschema.override"
+ )
+
+
+def test_debputy_docbase_naming() -> None:
+ plugin = initialize_plugin_under_test_preloaded(
+ 1,
+ initialize_debputy_features,
+ plugin_name="debputy",
+ load_debputy_plugin=False,
+ )
+ doc_base_pff = plugin.packager_provided_files_by_stem()["doc-base"]
+ fs_root = build_virtual_file_system(
+ [virtual_path_def("foo.doc-base", content="Document: bar")]
+ )
+ _, basename = doc_base_pff.compute_dest("foo", path=fs_root["foo.doc-base"])
+ assert basename == "foo.bar"
+
+
+def test_debputy_adr_examples() -> None:
+ plugin = initialize_plugin_under_test_preloaded(
+ 1,
+ initialize_debputy_features,
+ plugin_name="debputy",
+ load_debputy_plugin=False,
+ )
+ issues = plugin.automatic_discard_rules_examples_with_issues()
+ assert not issues
+
+
+def test_debputy_metadata_detector_gsettings_dependencies():
+ plugin = initialize_plugin_under_test_preloaded(
+ 1,
+ initialize_debputy_features,
+ plugin_name="debputy",
+ load_debputy_plugin=False,
+ )
+ # By default, the plugin will not add a substvars
+ fs_root = build_virtual_file_system(["./bin/ls"])
+ metadata = plugin.run_metadata_detector("gsettings-dependencies", fs_root)
+ assert "misc:Depends" not in metadata.substvars
+
+ # It will not react if there is only directories or non-files
+ fs_root = build_virtual_file_system(["./usr/share/glib-2.0/schemas/some-dir/"])
+ metadata = plugin.run_metadata_detector("gsettings-dependencies", fs_root)
+ assert "misc:Depends" not in metadata.substvars
+
+ # However, it will if there is a file beneath the schemas dir
+ fs_root = build_virtual_file_system(["./usr/share/glib-2.0/schemas/foo.xml"])
+ metadata = plugin.run_metadata_detector("gsettings-dependencies", fs_root)
+ assert (
+ metadata.substvars["misc:Depends"]
+ == "dconf-gsettings-backend | gsettings-backend"
+ )
+
+
+def test_debputy_metadata_detector_initramfs_hooks():
+ plugin = initialize_plugin_under_test_preloaded(
+ 1,
+ initialize_debputy_features,
+ plugin_name="debputy",
+ load_debputy_plugin=False,
+ )
+ metadata_detector_id = "initramfs-hooks"
+
+ # By default, the plugin will not add a trigger
+ fs_root = build_virtual_file_system(["./bin/ls"])
+ metadata = plugin.run_metadata_detector(metadata_detector_id, fs_root)
+ assert metadata.triggers == []
+
+ # It will not react if the directory is empty
+ fs_root = build_virtual_file_system(
+ [
+ # Use an absolute path to verify that also work (it should and third-party plugin are likely
+ # use absolute paths)
+ "/usr/share/initramfs-tools/hooks/"
+ ]
+ )
+ metadata = plugin.run_metadata_detector(metadata_detector_id, fs_root)
+ assert metadata.triggers == []
+
+ # However, it will if there is a file beneath the schemas dir
+ fs_root = build_virtual_file_system(["./usr/share/initramfs-tools/hooks/some-hook"])
+ metadata = plugin.run_metadata_detector(metadata_detector_id, fs_root)
+ result = [t.serialized_format() for t in metadata.triggers]
+ assert result == ["activate-noawait update-initramfs"]
+
+
+def test_debputy_metadata_detector_systemd_tmpfiles():
+ plugin = initialize_plugin_under_test_preloaded(
+ 1,
+ initialize_debputy_features,
+ plugin_name="debputy",
+ load_debputy_plugin=False,
+ )
+ metadata_detector_id = "systemd-tmpfiles"
+
+ # By default, the plugin will not add anything
+ fs_root = build_virtual_file_system(["./bin/ls"])
+ metadata = plugin.run_metadata_detector(metadata_detector_id, fs_root)
+ assert metadata.maintscripts() == []
+
+ # It only reacts to ".conf" files
+ fs_root = build_virtual_file_system(["./usr/lib/tmpfiles.d/foo"])
+ metadata = plugin.run_metadata_detector(metadata_detector_id, fs_root)
+ assert metadata.maintscripts() == []
+
+ fs_root = build_virtual_file_system(
+ [
+ "./usr/lib/tmpfiles.d/foo.conf",
+ "./etc/tmpfiles.d/foo.conf",
+ ]
+ )
+ metadata = plugin.run_metadata_detector(metadata_detector_id, fs_root)
+ snippets = metadata.maintscripts()
+ assert len(snippets) == 1
+ snippet = snippets[0]
+ assert snippet.maintscript == "postinst"
+ assert snippet.registration_method == "on_configure"
+ # The snippet should use "systemd-tmpfiles [...] --create foo.conf ..."
+ assert "--create foo.conf" in snippet.plugin_provided_script
+ # The "foo.conf" should only be listed once
+ assert snippet.plugin_provided_script.count("foo.conf") == 1
+
+
+def test_debputy_metadata_detector_systemd_sysusers():
+ plugin = initialize_plugin_under_test_preloaded(
+ 1,
+ initialize_debputy_features,
+ plugin_name="debputy",
+ load_debputy_plugin=False,
+ )
+ metadata_detector_id = "systemd-sysusers"
+
+ # By default, the plugin will not add anything
+ fs_root = build_virtual_file_system(["./bin/ls"])
+ metadata = plugin.run_metadata_detector(metadata_detector_id, fs_root)
+ assert metadata.maintscripts() == []
+
+ # It only reacts to ".conf" files
+ fs_root = build_virtual_file_system(["./usr/lib/sysusers.d/foo"])
+ metadata = plugin.run_metadata_detector(metadata_detector_id, fs_root)
+ assert metadata.maintscripts() == []
+
+ fs_root = build_virtual_file_system(["./usr/lib/sysusers.d/foo.conf"])
+ metadata = plugin.run_metadata_detector(metadata_detector_id, fs_root)
+ snippets = metadata.maintscripts()
+ assert len(snippets) == 1
+ snippet = snippets[0]
+ assert snippet.maintscript == "postinst"
+ assert snippet.registration_method == "on_configure"
+ # The snippet should use "systemd-sysusers [...] foo.conf ..."
+ assert "systemd-sysusers" in snippet.plugin_provided_script
+ assert "foo.conf" in snippet.plugin_provided_script
+ # The "foo.conf" should only be listed once
+ assert snippet.plugin_provided_script.count("foo.conf") == 1
+
+
+def test_debputy_metadata_detector_xfonts():
+ plugin = initialize_plugin_under_test_preloaded(
+ 1,
+ initialize_debputy_features,
+ plugin_name="debputy",
+ load_debputy_plugin=False,
+ )
+ metadata_detector_id = "xfonts"
+
+ # By default, the plugin will not add anything
+ fs_root = build_virtual_file_system(["./bin/ls"])
+ metadata = plugin.run_metadata_detector(metadata_detector_id, fs_root)
+ assert metadata.maintscripts() == []
+ assert "misc:Depends" not in metadata.substvars
+
+ # It ignores files in the X11 dir and directories starting with ".".
+ fs_root = build_virtual_file_system(
+ ["./usr/share/fonts/X11/foo", "./usr/share/fonts/X11/.a/"]
+ )
+ metadata = plugin.run_metadata_detector(metadata_detector_id, fs_root)
+ assert metadata.maintscripts() == []
+ assert "misc:Depends" not in metadata.substvars
+
+ fs_root = build_virtual_file_system(
+ [
+ "./usr/share/fonts/X11/some-font-dir/",
+ ]
+ )
+ metadata = plugin.run_metadata_detector(metadata_detector_id, fs_root)
+ snippets = metadata.maintscripts()
+ assert metadata.substvars["misc:Depends"] == "xfonts-utils"
+ assert len(snippets) == 2
+ assert set(s.maintscript for s in snippets) == {"postinst", "postrm"}
+ postinst_snippet = metadata.maintscripts(maintscript="postinst")[0]
+ postrm_snippet = metadata.maintscripts(maintscript="postrm")[0]
+
+ assert postinst_snippet.maintscript == "postinst"
+ assert postinst_snippet.registration_method == "unconditionally_in_script"
+ assert (
+ "update-fonts-scale some-font-dir"
+ not in postinst_snippet.plugin_provided_script
+ )
+ assert "--x11r7-layout some-font-dir" in postinst_snippet.plugin_provided_script
+ assert (
+ f"update-fonts-alias --include" not in postinst_snippet.plugin_provided_script
+ )
+
+ assert postrm_snippet.maintscript == "postrm"
+ assert postrm_snippet.registration_method == "unconditionally_in_script"
+ assert (
+ "update-fonts-scale some-font-dir" not in postrm_snippet.plugin_provided_script
+ )
+ assert "--x11r7-layout some-font-dir" in postrm_snippet.plugin_provided_script
+ assert f"update-fonts-alias --exclude" not in postrm_snippet.plugin_provided_script
+
+
+def test_debputy_metadata_detector_xfonts_scale_and_alias():
+ plugin = initialize_plugin_under_test_preloaded(
+ 1,
+ initialize_debputy_features,
+ plugin_name="debputy",
+ load_debputy_plugin=False,
+ )
+
+ metadata_detector_id = "xfonts"
+ package_name = "bar"
+ fs_root = build_virtual_file_system(
+ [
+ "./usr/share/fonts/X11/some-font-dir/",
+ f"./etc/X11/xfonts/some-font-dir/{package_name}.scale",
+ f"./etc/X11/xfonts/some-font-dir/{package_name}.alias",
+ ]
+ )
+ metadata = plugin.run_metadata_detector(
+ metadata_detector_id,
+ fs_root,
+ package_metadata_context(
+ package_fields={
+ "Package": package_name,
+ }
+ ),
+ )
+ snippets = metadata.maintscripts()
+ assert metadata.substvars["misc:Depends"] == "xfonts-utils"
+ assert len(snippets) == 2
+ assert set(s.maintscript for s in snippets) == {"postinst", "postrm"}
+ postinst_snippet = metadata.maintscripts(maintscript="postinst")[0]
+ postrm_snippet = metadata.maintscripts(maintscript="postrm")[0]
+
+ assert postinst_snippet.maintscript == "postinst"
+ assert postinst_snippet.registration_method == "unconditionally_in_script"
+ assert "update-fonts-scale some-font-dir" in postinst_snippet.plugin_provided_script
+ assert "--x11r7-layout some-font-dir" in postinst_snippet.plugin_provided_script
+ assert (
+ f"update-fonts-alias --include /etc/X11/xfonts/some-font-dir/{package_name}.alias some-font-dir"
+ in postinst_snippet.plugin_provided_script
+ )
+
+ assert postrm_snippet.maintscript == "postrm"
+ assert postrm_snippet.registration_method == "unconditionally_in_script"
+ assert "update-fonts-scale some-font-dir" in postrm_snippet.plugin_provided_script
+ assert "--x11r7-layout some-font-dir" in postrm_snippet.plugin_provided_script
+ assert (
+ f"update-fonts-alias --exclude /etc/X11/xfonts/some-font-dir/{package_name}.alias some-font-dir"
+ in postrm_snippet.plugin_provided_script
+ )
+
+
+def test_debputy_metadata_detector_icon_cache():
+ plugin = initialize_plugin_under_test_preloaded(
+ 1,
+ initialize_debputy_features,
+ plugin_name="debputy",
+ load_debputy_plugin=False,
+ )
+ metadata_detector_id = "icon-cache"
+ icon_dir = "usr/share/icons"
+
+ # By default, the plugin will not add anything
+ fs_root = build_virtual_file_system(["./bin/ls"])
+ metadata = plugin.run_metadata_detector(metadata_detector_id, fs_root)
+ assert metadata.maintscripts() == []
+
+ fs_root = build_virtual_file_system(
+ [
+ # Ignored subdirs (dh_icons ignores them too)
+ f"./{icon_dir}/gnome/foo.png",
+ f"./{icon_dir}/hicolor/foo.png",
+ # Unknown image format, so it does not trigger the update-icon-caches call
+ f"./{icon_dir}/subdir-a/unknown-image-format.img",
+ ]
+ )
+ metadata = plugin.run_metadata_detector(metadata_detector_id, fs_root)
+ assert metadata.maintscripts() == []
+
+ fs_root = build_virtual_file_system(
+ [
+ f"./{icon_dir}/subdir-a/foo.png",
+ f"./{icon_dir}/subdir-b/subsubdir/bar.svg",
+ ]
+ )
+ metadata = plugin.run_metadata_detector(metadata_detector_id, fs_root)
+ snippets = metadata.maintscripts()
+ assert len(snippets) == 2
+ assert set(s.maintscript for s in snippets) == {"postinst", "postrm"}
+ postinst_snippet = metadata.maintscripts(maintscript="postinst")[0]
+ postrm_snippet = metadata.maintscripts(maintscript="postrm")[0]
+
+ assert postinst_snippet.registration_method == "on_configure"
+ assert postrm_snippet.registration_method == "unconditionally_in_script"
+
+ # Directory order is stable according to the BinaryPackagePath API.
+ assert (
+ f"update-icon-caches /{icon_dir}/subdir-a /{icon_dir}/subdir-b"
+ in postinst_snippet.plugin_provided_script
+ )
+ assert (
+ f"update-icon-caches /{icon_dir}/subdir-a /{icon_dir}/subdir-b"
+ in postrm_snippet.plugin_provided_script
+ )
+
+
+def test_debputy_metadata_detector_kernel_modules():
+ plugin = initialize_plugin_under_test_preloaded(
+ 1,
+ initialize_debputy_features,
+ plugin_name="debputy",
+ load_debputy_plugin=False,
+ )
+ metadata_detector_id = "kernel-modules"
+ module_dir = "lib/modules"
+
+ # By default, the plugin will not add anything
+ fs_root = build_virtual_file_system(["./bin/ls"])
+ metadata = plugin.run_metadata_detector(metadata_detector_id, fs_root)
+ assert metadata.maintscripts() == []
+
+ fs_root = build_virtual_file_system(
+ [
+ # Ignore files directly in the path or with wrong extension
+ f"./{module_dir}/README",
+ f"./{module_dir}/3.11/ignored-file.txt",
+ ]
+ )
+ metadata = plugin.run_metadata_detector(metadata_detector_id, fs_root)
+ assert metadata.maintscripts() == []
+
+ fs_root = build_virtual_file_system(
+ [
+ f"./{module_dir}/3.11/foo.ko",
+ f"./usr/{module_dir}/3.12/bar.ko.xz",
+ ]
+ )
+ metadata = plugin.run_metadata_detector(metadata_detector_id, fs_root)
+ snippets = metadata.maintscripts()
+ assert len(snippets) == 4 # Two for each version
+ assert set(s.maintscript for s in snippets) == {"postinst", "postrm"}
+ postinst_snippets = metadata.maintscripts(maintscript="postinst")
+ postrm_snippets = metadata.maintscripts(maintscript="postrm")
+
+ assert len(postinst_snippets) == 2
+ assert len(postrm_snippets) == 2
+ assert {s.registration_method for s in postinst_snippets} == {"on_configure"}
+ assert {s.registration_method for s in postrm_snippets} == {
+ "unconditionally_in_script"
+ }
+
+ assert (
+ "depmod -a -F /boot/System.map-3.11 3.11"
+ in postinst_snippets[0].plugin_provided_script
+ )
+ assert (
+ "depmod -a -F /boot/System.map-3.12 3.12"
+ in postinst_snippets[1].plugin_provided_script
+ )
+
+ assert (
+ "depmod -a -F /boot/System.map-3.11 3.11"
+ in postrm_snippets[0].plugin_provided_script
+ )
+ assert (
+ "depmod -a -F /boot/System.map-3.12 3.12"
+ in postrm_snippets[1].plugin_provided_script
+ )
+
+
+def test_debputy_metadata_detector_dpkg_shlibdeps():
+ plugin = initialize_plugin_under_test_preloaded(
+ 1,
+ initialize_debputy_features,
+ plugin_name="debputy",
+ load_debputy_plugin=False,
+ )
+ metadata_detector_id = "dpkg-shlibdeps"
+ skip_root_dir = "usr/lib/debug/"
+
+ # By default, the plugin will not add anything
+ fs_root = build_virtual_file_system(
+ [
+ "./usr/share/doc/foo/copyright",
+ virtual_path_def("./usr/lib/debputy/test.py", fs_path=__file__),
+ ]
+ )
+ metadata = plugin.run_metadata_detector(metadata_detector_id, fs_root)
+ assert "shlibs:Depends" not in metadata.substvars
+
+ fs_root = build_virtual_file_system(
+ [
+ # Verify that certain directories are skipped as promised
+ virtual_path_def(f"./{skip_root_dir}/bin/ls", fs_path="/bin/ls")
+ ]
+ )
+ metadata = plugin.run_metadata_detector(metadata_detector_id, fs_root)
+ assert "shlibs:Depends" not in metadata.substvars
+
+ # But we detect ELF binaries elsewhere
+ fs_root = build_virtual_file_system(
+ [virtual_path_def(f"./bin/ls", fs_path="/bin/ls")]
+ )
+ metadata = plugin.run_metadata_detector(metadata_detector_id, fs_root)
+ # Do not make assertions about the content of `shlibs:Depends` as
+ # package name and versions change over time.
+ assert "shlibs:Depends" in metadata.substvars
+
+ # Re-run to verify it runs for udebs as well
+ metadata = plugin.run_metadata_detector(
+ metadata_detector_id,
+ fs_root,
+ context=package_metadata_context(
+ package_fields={"Package-Type": "udeb"},
+ ),
+ )
+ assert "shlibs:Depends" in metadata.substvars
+
+
+def test_debputy_metadata_detector_pycompile_files():
+ plugin = initialize_plugin_under_test_preloaded(
+ 1,
+ initialize_debputy_features,
+ plugin_name="debputy",
+ load_debputy_plugin=False,
+ )
+ metadata_detector_id = "pycompile-files"
+ module_dir = "usr/lib/python3/dist-packages"
+
+ # By default, the plugin will not add anything
+ fs_root = build_virtual_file_system(["./bin/ls"])
+ metadata = plugin.run_metadata_detector(metadata_detector_id, fs_root)
+ assert metadata.maintscripts() == []
+
+ fs_root = build_virtual_file_system(
+ [
+ # Ignore files in unknown directories by default
+ "./random-dir/foo.py",
+ # Must be in "dist-packages" to count
+ "./usr/lib/python3/foo.py",
+ ]
+ )
+ metadata = plugin.run_metadata_detector(metadata_detector_id, fs_root)
+ assert metadata.maintscripts() == []
+
+ fs_root = build_virtual_file_system(
+ [
+ f"./{module_dir}/debputy/foo.py",
+ ]
+ )
+ metadata = plugin.run_metadata_detector(metadata_detector_id, fs_root)
+ snippets = metadata.maintscripts()
+ assert len(snippets) == 2
+ assert set(s.maintscript for s in snippets) == {"postinst", "prerm"}
+ postinst_snippets = metadata.maintscripts(maintscript="postinst")
+ prerm_snippets = metadata.maintscripts(maintscript="prerm")
+
+ assert len(postinst_snippets) == 1
+ assert len(prerm_snippets) == 1
+ assert {s.registration_method for s in postinst_snippets} == {"on_configure"}
+ assert {s.registration_method for s in prerm_snippets} == {
+ "unconditionally_in_script"
+ }
+
+ assert "py3compile -p foo" in postinst_snippets[0].plugin_provided_script
+
+ assert "py3clean -p foo" in prerm_snippets[0].plugin_provided_script
+
+
+def test_debputy_metadata_detector_pycompile_files_private_package_dir():
+ plugin = initialize_plugin_under_test_preloaded(
+ 1,
+ initialize_debputy_features,
+ plugin_name="debputy",
+ load_debputy_plugin=False,
+ )
+ metadata_detector_id = "pycompile-files"
+ module_dir = "usr/share/foo"
+
+ fs_root = build_virtual_file_system(
+ [
+ f"./{module_dir}/debputy/foo.py",
+ ]
+ )
+ metadata = plugin.run_metadata_detector(metadata_detector_id, fs_root)
+ snippets = metadata.maintscripts()
+ assert len(snippets) == 2
+ assert set(s.maintscript for s in snippets) == {"postinst", "prerm"}
+ postinst_snippets = metadata.maintscripts(maintscript="postinst")
+ prerm_snippets = metadata.maintscripts(maintscript="prerm")
+
+ assert len(postinst_snippets) == 1
+ assert len(prerm_snippets) == 1
+ assert {s.registration_method for s in postinst_snippets} == {"on_configure"}
+ assert {s.registration_method for s in prerm_snippets} == {
+ "unconditionally_in_script"
+ }
+
+ assert (
+ f"py3compile -p foo /{module_dir}"
+ in postinst_snippets[0].plugin_provided_script
+ )
+
+ assert "py3clean -p foo" in prerm_snippets[0].plugin_provided_script
+
+
+def _extract_service(
+ services: Sequence[DetectedService[DSD]], name: str
+) -> DetectedService[DSD]:
+ v = [s for s in services if name in s.names]
+ assert len(v) == 1
+ return v[0]
+
+
+def test_system_service_detection() -> None:
+ plugin = initialize_plugin_under_test_preloaded(
+ 1,
+ initialize_debputy_features,
+ plugin_name="debputy",
+ load_debputy_plugin=False,
+ )
+ systemd_service_root_dir = "usr/lib/systemd"
+ systemd_service_system_dir = f"{systemd_service_root_dir}/system"
+ systemd_service_user_dir = f"{systemd_service_root_dir}/user"
+
+ services, _ = plugin.run_service_detection_and_integrations(
+ "systemd", build_virtual_file_system([])
+ )
+ assert not services
+
+ services, _ = plugin.run_service_detection_and_integrations(
+ "systemd",
+ build_virtual_file_system(
+ [f"{systemd_service_system_dir}/", f"{systemd_service_user_dir}/"]
+ ),
+ )
+ assert not services
+
+ fs_root = build_virtual_file_system(
+ [
+ virtual_path_def(
+ f"{systemd_service_system_dir}/foo.service",
+ content=textwrap.dedent(
+ """\
+ Alias="myname.service"
+ [Install]
+ """
+ ),
+ ),
+ virtual_path_def(
+ f"{systemd_service_system_dir}/foo@.service",
+ content=textwrap.dedent(
+ """\
+ # dh_installsystemd ignores template services - we do for now as well.
+ Alias="ignored.service"
+ [Install]
+ """
+ ),
+ ),
+ virtual_path_def(
+ f"{systemd_service_system_dir}/alias.service", link_target="foo.service"
+ ),
+ virtual_path_def(f"{systemd_service_system_dir}/bar.timer", content=""),
+ ]
+ )
+ services, metadata = plugin.run_service_detection_and_integrations(
+ "systemd",
+ fs_root,
+ service_context_type_hint=SystemdServiceContext,
+ )
+ assert len(services) == 2
+ assert {s.names[0] for s in services} == {"foo.service", "bar.timer"}
+ foo_service = _extract_service(services, "foo.service")
+ assert set(foo_service.names) == {
+ "foo.service",
+ "foo",
+ "alias",
+ "alias.service",
+ "myname.service",
+ "myname",
+ }
+ assert foo_service.type_of_service == "service"
+ assert foo_service.service_scope == "system"
+ assert foo_service.enable_by_default
+ assert foo_service.start_by_default
+ assert foo_service.default_upgrade_rule == "restart"
+ assert foo_service.service_context.had_install_section
+
+ bar_timer = _extract_service(services, "bar.timer")
+ assert set(bar_timer.names) == {"bar.timer"}
+ assert bar_timer.type_of_service == "timer"
+ assert bar_timer.service_scope == "system"
+ assert not bar_timer.enable_by_default
+ assert bar_timer.start_by_default
+ assert bar_timer.default_upgrade_rule == "restart"
+ assert not bar_timer.service_context.had_install_section
+
+ snippets = metadata.maintscripts()
+ assert len(snippets) == 4
+ postinsts = metadata.maintscripts(maintscript="postinst")
+ assert len(postinsts) == 2
+ enable_postinst, start_postinst = postinsts
+ assert (
+ "deb-systemd-helper debian-installed foo.service"
+ in enable_postinst.plugin_provided_script
+ )
+ assert (
+ "deb-systemd-invoke start foo.service" in start_postinst.plugin_provided_script
+ )
+ assert (
+ "deb-systemd-invoke restart foo.service"
+ in start_postinst.plugin_provided_script
+ )
+
+
+def test_sysv_service_detection() -> None:
+ plugin = initialize_plugin_under_test_preloaded(
+ 1,
+ initialize_debputy_features,
+ plugin_name="debputy",
+ load_debputy_plugin=False,
+ )
+ init_dir = "etc/init.d"
+
+ services, _ = plugin.run_service_detection_and_integrations(
+ "sysvinit", build_virtual_file_system([])
+ )
+ assert not services
+
+ services, _ = plugin.run_service_detection_and_integrations(
+ "sysvinit",
+ build_virtual_file_system(
+ [
+ f"{init_dir}/",
+ ]
+ ),
+ )
+ assert not services
+
+ services, _ = plugin.run_service_detection_and_integrations(
+ "sysvinit",
+ build_virtual_file_system(
+ [
+ virtual_path_def(
+ f"{init_dir}/README",
+ mode=0o644,
+ ),
+ ]
+ ),
+ )
+ assert not services
+
+ fs_root = build_virtual_file_system(
+ [
+ virtual_path_def(
+ f"{init_dir}/foo",
+ mode=0o755,
+ ),
+ ]
+ )
+ services, metadata = plugin.run_service_detection_and_integrations(
+ "sysvinit", fs_root
+ )
+ assert len(services) == 1
+ assert {s.names[0] for s in services} == {"foo"}
+ foo_service = _extract_service(services, "foo")
+ assert set(foo_service.names) == {"foo"}
+ assert foo_service.type_of_service == "service"
+ assert foo_service.service_scope == "system"
+ assert foo_service.enable_by_default
+ assert foo_service.start_by_default
+ assert foo_service.default_upgrade_rule == "restart"
+
+ snippets = metadata.maintscripts()
+ assert len(snippets) == 4
+ postinsts = metadata.maintscripts(maintscript="postinst")
+ assert len(postinsts) == 1
+ postinst = postinsts[0]
+ assert postinst.registration_method == "on_configure"
+ assert "" in postinst.plugin_provided_script
+ assert "update-rc.d foo defaults" in postinst.plugin_provided_script
+ assert (
+ "invoke-rc.d --skip-systemd-native foo start" in postinst.plugin_provided_script
+ )
+ assert (
+ "invoke-rc.d --skip-systemd-native foo restart"
+ in postinst.plugin_provided_script
+ )
+
+
+def test_debputy_manifest_variables() -> None:
+ plugin = initialize_plugin_under_test_preloaded(
+ 1,
+ initialize_debputy_features,
+ plugin_name="debputy",
+ load_debputy_plugin=False,
+ )
+ manifest_variables_no_dch = plugin.manifest_variables()
+ assert manifest_variables_no_dch.keys() == {
+ "DEB_BUILD_ARCH",
+ "DEB_BUILD_ARCH_ABI",
+ "DEB_BUILD_ARCH_BITS",
+ "DEB_BUILD_ARCH_CPU",
+ "DEB_BUILD_ARCH_ENDIAN",
+ "DEB_BUILD_ARCH_LIBC",
+ "DEB_BUILD_ARCH_OS",
+ "DEB_BUILD_GNU_CPU",
+ "DEB_BUILD_GNU_SYSTEM",
+ "DEB_BUILD_GNU_TYPE",
+ "DEB_BUILD_MULTIARCH",
+ "DEB_HOST_ARCH",
+ "DEB_HOST_ARCH_ABI",
+ "DEB_HOST_ARCH_BITS",
+ "DEB_HOST_ARCH_CPU",
+ "DEB_HOST_ARCH_ENDIAN",
+ "DEB_HOST_ARCH_LIBC",
+ "DEB_HOST_ARCH_OS",
+ "DEB_HOST_GNU_CPU",
+ "DEB_HOST_GNU_SYSTEM",
+ "DEB_HOST_GNU_TYPE",
+ "DEB_HOST_MULTIARCH",
+ "DEB_SOURCE",
+ "DEB_TARGET_ARCH",
+ "DEB_TARGET_ARCH_ABI",
+ "DEB_TARGET_ARCH_BITS",
+ "DEB_TARGET_ARCH_CPU",
+ "DEB_TARGET_ARCH_ENDIAN",
+ "DEB_TARGET_ARCH_LIBC",
+ "DEB_TARGET_ARCH_OS",
+ "DEB_TARGET_GNU_CPU",
+ "DEB_TARGET_GNU_SYSTEM",
+ "DEB_TARGET_GNU_TYPE",
+ "DEB_TARGET_MULTIARCH",
+ "DEB_VERSION",
+ "DEB_VERSION_EPOCH_UPSTREAM",
+ "DEB_VERSION_UPSTREAM",
+ "DEB_VERSION_UPSTREAM_REVISION",
+ "PACKAGE",
+ "SOURCE_DATE_EPOCH",
+ "_DEBPUTY_INTERNAL_NON_BINNMU_SOURCE",
+ "_DEBPUTY_SND_SOURCE_DATE_EPOCH",
+ "path:BASH_COMPLETION_DIR",
+ "path:GNU_INFO_DIR",
+ "token:CLOSE_CURLY_BRACE",
+ "token:DOUBLE_CLOSE_CURLY_BRACE",
+ "token:DOUBLE_OPEN_CURLY_BRACE",
+ "token:NEWLINE",
+ "token:NL",
+ "token:OPEN_CURLY_BRACE",
+ "token:TAB",
+ }
+
+ for v in [
+ "DEB_SOURCE",
+ "DEB_VERSION",
+ "DEB_VERSION_EPOCH_UPSTREAM",
+ "DEB_VERSION_UPSTREAM",
+ "DEB_VERSION_UPSTREAM_REVISION",
+ "SOURCE_DATE_EPOCH",
+ "_DEBPUTY_INTERNAL_NON_BINNMU_SOURCE",
+ "_DEBPUTY_SND_SOURCE_DATE_EPOCH",
+ ]:
+ with pytest.raises(DebputyManifestVariableRequiresDebianDirError):
+ manifest_variables_no_dch[v]
+
+ with pytest.raises(DebputySubstitutionError):
+ manifest_variables_no_dch["PACKAGE"]
+
+ dch_content = textwrap.dedent(
+ """\
+ mscgen (1:0.20-15) unstable; urgency=medium
+
+ * Irrelevant stuff here...
+ * Also, some details have been tweaked for better testing
+
+ -- Niels Thykier <niels@thykier.net> Mon, 09 Oct 2023 14:50:06 +0000
+ """
+ )
+
+ debian_dir = build_virtual_file_system(
+ [virtual_path_def("changelog", content=dch_content)]
+ )
+ resolution_context = manifest_variable_resolution_context(debian_dir=debian_dir)
+ manifest_variables = plugin.manifest_variables(
+ resolution_context=resolution_context
+ )
+
+ assert manifest_variables["DEB_SOURCE"] == "mscgen"
+ assert manifest_variables["DEB_VERSION"] == "1:0.20-15"
+ assert manifest_variables["_DEBPUTY_INTERNAL_NON_BINNMU_SOURCE"] == "1:0.20-15"
+
+ assert manifest_variables["DEB_VERSION_EPOCH_UPSTREAM"] == "1:0.20"
+ assert manifest_variables["DEB_VERSION_UPSTREAM"] == "0.20"
+ assert manifest_variables["DEB_VERSION_UPSTREAM_REVISION"] == "0.20-15"
+ assert manifest_variables["SOURCE_DATE_EPOCH"] == "1696863006"
+ assert manifest_variables["_DEBPUTY_SND_SOURCE_DATE_EPOCH"] == "1696863006"
+
+ # This one remains unresolvable
+ with pytest.raises(DebputySubstitutionError):
+ manifest_variables["PACKAGE"]
+
+ static_values = {
+ "path:BASH_COMPLETION_DIR": "/usr/share/bash-completion/completions",
+ "path:GNU_INFO_DIR": "/usr/share/info",
+ }
+
+ for k, v in static_values.items():
+ assert manifest_variables[k] == v
+
+ dch_content_bin_nmu = textwrap.dedent(
+ """\
+ mscgen (1:0.20-15+b4) unstable; urgency=medium, binary-only=yes
+
+ * Some binNMU entry here
+
+ -- Niels Thykier <niels@thykier.net> Mon, 10 Nov 2023 16:01:17 +0000
+
+ mscgen (1:0.20-15) unstable; urgency=medium
+
+ * Irrelevant stuff here...
+ * Also, some details have been tweaked for better testing
+
+ -- Niels Thykier <niels@thykier.net> Mon, 09 Oct 2023 14:50:06 +0000
+ """
+ )
+
+ debian_dir_bin_nmu = build_virtual_file_system(
+ [virtual_path_def("changelog", content=dch_content_bin_nmu)]
+ )
+ resolution_context_bin_nmu = manifest_variable_resolution_context(
+ debian_dir=debian_dir_bin_nmu
+ )
+ manifest_variables_bin_nmu = plugin.manifest_variables(
+ resolution_context=resolution_context_bin_nmu
+ )
+
+ assert manifest_variables_bin_nmu["DEB_SOURCE"] == "mscgen"
+ assert manifest_variables_bin_nmu["DEB_VERSION"] == "1:0.20-15+b4"
+ assert (
+ manifest_variables_bin_nmu["_DEBPUTY_INTERNAL_NON_BINNMU_SOURCE"] == "1:0.20-15"
+ )
+
+ assert manifest_variables_bin_nmu["DEB_VERSION_EPOCH_UPSTREAM"] == "1:0.20"
+ assert manifest_variables_bin_nmu["DEB_VERSION_UPSTREAM"] == "0.20"
+ assert manifest_variables_bin_nmu["DEB_VERSION_UPSTREAM_REVISION"] == "0.20-15+b4"
+ assert manifest_variables_bin_nmu["SOURCE_DATE_EPOCH"] == "1699632077"
+ assert manifest_variables_bin_nmu["_DEBPUTY_SND_SOURCE_DATE_EPOCH"] == "1696863006"
+
+
+def test_cap_validator() -> None:
+ has_libcap, _, is_valid_cap = load_libcap()
+
+ if not has_libcap:
+ if os.environ.get("DEBPUTY_REQUIRE_LIBCAP", "") != "":
+ pytest.fail("Could not load libcap, but DEBPUTY_REQUIRE_CAP was non-empty")
+ pytest.skip("Could not load libcap.so")
+ assert not is_valid_cap("foo")
+ assert is_valid_cap("cap_dac_override,cap_bpf,cap_net_admin=ep")
+
+
+def test_clean_la_files() -> None:
+ plugin = initialize_plugin_under_test_preloaded(
+ 1,
+ initialize_debputy_features,
+ plugin_name="debputy",
+ load_debputy_plugin=False,
+ )
+ fs_root = build_virtual_file_system(
+ [virtual_path_def("usr/bin/foo", content="#!/bin/sh\n")]
+ )
+ # Does nothing by default
+ plugin.run_package_processor(
+ "clean-la-files",
+ fs_root,
+ )
+
+ la_file_content = textwrap.dedent(
+ """\
+ dependency_libs = 'foo bar'
+ another_line = 'foo bar'
+ """
+ )
+ expected_content = textwrap.dedent(
+ """\
+ dependency_libs = ''
+ another_line = 'foo bar'
+ """
+ )
+ la_file_content_no_change = expected_content
+ expected_content = textwrap.dedent(
+ """\
+ dependency_libs = ''
+ another_line = 'foo bar'
+ """
+ )
+
+ fs_root = build_virtual_file_system(
+ [
+ virtual_path_def("usr/lib/libfoo.la", materialized_content=la_file_content),
+ virtual_path_def(
+ "usr/lib/libfoo-unchanged.la",
+ content=la_file_content_no_change,
+ ),
+ ]
+ )
+
+ plugin.run_package_processor(
+ "clean-la-files",
+ fs_root,
+ )
+ for basename in ("libfoo.la", "libfoo-unchanged.la"):
+ la_file = fs_root.lookup(f"usr/lib/{basename}")
+ assert la_file is not None and la_file.is_file
+ if basename == "libfoo-unchanged.la":
+ # it should never have been rewritten
+ assert not la_file.has_fs_path
+ else:
+ assert la_file.has_fs_path
+ with la_file.open() as fd:
+ rewritten_content = fd.read()
+ assert rewritten_content == expected_content
+
+
+def test_strip_nondeterminism() -> None:
+ plugin = initialize_plugin_under_test_preloaded(
+ 1,
+ initialize_debputy_features,
+ plugin_name="debputy",
+ load_debputy_plugin=False,
+ )
+
+ fs_root = build_virtual_file_system(
+ [
+ # Note, we are only testing a negative example as a positive example crashes
+ # because we do not have a SOURCE_DATE_EPOCH value/substitution
+ virtual_path_def("test/not-really-a-png.png", content="Not a PNG")
+ ]
+ )
+
+ plugin.run_package_processor(
+ "strip-nondeterminism",
+ fs_root,
+ )
+
+
+def test_translate_capabilities() -> None:
+ attribute_path = AttributePath.test_path()
+ plugin = initialize_plugin_under_test_preloaded(
+ 1,
+ initialize_debputy_features,
+ plugin_name="debputy",
+ load_debputy_plugin=False,
+ )
+
+ fs_root = build_virtual_file_system([virtual_path_def("usr/bin/foo", mode=0o4755)])
+
+ foo = fs_root.lookup("usr/bin/foo")
+ assert foo is not None
+ assert foo.is_file
+ assert foo.is_read_write
+
+ metadata_no_cap = plugin.run_metadata_detector(
+ "translate-capabilities",
+ fs_root,
+ )
+
+ assert not metadata_no_cap.maintscripts(maintscript="postinst")
+
+ cap = foo.metadata(DebputyCapability)
+ assert not cap.is_present
+ assert cap.can_write
+ cap.value = DebputyCapability(
+ capabilities="cap_net_raw+ep",
+ capability_mode=SymbolicMode.parse_filesystem_mode(
+ "u-s",
+ attribute_path["cap_mode"],
+ ),
+ definition_source="test",
+ )
+ metadata_w_cap = plugin.run_metadata_detector(
+ "translate-capabilities",
+ fs_root,
+ )
+
+ postinsts = metadata_w_cap.maintscripts(maintscript="postinst")
+ assert len(postinsts) == 1
+ postinst = postinsts[0]
+ assert postinst.registration_method == "on_configure"
+ assert "setcap cap_net_raw+ep " in postinst.plugin_provided_script
+ assert "chmod u-s " in postinst.plugin_provided_script
+
+
+def test_pam_auth_update() -> None:
+ plugin = initialize_plugin_under_test_preloaded(
+ 1,
+ initialize_debputy_features,
+ plugin_name="debputy",
+ load_debputy_plugin=False,
+ )
+
+ fs_root = build_virtual_file_system(["usr/bin/foo"])
+
+ empty_metadata = plugin.run_metadata_detector("pam-auth-update", fs_root)
+ assert not empty_metadata.maintscripts()
+
+ fs_root = build_virtual_file_system(["/usr/share/pam-configs/foo-pam"])
+
+ pam_metadata = plugin.run_metadata_detector("pam-auth-update", fs_root)
+ postinsts = pam_metadata.maintscripts(maintscript="postinst")
+ assert len(postinsts) == 1
+ prerms = pam_metadata.maintscripts(maintscript="prerm")
+ assert len(prerms) == 1
+
+ postinst = postinsts[0]
+ assert postinst.registration_method == "on_configure"
+ assert "pam-auth-update --package" in postinst.plugin_provided_script
+
+ prerms = prerms[0]
+ assert prerms.registration_method == "on_before_removal"
+ assert "pam-auth-update --package --remove foo-pam" in prerms.plugin_provided_script
+
+
+def test_auto_depends_solink() -> None:
+ plugin = initialize_plugin_under_test_preloaded(
+ 1,
+ initialize_debputy_features,
+ plugin_name="debputy",
+ load_debputy_plugin=False,
+ )
+
+ fs_root = build_virtual_file_system(["usr/bin/foo"])
+
+ empty_metadata = plugin.run_metadata_detector(
+ "auto-depends-arch-any-solink",
+ fs_root,
+ )
+ assert "misc:Depends" not in empty_metadata.substvars
+ fs_root = build_virtual_file_system(
+ [
+ "usr/lib/x86_64-linux-gnu/libfoo.la",
+ virtual_path_def(
+ "usr/lib/x86_64-linux-gnu/libfoo.so", link_target="libfoo.so.1"
+ ),
+ ]
+ )
+
+ still_empty_metadata = plugin.run_metadata_detector(
+ "auto-depends-arch-any-solink",
+ fs_root,
+ )
+ assert "misc:Depends" not in still_empty_metadata.substvars
+
+ libfoo1_fs_root = build_virtual_file_system(
+ [
+ virtual_path_def(
+ "usr/lib/x86_64-linux-gnu/libfoo.so.1", link_target="libfoo.so.1.0.0"
+ ),
+ ]
+ )
+
+ context_correct = package_metadata_context(
+ package_fields={
+ "Package": "libfoo-dev",
+ },
+ accessible_package_roots=[
+ (
+ {
+ "Package": "libfoo1",
+ "Architecture": "any",
+ },
+ libfoo1_fs_root,
+ )
+ ],
+ )
+ sodep_metadata = plugin.run_metadata_detector(
+ "auto-depends-arch-any-solink",
+ fs_root,
+ context=context_correct,
+ )
+ assert "misc:Depends" in sodep_metadata.substvars
+ assert sodep_metadata.substvars["misc:Depends"] == "libfoo1 (= ${binary:Version})"
+
+ context_incorrect = package_metadata_context(
+ package_fields={"Package": "libfoo-dev", "Architecture": "all"},
+ accessible_package_roots=[
+ (
+ {
+ "Package": "foo",
+ "Architecture": "all",
+ },
+ build_virtual_file_system([]),
+ )
+ ],
+ )
+ sodep_metadata = plugin.run_metadata_detector(
+ "auto-depends-arch-any-solink",
+ fs_root,
+ context=context_incorrect,
+ )
+ assert "misc:Depends" not in sodep_metadata.substvars
+
+ context_too_many_matches = package_metadata_context(
+ package_fields={"Package": "libfoo-dev"},
+ accessible_package_roots=[
+ (
+ {
+ "Package": "libfoo1-a",
+ "Architecture": "any",
+ },
+ libfoo1_fs_root,
+ ),
+ (
+ {
+ "Package": "libfoo1-b",
+ "Architecture": "any",
+ },
+ libfoo1_fs_root,
+ ),
+ ],
+ )
+ sodep_metadata = plugin.run_metadata_detector(
+ "auto-depends-arch-any-solink",
+ fs_root,
+ context=context_too_many_matches,
+ )
+ assert "misc:Depends" not in sodep_metadata.substvars
diff --git a/tests/test_declarative_parser.py b/tests/test_declarative_parser.py
new file mode 100644
index 0000000..a5061cb
--- /dev/null
+++ b/tests/test_declarative_parser.py
@@ -0,0 +1,211 @@
+from typing import List, TypedDict, NotRequired, Annotated, Union, Mapping
+
+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.mapper_code import type_mapper_str2package
+from debputy.manifest_parser.parser_data import ParserContextData
+from debputy.manifest_parser.util import AttributePath
+from debputy.packages import BinaryPackage
+from debputy.substitution import NULL_SUBSTITUTION
+from tutil import faked_binary_package
+
+
+class TFinalEntity(DebputyParsedContent):
+ sources: List[str]
+ install_as: NotRequired[str]
+ into: NotRequired[List[BinaryPackage]]
+ recursive: NotRequired[bool]
+
+
+class TSourceEntity(TypedDict):
+ sources: NotRequired[List[str]]
+ source: Annotated[NotRequired[str], DebputyParseHint.target_attribute("sources")]
+ as_: NotRequired[
+ Annotated[
+ str,
+ DebputyParseHint.target_attribute("install_as"),
+ DebputyParseHint.conflicts_with_source_attributes("sources"),
+ ]
+ ]
+ into: NotRequired[Union[BinaryPackage, List[BinaryPackage]]]
+ recursive: NotRequired[bool]
+
+
+TSourceEntityAltFormat = Union[TSourceEntity, List[str], str]
+
+
+foo_package = faked_binary_package("foo")
+context_packages = {
+ foo_package.name: foo_package,
+}
+context_package_states = {
+ p.name: PackageTransformationDefinition(
+ p,
+ NULL_SUBSTITUTION,
+ False,
+ )
+ for p in context_packages.values()
+}
+
+
+class TestParserContextData(ParserContextData):
+ @property
+ def _package_states(self) -> Mapping[str, PackageTransformationDefinition]:
+ return context_package_states
+
+ @property
+ def binary_packages(self) -> Mapping[str, BinaryPackage]:
+ return context_packages
+
+
+@pytest.fixture
+def parser_context():
+ return TestParserContextData()
+
+
+@pytest.mark.parametrize(
+ "source_payload,expected_data,expected_attribute_path,parse_content,source_content",
+ [
+ (
+ {"sources": ["foo", "bar"]},
+ {"sources": ["foo", "bar"]},
+ {
+ "sources": "sources",
+ },
+ TFinalEntity,
+ None,
+ ),
+ (
+ {"sources": ["foo", "bar"], "install-as": "as-value"},
+ {"sources": ["foo", "bar"], "install_as": "as-value"},
+ {"sources": "sources", "install_as": "install-as"},
+ TFinalEntity,
+ None,
+ ),
+ (
+ {"sources": ["foo", "bar"], "install-as": "as-value", "into": ["foo"]},
+ {
+ "sources": ["foo", "bar"],
+ "install_as": "as-value",
+ "into": [foo_package],
+ },
+ {"sources": "sources", "install_as": "install-as", "into": "into"},
+ TFinalEntity,
+ None,
+ ),
+ (
+ {"source": "foo", "as": "as-value", "into": ["foo"]},
+ {
+ "sources": ["foo"],
+ "install_as": "as-value",
+ "into": [foo_package],
+ },
+ {"sources": "source", "install_as": "as", "into": "into"},
+ TFinalEntity,
+ TSourceEntity,
+ ),
+ (
+ {"source": "foo", "as": "as-value", "into": ["foo"]},
+ {
+ "sources": ["foo"],
+ "install_as": "as-value",
+ "into": [foo_package],
+ },
+ {"sources": "source", "install_as": "as", "into": "into"},
+ TFinalEntity,
+ TSourceEntityAltFormat,
+ ),
+ (
+ ["foo", "bar"],
+ {
+ "sources": ["foo", "bar"],
+ },
+ {"sources": "parse-root"},
+ TFinalEntity,
+ TSourceEntityAltFormat,
+ ),
+ (
+ "foo",
+ {
+ "sources": ["foo"],
+ },
+ {"sources": "parse-root"},
+ TFinalEntity,
+ TSourceEntityAltFormat,
+ ),
+ (
+ "foo",
+ {
+ "sources": ["foo"],
+ },
+ {"sources": "parse-root"},
+ TFinalEntity,
+ str,
+ ),
+ (
+ ["foo", "bar"],
+ {
+ "sources": ["foo", "bar"],
+ },
+ {"sources": "parse-root"},
+ TFinalEntity,
+ List[str],
+ ),
+ (
+ "foo",
+ {
+ "sources": ["foo"],
+ },
+ {"sources": "parse-root"},
+ TFinalEntity,
+ Union[str, List[str]],
+ ),
+ (
+ ["foo", "bar"],
+ {
+ "sources": ["foo", "bar"],
+ },
+ {"sources": "parse-root"},
+ TFinalEntity,
+ Union[str, List[str]],
+ ),
+ (
+ {"source": "foo", "recursive": True},
+ {
+ "sources": ["foo"],
+ "recursive": True,
+ },
+ {"sources": "source", "recursive": "recursive"},
+ TFinalEntity,
+ TSourceEntityAltFormat,
+ ),
+ ],
+)
+def test_declarative_parser_ok(
+ attribute_path: AttributePath,
+ parser_context: ParserContextData,
+ source_payload,
+ expected_data,
+ expected_attribute_path,
+ parse_content,
+ source_content,
+):
+ pg = ParserGenerator()
+ pg.register_mapped_type(TypeMapping(BinaryPackage, str, type_mapper_str2package))
+ parser = pg.parser_from_typed_dict(
+ parse_content,
+ source_content=source_content,
+ )
+ data_path = attribute_path["parse-root"]
+ parsed_data = parser.parse_input(
+ source_payload, data_path, parser_context=parser_context
+ )
+ assert expected_data == parsed_data
+ attributes = {k: data_path[k].name for k in expected_attribute_path}
+ assert attributes == expected_attribute_path
diff --git a/tests/test_fs_metadata.py b/tests/test_fs_metadata.py
new file mode 100644
index 0000000..14a397f
--- /dev/null
+++ b/tests/test_fs_metadata.py
@@ -0,0 +1,770 @@
+import dataclasses
+import textwrap
+from typing import Tuple, List, Optional, Union
+
+import pytest
+
+from debputy.filesystem_scan import PathDef, build_virtual_fs
+from debputy.highlevel_manifest_parser import YAMLManifestParser
+from debputy.intermediate_manifest import PathType, IntermediateManifest, TarMember
+from debputy.plugin.api import virtual_path_def
+from debputy.plugin.api.test_api import build_virtual_file_system
+from debputy.transformation_rules import TransformationRuntimeError
+
+
+@pytest.fixture()
+def manifest_parser_pkg_foo(
+ amd64_dpkg_architecture_variables,
+ dpkg_arch_query,
+ source_package,
+ package_single_foo_arch_all_cxt_amd64,
+ amd64_substitution,
+ no_profiles_or_build_options,
+ debputy_plugin_feature_set,
+) -> YAMLManifestParser:
+ # We need an empty directory to avoid triggering packager provided files.
+ debian_dir = build_virtual_file_system([])
+ return YAMLManifestParser(
+ "debian/test-debputy.manifest",
+ source_package,
+ package_single_foo_arch_all_cxt_amd64,
+ amd64_substitution,
+ amd64_dpkg_architecture_variables,
+ dpkg_arch_query,
+ no_profiles_or_build_options,
+ debputy_plugin_feature_set,
+ debian_dir=debian_dir,
+ )
+
+
+@dataclasses.dataclass(slots=True, kw_only=True)
+class Expected:
+ mtime: Optional[float]
+ mode: Optional[int] = None
+ link_target: Optional[str] = None
+ owner: str = "root"
+ group: str = "root"
+ has_fs_path: bool = True
+
+
+def _show_name_on_error(_: str) -> bool:
+ return False
+
+
+def _has_fs_path(tm: TarMember) -> bool:
+ return tm.fs_path is not None
+
+
+def verify_paths(
+ intermediate_manifest: IntermediateManifest,
+ expected_results: List[Tuple[Union[str, PathDef], Expected]],
+) -> None:
+ result = {tm.member_path: tm for tm in intermediate_manifest}
+ expected_table = {
+ f"./{p}" if isinstance(p, str) else f"./{p.path_name}": e
+ for p, e in expected_results
+ }
+
+ for path_name, expected in expected_table.items():
+ tm = result[path_name]
+ if tm.path_type == PathType.SYMLINK:
+ assert tm.link_target == expected.link_target or _show_name_on_error(
+ path_name
+ )
+ else:
+ assert tm.link_target == "" or _show_name_on_error(path_name)
+ if expected.mode is not None:
+ assert oct(tm.mode) == oct(expected.mode) or _show_name_on_error(path_name)
+ if expected.mtime is not None:
+ assert tm.mtime == expected.mtime or _show_name_on_error(path_name)
+ assert tm.owner == expected.owner or _show_name_on_error(path_name)
+ assert tm.group == expected.group or _show_name_on_error(path_name)
+ assert _has_fs_path(tm) == expected.has_fs_path or _show_name_on_error(
+ path_name
+ )
+
+ del result["./"]
+ if len(result) != len(expected_results):
+ for tm in result.values():
+ assert tm.member_path in expected_table
+
+
+def test_mtime_clamp_and_builtin_dir_mode(manifest_parser_pkg_foo):
+ manifest = manifest_parser_pkg_foo.build_manifest()
+ claim_mtime_to = 255
+ path_defs: List[Tuple[PathDef, Expected]] = [
+ (
+ virtual_path_def("usr/", mode=0o700, mtime=10, fs_path="/nowhere/usr/"),
+ Expected(mode=0o755, mtime=10),
+ ),
+ (
+ virtual_path_def(
+ "usr/bin/", mode=0o2534, mtime=5000, fs_path="/nowhere/usr/bin/"
+ ),
+ Expected(mode=0o755, mtime=claim_mtime_to),
+ ),
+ (
+ virtual_path_def(
+ "usr/bin/my-exec",
+ mtime=5000,
+ fs_path="/nowhere/usr/bin/my-exec",
+ link_target="../../some/where/else",
+ ),
+ # Implementation detail; symlinks do not refer to their FS path in the intermediate manifest.
+ Expected(
+ mtime=claim_mtime_to, link_target="/some/where/else", has_fs_path=False
+ ),
+ ),
+ ]
+
+ fs_root = build_virtual_fs([d[0] for d in path_defs], read_write_fs=True)
+
+ assert [p.name for p in manifest.all_packages] == ["foo"]
+
+ intermediate_manifest = manifest.apply_to_binary_staging_directory(
+ "foo", fs_root, claim_mtime_to
+ )
+ verify_paths(intermediate_manifest, path_defs)
+
+
+def test_transformations_create_symlink(manifest_parser_pkg_foo):
+ content = textwrap.dedent(
+ """\
+ manifest-version: '0.1'
+ packages:
+ foo:
+ transformations:
+ - create-symlink:
+ path: 'usr/bin/my-exec'
+ target: '../../some/where/else'
+ - create-symlink:
+ path: 'usr/bin/{{PACKAGE}}'
+ target: '/usr/lib/{{DEB_HOST_MULTIARCH}}/{{PACKAGE}}/tool'
+ """
+ )
+ manifest = manifest_parser_pkg_foo.parse_manifest(fd=content)
+ claim_mtime_to = 255
+ fs_root = build_virtual_fs(["./"], read_write_fs=True)
+ expected_results = [
+ ("usr/", Expected(mode=0o755, mtime=claim_mtime_to, has_fs_path=False)),
+ ("usr/bin/", Expected(mode=0o755, mtime=claim_mtime_to, has_fs_path=False)),
+ (
+ "usr/bin/my-exec",
+ Expected(
+ mtime=claim_mtime_to, link_target="/some/where/else", has_fs_path=False
+ ),
+ ),
+ (
+ "usr/bin/foo",
+ Expected(
+ mtime=claim_mtime_to,
+ # Test is using a "static" dpkg-architecture, so it will always be `x86_64-linux-gnu`
+ link_target="../lib/x86_64-linux-gnu/foo/tool",
+ has_fs_path=False,
+ ),
+ ),
+ ]
+
+ assert [p.name for p in manifest.all_packages] == ["foo"]
+
+ intermediate_manifest = manifest.apply_to_binary_staging_directory(
+ "foo", fs_root, claim_mtime_to
+ )
+ verify_paths(intermediate_manifest, expected_results)
+
+
+def test_transformations_create_symlink_replace_success(manifest_parser_pkg_foo):
+ content = textwrap.dedent(
+ """\
+ manifest-version: '0.1'
+ packages:
+ foo:
+ transformations:
+ - create-symlink:
+ path: 'usr/bin/my-exec'
+ target: '../../some/where/else'
+ """
+ )
+ manifest = manifest_parser_pkg_foo.parse_manifest(fd=content)
+ clamp_mtime_to = 255
+ fs_root = build_virtual_fs(["./usr/bin/my-exec"], read_write_fs=True)
+ expected_results = [
+ ("usr/", Expected(mode=0o755, mtime=clamp_mtime_to, has_fs_path=False)),
+ ("usr/bin/", Expected(mode=0o755, mtime=clamp_mtime_to, has_fs_path=False)),
+ (
+ "usr/bin/my-exec",
+ Expected(
+ mtime=clamp_mtime_to, link_target="/some/where/else", has_fs_path=False
+ ),
+ ),
+ ]
+
+ assert [p.name for p in manifest.all_packages] == ["foo"]
+
+ intermediate_manifest = manifest.apply_to_binary_staging_directory(
+ "foo", fs_root, clamp_mtime_to
+ )
+ verify_paths(intermediate_manifest, expected_results)
+
+
+@pytest.mark.parametrize(
+ "replacement_rule, reason",
+ [
+ (
+ "abort-on-non-empty-directory",
+ "the path is a non-empty directory",
+ ),
+ (
+ "error-if-directory",
+ "the path is a directory",
+ ),
+ (
+ "error-if-exists",
+ "the path exists",
+ ),
+ ],
+)
+def test_transformations_create_symlink_replace_failure(
+ manifest_parser_pkg_foo, replacement_rule, reason
+):
+ content = textwrap.dedent(
+ f"""\
+ manifest-version: '0.1'
+ packages:
+ foo:
+ transformations:
+ - create-symlink:
+ path: 'usr/share/foo'
+ target: 'somewhere-else'
+ replacement-rule: {replacement_rule}
+ """
+ )
+ manifest = manifest_parser_pkg_foo.parse_manifest(fd=content)
+ clamp_mtime_to = 255
+ fs_root = build_virtual_fs(["./usr/share/foo/bar"], read_write_fs=True)
+
+ assert [p.name for p in manifest.all_packages] == ["foo"]
+
+ with pytest.raises(TransformationRuntimeError) as e_info:
+ manifest.apply_to_binary_staging_directory("foo", fs_root, clamp_mtime_to)
+
+ msg = (
+ 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>."
+ )
+ assert e_info.value.args[0] == msg
+
+
+def test_transformations_create_symlink_replace_with_explicit_remove(
+ manifest_parser_pkg_foo,
+):
+ content = textwrap.dedent(
+ """\
+ manifest-version: '0.1'
+ packages:
+ foo:
+ transformations:
+ - remove: usr/share/foo
+ - create-symlink:
+ path: 'usr/share/foo'
+ target: 'somewhere-else'
+ """
+ )
+ manifest = manifest_parser_pkg_foo.parse_manifest(fd=content)
+ clamp_mtime_to = 255
+ fs_root = build_virtual_fs(["./usr/share/foo/bar"], read_write_fs=True)
+ expected_results = [
+ ("usr/", Expected(mode=0o755, mtime=clamp_mtime_to, has_fs_path=False)),
+ ("usr/share/", Expected(mode=0o755, mtime=clamp_mtime_to, has_fs_path=False)),
+ (
+ "usr/share/foo",
+ Expected(
+ mtime=clamp_mtime_to, link_target="somewhere-else", has_fs_path=False
+ ),
+ ),
+ ]
+
+ assert [p.name for p in manifest.all_packages] == ["foo"]
+ intermediate_manifest = manifest.apply_to_binary_staging_directory(
+ "foo", fs_root, clamp_mtime_to
+ )
+ verify_paths(intermediate_manifest, expected_results)
+
+
+def test_transformations_create_symlink_replace_with_replacement_rule(
+ manifest_parser_pkg_foo,
+):
+ content = textwrap.dedent(
+ """\
+ manifest-version: '0.1'
+ packages:
+ foo:
+ transformations:
+ - remove: usr/share/foo
+ - create-symlink:
+ path: 'usr/share/foo'
+ target: 'somewhere-else'
+ replacement-rule: 'discard-existing'
+ """
+ )
+ manifest = manifest_parser_pkg_foo.parse_manifest(fd=content)
+ clamp_mtime_to = 255
+ fs_root = build_virtual_fs(["./usr/share/foo/bar"], read_write_fs=True)
+ expected_results = [
+ ("usr/", Expected(mode=0o755, mtime=clamp_mtime_to, has_fs_path=False)),
+ ("usr/share/", Expected(mode=0o755, mtime=clamp_mtime_to, has_fs_path=False)),
+ (
+ "usr/share/foo",
+ Expected(
+ mtime=clamp_mtime_to, link_target="somewhere-else", has_fs_path=False
+ ),
+ ),
+ ]
+
+ assert [p.name for p in manifest.all_packages] == ["foo"]
+ intermediate_manifest = manifest.apply_to_binary_staging_directory(
+ "foo", fs_root, clamp_mtime_to
+ )
+ verify_paths(intermediate_manifest, expected_results)
+
+
+def test_transformations_path_metadata(manifest_parser_pkg_foo):
+ content = textwrap.dedent(
+ """\
+ manifest-version: '0.1'
+ packages:
+ foo:
+ transformations:
+ - path-metadata:
+ path: 'usr/bin/my-exec'
+ mode: "-x"
+ owner: "bin"
+ group: 2
+ """
+ )
+ manifest = manifest_parser_pkg_foo.parse_manifest(fd=content)
+ claim_mtime_to = 255
+ fs_root = build_virtual_fs(
+ [
+ virtual_path_def(
+ "./usr/bin/my-exec", fs_path="/no-where", mode=0o755, mtime=10
+ ),
+ ],
+ read_write_fs=True,
+ )
+ expected_results = [
+ ("usr/", Expected(mode=0o755, mtime=claim_mtime_to, has_fs_path=False)),
+ ("usr/bin/", Expected(mode=0o755, mtime=claim_mtime_to, has_fs_path=False)),
+ (
+ "usr/bin/my-exec",
+ Expected(
+ mtime=10,
+ has_fs_path=True,
+ mode=0o644,
+ owner="bin",
+ group="bin",
+ ),
+ ),
+ ]
+
+ assert [p.name for p in manifest.all_packages] == ["foo"]
+
+ intermediate_manifest = manifest.apply_to_binary_staging_directory(
+ "foo", fs_root, claim_mtime_to
+ )
+ verify_paths(intermediate_manifest, expected_results)
+
+
+def test_transformations_directories(manifest_parser_pkg_foo):
+ content = textwrap.dedent(
+ """\
+ manifest-version: '0.1'
+ packages:
+ foo:
+ transformations:
+ - create-directories:
+ path: some/empty/directory
+ mode: "0700"
+ - create-directories: another/empty/directory
+ - create-directories:
+ path: a/third-empty/directory
+ owner: www-data
+ group: www-data
+ """
+ )
+ manifest = manifest_parser_pkg_foo.parse_manifest(fd=content)
+ claim_mtime_to = 255
+ paths = [
+ virtual_path_def("some/", mtime=10, fs_path="/nowhere/some"),
+ virtual_path_def("some/empty/", mtime=10, fs_path="/nowhere/some/empty"),
+ virtual_path_def(
+ "some/empty/directory/",
+ mode=0o755,
+ mtime=10,
+ fs_path="/nowhere/some/empty/directory",
+ ),
+ ]
+ fs_root = build_virtual_fs(paths, read_write_fs=True)
+ expected_results = [
+ ("some/", Expected(mode=0o755, mtime=10, has_fs_path=True)),
+ ("some/empty/", Expected(mode=0o755, mtime=10, has_fs_path=True)),
+ (
+ "some/empty/directory/",
+ Expected(mode=0o700, mtime=10, has_fs_path=True),
+ ),
+ ("another/", Expected(mode=0o755, mtime=claim_mtime_to, has_fs_path=False)),
+ (
+ "another/empty/",
+ Expected(mode=0o755, mtime=claim_mtime_to, has_fs_path=False),
+ ),
+ (
+ "another/empty/directory/",
+ Expected(mode=0o755, mtime=claim_mtime_to, has_fs_path=False),
+ ),
+ ("a/", Expected(mode=0o755, mtime=claim_mtime_to, has_fs_path=False)),
+ (
+ "a/third-empty/",
+ Expected(mode=0o755, mtime=claim_mtime_to, has_fs_path=False),
+ ),
+ (
+ "a/third-empty/directory/",
+ Expected(
+ mode=0o755,
+ mtime=claim_mtime_to,
+ owner="www-data",
+ group="www-data",
+ has_fs_path=False,
+ ),
+ ),
+ ]
+
+ assert [p.name for p in manifest.all_packages] == ["foo"]
+
+ intermediate_manifest = manifest.apply_to_binary_staging_directory(
+ "foo", fs_root, claim_mtime_to
+ )
+ verify_paths(intermediate_manifest, expected_results)
+
+
+def test_transformation_remove(manifest_parser_pkg_foo):
+ content = textwrap.dedent(
+ """\
+ manifest-version: '0.1'
+ packages:
+ foo:
+ transformations:
+ - remove: some/empty
+ """
+ )
+ manifest = manifest_parser_pkg_foo.parse_manifest(fd=content)
+ claim_mtime_to = 255
+ paths = [
+ virtual_path_def("some/", mode=0o700, mtime=10, fs_path="/nowhere/some"),
+ virtual_path_def(
+ "some/empty/", mode=0o700, mtime=10, fs_path="/nowhere/some/empty"
+ ),
+ virtual_path_def(
+ "some/empty/directory/",
+ mode=0o755,
+ mtime=10,
+ fs_path="/nowhere/some/empty/directory",
+ ),
+ ]
+ fs_root = build_virtual_fs(paths, read_write_fs=True)
+ expected_results = []
+
+ assert [p.name for p in manifest.all_packages] == ["foo"]
+
+ intermediate_manifest = manifest.apply_to_binary_staging_directory(
+ "foo", fs_root, claim_mtime_to
+ )
+
+ verify_paths(intermediate_manifest, expected_results)
+
+
+def test_transformation_remove_keep_empty(manifest_parser_pkg_foo):
+ content = textwrap.dedent(
+ """\
+ manifest-version: '0.1'
+ packages:
+ foo:
+ transformations:
+ - remove:
+ path: some/empty
+ keep-empty-parent-dirs: true
+ """
+ )
+ manifest = manifest_parser_pkg_foo.parse_manifest(fd=content)
+ claim_mtime_to = 255
+ paths = [
+ virtual_path_def("some/", mode=0o700, mtime=10, fs_path="/nowhere/some"),
+ virtual_path_def(
+ "some/empty/", mode=0o700, mtime=10, fs_path="/nowhere/some/empty"
+ ),
+ virtual_path_def(
+ "some/empty/directory/",
+ mode=0o755,
+ mtime=10,
+ fs_path="/nowhere/some/empty/directory",
+ ),
+ ]
+ fs_root = build_virtual_fs(paths, read_write_fs=True)
+ expected_results = [
+ ("some/", Expected(mode=0o755, 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
+ )
+
+ verify_paths(intermediate_manifest, expected_results)
+
+
+def test_transformation_remove_glob(manifest_parser_pkg_foo):
+ content = textwrap.dedent(
+ """\
+ manifest-version: '0.1'
+ packages:
+ foo:
+ transformations:
+ - remove: some/*.json
+ """
+ )
+ manifest = manifest_parser_pkg_foo.parse_manifest(fd=content)
+ claim_mtime_to = 255
+ paths = [
+ virtual_path_def("some/", mode=0o700, mtime=10, fs_path="/nowhere/some"),
+ virtual_path_def(
+ "some/foo.json",
+ mode=0o600,
+ mtime=10,
+ ),
+ virtual_path_def(
+ "some/bar.json",
+ mode=0o600,
+ mtime=10,
+ ),
+ virtual_path_def(
+ "some/empty/", mode=0o700, mtime=10, fs_path="/nowhere/some/empty"
+ ),
+ virtual_path_def(
+ "some/bar.txt", mode=0o600, mtime=10, fs_path="/nowhere/some/bar.txt"
+ ),
+ virtual_path_def(
+ "some/bar.JSON", mode=0o600, mtime=10, fs_path="/nowhere/some/bar.JSON"
+ ),
+ ]
+ fs_root = build_virtual_fs(paths, read_write_fs=True)
+ expected_results = [
+ ("some/", Expected(mode=0o755, mtime=10)),
+ ("some/empty/", Expected(mode=0o755, mtime=10)),
+ ("some/bar.txt", Expected(mode=0o644, mtime=10)),
+ # Survives because pattern is case-sensitive
+ ("some/bar.JSON", 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
+ )
+
+ verify_paths(intermediate_manifest, expected_results)
+
+
+def test_transformation_remove_no_match(manifest_parser_pkg_foo):
+ content = textwrap.dedent(
+ """\
+ manifest-version: '0.1'
+ packages:
+ foo:
+ transformations:
+ - remove: some/non-existing-path
+ """
+ )
+ manifest = manifest_parser_pkg_foo.parse_manifest(fd=content)
+ claim_mtime_to = 255
+ paths = [
+ virtual_path_def("some/", mode=0o700, mtime=10, fs_path="/nowhere/some"),
+ virtual_path_def(
+ "some/empty/", mode=0o700, mtime=10, fs_path="/nowhere/some/empty"
+ ),
+ virtual_path_def(
+ "some/empty/directory/",
+ mode=0o755,
+ mtime=10,
+ fs_path="/nowhere/some/empty/directory",
+ ),
+ ]
+ fs_root = build_virtual_fs(paths, read_write_fs=True)
+ assert [p.name for p in manifest.all_packages] == ["foo"]
+
+ with pytest.raises(TransformationRuntimeError) as e_info:
+ 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'
+ " 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):
+ content = textwrap.dedent(
+ """\
+ manifest-version: '0.1'
+ packages:
+ foo:
+ transformations:
+ - move:
+ source: some/dir
+ target: new/dir/where-else
+ """
+ )
+ manifest = manifest_parser_pkg_foo.parse_manifest(fd=content)
+ claim_mtime_to = 255
+ paths = [
+ virtual_path_def("some/", mode=0o700, mtime=10, fs_path="/nowhere/some"),
+ virtual_path_def(
+ "some/dir/", mode=0o700, mtime=10, fs_path="/nowhere/some/empty"
+ ),
+ virtual_path_def(
+ "some/dir/some-dir-symlink1", mtime=10, link_target="/abs/some-target1"
+ ),
+ virtual_path_def(
+ "some/dir/some-dir-symlink2", mtime=10, link_target="../some-target2"
+ ),
+ virtual_path_def(
+ "some/dir/some-dir-symlink3",
+ mtime=10,
+ link_target="/new/dir/where-else/some-target3",
+ ),
+ ]
+ fs_root = build_virtual_fs(paths, read_write_fs=True)
+ assert [p.name for p in manifest.all_packages] == ["foo"]
+
+ expected_results = [
+ ("some/", Expected(mode=0o755, mtime=10)),
+ ("new/", Expected(mode=0o755, mtime=claim_mtime_to, has_fs_path=False)),
+ ("new/dir/", Expected(mode=0o755, mtime=claim_mtime_to, has_fs_path=False)),
+ ("new/dir/where-else/", Expected(mode=0o755, mtime=10)),
+ # FIXME: should be 10
+ (
+ "new/dir/where-else/some-dir-symlink1",
+ Expected(mtime=None, link_target="/abs/some-target1", has_fs_path=False),
+ ),
+ (
+ "new/dir/where-else/some-dir-symlink2",
+ Expected(mtime=None, link_target="../some-target2", has_fs_path=False),
+ ),
+ (
+ "new/dir/where-else/some-dir-symlink3",
+ Expected(mtime=None, link_target="some-target3", has_fs_path=False),
+ ),
+ ]
+ 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)
+
+
+def test_transformation_move_no_match(manifest_parser_pkg_foo):
+ content = textwrap.dedent(
+ """\
+ manifest-version: '0.1'
+ packages:
+ foo:
+ transformations:
+ - move:
+ source: some/non-existing-path
+ target: some/where-else
+ """
+ )
+ manifest = manifest_parser_pkg_foo.parse_manifest(fd=content)
+ claim_mtime_to = 255
+ paths = [
+ virtual_path_def("some/", mode=0o700, mtime=10, fs_path="/nowhere/some"),
+ virtual_path_def(
+ "some/empty/", mode=0o700, mtime=10, fs_path="/nowhere/some/empty"
+ ),
+ virtual_path_def(
+ "some/empty/directory/",
+ mode=0o755,
+ mtime=10,
+ fs_path="/nowhere/some/empty/directory",
+ ),
+ ]
+ fs_root = build_virtual_fs(paths, read_write_fs=True)
+ assert [p.name for p in manifest.all_packages] == ["foo"]
+
+ with pytest.raises(TransformationRuntimeError) as e_info:
+ 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'
+ " 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):
+ manifest = manifest_parser_pkg_foo.build_manifest()
+ claim_mtime_to = 255
+ sh_script_content = "#!/bin/sh"
+ python_script_content = "#! /usr/bin/python"
+ unrelated_content = "... random stuff ..."
+ paths = [
+ virtual_path_def("some/", mode=0o700, mtime=10, fs_path="/nowhere/some"),
+ virtual_path_def(
+ "some/dir/", mode=0o700, mtime=10, fs_path="/nowhere/some/empty"
+ ),
+ virtual_path_def(
+ "some/dir/script.sh",
+ mode=0o600,
+ mtime=10,
+ fs_path="/nowhere/script.sh",
+ content=sh_script_content,
+ ),
+ virtual_path_def(
+ "some/dir/script.py",
+ mode=0o600,
+ mtime=10,
+ fs_path="/nowhere/script.py",
+ content=python_script_content,
+ ),
+ virtual_path_def(
+ "some/dir/non-script-file",
+ mode=0o600,
+ mtime=10,
+ fs_path="/nowhere/non-script-file",
+ content=unrelated_content,
+ ),
+ ]
+ fs_root = build_virtual_fs(paths, read_write_fs=True)
+ assert [p.name for p in manifest.all_packages] == ["foo"]
+
+ expected_results = [
+ ("some/", Expected(mode=0o755, mtime=10)),
+ ("some/dir/", Expected(mode=0o755, mtime=10)),
+ ("some/dir/script.sh", Expected(mode=0o755, mtime=10)),
+ ("some/dir/script.py", Expected(mode=0o755, mtime=10)),
+ ("some/dir/non-script-file", 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
new file mode 100644
index 0000000..c8ffb84
--- /dev/null
+++ b/tests/test_install_rules.py
@@ -0,0 +1,1059 @@
+import textwrap
+
+import pytest
+
+from debputy.highlevel_manifest_parser import YAMLManifestParser
+from debputy.installations import (
+ InstallSearchDirContext,
+ NoMatchForInstallPatternError,
+ SearchDir,
+)
+from debputy.plugin.api import virtual_path_def
+from debputy.plugin.api.test_api import build_virtual_file_system
+
+
+@pytest.fixture()
+def manifest_parser_pkg_foo(
+ amd64_dpkg_architecture_variables,
+ dpkg_arch_query,
+ source_package,
+ package_single_foo_arch_all_cxt_amd64,
+ amd64_substitution,
+ no_profiles_or_build_options,
+ debputy_plugin_feature_set,
+) -> YAMLManifestParser:
+ # We need an empty directory to avoid triggering packager provided files.
+ debian_dir = build_virtual_file_system([])
+ return YAMLManifestParser(
+ "debian/test-debputy.manifest",
+ source_package,
+ package_single_foo_arch_all_cxt_amd64,
+ amd64_substitution,
+ amd64_dpkg_architecture_variables,
+ dpkg_arch_query,
+ no_profiles_or_build_options,
+ debputy_plugin_feature_set,
+ debian_dir=debian_dir,
+ )
+
+
+@pytest.fixture()
+def manifest_parser_pkg_foo_w_udeb(
+ amd64_dpkg_architecture_variables,
+ dpkg_arch_query,
+ source_package,
+ package_foo_w_udeb_arch_any_cxt_amd64,
+ amd64_substitution,
+ no_profiles_or_build_options,
+ debputy_plugin_feature_set,
+) -> YAMLManifestParser:
+ # We need an empty directory to avoid triggering packager provided files.
+ debian_dir = build_virtual_file_system([])
+ return YAMLManifestParser(
+ "debian/test-debputy.manifest",
+ source_package,
+ package_foo_w_udeb_arch_any_cxt_amd64,
+ amd64_substitution,
+ amd64_dpkg_architecture_variables,
+ dpkg_arch_query,
+ no_profiles_or_build_options,
+ debputy_plugin_feature_set,
+ debian_dir=debian_dir,
+ )
+
+
+def test_install_rules(manifest_parser_pkg_foo) -> None:
+ debian_source_root_dir = build_virtual_file_system(
+ [virtual_path_def(".", fs_path="/nowhere")]
+ )
+ debian_tmp_dir = build_virtual_file_system(
+ [
+ virtual_path_def(".", fs_path="/nowhere/debian/tmp"),
+ virtual_path_def("usr/", fs_path="/nowhere/debian/tmp/usr"),
+ virtual_path_def("usr/bin/", fs_path="/nowhere/debian/tmp/usr/bin"),
+ virtual_path_def(
+ "usr/bin/foo",
+ fs_path="/nowhere/debian/tmp/usr/bin/foo",
+ content="#!/bin/sh\n",
+ mtime=10,
+ ),
+ virtual_path_def(
+ "usr/bin/foo-util",
+ fs_path="/nowhere/debian/tmp/usr/bin/foo-util",
+ content="#!/bin/sh\n",
+ mtime=10,
+ ),
+ virtual_path_def(
+ "usr/bin/tool.sh",
+ fs_path="/nowhere/debian/tmp/usr/bin/tool.sh",
+ link_target="./foo",
+ ),
+ virtual_path_def("usr/share/", fs_path="/nowhere/debian/tmp/usr/share"),
+ virtual_path_def(
+ "usr/share/foo/", fs_path="/nowhere/debian/tmp/usr/share/foo"
+ ),
+ virtual_path_def(
+ "usr/share/foo/foo.txt",
+ fs_path="/nowhere/debian/tmp/usr/share/foo/foo.txt",
+ content="A text file",
+ ),
+ ]
+ )
+ manifest_content = textwrap.dedent(
+ """\
+ manifest-version: "0.1"
+ installations:
+ - install:
+ - /usr/share/foo
+ - /usr/bin/foo
+ - /usr/bin/foo-util
+ - install:
+ source: usr/bin/tool.sh
+ as: usr/bin/tool
+ """
+ )
+ manifest = manifest_parser_pkg_foo.parse_manifest(fd=manifest_content)
+ all_pkgs = frozenset(manifest.all_packages)
+
+ result = manifest.perform_installations(
+ 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
+
+ ub_dir = foo_fs_root.lookup("/usr/bin")
+ assert ub_dir is not None
+ assert ub_dir.is_dir
+ assert not ub_dir.has_fs_path # This will be "generated"
+
+ tool = ub_dir.get("tool")
+ assert tool is not None
+ assert tool.is_symlink
+ assert tool.readlink() == "./foo"
+
+ assert {"foo", "foo-util", "tool"} == {p.name for p in ub_dir.iterdir}
+ for n in ["foo", "foo-util"]:
+ assert ub_dir[n].mtime == 10
+ usf_dir = foo_fs_root.lookup("/usr/share/foo")
+ assert usf_dir is not None
+ assert usf_dir.is_dir
+ # Here we are installing an actual directory, so it should be present too
+ assert usf_dir.has_fs_path
+ assert usf_dir.fs_path == "/nowhere/debian/tmp/usr/share/foo"
+ assert {"foo.txt"} == {p.name for p in usf_dir.iterdir}
+
+
+def test_multi_dest_install_rules(manifest_parser_pkg_foo) -> None:
+ debian_source_root_dir = build_virtual_file_system(
+ [
+ virtual_path_def(".", fs_path="/nowhere"),
+ virtual_path_def("source/", fs_path="/nowhere/source"),
+ virtual_path_def("source/foo/", fs_path="/nowhere/foo"),
+ virtual_path_def(
+ "source/foo/foo-a.data",
+ fs_path="/nowhere/foo/foo-a.data",
+ content="data file",
+ ),
+ virtual_path_def(
+ "source/foo/foo-b.data",
+ fs_path="/nowhere/foo/foo-b.data",
+ link_target="./foo-a.data",
+ ),
+ virtual_path_def("source/bar/", fs_path="/nowhere/bar"),
+ virtual_path_def(
+ "source/bar/bar-a.data",
+ fs_path="/nowhere/bar/bar-a.data",
+ content="data file",
+ ),
+ virtual_path_def(
+ "source/bar/bar-b.data",
+ fs_path="/nowhere/bar/bar-b.data",
+ content="data file",
+ ),
+ virtual_path_def(
+ "source/tool.sh",
+ fs_path="/nowhere/source/tool.sh",
+ content="#!/bin/sh\n# run some command ...",
+ ),
+ ]
+ )
+ manifest_content = textwrap.dedent(
+ """\
+ manifest-version: "0.1"
+ installations:
+ - multi-dest-install:
+ sources:
+ - source/foo/*
+ - source/bar
+ dest-dirs:
+ - usr/share/foo
+ - usr/share/foo2
+ - multi-dest-install:
+ source: source/tool.sh
+ as:
+ - usr/share/foo/tool
+ - usr/share/foo2/tool
+ """
+ )
+ manifest = manifest_parser_pkg_foo.parse_manifest(fd=manifest_content)
+ all_pkgs = frozenset(manifest.all_packages)
+
+ result = manifest.perform_installations(
+ install_request_context=InstallSearchDirContext(
+ [
+ SearchDir(debian_source_root_dir, all_pkgs),
+ ],
+ [],
+ )
+ )
+ assert "foo" in result
+ foo_fs_root = result["foo"].fs_root
+
+ for stem in ["foo", "foo2"]:
+ foo_dir = foo_fs_root.lookup(f"/usr/share/{stem}")
+ assert foo_dir is not None
+ assert foo_dir.is_dir
+
+ assert {"foo-a.data", "foo-b.data", "bar", "tool"} == {
+ p.name for p in foo_dir.iterdir
+ }
+
+ tool = foo_dir["tool"]
+ assert tool.is_file
+ with tool.open() as fd:
+ content = fd.read()
+ assert content.startswith("#!/bin/sh")
+ foo_a = foo_dir["foo-a.data"]
+ assert foo_a.is_file
+ assert foo_a.fs_path == "/nowhere/foo/foo-a.data"
+ with foo_a.open() as fd:
+ content = fd.read()
+ assert "data" in content
+ foo_b = foo_dir["foo-b.data"]
+ assert foo_b.is_symlink
+ assert foo_b.readlink() == "./foo-a.data"
+
+ bar = foo_dir["bar"]
+ assert bar.is_dir
+ assert {"bar-a.data", "bar-b.data"} == {p.name for p in bar.iterdir}
+ assert {"/nowhere/bar/bar-a.data", "/nowhere/bar/bar-b.data"} == {
+ p.fs_path for p in bar.iterdir
+ }
+
+
+def test_install_rules_with_glob(manifest_parser_pkg_foo) -> None:
+ debian_source_root_dir = build_virtual_file_system(
+ [virtual_path_def(".", fs_path="/nowhere")]
+ )
+ debian_tmp_dir = build_virtual_file_system(
+ [
+ virtual_path_def(".", fs_path="/nowhere/debian/tmp"),
+ virtual_path_def("usr/", fs_path="/nowhere/debian/tmp/usr"),
+ virtual_path_def("usr/bin/", fs_path="/nowhere/debian/tmp/usr/bin"),
+ virtual_path_def(
+ "usr/bin/foo",
+ fs_path="/nowhere/debian/tmp/usr/bin/foo",
+ content="#!/bin/sh\n",
+ ),
+ virtual_path_def(
+ "usr/bin/foo-util",
+ fs_path="/nowhere/debian/tmp/usr/bin/foo-util",
+ content="#!/bin/sh\n",
+ ),
+ virtual_path_def(
+ "usr/bin/tool.sh",
+ fs_path="/nowhere/debian/tmp/usr/bin/tool.sh",
+ link_target="./foo",
+ ),
+ virtual_path_def("usr/share/", fs_path="/nowhere/debian/tmp/usr/share"),
+ virtual_path_def(
+ "usr/share/foo/", fs_path="/nowhere/debian/tmp/usr/share/foo"
+ ),
+ virtual_path_def(
+ "usr/share/foo/foo.txt",
+ fs_path="/nowhere/debian/tmp/usr/share/foo/foo.txt",
+ content="A text file",
+ ),
+ ]
+ )
+ manifest_content = textwrap.dedent(
+ """\
+ manifest-version: "0.1"
+ installations:
+ - install:
+ source: usr/bin/tool.sh
+ as: usr/bin/tool
+ - install:
+ - /usr/share/foo
+ - /usr/bin/foo*
+ """
+ )
+ manifest = manifest_parser_pkg_foo.parse_manifest(fd=manifest_content)
+ all_pkgs = frozenset(manifest.all_packages)
+
+ result = manifest.perform_installations(
+ 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
+
+ ub_dir = foo_fs_root.lookup("/usr/bin")
+ assert ub_dir is not None
+ assert ub_dir.is_dir
+ assert not ub_dir.has_fs_path # This will be "generated"
+
+ tool = ub_dir.get("tool")
+ assert tool is not None
+ assert tool.is_symlink
+ assert tool.readlink() == "./foo"
+
+ assert {"foo", "foo-util", "tool"} == {p.name for p in ub_dir.iterdir}
+ usf_dir = foo_fs_root.lookup("/usr/share/foo")
+ assert usf_dir is not None
+ assert usf_dir.is_dir
+ # Here we are installing an actual directory, so it should be present too
+ assert usf_dir.has_fs_path
+ assert usf_dir.fs_path == "/nowhere/debian/tmp/usr/share/foo"
+ assert {"foo.txt"} == {p.name for p in usf_dir.iterdir}
+
+
+def test_install_rules_auto_discard_rules_dir(manifest_parser_pkg_foo) -> None:
+ debian_source_root_dir = build_virtual_file_system(
+ [virtual_path_def(".", fs_path="/nowhere")]
+ )
+ debian_tmp_dir = build_virtual_file_system(
+ [
+ virtual_path_def(".", fs_path="/nowhere/debian/tmp"),
+ virtual_path_def("usr/", fs_path="/nowhere/debian/tmp/usr"),
+ virtual_path_def("usr/lib/", fs_path="/nowhere/debian/tmp/usr/lib"),
+ virtual_path_def(
+ "usr/lib/libfoo.so.1.0.0",
+ fs_path="/nowhere/debian/tmp/usr/lib/libfoo.so.1.0.0",
+ content="Not really an ELF FILE",
+ ),
+ virtual_path_def(
+ "usr/lib/libfoo.la",
+ fs_path="/nowhere/debian/tmp/usr/lib/libfoo.la",
+ content="Not really a LA FILE",
+ ),
+ ]
+ )
+ manifest_content = textwrap.dedent(
+ """\
+ manifest-version: "0.1"
+ installations:
+ - install:
+ - /usr/lib
+ """
+ )
+ manifest = manifest_parser_pkg_foo.parse_manifest(fd=manifest_content)
+
+ all_pkgs = frozenset(manifest.all_packages)
+
+ result = manifest.perform_installations(
+ 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
+
+ lib_dir = foo_fs_root.lookup("/usr/lib")
+ assert lib_dir is not None
+ assert lib_dir.is_dir
+ assert lib_dir.has_fs_path
+ assert lib_dir.fs_path == "/nowhere/debian/tmp/usr/lib"
+
+ so_file = lib_dir.get("libfoo.so.1.0.0")
+ assert so_file is not None
+ assert so_file.is_file
+ assert so_file.has_fs_path
+ assert so_file.fs_path == "/nowhere/debian/tmp/usr/lib/libfoo.so.1.0.0"
+
+ assert {"libfoo.so.1.0.0"} == {p.name for p in lib_dir.iterdir}
+
+
+def test_install_rules_auto_discard_rules_glob(manifest_parser_pkg_foo) -> None:
+ debian_source_root_dir = build_virtual_file_system(
+ [virtual_path_def(".", fs_path="/nowhere")]
+ )
+ debian_tmp_dir = build_virtual_file_system(
+ [
+ virtual_path_def(".", fs_path="/nowhere/debian/tmp"),
+ virtual_path_def("usr/", fs_path="/nowhere/debian/tmp/usr"),
+ virtual_path_def("usr/lib/", fs_path="/nowhere/debian/tmp/usr/lib"),
+ virtual_path_def(
+ "usr/lib/libfoo.so.1.0.0",
+ fs_path="/nowhere/debian/tmp/usr/lib/libfoo.so.1.0.0",
+ content="Not really an ELF FILE",
+ ),
+ virtual_path_def(
+ "usr/lib/libfoo.la",
+ fs_path="/nowhere/debian/tmp/usr/lib/libfoo.la",
+ content="Not really an ELF FILE",
+ ),
+ ]
+ )
+ manifest_content = textwrap.dedent(
+ """\
+ manifest-version: "0.1"
+ installations:
+ - install:
+ - /usr/lib/*
+ """
+ )
+ manifest = manifest_parser_pkg_foo.parse_manifest(fd=manifest_content)
+ all_pkgs = frozenset(manifest.all_packages)
+
+ result = manifest.perform_installations(
+ 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
+
+ lib_dir = foo_fs_root.lookup("/usr/lib")
+ assert lib_dir is not None
+ assert lib_dir.is_dir
+ assert not lib_dir.has_fs_path
+
+ so_file = lib_dir.get("libfoo.so.1.0.0")
+ assert so_file is not None
+ assert so_file.is_file
+ assert so_file.has_fs_path
+ assert so_file.fs_path == "/nowhere/debian/tmp/usr/lib/libfoo.so.1.0.0"
+
+ assert {"libfoo.so.1.0.0"} == {p.name for p in lib_dir.iterdir}
+
+
+def test_install_rules_auto_discard_rules_overruled_by_explicit_install_rule(
+ manifest_parser_pkg_foo,
+) -> None:
+ debian_source_root_dir = build_virtual_file_system(
+ [virtual_path_def(".", fs_path="/nowhere")]
+ )
+ debian_tmp_dir = build_virtual_file_system(
+ [
+ virtual_path_def(".", fs_path="/nowhere/debian/tmp"),
+ virtual_path_def("usr/", fs_path="/nowhere/debian/tmp/usr"),
+ virtual_path_def("usr/lib/", fs_path="/nowhere/debian/tmp/usr/lib"),
+ virtual_path_def(
+ "usr/lib/libfoo.so.1.0.0",
+ fs_path="/nowhere/debian/tmp/usr/lib/libfoo.so.1.0.0",
+ content="Not really an ELF FILE",
+ ),
+ virtual_path_def(
+ "usr/lib/libfoo.la",
+ fs_path="/nowhere/debian/tmp/usr/lib/libfoo.la",
+ content="Not really an ELF FILE",
+ ),
+ virtual_path_def(
+ "usr/lib/libfoo.so",
+ fs_path="/nowhere/debian/tmp/usr/lib/libfoo.so",
+ link_target="libfoo.so.1.0.0",
+ ),
+ ]
+ )
+ manifest_content = textwrap.dedent(
+ """\
+ manifest-version: "0.1"
+ installations:
+ - install:
+ - /usr/lib
+ - /usr/lib/libfoo.la
+ """
+ )
+ manifest = manifest_parser_pkg_foo.parse_manifest(fd=manifest_content)
+ all_pkgs = frozenset(manifest.all_packages)
+
+ result = manifest.perform_installations(
+ 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
+
+ lib_dir = foo_fs_root.lookup("/usr/lib")
+ assert lib_dir is not None
+ assert lib_dir.is_dir
+ assert lib_dir.has_fs_path
+ assert lib_dir.fs_path == "/nowhere/debian/tmp/usr/lib"
+
+ so_file = lib_dir.get("libfoo.so.1.0.0")
+ assert so_file is not None
+ assert so_file.is_file
+ assert so_file.has_fs_path
+ assert so_file.fs_path == "/nowhere/debian/tmp/usr/lib/libfoo.so.1.0.0"
+
+ la_file = lib_dir.get("libfoo.la")
+ assert la_file is not None
+ assert la_file.is_file
+ assert la_file.has_fs_path
+ assert la_file.fs_path == "/nowhere/debian/tmp/usr/lib/libfoo.la"
+
+ so_link = lib_dir.get("libfoo.so")
+ assert so_link is not None
+ assert so_link.is_symlink
+ assert so_link.readlink() == "libfoo.so.1.0.0"
+
+ assert {"libfoo.so.1.0.0", "libfoo.so", "libfoo.la"} == {
+ p.name for p in lib_dir.iterdir
+ }
+
+
+def test_install_rules_install_as_with_var(manifest_parser_pkg_foo) -> None:
+ debian_source_root_dir = build_virtual_file_system(
+ [
+ virtual_path_def(".", fs_path="/nowhere"),
+ virtual_path_def("build/", fs_path="/nowhere/build"),
+ virtual_path_def(
+ "build/private-arch-tool.sh",
+ fs_path="/nowhere/build/private-arch-tool.sh",
+ content="#!/bin/sh\n",
+ ),
+ ]
+ )
+ debian_tmp_dir = build_virtual_file_system(
+ [
+ virtual_path_def(".", fs_path="/nowhere/debian/tmp"),
+ ]
+ )
+ manifest_content = textwrap.dedent(
+ """\
+ manifest-version: "0.1"
+ installations:
+ - install:
+ source: build/private-arch-tool.sh
+ as: /usr/lib/{{DEB_HOST_MULTIARCH}}/foo/private-arch-tool
+ """
+ )
+ manifest = manifest_parser_pkg_foo.parse_manifest(fd=manifest_content)
+ all_pkgs = frozenset(manifest.all_packages)
+
+ result = manifest.perform_installations(
+ 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
+
+ # The variable is always resolved in amd64 context, so we can hard code the resolved
+ # variable
+ tool = foo_fs_root.lookup("/usr/lib/x86_64-linux-gnu/foo/private-arch-tool")
+ assert tool is not None
+ assert tool.is_file
+ assert tool.fs_path == "/nowhere/build/private-arch-tool.sh"
+
+
+def test_install_rules_no_matches(manifest_parser_pkg_foo) -> None:
+ debian_source_root_dir = build_virtual_file_system(
+ [
+ virtual_path_def(".", fs_path="/nowhere"),
+ virtual_path_def("build/", fs_path="/nowhere/build"),
+ virtual_path_def(
+ "build/private-arch-tool.sh",
+ fs_path="/nowhere/build/private-arch-tool.sh",
+ content="#!/bin/sh\n",
+ ),
+ ]
+ )
+ debian_tmp_dir = build_virtual_file_system(
+ [
+ virtual_path_def(".", fs_path="/nowhere/debian/tmp"),
+ ]
+ )
+ manifest_content = textwrap.dedent(
+ """\
+ manifest-version: "0.1"
+ installations:
+ - install:
+ # Typo: the path should have ended with ".sh"
+ source: build/private-arch-tool
+ as: /usr/lib/foo/private-arch-tool
+ """
+ )
+ manifest = manifest_parser_pkg_foo.parse_manifest(fd=manifest_content)
+ all_pkgs = frozenset(manifest.all_packages)
+
+ with pytest.raises(NoMatchForInstallPatternError) as e_info:
+ manifest.perform_installations(
+ 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>)."
+ " Match rule: ./build/private-arch-tool (the exact path / no globbing)"
+ )
+ assert e_info.value.message == expected_msg
+
+
+def test_install_rules_per_package_search_dirs(manifest_parser_pkg_foo_w_udeb) -> None:
+ debian_source_root_dir = build_virtual_file_system(
+ [virtual_path_def(".", fs_path="/nowhere")]
+ )
+ debian_tmp_deb_dir = build_virtual_file_system(
+ [
+ virtual_path_def(".", fs_path="/nowhere/debian/tmp-deb"),
+ virtual_path_def("usr/", fs_path="/nowhere/debian/tmp-deb/usr"),
+ virtual_path_def("usr/bin/", fs_path="/nowhere/debian/tmp-deb/usr/bin"),
+ virtual_path_def(
+ "usr/bin/foo",
+ fs_path="/nowhere/debian/tmp-deb/usr/bin/foo",
+ content="#!/bin/sh\ndeb",
+ ),
+ virtual_path_def(
+ "usr/bin/foo-util",
+ fs_path="/nowhere/debian/tmp-deb/usr/bin/foo-util",
+ content="#!/bin/sh\ndeb",
+ ),
+ virtual_path_def(
+ "usr/bin/tool.sh",
+ fs_path="/nowhere/debian/tmp-deb/usr/bin/tool.sh",
+ link_target="./foo",
+ ),
+ virtual_path_def("usr/share/", fs_path="/nowhere/debian/tmp-deb/usr/share"),
+ virtual_path_def(
+ "usr/share/foo/", fs_path="/nowhere/debian/tmp-deb/usr/share/foo"
+ ),
+ virtual_path_def(
+ "usr/share/foo/foo.txt",
+ fs_path="/nowhere/debian/tmp-deb/usr/share/foo/foo.txt",
+ content="A deb text file",
+ ),
+ ]
+ )
+ debian_tmp_udeb_dir = build_virtual_file_system(
+ [
+ virtual_path_def(".", fs_path="/nowhere/debian/tmp-udeb"),
+ virtual_path_def("usr/", fs_path="/nowhere/debian/tmp-udeb/usr"),
+ virtual_path_def("usr/bin/", fs_path="/nowhere/debian/tmp-udeb/usr/bin"),
+ virtual_path_def(
+ "usr/bin/foo",
+ fs_path="/nowhere/debian/tmp-udeb/usr/bin/foo",
+ content="#!/bin/sh\nudeb",
+ ),
+ virtual_path_def(
+ "usr/bin/foo-util",
+ fs_path="/nowhere/debian/tmp-udeb/usr/bin/foo-util",
+ content="#!/bin/sh\nudeb",
+ ),
+ virtual_path_def(
+ "usr/bin/tool.sh",
+ fs_path="/nowhere/debian/tmp-udeb/usr/bin/tool.sh",
+ link_target="./foo",
+ ),
+ virtual_path_def(
+ "usr/share/", fs_path="/nowhere/debian/tmp-udeb/usr/share"
+ ),
+ virtual_path_def(
+ "usr/share/foo/", fs_path="/nowhere/debian/tmp-udeb/usr/share/foo"
+ ),
+ virtual_path_def(
+ "usr/share/foo/foo.txt",
+ fs_path="/nowhere/debian/tmp-udeb/usr/share/foo/foo.txt",
+ content="A udeb text file",
+ ),
+ ]
+ )
+ manifest_content = textwrap.dedent(
+ """\
+ manifest-version: "0.1"
+ installations:
+ - install:
+ source: usr/bin/tool.sh
+ as: usr/bin/tool
+ into:
+ - foo
+ - foo-udeb
+ - install:
+ sources:
+ - /usr/share/foo
+ - /usr/bin/foo*
+ into:
+ - foo
+ - foo-udeb
+ """
+ )
+ manifest = manifest_parser_pkg_foo_w_udeb.parse_manifest(fd=manifest_content)
+ all_pkgs = frozenset(manifest.all_packages)
+ all_deb_pkgs = frozenset({p for p in all_pkgs if not p.is_udeb})
+ all_udeb_pkgs = frozenset({p for p in all_pkgs if p.is_udeb})
+
+ result = manifest.perform_installations(
+ install_request_context=InstallSearchDirContext(
+ [
+ SearchDir(debian_tmp_deb_dir, all_deb_pkgs),
+ SearchDir(debian_tmp_udeb_dir, all_udeb_pkgs),
+ SearchDir(debian_source_root_dir, all_pkgs),
+ ],
+ [debian_tmp_deb_dir],
+ )
+ )
+ for pkg, ptype in [
+ ("foo", "deb"),
+ ("foo-udeb", "udeb"),
+ ]:
+ assert pkg in result
+ fs_root = result[pkg].fs_root
+
+ ub_dir = fs_root.lookup("/usr/bin")
+ assert ub_dir is not None
+ assert ub_dir.is_dir
+ assert not ub_dir.has_fs_path # This will be "generated"
+
+ tool = ub_dir.get("tool")
+ assert tool is not None
+ assert tool.is_symlink
+ assert tool.readlink() == "./foo"
+
+ assert {"foo", "foo-util", "tool"} == {p.name for p in ub_dir.iterdir}
+
+ for p in ub_dir.iterdir:
+ assert p.has_fs_path
+ assert f"/nowhere/debian/tmp-{ptype}/usr/bin" in p.fs_path
+
+ usf_dir = fs_root.lookup("/usr/share/foo")
+ assert usf_dir is not None
+ assert usf_dir.is_dir
+ # Here we are installing an actual directory, so it should be present too
+ assert usf_dir.has_fs_path
+ assert usf_dir.fs_path == f"/nowhere/debian/tmp-{ptype}/usr/share/foo"
+ assert {"foo.txt"} == {p.name for p in usf_dir.iterdir}
+ foo_txt = usf_dir["foo.txt"]
+ assert foo_txt.fs_path == f"/nowhere/debian/tmp-{ptype}/usr/share/foo/foo.txt"
+
+
+def test_install_rules_multi_into(manifest_parser_pkg_foo_w_udeb) -> None:
+ debian_source_root_dir = build_virtual_file_system(
+ [
+ virtual_path_def(".", fs_path="/nowhere"),
+ virtual_path_def("source/", fs_path="/nowhere/source"),
+ virtual_path_def("source/foo/", fs_path="/nowhere/foo"),
+ virtual_path_def(
+ "source/foo/foo-a.data",
+ fs_path="/nowhere/foo/foo-a.data",
+ content="data file",
+ ),
+ virtual_path_def(
+ "source/foo/foo-b.data",
+ fs_path="/nowhere/foo/foo-b.data",
+ link_target="./foo-a.data",
+ ),
+ virtual_path_def("source/bar/", fs_path="/nowhere/bar"),
+ virtual_path_def(
+ "source/bar/bar-a.data",
+ fs_path="/nowhere/bar/bar-a.data",
+ content="data file",
+ ),
+ virtual_path_def(
+ "source/bar/bar-b.data",
+ fs_path="/nowhere/bar/bar-b.data",
+ content="data file",
+ ),
+ virtual_path_def(
+ "source/tool.sh",
+ fs_path="/nowhere/source/tool.sh",
+ content="#!/bin/sh\n# run some command ...",
+ ),
+ ]
+ )
+ manifest_content = textwrap.dedent(
+ """\
+ manifest-version: "0.1"
+ installations:
+ - install:
+ sources:
+ - source/foo/*
+ - source/bar
+ dest-dir: usr/share/foo
+ into:
+ - foo
+ - foo-udeb
+ - install:
+ source: source/tool.sh
+ as: usr/share/foo/tool
+ into:
+ - foo
+ - foo-udeb
+ """
+ )
+ manifest = manifest_parser_pkg_foo_w_udeb.parse_manifest(fd=manifest_content)
+ all_pkgs = frozenset(manifest.all_packages)
+
+ result = manifest.perform_installations(
+ install_request_context=InstallSearchDirContext(
+ [
+ SearchDir(debian_source_root_dir, all_pkgs),
+ ],
+ [],
+ )
+ )
+ for pkg in ["foo", "foo-udeb"]:
+ assert pkg in result
+ foo_fs_root = result[pkg].fs_root
+
+ foo_dir = foo_fs_root.lookup("/usr/share/foo")
+ assert foo_dir is not None
+ assert foo_dir.is_dir
+
+ assert {"foo-a.data", "foo-b.data", "bar", "tool"} == {
+ p.name for p in foo_dir.iterdir
+ }
+
+ tool = foo_dir["tool"]
+ assert tool.is_file
+ with tool.open() as fd:
+ content = fd.read()
+ assert content.startswith("#!/bin/sh")
+ foo_a = foo_dir["foo-a.data"]
+ assert foo_a.is_file
+ assert foo_a.fs_path == "/nowhere/foo/foo-a.data"
+ with foo_a.open() as fd:
+ content = fd.read()
+ assert "data" in content
+ foo_b = foo_dir["foo-b.data"]
+ assert foo_b.is_symlink
+ assert foo_b.readlink() == "./foo-a.data"
+
+ bar = foo_dir["bar"]
+ assert bar.is_dir
+ assert {"bar-a.data", "bar-b.data"} == {p.name for p in bar.iterdir}
+ assert {"/nowhere/bar/bar-a.data", "/nowhere/bar/bar-b.data"} == {
+ p.fs_path for p in bar.iterdir
+ }
+
+
+def test_auto_install_d_pkg(manifest_parser_pkg_foo_w_udeb) -> None:
+ debian_source_root_dir = build_virtual_file_system(
+ [virtual_path_def(".", fs_path="/nowhere")]
+ )
+ debian_foo_dir = build_virtual_file_system(
+ [
+ virtual_path_def(".", fs_path="/nowhere/debian/foo"),
+ virtual_path_def("usr/", fs_path="/nowhere/debian/foo/usr"),
+ virtual_path_def("usr/bin/", fs_path="/nowhere/debian/foo/usr/bin"),
+ virtual_path_def(
+ "usr/bin/foo",
+ fs_path="/nowhere/debian/foo/usr/bin/foo",
+ content="#!/bin/sh\ndeb",
+ ),
+ virtual_path_def(
+ "usr/bin/foo-util",
+ fs_path="/nowhere/debian/foo/usr/bin/foo-util",
+ content="#!/bin/sh\ndeb",
+ ),
+ virtual_path_def(
+ "usr/bin/tool",
+ fs_path="/nowhere/debian/foo/usr/bin/tool",
+ link_target="./foo",
+ ),
+ virtual_path_def("usr/share/", fs_path="/nowhere/debian/foo/usr/share"),
+ virtual_path_def(
+ "usr/share/foo/", fs_path="/nowhere/debian/foo/usr/share/foo"
+ ),
+ virtual_path_def(
+ "usr/share/foo/foo.txt",
+ fs_path="/nowhere/debian/foo/usr/share/foo/foo.txt",
+ content="A deb text file",
+ ),
+ ]
+ )
+ debian_foo_udeb_dir = build_virtual_file_system(
+ [
+ virtual_path_def(".", fs_path="/nowhere/debian/foo-udeb"),
+ virtual_path_def("usr/", fs_path="/nowhere/debian/foo-udeb/usr"),
+ virtual_path_def("usr/bin/", fs_path="/nowhere/debian/foo-udeb/usr/bin"),
+ virtual_path_def(
+ "usr/bin/foo",
+ fs_path="/nowhere/debian/foo-udeb/usr/bin/foo",
+ content="#!/bin/sh\nudeb",
+ ),
+ virtual_path_def(
+ "usr/bin/foo-util",
+ fs_path="/nowhere/debian/foo-udeb/usr/bin/foo-util",
+ content="#!/bin/sh\nudeb",
+ ),
+ virtual_path_def(
+ "usr/bin/tool",
+ fs_path="/nowhere/debian/foo-udeb/usr/bin/tool",
+ link_target="./foo",
+ ),
+ virtual_path_def(
+ "usr/share/", fs_path="/nowhere/debian/foo-udeb/usr/share"
+ ),
+ virtual_path_def(
+ "usr/share/foo/", fs_path="/nowhere/debian/foo-udeb/usr/share/foo"
+ ),
+ virtual_path_def(
+ "usr/share/foo/foo.txt",
+ fs_path="/nowhere/debian/foo-udeb/usr/share/foo/foo.txt",
+ content="A udeb text file",
+ ),
+ ]
+ )
+ manifest_content = textwrap.dedent(
+ """\
+ manifest-version: "0.1"
+ """
+ )
+ manifest = manifest_parser_pkg_foo_w_udeb.parse_manifest(fd=manifest_content)
+ all_pkgs = frozenset(manifest.all_packages)
+
+ result = manifest.perform_installations(
+ install_request_context=InstallSearchDirContext(
+ [
+ SearchDir(debian_source_root_dir, all_pkgs),
+ ],
+ [debian_foo_dir],
+ {
+ "foo": debian_foo_dir,
+ "foo-udeb": debian_foo_udeb_dir,
+ },
+ )
+ )
+ for pkg in ["foo", "foo-udeb"]:
+ assert pkg in result
+ fs_root = result[pkg].fs_root
+ ub_dir = fs_root.lookup("/usr/bin")
+ assert ub_dir is not None
+ assert ub_dir.is_dir
+ assert ub_dir.has_fs_path
+ assert ub_dir.fs_path == f"/nowhere/debian/{pkg}/usr/bin"
+
+ assert {"foo", "foo-util", "tool"} == {p.name for p in ub_dir.iterdir}
+
+ tool = ub_dir.get("tool")
+ assert tool is not None
+ assert tool.is_symlink
+ assert tool.readlink() == "./foo"
+
+ for p in ub_dir.iterdir:
+ assert p.has_fs_path
+ assert f"/nowhere/debian/{pkg}/usr/bin" in p.fs_path
+
+ usf_dir = fs_root.lookup("/usr/share/foo")
+ assert usf_dir is not None
+ assert usf_dir.is_dir
+ # Here we are installing an actual directory, so it should be present too
+ assert usf_dir.has_fs_path
+ assert usf_dir.fs_path == f"/nowhere/debian/{pkg}/usr/share/foo"
+ assert {"foo.txt"} == {p.name for p in usf_dir.iterdir}
+ foo_txt = usf_dir["foo.txt"]
+ assert foo_txt.fs_path == f"/nowhere/debian/{pkg}/usr/share/foo/foo.txt"
+
+
+def test_install_doc_rules_ignore_udeb(manifest_parser_pkg_foo_w_udeb) -> None:
+ debian_source_root_dir = build_virtual_file_system(
+ [
+ virtual_path_def(".", fs_path="/nowhere"),
+ virtual_path_def("source/", fs_path="/nowhere/source"),
+ virtual_path_def("source/foo/", fs_path="/nowhere/foo"),
+ virtual_path_def(
+ "source/foo/foo-a.txt",
+ fs_path="/nowhere/foo/foo-a.txt",
+ content="data file",
+ ),
+ virtual_path_def(
+ "source/foo/foo-b.txt",
+ fs_path="/nowhere/foo/foo-b.txt",
+ link_target="./foo-a.txt",
+ ),
+ virtual_path_def("source/html/", fs_path="/nowhere/html"),
+ virtual_path_def(
+ "source/html/bar-a.html",
+ fs_path="/nowhere/html/bar-a.html",
+ content="data file",
+ ),
+ virtual_path_def(
+ "source/html/bar-b.html",
+ fs_path="/nowhere/html/bar-b.html",
+ content="data file",
+ ),
+ ]
+ )
+ manifest_content = textwrap.dedent(
+ """\
+ manifest-version: "0.1"
+ installations:
+ - install-doc:
+ sources:
+ - source/foo/*
+ - source/html
+ """
+ )
+ manifest = manifest_parser_pkg_foo_w_udeb.parse_manifest(fd=manifest_content)
+ all_pkgs = frozenset(manifest.all_packages)
+
+ result = manifest.perform_installations(
+ install_request_context=InstallSearchDirContext(
+ [
+ SearchDir(debian_source_root_dir, all_pkgs),
+ ],
+ [],
+ )
+ )
+ assert "foo" in result
+ foo_fs_root = result["foo"].fs_root
+
+ foo_dir = foo_fs_root.lookup("/usr/share/doc/foo")
+ assert foo_dir is not None
+ assert foo_dir.is_dir
+
+ assert {"foo-a.txt", "foo-b.txt", "html"} == {p.name for p in foo_dir.iterdir}
+
+ foo_a = foo_dir["foo-a.txt"]
+ assert foo_a.is_file
+ assert foo_a.fs_path == "/nowhere/foo/foo-a.txt"
+ foo_b = foo_dir["foo-b.txt"]
+ assert foo_b.is_symlink
+ assert foo_b.readlink() == "./foo-a.txt"
+
+ html_dir = foo_dir["html"]
+ assert html_dir.is_dir
+ assert {"bar-a.html", "bar-b.html"} == {p.name for p in html_dir.iterdir}
+ assert {"/nowhere/html/bar-a.html", "/nowhere/html/bar-b.html"} == {
+ p.fs_path for p in html_dir.iterdir
+ }
+
+ assert "foo-udeb" in result
+ foo_udeb_fs_root = result["foo-udeb"].fs_root
+
+ udeb_doc_dir = foo_udeb_fs_root.lookup("/usr/share/doc")
+ assert udeb_doc_dir is None
diff --git a/tests/test_interpreter.py b/tests/test_interpreter.py
new file mode 100644
index 0000000..6cbfd44
--- /dev/null
+++ b/tests/test_interpreter.py
@@ -0,0 +1,162 @@
+import textwrap
+from typing import Optional
+
+import pytest
+
+from debputy.highlevel_manifest import HighLevelManifest
+from debputy.highlevel_manifest_parser import YAMLManifestParser
+from debputy.interpreter import extract_shebang_interpreter
+from debputy.plugin.api import virtual_path_def
+from debputy.plugin.api.test_api import build_virtual_file_system
+from debputy.transformation_rules import NormalizeShebangLineTransformation
+
+
+@pytest.mark.parametrize(
+ "raw_shebang,original_command,command_full_basename,command_stem,correct_command,corrected_shebang_line",
+ [
+ (
+ b"#! /usr/bin/false\r\n",
+ "/usr/bin/false",
+ "false",
+ "false",
+ None,
+ None,
+ ),
+ (
+ b"#!/usr/bin/python3 -b",
+ "/usr/bin/python3",
+ "python3",
+ "python",
+ "/usr/bin/python3",
+ None,
+ ),
+ (
+ b"#!/usr/bin/env python3 -b",
+ "/usr/bin/env python3",
+ "python3",
+ "python",
+ "/usr/bin/python3",
+ "#! /usr/bin/python3 -b",
+ ),
+ (
+ b"#! /bin/env python3.12-dbg -b",
+ "/bin/env python3.12-dbg",
+ "python3.12-dbg",
+ "python",
+ "/usr/bin/python3.12-dbg",
+ "#! /usr/bin/python3.12-dbg -b",
+ ),
+ (
+ b"#! /usr/bin/bash",
+ "/usr/bin/bash",
+ "bash",
+ "bash",
+ "/bin/bash",
+ "#! /bin/bash",
+ ),
+ (
+ b"#! /usr/bin/env sh",
+ "/usr/bin/env sh",
+ "sh",
+ "sh",
+ "/bin/sh",
+ "#! /bin/sh",
+ ),
+ (
+ b"#! /usr/local/bin/perl",
+ "/usr/local/bin/perl",
+ "perl",
+ "perl",
+ "/usr/bin/perl",
+ "#! /usr/bin/perl",
+ ),
+ ],
+)
+def test_interpreter_detection(
+ raw_shebang: bytes,
+ original_command: str,
+ command_full_basename: str,
+ command_stem: str,
+ correct_command: Optional[str],
+ corrected_shebang_line: Optional[str],
+) -> None:
+ interpreter = extract_shebang_interpreter(raw_shebang)
+ # The `and ...` part is just to get the raw line in the error message
+ assert interpreter is not None or raw_shebang == b""
+
+ assert interpreter.original_command == original_command
+ assert interpreter.command_full_basename == command_full_basename
+ assert interpreter.command_stem == command_stem
+ assert interpreter.correct_command == correct_command
+ assert interpreter.corrected_shebang_line == corrected_shebang_line
+ assert interpreter.fixup_needed == (corrected_shebang_line is not None)
+
+
+@pytest.mark.parametrize(
+ "raw_data",
+ [
+ b"#!#!#! /usr/bin/false",
+ b"#!perl", # Used in files as an editor hint
+ b"\x7FELF/usr/bin/perl",
+ b"\x00\01\x02\x03/usr/bin/perl",
+ b"PK\x03\x03/usr/bin/perl",
+ ],
+)
+def test_interpreter_negative(raw_data: bytes) -> None:
+ assert extract_shebang_interpreter(raw_data) is None
+
+
+@pytest.fixture
+def empty_manifest(
+ amd64_dpkg_architecture_variables,
+ dpkg_arch_query,
+ source_package,
+ package_single_foo_arch_all_cxt_amd64,
+ amd64_substitution,
+ no_profiles_or_build_options,
+ debputy_plugin_feature_set,
+) -> HighLevelManifest:
+ # We need an empty directory to avoid triggering packager provided files.
+ debian_dir = build_virtual_file_system([])
+ return YAMLManifestParser(
+ "debian/test-debputy.manifest",
+ source_package,
+ package_single_foo_arch_all_cxt_amd64,
+ amd64_substitution,
+ amd64_dpkg_architecture_variables,
+ dpkg_arch_query,
+ no_profiles_or_build_options,
+ debputy_plugin_feature_set,
+ debian_dir=debian_dir,
+ ).build_manifest()
+
+
+def test_interpreter_rewrite(empty_manifest: HighLevelManifest) -> None:
+ condition_context = empty_manifest.condition_context("foo")
+ fs_root = build_virtual_file_system(
+ [
+ virtual_path_def("usr/bin/foo", content="random data"),
+ virtual_path_def(
+ "usr/bin/foo.sh",
+ materialized_content="#!/usr/bin/sh\nset -e\n",
+ ),
+ ]
+ )
+ interpreter_normalization = NormalizeShebangLineTransformation()
+ interpreter_normalization.transform_file_system(fs_root, condition_context)
+ foo = fs_root.lookup("usr/bin/foo")
+ foo_sh = fs_root.lookup("usr/bin/foo.sh")
+
+ assert foo.is_file
+ with foo.open() as fd:
+ assert fd.read() == "random data"
+
+ assert foo_sh.is_file
+ with foo_sh.open() as fd:
+ expected = textwrap.dedent(
+ """\
+ #! /bin/sh
+ set -e
+ """
+ )
+ assert fd.read() == expected
diff --git a/tests/test_migrations.py b/tests/test_migrations.py
new file mode 100644
index 0000000..dc07d4c
--- /dev/null
+++ b/tests/test_migrations.py
@@ -0,0 +1,1767 @@
+import io
+import textwrap
+from typing import Iterable, Callable, Optional, List, Tuple, Sequence
+
+import pytest
+
+from debputy.dh_migration.migrators import Migrator
+from debputy.dh_migration.migrators_impl import (
+ migrate_tmpfile,
+ migrate_lintian_overrides_files,
+ detect_pam_files,
+ migrate_doc_base_files,
+ migrate_installexamples_file,
+ migrate_installdocs_file,
+ migrate_install_file,
+ migrate_maintscript,
+ migrate_links_files,
+ detect_dh_addons,
+ migrate_not_installed_file,
+ migrate_installman_file,
+ migrate_bash_completion,
+ migrate_installinfo_file,
+ migrate_dh_installsystemd_files,
+ detect_obsolete_substvars,
+ MIGRATION_TARGET_DH_DEBPUTY,
+ MIGRATION_TARGET_DH_DEBPUTY_RRR,
+ detect_dh_addons_zz_debputy_rrr,
+)
+from debputy.dh_migration.models import (
+ FeatureMigration,
+ AcceptableMigrationIssues,
+ UnsupportedFeature,
+)
+from debputy.highlevel_manifest import HighLevelManifest
+from debputy.highlevel_manifest_parser import YAMLManifestParser
+from debputy.plugin.api import virtual_path_def, VirtualPath
+from debputy.plugin.api.test_api import (
+ build_virtual_file_system,
+)
+
+
+DEBIAN_DIR_ENTRY = virtual_path_def(".", fs_path="/nowhere/debian")
+
+
+@pytest.fixture()
+def manifest_parser_pkg_foo_factory(
+ amd64_dpkg_architecture_variables,
+ dpkg_arch_query,
+ source_package,
+ package_single_foo_arch_all_cxt_amd64,
+ amd64_substitution,
+ no_profiles_or_build_options,
+ debputy_plugin_feature_set,
+) -> Callable[[], YAMLManifestParser]:
+ # We need an empty directory to avoid triggering packager provided files.
+ debian_dir = build_virtual_file_system([])
+
+ def _factory():
+ return YAMLManifestParser(
+ "debian/test-debputy.manifest",
+ source_package,
+ package_single_foo_arch_all_cxt_amd64,
+ amd64_substitution,
+ amd64_dpkg_architecture_variables,
+ dpkg_arch_query,
+ no_profiles_or_build_options,
+ debputy_plugin_feature_set,
+ debian_dir=debian_dir,
+ )
+
+ return _factory
+
+
+@pytest.fixture(scope="session")
+def accept_no_migration_issues() -> AcceptableMigrationIssues:
+ return AcceptableMigrationIssues(frozenset())
+
+
+@pytest.fixture(scope="session")
+def accept_any_migration_issues() -> AcceptableMigrationIssues:
+ return AcceptableMigrationIssues(frozenset(["ALL"]))
+
+
+@pytest.fixture
+def empty_manifest_pkg_foo(
+ manifest_parser_pkg_foo_factory: Callable[[], YAMLManifestParser],
+) -> HighLevelManifest:
+ return manifest_parser_pkg_foo_factory().build_manifest()
+
+
+def run_migrator(
+ migrator: Migrator,
+ path: VirtualPath,
+ manifest: HighLevelManifest,
+ acceptable_migration_issues: AcceptableMigrationIssues,
+ *,
+ migration_target=MIGRATION_TARGET_DH_DEBPUTY,
+) -> FeatureMigration:
+ feature_migration = FeatureMigration(migrator.__name__)
+ migrator(
+ path,
+ manifest,
+ acceptable_migration_issues,
+ feature_migration,
+ migration_target,
+ )
+ return feature_migration
+
+
+def _assert_unsupported_feature(
+ migrator: Migrator,
+ path: VirtualPath,
+ manifest: HighLevelManifest,
+ acceptable_migration_issues: AcceptableMigrationIssues,
+):
+ with pytest.raises(UnsupportedFeature) as e:
+ run_migrator(migrator, path, manifest, acceptable_migration_issues)
+ return e
+
+
+def _write_manifest(manifest: HighLevelManifest) -> str:
+ with io.StringIO() as fd:
+ manifest.mutable_manifest.write_to(fd)
+ return fd.getvalue()
+
+
+def _verify_migrator_generates_parsable_manifest(
+ migrator: Migrator,
+ parser_factory: Callable[[], YAMLManifestParser],
+ acceptable_migration_issues: AcceptableMigrationIssues,
+ dh_config_name: str,
+ dh_config_content: str,
+ expected_manifest_content: str,
+ expected_warnings: Optional[List[str]] = None,
+ expected_renamed_paths: Optional[List[Tuple[str, str]]] = None,
+ expected_removals: Optional[List[str]] = None,
+ required_plugins: Optional[Sequence[str]] = tuple(),
+ dh_config_mode: Optional[int] = None,
+) -> None:
+ # No file, no changes
+ empty_fs = build_virtual_file_system([DEBIAN_DIR_ENTRY])
+ migration = run_migrator(
+ migrator,
+ empty_fs,
+ parser_factory().build_manifest(),
+ acceptable_migration_issues,
+ )
+
+ assert not migration.anything_to_do
+ assert not migration.warnings
+ assert not migration.remove_paths_on_success
+ assert not migration.rename_paths_on_success
+ assert not migration.required_plugins
+
+ if dh_config_mode is None:
+ if dh_config_content.startswith(("#!/usr/bin/dh-exec", "#! /usr/bin/dh-exec")):
+ dh_config_mode = 0o755
+ else:
+ dh_config_mode = 0o644
+
+ # Test with a dh_config file now
+ fs_w_dh_config = build_virtual_file_system(
+ [
+ DEBIAN_DIR_ENTRY,
+ virtual_path_def(
+ dh_config_name,
+ fs_path=f"/nowhere/debian/{dh_config_name}",
+ content=dh_config_content,
+ mode=dh_config_mode,
+ ),
+ ]
+ )
+ manifest = parser_factory().build_manifest()
+
+ migration = run_migrator(
+ migrator,
+ fs_w_dh_config,
+ manifest,
+ acceptable_migration_issues,
+ )
+
+ assert migration.anything_to_do
+ if expected_warnings is not None:
+ assert migration.warnings == expected_warnings
+ else:
+ assert not migration.warnings
+ assert migration.remove_paths_on_success == [f"/nowhere/debian/{dh_config_name}"]
+ if expected_removals is None:
+ assert migration.remove_paths_on_success == [
+ f"/nowhere/debian/{dh_config_name}"
+ ]
+ else:
+ assert migration.remove_paths_on_success == expected_removals
+ if expected_renamed_paths is not None:
+ assert migration.rename_paths_on_success == expected_renamed_paths
+ else:
+ assert not migration.rename_paths_on_success
+ assert tuple(migration.required_plugins) == tuple(required_plugins)
+ actual_manifest = _write_manifest(manifest)
+ assert actual_manifest == expected_manifest_content
+
+ # Verify that the output is actually parsable
+ parser_factory().parse_manifest(fd=actual_manifest)
+
+
+def test_migrate_tmpfile(
+ empty_manifest_pkg_foo: HighLevelManifest,
+ accept_no_migration_issues: AcceptableMigrationIssues,
+) -> None:
+ migrator = migrate_tmpfile
+ empty_debian_dir = build_virtual_file_system([DEBIAN_DIR_ENTRY])
+ migration = run_migrator(
+ migrator,
+ empty_debian_dir,
+ empty_manifest_pkg_foo,
+ accept_no_migration_issues,
+ )
+
+ assert not migration.anything_to_do
+ assert not migration.warnings
+ assert not migration.remove_paths_on_success
+ assert not migration.rename_paths_on_success
+
+ tmpfile_debian_dir = build_virtual_file_system(
+ [
+ DEBIAN_DIR_ENTRY,
+ virtual_path_def("tmpfile", fs_path="/nowhere/debian/tmpfile"),
+ ]
+ )
+
+ migration = run_migrator(
+ migrator,
+ tmpfile_debian_dir,
+ empty_manifest_pkg_foo,
+ accept_no_migration_issues,
+ )
+
+ assert migration.anything_to_do
+ assert not migration.warnings
+ assert not migration.remove_paths_on_success
+ assert migration.rename_paths_on_success == [
+ ("/nowhere/debian/tmpfile", "/nowhere/debian/tmpfiles")
+ ]
+
+ tmpfile_debian_dir = build_virtual_file_system(
+ [
+ DEBIAN_DIR_ENTRY,
+ # Use real paths here to make `cmp -s` discover that they are the same
+ virtual_path_def("tmpfile", fs_path="debian/control"),
+ virtual_path_def("tmpfiles", fs_path="debian/control"),
+ ]
+ )
+
+ migration = run_migrator(
+ migrator,
+ tmpfile_debian_dir,
+ empty_manifest_pkg_foo,
+ accept_no_migration_issues,
+ )
+ assert migration.anything_to_do
+ assert not migration.warnings
+ assert migration.remove_paths_on_success == ["debian/control"]
+ assert not migration.rename_paths_on_success
+
+ conflict_tmpfile_debian_dir = build_virtual_file_system(
+ [
+ DEBIAN_DIR_ENTRY,
+ # Use real paths here to make `cmp -s` discover a difference
+ virtual_path_def("tmpfile", fs_path="debian/control"),
+ virtual_path_def("tmpfiles", fs_path="debian/changelog"),
+ ]
+ )
+
+ migration = run_migrator(
+ migrator,
+ conflict_tmpfile_debian_dir,
+ empty_manifest_pkg_foo,
+ accept_no_migration_issues,
+ )
+
+ assert len(migration.warnings) == 1
+ assert not migration.remove_paths_on_success
+ assert not migration.rename_paths_on_success
+
+ conflict_tmpfile_debian_dir = build_virtual_file_system(
+ [
+ DEBIAN_DIR_ENTRY,
+ virtual_path_def("tmpfile", fs_path="/nowhere/debian/tmpfile"),
+ virtual_path_def("tmpfiles/", fs_path="/nowhere/debian/tmpfiles"),
+ ]
+ )
+
+ migration = run_migrator(
+ migrator,
+ conflict_tmpfile_debian_dir,
+ empty_manifest_pkg_foo,
+ accept_no_migration_issues,
+ )
+
+ assert len(migration.warnings) == 1
+ assert not migration.remove_paths_on_success
+ assert not migration.rename_paths_on_success
+
+ conflict_tmpfile_debian_dir = build_virtual_file_system(
+ [
+ DEBIAN_DIR_ENTRY,
+ virtual_path_def(
+ "tmpfile",
+ link_target="/nowhere/debian/tmpfiles",
+ fs_path="/nowhere/debian/tmpfile",
+ ),
+ ]
+ )
+
+ migration = run_migrator(
+ migrator,
+ conflict_tmpfile_debian_dir,
+ empty_manifest_pkg_foo,
+ accept_no_migration_issues,
+ )
+
+ assert len(migration.warnings) == 1
+ assert not migration.remove_paths_on_success
+ assert not migration.rename_paths_on_success
+
+
+def test_migrate_lintian_overrides_files(
+ empty_manifest_pkg_foo: HighLevelManifest,
+ accept_no_migration_issues: AcceptableMigrationIssues,
+ accept_any_migration_issues: AcceptableMigrationIssues,
+) -> None:
+ migrator = migrate_lintian_overrides_files
+ no_override_fs = build_virtual_file_system([DEBIAN_DIR_ENTRY])
+ single_noexec_override_fs = build_virtual_file_system(
+ [
+ DEBIAN_DIR_ENTRY,
+ virtual_path_def(
+ "foo.lintian-overrides",
+ fs_path="/nowhere/no-exec/debian/foo.lintian-overrides",
+ ),
+ ]
+ )
+ single_exec_override_fs = build_virtual_file_system(
+ [
+ DEBIAN_DIR_ENTRY,
+ virtual_path_def(
+ "foo.lintian-overrides",
+ fs_path="/nowhere/exec/debian/foo.lintian-overrides",
+ mode=0o755,
+ ),
+ ]
+ )
+ for no_issue_fs in [no_override_fs, single_noexec_override_fs]:
+ migration = run_migrator(
+ migrator,
+ no_issue_fs,
+ empty_manifest_pkg_foo,
+ accept_no_migration_issues,
+ )
+
+ assert not migration.anything_to_do
+ assert not migration.warnings
+ assert not migration.remove_paths_on_success
+ assert not migration.rename_paths_on_success
+
+ _assert_unsupported_feature(
+ migrator,
+ single_exec_override_fs,
+ empty_manifest_pkg_foo,
+ accept_no_migration_issues,
+ )
+
+ migration = run_migrator(
+ migrator,
+ single_exec_override_fs,
+ empty_manifest_pkg_foo,
+ accept_any_migration_issues,
+ )
+
+ assert migration.anything_to_do
+ assert len(migration.warnings) == 1
+ assert not migration.remove_paths_on_success
+ assert not migration.rename_paths_on_success
+
+
+def test_detect_pam_files(
+ empty_manifest_pkg_foo: HighLevelManifest,
+ accept_no_migration_issues: AcceptableMigrationIssues,
+) -> None:
+ migrator = detect_pam_files
+ empty_fs = build_virtual_file_system([DEBIAN_DIR_ENTRY])
+ pam_fs = build_virtual_file_system(
+ [
+ DEBIAN_DIR_ENTRY,
+ virtual_path_def("pam", fs_path="/nowhere/debian/foo.pam"),
+ ]
+ )
+
+ migration = run_migrator(
+ migrator,
+ empty_fs,
+ empty_manifest_pkg_foo,
+ accept_no_migration_issues,
+ )
+
+ assert not migration.anything_to_do
+ assert not migration.warnings
+ assert migration.assumed_compat is None
+ assert not migration.remove_paths_on_success
+ assert not migration.rename_paths_on_success
+
+ migration = run_migrator(
+ migrator,
+ pam_fs,
+ empty_manifest_pkg_foo,
+ accept_no_migration_issues,
+ )
+
+ assert not migration.anything_to_do
+ assert not migration.warnings
+ assert migration.assumed_compat == 14
+ assert not migration.remove_paths_on_success
+ assert not migration.rename_paths_on_success
+
+
+def test_migrate_doc_base_files(
+ empty_manifest_pkg_foo: HighLevelManifest,
+ accept_no_migration_issues: AcceptableMigrationIssues,
+) -> None:
+ migrator = migrate_doc_base_files
+ empty_fs = build_virtual_file_system([DEBIAN_DIR_ENTRY])
+ doc_base_ok_fs = build_virtual_file_system(
+ [
+ DEBIAN_DIR_ENTRY,
+ virtual_path_def("doc-base", fs_path="/nowhere/debian/doc-base"),
+ virtual_path_def(
+ "foo.doc-base.EX", fs_path="/nowhere/debian/foo.doc-base.EX"
+ ),
+ ]
+ )
+ doc_base_migration_fs = build_virtual_file_system(
+ [
+ DEBIAN_DIR_ENTRY,
+ virtual_path_def(
+ "foo.doc-base.bar", fs_path="/nowhere/debian/foo.doc-base.bar"
+ ),
+ ]
+ )
+
+ for no_change_fs in [empty_fs, doc_base_ok_fs]:
+ migration = run_migrator(
+ migrator,
+ no_change_fs,
+ empty_manifest_pkg_foo,
+ accept_no_migration_issues,
+ )
+
+ assert not migration.anything_to_do
+ assert not migration.warnings
+ assert not migration.remove_paths_on_success
+ assert not migration.rename_paths_on_success
+
+ migration = run_migrator(
+ migrator,
+ doc_base_migration_fs,
+ empty_manifest_pkg_foo,
+ accept_no_migration_issues,
+ )
+
+ assert migration.anything_to_do
+ assert not migration.warnings
+ assert not migration.remove_paths_on_success
+ assert migration.rename_paths_on_success == [
+ ("/nowhere/debian/foo.doc-base.bar", "/nowhere/debian/foo.bar.doc-base")
+ ]
+
+
+def test_migrate_dh_installsystemd_files(
+ empty_manifest_pkg_foo: HighLevelManifest,
+ accept_no_migration_issues: AcceptableMigrationIssues,
+) -> None:
+ migrator = migrate_dh_installsystemd_files
+ empty_fs = build_virtual_file_system([DEBIAN_DIR_ENTRY])
+ files_ok_fs = build_virtual_file_system(
+ [
+ DEBIAN_DIR_ENTRY,
+ virtual_path_def("@service", fs_path="/nowhere/debian/@service"),
+ virtual_path_def("foo.@service", fs_path="/nowhere/debian/foo.@service"),
+ ]
+ )
+ migration_fs = build_virtual_file_system(
+ [
+ DEBIAN_DIR_ENTRY,
+ virtual_path_def("foo@.service", fs_path="/nowhere/debian/foo@.service"),
+ ]
+ )
+
+ for no_change_fs in [empty_fs, files_ok_fs]:
+ migration = run_migrator(
+ migrator,
+ no_change_fs,
+ empty_manifest_pkg_foo,
+ accept_no_migration_issues,
+ )
+
+ assert not migration.anything_to_do
+ assert not migration.warnings
+ assert not migration.remove_paths_on_success
+ assert not migration.rename_paths_on_success
+
+ migration = run_migrator(
+ migrator,
+ migration_fs,
+ empty_manifest_pkg_foo,
+ accept_no_migration_issues,
+ )
+
+ assert migration.anything_to_do
+ assert not migration.warnings
+ assert not migration.remove_paths_on_success
+ assert migration.rename_paths_on_success == [
+ ("/nowhere/debian/foo@.service", "/nowhere/debian/foo.@service")
+ ]
+
+
+def test_migrate_installexamples_file(
+ manifest_parser_pkg_foo_factory: Callable[[], YAMLManifestParser],
+ accept_no_migration_issues: AcceptableMigrationIssues,
+) -> None:
+ dh_config_content = textwrap.dedent(
+ """\
+ foo/*
+ bar
+ """
+ )
+ expected_manifest_content = textwrap.dedent(
+ """\
+ manifest-version: '0.1'
+ installations:
+ - install-examples:
+ sources:
+ - foo/*
+ - bar
+ """
+ )
+ _verify_migrator_generates_parsable_manifest(
+ migrate_installexamples_file,
+ manifest_parser_pkg_foo_factory,
+ accept_no_migration_issues,
+ "examples",
+ dh_config_content,
+ expected_manifest_content,
+ )
+
+
+def test_migrate_installinfo_file(
+ manifest_parser_pkg_foo_factory: Callable[[], YAMLManifestParser],
+ accept_no_migration_issues: AcceptableMigrationIssues,
+) -> None:
+ dh_config_content = textwrap.dedent(
+ """\
+ foo/*
+ bar
+ """
+ )
+ expected_manifest_content = textwrap.dedent(
+ """\
+ manifest-version: '0.1'
+ installations:
+ - install-docs:
+ sources:
+ - foo/*
+ - bar
+ dest-dir: '{{path:GNU_INFO_DIR}}'
+ """
+ )
+ _verify_migrator_generates_parsable_manifest(
+ migrate_installinfo_file,
+ manifest_parser_pkg_foo_factory,
+ accept_no_migration_issues,
+ "info",
+ dh_config_content,
+ expected_manifest_content,
+ )
+
+
+def test_migrate_installinfo_file_conditionals(
+ manifest_parser_pkg_foo_factory: Callable[[], YAMLManifestParser],
+ accept_no_migration_issues: AcceptableMigrationIssues,
+) -> None:
+ dh_config_content = textwrap.dedent(
+ """\
+ #!/usr/bin/dh-exec
+ foo/* <!pkg.foo.noinfo>
+ bar <!pkg.foo.noinfo>
+ """
+ )
+ expected_manifest_content = textwrap.dedent(
+ """\
+ manifest-version: '0.1'
+ installations:
+ - install-docs:
+ sources:
+ - foo/*
+ - bar
+ dest-dir: '{{path:GNU_INFO_DIR}}'
+ when:
+ build-profiles-matches: <!pkg.foo.noinfo>
+ """
+ )
+ _verify_migrator_generates_parsable_manifest(
+ migrate_installinfo_file,
+ manifest_parser_pkg_foo_factory,
+ accept_no_migration_issues,
+ "info",
+ dh_config_content,
+ expected_manifest_content,
+ )
+
+
+def test_migrate_installexamples_file_single_source(
+ manifest_parser_pkg_foo_factory: Callable[[], YAMLManifestParser],
+ accept_no_migration_issues: AcceptableMigrationIssues,
+) -> None:
+ dh_config_content = textwrap.dedent(
+ """\
+ foo/*
+ """
+ )
+ expected_manifest_content = textwrap.dedent(
+ """\
+ manifest-version: '0.1'
+ installations:
+ - install-examples:
+ source: foo/*
+ """
+ )
+ _verify_migrator_generates_parsable_manifest(
+ migrate_installexamples_file,
+ manifest_parser_pkg_foo_factory,
+ accept_no_migration_issues,
+ "examples",
+ dh_config_content,
+ expected_manifest_content,
+ )
+
+
+def test_migrate_installdocs_file(
+ manifest_parser_pkg_foo_factory: Callable[[], YAMLManifestParser],
+ accept_no_migration_issues: AcceptableMigrationIssues,
+) -> None:
+ dh_config_content = textwrap.dedent(
+ """\
+ foo/*
+ bar
+ """
+ )
+ expected_manifest_content = textwrap.dedent(
+ """\
+ manifest-version: '0.1'
+ installations:
+ - install-docs:
+ sources:
+ - foo/*
+ - bar
+ """
+ )
+ _verify_migrator_generates_parsable_manifest(
+ migrate_installdocs_file,
+ manifest_parser_pkg_foo_factory,
+ accept_no_migration_issues,
+ "docs",
+ dh_config_content,
+ expected_manifest_content,
+ )
+
+
+def test_migrate_installdocs_file_single_source(
+ manifest_parser_pkg_foo_factory: Callable[[], YAMLManifestParser],
+ accept_no_migration_issues: AcceptableMigrationIssues,
+) -> None:
+ dh_config_content = textwrap.dedent(
+ """\
+ foo/*
+ """
+ )
+ expected_manifest_content = textwrap.dedent(
+ """\
+ manifest-version: '0.1'
+ installations:
+ - install-docs:
+ source: foo/*
+ """
+ )
+ _verify_migrator_generates_parsable_manifest(
+ migrate_installdocs_file,
+ manifest_parser_pkg_foo_factory,
+ accept_no_migration_issues,
+ "docs",
+ dh_config_content,
+ expected_manifest_content,
+ )
+
+
+def test_migrate_install_file(
+ manifest_parser_pkg_foo_factory: Callable[[], YAMLManifestParser],
+ accept_no_migration_issues: AcceptableMigrationIssues,
+) -> None:
+ dh_config_content = textwrap.dedent(
+ """\
+ bar usr/bin
+ """
+ )
+ expected_manifest_content = textwrap.dedent(
+ """\
+ manifest-version: '0.1'
+ installations:
+ - install:
+ source: bar
+ dest-dir: usr/bin
+ """
+ )
+ _verify_migrator_generates_parsable_manifest(
+ migrate_install_file,
+ manifest_parser_pkg_foo_factory,
+ accept_no_migration_issues,
+ "install",
+ dh_config_content,
+ expected_manifest_content,
+ )
+
+
+def test_migrate_install_file_conditionals_simple_arch(
+ manifest_parser_pkg_foo_factory: Callable[[], YAMLManifestParser],
+ accept_no_migration_issues: AcceptableMigrationIssues,
+) -> None:
+ dh_config_content = textwrap.dedent(
+ """\
+ #!/usr/bin/dh-exec
+ bar usr/bin [linux-any]
+ foo usr/bin [linux-any]
+ """
+ )
+ expected_manifest_content = textwrap.dedent(
+ """\
+ manifest-version: '0.1'
+ installations:
+ - install:
+ sources:
+ - bar
+ - foo
+ dest-dir: usr/bin
+ when:
+ arch-matches: linux-any
+ """
+ )
+ _verify_migrator_generates_parsable_manifest(
+ migrate_install_file,
+ manifest_parser_pkg_foo_factory,
+ accept_no_migration_issues,
+ "install",
+ dh_config_content,
+ expected_manifest_content,
+ )
+
+
+def test_migrate_install_file_util_linux_locales(
+ manifest_parser_pkg_foo_factory: Callable[[], YAMLManifestParser],
+ accept_no_migration_issues: AcceptableMigrationIssues,
+) -> None:
+ # Parts of the `d/util-linux-locales.install` file. It uses d/tmp for most of its paths
+ # and that breaks the default dest-dir (dh_install always strips --sourcedir, `debputy`
+ # currently does not)
+ dh_config_content = textwrap.dedent(
+ """\
+ #!/usr/bin/dh-exec
+ usr/share/locale/*/*/util-linux.mo
+
+ # bsdextrautils
+ debian/tmp/usr/share/man/*/man1/col.1 <!nodoc>
+
+ debian/tmp/usr/share/man/*/man3/libblkid.3 <!nodoc>
+ """
+ )
+ expected_manifest_content = textwrap.dedent(
+ """\
+ manifest-version: '0.1'
+ installations:
+ - install:
+ sources:
+ - usr/share/man/*/man1/col.1
+ - usr/share/man/*/man3/libblkid.3
+ when:
+ build-profiles-matches: <!nodoc>
+ - install:
+ source: usr/share/locale/*/*/util-linux.mo
+ """
+ )
+ _verify_migrator_generates_parsable_manifest(
+ migrate_install_file,
+ manifest_parser_pkg_foo_factory,
+ accept_no_migration_issues,
+ "install",
+ dh_config_content,
+ expected_manifest_content,
+ )
+
+
+def test_migrate_install_file_conditionals_simple_combined_cond(
+ manifest_parser_pkg_foo_factory: Callable[[], YAMLManifestParser],
+ accept_no_migration_issues: AcceptableMigrationIssues,
+) -> None:
+ for cond in ["<!foo> <!bar> [linux-any]", "[linux-any] <!foo> <!bar>"]:
+ dh_config_content = textwrap.dedent(
+ """\
+ #!/usr/bin/dh-exec
+ bar usr/bin {CONDITION}
+ foo usr/bin {CONDITION}
+ """
+ ).format(CONDITION=cond)
+ expected_manifest_content = textwrap.dedent(
+ """\
+ manifest-version: '0.1'
+ installations:
+ - install:
+ sources:
+ - bar
+ - foo
+ dest-dir: usr/bin
+ when:
+ all-of:
+ - arch-matches: linux-any
+ - build-profiles-matches: <!foo> <!bar>
+ """
+ )
+ _verify_migrator_generates_parsable_manifest(
+ migrate_install_file,
+ manifest_parser_pkg_foo_factory,
+ accept_no_migration_issues,
+ "install",
+ dh_config_content,
+ expected_manifest_content,
+ )
+
+
+def test_migrate_install_file_conditionals_unknown_subst(
+ manifest_parser_pkg_foo_factory: Callable[[], YAMLManifestParser],
+ accept_any_migration_issues: AcceptableMigrationIssues,
+) -> None:
+ dh_config_content = textwrap.dedent(
+ """\
+ #!/usr/bin/dh-exec
+ bar ${unknown_substvar}
+ """
+ )
+ expected_manifest_content = textwrap.dedent(
+ """\
+ manifest-version: '0.1'
+ definitions:
+ variables:
+ unknown_substvar: 'TODO: Provide variable value for unknown_substvar'
+ installations:
+ - install:
+ source: bar
+ dest-dir: '{{unknown_substvar}}'
+ """
+ )
+ expected_warning = (
+ "TODO: MANUAL MIGRATION of unresolved substitution variable {{unknown_substvar}}"
+ ' from ./install line 2 token "${unknown_substvar}"'
+ )
+ _verify_migrator_generates_parsable_manifest(
+ migrate_install_file,
+ manifest_parser_pkg_foo_factory,
+ accept_any_migration_issues,
+ "install",
+ dh_config_content,
+ expected_manifest_content,
+ expected_warnings=[expected_warning],
+ )
+
+
+def test_migrate_install_file_multidest(
+ manifest_parser_pkg_foo_factory: Callable[[], YAMLManifestParser],
+ accept_no_migration_issues: AcceptableMigrationIssues,
+) -> None:
+ dh_config_content = textwrap.dedent(
+ """\
+ # Issue #66
+ # - observed in kafs-client / the original install file copied in here.
+
+ src/aklog-kafs usr/bin
+ src/kafs-check-config usr/bin
+ #
+ src/kafs-preload usr/sbin
+ #
+ src/kafs-dns usr/libexec
+ #
+ conf/cellservdb.conf usr/share/kafs-client
+ conf/client.conf etc/kafs
+ #
+ conf/kafs_dns.conf etc/request-key.d
+ #
+ conf/cellservdb.conf usr/share/kafs
+ """
+ )
+ expected_manifest_content = textwrap.dedent(
+ """\
+ manifest-version: '0.1'
+ installations:
+ - install:
+ sources:
+ - src/aklog-kafs
+ - src/kafs-check-config
+ dest-dir: usr/bin
+ - install:
+ source: src/kafs-preload
+ dest-dir: usr/sbin
+ - install:
+ source: src/kafs-dns
+ dest-dir: usr/libexec
+ - install:
+ source: conf/client.conf
+ dest-dir: etc/kafs
+ - install:
+ source: conf/kafs_dns.conf
+ dest-dir: etc/request-key.d
+ - multi-dest-install:
+ source: conf/cellservdb.conf
+ dest-dirs:
+ - usr/share/kafs-client
+ - usr/share/kafs
+ """
+ )
+ _verify_migrator_generates_parsable_manifest(
+ migrate_install_file,
+ manifest_parser_pkg_foo_factory,
+ accept_no_migration_issues,
+ "install",
+ dh_config_content,
+ expected_manifest_content,
+ )
+
+
+def test_migrate_install_file_multidest_default_dest(
+ manifest_parser_pkg_foo_factory: Callable[[], YAMLManifestParser],
+ accept_no_migration_issues: AcceptableMigrationIssues,
+) -> None:
+ dh_config_content = textwrap.dedent(
+ """\
+ # Relaed to issue #66 - testing corner case not present in the original install file
+
+ src/aklog-kafs usr/bin
+ src/kafs-check-config usr/bin
+ #
+ src/kafs-preload usr/sbin
+ #
+ src/kafs-dns usr/libexec
+ #
+ usr/share/kafs-client/cellservdb.conf
+ conf/client.conf etc/kafs
+ #
+ conf/kafs_dns.conf etc/request-key.d
+ #
+ usr/share/kafs-client/cellservdb.conf usr/share/kafs
+ """
+ )
+ expected_manifest_content = textwrap.dedent(
+ """\
+ manifest-version: '0.1'
+ installations:
+ - install:
+ sources:
+ - src/aklog-kafs
+ - src/kafs-check-config
+ dest-dir: usr/bin
+ - install:
+ source: src/kafs-preload
+ dest-dir: usr/sbin
+ - install:
+ source: src/kafs-dns
+ dest-dir: usr/libexec
+ - install:
+ source: conf/client.conf
+ dest-dir: etc/kafs
+ - install:
+ source: conf/kafs_dns.conf
+ dest-dir: etc/request-key.d
+ - multi-dest-install:
+ source: usr/share/kafs-client/cellservdb.conf
+ dest-dirs:
+ - usr/share/kafs
+ - usr/share/kafs-client
+ """
+ )
+ _verify_migrator_generates_parsable_manifest(
+ migrate_install_file,
+ manifest_parser_pkg_foo_factory,
+ accept_no_migration_issues,
+ "install",
+ dh_config_content,
+ expected_manifest_content,
+ )
+
+
+def test_migrate_install_file_multidest_default_dest_warning(
+ manifest_parser_pkg_foo_factory: Callable[[], YAMLManifestParser],
+ accept_no_migration_issues: AcceptableMigrationIssues,
+) -> None:
+ dh_config_content = textwrap.dedent(
+ """\
+ # Relaed to issue #66 - testing corner case not present in the original install file
+
+ src/aklog-kafs usr/bin
+ src/kafs-check-config usr/bin
+ #
+ src/kafs-preload usr/sbin
+ #
+ src/kafs-dns usr/libexec
+ #
+ usr/share/kafs-*/cellservdb.conf
+ conf/client.conf etc/kafs
+ #
+ conf/kafs_dns.conf etc/request-key.d
+ #
+ usr/share/kafs-*/cellservdb.conf usr/share/kafs
+ """
+ )
+ expected_manifest_content = textwrap.dedent(
+ """\
+ manifest-version: '0.1'
+ installations:
+ - install:
+ sources:
+ - src/aklog-kafs
+ - src/kafs-check-config
+ dest-dir: usr/bin
+ - install:
+ source: src/kafs-preload
+ dest-dir: usr/sbin
+ - install:
+ source: src/kafs-dns
+ dest-dir: usr/libexec
+ - install:
+ source: conf/client.conf
+ dest-dir: etc/kafs
+ - install:
+ source: conf/kafs_dns.conf
+ dest-dir: etc/request-key.d
+ - multi-dest-install:
+ source: usr/share/kafs-*/cellservdb.conf
+ dest-dirs:
+ - usr/share/kafs
+ - 'FIXME: usr/share/kafs-* (could not reliably compute the dest dir)'
+ """
+ )
+ warnings = [
+ "TODO: FIXME left in dest-dir(s) of some installation rules."
+ " Please review these and remove the FIXME (plus correct as necessary)"
+ ]
+ _verify_migrator_generates_parsable_manifest(
+ migrate_install_file,
+ manifest_parser_pkg_foo_factory,
+ accept_no_migration_issues,
+ "install",
+ dh_config_content,
+ expected_manifest_content,
+ expected_warnings=warnings,
+ )
+
+
+def test_migrate_installman_file(
+ manifest_parser_pkg_foo_factory: Callable[[], YAMLManifestParser],
+ accept_no_migration_issues: AcceptableMigrationIssues,
+) -> None:
+ dh_config_content = textwrap.dedent(
+ """\
+ man/foo.1 man/bar.1
+ man2/*.2
+ man3/bar.3 man3/bar.de.3
+ man/de/man3/bar.pl.3
+ """
+ )
+ expected_manifest_content = textwrap.dedent(
+ """\
+ manifest-version: '0.1'
+ installations:
+ - install-man:
+ sources:
+ - man/foo.1
+ - man/bar.1
+ - man2/*.2
+ - man/de/man3/bar.pl.3
+ - install-man:
+ sources:
+ - man3/bar.3
+ - man3/bar.de.3
+ language: derive-from-basename
+ """
+ )
+ expected_warnings = [
+ 'Detected manpages that might rely on "derive-from-basename" logic. Please double check'
+ " that the generated `install-man` rules are correct"
+ ]
+ _verify_migrator_generates_parsable_manifest(
+ migrate_installman_file,
+ manifest_parser_pkg_foo_factory,
+ accept_no_migration_issues,
+ "manpages",
+ dh_config_content,
+ expected_manifest_content,
+ expected_warnings=expected_warnings,
+ )
+
+
+def test_migrate_install_dh_exec_file(
+ manifest_parser_pkg_foo_factory: Callable[[], YAMLManifestParser],
+ accept_no_migration_issues: AcceptableMigrationIssues,
+) -> None:
+ dh_config_content = textwrap.dedent(
+ """\
+ #!/usr/bin/dh-exec
+
+ foo/script.sh => usr/bin/script
+ => usr/bin/bar
+ usr/bin/* usr/share/foo/extra/* usr/share/foo/extra
+ another-util usr/share/foo/extra
+ # This will not be merged with `=> usr/bin/bar`
+ usr/share/foo/features
+ usr/share/foo/bugs
+ some-file.txt usr/share/foo/online-doc
+ # TODO: Support migration of these
+ # pathA pathB conditional/arch [linux-anx]
+ # <!pkg.foo.condition> another-path conditional/profile
+ """
+ )
+ expected_manifest_content = textwrap.dedent(
+ """\
+ manifest-version: '0.1'
+ installations:
+ - install:
+ source: usr/bin/bar
+ - install:
+ source: foo/script.sh
+ as: usr/bin/script
+ - install:
+ sources:
+ - usr/bin/*
+ - usr/share/foo/extra/*
+ - another-util
+ dest-dir: usr/share/foo/extra
+ - install:
+ source: some-file.txt
+ dest-dir: usr/share/foo/online-doc
+ - install:
+ sources:
+ - usr/share/foo/features
+ - usr/share/foo/bugs
+ """
+ )
+ _verify_migrator_generates_parsable_manifest(
+ migrate_install_file,
+ manifest_parser_pkg_foo_factory,
+ accept_no_migration_issues,
+ "install",
+ dh_config_content,
+ expected_manifest_content,
+ )
+
+
+def test_migrate_maintscript(
+ manifest_parser_pkg_foo_factory: Callable[[], YAMLManifestParser],
+ accept_no_migration_issues: AcceptableMigrationIssues,
+) -> None:
+ dh_config_content = textwrap.dedent(
+ """\
+ rm_conffile /etc/foo.conf
+ mv_conffile /etc/bar.conf /etc/new-foo.conf 1.0~ bar
+ """
+ )
+ expected_manifest_content = textwrap.dedent(
+ """\
+ manifest-version: '0.1'
+ packages:
+ foo:
+ conffile-management:
+ - remove:
+ path: /etc/foo.conf
+ - rename:
+ source: /etc/bar.conf
+ target: /etc/new-foo.conf
+ prior-to-version: 1.0~
+ owning-package: bar
+ """
+ )
+ _verify_migrator_generates_parsable_manifest(
+ migrate_maintscript,
+ manifest_parser_pkg_foo_factory,
+ accept_no_migration_issues,
+ "maintscript",
+ dh_config_content,
+ expected_manifest_content,
+ )
+
+
+def test_migrate_not_installed_file(
+ manifest_parser_pkg_foo_factory: Callable[[], YAMLManifestParser],
+ accept_no_migration_issues: AcceptableMigrationIssues,
+) -> None:
+ dh_config_content = textwrap.dedent(
+ """\
+ foo/*.txt bar/${DEB_HOST_MULTIARCH}/*.so*
+ baz/script.sh
+ """
+ )
+ expected_manifest_content = textwrap.dedent(
+ """\
+ manifest-version: '0.1'
+ installations:
+ - discard:
+ - foo/*.txt
+ - bar/{{DEB_HOST_MULTIARCH}}/*.so*
+ - baz/script.sh
+ """
+ )
+ _verify_migrator_generates_parsable_manifest(
+ migrate_not_installed_file,
+ manifest_parser_pkg_foo_factory,
+ accept_no_migration_issues,
+ "not-installed",
+ dh_config_content,
+ expected_manifest_content,
+ )
+
+
+def test_migrate_links_files(
+ manifest_parser_pkg_foo_factory: Callable[[], YAMLManifestParser],
+ accept_no_migration_issues: AcceptableMigrationIssues,
+) -> None:
+ dh_config_content = textwrap.dedent(
+ """\
+ usr/share/target usr/bin/symlink
+ """
+ )
+ expected_manifest_content = textwrap.dedent(
+ """\
+ manifest-version: '0.1'
+ packages:
+ foo:
+ transformations:
+ - create-symlink:
+ path: usr/bin/symlink
+ target: /usr/share/target
+ """
+ )
+ _verify_migrator_generates_parsable_manifest(
+ migrate_links_files,
+ manifest_parser_pkg_foo_factory,
+ accept_no_migration_issues,
+ "links",
+ dh_config_content,
+ expected_manifest_content,
+ )
+
+
+def test_detect_obsolete_substvars(
+ empty_manifest_pkg_foo: HighLevelManifest,
+ accept_no_migration_issues: AcceptableMigrationIssues,
+) -> None:
+ migrator = detect_obsolete_substvars
+
+ dctrl_content = textwrap.dedent(
+ """\
+ Source: foo
+ Build-Depends: debhelper-compat (= 13),
+ dh-sequence-debputy,
+ dh-sequence-foo,
+
+ Package: foo
+ Architecture: any
+ Description: ...
+ Depends: ${misc:Depends},
+ ${shlibs:Depends},
+ bar (>= 1.0~) | baz, ${so:Depends},
+ """
+ )
+ dctrl_fs = build_virtual_file_system(
+ [
+ DEBIAN_DIR_ENTRY,
+ virtual_path_def(
+ "control",
+ fs_path="/nowhere/debian/control",
+ content=dctrl_content,
+ ),
+ ]
+ )
+
+ migration = run_migrator(
+ migrator,
+ dctrl_fs,
+ empty_manifest_pkg_foo,
+ accept_no_migration_issues,
+ )
+ msg = (
+ "The following relationship substitution variables can be removed from foo:"
+ " ${misc:Depends}, ${shlibs:Depends}, ${so:Depends}"
+ )
+ assert migration.anything_to_do
+ assert migration.warnings == [msg]
+ assert not migration.remove_paths_on_success
+ assert not migration.rename_paths_on_success
+ assert not migration.required_plugins
+
+
+def test_detect_obsolete_substvars_remove_field(
+ empty_manifest_pkg_foo: HighLevelManifest,
+ accept_no_migration_issues: AcceptableMigrationIssues,
+) -> None:
+ migrator = detect_obsolete_substvars
+
+ dctrl_content = textwrap.dedent(
+ """\
+ Source: foo
+ Build-Depends: debhelper-compat (= 13),
+ dh-sequence-debputy,
+ dh-sequence-foo,
+
+ Package: foo
+ Architecture: any
+ Description: ...
+ Pre-Depends: ${misc:Pre-Depends}
+ Depends: bar (>= 1.0~) | baz
+ """
+ )
+ dctrl_fs = build_virtual_file_system(
+ [
+ DEBIAN_DIR_ENTRY,
+ virtual_path_def(
+ "control",
+ fs_path="/nowhere/debian/control",
+ content=dctrl_content,
+ ),
+ ]
+ )
+
+ migration = run_migrator(
+ migrator,
+ dctrl_fs,
+ empty_manifest_pkg_foo,
+ accept_no_migration_issues,
+ )
+ msg = (
+ "The following relationship fields can be removed from foo: Pre-Depends."
+ " (The content in them would be applied automatically.)"
+ )
+ assert migration.anything_to_do
+ assert migration.warnings == [msg]
+ assert not migration.remove_paths_on_success
+ assert not migration.rename_paths_on_success
+ assert not migration.required_plugins
+
+
+def test_detect_obsolete_substvars_remove_field_essential(
+ empty_manifest_pkg_foo: HighLevelManifest,
+ accept_no_migration_issues: AcceptableMigrationIssues,
+) -> None:
+ migrator = detect_obsolete_substvars
+
+ dctrl_content = textwrap.dedent(
+ """\
+ Source: foo
+ Build-Depends: debhelper-compat (= 13),
+ dh-sequence-debputy,
+ dh-sequence-foo,
+
+ Package: foo
+ Architecture: any
+ Description: ...
+ Essential: yes
+ # Obsolete because the package is essential
+ Pre-Depends: ${shlibs:Depends}
+ Depends: bar (>= 1.0~) | baz
+ """
+ )
+ dctrl_fs = build_virtual_file_system(
+ [
+ DEBIAN_DIR_ENTRY,
+ virtual_path_def(
+ "control",
+ fs_path="/nowhere/debian/control",
+ content=dctrl_content,
+ ),
+ ]
+ )
+
+ migration = run_migrator(
+ migrator,
+ dctrl_fs,
+ empty_manifest_pkg_foo,
+ accept_no_migration_issues,
+ )
+ msg = (
+ "The following relationship fields can be removed from foo: Pre-Depends."
+ " (The content in them would be applied automatically.)"
+ )
+ assert migration.anything_to_do
+ assert migration.warnings == [msg]
+ assert not migration.remove_paths_on_success
+ assert not migration.rename_paths_on_success
+ assert not migration.required_plugins
+
+
+def test_detect_obsolete_substvars_remove_field_non_essential(
+ empty_manifest_pkg_foo: HighLevelManifest,
+ accept_no_migration_issues: AcceptableMigrationIssues,
+) -> None:
+ migrator = detect_obsolete_substvars
+
+ dctrl_content = textwrap.dedent(
+ """\
+ Source: foo
+ Build-Depends: debhelper-compat (= 13),
+ dh-sequence-debputy,
+ dh-sequence-foo,
+
+ Package: foo
+ Architecture: any
+ Description: ...
+ # This is not obsolete since the package is not essential
+ Pre-Depends: ${shlibs:Depends}
+ Depends: bar (>= 1.0~) | baz
+ """
+ )
+ dctrl_fs = build_virtual_file_system(
+ [
+ DEBIAN_DIR_ENTRY,
+ virtual_path_def(
+ "control",
+ fs_path="/nowhere/debian/control",
+ content=dctrl_content,
+ ),
+ ]
+ )
+
+ migration = run_migrator(
+ migrator,
+ dctrl_fs,
+ empty_manifest_pkg_foo,
+ accept_no_migration_issues,
+ )
+ assert not migration.anything_to_do
+ assert not migration.warnings
+ assert not migration.remove_paths_on_success
+ assert not migration.rename_paths_on_success
+ assert not migration.required_plugins
+
+
+def test_detect_dh_addons(
+ empty_manifest_pkg_foo: HighLevelManifest,
+ accept_no_migration_issues: AcceptableMigrationIssues,
+ accept_any_migration_issues: AcceptableMigrationIssues,
+) -> None:
+ migrator = detect_dh_addons
+ empty_fs = build_virtual_file_system([DEBIAN_DIR_ENTRY])
+
+ dctrl_no_addons_content = textwrap.dedent(
+ """\
+ Source: foo
+ Build-Depends: debhelper-compat (= 13)
+
+ Package: foo
+ Architecture: all
+ Description: ...
+ """
+ )
+
+ dctrl_w_addons_content = textwrap.dedent(
+ """\
+ Source: foo
+ Build-Depends: debhelper-compat (= 13),
+ dh-sequence-debputy,
+ dh-sequence-foo,
+
+ Package: foo
+ Architecture: all
+ Description: ...
+ """
+ )
+
+ dctrl_w_migrateable_addons_content = textwrap.dedent(
+ """\
+ Source: foo
+ Build-Depends: debhelper-compat (= 13),
+ dh-sequence-debputy,
+ dh-sequence-numpy3,
+
+ Package: foo
+ Architecture: all
+ Description: ...
+ """
+ )
+
+ dctrl_no_addons_fs = build_virtual_file_system(
+ [
+ DEBIAN_DIR_ENTRY,
+ virtual_path_def(
+ "control",
+ fs_path="/nowhere/debian/control-without-addons",
+ content=dctrl_no_addons_content,
+ ),
+ ]
+ )
+ dctrl_w_addons_fs = build_virtual_file_system(
+ [
+ DEBIAN_DIR_ENTRY,
+ virtual_path_def(
+ "control",
+ fs_path="/nowhere/debian/control-with-addons",
+ content=dctrl_w_addons_content,
+ ),
+ ]
+ )
+ dctrl_w_migrateable_addons_fs = build_virtual_file_system(
+ [
+ DEBIAN_DIR_ENTRY,
+ virtual_path_def(
+ "control",
+ fs_path="/nowhere/debian/control-with-migrateable-addons",
+ content=dctrl_w_migrateable_addons_content,
+ ),
+ ]
+ )
+ no_ctrl_msg = (
+ "Cannot find debian/control. Detection of unsupported/missing dh-sequence addon"
+ " could not be performed. Please ensure the package will Build-Depend on dh-sequence-zz-debputy"
+ " and not rely on any other debhelper sequence addons except those debputy explicitly supports."
+ )
+ missing_debputy_bd_msg = "Missing Build-Depends on dh-sequence-zz-debputy"
+ unsupported_sequence_msg = (
+ 'The dh addon "foo" is not known to work with dh-debputy and might malfunction'
+ )
+
+ migration = run_migrator(
+ migrator,
+ empty_fs,
+ empty_manifest_pkg_foo,
+ accept_no_migration_issues,
+ )
+ assert migration.anything_to_do
+ assert migration.warnings == [no_ctrl_msg]
+ assert not migration.remove_paths_on_success
+ assert not migration.rename_paths_on_success
+ assert not migration.required_plugins
+
+ migration = run_migrator(
+ migrator,
+ dctrl_no_addons_fs,
+ empty_manifest_pkg_foo,
+ accept_no_migration_issues,
+ )
+ assert migration.anything_to_do
+ assert migration.warnings == [missing_debputy_bd_msg]
+ assert not migration.remove_paths_on_success
+ assert not migration.rename_paths_on_success
+ assert not migration.required_plugins
+
+ _assert_unsupported_feature(
+ migrator,
+ dctrl_w_addons_fs,
+ empty_manifest_pkg_foo,
+ accept_no_migration_issues,
+ )
+
+ migration = run_migrator(
+ migrator,
+ dctrl_w_addons_fs,
+ empty_manifest_pkg_foo,
+ accept_any_migration_issues,
+ )
+
+ assert migration.anything_to_do
+ assert migration.warnings == [unsupported_sequence_msg]
+ assert not migration.remove_paths_on_success
+ assert not migration.rename_paths_on_success
+ assert not migration.required_plugins
+
+ migration = run_migrator(
+ migrator,
+ dctrl_w_migrateable_addons_fs,
+ empty_manifest_pkg_foo,
+ accept_any_migration_issues,
+ )
+ assert not migration.anything_to_do
+ assert not migration.warnings
+ assert not migration.remove_paths_on_success
+ assert not migration.rename_paths_on_success
+ assert migration.required_plugins == ["numpy3"]
+
+
+def test_detect_dh_addons_rrr(
+ empty_manifest_pkg_foo: HighLevelManifest,
+ accept_no_migration_issues: AcceptableMigrationIssues,
+) -> None:
+ migrator = detect_dh_addons_zz_debputy_rrr
+ empty_fs = build_virtual_file_system([DEBIAN_DIR_ENTRY])
+
+ dctrl_no_addons_content = textwrap.dedent(
+ """\
+ Source: foo
+ Build-Depends: debhelper-compat (= 13)
+
+ Package: foo
+ Architecture: all
+ Description: ...
+ """
+ )
+
+ dctrl_w_addons_content = textwrap.dedent(
+ """\
+ Source: foo
+ Build-Depends: debhelper-compat (= 13),
+ dh-sequence-zz-debputy-rrr,
+ dh-sequence-foo,
+
+ Package: foo
+ Architecture: all
+ Description: ...
+ """
+ )
+
+ dctrl_no_addons_fs = build_virtual_file_system(
+ [
+ DEBIAN_DIR_ENTRY,
+ virtual_path_def(
+ "control",
+ fs_path="/nowhere/debian/control-without-addons",
+ content=dctrl_no_addons_content,
+ ),
+ ]
+ )
+ dctrl_w_addons_fs = build_virtual_file_system(
+ [
+ DEBIAN_DIR_ENTRY,
+ virtual_path_def(
+ "control",
+ fs_path="/nowhere/debian/control-with-addons",
+ content=dctrl_w_addons_content,
+ ),
+ ]
+ )
+ no_ctrl_msg = (
+ "Cannot find debian/control. Detection of unsupported/missing dh-sequence addon"
+ " could not be performed. Please ensure the package will Build-Depend on dh-sequence-zz-debputy-rrr."
+ )
+ missing_debputy_bd_msg = "Missing Build-Depends on dh-sequence-zz-debputy-rrr"
+
+ migration = run_migrator(
+ migrator,
+ empty_fs,
+ empty_manifest_pkg_foo,
+ accept_no_migration_issues,
+ migration_target=MIGRATION_TARGET_DH_DEBPUTY_RRR,
+ )
+ assert migration.anything_to_do
+ assert migration.warnings == [no_ctrl_msg]
+ assert not migration.remove_paths_on_success
+ assert not migration.rename_paths_on_success
+ assert not migration.required_plugins
+
+ migration = run_migrator(
+ migrator,
+ dctrl_no_addons_fs,
+ empty_manifest_pkg_foo,
+ accept_no_migration_issues,
+ )
+ assert migration.anything_to_do
+ assert migration.warnings == [missing_debputy_bd_msg]
+ assert not migration.remove_paths_on_success
+ assert not migration.rename_paths_on_success
+ assert not migration.required_plugins
+
+ migration = run_migrator(
+ migrator,
+ dctrl_w_addons_fs,
+ empty_manifest_pkg_foo,
+ accept_no_migration_issues,
+ )
+
+ assert not migration.anything_to_do
+ assert not migration.warnings
+ assert not migration.remove_paths_on_success
+ assert not migration.rename_paths_on_success
+ assert not migration.required_plugins
+
+
+def test_migrate_bash_completion_file_no_changes(
+ manifest_parser_pkg_foo_factory: Callable[[], YAMLManifestParser],
+ accept_no_migration_issues: AcceptableMigrationIssues,
+) -> None:
+ dh_config_content = textwrap.dedent(
+ """\
+ compgen -A
+ """
+ )
+ dh_config_name = "bash-completion"
+ fs = build_virtual_file_system(
+ [
+ DEBIAN_DIR_ENTRY,
+ virtual_path_def(
+ dh_config_name,
+ fs_path=f"/nowhere/debian/{dh_config_name}",
+ content=dh_config_content,
+ ),
+ ]
+ )
+ migration = run_migrator(
+ migrate_bash_completion,
+ fs,
+ manifest_parser_pkg_foo_factory().build_manifest(),
+ accept_no_migration_issues,
+ )
+ assert not migration.rename_paths_on_success
+ assert not migration.remove_paths_on_success
+ assert not migration.warnings
+ assert not migration.required_plugins
+
+
+def test_migrate_bash_completion_file(
+ manifest_parser_pkg_foo_factory: Callable[[], YAMLManifestParser],
+ accept_no_migration_issues: AcceptableMigrationIssues,
+) -> None:
+ dh_config_content = textwrap.dedent(
+ """\
+ foo/*
+ bar baz
+ debian/bar-completion bar
+ debian/foo-completion foo
+ debian/*.cmpl
+ """
+ )
+ expected_manifest_content = textwrap.dedent(
+ """\
+ manifest-version: '0.1'
+ installations:
+ - install:
+ sources:
+ - foo/*
+ - debian/*.cmpl
+ dest-dir: '{{path:BASH_COMPLETION_DIR}}'
+ - install:
+ source: bar
+ as: '{{path:BASH_COMPLETION_DIR}}/baz'
+ """
+ )
+ expected_renames = [
+ ("debian/bar-completion", "debian/foo.bar.bash-completion"),
+ ("debian/foo-completion", "debian/foo.bash-completion"),
+ ]
+ _verify_migrator_generates_parsable_manifest(
+ migrate_bash_completion,
+ manifest_parser_pkg_foo_factory,
+ accept_no_migration_issues,
+ "bash-completion",
+ dh_config_content,
+ expected_manifest_content,
+ expected_renamed_paths=expected_renames,
+ )
diff --git a/tests/test_output_filename.py b/tests/test_output_filename.py
new file mode 100644
index 0000000..fab0e98
--- /dev/null
+++ b/tests/test_output_filename.py
@@ -0,0 +1,27 @@
+from pathlib import Path
+
+import pytest
+
+from debputy.util import compute_output_filename
+
+
+def write_unpacked_deb(root: Path, package: str, version: str, arch: str):
+ (root / "control").write_text(
+ f"Package: {package}\nVersion: {version}\nArchitecture: {arch}\n"
+ )
+
+
+@pytest.mark.parametrize(
+ "package,version,arch,is_udeb,expected",
+ [
+ ("fake", "1.0", "amd64", False, "fake_1.0_amd64.deb"),
+ ("fake", "1.0", "amd64", True, "fake_1.0_amd64.udeb"),
+ ("fake", "2:1.0", "amd64", False, "fake_1.0_amd64.deb"),
+ ("fake", "2:1.0", "amd64", True, "fake_1.0_amd64.udeb"),
+ ("fake", "3.0", "all", False, "fake_3.0_all.deb"),
+ ("fake", "3.0", "all", True, "fake_3.0_all.udeb"),
+ ],
+)
+def test_generate_deb_filename(tmp_path, package, version, arch, is_udeb, expected):
+ write_unpacked_deb(tmp_path, package, version, arch)
+ assert compute_output_filename(str(tmp_path), is_udeb) == expected
diff --git a/tests/test_packager_provided_files.py b/tests/test_packager_provided_files.py
new file mode 100644
index 0000000..149564d
--- /dev/null
+++ b/tests/test_packager_provided_files.py
@@ -0,0 +1,210 @@
+import random
+from typing import cast
+
+import pytest
+
+from debputy.packager_provided_files import detect_all_packager_provided_files
+from debputy.plugin.api import DebputyPluginInitializer
+from debputy.plugin.api.feature_set import PluginProvidedFeatureSet
+from debputy.plugin.api.test_api import (
+ InitializedPluginUnderTest,
+ build_virtual_file_system,
+)
+from debputy.plugin.api.test_api.test_impl import initialize_plugin_under_test_preloaded
+from tutil import faked_binary_package, binary_package_table
+
+
+def ppf_test_plugin(api: DebputyPluginInitializer) -> None:
+ api.packager_provided_file(
+ "arch-specific-dash",
+ "/some/test-directory/{name}.conf",
+ allow_architecture_segment=True,
+ )
+ api.packager_provided_file(
+ "arch.specific.dot",
+ "/some/test.directory/{name}",
+ allow_architecture_segment=True,
+ )
+
+ api.packager_provided_file(
+ "arch.specific.with.priority",
+ "/some/test.priority.directory/{priority:02}-{name}",
+ allow_architecture_segment=True,
+ default_priority=60,
+ )
+
+ api.packager_provided_file(
+ "packageless-fallback",
+ "/some/test-plfb/{name}",
+ packageless_is_fallback_for_all_packages=True,
+ )
+ api.packager_provided_file(
+ "packageless.fallback",
+ "/some/test.plfb/{name}",
+ packageless_is_fallback_for_all_packages=True,
+ )
+
+
+@pytest.mark.parametrize(
+ "package_name,basename,install_target,is_main_binary",
+ [
+ ("foo", "foo.arch-specific-dash", "./some/test-directory/foo.conf", True),
+ # main package match
+ ("foo", "arch-specific-dash", "./some/test-directory/foo.conf", True),
+ # arch match
+ ("foo", "foo.arch-specific-dash.amd64", "./some/test-directory/foo.conf", True),
+ # Matches with periods in both package name and in the file type
+ ("foo.bar", "foo.bar.arch.specific.dot", "./some/test.directory/foo.bar", True),
+ ("foo.bar", "arch.specific.dot", "./some/test.directory/foo.bar", True),
+ (
+ "foo.bar",
+ "foo.bar.arch.specific.dot.amd64",
+ "./some/test.directory/foo.bar",
+ True,
+ ),
+ # Priority
+ (
+ "foo.bar",
+ "foo.bar.arch.specific.with.priority",
+ "./some/test.priority.directory/60-foo.bar",
+ True,
+ ),
+ (
+ "foo.bar",
+ "arch.specific.with.priority",
+ "./some/test.priority.directory/60-foo.bar",
+ True,
+ ),
+ (
+ "foo.bar",
+ "foo.bar.arch.specific.with.priority.amd64",
+ "./some/test.priority.directory/60-foo.bar",
+ True,
+ ),
+ # Name
+ (
+ "foo.bar",
+ "foo.bar.special.name.arch.specific.with.priority",
+ "./some/test.priority.directory/60-special.name",
+ True,
+ ),
+ (
+ "foo.bar",
+ "foo.bar.special.name.arch.specific.with.priority.amd64",
+ "./some/test.priority.directory/60-special.name",
+ True,
+ ),
+ (
+ "foo.bar",
+ "packageless-fallback",
+ "./some/test-plfb/foo.bar",
+ False,
+ ),
+ (
+ "foo.bar",
+ "packageless.fallback",
+ "./some/test.plfb/foo.bar",
+ False,
+ ),
+ ],
+)
+def test_packager_provided_files(
+ package_name: str, basename: str, install_target: str, is_main_binary: bool
+) -> None:
+ # Inject our custom files
+ plugin = initialize_plugin_under_test_preloaded(
+ 1,
+ ppf_test_plugin,
+ plugin_name="pff-test-plugin",
+ )
+ debputy_plugin_feature_set = _fetch_debputy_plugin_feature_set(plugin)
+
+ debian_dir = build_virtual_file_system([basename])
+ binary_under_test = faked_binary_package(
+ package_name, is_main_package=is_main_binary
+ )
+ main_package = (
+ binary_under_test if is_main_binary else faked_binary_package("main-pkg")
+ )
+ binaries = [main_package]
+ if not is_main_binary:
+ binaries.append(binary_under_test)
+ binary_packages = binary_package_table(*binaries)
+
+ ppf = detect_all_packager_provided_files(
+ debputy_plugin_feature_set.packager_provided_files,
+ debian_dir,
+ binary_packages,
+ )
+ assert package_name in ppf
+ all_matched = ppf[package_name].auto_installable
+ assert len(all_matched) == 1
+ matched = all_matched[0]
+ assert basename == matched.path.name
+ actual_install_target = "/".join(matched.compute_dest())
+ assert actual_install_target == install_target
+
+
+@pytest.mark.parametrize(
+ "package_name,expected_basename,non_matched",
+ [
+ ("foo", "foo.arch-specific-dash", ["arch-specific-dash"]),
+ (
+ "foo",
+ "foo.arch-specific-dash.amd64",
+ [
+ "foo.arch-specific-dash",
+ "arch-specific-dash",
+ "foo.arch-specific-dash.i386",
+ ],
+ ),
+ (
+ "foo",
+ "foo.arch-specific-dash",
+ ["arch-specific-dash", "foo.arch-specific-dash.i386"],
+ ),
+ ],
+)
+def test_packager_provided_files_priority(
+ package_name, expected_basename, non_matched
+) -> None:
+ assert len(non_matched) > 0
+ # Inject our custom files
+ plugin = initialize_plugin_under_test_preloaded(
+ 1,
+ ppf_test_plugin,
+ plugin_name="pff-test-plugin",
+ )
+ debputy_plugin_feature_set = _fetch_debputy_plugin_feature_set(plugin)
+ binary_packages = binary_package_table(faked_binary_package(package_name))
+ all_entries_base = [x for x in non_matched]
+
+ for order in (0, len(all_entries_base), None):
+ all_entries = all_entries_base.copy()
+ print(f"Order: {order}")
+ if order is not None:
+ all_entries.insert(order, expected_basename)
+ else:
+ all_entries.append(expected_basename)
+ # Ensure there are no order dependencies in the test by randomizing it.
+ random.shuffle(all_entries)
+
+ debian_dir = build_virtual_file_system(all_entries)
+ ppf = detect_all_packager_provided_files(
+ debputy_plugin_feature_set.packager_provided_files,
+ debian_dir,
+ binary_packages,
+ )
+ assert package_name in ppf
+ all_matched = ppf[package_name].auto_installable
+ assert len(all_matched) == 1
+ matched = all_matched[0]
+ assert expected_basename == matched.path.name
+
+
+def _fetch_debputy_plugin_feature_set(
+ plugin: InitializedPluginUnderTest,
+) -> PluginProvidedFeatureSet:
+ # Very much not public API, but we need it to avoid testing on production data (also, it is hard to find
+ # relevant debputy files for all the cases we want to test).
+ return cast("InitializedPluginUnderTestImpl", plugin)._feature_set
diff --git a/tests/test_packer_pack.py b/tests/test_packer_pack.py
new file mode 100644
index 0000000..17529ef
--- /dev/null
+++ b/tests/test_packer_pack.py
@@ -0,0 +1,86 @@
+import argparse
+import json
+from pathlib import Path
+
+from debputy.commands import deb_packer
+from debputy.intermediate_manifest import TarMember, PathType
+
+
+def write_unpacked_deb(root: Path, package: str, version: str, arch: str):
+ debian = root / "DEBIAN"
+ debian.mkdir(mode=0o755)
+ (debian / "control").write_text(
+ f"Package: {package}\nVersion: {version}\nArchitecture: {arch}\n"
+ )
+
+
+def test_pack_smoke(tmp_path):
+ mtime = 1668973695
+
+ root_dir = tmp_path / "root"
+ root_dir.mkdir()
+ write_unpacked_deb(root_dir, "fake", "1.0", "amd64")
+ output_path = tmp_path / "out"
+ output_path.mkdir()
+ deb_file = Path(output_path) / "output.deb"
+
+ parsed_args = argparse.Namespace(
+ is_udeb=False, compression_level=None, compression_strategy=None
+ )
+
+ data_compression = deb_packer.COMPRESSIONS["xz"]
+ data_compression_cmd = data_compression.as_cmdline(parsed_args)
+ ctrl_compression = data_compression
+ ctrl_compression_cmd = data_compression_cmd
+
+ package_manifest = tmp_path / "temporary-manifest.json"
+ package_manifest.write_text(
+ json.dumps(
+ [
+ TarMember.virtual_path(
+ "./", PathType.DIRECTORY, mode=0o755, mtime=1668973695
+ ).to_manifest()
+ ]
+ )
+ )
+
+ deb_packer.pack(
+ str(deb_file),
+ ctrl_compression,
+ data_compression,
+ str(root_dir),
+ str(package_manifest),
+ mtime,
+ ctrl_compression_cmd,
+ data_compression_cmd,
+ prefer_raw_exceptions=True,
+ )
+
+ binary = deb_file.read_bytes()
+
+ assert binary == (
+ b"!<arch>\n"
+ b"debian-binary 1668973695 0 0 100644 4 `\n"
+ b"2.0\n"
+ b"control.tar.xz 1668973695 0 0 100644 244 `\n"
+ b"\xfd7zXZ\x00\x00\x04\xe6\xd6\xb4F\x04\xc0\xb4\x01\x80P!\x01\x16\x00\x00\x00"
+ b"\x00\x00\x00\x00\x19\x87 E\xe0'\xff\x00\xac]\x00\x17\x0b\xbc\x1c}"
+ b"\x01\x95\xc0\x1dJ>y\x15\xc2\xcc&\xa3^\x11\xb5\x81\xa6\x8cI\xd2\xf0m\xdd\x04"
+ b"M\xb2|Tdy\xf5\x00H\xab\xa6B\x11\x8d2\x0e\x1d\xf8F\x9e\x9a\xb0\xb8_]\xa3;M"
+ b"t\x90\x9a\xe3)\xeb\xadF\xfet'b\x05\x85\xd5\x04g\x7f\x89\xeb=(\xfd\xf6"
+ b'"p\xc3\x91\xf2\xd3\xd2\xb3\xed%i\x9a\xfa\\\xde7\xd5\x01\x18I\x14D\x10E'
+ b"\xba\xdf\xfb\x12{\x84\xc4\x10\x08,\xbc\x9e\xac+w\x07\r`|\xcfFL#\xbb"
+ b"S\x91\xb4\\\x9b\x80&\x1d\x9ej\x13\xe3\x13\x02=\xe9\xd5\xcf\xb0\xdf?L\xf1\x96"
+ b"\xd2\xc6bh\x19|?\xc2j\xe58If\xb7Y\xb9\x18:\x00\x00|\xfb\xcf\x82e/\xd05"
+ b"\x00\x01\xd0\x01\x80P\x00\x00\xc9y\xeem\xb1\xc4g\xfb\x02\x00\x00\x00"
+ b"\x00\x04YZ"
+ b"data.tar.xz 1668973695 0 0 100644 168 `\n"
+ b"\xfd7zXZ\x00\x00\x04\xe6\xd6\xb4F\x04\xc0h\x80P!\x01\x16\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\xc2bi\xe8\xe0'\xff\x00`]\x00\x17\x0b\xbc\x1c}"
+ b"\x01\x95\xc0\x1dJ>y\x15\xc2\xcc&\xa3^\x11\xb5\x81\xa6\x8cI\xd2\xf0m\xdd\x04"
+ b"M\xb2|Tdy\xf5\x00H\xab\xa6B\x11\x8d2\x0e\x1d\xf8F\x9e\x9a\xb0\xb8_]\xa4W%"
+ b"\xa2\x14N\xb9\xe7\xbd\xf3a\x16\xe5\xb7\xe6\x80\x95\xcc\xe6+\xe1;I"
+ b"\xf2\x1f\xed\x08\xac\xd7UZ\xc0P\x0b\xfb\nK\xef~\xcb\x8f\x80\x00\x9b\x19\xf8A"
+ b"Q_\xe7\xeb\x00\x01\x84\x01\x80P\x00\x00(3\xf1\xfa\xb1\xc4g\xfb"
+ b"\x02\x00\x00\x00\x00\x04YZ"
+ )
diff --git a/tests/test_parser.py b/tests/test_parser.py
new file mode 100644
index 0000000..bc041fc
--- /dev/null
+++ b/tests/test_parser.py
@@ -0,0 +1,473 @@
+import textwrap
+
+import pytest
+
+from debputy import DEBPUTY_DOC_ROOT_DIR
+from debputy.exceptions import DebputySubstitutionError
+from debputy.highlevel_manifest_parser import YAMLManifestParser
+from debputy.manifest_parser.exceptions import ManifestParseException
+from debputy.plugin.api.test_api import build_virtual_file_system
+
+
+def normalize_doc_link(message) -> str:
+ return message.replace(DEBPUTY_DOC_ROOT_DIR, "{{DEBPUTY_DOC_ROOT_DIR}}")
+
+
+@pytest.fixture()
+def manifest_parser_pkg_foo(
+ amd64_dpkg_architecture_variables,
+ dpkg_arch_query,
+ source_package,
+ package_single_foo_arch_all_cxt_amd64,
+ amd64_substitution,
+ no_profiles_or_build_options,
+ debputy_plugin_feature_set,
+) -> YAMLManifestParser:
+ # We need an empty directory to avoid triggering packager provided files.
+ debian_dir = build_virtual_file_system([])
+ return YAMLManifestParser(
+ "debian/test-debputy.manifest",
+ source_package,
+ package_single_foo_arch_all_cxt_amd64,
+ amd64_substitution,
+ amd64_dpkg_architecture_variables,
+ dpkg_arch_query,
+ no_profiles_or_build_options,
+ debputy_plugin_feature_set,
+ debian_dir=debian_dir,
+ )
+
+
+def test_parsing_no_manifest(manifest_parser_pkg_foo):
+ manifest = manifest_parser_pkg_foo.build_manifest()
+
+ assert [p.name for p in manifest.all_packages] == ["foo"]
+ assert [p.name for p in manifest.active_packages] == ["foo"]
+
+
+def test_parsing_version_only(manifest_parser_pkg_foo):
+ content = textwrap.dedent(
+ """\
+ manifest-version: '0.1'
+ """
+ )
+
+ manifest = manifest_parser_pkg_foo.parse_manifest(fd=content)
+
+ assert [p.name for p in manifest.all_packages] == ["foo"]
+ assert [p.name for p in manifest.active_packages] == ["foo"]
+
+
+def test_parsing_variables(manifest_parser_pkg_foo):
+ # https://salsa.debian.org/debian/debputy/-/issues/58
+ content = textwrap.dedent(
+ """\
+ manifest-version: '0.1'
+ definitions:
+ variables:
+ LIBPATH: "/usr/lib/{{DEB_HOST_MULTIARCH}}"
+ SONAME: "1"
+ LETTER_O: "o"
+ installations:
+ - install:
+ source: build/libfoo.so.{{SONAME}}
+ dest-dir: "{{LIBPATH}}"
+ into: f{{LETTER_O}}{{LETTER_O}}
+ packages:
+ f{{LETTER_O}}{{LETTER_O}}:
+ transformations:
+ - create-symlink:
+ path: "{{LIBPATH}}/libfoo.so.{{SONAME}}.0.0"
+ target: "{{LIBPATH}}/libfoo.so.{{SONAME}}"
+ """
+ )
+ manifest_parser_pkg_foo.parse_manifest(fd=content)
+ # TODO: Verify that the substitution is applied correctly throughout
+ # (currently, the test just verifies that we do not reject the manifest)
+
+
+@pytest.mark.parametrize(
+ "varname",
+ [
+ "PACKAGE",
+ "DEB_HOST_ARCH",
+ "DEB_BLAH_ARCH",
+ "env:UNEXISTING",
+ "token:TAB",
+ ],
+)
+def test_parsing_variables_reserved(manifest_parser_pkg_foo, varname):
+ content = textwrap.dedent(
+ f"""\
+ manifest-version: '0.1'
+ definitions:
+ variables:
+ '{varname}': "test"
+ """
+ )
+
+ 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}.'
+ assert normalize_doc_link(e_info.value.args[0]) == msg
+
+
+def test_parsing_variables_interdependent_ok(manifest_parser_pkg_foo):
+ content = textwrap.dedent(
+ """\
+ manifest-version: '0.1'
+ definitions:
+ variables:
+ DOC_PATH: "/usr/share/doc/foo"
+ EXAMPLE_PATH: "{{DOC_PATH}}/examples"
+ installations:
+ - install:
+ source: foo.example
+ dest-dir: "{{EXAMPLE_PATH}}"
+ """
+ )
+
+ manifest = manifest_parser_pkg_foo.parse_manifest(fd=content)
+ resolved = manifest.substitution.substitute("{{EXAMPLE_PATH}}", "test")
+ assert resolved == "/usr/share/doc/foo/examples"
+
+
+def test_parsing_variables_unused(manifest_parser_pkg_foo):
+ content = textwrap.dedent(
+ f"""\
+ manifest-version: '0.1'
+ definitions:
+ variables:
+ UNUSED: "test"
+ """
+ )
+
+ with pytest.raises(ManifestParseException) as e_info:
+ manifest_parser_pkg_foo.parse_manifest(fd=content)
+
+ msg = (
+ 'The variable "UNUSED" is unused. Either use it or remove it.'
+ " The variable was declared at definitions.variables.UNUSED."
+ )
+ assert normalize_doc_link(e_info.value.args[0]) == msg
+
+
+def test_parsing_package_foo_empty(manifest_parser_pkg_foo):
+ content = textwrap.dedent(
+ """\
+ manifest-version: '0.1'
+ packages:
+ foo:
+ """
+ )
+
+ with pytest.raises(ManifestParseException) as e_info:
+ manifest_parser_pkg_foo.parse_manifest(fd=content)
+
+ msg = (
+ "The attribute packages.foo 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
+
+
+def test_parsing_package_bar_empty(manifest_parser_pkg_foo):
+ content = textwrap.dedent(
+ """\
+ manifest-version: '0.1'
+ packages:
+ bar:
+ """
+ )
+
+ with pytest.raises(ManifestParseException) as e_info:
+ manifest_parser_pkg_foo.parse_manifest(fd=content)
+
+ assert 'package "bar" is not present' in e_info.value.args[0]
+
+
+def test_transformations_no_list(manifest_parser_pkg_foo):
+ content = textwrap.dedent(
+ """\
+ manifest-version: '0.1'
+ packages:
+ foo:
+ transformations:
+ create-symlinks:
+ path: a
+ target: b
+ """
+ )
+
+ with pytest.raises(ManifestParseException) as e_info:
+ manifest_parser_pkg_foo.parse_manifest(fd=content)
+
+ assert "packages.foo.transformations" in e_info.value.args[0]
+ assert "must be a list" in e_info.value.args[0]
+
+
+def test_create_symlinks_missing_path(manifest_parser_pkg_foo):
+ content = textwrap.dedent(
+ """\
+ manifest-version: '0.1'
+ packages:
+ foo:
+ transformations:
+ - create-symlink:
+ target: b
+ """
+ )
+
+ with pytest.raises(ManifestParseException) as e_info:
+ 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: "
+ "{{DEBPUTY_DOC_ROOT_DIR}}/MANIFEST-FORMAT.md#create-symlinks-transformation-rule-create-symlink)"
+ )
+ assert normalize_doc_link(e_info.value.args[0]) == msg
+
+
+def test_create_symlinks_unknown_replacement_rule(manifest_parser_pkg_foo):
+ content = textwrap.dedent(
+ """\
+ manifest-version: '0.1'
+ packages:
+ foo:
+ transformations:
+ - create-symlink:
+ path: usr/share/foo
+ target: /usr/share/bar
+ replacement-rule: golf
+ """
+ )
+
+ with pytest.raises(ManifestParseException) as e_info:
+ manifest_parser_pkg_foo.parse_manifest(fd=content)
+
+ msg = (
+ 'The attribute "packages.foo.transformations[0].create-symlink.replacement-rule <Search for: usr/share/foo>"'
+ " 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"'
+ )
+ assert normalize_doc_link(e_info.value.args[0]) == msg
+
+
+def test_create_symlinks_missing_target(manifest_parser_pkg_foo):
+ content = textwrap.dedent(
+ """\
+ manifest-version: '0.1'
+ packages:
+ foo:
+ transformations:
+ - create-symlink:
+ path: a
+ """
+ )
+
+ with pytest.raises(ManifestParseException) as e_info:
+ 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: "
+ "{{DEBPUTY_DOC_ROOT_DIR}}/MANIFEST-FORMAT.md#create-symlinks-transformation-rule-create-symlink)"
+ )
+ assert normalize_doc_link(e_info.value.args[0]) == msg
+
+
+def test_create_symlinks_not_normalized_path(manifest_parser_pkg_foo):
+ content = textwrap.dedent(
+ """\
+ manifest-version: '0.1'
+ packages:
+ foo:
+ transformations:
+ - create-symlink:
+ path: ../bar
+ target: b
+ """
+ )
+
+ with pytest.raises(ManifestParseException) as e_info:
+ manifest_parser_pkg_foo.parse_manifest(fd=content)
+
+ expected = (
+ 'The path "../bar" provided in packages.foo.transformations[0].create-symlink.path <Search for: ../bar>'
+ ' should be relative to the root of the package and not use any ".." or "." segments.'
+ )
+ assert e_info.value.args[0] == expected
+
+
+def test_unresolvable_subst_in_source_context(manifest_parser_pkg_foo):
+ content = textwrap.dedent(
+ """\
+ manifest-version: '0.1'
+ installations:
+ - install:
+ source: "foo.sh"
+ as: "usr/bin/{{PACKAGE}}"
+ """
+ )
+
+ with pytest.raises(DebputySubstitutionError) as e_info:
+ manifest_parser_pkg_foo.parse_manifest(fd=content)
+
+ expected = (
+ "The variable {{PACKAGE}} is not available while processing installations[0].install.as"
+ " <Search for: foo.sh>."
+ )
+
+ assert e_info.value.args[0] == expected
+
+
+def test_yaml_error_duplicate_key(manifest_parser_pkg_foo):
+ content = textwrap.dedent(
+ """\
+ manifest-version: '0.1'
+ packages:
+ foo:
+ transformations:
+ - create-symlink:
+ path: ../bar
+ target: b
+ # Duplicate key error
+ path: ../foo
+ """
+ )
+
+ with pytest.raises(ManifestParseException) as e_info:
+ manifest_parser_pkg_foo.parse_manifest(fd=content)
+
+ assert "duplicate key" in e_info.value.args[0]
+
+
+def test_yaml_error_tab_start(manifest_parser_pkg_foo):
+ content = textwrap.dedent(
+ """\
+ manifest-version: '0.1'
+ packages:
+ foo:
+ transformations:
+ - create-symlink:
+ path: ../bar
+ target: b
+ # Tab is not allowed here in this case.
+ \ta
+ """
+ )
+
+ with pytest.raises(ManifestParseException) as e_info:
+ manifest_parser_pkg_foo.parse_manifest(fd=content)
+
+ assert "'\\t' that cannot start any token" in e_info.value.args[0]
+
+
+def test_yaml_octal_mode_int(manifest_parser_pkg_foo):
+ content = textwrap.dedent(
+ """\
+ manifest-version: '0.1'
+ packages:
+ foo:
+ transformations:
+ - path-metadata:
+ path: usr/share/bar
+ mode: 0755
+ """
+ )
+
+ with pytest.raises(ManifestParseException) as e_info:
+ 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'
+ " have a valid structure/type: The attribute must be a FileSystemMode (string)"
+ )
+
+ assert e_info.value.args[0] == msg
+
+
+def test_yaml_clean_after_removal(manifest_parser_pkg_foo):
+ content = textwrap.dedent(
+ """\
+ manifest-version: '0.1'
+ packages:
+ foo:
+ clean-after-removal:
+ - /var/log/foo/*.log
+ - /var/log/foo/*.log.gz
+ - path: /var/log/foo/
+ ignore-non-empty-dir: true
+ - /etc/non-conffile-configuration.conf
+ - path: /var/cache/foo
+ recursive: true
+
+ """
+ )
+
+ manifest_parser_pkg_foo.parse_manifest(fd=content)
+
+
+def test_binary_version(manifest_parser_pkg_foo):
+ content = textwrap.dedent(
+ """\
+ manifest-version: '0.1'
+ packages:
+ foo:
+ binary-version: 1:2.3
+
+ """
+ )
+
+ manifest = manifest_parser_pkg_foo.parse_manifest(fd=content)
+ assert manifest.package_state_for("foo").binary_version == "1:2.3"
+
+
+@pytest.mark.parametrize(
+ "path,is_accepted",
+ [
+ ("usr/bin/foo", False),
+ ("var/cache/foo*", False),
+ ("var/cache/foo", True),
+ ("var/cache/foo/", True),
+ ("var/cache/foo/*", True),
+ ("var/cache/foo/*.*", True),
+ ("var/cache/foo/*.txt", True),
+ ("var/cache/foo/cache.*", True),
+ ("etc/foo*", False),
+ ("etc/foo/*", True),
+ ("etc/foo/", True),
+ # /var/log is special-cased
+ ("/var/log/foo*", True),
+ ("/var/log/foo/*.*", True),
+ ("/var/log/foo/", True),
+ # Unsupported pattern at the time of writing
+ ("/var/log/foo/*.*.*", False),
+ # Questionable rules
+ ("*", False),
+ ("*.la", False),
+ ("*/foo/*", False),
+ ],
+)
+def test_yaml_clean_after_removal_unsafe_path(
+ manifest_parser_pkg_foo,
+ path: str,
+ is_accepted: bool,
+) -> None:
+ content = textwrap.dedent(
+ f"""\
+ manifest-version: '0.1'
+ packages:
+ foo:
+ clean-after-removal:
+ - {path}
+ """
+ )
+
+ if is_accepted:
+ manifest_parser_pkg_foo.parse_manifest(fd=content)
+ else:
+ with pytest.raises(ManifestParseException) as e_info:
+ manifest_parser_pkg_foo.parse_manifest(fd=content)
diff --git a/tests/test_path.py b/tests/test_path.py
new file mode 100644
index 0000000..a9e826e
--- /dev/null
+++ b/tests/test_path.py
@@ -0,0 +1,47 @@
+from typing import cast
+
+import pytest
+
+from debputy.exceptions import SymlinkLoopError
+from debputy.filesystem_scan import VirtualPathBase
+from debputy.plugin.api import virtual_path_def
+from debputy.plugin.api.test_api import build_virtual_file_system
+
+
+def test_symlink_lookup() -> None:
+ fs = build_virtual_file_system(
+ [
+ virtual_path_def("./usr/share/doc/bar", link_target="foo"),
+ "./usr/share/doc/foo/copyright",
+ virtual_path_def("./usr/share/bar/data", link_target="../foo/data"),
+ "./usr/share/foo/data/foo.dat",
+ virtual_path_def("./usr/share/baz/data", link_target="/usr/share/foo/data"),
+ virtual_path_def(
+ "./usr/share/test/loop-a", link_target="/usr/share/test/loop-b"
+ ),
+ virtual_path_def("./usr/share/test/loop-b", link_target="./loop-c"),
+ virtual_path_def("./usr/share/test/loop-c", link_target="../test/loop-a"),
+ ]
+ )
+ assert fs.lookup("/usr/share/doc/bar/copyright") is not None
+ assert fs.lookup("/usr/share/bar/data/foo.dat") is not None
+ assert fs.lookup("/usr/share/baz/data/foo.dat") is not None
+
+ vp_fs: VirtualPathBase = cast("VirtualPathBase", fs)
+ p, missing = vp_fs.attempt_lookup("/usr/share/doc/foo/non-existent")
+ assert p.path == "./usr/share/doc/foo"
+ assert missing == ["non-existent"]
+
+ p, missing = vp_fs.attempt_lookup("/usr/share/bar/data/non-existent")
+ assert p.path == "./usr/share/foo/data"
+ assert missing == ["non-existent"]
+
+ p, missing = vp_fs.attempt_lookup("/usr/share/baz/data/non-existent")
+ assert p.path == "./usr/share/foo/data"
+ assert missing == ["non-existent"]
+
+ # The symlink can be looked up
+ assert fs.lookup("./usr/share/test/loop-a") is not None
+ with pytest.raises(SymlinkLoopError):
+ # But resolving it will cause issues
+ fs.lookup("./usr/share/test/loop-a/")
diff --git a/tests/test_plugin_tester.py b/tests/test_plugin_tester.py
new file mode 100644
index 0000000..8078a02
--- /dev/null
+++ b/tests/test_plugin_tester.py
@@ -0,0 +1,285 @@
+import json
+import os.path
+from typing import List, Tuple, Type, cast
+
+import pytest
+
+from debputy.exceptions import DebputyFSIsROError
+from debputy.plugin.api import (
+ DebputyPluginInitializer,
+ BinaryCtrlAccessor,
+ PackageProcessingContext,
+ VirtualPath,
+ virtual_path_def,
+)
+from debputy.exceptions import PluginConflictError, PluginAPIViolationError
+from debputy.plugin.api.impl import DebputyPluginInitializerProvider
+from debputy.plugin.api.impl_types import automatic_discard_rule_example
+from debputy.plugin.api.test_api import (
+ build_virtual_file_system,
+ package_metadata_context,
+ initialize_plugin_under_test,
+)
+from debputy.plugin.api.test_api.test_impl import (
+ initialize_plugin_under_test_preloaded,
+ initialize_plugin_under_test_from_inline_json,
+)
+
+CUSTOM_PLUGIN_JSON_FILE = os.path.join(
+ os.path.dirname(__file__), "data", "custom-plugin.json.in"
+)
+
+
+def bad_metadata_detector_fs_rw(
+ path: VirtualPath,
+ _ctrl: BinaryCtrlAccessor,
+ _context: PackageProcessingContext,
+) -> None:
+ del path["foo"]
+
+
+def conflicting_plugin(api: DebputyPluginInitializer) -> None:
+ api.packager_provided_file(
+ "logrotate",
+ "/some/where/that/is/not/etc/logrotate.d/{name}",
+ )
+
+
+def bad_plugin(api: DebputyPluginInitializer) -> None:
+ api.metadata_or_maintscript_detector("fs_rw", bad_metadata_detector_fs_rw)
+
+
+def adr_inconsistent_example_plugin(api: DebputyPluginInitializerProvider) -> None:
+ api.automatic_discard_rule(
+ "adr-example-test",
+ lambda p: p.name == "discard-me",
+ examples=automatic_discard_rule_example(
+ "foo/discard-me",
+ ("bar/discard-me", False),
+ "baz/something",
+ ("discard-me/foo", False),
+ ),
+ )
+
+
+def test_conflict_with_debputy():
+ with pytest.raises(PluginConflictError) as e_info:
+ initialize_plugin_under_test_preloaded(
+ 1,
+ conflicting_plugin,
+ plugin_name="conflicting-plugin",
+ )
+ message = (
+ 'The stem "logrotate" is registered twice for packager provided files.'
+ " Once by debputy and once by conflicting-plugin"
+ )
+ assert message == e_info.value.args[0]
+
+
+def test_metadata_read_only():
+ plugin = initialize_plugin_under_test_preloaded(
+ 1,
+ bad_plugin,
+ plugin_name="bad-plugin",
+ )
+ fs = build_virtual_file_system(["./foo"])
+ with pytest.raises(PluginAPIViolationError) as e_info:
+ plugin.run_metadata_detector("fs_rw", fs)
+
+ assert isinstance(e_info.value.__cause__, DebputyFSIsROError)
+
+
+def test_packager_provided_files():
+ plugin = initialize_plugin_under_test(plugin_desc_file=CUSTOM_PLUGIN_JSON_FILE)
+ assert plugin.packager_provided_files_by_stem().keys() == {
+ "my-file",
+ "test-file-from-json",
+ }
+ my_file = [p for p in plugin.packager_provided_files() if p.stem == "my-file"][0]
+
+ assert my_file.stem == "my-file"
+ assert my_file.compute_dest("g++-3.1")[1] == "g__-3.1.conf"
+
+
+def test_path_checks():
+ symlink_path = "./foo"
+ with pytest.raises(ValueError) as e_info:
+ virtual_path_def(symlink_path, link_target="/bar", mode=0o0755)
+ assert (
+ e_info.value.args[0]
+ == f'Please do not provide mode for symlinks. Triggered by "{symlink_path}"'
+ )
+ with pytest.raises(ValueError) as e_info:
+ virtual_path_def(symlink_path + "/", link_target="/bar")
+ msg = (
+ "Path name looks like a directory, but a symlink target was also provided."
+ f' Please remove the trailing slash OR the symlink_target. Triggered by "{symlink_path}/"'
+ )
+ assert e_info.value.args[0] == msg
+
+
+def test_metadata_detector_applies_to_check():
+ plugin_name = "custom-plugin"
+ metadata_detector_id = "udeb-only"
+ plugin = initialize_plugin_under_test(plugin_desc_file=CUSTOM_PLUGIN_JSON_FILE)
+ with pytest.raises(ValueError) as e_info:
+ plugin.run_metadata_detector(
+ metadata_detector_id,
+ build_virtual_file_system(["./usr/share/doc/foo/copyright"]),
+ )
+ msg = f'The detector "{metadata_detector_id}" from {plugin_name} does not apply to the given package.'
+ assert e_info.value.args[0].startswith(msg)
+
+ metadata = plugin.run_metadata_detector(
+ metadata_detector_id,
+ build_virtual_file_system(["./usr/share/doc/foo/copyright"]),
+ context=package_metadata_context(package_fields={"Package-Type": "udeb"}),
+ )
+ assert metadata.substvars["Test:Udeb-Metadata-Detector"] == "was-run"
+
+
+@pytest.mark.parametrize(
+ "variables,exec_type",
+ [
+ (
+ [("DEBPUTY_VAR", "RESERVED")],
+ ValueError,
+ ),
+ (
+ [("_DEBPUTY_VAR", "RESERVED")],
+ ValueError,
+ ),
+ (
+ [("_FOO", "RESERVED")],
+ ValueError,
+ ),
+ (
+ [("path:_var", "RESERVED")],
+ ValueError,
+ ),
+ (
+ [("path:DEBPUTY_VAR", "RESERVED")],
+ ValueError,
+ ),
+ (
+ [("DEB_VAR", "RESERVED")],
+ ValueError,
+ ),
+ (
+ [("DPKG_VAR", "RESERVED")],
+ ValueError,
+ ),
+ (
+ [("PACKAGE", "RESERVED")],
+ ValueError,
+ ),
+ (
+ [("foo:var", "RESERVED")],
+ ValueError,
+ ),
+ (
+ [("env:var", "RESERVED")],
+ ValueError,
+ ),
+ (
+ [("SOURCE_DATE_EPOCH", "RESERVED")],
+ ValueError,
+ ),
+ (
+ [("!MY_VAR", "INVALID_NAME")],
+ ValueError,
+ ),
+ (
+ [("VALUE_DEPENDS_ON_VAR", "{{UNKNOWN_VAR}}")],
+ ValueError,
+ ),
+ (
+ [("VALUE_DEPENDS_ON_VAR", "{{DEB_HOST_ARCH}}")],
+ ValueError,
+ ),
+ (
+ [("DEFINED_TWICE", "ONCE"), ("DEFINED_TWICE", "TWICE")],
+ PluginConflictError,
+ ),
+ ],
+)
+def test_invalid_manifest_variables(
+ variables: List[Tuple[str, str]],
+ exec_type: Type[Exception],
+) -> None:
+ def _initializer(api: DebputyPluginInitializer):
+ with pytest.raises(exec_type):
+ for varname, value in variables:
+ api.manifest_variable(varname, value)
+
+ initialize_plugin_under_test_preloaded(
+ 1,
+ _initializer,
+ plugin_name="test-plugin",
+ load_debputy_plugin=False,
+ )
+
+
+def test_valid_manifest_variables() -> None:
+ variables = {
+ "PLUGIN_VAR": "TEST VALUE",
+ "ANOTHER_PLUGIN_VAR": "ANOTHER VALUE",
+ "path:SOMEWHERE_DIR": "/usr/share/some-where",
+ }
+
+ def _initializer(api: DebputyPluginInitializer):
+ for k, v in variables.items():
+ api.manifest_variable(k, v)
+
+ plugin = initialize_plugin_under_test_preloaded(
+ 1,
+ _initializer,
+ plugin_name="test-plugin",
+ load_debputy_plugin=False,
+ )
+
+ assert plugin.declared_manifest_variables == variables.keys()
+
+
+def test_valid_manifest_variables_json() -> None:
+ variables = {
+ "PLUGIN_VAR": "TEST VALUE",
+ "ANOTHER_PLUGIN_VAR": "ANOTHER VALUE",
+ "path:SOMEWHERE_DIR": "/usr/share/some-where",
+ }
+ content = {
+ "api-compat-version": 1,
+ "manifest-variables": [
+ {
+ "name": k,
+ "value": v,
+ }
+ for k, v in variables.items()
+ ],
+ }
+ plugin = initialize_plugin_under_test_from_inline_json(
+ "test-plugin",
+ json.dumps(content),
+ )
+ assert plugin.declared_manifest_variables == variables.keys()
+
+
+def test_automatic_discard_rules_example() -> None:
+ plugin = initialize_plugin_under_test_preloaded(
+ 1,
+ # Internal API used
+ cast("PluginInitializationEntryPoint", adr_inconsistent_example_plugin),
+ # API is restricted
+ plugin_name="debputy",
+ load_debputy_plugin=False,
+ )
+ issues = plugin.automatic_discard_rules_examples_with_issues()
+ assert len(issues) == 1
+ issue = issues[0]
+ assert issue.name == "adr-example-test"
+ assert issue.example_index == 0
+ assert set(issue.inconsistent_paths) == {
+ "/discard-me/foo",
+ "/bar/discard-me",
+ "/baz/something",
+ }
diff --git a/tests/test_substitute.py b/tests/test_substitute.py
new file mode 100644
index 0000000..a83cc7f
--- /dev/null
+++ b/tests/test_substitute.py
@@ -0,0 +1,66 @@
+import pytest
+
+from debputy.architecture_support import faked_arch_table
+from debputy.dh_migration.models import (
+ DHMigrationSubstitution,
+ AcceptableMigrationIssues,
+ FeatureMigration,
+)
+from debputy.filesystem_scan import FSROOverlay
+from debputy.highlevel_manifest import MutableYAMLManifest
+from debputy.substitution import SubstitutionImpl, VariableContext
+
+MOCK_ENV = {
+ # This conflicts with the dpkg arch table intentionally (to ensure we can tell which one is being resolved)
+ "DEB_HOST_ARCHITECTURE": "i386",
+}
+MOCK_DPKG_ARCH_TABLE = faked_arch_table("amd64", build_arch="i386")
+MOCK_VARIABLE_CONTEXT = VariableContext(FSROOverlay.create_root_dir("debian", "debian"))
+
+
+@pytest.mark.parametrize(
+ "value,expected",
+ [
+ (
+ "unchanged",
+ "unchanged",
+ ),
+ (
+ "unchanged\\{{\n}}",
+ "unchanged\\{{\n}}",
+ ), # Newline is not an allowed part of a substitution
+ (
+ "{{token:DOUBLE_OPEN_CURLY_BRACE}}{{token:NL}}{{token:DOUBLE_CLOSE_CURLY_BRACE}}",
+ "{{\n}}",
+ ),
+ (
+ "{{token:DOUBLE_OPEN_CURLY_BRACE}}token:TAB}}{{token:TAB{{token:DOUBLE_CLOSE_CURLY_BRACE}}",
+ "{{token:TAB}}{{token:TAB}}",
+ ),
+ (
+ "/usr/lib/{{DEB_HOST_MULTIARCH}}",
+ f'/usr/lib/{MOCK_DPKG_ARCH_TABLE["DEB_HOST_MULTIARCH"]}',
+ ),
+ ],
+)
+def test_substitution_match(debputy_plugin_feature_set, value, expected) -> None:
+ subst = SubstitutionImpl(
+ plugin_feature_set=debputy_plugin_feature_set,
+ dpkg_arch_table=MOCK_DPKG_ARCH_TABLE,
+ environment=MOCK_ENV,
+ variable_context=MOCK_VARIABLE_CONTEXT,
+ )
+ replacement = subst.substitute(value, "test def")
+ assert replacement == expected
+
+
+def test_migrate_substitution() -> None:
+ feature_migration = FeatureMigration("test migration")
+ subst = DHMigrationSubstitution(
+ MOCK_DPKG_ARCH_TABLE,
+ AcceptableMigrationIssues(frozenset()),
+ feature_migration,
+ MutableYAMLManifest({}),
+ )
+ replacement = subst.substitute("usr/lib/${DEB_HOST_MULTIARCH}/foo", "test def")
+ assert replacement == "usr/lib/{{DEB_HOST_MULTIARCH}}/foo"
diff --git a/tests/test_symbolic_mode.py b/tests/test_symbolic_mode.py
new file mode 100644
index 0000000..a0ad81d
--- /dev/null
+++ b/tests/test_symbolic_mode.py
@@ -0,0 +1,25 @@
+import pytest
+
+from debputy.manifest_parser.base_types import SymbolicMode
+
+
+@pytest.mark.parametrize(
+ "base_mode,is_dir,symbolic_mode,expected",
+ [
+ (0o0644, False, "u+rwX,og=rX", 0o0644),
+ (0o0000, False, "u+rwX,og=rX", 0o0644),
+ (0o0400, True, "u+rwX,og=rX", 0o0755),
+ (0o0000, True, "u+rwX,og=rX", 0o0755),
+ (0o2400, False, "u+rwxs,og=rx", 0o04755),
+ (0o7400, False, "u=rwX,og=rX", 0o0644),
+ (0o0641, False, "u=rwX,og=rX", 0o0755),
+ (0o4755, False, "a-x", 0o04644),
+ ],
+)
+def test_generate_deb_filename(
+ attribute_path, base_mode, is_dir, symbolic_mode, expected
+):
+ print(attribute_path.path)
+ parsed_mode = SymbolicMode.parse_filesystem_mode(symbolic_mode, attribute_path)
+ actual = parsed_mode.compute_mode(base_mode, is_dir)
+ assert oct(actual)[2:] == oct(expected)[2:]
diff --git a/tests/test_symlink_normalization.py b/tests/test_symlink_normalization.py
new file mode 100644
index 0000000..0b11dc8
--- /dev/null
+++ b/tests/test_symlink_normalization.py
@@ -0,0 +1,35 @@
+import pytest
+
+from debputy.util import debian_policy_normalize_symlink_target
+
+
+@pytest.mark.parametrize(
+ "link_path,link_target,expected",
+ [
+ ("usr/share/doc/pkg/my-symlink", "/etc/foo.conf", "/etc/foo.conf"),
+ ("usr/share/doc/pkg/my-symlink", "/usr/share/doc/pkg", "."),
+ ("usr/share/doc/pkg/my-symlink", "/usr/share/doc/pkg/.", "."),
+ ("usr/share/doc/pkg/my-symlink", "/usr/share/bar/../doc/pkg/.", "."),
+ (
+ "usr/share/doc/pkg/my-symlink",
+ "/usr/share/bar/../doc/pkg/../other-pkg",
+ "../other-pkg",
+ ),
+ ("usr/share/doc/pkg/my-symlink", "/usr/share/doc/other-pkg/.", "../other-pkg"),
+ ("usr/share/doc/pkg/my-symlink", "../other-pkg/.", "../other-pkg"),
+ ("usr/share/doc/pkg/my-symlink", "/usr/share/doc/other-pkg", "../other-pkg"),
+ ("usr/share/doc/pkg/my-symlink", "../other-pkg", "../other-pkg"),
+ (
+ "usr/share/doc/pkg/my-symlink",
+ "/usr/share/doc/pkg/../../../../etc/foo.conf",
+ "/etc/foo.conf",
+ ),
+ ],
+)
+def test_symlink_normalization(link_path: str, link_target: str, expected: str) -> None:
+ actual = debian_policy_normalize_symlink_target(
+ link_path,
+ link_target,
+ normalize_link_path=True,
+ )
+ assert actual == expected
diff --git a/tests/tutil.py b/tests/tutil.py
new file mode 100644
index 0000000..9b622b9
--- /dev/null
+++ b/tests/tutil.py
@@ -0,0 +1,66 @@
+from typing import Tuple, Mapping
+
+from debian.deb822 import Deb822
+from debian.debian_support import DpkgArchTable
+
+from debputy.architecture_support import (
+ faked_arch_table,
+ DpkgArchitectureBuildProcessValuesTable,
+)
+from debputy.packages import BinaryPackage
+
+_DPKG_ARCHITECTURE_TABLE_NATIVE_AMD64 = None
+_DPKG_ARCH_QUERY_TABLE = None
+
+
+def faked_binary_package(
+ package, architecture="any", section="misc", is_main_package=False, **fields
+) -> BinaryPackage:
+ _arch_data_tables_loaded()
+
+ dpkg_arch_table, dpkg_arch_query = _arch_data_tables_loaded()
+ return BinaryPackage(
+ Deb822(
+ {
+ "Package": package,
+ "Architecture": architecture,
+ "Section": section,
+ **fields,
+ }
+ ),
+ dpkg_arch_table,
+ dpkg_arch_query,
+ is_main_package=is_main_package,
+ )
+
+
+def binary_package_table(*args: BinaryPackage) -> Mapping[str, BinaryPackage]:
+ packages = list(args)
+ if not any(p.is_main_package for p in args):
+ p = args[0]
+ np = faked_binary_package(
+ p.name,
+ architecture=p.declared_architecture,
+ section=p.archive_section,
+ is_main_package=True,
+ **{
+ k: v
+ for k, v in p.fields.items()
+ if k.lower() not in ("package", "architecture", "section")
+ },
+ )
+ packages[0] = np
+ return {p.name: p for p in packages}
+
+
+def _arch_data_tables_loaded() -> (
+ Tuple[DpkgArchitectureBuildProcessValuesTable, DpkgArchTable]
+):
+ global _DPKG_ARCHITECTURE_TABLE_NATIVE_AMD64
+ global _DPKG_ARCH_QUERY_TABLE
+ if _DPKG_ARCHITECTURE_TABLE_NATIVE_AMD64 is None:
+ _DPKG_ARCHITECTURE_TABLE_NATIVE_AMD64 = faked_arch_table("amd64")
+ if _DPKG_ARCH_QUERY_TABLE is None:
+ # TODO: Make a faked table instead, so we do not have data dependencies in the test.
+ _DPKG_ARCH_QUERY_TABLE = DpkgArchTable.load_arch_table()
+ return _DPKG_ARCHITECTURE_TABLE_NATIVE_AMD64, _DPKG_ARCH_QUERY_TABLE