debian-forge/test/mod/test_fmt_v1.py
2024-01-02 19:31:31 +01:00

437 lines
13 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 = {
"sources": {
"org.osbuild.files": {
"urls": {
"sha256:6eeebf21f245bf0d6f58962dc49b6dfb51f55acb6a595c6b9cbe9628806b80a4":
"https://internet/curl-7.69.1-1.fc32.x86_64.rpm",
}
},
"org.osbuild.ostree": {
"commits": {
"439911411ce7868a7b058c2a660e421991eb2df10e2bdce1fa559bd4390105d1": {
"remote": {
"url": "file:///repo",
"gpgkeys": ["data"]
}
}
}
}
},
"pipeline": {
"build": {
"pipeline": {
"build": {
"pipeline": {
"stages": [
{
"name": "org.osbuild.noop",
"options": {"zero": 0}
}
]
},
"runner": "org.osbuild.linux"
},
"stages": [
{
"name": "org.osbuild.noop",
"options": {"one": 1}
}
]
},
"runner": "org.osbuild.linux"
},
"stages": [
{
"name": "org.osbuild.noop",
"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)
with ObjectStore(storedir) as store:
res = manifest.build(store, manifest.pipelines, monitor, libdir)
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.name, 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.name, runner)
check_stage(have, want)
# the assembler pipeline
pl = manifest["assembler"]
have = pl.stages[0]
want = description["pipeline"]["assembler"]
self.assertEqual(pl.runner.name, 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)
def test_format_info(self):
index = osbuild.meta.Index(os.curdir)
lst = index.list_formats()
self.assertIn("osbuild.formats.v1", lst)
# an empty manifest is format "1"
info = index.detect_format_info({})
self.assertEqual(info.version, "1")
self.assertEqual(info.module, fmt)
# the basic test manifest
info = index.detect_format_info(BASIC_PIPELINE)
self.assertEqual(info.version, "1")
self.assertEqual(info.module, fmt)
# pylint: disable=too-many-statements
@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)
print(result)
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.linux",
"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"])
# check we get results for the assembler pipeline
description = {
"pipeline": {
"stages": [
{
"name": "org.osbuild.noop"
},
],
"assembler": {
"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("assembler", result)
self.assertIn("success", result["assembler"])
self.assertFalse(result["assembler"]["success"])
# check the overall result is False as well
self.assertIn("success", result)
self.assertFalse(result["success"], result)
# check we get all the output nodes for a successful build
description = {
"pipeline": {
"stages": [
{
"name": "org.osbuild.noop"
},
],
"assembler": {
"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("stages", result)
for stage in result["stages"]:
self.assertIn("success", stage)
self.assertTrue(stage["success"])
self.assertIn("assembler", result)
self.assertIn("success", result["assembler"])
self.assertTrue(result["assembler"]["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)
# the basic test manifest
res = fmt.validate(BASIC_PIPELINE, 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