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 ba38568075c..214719ee73f 100755 --- a/tests/misra/test_misra.sh +++ b/tests/misra/test_misra.sh @@ -59,10 +59,19 @@ cppcheck() { fi } -PANDA_OPTS="--enable=all --disable=unusedFunction --addon=misra" +CPPCHECK_COMMON="-DSTM32H7 -DSTM32H725xx -I $PANDA_DIR/board/stm32h7/inc/" + +if [ -n "$MISRA_ONLY" ]; then + # 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 1f25f5ff1a5..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,13 +64,14 @@ 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): 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 +86,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