debian-forge/test/mod/test_fmt_v1.py
Christian Kellner 53e9ec850b osbuild: assemblers are pipelines now
Convert the assembler phase of the main pipeline in the old format
into a new Pipeline that as the assembler as a stage, where the
input of that stage is the main pipeline. This removes the need of
having "assemblers" as special concepts and thus the corresponding
code in `Pipeline` is removed. The new assembler pipeline is marked
as exported, but the pipeline that builds the tree is not anymore.
Adapt the `describe` and `output` functions of the `v1` format to
handle the assembler pipeline. Also change the tests accordingly.

NB: The id reported for the assembler via `--inspect` and the result
will change as a result of this, since the assembler stage is now
the first and only stage of a new pipeline and thus has no base
anymore.
2021-01-22 15:03:19 +01:00

334 lines
10 KiB
Python

#
# Tests specific for version 1 of the format
#
import os
import pathlib
import sys
import tempfile
import unittest
from typing import Dict
import osbuild
import osbuild.meta
from osbuild.formats import v1 as fmt
from osbuild.monitor import NullMonitor
from osbuild.objectstore import ObjectStore
from .. import test
BASIC_PIPELINE = {
"pipeline": {
"build": {
"pipeline": {
"build": {
"pipeline": {
"stages": [
{
"name": "org.osbuild.noop",
"options": {"zero": 0}
}
]
},
"runner": "org.osbuild.linux"
},
"stages": [
{
"name": "org.osbuild.test",
"options": {"one": 1}
}
]
},
"runner": "org.osbuild.test"
},
"stages": [
{
"name": "org.osbuild.test",
"options": {"one": 2}
}
],
"assembler": {
"name": "org.osbuild.noop"
}
}
}
class TestFormatV1(unittest.TestCase):
@staticmethod
def build_manifest(manifest: osbuild.pipeline.Manifest, tmpdir: str):
"""Build a manifest and return the result"""
storedir = pathlib.Path(tmpdir, "store")
monitor = NullMonitor(sys.stderr.fileno())
libdir = os.path.abspath(os.curdir)
store = ObjectStore(storedir)
outdir = pathlib.Path(tmpdir, "out")
outdir.mkdir()
res = manifest.build(store, monitor, libdir, outdir)
return res
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"], {})
def test_load(self):
# Load a pipeline and check the resulting manifest
def check_stage(have: osbuild.Stage, want: Dict):
self.assertEqual(have.name, want["name"])
self.assertEqual(have.options, want.get("options", {}))
index = osbuild.meta.Index(os.curdir)
description = BASIC_PIPELINE
# load the manifest description, that will check all
# the stages can be found in the index and have valid
# arguments, i.e. the schema is correct
manifest = fmt.load(description, index)
self.assertIsNotNone(manifest)
# We have to have two build pipelines and a main pipeline
self.assertTrue(manifest.pipelines)
self.assertTrue(len(manifest.pipelines) == 4)
# access the individual pipelines via their names
# the inner most build pipeline
build = description["pipeline"]["build"]["pipeline"]["build"]
pl = manifest["build-build"]
have = pl.stages[0]
want = build["pipeline"]["stages"][0]
check_stage(have, want)
runner = build["runner"]
# the build pipeline for the 'tree' pipeline
build = description["pipeline"]["build"]
pl = manifest["build"]
have = pl.stages[0]
want = build["pipeline"]["stages"][0]
self.assertEqual(pl.runner, runner)
check_stage(have, want)
runner = build["runner"]
# the main, aka 'tree', pipeline
pl = manifest["tree"]
have = pl.stages[0]
want = description["pipeline"]["stages"][0]
self.assertEqual(pl.runner, runner)
check_stage(have, want)
# the assembler pipeline
pl = manifest["assembler"]
have = pl.stages[0]
want = description["pipeline"]["assembler"]
self.assertEqual(pl.runner, runner)
check_stage(have, want)
def test_describe(self):
index = osbuild.meta.Index(os.curdir)
manifest = fmt.load(BASIC_PIPELINE, index)
self.assertIsNotNone(manifest)
self.assertEqual(fmt.describe(manifest), BASIC_PIPELINE)
@unittest.skipUnless(test.TestBase.can_bind_mount(), "root-only")
def test_format_output(self):
"""Test that output formatting is as expected"""
index = osbuild.meta.Index(os.curdir)
description = {
"pipeline": {
"stages": [
{
"name": "org.osbuild.noop"
},
{
"name": "org.osbuild.error"
}
]
}
}
manifest = fmt.load(description, index)
self.assertIsNotNone(manifest)
with tempfile.TemporaryDirectory() as tmpdir:
res = self.build_manifest(manifest, tmpdir)
self.assertIsNotNone(res)
result = fmt.output(manifest, res)
self.assertIsNotNone(result)
self.assertIn("success", result)
self.assertFalse(result["success"])
self.assertIn("stages", result)
stages = result["stages"]
self.assertEqual(len(stages), 2)
self.assertTrue(stages[0]["success"])
self.assertFalse(stages[1]["success"])
# check we get results for the build pipeline
description = {
"pipeline": {
"build": {
"pipeline": {
"stages": [
{
"name": "org.osbuild.error"
}
]
},
"runner": "org.osbuild.test",
"stages": [
{
"name": "org.osbuild.noop"
}
]
}
}
}
manifest = fmt.load(description, index)
self.assertIsNotNone(manifest)
with tempfile.TemporaryDirectory() as tmpdir:
res = self.build_manifest(manifest, tmpdir)
self.assertIsNotNone(res)
result = fmt.output(manifest, res)
self.assertIsNotNone(result)
self.assertIn("success", result)
self.assertFalse(result["success"])
self.assertIn("build", result)
self.assertIn("success", result["build"])
self.assertFalse(result["build"]["success"])
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