Add a new `info` property that holds the `meta.ModuleInfo` info for the stage. This gives each instance of a stage access to meta (or class) information about it, i.e. its schema, docs but, more importantly, also its name and path to the executable. Thefore the `name` property is coverted into a transient property which access the `name` member of `info`. Change the `formats/v1` load mechanism to carry a new `index` argument which is used to load the `ModuleInfo` for each stage. Adapt all tests to load the info as well when creating stages.
269 lines
8.7 KiB
Python
269 lines
8.7 KiB
Python
#
|
|
# 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.formats import v1 as fmt
|
|
from osbuild.monitor import NullMonitor
|
|
from osbuild.pipeline import detect_host_runner
|
|
from .. import test
|
|
|
|
|
|
class TestDescriptions(unittest.TestCase):
|
|
def test_canonical(self):
|
|
"""Degenerate case. Make sure we always return the same canonical
|
|
description when passing empty or null values."""
|
|
|
|
index = osbuild.meta.Index(os.curdir)
|
|
|
|
cases = [
|
|
{},
|
|
{"assembler": None},
|
|
{"stages": []},
|
|
{"build": {}},
|
|
{"build": None}
|
|
]
|
|
for pipeline in cases:
|
|
manifest = {"pipeline": pipeline}
|
|
with self.subTest(pipeline):
|
|
desc = fmt.describe(fmt.load(manifest, index))
|
|
self.assertEqual(desc["pipeline"], {})
|
|
|
|
@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, {})
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
|
|
data = pathlib.Path(tmpdir, "data")
|
|
cache = pathlib.Path(tmpdir, "cache")
|
|
root = pathlib.Path("/")
|
|
runner = detect_host_runner()
|
|
monitor = NullMonitor(sys.stderr.fileno())
|
|
libdir = os.path.abspath(os.curdir)
|
|
|
|
for p in [data, cache]:
|
|
p.mkdir()
|
|
|
|
res = stage.run(data, runner, root, cache, monitor, libdir)
|
|
|
|
self.assertEqual(res.success, True)
|
|
self.assertEqual(res.id, stage.id)
|
|
|
|
@unittest.skipUnless(test.TestBase.can_bind_mount(), "root-only")
|
|
def test_assembler_run(self):
|
|
asm = osbuild.Assembler("org.osbuild.noop", None, None, {})
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
|
|
data = pathlib.Path(tmpdir, "data")
|
|
cache = pathlib.Path(tmpdir, "cache")
|
|
output = pathlib.Path(tmpdir, "output")
|
|
root = pathlib.Path("/")
|
|
runner = detect_host_runner()
|
|
monitor = NullMonitor(sys.stderr.fileno())
|
|
libdir = os.path.abspath(os.curdir)
|
|
|
|
for p in [data, cache, output]:
|
|
p.mkdir()
|
|
|
|
res = asm.run(data, runner, root, monitor, libdir, output)
|
|
|
|
self.assertEqual(res.success, True)
|
|
self.assertEqual(res.id, asm.id)
|
|
|
|
def test_pipeline(self):
|
|
index = osbuild.meta.Index(os.curdir)
|
|
|
|
test_info = index.get_module_info("Stage", "org.osbuild.test")
|
|
build = osbuild.Pipeline("org.osbuild.test")
|
|
build.add_stage(test_info, {"one": 1})
|
|
|
|
pipeline = osbuild.Pipeline("org.osbuild.test", build.tree_id)
|
|
pipeline.add_stage(test_info, {"one": 2})
|
|
pipeline.set_assembler("org.osbuild.test")
|
|
|
|
manifest = osbuild.Manifest([build, pipeline], {})
|
|
|
|
self.assertEqual(fmt.describe(manifest), {
|
|
"pipeline": {
|
|
"build": {
|
|
"pipeline": {
|
|
"stages": [
|
|
{
|
|
"name": "org.osbuild.test",
|
|
"options": {"one": 1}
|
|
}
|
|
]
|
|
},
|
|
"runner": "org.osbuild.test"
|
|
},
|
|
"stages": [
|
|
{
|
|
"name": "org.osbuild.test",
|
|
"options": {"one": 2}
|
|
}
|
|
],
|
|
"assembler": {
|
|
"name": "org.osbuild.test"
|
|
}
|
|
}
|
|
})
|
|
|
|
def test_moduleinfo(self):
|
|
index = osbuild.meta.Index(os.curdir)
|
|
|
|
modules = []
|
|
for klass in ("Stage", "Assembler", "Source"):
|
|
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.schema, name)
|
|
res = schema.check()
|
|
if not res:
|
|
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_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)
|
|
|
|
def test_validation(self):
|
|
index = osbuild.meta.Index(os.curdir)
|
|
|
|
# an empty manifest is OK
|
|
res = fmt.validate({}, index)
|
|
self.assertEqual(res.valid, True)
|
|
|
|
# something totally invalid (by Ondřej Budai)
|
|
totally_invalid = {
|
|
"osbuild": {
|
|
"state": "awesome",
|
|
"but": {
|
|
"input-validation": 1
|
|
}
|
|
}
|
|
}
|
|
|
|
res = fmt.validate(totally_invalid, index)
|
|
self.assertEqual(res.valid, False)
|
|
# The top-level 'osbuild' is an additional property
|
|
self.assertEqual(len(res), 1)
|
|
|
|
# This is missing the runner
|
|
no_runner = {
|
|
"pipeline": {
|
|
"build": {
|
|
"pipeline": {}
|
|
}
|
|
}
|
|
}
|
|
|
|
res = fmt.validate(no_runner, index)
|
|
self.assertEqual(res.valid, False)
|
|
self.assertEqual(len(res), 1) # missing runner
|
|
lst = res[".pipeline.build"]
|
|
self.assertEqual(len(lst), 1)
|
|
|
|
# de-dup issues: the manifest checking will report
|
|
# the extra element and the recursive build pipeline
|
|
# check will also report that same error; make sure
|
|
# they get properly de-duplicated
|
|
no_runner_extra = {
|
|
"pipeline": {
|
|
"build": { # missing runner
|
|
"pipeline": {
|
|
"extra": True, # should not be there
|
|
"stages": [{
|
|
"name": "org.osbuild.chrony",
|
|
"options": {
|
|
"timeservers": "string" # should be an array
|
|
}
|
|
}]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
res = fmt.validate(no_runner_extra, index)
|
|
self.assertEqual(res.valid, False)
|
|
self.assertEqual(len(res), 3)
|
|
lst = res[".pipeline.build.pipeline"]
|
|
self.assertEqual(len(lst), 1) # should only have one
|
|
lst = res[".pipeline.build.pipeline.stages[0].options.timeservers"]
|
|
self.assertEqual(len(lst), 1) # should only have one
|
|
|
|
# stage issues
|
|
stage_check = {
|
|
"pipeline": {
|
|
"stages": [{
|
|
"name": "org.osbuild.grub2",
|
|
"options": {
|
|
"uefi": {
|
|
"install": False,
|
|
# missing "vendor"
|
|
},
|
|
# missing rootfs or root_fs_uuid
|
|
}
|
|
}]
|
|
}
|
|
}
|
|
|
|
res = fmt.validate(stage_check, index)
|
|
self.assertEqual(res.valid, False)
|
|
self.assertEqual(len(res), 2)
|
|
lst = res[".pipeline.stages[0].options"]
|
|
self.assertEqual(len(lst), 1) # missing rootfs
|
|
lst = res[".pipeline.stages[0].options.uefi"]
|
|
self.assertEqual(len(lst), 1) # missing "osname"
|
|
|
|
assembler_check = {
|
|
"pipeline": {
|
|
"assembler": {
|
|
"name": "org.osbuild.tar",
|
|
"options": {
|
|
"compression": "foobar"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
res = fmt.validate(assembler_check, index)
|
|
self.assertEqual(res.valid, False)
|
|
self.assertEqual(len(res), 2)
|
|
lst = res[".pipeline.assembler.options"]
|
|
self.assertEqual(len(lst), 1) # missing "filename"
|
|
lst = res[".pipeline.assembler.options.compression"]
|
|
self.assertEqual(len(lst), 1) # wrong compression method
|