Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/selftest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
35 changes: 31 additions & 4 deletions share/frontends/iar_sbom.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <file> or <group> carries an <excluded> 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 <file> elements under el (the project or a <group>) that are
not excluded from cfg_name. An excluded <group> 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 <excluded> 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
Expand Down Expand Up @@ -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:
Expand Down
134 changes: 134 additions & 0 deletions tests/test_iar_sbom.py
Original file line number Diff line number Diff line change
@@ -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 <excluded> 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 = """<project>
<file><name>$PROJ_DIR$/a.c</name></file>
<file><name>$PROJ_DIR$/b.c</name>
<excluded><configuration>Release</configuration></excluded>
</file>
<file><name>$PROJ_DIR$/c.c</name>
<excluded><configuration>Debug</configuration></excluded>
</file>
</project>"""


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 <group>s, and an
# <excluded> block on a <group> drops every file in it and its subgroups.
# Group "lib" is excluded from Bench and Prof (two <configuration>s in one
# <excluded>), but not from Demo.
_EWP_GROUPS = """<project>
<configuration><name>Demo</name></configuration>
<configuration><name>Bench</name></configuration>
<configuration><name>Prof</name></configuration>
<group>
<name>App</name>
<file><name>$PROJ_DIR$\\app\\main.c</name></file>
</group>
<group>
<name>lib</name>
<excluded>
<configuration>Bench</configuration>
<configuration>Prof</configuration>
</excluded>
<file><name>$PROJ_DIR$\\lib\\internal.c</name></file>
<group>
<name>sub</name>
<file><name>$PROJ_DIR$\\lib\\sub\\keys.c</name></file>
</group>
</group>
<group>
<name>crypto</name>
<group>
<name>src</name>
<file><name>$PROJ_DIR$\\crypto\\aes.c</name></file>
<file><name>$PROJ_DIR$\\crypto\\evp.c</name>
<excluded>
<configuration>Demo</configuration>
<configuration>Prof</configuration>
</excluded>
</file>
</group>
</group>
</project>"""


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)
Loading