Add STAGE_DESC, STAGE_INFO, and STAGE_OPTS to stages

This commit adds semi-structured documentation to all osbuild stages and
assemblers. The variables added work like this:

* STAGE_DESC: Short description of the stage.
* STAGE_INFO: Longer documentation of the stage, including expected
              behavior, required binaries, etc.
* STAGE_OPTS: A JSON Schema describing the stage's expected/allowed
              options. (see https://json-schema.org/ for details)

It also has a little unittest to check stageinfo - specifically:

1. All (executable) stages in stages/* and assemblers/ must define strings named
   STAGE_DESC, STAGE_INFO, and STAGE_OPTS
2. The contents of STAGE_OPTS must be valid JSON (if you put '{' '}'
   around it)
3. STAGE_OPTS, if non-empty, should have a "properties" object
4. if STAGE_OPTS lists "required" properties, those need to be present
   in the "properties" object.

The test is *not* included in .travis.yml because I'm not sure we want
to fail the build for this, but it's still helpful as a lint-style
check.
This commit is contained in:
Will Woods 2019-11-04 18:10:06 -05:00 committed by Tom Gundersen
parent 9d4b526a25
commit 6164b38fb9
25 changed files with 893 additions and 0 deletions

75
test/test_stageinfo.py Normal file
View file

@ -0,0 +1,75 @@
import json
import unittest
from pathlib import Path
class TestStageInfo(unittest.TestCase):
@staticmethod
def iter_stages(stagedir):
'''Yield executable files in `stagedir`'''
for p in Path(stagedir).iterdir():
if p.is_file() and not p.is_symlink() and p.stat().st_mode & 1:
yield p
@staticmethod
def get_stage_info(stage: Path) -> dict:
'''Return the STAGE_* variables from the given stage.'''
# TODO: This works for now, but stages should probably have some
# standard way of dumping this info so we (and other higher-level
# tools) don't have to parse the code and walk through the AST
# to find these values.
import ast
stage_info = {}
with open(stage) as fobj:
stage_ast = ast.parse(fobj.read(), filename=stage)
# STAGE_* assignments are at the toplevel, no need to walk()
for node in stage_ast.body:
if type(node) is ast.Assign and type(node.value) == ast.Str:
for target in node.targets:
if target.id.startswith("STAGE_"):
stage_info[target.id] = node.value.s
return stage_info
@staticmethod
def parse_stage_opts(stage_opts: str) -> dict:
if not stage_opts.lstrip().startswith('{'):
stage_opts = '{' + stage_opts + '}'
return json.loads(stage_opts)
def setUp(self):
self.topdir = Path(".") # NOTE: this could be smarter...
self.stages_dir = self.topdir / "stages"
self.assemblers_dir = self.topdir / "assemblers"
self.stages = list(self.iter_stages(self.stages_dir))
self.assemblers = list(self.iter_stages(self.assemblers_dir))
def check_stage_info(self, stage):
with self.subTest(check="STAGE_{INFO,DESC,OPTS} vars present"):
stage_info = self.get_stage_info(stage)
self.assertIn("STAGE_DESC", stage_info)
self.assertIn("STAGE_INFO", stage_info)
self.assertIn("STAGE_OPTS", stage_info)
with self.subTest(check="STAGE_OPTS is valid JSON"):
stage_opts = self.parse_stage_opts(stage_info["STAGE_OPTS"])
self.assertIsNotNone(stage_opts)
# TODO: we probably want an actual JSON Schema validator but
# a nicely basic sanity test for our current STAGE_OPTS is:
# 1. If it's not empty, there should be a "properties" object,
# 2. If "required" exists, each item should be a property name
with self.subTest(check="STAGE_OPTS is valid JSON Schema"):
if stage_opts:
self.assertIn("properties", stage_opts)
self.assertIsInstance(stage_opts["properties"], dict)
for prop in stage_opts.get("required", []):
self.assertIn(prop, stage_opts["properties"])
def test_stage_info(self):
for stage in self.stages:
with self.subTest(stage=stage.name):
self.check_stage_info(stage)
for assembler in self.assemblers:
with self.subTest(assembler=assembler.name):
self.check_stage_info(assembler)