Skip to content

Commit 3c69ec3

Browse files
Merge pull request #95 from pythonbpf/vmlinux-gen-fix
Fix make test: regenerate vmlinux.py fixture and stop mis-collecting it
2 parents 03b1232 + 559d057 commit 3c69ec3

4 files changed

Lines changed: 118 additions & 5 deletions

File tree

Makefile

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,29 @@ clean:
66
rm -rf examples/*.ll examples/*.o
77
rm -rf htmlcov .coverage
88

9-
test:
9+
# Regenerate the master vmlinux.py from the running kernel's BTF, then
10+
# symlink it into every directory under tests/ so both pytest (which
11+
# resolves "import vmlinux" via pythonpath=["."]) and any test file run
12+
# standalone from its own directory see the same, always-fresh fixture.
13+
vmlinux:
14+
python3 tools/vmlinux-gen.py -o vmlinux.py
15+
@find tests -type d -not -path '*/__pycache__*' | while read -r d; do \
16+
target=$$(python3 -c "import os,sys; print(os.path.relpath('vmlinux.py', sys.argv[1]))" "$$d"); \
17+
ln -sf "$$target" "$$d/vmlinux.py"; \
18+
done
19+
20+
test: vmlinux
1021
pytest tests/ -W ignore::DeprecationWarning -v --tb=short -m "not verifier"
1122

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

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

2132
all: clean install
2233

23-
.PHONY: all clean install test test-cov test-verifier
34+
.PHONY: all clean install test test-cov test-verifier vmlinux

pythonbpf/functions/functions_pass.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -403,7 +403,7 @@ def process_bpf_chunk(func_node, compilation_context, return_type):
403403
if func_node.args.args:
404404
# Only look at the first argument for now
405405
param = func.args[0]
406-
param.add_attribute("nocapture")
406+
param.add_attribute("captures(none)")
407407

408408
probe_string = get_probe_string(func_node)
409409
if probe_string is not None:

tests/framework/collector.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ def collect_all_test_files() -> list[BpfTestCase]:
3535
cases = []
3636
for subdir in ("passing_tests", "failing_tests"):
3737
for py_file in sorted((TESTS_DIR / subdir).rglob("*.py")):
38+
if py_file.name == "vmlinux.py":
39+
# Not a test case: the per-directory symlink to the master
40+
# vmlinux fixture module, kept alongside tests that import it.
41+
continue
3842
rel = str(py_file.relative_to(TESTS_DIR))
3943
needs_vmlinux = _is_vmlinux_test(rel)
4044

tools/vmlinux-gen.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,11 +261,109 @@ def repl(m):
261261
for name in invalid_ctypes:
262262
data = re.sub(rf"\bctypes\.{name}\b", name, data)
263263

264+
data = self.disambiguate_anonymous_field_names(data)
265+
264266
with open(self.output_file, "w") as f:
265267
f.write(data)
266268

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

271+
def disambiguate_anonymous_field_names(self, data):
272+
"""Make clang2py's generic '_N' field names globally unique per struct/union.
273+
274+
clang2py names both (a) unnamed anonymous struct/union members that go
275+
into `_anonymous_`, and (b) other unnamed/reserved members, using the
276+
same per-struct sequential scheme ('_0', '_1', ...). ctypes flattens
277+
anonymous members by promoting their field names onto the parent class,
278+
so if a struct has an anonymous member (say '_0') whose type itself has
279+
a field also named '_1', and that same struct has ANOTHER anonymous
280+
member named '_1', the promoted name collides with the parent's own
281+
field key. ctypes then fails with a cryptic
282+
"type object 'c_ulong' has no attribute '_fields_'" (or similar) when
283+
`_fields_` is assigned. Prefixing every generic '_N' key with its
284+
owning struct/union name keeps these names unique across the whole
285+
file, so promoted names can never collide with a sibling's own key.
286+
"""
287+
self.log("Disambiguating generic anonymous-field names...")
288+
289+
def sanitize(name):
290+
return re.sub(r"[^0-9A-Za-z_]", "_", name)
291+
292+
lines = data.split("\n")
293+
class_re = re.compile(r"^class\s+(\w+)\(")
294+
stmt_fields_re = re.compile(r"^(\w+)\._fields_\s*=\s*\[\s*$")
295+
inline_fields_re = re.compile(r"^\s+_fields_\s*=\s*\[\s*$")
296+
anon_stmt_re = re.compile(r"^(\w+)\._anonymous_\s*=\s*\((.*)\)\s*$")
297+
anon_inline_re = re.compile(r"^(\s+)_anonymous_\s*=\s*\((.*)\)\s*$")
298+
key_re = re.compile(r"(\('_)([0-9]+)(',)")
299+
300+
last_class = None
301+
out = []
302+
i = 0
303+
n = len(lines)
304+
renamed_count = 0
305+
while i < n:
306+
line = lines[i]
307+
308+
m_class = class_re.match(line)
309+
if m_class:
310+
last_class = m_class.group(1)
311+
out.append(line)
312+
i += 1
313+
continue
314+
315+
m_anon = anon_stmt_re.match(line)
316+
if m_anon:
317+
cls, body = m_anon.group(1), m_anon.group(2)
318+
prefix = sanitize(cls)
319+
new_body, cnt = re.subn(
320+
r"'_([0-9]+)'", lambda mm: f"'_{prefix}_{mm.group(1)}'", body
321+
)
322+
renamed_count += cnt
323+
out.append(f"{cls}._anonymous_ = ({new_body})")
324+
i += 1
325+
continue
326+
327+
m_anon_inline = anon_inline_re.match(line)
328+
if m_anon_inline and last_class:
329+
indent, body = m_anon_inline.group(1), m_anon_inline.group(2)
330+
prefix = sanitize(last_class)
331+
new_body, cnt = re.subn(
332+
r"'_([0-9]+)'", lambda mm: f"'_{prefix}_{mm.group(1)}'", body
333+
)
334+
renamed_count += cnt
335+
out.append(f"{indent}_anonymous_ = ({new_body})")
336+
i += 1
337+
continue
338+
339+
m_stmt = stmt_fields_re.match(line)
340+
m_inline = inline_fields_re.match(line) if not m_stmt else None
341+
if m_stmt or m_inline:
342+
cls = m_stmt.group(1) if m_stmt else last_class
343+
prefix = sanitize(cls) if cls else None
344+
out.append(line)
345+
i += 1
346+
while i < n and lines[i].strip() != "]":
347+
fl = lines[i]
348+
if prefix:
349+
fl, cnt = key_re.subn(
350+
lambda mm: f"{mm.group(1)}{prefix}_{mm.group(2)}{mm.group(3)}",
351+
fl,
352+
)
353+
renamed_count += cnt
354+
out.append(fl)
355+
i += 1
356+
if i < n:
357+
out.append(lines[i]) # closing ']'
358+
i += 1
359+
continue
360+
361+
out.append(line)
362+
i += 1
363+
364+
self.log(f"Renamed {renamed_count} generic field keys")
365+
return "\n".join(out)
366+
269367
def cleanup(self):
270368
"""Remove temporary files if not keeping them."""
271369
if not self.keep_intermediate and self.temp_dir != ".":

0 commit comments

Comments
 (0)