Files
forgefirm/forgetest/tests/test_tree_manifest.py
T
2026-09-18 12:14:22 -04:00

128 lines
5.3 KiB
Python

# Copyright 2026 514 LLC d/b/a OpenGlow
# Written by Scott Wiederhold
# https://community.openglow.org
# SPDX-License-Identifier: MIT
"""scripts/manifest-from-tree.py - the workstation/CI mirror of the image
manifest: recipe pins are read through their pin files, and a layer's
content hash leaves the pin files out (a component bump is not a platform
change; a recipe-body change is)."""
import importlib.util
import os
import shutil
import subprocess
import tempfile
import unittest
import helpers # noqa: F401 (sys.path)
REPO = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
SCRIPT = os.path.join(REPO, "scripts", "manifest-from-tree.py")
def load_script():
spec = importlib.util.spec_from_file_location("manifest_from_tree", SCRIPT)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
def git_ok():
try:
subprocess.run(["git", "--version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
return True
except (OSError, subprocess.CalledProcessError):
return False
RECIPE = '''DESCRIPTION = "component"
LICENSE = "MIT"
SRC_URI = "git://github.com/example/comp.git;protocol=https;branch=main"
# SRCREV and PV live in the pin file.
require comp-pin.inc
S = "${WORKDIR}/git"
'''
PIN_A = 'SRCREV = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"\nPV = "1.0"\n'
PIN_B = 'SRCREV = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"\nPV = "1.1"\n'
@unittest.skipUnless(git_ok(), "git not available")
class TreeManifestTests(unittest.TestCase):
def setUp(self):
self.mod = load_script()
self.tmp = tempfile.mkdtemp(prefix="forgetest-tree-")
self.layer = os.path.join(self.tmp, "meta-x")
self.rdir = os.path.join(self.layer, "recipes-x", "comp")
os.makedirs(self.rdir)
subprocess.run(["git", "init", "-q"], cwd=self.tmp, check=True,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
self.write("comp.bb", RECIPE)
self.write("comp-pin.inc", PIN_A)
self.write("README.md", "# docs\n")
def tearDown(self):
shutil.rmtree(self.tmp, ignore_errors=True)
def write(self, name, text):
with open(os.path.join(self.rdir, name), "w", encoding="utf-8", newline="\n") as f:
f.write(text)
def test_parse_recipe_follows_the_pin_file(self):
url, rev = self.mod.parse_recipe(os.path.join(self.rdir, "comp.bb"))
self.assertEqual(url, "https://github.com/example/comp.git")
self.assertEqual(rev, "a" * 40)
self.write("comp-pin.inc", PIN_B)
self.assertEqual(self.mod.parse_recipe(os.path.join(self.rdir, "comp.bb"))[1], "b" * 40)
def test_parse_recipe_inline_pin_still_works(self):
self.write("comp.bb", RECIPE.replace("require comp-pin.inc\n", PIN_A))
os.remove(os.path.join(self.rdir, "comp-pin.inc"))
self.assertEqual(self.mod.parse_recipe(os.path.join(self.rdir, "comp.bb"))[1], "a" * 40)
def test_layer_hash_ignores_pin_bumps_and_docs(self):
base = self.mod.layer_content(self.layer)
self.assertIsNotNone(base)
self.write("comp-pin.inc", PIN_B)
self.assertEqual(self.mod.layer_content(self.layer), base, "a pin bump is not layer content")
self.write("README.md", "# docs, revised\n")
self.assertEqual(self.mod.layer_content(self.layer), base, "documentation is not layer content")
def test_layer_hash_sees_recipe_body_changes(self):
base = self.mod.layer_content(self.layer)
self.write("comp.bb", RECIPE + 'EXTRA_OEMAKE += "KCFLAGS=-Werror"\n')
self.assertNotEqual(self.mod.layer_content(self.layer), base)
def test_layer_hash_sees_a_pin_written_into_the_recipe(self):
# The safe direction: a pin that bypasses its pin file still hashes.
base = self.mod.layer_content(self.layer)
self.write("comp.bb", RECIPE + PIN_B)
self.assertNotEqual(self.mod.layer_content(self.layer), base)
def test_layer_hash_matches_the_bbclass_rule(self):
# The same skip list as *.md plus FORGEFIRM_MANIFEST_PIN_SUFFIX and
# FORGEFIRM_MANIFEST_VERSION_SUFFIX in
# forgefirm-image-manifest.bbclass. The two implementations compute
# the same identity - one on the build host, one on a workstation -
# so a suffix added to either without the other is the failure this
# guards.
bbclass = os.path.join(REPO, "meta-forgefirm", "classes", "forgefirm-image-manifest.bbclass")
with open(bbclass, encoding="utf-8") as f:
text = f.read()
self.assertIn('FORGEFIRM_MANIFEST_PIN_SUFFIX ?= "-pin.inc"', text)
self.assertIn('FORGEFIRM_MANIFEST_VERSION_SUFFIX ?= "forgefirm-release.inc"', text)
self.assertEqual(self.mod.LAYER_SKIP_SUFFIXES,
(".md", "-pin.inc", "forgefirm-release.inc"))
def test_layer_hash_ignores_the_release_version(self):
# The version is metadata: setting the release number must not move
# the layer hash, or the bump would invalidate the campaign that
# authorizes the release.
self.write("forgefirm-release.inc",'FORGEFIRM_RELEASE ?= "0.0.1"\n')
base = self.mod.layer_content(self.layer)
self.write("forgefirm-release.inc",'FORGEFIRM_RELEASE ?= "0.0.2"\n')
self.assertEqual(self.mod.layer_content(self.layer), base)
if __name__ == "__main__":
unittest.main()