Files
panda/tests/misra/test_mutation.py
Adeeb Shihadeh 175f1a581a 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 <noreply@anthropic.com>
2026-02-28 17:32:50 -08:00

95 lines
3.0 KiB
Python
Executable File

#!/usr/bin/env python3
import os
import glob
import pytest
import shutil
import subprocess
import tempfile
import random
HERE = os.path.abspath(os.path.dirname(__file__))
ROOT = os.path.join(HERE, "../../")
# skip mutating these paths
IGNORED_PATHS = (
'board/obj',
'board/jungle',
'board/body',
'board/stm32h7/inc',
'board/fake_stm.h',
# bootstub only files
'board/flasher.h',
'board/bootstub.c',
'board/bootstub_declarations.h',
'board/stm32h7/llflash.h',
'board/crypto',
'board/certs',
)
mutations = [
(None, None, False), # no mods, should pass
("board/stm32h7/llfdcan.h", "s/return ret;/if (true) { return ret; } else { return false; }/g", True),
]
patterns = [
# misra-c2012-13.3
"$a void test(int tmp) { int tmp2 = tmp++ + 2; if (tmp2) {;}}",
# misra-c2012-13.4
"$a int test(int x, int y) { return (x=2) && (y=2); }",
# misra-c2012-13.5
"$a void test(int tmp) { if (true && tmp++) {;} }",
# misra-c2012-13.6
"$a void test(int tmp) { if (sizeof(tmp++)) {;} }",
# misra-c2012-14.1
"$a void test(float len) { for (float j = 0; j < len; j++) {;} }",
# misra-c2012-14.4
"$a void test(int len) { if (len - 8) {;} }",
# misra-c2012-16.4
r"$a void test(int temp) {switch (temp) { case 1: ; }}\n",
# misra-c2012-17.8
"$a void test(int cnt) { for (cnt=0;;cnt++) {;} }",
# misra-c2012-20.4
r"$a #define auto 1\n",
# misra-c2012-20.5
r"$a #define TEST 1\n#undef TEST\n",
]
all_files = glob.glob('board/**', root_dir=ROOT, recursive=True)
files = sorted(f for f in all_files if f.endswith(('.c', '.h')) and not f.startswith(IGNORED_PATHS))
assert len(files) > 50, all(d in files for d in ('board/main.c', 'board/stm32h7/llfdcan.h'))
# fixed seed so every xdist worker collects the same test params
rng = random.Random(len(files))
for p in patterns:
mutations.append((rng.choice(files), p, True))
@pytest.mark.parametrize("fn, patch, should_fail", mutations)
def test_misra_mutation(fn, patch, should_fail):
with tempfile.TemporaryDirectory() as tmp:
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:
fpath = os.path.join(tmp, "panda", fn)
with open(fpath) as f:
content = f.read()
if patch.startswith("s/"):
old, new = patch[2:].rsplit("/g", 1)[0].split("/", 1)
content = content.replace(old, new)
elif patch.startswith("$a "):
content += patch[3:].replace(r"\n", "\n")
with open(fpath, "w") as f:
f.write(content)
# 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