debian-forge/test/mod/test_fmt_v1.py
Christian Kellner 569345cc72 pipeline: identify pipelines by name
Every pipeline that gets added to the `Manifest` now need to have
a unique name by which it can be identified. The version 1 format
loader is changed so that the main pipeline that builds the tree
is always called `tree`. The build pipeline for it will be called
`build` and further recursive build pipelines `build-build`, where
the number of repetitions of `build` corresponds to their level of
nesting. An assembler, if it exists, will be added as `assembler`.
The `Manifest.__getitem__` helper is changed so it will first try
to access pipeline via its name and then fall back to an id based
search. NB: in the degenrate case of multiple pipelines that have
exactly the same `id`, i.e. same stages, with the same options and
same build pipeline, only the first one will be return; but only
the first one here will be built as well, so this is in practice
not a problem.
The formatter uses this helper to get the tree pipeline  via its
name wherever it is needed.
This also adds an `__iter__` method `Manifest` to ease iterating
over just the pipeline values, a la `for pipeline in manifet`.
2021-01-22 15:03:19 +01:00

309 lines
9.3 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": {
"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)
print(libdir)
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["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 a build pipeline and a main pipeline
self.assertTrue(manifest.pipelines)
self.assertTrue(len(manifest.pipelines) == 2)
build = description["pipeline"]["build"]
pl = manifest["build"]
have = pl.stages[0]
want = build["pipeline"]["stages"][0]
check_stage(have, want)
runner = build["runner"]
# main pipeline is the next one
pl = manifest["tree"]
have = pl.stages[0]
want = description["pipeline"]["stages"][0]
self.assertEqual(pl.runner, runner)
check_stage(have, want)
# the assembler
have = pl.assembler
want = description["pipeline"]["assembler"]
self.assertEqual(have.name, want["name"])
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