summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
authorhathach <[email protected]>2026-08-21 23:14:25 +0700
committerhathach <[email protected]>2026-08-21 23:14:25 +0700
commit6ff0ef97702c0e6b6d17b7a8fe856b31164efe57 (patch)
treeae456b01bc223be69db574be753c191c92abb83a /test
parent03a329eeda09e073d7de9be84df55d65392e4013 (diff)
build_utils: key the caches on the tree, not just the arguments
The eight lru_cache layers take repo-RELATIVE paths - 'hw/bsp/<fam>', 'examples/<ex>/skip.txt', the literal 'hw/bsp' glob - while ci_select._in_repo() chdirs around every call so one process can classify more than one tree. With no cwd in the key the second tree gets the first tree's answers. Reproduced: skip_example('host/bare_api','metro_m0_express') is False at the repo root and STILL False after chdir into a tree where that board does not exist; only cache_clear() gave the right answer. It bites the code-size skill's base-vs-branch worktree compare, /pre-pr, and the first test that points classify_build at a fixture tree. Master had no caching here, so the hazard arrived with it. _cwd_cache puts os.getcwd() in the key. The 199-test suite passed before only because every test happens to pass the real REPO; the new TestCachesAreKeyedOnTheTree crosses trees deliberately. Also adds the drift guard the class rule was missing. Ports, hw/mcu, get_deps tokens and bsp families each have one; the class rule had only a comment claiming vendor_host.c was the sole "enabled by no example config" case until its removal - which src/class/bth falsifies today. TestClassesWithNoEnablingExample pins the set to {bth}, so a class added before its first example, or an example config flipped to 0, fails here instead of silently selecting nothing on both axes. Verified it fires by adding a class dir nothing enables.
Diffstat (limited to 'test')
-rw-r--r--test/hil/test/test_ci_select.py63
1 files changed, 60 insertions, 3 deletions
diff --git a/test/hil/test/test_ci_select.py b/test/hil/test/test_ci_select.py
index 66b20b2e4..9b2c61ef2 100644
--- a/test/hil/test/test_ci_select.py
+++ b/test/hil/test/test_ci_select.py
@@ -840,6 +840,62 @@ class TestRostersDoNotOverlap(unittest.TestCase):
seen[b['name']] = b.get('tests')
+class TestCachesAreKeyedOnTheTree(unittest.TestCase):
+ """build_utils caches on repo-RELATIVE paths while ci_select._in_repo() chdirs
+ between trees, so the cwd has to be part of every cache key. Without it a second
+ tree gets the first tree's skip.txt/only.txt and FAMILY_MCUS - which is exactly the
+ base-vs-branch comparison the code-size skill does in one process."""
+
+ def test_a_second_tree_is_not_answered_from_the_first(self):
+ import build_utils, tempfile
+ old = os.getcwd()
+ try:
+ os.chdir(REPO)
+ self.assertFalse(build_utils.skip_example('host/bare_api', 'metro_m0_express'))
+ with tempfile.TemporaryDirectory() as d:
+ os.makedirs(os.path.join(d, 'hw/bsp'), exist_ok=True)
+ os.chdir(d)
+ # the board does not exist in this tree at all -> unknown board -> skip
+ self.assertTrue(build_utils.skip_example('host/bare_api', 'metro_m0_express'),
+ 'the empty tree was answered from the repo tree cache')
+ os.chdir(REPO)
+ self.assertFalse(build_utils.skip_example('host/bare_api', 'metro_m0_express'),
+ 'and the repo answer must survive the excursion')
+ finally:
+ os.chdir(old)
+
+
+class TestClassesWithNoEnablingExample(unittest.TestCase):
+ """The class rule is the one rule with no drift guard: ports, hw/mcu, get_deps
+ tokens and bsp families all have one. A class dir that no example config enables
+ selects NOTHING on both axes (the maintainer's empty-means-empty ruling), which is
+ right - but it must be a listed state, not a surprise, or a class added before its
+ first example silently stops being built."""
+
+ # class dirs no example's tusb_config.h turns on, for either role. Must only shrink:
+ # a new entry means a class nothing compiles, so a break in it reaches master.
+ NO_EXAMPLE = {'bth'}
+
+ def test_only_the_known_classes_select_nothing(self):
+ import glob as _glob
+ dead = set()
+ for d in sorted(_glob.glob(os.path.join(REPO, 'src/class/*'))):
+ if not os.path.isdir(d):
+ continue
+ cls = os.path.basename(d)
+ hit = False
+ for base in sorted(os.path.basename(f) for f in _glob.glob(os.path.join(d, '*.[ch]'))):
+ roles = ci_select._class_roles(base)
+ if ci_select._build_class_examples(cls, base, roles, REPO):
+ hit = True
+ break
+ if not hit:
+ dead.add(cls)
+ self.assertEqual(dead, self.NO_EXAMPLE,
+ 'a class dir enabled by no example config: it selects nothing on '
+ 'both axes, so nothing compiles it until the next master push')
+
+
class TestNoTrackedFileIsUnclassified(unittest.TestCase):
"""Rule 17 (unclassified -> full on both axes) is the fail-open net for paths nobody
anticipated. It must stay that way - a wrong `full` costs runner minutes and is
@@ -1373,11 +1429,12 @@ class TestBuildPostFilter(unittest.TestCase):
self.assertTrue(any('gone from tree' in r for r in s['reasons']), s['reasons'])
def test_class_source_selecting_nothing_selects_nothing(self):
- # synthetic class-with-no-enabling-config case (vendor_host.c was the live
- # instance until its removal): no config enables CFG_TUH_VENDOR, so
+ # a class-with-no-enabling-config case: no config enables CFG_TUH_VENDOR, so
# nothing exercises it and nothing builds - empty means empty (maintainer
# decision; the file is still parsed by every full master-push build, which is
- # the accepted net for a break outside its #if guard)
+ # the accepted net for a break outside its #if guard). src/class/bth is the
+ # live instance of this state today; TestClassesWithNoEnablingExample pins the
+ # whole set, so a new one cannot appear unnoticed.
s = ci_select.classify_build(['src/class/vendor/vendor_host.c'], REPO)
self.assertFalse(s['full'])
self.assertEqual(s['families'], [])