From d4a0837cf01554f7f47821e5a1516f1a791925d2 Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Thu, 9 Nov 2023 17:53:24 +0100 Subject: [PATCH] stages(erofs): add tests and fix small bug in options handling This adds tests for the erofs stage. The tests are slightly different from the existing tests that run the filesystem utils inside the stages. Depending on what exactly we want to test we may still need a run inside the stages. However running this inside a container should be good enough if we just want to validate that the options are passed correctly and the file is created. --- stages/org.osbuild.erofs | 1 + stages/test/test_erofs.py | 125 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 stages/test/test_erofs.py diff --git a/stages/org.osbuild.erofs b/stages/org.osbuild.erofs index f74dabdc..2484b714 100755 --- a/stages/org.osbuild.erofs +++ b/stages/org.osbuild.erofs @@ -87,6 +87,7 @@ def main(inputs, output_dir, options): method += f",{compression['level']}" cmd += ["-z", method] + options = options.get("options") if options: cmd += ["-E", ",".join(options)] diff --git a/stages/test/test_erofs.py b/stages/test/test_erofs.py new file mode 100644 index 00000000..da534344 --- /dev/null +++ b/stages/test/test_erofs.py @@ -0,0 +1,125 @@ +#!/usr/bin/python3 + +import os.path +import subprocess +from unittest import mock + +import pytest + +import osbuild.meta +from osbuild.testutil import has_executable +from osbuild.testutil.imports import import_module_from_path + + +def make_fake_input_tree(tmpdir, fake_content: dict) -> str: + basedir = os.path.join(tmpdir, "tree") + for path, content in fake_content.items(): + dirp, name = os.path.split(os.path.join(basedir, path.lstrip("/"))) + os.makedirs(dirp, exist_ok=True) + with open(os.path.join(dirp, name), "w", encoding="utf-8") as fp: + fp.write(content) + return basedir + + +TEST_INPUT = [ + ({}, []), + ({"compression": {"method": "lz4"}}, ["-z", "lz4"]), + ({"compression": {"method": "lz4hc", "level": 9}}, ["-z", "lz4hc,9"]), + ({"options": ["dedupe"]}, ["-E", "dedupe"]), + ({"options": ["all-fragments", "force-inode-compact"]}, ["-E", "all-fragments,force-inode-compact"]), +] + + +@pytest.mark.skipif(not has_executable("mkfs.erofs"), reason="no mkfs.erofs") +@pytest.mark.parametrize("test_options,expected", TEST_INPUT) +def test_erofs_integration(tmp_path, test_options, expected): # pylint: disable=unused-argument + fake_input_tree = make_fake_input_tree(tmp_path, { + "/file-in-root.txt": "other content", + "/subdir/file-in-subdir.txt": "subdir content", + }) + inputs = { + "tree": { + "path": fake_input_tree, + } + } + stage_path = os.path.join(os.path.dirname(__file__), "../org.osbuild.erofs") + stage = import_module_from_path("erofs_stage", stage_path) + filename = "some-file.img" + options = { + "filename": filename, + } + options.update(test_options) + + stage.main(inputs, tmp_path, options) + + img_path = os.path.join(tmp_path, "some-file.img") + assert os.path.exists(img_path) + # validate the content + output = subprocess.check_output([ + "dump.erofs", "--ls", "--path=/", img_path], encoding="utf-8") + assert "subdir\n" in output + assert "file-in-root.txt\n" in output + output = subprocess.check_output([ + "dump.erofs", "--ls", "--path=/subdir", img_path], encoding="utf-8") + assert "file-in-subdir.txt\n" in output + + +@mock.patch("subprocess.run") +@pytest.mark.parametrize("test_options,expected", TEST_INPUT) +def test_erofs(mock_run, tmp_path, test_options, expected): + fake_input_tree = make_fake_input_tree(tmp_path, { + "/some-dir/some-file.txt": "content", + }) + inputs = { + "tree": { + "path": fake_input_tree, + } + } + stage_path = os.path.join(os.path.dirname(__file__), "../org.osbuild.erofs") + stage = import_module_from_path("erofs_stage", stage_path) + filename = "some-file.img" + options = { + "filename": filename, + } + options.update(test_options) + + stage.main(inputs, tmp_path, options) + + expected = [ + "mkfs.erofs", + f"{os.path.join(tmp_path, filename)}", + f"{fake_input_tree}", + ] + expected + mock_run.assert_called_with(expected, check=True) + + +@pytest.mark.parametrize("test_data,expected_err", [ + # bad + ({"extra": "option"}, "'extra' was unexpected"), + ({"compression": {"method": "invalid"}}, "'invalid' is not one of ["), + ({"compression": {"method": "lz4", "level": "string"}}, "'string' is not of type "), + # good + ({"compression": {"method": "lz4"}}, ""), +]) +def test_schema_validation_erofs(test_data, expected_err): + name = "org.osbuild.erofs" + root = os.path.join(os.path.dirname(__file__), "../..") + mod_info = osbuild.meta.ModuleInfo.load(root, "Stage", name) + schema = osbuild.meta.Schema(mod_info.get_schema(version="2"), name) + + test_input = { + "type": "org.osbuild.erofs", + "options": { + "filename": "some-filename.img", + } + } + test_input["options"].update(test_data) + res = schema.validate(test_input) + + if expected_err == "": + assert res.valid is True, f"err: {[e.as_dict() for e in res.errors]}" + else: + assert res.valid is False + assert len(res.errors) == 1, [e.as_dict() for e in res.errors] + err_msgs = [e.as_dict()["message"] for e in res.errors] + assert expected_err in err_msgs[0]