# # Basic tests for a collection of osbuild modules. # import json import os import pathlib import sys import tempfile import unittest import osbuild import osbuild.meta from osbuild.monitor import NullMonitor from osbuild.objectstore import ObjectStore from osbuild.pipeline import Manifest, detect_host_runner from .. import test def names(*lst): return [x.name for x in lst] class MockStore: def __init__(self) -> None: self.have = set() def contains(self, pipeline_id): return pipeline_id in self.have class TestDescriptions(unittest.TestCase): @unittest.skipUnless(test.TestBase.can_bind_mount(), "root-only") def test_stage_run(self): index = osbuild.meta.Index(os.curdir) info = index.get_module_info("Stage", "org.osbuild.noop") stage = osbuild.Stage(info, {}, None, None, {}, None) with tempfile.TemporaryDirectory() as tmpdir: data = pathlib.Path(tmpdir, "data") storedir = pathlib.Path(tmpdir, "store") root = pathlib.Path("/") runner = detect_host_runner() monitor = NullMonitor(sys.stderr.fileno()) libdir = os.path.abspath(os.curdir) store = ObjectStore(storedir) data.mkdir() res = stage.run(data, runner, root, store, monitor, libdir) self.assertEqual(res.success, True) self.assertEqual(res.id, stage.id) def test_manifest(self): index = osbuild.meta.Index(os.curdir) info = index.get_module_info("Stage", "org.osbuild.noop") manifest = Manifest() # each pipeline gets a noop stage with different # options so that their ids are different build = manifest.add_pipeline("build", None, None) build.add_stage(info, {"option": 1}) tree = manifest.add_pipeline("tree", "org.osbuild.linux", build.id) tree.add_stage(info, {"option": 2}) assembler = manifest.add_pipeline("assembler", "org.osbuild.inux", build.id) assembler.add_stage(info, {"option": 3}) self.assertEqual(len(manifest.pipelines), 3) self.assertIn("build", manifest.pipelines) self.assertIn("tree", manifest.pipelines) self.assertIn("assembler", manifest.pipelines) self.assertIn("build", manifest) self.assertIn("tree", manifest) self.assertIn("assembler", manifest) # make sure the order is correct lst = ["build", "tree", "assembler"] for a, b in zip(manifest.pipelines, lst): self.assertEqual(a, b) for a, b in zip(manifest, [build, tree, assembler]): self.assertEqual(a.name, b.name) # check we get exceptions on unknown names with self.assertRaises(KeyError): _ = manifest.pipelines["foo"] with self.assertRaises(KeyError): _ = manifest["foo"] # check helper functions # access by name check = manifest["build"] self.assertEqual(build.name, check.name) check = manifest["tree"] self.assertEqual(tree.name, check.name) check = manifest["assembler"] self.assertEqual(assembler.name, check.name) # `None` return for unknown items check = manifest.get("foo") self.assertIsNone(check) # id based access for i in [build, tree, assembler]: check = manifest[i.id] self.assertEqual(i.name, check.name) # pylint: disable=too-many-statements def test_on_demand(self): index = osbuild.meta.Index(os.curdir) manifest = Manifest() noop = index.get_module_info("Stage", "org.osbuild.noop") noip = index.get_module_info("Input", "org.osbuild.noop") # the shared build pipeline build = manifest.add_pipeline("build", None, None) build.add_stage(noop, {"option": 1}) # a pipeline simulating some intermediate artefact # that other pipeline need as dependency dep = manifest.add_pipeline("dep", "org.osbuild.linux", build.id) dep.add_stage(noop, {"option": 2}) # a pipeline that is not linked to the "main" # assembler artefact and thus should normally # not be built unless explicitly requested # has an input that depends on `dep` ul = manifest.add_pipeline("unlinked", "org.osbuild.linux", build.id) stage = ul.add_stage(noop, {"option": 3}) ip = stage.add_input("dep", noip, "org.osbuild.pipeline") ip.add_reference(dep.id) # the main os root file system rootfs = manifest.add_pipeline("rootfs", "org.osbuild.inux", build.id) stage = rootfs.add_stage(noop, {"option": 4}) # the main raw image artefact, depending on "dep" and # "rootfs" image = manifest.add_pipeline("image", "org.osbuild.inux", build.id) stage = image.add_stage(noop, {"option": 5}) ip = stage.add_input("dep", noip, "org.osbuild.pipeline") ip.add_reference(dep.id) stage = image.add_stage(noop, {"option": 6}) # a stage using the rootfs as input (named 'image') ip = stage.add_input("image", noip, "org.osbuild.pipeline") ip.add_reference(rootfs.id) # some compression of the image, like a qcow2 qcow2 = manifest.add_pipeline("qcow2", "org.osbuild.inux", build.id) stage = qcow2.add_stage(noop, {"option": 7}) ip = stage.add_input("image", noip, "org.osbuild.pipeline") ip.add_reference(image.id) fmt = index.get_format_info("osbuild.formats.v2").module self.assertIsNotNone(fmt) print(json.dumps(fmt.describe(manifest), indent=2)) # The pipeline graph in the manifest with dependencies: # ├─╼ build # ├─╼ dep # │ └ build # ├─╼ unlinked # │ ├ build # │ └ dep # ├─╼ rootfs # │ └ build # ├─╼ image # │ ├ build # │ ├ dep # │ └ rootfs # └─╼ qcow2 # ├ build # └ image store = MockStore() # check an empty input leads to an empty list res = manifest.depsolve(store, []) assert res == [] # the build pipeline should resolve to just itself res = manifest.depsolve(store, names(build)) assert res == names(build) # if we build the 'unlinked' pipeline, we get it # and its dependencies, dep and build res = manifest.depsolve(store, names(ul)) assert res == names(build, dep, ul) # building image with nothing in the store should # result in all pipelines but 'unlinked' res = manifest.depsolve(store, names(image)) assert res == names(build, rootfs, dep, image) # ensure the order of inputs is preserved during # the depsolving so that we build things in the # same way they were requested res = manifest.depsolve(store, names(ul, image)) assert res == names(build, dep, ul, rootfs, image) res = manifest.depsolve(store, names(image, ul)) assert res == names(build, rootfs, dep, image, ul) # if we have the 'dep' dependency in the store, # we should be not be building that store.have.add(dep.id) res = manifest.depsolve(store, names(image)) assert res == names(build, rootfs, image) # if we only have the build pipeline in the # store we should not build that store.have.clear() store.have.add(build.id) res = manifest.depsolve(store, names(image)) assert res == names(rootfs, dep, image) # if we have the final artefact in the store, # nothing should be built at all store.have.clear() store.have.add(image.id) res = manifest.depsolve(store, names(image)) assert res == [] # we have a checkpoint of the stage in the image # pipeline with the `dep` dependency, so that # it effectively only depends on `rootfs` store.have.clear() store.have.add(image.stages[0].id) res = manifest.depsolve(store, names(image)) assert res == names(build, rootfs, image) def check_moduleinfo(self, version): index = osbuild.meta.Index(os.curdir) modules = [] for klass in ("Assembler", "Device", "Input", "Mount", "Source", "Stage"): mods = index.list_modules_for_class(klass) modules += [(klass, module) for module in mods] self.assertTrue(modules) for module in modules: klass, name = module try: info = osbuild.meta.ModuleInfo.load(os.curdir, klass, name) schema = osbuild.meta.Schema(info.get_schema(version), name) res = schema.check() if not res: err = "SCHEMA: " + json.dumps(schema.data, indent=2) + "\n" err += "\n ".join(str(e) for e in res) self.fail(str(res) + "\n " + err) except json.decoder.JSONDecodeError as e: msg = f"{klass} '{name}' has invalid STAGE_OPTS\n\t" + str(e) self.fail(msg) def test_moduleinfo(self): for version in ["1", "2"]: with self.subTest(version=version): self.check_moduleinfo(version) def test_schema(self): schema = osbuild.meta.Schema(None) self.assertFalse(schema) schema = osbuild.meta.Schema({"type": "bool"}) # should be 'boolean' self.assertFalse(schema.check().valid) self.assertFalse(schema) schema = osbuild.meta.Schema({"type": "array", "minItems": 3}) self.assertTrue(schema.check().valid) self.assertTrue(schema) res = schema.validate([1, 2]) self.assertFalse(res) res = schema.validate([1, 2, 3]) self.assertTrue(res)