Skip to content
Merged
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
19 changes: 15 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,29 @@ clean:
rm -rf examples/*.ll examples/*.o
rm -rf htmlcov .coverage

test:
# Regenerate the master vmlinux.py from the running kernel's BTF, then
# symlink it into every directory under tests/ so both pytest (which
# resolves "import vmlinux" via pythonpath=["."]) and any test file run
# standalone from its own directory see the same, always-fresh fixture.
vmlinux:
python3 tools/vmlinux-gen.py -o vmlinux.py
@find tests -type d -not -path '*/__pycache__*' | while read -r d; do \
target=$$(python3 -c "import os,sys; print(os.path.relpath('vmlinux.py', sys.argv[1]))" "$$d"); \
ln -sf "$$target" "$$d/vmlinux.py"; \
done

test: vmlinux
pytest tests/ -W ignore::DeprecationWarning -v --tb=short -m "not verifier"

test-cov:
test-cov: vmlinux
pytest tests/ -W ignore::DeprecationWarning -v --tb=short -m "not verifier" \
--cov=pythonbpf --cov-report=term-missing --cov-report=html

test-verifier:
test-verifier: vmlinux
@echo "NOTE: verifier tests shell out to 'sudo bpftool'; run 'sudo -v' first so"
@echo " the timestamp does not lapse mid-run. bpftool must be installed."
pytest tests/test_verifier.py -W ignore::DeprecationWarning -v --tb=short -m verifier

all: clean install

.PHONY: all clean install test test-cov test-verifier
.PHONY: all clean install test test-cov test-verifier vmlinux
2 changes: 1 addition & 1 deletion pythonbpf/functions/functions_pass.py
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,7 @@ def process_bpf_chunk(func_node, compilation_context, return_type):
if func_node.args.args:
# Only look at the first argument for now
param = func.args[0]
param.add_attribute("nocapture")
param.add_attribute("captures(none)")

probe_string = get_probe_string(func_node)
if probe_string is not None:
Expand Down
4 changes: 4 additions & 0 deletions tests/framework/collector.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ def collect_all_test_files() -> list[BpfTestCase]:
cases = []
for subdir in ("passing_tests", "failing_tests"):
for py_file in sorted((TESTS_DIR / subdir).rglob("*.py")):
if py_file.name == "vmlinux.py":
# Not a test case: the per-directory symlink to the master
# vmlinux fixture module, kept alongside tests that import it.
continue
rel = str(py_file.relative_to(TESTS_DIR))
needs_vmlinux = _is_vmlinux_test(rel)

Expand Down
98 changes: 98 additions & 0 deletions tools/vmlinux-gen.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,11 +261,109 @@ def repl(m):
for name in invalid_ctypes:
data = re.sub(rf"\bctypes\.{name}\b", name, data)

data = self.disambiguate_anonymous_field_names(data)

with open(self.output_file, "w") as f:
f.write(data)

self.log(f"Saved final output to {self.output_file}")

def disambiguate_anonymous_field_names(self, data):
"""Make clang2py's generic '_N' field names globally unique per struct/union.

clang2py names both (a) unnamed anonymous struct/union members that go
into `_anonymous_`, and (b) other unnamed/reserved members, using the
same per-struct sequential scheme ('_0', '_1', ...). ctypes flattens
anonymous members by promoting their field names onto the parent class,
so if a struct has an anonymous member (say '_0') whose type itself has
a field also named '_1', and that same struct has ANOTHER anonymous
member named '_1', the promoted name collides with the parent's own
field key. ctypes then fails with a cryptic
"type object 'c_ulong' has no attribute '_fields_'" (or similar) when
`_fields_` is assigned. Prefixing every generic '_N' key with its
owning struct/union name keeps these names unique across the whole
file, so promoted names can never collide with a sibling's own key.
"""
self.log("Disambiguating generic anonymous-field names...")

def sanitize(name):
return re.sub(r"[^0-9A-Za-z_]", "_", name)

lines = data.split("\n")
class_re = re.compile(r"^class\s+(\w+)\(")
stmt_fields_re = re.compile(r"^(\w+)\._fields_\s*=\s*\[\s*$")
inline_fields_re = re.compile(r"^\s+_fields_\s*=\s*\[\s*$")
anon_stmt_re = re.compile(r"^(\w+)\._anonymous_\s*=\s*\((.*)\)\s*$")
anon_inline_re = re.compile(r"^(\s+)_anonymous_\s*=\s*\((.*)\)\s*$")
key_re = re.compile(r"(\('_)([0-9]+)(',)")

last_class = None
out = []
i = 0
n = len(lines)
renamed_count = 0
while i < n:
line = lines[i]

m_class = class_re.match(line)
if m_class:
last_class = m_class.group(1)
out.append(line)
i += 1
continue

m_anon = anon_stmt_re.match(line)
if m_anon:
cls, body = m_anon.group(1), m_anon.group(2)
prefix = sanitize(cls)
new_body, cnt = re.subn(
r"'_([0-9]+)'", lambda mm: f"'_{prefix}_{mm.group(1)}'", body
)
renamed_count += cnt
out.append(f"{cls}._anonymous_ = ({new_body})")
i += 1
continue

m_anon_inline = anon_inline_re.match(line)
if m_anon_inline and last_class:
indent, body = m_anon_inline.group(1), m_anon_inline.group(2)
prefix = sanitize(last_class)
new_body, cnt = re.subn(
r"'_([0-9]+)'", lambda mm: f"'_{prefix}_{mm.group(1)}'", body
)
renamed_count += cnt
out.append(f"{indent}_anonymous_ = ({new_body})")
i += 1
continue

m_stmt = stmt_fields_re.match(line)
m_inline = inline_fields_re.match(line) if not m_stmt else None
if m_stmt or m_inline:
cls = m_stmt.group(1) if m_stmt else last_class
prefix = sanitize(cls) if cls else None
out.append(line)
i += 1
while i < n and lines[i].strip() != "]":
fl = lines[i]
if prefix:
fl, cnt = key_re.subn(
lambda mm: f"{mm.group(1)}{prefix}_{mm.group(2)}{mm.group(3)}",
fl,
)
renamed_count += cnt
out.append(fl)
i += 1
if i < n:
out.append(lines[i]) # closing ']'
i += 1
continue

out.append(line)
i += 1

self.log(f"Renamed {renamed_count} generic field keys")
return "\n".join(out)

def cleanup(self):
"""Remove temporary files if not keeping them."""
if not self.keep_intermediate and self.temp_dir != ".":
Expand Down
Loading