summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
authorhathach <[email protected]>2026-08-21 22:17:51 +0700
committerhathach <[email protected]>2026-08-21 22:17:51 +0700
commit03a329eeda09e073d7de9be84df55d65392e4013 (patch)
treeb59db5c7ecb4dc5c9f1e87d35634c9fc58a3d462 /test
parentfd70160a2f5fd23de1abfbaefb6399746a90b588 (diff)
ci_select: classify the 254 files that were reaching rule 17
Rule 17 (unclassified -> full on both axes) is the fail-open net for paths nobody anticipated, and it must stay that way: a wrong `full` costs runner minutes and is visible in the run, a wrong `empty` costs a merged regression and is invisible. But nothing in the tree should REACH it, and 254 tracked files did. The cost was real. PR #3842 changed a skill, a README and .gitignore; .gitignore matched no rule, so both axes went full and 74 cmake legs span up runners to do checkout + toolchain + get_deps before skipping the build, plus the whole 30-board rig. Three changes, none of which touch rule 17 itself: 1. _META_RE - repo metadata and tooling no Build step reads: .gitignore, .gitattributes, .clang-format, .codespellrc, .pre-commit-config.yaml, .readthedocs.yaml, .PVS-Studio/, .idea/, sonar-project.properties, the packaging manifests, CMakePresets, udev rules, test/{fuzz,unit-test} (their own jobs build those), the non-build .github/ files, and the tools/*.py scripts no build invokes. Deliberately NOT included, and still full: .circleci/**, .github/workflows/build*.yml, .github/actions/**, .github/scripts/**. The line is "does a Build step read this", not "is it source". 2. Rules 15 and 16 now match what they already claimed. Row 15 names examples/<role>/CMakeLists.txt and the regex never had it; row 16 says tools/build*.py but anchored tools/build\.py$. Both got the right answer only because rule 17 caught them on the way past. Also names their siblings - family_support.mk, family_rules.mk, src/CMakeLists.txt, src/tinyusb.mk - and .circleci/**, which generates the whole CircleCI matrix and was in no row at all. 3. src/typec/** gets row 12b. It is listed unconditionally by both build systems but its body is `#if CFG_TUC_ENABLED`, which only examples/typec/power_delivery sets - the same shape as the class rule, so the same answer: the examples that enable it (stm32g4 and stm32u5 after the buildability prune), and nothing on the rig, which runs no typec test. It was force-fulling 82 families and all 30 boards. TestNoTrackedFileIsUnclassified walks every tracked file and asserts none reaches rule 17, on both axes - 254 -> 0. Verified it fails when a new unclassified path appears. That turns 17 into what it should be: unreachable for anything in the tree, so it fires only for genuinely new shapes, and the author is told to write the row rather than letting the fall-through pick an answer for them. test_full_paths used sonar-project.properties as its stand-in for "unclassified"; that is now metadata, so the case moved to the new test_repo_metadata_is_not_a_build_input, with test_the_build_machinery_is_still_full pinning the other side of the line.
Diffstat (limited to 'test')
-rw-r--r--test/hil/test/test_ci_select.py62
1 files changed, 61 insertions, 1 deletions
diff --git a/test/hil/test/test_ci_select.py b/test/hil/test/test_ci_select.py
index 8f1841531..66b20b2e4 100644
--- a/test/hil/test/test_ci_select.py
+++ b/test/hil/test/test_ci_select.py
@@ -840,6 +840,45 @@ class TestRostersDoNotOverlap(unittest.TestCase):
seen[b['name']] = b.get('tests')
+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
+ visible in the run, a wrong `empty` costs a merged regression and is invisible - but
+ nothing in the tree should REACH it. Every tracked file is classified by a rule, so
+ 17 fires only for genuinely new shapes, and this test is what tells the author to
+ write the row instead of letting the fall-through pick an answer for them.
+
+ Before this guard, 254 tracked files reached 17: .gitignore took a docs-only PR to
+ 74 cmake legs and the whole rig, while examples/<role>/CMakeLists.txt got the RIGHT
+ answer from the wrong rule - row 15 names it, the regex never matched it."""
+
+ def _unclassified(self, axis):
+ import subprocess as sp
+ r = sp.run(['git', 'ls-files'], cwd=REPO, capture_output=True, text=True)
+ if r.returncode != 0:
+ self.skipTest('not a git checkout')
+ files = r.stdout.split()
+ self.assertGreater(len(files), 1000, 'suspiciously few tracked files')
+ out = []
+ for f in files:
+ s = (ci_select.classify_build([f], REPO) if axis == 'build'
+ else ci_select.classify([f], REPO, real_rosters()))
+ if any('unclassified' in why for why in s['reasons']):
+ out.append(f)
+ return out
+
+ def test_build_axis(self):
+ left = self._unclassified('build')
+ self.assertEqual(left, [], f'{len(left)} tracked files fall through to rule 17 on '
+ f'the build axis, e.g. {left[:5]} - classify them, or '
+ f'add the pattern to _META_RE if no build reads them')
+
+ def test_hil_axis(self):
+ left = self._unclassified('hil')
+ self.assertEqual(left, [], f'{len(left)} tracked files fall through to rule 17 on '
+ f'the HIL axis, e.g. {left[:5]}')
+
+
class TestLibRule(unittest.TestCase):
"""lib/** is not a full-matrix path: only the examples that build the lib need it."""
@@ -1241,7 +1280,28 @@ class TestBuildClassifier(unittest.TestCase):
'tools/build.py', 'tools/cmake/cpu/cortex-m4.cmake',
'examples/CMakeLists.txt', 'examples/device/CMakeLists.txt',
'examples/build_system/cmake/cpu.cmake', '.github/workflows/build.yml',
- 'sonar-project.properties', 'some/unknown/path.c'):
+ '.circleci/config.yml', 'src/CMakeLists.txt', 'src/tinyusb.mk',
+ 'hw/bsp/family_support.mk', 'tools/build_utils.py',
+ 'some/unknown/path.c'):
+ self.assertTrue(self.b([p])['full'], p)
+
+ def test_repo_metadata_is_not_a_build_input(self):
+ # these used to reach `full` through rule 17: a PR touching only .gitignore and a
+ # README created 74 cmake legs and booked the whole rig. No Build step reads them.
+ for p in ('sonar-project.properties', '.gitignore', '.gitattributes',
+ '.clang-format', '.idea/misc.xml', 'version.yml', 'library.json',
+ 'examples/CMakePresets.json', 'test/fuzz/fuzz.cc',
+ 'test/unit-test/project.yml', '.github/workflows/pr_comment.yml',
+ 'tools/gen_doc.py'):
+ s = self.b([p])
+ self.assertFalse(s['full'], p)
+ self.assertEqual(s['families'], [], p)
+
+ def test_the_build_machinery_is_still_full(self):
+ # the other side of the same line: these DECIDE what gets built
+ for p in ('.circleci/config.yml', '.github/workflows/build.yml',
+ '.github/scripts/ci_set_matrix.py', 'tools/ci_select.py',
+ 'tools/build_utils.py', 'tools/metrics.py'):
self.assertTrue(self.b([p])['full'], p)
def test_mixed_diff_unions_per_family(self):