Skip to content
Open
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
46 changes: 46 additions & 0 deletions tests/misra/misra_failfast.py
Original file line number Diff line number Diff line change
@@ -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()
13 changes: 11 additions & 2 deletions tests/misra/test_misra.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
15 changes: 10 additions & 5 deletions tests/misra/test_mutation.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
'board/bootstub.c',
'board/bootstub_declarations.h',
'board/stm32h7/llflash.h',
'board/crypto',
'board/certs',
)

mutations = [
Expand Down Expand Up @@ -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:
Expand All @@ -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
Loading