From ea98e8a16714ba4581a1cdc75cb0bce64eea02f2 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sat, 28 Feb 2026 16:32:37 -0800 Subject: [PATCH 1/2] speed up MISRA mutation tests - skip scons build (cppcheck analyzes source directly) - add MISRA_ONLY mode: use --enable=style instead of --enable=all - skip copying .venv and caches in copytree (211MB -> 5MB) Co-Authored-By: Claude Opus 4.6 --- tests/misra/test_misra.sh | 7 ++++++- tests/misra/test_mutation.py | 10 +++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/tests/misra/test_misra.sh b/tests/misra/test_misra.sh index ba38568075c..4efc3535a99 100755 --- a/tests/misra/test_misra.sh +++ b/tests/misra/test_misra.sh @@ -59,7 +59,12 @@ cppcheck() { fi } -PANDA_OPTS="--enable=all --disable=unusedFunction --addon=misra" +if [ -n "$MISRA_ONLY" ]; then + # MISRA addon only (--enable=style needed for misra output severity) + PANDA_OPTS="--enable=style --addon=misra" +else + PANDA_OPTS="--enable=all --disable=unusedFunction --addon=misra" +fi printf "\n${GREEN}** PANDA H7 CODE **${NC}\n" cppcheck $PANDA_OPTS -DSTM32H7 -DSTM32H725xx -I $PANDA_DIR/board/stm32h7/inc/ $PANDA_DIR/board/main.c diff --git a/tests/misra/test_mutation.py b/tests/misra/test_mutation.py index 1f25f5ff1a5..2c7f8dba703 100755 --- a/tests/misra/test_mutation.py +++ b/tests/misra/test_mutation.py @@ -68,7 +68,9 @@ @pytest.mark.parametrize("fn, patch, should_fail", mutations) def test_misra_mutation(fn, patch, should_fail): with tempfile.TemporaryDirectory() as tmp: - shutil.copytree(ROOT, tmp + "/panda", dirs_exist_ok=True) + SKIP = {'.venv', '.git', '__pycache__', '.mypy_cache', '.ruff_cache', '.pytest_cache', 'pandacan.egg-info'} + shutil.copytree(ROOT, tmp + "/panda", dirs_exist_ok=True, + ignore=lambda d, files: [f for f in files if f in SKIP]) # apply patch if fn is not None: @@ -83,7 +85,9 @@ def test_misra_mutation(fn, patch, should_fail): with open(fpath, "w") as f: f.write(content) - # run test - r = subprocess.run("SKIP_TABLES_DIFF=1 panda/tests/misra/test_misra.sh", cwd=tmp, shell=True) + # run test (SKIP_BUILD: cppcheck doesn't need firmware binaries, + # MISRA_ONLY: skip non-misra checkers for speed) + env = "SKIP_TABLES_DIFF=1 SKIP_BUILD=1 MISRA_ONLY=1" + r = subprocess.run(f"{env} panda/tests/misra/test_misra.sh", cwd=tmp, shell=True) failed = r.returncode != 0 assert failed == should_fail From 175f1a581a2c5450d2fe60ce2a15b9164166a413 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sat, 28 Feb 2026 17:32:50 -0800 Subject: [PATCH 2/2] add fail-fast MISRA addon for mutation tests The new misra_failfast.py wrapper patches misra.py's reportError to sys.exit(1) on the first real violation, handling cppcheck's inline and macro suppressions to avoid false positives on clean code. Also removes sampling (all 12 tests run in ~38s with xdist) and adds board/crypto and board/certs to ignored mutation paths since they're only included from bootstub.c. Co-Authored-By: Claude Opus 4.6 --- tests/misra/misra_failfast.py | 46 +++++++++++++++++++++++++++++++++++ tests/misra/test_misra.sh | 10 +++++--- tests/misra/test_mutation.py | 5 ++-- 3 files changed, 56 insertions(+), 5 deletions(-) create mode 100644 tests/misra/misra_failfast.py diff --git a/tests/misra/misra_failfast.py b/tests/misra/misra_failfast.py new file mode 100644 index 00000000000..f62d0d899d4 --- /dev/null +++ b/tests/misra/misra_failfast.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +"""Wrapper around cppcheck's misra.py that exits on first violation. + +Used by mutation tests to fail fast — we only need to know if ANY +violation exists, not enumerate them all. This saves ~10s per test +by killing the 54MB XML dump parsing early. +""" +import sys + +# cppcheck prepends its addons/ dir to sys.path, so these are directly importable +import cppcheckdata +import misra + +_original_reportError = misra.MisraChecker.reportError + +def _failfast_reportError(self, location, num1, num2): + ruleNum = num1 * 100 + num2 + + # misra's own suppression checks + if self.isRuleGloballySuppressed(ruleNum): + return + if self.settings.verify: + return _original_reportError(self, location, num1, num2) + if self.isRuleSuppressed(location.file, location.linenr, ruleNum): + return _original_reportError(self, location, num1, num2) + + errorId = 'misra-c2012-%d.%d' % (num1, num2) + + # cppcheck's own suppression checks (file-pattern, line, block). + # with --cli these are normally applied AFTER the addon, so we check here. + if cppcheckdata.is_suppressed(location, '', errorId): + return _original_reportError(self, location, num1, num2) + + # cppcheck-suppress-macro: suppresses a rule for all expansions of a macro. + # is_suppressed doesn't handle type="macro", so check manually. + if getattr(location, 'macroName', None): + for s in cppcheckdata.current_dumpfile_suppressions: + if s.suppressionType == 'macro' and s.errorId == errorId: + return _original_reportError(self, location, num1, num2) + + # real violation — report it then bail out + _original_reportError(self, location, num1, num2) + sys.exit(1) + +misra.MisraChecker.reportError = _failfast_reportError +misra.main() diff --git a/tests/misra/test_misra.sh b/tests/misra/test_misra.sh index 4efc3535a99..214719ee73f 100755 --- a/tests/misra/test_misra.sh +++ b/tests/misra/test_misra.sh @@ -59,15 +59,19 @@ cppcheck() { fi } +CPPCHECK_COMMON="-DSTM32H7 -DSTM32H725xx -I $PANDA_DIR/board/stm32h7/inc/" + if [ -n "$MISRA_ONLY" ]; then - # MISRA addon only (--enable=style needed for misra output severity) - PANDA_OPTS="--enable=style --addon=misra" + # fast path for mutation testing: use fail-fast misra addon that exits on + # first violation. cppcheck reports this as "Failed to execute addon" error, + # which the grep below catches. saves ~10s per mutation test. + PANDA_OPTS="--enable=style --addon=$DIR/misra_failfast.py" else PANDA_OPTS="--enable=all --disable=unusedFunction --addon=misra" fi printf "\n${GREEN}** PANDA H7 CODE **${NC}\n" -cppcheck $PANDA_OPTS -DSTM32H7 -DSTM32H725xx -I $PANDA_DIR/board/stm32h7/inc/ $PANDA_DIR/board/main.c +cppcheck $PANDA_OPTS $CPPCHECK_COMMON $PANDA_DIR/board/main.c # unused needs to run globally #printf "\n${GREEN}** UNUSED ALL CODE **${NC}\n" diff --git a/tests/misra/test_mutation.py b/tests/misra/test_mutation.py index 2c7f8dba703..e5761833f89 100755 --- a/tests/misra/test_mutation.py +++ b/tests/misra/test_mutation.py @@ -23,6 +23,8 @@ 'board/bootstub.c', 'board/bootstub_declarations.h', 'board/stm32h7/llflash.h', + 'board/crypto', + 'board/certs', ) mutations = [ @@ -62,8 +64,7 @@ for p in patterns: mutations.append((rng.choice(files), p, True)) -# sample to keep CI fast, but always include the no-mutation case -mutations = [mutations[0]] + rng.sample(mutations[1:], min(2, len(mutations) - 1)) + @pytest.mark.parametrize("fn, patch, should_fail", mutations) def test_misra_mutation(fn, patch, should_fail):