diff --git a/.github/workflows/selftest.yml b/.github/workflows/selftest.yml index 1317550..2928aee 100644 --- a/.github/workflows/selftest.yml +++ b/.github/workflows/selftest.yml @@ -48,6 +48,7 @@ jobs: tests/test_gen_sbom.py \ tests/test_sbom.py \ tests/test_sbom_identity.py \ + tests/test_iar_sbom.py \ tests/test_wolfglass_sync.py - name: Run generator unit tests @@ -62,6 +63,9 @@ jobs: - name: Run SBOM identity tests run: python -m unittest tests/test_sbom_identity.py + - name: Run IAR frontend tests + run: python -m unittest tests/test_iar_sbom.py + - name: Run wolfglass-sync tests run: python -m unittest tests/test_wolfglass_sync.py diff --git a/share/frontends/iar_sbom.py b/share/frontends/iar_sbom.py index 788bdff..839dae0 100755 --- a/share/frontends/iar_sbom.py +++ b/share/frontends/iar_sbom.py @@ -61,10 +61,37 @@ def parse_configs(root): return configs -def collect_sources(root, proj_dir): - """Return (present, missing) absolute paths of compiled source files.""" +def _excluded_from(el, cfg_name): + """True if a or carries an block naming + cfg_name -- IAR drops it from that build configuration, so it is not + compiled in.""" + excluded = el.find('excluded') + if excluded is None: + return False + return any((c.text or '').strip() == cfg_name + for c in excluded.findall('configuration')) + + +def _compiled_files(el, cfg_name): + """Yield the elements under el (the project or a ) that are + not excluded from cfg_name. An excluded drops every file in it + and in its nested groups.""" + for child in el: + if _excluded_from(child, cfg_name): + continue + if child.tag == 'group': + yield from _compiled_files(child, cfg_name) + elif child.tag == 'file': + yield child + + +def collect_sources(root, proj_dir, cfg_name): + """Return (present, missing) absolute paths of the source files compiled in + configuration cfg_name. Files and groups IAR marks for + cfg_name are dropped; listing them would over-report the compiled source + set.""" srcs = [] - for file_el in root.iter('file'): + for file_el in _compiled_files(root, cfg_name): name_el = file_el.find('name') if name_el is None or not name_el.text: continue @@ -136,7 +163,7 @@ def main(): cfg_name = max(configs, key=lambda k: len(configs[k])) defines = configs[cfg_name] - srcs, missing = collect_sources(root, proj_dir) + srcs, missing = collect_sources(root, proj_dir, cfg_name) if not srcs: sys.exit("ERROR: no existing source files found in .ewp") if missing: diff --git a/tests/test_iar_sbom.py b/tests/test_iar_sbom.py new file mode 100644 index 0000000..0d59a86 --- /dev/null +++ b/tests/test_iar_sbom.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +"""Tests for the IAR frontend (share/frontends/iar_sbom.py). + +Focus: collect_sources must honour IAR's per-configuration markers, +so a file excluded from the selected build configuration is not reported as a +compiled source (which would over-report the artifact's source set).""" + +import importlib.util +import os +import tempfile +import unittest +import xml.etree.ElementTree as ET + +REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +IAR = os.path.join(REPO, "share", "frontends", "iar_sbom.py") + +_spec = importlib.util.spec_from_file_location("iar_sbom", IAR) +iar = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(iar) + +# a.c: compiled in every config; b.c: excluded from Release; +# c.c: excluded from Debug only. +_EWP = """ + $PROJ_DIR$/a.c + $PROJ_DIR$/b.c + Release + + $PROJ_DIR$/c.c + Debug + +""" + + +class TestExcludedFiles(unittest.TestCase): + def setUp(self): + self.tmp = tempfile.TemporaryDirectory() + self.proj = self.tmp.name + for n in ("a.c", "b.c", "c.c"): + open(os.path.join(self.proj, n), "w").close() + self.root = ET.fromstring(_EWP) + + def tearDown(self): + self.tmp.cleanup() + + def _names(self, cfg): + present, _missing = iar.collect_sources(self.root, self.proj, cfg) + return {os.path.basename(p) for p in present} + + def test_release_drops_file_excluded_from_release(self): + names = self._names("Release") + self.assertIn("a.c", names) + self.assertIn("c.c", names) # excluded from Debug, not Release + self.assertNotIn("b.c", names) # excluded from Release + + def test_debug_drops_file_excluded_from_debug(self): + names = self._names("Debug") + self.assertIn("a.c", names) + self.assertIn("b.c", names) # excluded from Release, not Debug + self.assertNotIn("c.c", names) # excluded from Debug + + +# Shaped like a real EWARM .ewp: files live in nested s, and an +# block on a drops every file in it and its subgroups. +# Group "lib" is excluded from Bench and Prof (two s in one +# ), but not from Demo. +_EWP_GROUPS = """ + Demo + Bench + Prof + + App + $PROJ_DIR$\\app\\main.c + + + lib + + Bench + Prof + + $PROJ_DIR$\\lib\\internal.c + + sub + $PROJ_DIR$\\lib\\sub\\keys.c + + + + crypto + + src + $PROJ_DIR$\\crypto\\aes.c + $PROJ_DIR$\\crypto\\evp.c + + Demo + Prof + + + + +""" + + +class TestGroupExclusion(unittest.TestCase): + def setUp(self): + self.tmp = tempfile.TemporaryDirectory() + self.proj = self.tmp.name + for rel in ("app/main.c", "lib/internal.c", "lib/sub/keys.c", + "crypto/aes.c", "crypto/evp.c"): + path = os.path.join(self.proj, rel) + os.makedirs(os.path.dirname(path), exist_ok=True) + open(path, "w").close() + self.root = ET.fromstring(_EWP_GROUPS) + + def tearDown(self): + self.tmp.cleanup() + + def _rels(self, cfg): + present, missing = iar.collect_sources(self.root, self.proj, cfg) + self.assertEqual(missing, []) + return sorted(os.path.relpath(p, self.proj) for p in present) + + def test_group_not_excluded_keeps_nested_files(self): + self.assertEqual(self._rels("Demo"), [ + "app/main.c", "crypto/aes.c", "lib/internal.c", "lib/sub/keys.c"]) + + def test_excluded_group_drops_files_and_subgroups(self): + self.assertEqual(self._rels("Bench"), [ + "app/main.c", "crypto/aes.c", "crypto/evp.c"]) + + def test_second_configuration_in_excluded_block(self): + self.assertEqual(self._rels("Prof"), ["app/main.c", "crypto/aes.c"]) + + +if __name__ == "__main__": + unittest.main(verbosity=2)