Tests: Add automated testing framework with coverage support

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Pragyansh Chaturvedi
2026-03-30 16:12:57 +05:30
parent 0498885f71
commit da57911122
12 changed files with 425 additions and 2 deletions

View File

View File

@ -0,0 +1,17 @@
from dataclasses import dataclass
from pathlib import Path
@dataclass
class BpfTestCase:
path: Path
rel_path: str
is_expected_fail: bool = False
xfail_reason: str = ""
xfail_level: str = "ir" # "ir" or "llc"
needs_vmlinux: bool = False
skip_reason: str = ""
@property
def test_id(self) -> str:
return self.rel_path.replace("/", "::")

View File

@ -0,0 +1,60 @@
import sys
from pathlib import Path
if sys.version_info >= (3, 11):
import tomllib
else:
import tomli as tomllib
from .bpf_test_case import BpfTestCase
TESTS_DIR = Path(__file__).parent.parent
CONFIG_FILE = TESTS_DIR / "test_config.toml"
VMLINUX_TEST_DIRS = {"passing_tests/vmlinux"}
VMLINUX_TEST_PREFIXES = {
"failing_tests/vmlinux",
"failing_tests/xdp",
}
def _is_vmlinux_test(rel_path: str) -> bool:
for prefix in VMLINUX_TEST_DIRS | VMLINUX_TEST_PREFIXES:
if rel_path.startswith(prefix):
return True
return False
def _load_config() -> dict:
if not CONFIG_FILE.exists():
return {}
with open(CONFIG_FILE, "rb") as f:
return tomllib.load(f)
def collect_all_test_files() -> list[BpfTestCase]:
config = _load_config()
xfail_map: dict = config.get("xfail", {})
cases = []
for subdir in ("passing_tests", "failing_tests"):
for py_file in sorted((TESTS_DIR / subdir).rglob("*.py")):
rel = str(py_file.relative_to(TESTS_DIR))
needs_vmlinux = _is_vmlinux_test(rel)
xfail_entry = xfail_map.get(rel)
is_expected_fail = xfail_entry is not None
xfail_reason = xfail_entry.get("reason", "") if xfail_entry else ""
xfail_level = xfail_entry.get("level", "ir") if xfail_entry else "ir"
cases.append(
BpfTestCase(
path=py_file,
rel_path=rel,
is_expected_fail=is_expected_fail,
xfail_reason=xfail_reason,
xfail_level=xfail_level,
needs_vmlinux=needs_vmlinux,
)
)
return cases

View File

@ -0,0 +1,23 @@
import logging
from pathlib import Path
from pythonbpf.codegen import compile_to_ir, _run_llc
def run_ir_generation(test_path: Path, output_ll: Path):
"""Run compile_to_ir on a BPF test file.
Returns the (output, structs_sym_tab, maps_sym_tab) tuple from compile_to_ir.
Raises on exception. Any logging.ERROR records captured by pytest caplog
indicate a compile failure even when no exception is raised.
"""
return compile_to_ir(str(test_path), str(output_ll), loglevel=logging.WARNING)
def run_llc(ll_path: Path, obj_path: Path) -> bool:
"""Compile a .ll file to a BPF .o using llc.
Raises subprocess.CalledProcessError on failure (llc uses check=True).
Returns True on success.
"""
return _run_llc(str(ll_path), str(obj_path))

View File

@ -0,0 +1,25 @@
import subprocess
import uuid
from pathlib import Path
def verify_object(obj_path: Path) -> tuple[bool, str]:
"""Run bpftool prog load -d to verify a BPF object file against the kernel verifier.
Pins the program temporarily at /sys/fs/bpf/bpf_prog_test_<uuid>, then removes it.
Returns (success, combined_output). Requires sudo / root.
"""
pin_path = f"/sys/fs/bpf/bpf_prog_test_{uuid.uuid4().hex[:8]}"
try:
result = subprocess.run(
["sudo", "bpftool", "prog", "load", "-d", str(obj_path), pin_path],
capture_output=True,
text=True,
timeout=30,
)
output = result.stdout + result.stderr
return result.returncode == 0, output
except subprocess.TimeoutExpired:
return False, "bpftool timed out after 30s"
finally:
subprocess.run(["sudo", "rm", "-f", pin_path], check=False, capture_output=True)