Skip to content

Commit cd60103

Browse files
committed
test: Add new checkers for pyzes
Related-To: NEO-18718 Signed-off-by: shubham kumar <shubham.kumar@intel.com>
1 parent b77ced6 commit cd60103

2 files changed

Lines changed: 385 additions & 0 deletions

File tree

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
##
2+
# Copyright (C) 2026 Intel Corporation
3+
#
4+
# SPDX-License-Identifier: MIT
5+
#
6+
##
7+
8+
name: Python Bindings - Validation Checks
9+
10+
on:
11+
pull_request:
12+
branches:
13+
- '**'
14+
paths:
15+
- 'bindings/sysman/python/**'
16+
push:
17+
branches:
18+
- main
19+
- master
20+
- python_bindings
21+
paths:
22+
- 'bindings/sysman/python/**'
23+
workflow_dispatch:
24+
25+
env:
26+
PYTHON_VERSION: '3.10'
27+
28+
jobs:
29+
validation-checks:
30+
name: Validation Checks
31+
runs-on: ubuntu-latest
32+
defaults:
33+
run:
34+
working-directory: bindings/sysman/python
35+
permissions:
36+
contents: read
37+
38+
steps:
39+
- name: Checkout code
40+
uses: actions/checkout@v4
41+
42+
- name: Set up Python ${{ env.PYTHON_VERSION }}
43+
uses: actions/setup-python@v5
44+
with:
45+
python-version: ${{ env.PYTHON_VERSION }}
46+
47+
- name: Create virtual environment and install
48+
run: |
49+
python -m venv .venv
50+
source .venv/bin/activate
51+
pip install --upgrade pip --quiet
52+
pip install -e . --quiet
53+
54+
- name: Run validation checks
55+
run: |
56+
source .venv/bin/activate
57+
58+
# Check 1: Verify installation location
59+
PYZES_LOCATION=$(python -c "import pyzes; print(pyzes.__file__ if hasattr(pyzes, '__file__') else 'N/A')")
60+
if [[ ! "$PYZES_LOCATION" =~ "bindings/sysman/python/source" ]]; then
61+
echo "❌ FAIL: pyzes is not loading from local source"
62+
echo "Location: $PYZES_LOCATION"
63+
exit 1
64+
fi
65+
66+
# Check 2: Test silent import (no stdout output)
67+
OUTPUT=$(python -c "import pyzes" 2>/dev/null)
68+
if [ -n "$OUTPUT" ]; then
69+
echo "❌ FAIL: Import produced stdout output:"
70+
echo "$OUTPUT"
71+
echo ""
72+
echo "The 'import pyzes' statement should not print anything to stdout."
73+
echo "Please remove any print statements from the module initialization."
74+
exit 1
75+
fi
76+
77+
# Check 3: Validate structure definitions match C headers
78+
python test/validate_structures.py || {
79+
echo ""
80+
echo "Structure validation failed. Please ensure Python ctypes structures match the C headers."
81+
exit 1
82+
}
83+
84+
echo "✅ All validation checks passed: Local installation verified, import is silent, structures validated"
Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
#!/usr/bin/env python3
2+
##
3+
# Copyright (C) 2026 Intel Corporation
4+
#
5+
# SPDX-License-Identifier: MIT
6+
#
7+
##
8+
9+
"""
10+
Validates that Python ctypes structures in pyzes.py match the C structures
11+
defined in the Level-Zero headers (include/zes_api.h, include/ze_api.h).
12+
"""
13+
14+
import re
15+
import sys
16+
from pathlib import Path
17+
from typing import Dict, List, Tuple, Optional
18+
19+
# Type mapping from C to Python ctypes
20+
C_TO_PYTHON_TYPE_MAP = {
21+
# Structure types
22+
"ze_structure_type_t": "c_int32",
23+
"zes_structure_type_t": "c_int32",
24+
# Pointer types
25+
"void*": "c_void_p",
26+
"const void*": "c_void_p",
27+
# Base C types
28+
"char": "c_char",
29+
"uint8_t": "c_ubyte",
30+
"uint16_t": "c_uint16",
31+
"uint32_t": "c_uint32",
32+
"uint64_t": "c_uint64",
33+
"int8_t": "c_int8",
34+
"int16_t": "c_int16",
35+
"int32_t": "c_int32",
36+
"int64_t": "c_int64",
37+
"float": "c_float",
38+
"double": "c_double",
39+
}
40+
41+
42+
def parse_c_structure(header_content: str, struct_name: str) -> Optional[List[Tuple[str, str]]]:
43+
"""
44+
Parse a C structure definition from header content.
45+
Returns list of (field_name, field_type) tuples.
46+
"""
47+
# Find the structure definition
48+
pattern = rf"typedef\s+struct\s+_{struct_name}\s*\{{\s*(.*?)\}}\s*{struct_name}\s*;"
49+
match = re.search(pattern, header_content, re.DOTALL)
50+
51+
if not match:
52+
return None
53+
54+
struct_body = match.group(1)
55+
fields = []
56+
57+
# Parse each field line
58+
field_pattern = r"^\s*([a-zA-Z_][\w\s\*]+?)\s+([a-zA-Z_]\w+)(\[[^\]]+\])?\s*;.*?$"
59+
60+
for line in struct_body.split('\n'):
61+
# Skip comments and empty lines
62+
line = re.sub(r'///.*$', '', line).strip()
63+
if not line or line.startswith('//'):
64+
continue
65+
66+
match = re.match(field_pattern, line)
67+
if match:
68+
field_type = match.group(1).strip()
69+
field_name = match.group(2).strip()
70+
array_spec = match.group(3)
71+
72+
# Construct the full type
73+
if array_spec:
74+
full_type = f"{field_type}{array_spec}"
75+
else:
76+
full_type = field_type
77+
78+
fields.append((field_name, full_type))
79+
80+
return fields if fields else None
81+
82+
83+
def parse_python_structure(pyzes_content: str, struct_name: str) -> Optional[List[Tuple[str, str]]]:
84+
"""
85+
Parse a Python ctypes structure definition from pyzes.py.
86+
Returns list of (field_name, field_type) tuples.
87+
"""
88+
# Find the class definition
89+
pattern = rf"class\s+{struct_name}\s*\(_PrintableStructure\):.*?_fields_\s*=\s*\[(.*?)\]"
90+
match = re.search(pattern, pyzes_content, re.DOTALL)
91+
92+
if not match:
93+
return None
94+
95+
fields_str = match.group(1)
96+
fields = []
97+
98+
# Parse each field tuple
99+
field_pattern = r'\(\s*"([^"]+)"\s*,\s*([^\)]+?)\s*\)'
100+
101+
for field_match in re.finditer(field_pattern, fields_str):
102+
field_name = field_match.group(1).strip()
103+
field_type = field_match.group(2).strip().rstrip(',')
104+
fields.append((field_name, field_type))
105+
106+
return fields if fields else None
107+
108+
109+
def normalize_c_type(c_type: str) -> str:
110+
"""
111+
Convert C type to expected Python ctypes representation.
112+
"""
113+
c_type = c_type.strip()
114+
115+
# Handle arrays: char[SIZE] -> c_char * SIZE
116+
array_match = re.match(r'(\w+)\[([^\]]+)\]', c_type)
117+
if array_match:
118+
base_type = array_match.group(1)
119+
size = array_match.group(2)
120+
python_base = C_TO_PYTHON_TYPE_MAP.get(base_type, base_type)
121+
return f"{python_base} * {size}"
122+
123+
# Map type if in dictionary, otherwise keep as-is
124+
return C_TO_PYTHON_TYPE_MAP.get(c_type, c_type)
125+
126+
127+
def types_are_equivalent(type1: str, type2: str) -> bool:
128+
"""Check if two types are equivalent."""
129+
if type1 == type2:
130+
return True
131+
132+
# Check array types with same base and size
133+
array_match1 = re.match(r'(\w+)\s*\*\s*(.+)', type1)
134+
array_match2 = re.match(r'(\w+)\s*\*\s*(.+)', type2)
135+
136+
if array_match1 and array_match2:
137+
base1, size1 = array_match1.groups()
138+
base2, size2 = array_match2.groups()
139+
140+
if base1 == base2 and size1 == size2:
141+
return True
142+
143+
return False
144+
145+
146+
def compare_structures(c_fields: List[Tuple[str, str]],
147+
py_fields: List[Tuple[str, str]],
148+
struct_name: str) -> List[str]:
149+
"""
150+
Compare C and Python structure fields.
151+
Returns list of error messages (empty if structures match).
152+
"""
153+
errors = []
154+
155+
if len(c_fields) != len(py_fields):
156+
errors.append(
157+
f"Field count mismatch: C has {len(c_fields)} fields, Python has {len(py_fields)} fields"
158+
)
159+
160+
# Compare field by field
161+
max_len = max(len(c_fields), len(py_fields))
162+
for i in range(max_len):
163+
if i >= len(c_fields):
164+
py_name, py_type = py_fields[i]
165+
errors.append(f"Extra field in Python: ({py_name}, {py_type})")
166+
continue
167+
168+
if i >= len(py_fields):
169+
c_name, c_type = c_fields[i]
170+
errors.append(f"Missing field in Python: ({c_name}, {c_type})")
171+
continue
172+
173+
c_name, c_type = c_fields[i]
174+
py_name, py_type = py_fields[i]
175+
176+
# Check field name
177+
if c_name != py_name:
178+
errors.append(
179+
f"Field {i}: name mismatch - C: '{c_name}' ({c_type}), Python: '{py_name}' ({py_type})"
180+
)
181+
# Skip type comparison when names don't match - they're different fields
182+
continue
183+
184+
# Check field type (only if names match)
185+
expected_type = normalize_c_type(c_type)
186+
if not types_are_equivalent(expected_type, py_type):
187+
errors.append(
188+
f"Field '{c_name}' (position {i}): type mismatch - C: '{c_type}' -> '{expected_type}', Python: '{py_type}'"
189+
)
190+
191+
return errors
192+
193+
194+
def get_all_structures_from_pyzes(pyzes_content: str) -> List[str]:
195+
"""
196+
Extract all structure names that inherit from _PrintableStructure in pyzes.py.
197+
Returns list of structure names.
198+
"""
199+
structures = []
200+
pattern = r'^class\s+(\w+)\(_PrintableStructure\):'
201+
202+
for match in re.finditer(pattern, pyzes_content, re.MULTILINE):
203+
struct_name = match.group(1)
204+
structures.append(struct_name)
205+
206+
return structures
207+
208+
209+
def main():
210+
"""Main validation function."""
211+
script_dir = Path(__file__).parent
212+
repo_root = script_dir.parent.parent.parent.parent
213+
214+
# Paths
215+
zes_header = repo_root / "include" / "zes_api.h"
216+
ze_header = repo_root / "include" / "ze_api.h"
217+
pyzes_file = repo_root / "bindings" / "sysman" / "python" / "source" / "pyzes.py"
218+
219+
# Check files exist
220+
for file_path in [zes_header, ze_header, pyzes_file]:
221+
if not file_path.exists():
222+
print(f"❌ FAIL: Required file not found: {file_path}")
223+
return 1
224+
225+
# Read files
226+
zes_content = zes_header.read_text()
227+
ze_content = ze_header.read_text()
228+
pyzes_content = pyzes_file.read_text()
229+
230+
# Dynamically discover all structures from pyzes.py
231+
structures_to_check = get_all_structures_from_pyzes(pyzes_content)
232+
233+
if not structures_to_check:
234+
print("❌ FAIL: No structures found in pyzes.py")
235+
return 1
236+
237+
print(f"Found {len(structures_to_check)} structures in pyzes.py")
238+
239+
all_errors = {}
240+
structures_checked = 0
241+
structures_not_in_headers = []
242+
243+
for struct_name in structures_to_check:
244+
# Parse from C header
245+
c_fields = parse_c_structure(zes_content, struct_name)
246+
if c_fields is None:
247+
# Try ze_api.h for core structures
248+
c_fields = parse_c_structure(ze_content, struct_name)
249+
250+
if c_fields is None:
251+
structures_not_in_headers.append(struct_name)
252+
continue
253+
254+
# Parse from Python
255+
py_fields = parse_python_structure(pyzes_content, struct_name)
256+
if py_fields is None:
257+
print(f"❌ FAIL: Structure '{struct_name}' not found in pyzes.py")
258+
all_errors[struct_name] = ["Structure not found in pyzes.py"]
259+
continue
260+
261+
# Compare
262+
errors = compare_structures(c_fields, py_fields, struct_name)
263+
if errors:
264+
all_errors[struct_name] = errors
265+
else:
266+
structures_checked += 1
267+
268+
# Report results
269+
print()
270+
print(f"{'='*70}")
271+
print(f"Structure Validation Summary")
272+
print(f"{'='*70}")
273+
print(f"Total structures found in pyzes.py: {len(structures_to_check)}")
274+
print(f"Structures validated successfully: {structures_checked}")
275+
276+
if structures_not_in_headers:
277+
print(f"Structures not found in C headers: {len(structures_not_in_headers)}")
278+
for struct_name in structures_not_in_headers:
279+
print(f" • {struct_name}")
280+
281+
if all_errors:
282+
print(f"Structures with mismatches: {len(all_errors)}")
283+
print(f"{'='*70}")
284+
print()
285+
print(f"❌ FAIL: {len(all_errors)} structure(s) have validation errors:\n")
286+
for struct_name, errors in all_errors.items():
287+
print(f"Structure: {struct_name}")
288+
for error in errors:
289+
print(f" • {error}")
290+
print()
291+
return 1
292+
else:
293+
print(f"Structures with mismatches: 0")
294+
print(f"{'='*70}")
295+
print()
296+
print(f"✅ All structure validations passed!")
297+
return 0
298+
299+
300+
if __name__ == "__main__":
301+
sys.exit(main())

0 commit comments

Comments
 (0)