diff options
Diffstat (limited to '')
-rw-r--r-- | tests/test_parser.py | 473 |
1 files changed, 473 insertions, 0 deletions
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) |