From 2581160cfcc0be12badf726dcede76fe67aaf21d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Hozza?= Date: Tue, 30 Jan 2024 14:05:56 +0100 Subject: [PATCH] stages/test: introduce 'stage_schema' fixture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce a 'stage_schema' fixture, which will load the stage schema by the stage name defined in the STAGE_NAME defined in the test module and optionally provided schema version and return it. If no schema version is specified, version "2" is assumed. Modify all stage unit tests to use this fixture, instead of loading the stage schema on their own. Signed-off-by: Tomáš Hozza --- stages/test/conftest.py | 24 ++++++++++++++ stages/test/test_autotailor.py | 18 +++-------- stages/test/test_bootupd.py | 10 ++---- stages/test/test_containers_storage_conf.py | 10 ++---- stages/test/test_erofs.py | 9 ++---- stages/test/test_kickstart.py | 36 ++++++++++----------- stages/test/test_machine-id.py | 11 ++----- stages/test/test_mkfs_ext4.py | 9 ++---- stages/test/test_ostree_post_copy.py | 28 ++++++---------- stages/test/test_selinux.py | 22 ++++++------- stages/test/test_skopeo.py | 11 ++----- stages/test/test_xz.py | 9 ++---- stages/test/test_zstd.py | 9 ++---- 13 files changed, 81 insertions(+), 125 deletions(-) diff --git a/stages/test/conftest.py b/stages/test/conftest.py index 4f1bcfef..f2f9d5a7 100644 --- a/stages/test/conftest.py +++ b/stages/test/conftest.py @@ -4,6 +4,7 @@ from types import ModuleType import pytest +import osbuild.meta from osbuild.testutil.imports import import_module_from_path @@ -19,3 +20,26 @@ def stage_module(request: pytest.FixtureRequest) -> ModuleType: caller_dir = pathlib.Path(request.node.fspath).parent module_path = caller_dir.parent / stage_name return import_module_from_path("stage", os.fspath(module_path)) + + +@pytest.fixture +def stage_schema(request: pytest.FixtureRequest) -> osbuild.meta.Schema: + """stage_schema is a fixture returns the schema for a stage module. + + This fixture may be indirectly parametrized with the stage schema version. + If the schema version is not specified, the version "2" is assumed. + The stage name must be defined in STAGE_NAME in the test module. + """ + if hasattr(request, "param") and not isinstance(request.param, str): + raise ValueError( + "stage_schema fixture may be indirectly parametrized only with the stage schema version string") + + if not hasattr(request.module, "STAGE_NAME"): + raise ValueError("stage_schema fixture must be used in a test module that defines STAGE_NAME") + + stage_name = request.module.STAGE_NAME + schema_version = request.param if hasattr(request, "param") else "2" + caller_dir = pathlib.Path(request.node.fspath).parent + root = caller_dir.parent.parent + mod_info = osbuild.meta.ModuleInfo.load(root, "Stage", stage_name) + return osbuild.meta.Schema(mod_info.get_schema(version=schema_version), stage_name) diff --git a/stages/test/test_autotailor.py b/stages/test/test_autotailor.py index 5a99ea0a..3fdd07c0 100644 --- a/stages/test/test_autotailor.py +++ b/stages/test/test_autotailor.py @@ -1,12 +1,10 @@ #!/usr/bin/python3 -import os.path import sys from unittest.mock import call, patch import pytest -import osbuild.meta from osbuild import testutil TEST_INPUT = [ @@ -53,16 +51,6 @@ def fake_input_fixture(): } -def schema_validate_stage_oscap_autotailor(fake_input, test_data): - version = "1" - root = os.path.join(os.path.dirname(__file__), "../..") - mod_info = osbuild.meta.ModuleInfo.load(root, "Stage", STAGE_NAME) - schema = osbuild.meta.Schema(mod_info.get_schema(version=version), STAGE_NAME) - test_input = fake_input - test_input["options"]["config"].update(test_data) - return schema.validate(test_input) - - @pytest.mark.parametrize("test_overrides,expected", TEST_INPUT) @patch("subprocess.run") def test_oscap_autotailor_overrides_smoke(mock_subprocess_run, fake_input, stage_module, test_overrides, expected): @@ -107,8 +95,10 @@ def test_oscap_autotailor_overrides_smoke(mock_subprocess_run, fake_input, stage }, " is not of type 'string', 'integer'"), ], ) -def test_schema_validation_oscap_autotailor(fake_input, test_data, expected_err): - res = schema_validate_stage_oscap_autotailor(fake_input, test_data) +@pytest.mark.parametrize("stage_schema", ["1"], indirect=True) +def test_schema_validation_oscap_autotailor(fake_input, stage_schema, test_data, expected_err): + fake_input["options"]["config"].update(test_data) + res = stage_schema.validate(fake_input) assert res.valid is False testutil.assert_jsonschema_error_contains(res, expected_err, expected_num_errs=1) diff --git a/stages/test/test_bootupd.py b/stages/test/test_bootupd.py index 68d7b241..06cc485b 100644 --- a/stages/test/test_bootupd.py +++ b/stages/test/test_bootupd.py @@ -1,11 +1,9 @@ #!/usr/bin/python3 -import os.path from unittest.mock import call, patch import pytest -import osbuild.meta from osbuild import testutil STAGE_NAME = "org.osbuild.bootupd" @@ -38,18 +36,14 @@ STAGE_NAME = "org.osbuild.bootupd" }, "") ]) -def test_bootupd_schema_validation(test_data, expected_err): - root = os.path.join(os.path.dirname(__file__), "../..") - mod_info = osbuild.meta.ModuleInfo.load(root, "Stage", STAGE_NAME) - schema = osbuild.meta.Schema(mod_info.get_schema(version="2"), STAGE_NAME) - +def test_bootupd_schema_validation(stage_schema, test_data, expected_err): test_input = { "type": STAGE_NAME, "options": { } } test_input["options"].update(test_data) - res = schema.validate(test_input) + res = stage_schema.validate(test_input) if expected_err == "": assert res.valid is True, f"err: {[e.as_dict() for e in res.errors]}" diff --git a/stages/test/test_containers_storage_conf.py b/stages/test/test_containers_storage_conf.py index 787294ed..2aed0a5e 100644 --- a/stages/test/test_containers_storage_conf.py +++ b/stages/test/test_containers_storage_conf.py @@ -10,7 +10,6 @@ try: except ModuleNotFoundError: import pytoml as toml -import osbuild.meta from osbuild import testutil TEST_INPUT = [ @@ -95,12 +94,7 @@ def test_containers_storage_conf_integration(tmp_path, stage_module, test_filena ({}, {"options": {"pull_options": {"use_hard_links": True}}}, "is not of type 'string'"), ], ) -def test_schema_validation_containers_storage_conf(test_data, storage_test_data, expected_err): - version = "2" - root = os.path.join(os.path.dirname(__file__), "../..") - mod_info = osbuild.meta.ModuleInfo.load(root, "Stage", STAGE_NAME) - schema = osbuild.meta.Schema(mod_info.get_schema(version=version), STAGE_NAME) - +def test_schema_validation_containers_storage_conf(stage_schema, test_data, storage_test_data, expected_err): test_input = { "type": STAGE_NAME, "options": { @@ -113,7 +107,7 @@ def test_schema_validation_containers_storage_conf(test_data, storage_test_data, test_input["options"].update(test_data) test_input["options"]["config"]["storage"].update(storage_test_data) - res = schema.validate(test_input) + res = stage_schema.validate(test_input) if expected_err == "": assert res.valid is True, f"err: {[e.as_dict() for e in res.errors]}" diff --git a/stages/test/test_erofs.py b/stages/test/test_erofs.py index d70173dc..3b41519f 100644 --- a/stages/test/test_erofs.py +++ b/stages/test/test_erofs.py @@ -6,7 +6,6 @@ from unittest import mock import pytest -import osbuild.meta from osbuild import testutil from osbuild.testutil import has_executable, make_fake_input_tree @@ -89,11 +88,7 @@ def test_erofs(mock_run, tmp_path, stage_module, test_options, expected): # good ({"compression": {"method": "lz4"}}, ""), ]) -def test_schema_validation_erofs(test_data, expected_err): - root = os.path.join(os.path.dirname(__file__), "../..") - mod_info = osbuild.meta.ModuleInfo.load(root, "Stage", STAGE_NAME) - schema = osbuild.meta.Schema(mod_info.get_schema(version="2"), STAGE_NAME) - +def test_schema_validation_erofs(stage_schema, test_data, expected_err): test_input = { "type": STAGE_NAME, "options": { @@ -101,7 +96,7 @@ def test_schema_validation_erofs(test_data, expected_err): } } test_input["options"].update(test_data) - res = schema.validate(test_input) + res = stage_schema.validate(test_input) if expected_err == "": assert res.valid is True, f"err: {[e.as_dict() for e in res.errors]}" diff --git a/stages/test/test_kickstart.py b/stages/test/test_kickstart.py index 0a515ff3..d4aab3a8 100644 --- a/stages/test/test_kickstart.py +++ b/stages/test/test_kickstart.py @@ -5,7 +5,6 @@ import subprocess import pytest -import osbuild.meta from osbuild import testutil from osbuild.testutil import has_executable @@ -218,25 +217,18 @@ TEST_INPUT = [ STAGE_NAME = "org.osbuild.kickstart" -def schema_validate_kickstart_stage(test_data): - version = "1" - root = os.path.join(os.path.dirname(__file__), "../..") - mod_info = osbuild.meta.ModuleInfo.load(root, "Stage", STAGE_NAME) - schema = osbuild.meta.Schema(mod_info.get_schema(version=version), STAGE_NAME) - test_input = { +@pytest.mark.parametrize("stage_schema", ["1"], indirect=True) +@pytest.mark.parametrize("test_input,expected", TEST_INPUT) +def test_kickstart_test_cases_valid(stage_schema, test_input, expected): # pylint: disable=unused-argument + """ ensure all test inputs are valid """ + test_data = { "name": STAGE_NAME, "options": { "path": "some-path", } } - test_input["options"].update(test_data) - return schema.validate(test_input) - - -@pytest.mark.parametrize("test_input,expected", TEST_INPUT) -def test_kickstart_test_cases_valid(test_input, expected): # pylint: disable=unused-argument - """ ensure all test inputs are valid """ - res = schema_validate_kickstart_stage(test_input) + test_data["options"].update(test_input) + res = stage_schema.validate(test_data) assert res.valid is True, f"input: {test_input}\nerr: {[e.as_dict() for e in res.errors]}" @@ -270,7 +262,7 @@ def test_kickstart_valid(tmp_path, stage_module, test_input, expected): # pylin @pytest.mark.parametrize( - "test_data,expected_err", + "test_input,expected_err", [ # BAD pattern, ensure some obvious ways to write arbitrary # kickstart files will not work @@ -357,8 +349,16 @@ def test_kickstart_valid(tmp_path, stage_module, test_input, expected): # pylin ], ) -def test_schema_validation_bad_apples(test_data, expected_err): - res = schema_validate_kickstart_stage(test_data) +@pytest.mark.parametrize("stage_schema", ["1"], indirect=True) +def test_schema_validation_bad_apples(stage_schema, test_input, expected_err): + test_data = { + "name": STAGE_NAME, + "options": { + "path": "some-path", + } + } + test_data["options"].update(test_input) + res = stage_schema.validate(test_data) assert res.valid is False testutil.assert_jsonschema_error_contains(res, expected_err, expected_num_errs=1) diff --git a/stages/test/test_machine-id.py b/stages/test/test_machine-id.py index 1e164b78..741cfb49 100644 --- a/stages/test/test_machine-id.py +++ b/stages/test/test_machine-id.py @@ -1,12 +1,10 @@ #!/usr/bin/python3 import os -import pathlib import unittest.mock import pytest -import osbuild.meta from osbuild import testutil STAGE_NAME = "org.osbuild.machine-id" @@ -59,17 +57,14 @@ def test_machine_id_first_boot_preserve( @pytest.mark.parametrize("test_data,expected_err", [ ({"first-boot": "invalid-option"}, "'invalid-option' is not one of "), ]) -def test_machine_id_schema_validation(test_data, expected_err): - root = pathlib.Path(__file__).parents[2] - mod_info = osbuild.meta.ModuleInfo.load(root, "Stage", STAGE_NAME) - schema = osbuild.meta.Schema(mod_info.get_schema(), STAGE_NAME) - +@pytest.mark.parametrize("stage_schema", ["1"], indirect=True) +def test_machine_id_schema_validation(stage_schema, test_data, expected_err): test_input = { "name": STAGE_NAME, "options": {}, } test_input["options"].update(test_data) - res = schema.validate(test_input) + res = stage_schema.validate(test_input) assert res.valid is False testutil.assert_jsonschema_error_contains(res, expected_err, expected_num_errs=1) diff --git a/stages/test/test_mkfs_ext4.py b/stages/test/test_mkfs_ext4.py index 7cdd74cd..e83666da 100644 --- a/stages/test/test_mkfs_ext4.py +++ b/stages/test/test_mkfs_ext4.py @@ -7,7 +7,6 @@ from unittest import mock import pytest -import osbuild.meta from osbuild import testutil from osbuild.testutil import has_executable @@ -27,11 +26,7 @@ STAGE_NAME = "org.osbuild.mkfs.ext4" # uuid but our schema is not strict enough right now ({"uuid": "some-uuid"}, ""), ]) -def test_schema_validation_mkfs_ext4(test_data, expected_err): - root = os.path.join(os.path.dirname(__file__), "../..") - mod_info = osbuild.meta.ModuleInfo.load(root, "Stage", STAGE_NAME) - schema = osbuild.meta.Schema(mod_info.get_schema(version="2"), STAGE_NAME) - +def test_schema_validation_mkfs_ext4(stage_schema, test_data, expected_err): test_input = { "type": STAGE_NAME, "devices": { @@ -43,7 +38,7 @@ def test_schema_validation_mkfs_ext4(test_data, expected_err): } } test_input["options"].update(test_data) - res = schema.validate(test_input) + res = stage_schema.validate(test_input) if expected_err == "": assert res.valid is True, f"err: {[e.as_dict() for e in res.errors]}" diff --git a/stages/test/test_ostree_post_copy.py b/stages/test/test_ostree_post_copy.py index 836bc7fc..8ad70607 100644 --- a/stages/test/test_ostree_post_copy.py +++ b/stages/test/test_ostree_post_copy.py @@ -1,31 +1,14 @@ #!/usr/bin/python3 -import os.path from unittest.mock import call, patch import pytest -import osbuild.meta from osbuild import testutil STAGE_NAME = "org.osbuild.ostree.post-copy" -def schema_validate_stage_ostree_post_copy(test_data): - version = "2" - root = os.path.join(os.path.dirname(__file__), "../..") - mod_info = osbuild.meta.ModuleInfo.load(root, "Stage", STAGE_NAME) - schema = osbuild.meta.Schema(mod_info.get_schema(version=version), STAGE_NAME) - test_input = { - "type": STAGE_NAME, - "options": { - "sysroot": "/some/sysroot", - }, - } - test_input.update(test_data) - return schema.validate(test_input) - - @patch("osbuild.util.ostree.cli") def test_ostree_post_copy_smoke(mock_ostree_cli, stage_module): paths = { @@ -50,8 +33,15 @@ def test_ostree_post_copy_smoke(mock_ostree_cli, stage_module): ({"mounts": "must-be-array"}, " is not of type 'array'"), ], ) -def test_schema_validation_ostree_post_copy(test_data, expected_err): - res = schema_validate_stage_ostree_post_copy(test_data) +def test_schema_validation_ostree_post_copy(stage_schema, test_data, expected_err): + test_input = { + "type": STAGE_NAME, + "options": { + "sysroot": "/some/sysroot", + }, + } + test_input.update(test_data) + res = stage_schema.validate(test_input) assert res.valid is False testutil.assert_jsonschema_error_contains(res, expected_err, expected_num_errs=1) diff --git a/stages/test/test_selinux.py b/stages/test/test_selinux.py index fda8c633..469d46d4 100644 --- a/stages/test/test_selinux.py +++ b/stages/test/test_selinux.py @@ -5,17 +5,12 @@ from unittest.mock import call, patch import pytest -import osbuild.meta from osbuild import testutil STAGE_NAME = "org.osbuild.selinux" -def schema_validation_selinux(test_data, implicit_file_contexts=True): - root = os.path.join(os.path.dirname(__file__), "../..") - mod_info = osbuild.meta.ModuleInfo.load(root, "Stage", STAGE_NAME) - schema = osbuild.meta.Schema(mod_info.get_schema(version="1"), STAGE_NAME) - +def get_test_input(test_data, implicit_file_contexts=True): test_input = { "name": STAGE_NAME, "options": {} @@ -24,7 +19,7 @@ def schema_validation_selinux(test_data, implicit_file_contexts=True): test_input["options"]["file_contexts"] = "some-context" test_input["options"].update(test_data) - return schema.validate(test_input) + return test_input @pytest.mark.parametrize("test_data,expected_err", [ @@ -36,8 +31,9 @@ def schema_validation_selinux(test_data, implicit_file_contexts=True): ({"labels": "xxx"}, "'xxx' is not of type 'object'"), ({"force_autorelabel": "foo"}, "'foo' is not of type 'boolean'"), ]) -def test_schema_validation_selinux(test_data, expected_err): - res = schema_validation_selinux(test_data) +@pytest.mark.parametrize("stage_schema", ["1"], indirect=True) +def test_schema_validation_selinux(stage_schema, test_data, expected_err): + res = stage_schema.validate(get_test_input(test_data)) if expected_err == "": assert res.valid is True, f"err: {[e.as_dict() for e in res.errors]}" else: @@ -45,9 +41,9 @@ def test_schema_validation_selinux(test_data, expected_err): testutil.assert_jsonschema_error_contains(res, expected_err, expected_num_errs=1) -def test_schema_validation_selinux_file_context_required(): - test_data = {} - res = schema_validation_selinux(test_data, implicit_file_contexts=False) +@pytest.mark.parametrize("stage_schema", ["1"], indirect=True) +def test_schema_validation_selinux_file_context_required(stage_schema): + res = stage_schema.validate(get_test_input({}, implicit_file_contexts=False)) assert res.valid is False expected_err = "'file_contexts' is a required property" testutil.assert_jsonschema_error_contains(res, expected_err, expected_num_errs=1) @@ -69,7 +65,7 @@ def test_selinux_file_contexts(mocked_setfiles, tmp_path, stage_module): @patch("osbuild.util.selinux.setfilecon") @patch("osbuild.util.selinux.setfiles") def test_selinux_labels(mocked_setfiles, mocked_setfilecon, tmp_path, stage_module): - osbuild.testutil.make_fake_input_tree(tmp_path, { + testutil.make_fake_input_tree(tmp_path, { "/usr/bin/bootc": "I'm only an imposter", }) diff --git a/stages/test/test_skopeo.py b/stages/test/test_skopeo.py index 2b2f0ae8..2b591e72 100644 --- a/stages/test/test_skopeo.py +++ b/stages/test/test_skopeo.py @@ -1,10 +1,7 @@ #!/usr/bin/python3 -import os.path - import pytest -import osbuild.meta from osbuild import testutil STAGE_NAME = "org.osbuild.skopeo" @@ -23,17 +20,13 @@ STAGE_NAME = "org.osbuild.skopeo" # *inputs* and it'll be a no-op in the stage ({"destination": {"type": "containers-storage"}}, ""), ]) -def test_schema_validation_skopeo(test_data, expected_err): - root = os.path.join(os.path.dirname(__file__), "../..") - mod_info = osbuild.meta.ModuleInfo.load(root, "Stage", STAGE_NAME) - schema = osbuild.meta.Schema(mod_info.get_schema(version="2"), STAGE_NAME) - +def test_schema_validation_skopeo(stage_schema, test_data, expected_err): test_input = { "type": STAGE_NAME, "options": {}, } test_input["options"].update(test_data) - res = schema.validate(test_input) + res = stage_schema.validate(test_input) if expected_err == "": assert res.valid is True, f"err: {[e.as_dict() for e in res.errors]}" diff --git a/stages/test/test_xz.py b/stages/test/test_xz.py index 705f959d..02415ce9 100644 --- a/stages/test/test_xz.py +++ b/stages/test/test_xz.py @@ -6,7 +6,6 @@ from unittest import mock import pytest -import osbuild.meta from osbuild import testutil from osbuild.testutil import has_executable, make_fake_input_tree @@ -19,18 +18,14 @@ STAGE_NAME = "org.osbuild.xz" # good ({"filename": "image.xz"}, ""), ]) -def test_schema_validation_xz(test_data, expected_err): - root = os.path.join(os.path.dirname(__file__), "../..") - mod_info = osbuild.meta.ModuleInfo.load(root, "Stage", STAGE_NAME) - schema = osbuild.meta.Schema(mod_info.get_schema(version="2"), STAGE_NAME) - +def test_schema_validation_xz(stage_schema, test_data, expected_err): test_input = { "type": STAGE_NAME, "options": { } } test_input["options"].update(test_data) - res = schema.validate(test_input) + res = stage_schema.validate(test_input) if expected_err == "": assert res.valid is True, f"err: {[e.as_dict() for e in res.errors]}" diff --git a/stages/test/test_zstd.py b/stages/test/test_zstd.py index 8d9be2a8..8cccde13 100644 --- a/stages/test/test_zstd.py +++ b/stages/test/test_zstd.py @@ -6,7 +6,6 @@ from unittest import mock import pytest -import osbuild.meta from osbuild import testutil from osbuild.testutil import has_executable, make_fake_input_tree @@ -19,18 +18,14 @@ STAGE_NAME = "org.osbuild.zstd" # good ({"filename": "image.zst"}, ""), ]) -def test_schema_validation_zstd(test_data, expected_err): - root = os.path.join(os.path.dirname(__file__), "../..") - mod_info = osbuild.meta.ModuleInfo.load(root, "Stage", STAGE_NAME) - schema = osbuild.meta.Schema(mod_info.get_schema(version="2"), STAGE_NAME) - +def test_schema_validation_zstd(stage_schema, test_data, expected_err): test_input = { "type": STAGE_NAME, "options": { } } test_input["options"].update(test_data) - res = schema.validate(test_input) + res = stage_schema.validate(test_input) if expected_err == "": assert res.valid is True, f"err: {[e.as_dict() for e in res.errors]}"