# SPDX-License-Identifier: Apache-2.0 """ Verify that every public function/enum in python_ops_fallback that also exists in c_ops has a matching signature. Does NOT require python_ops_fallback to implement everything in c_ops — only checks the intersection. If you implement a function in the fallback, its signature must match c_ops exactly. Default value rules (relaxed): - If c_ops has a default, fallback MUST also have one with the same value — otherwise callers relying on the default will break. - If c_ops has NO default, fallback MAY add one (superset, backward-compatible). Requires CUDA (c_ops must be importable). Automatically skipped on CPU-only CI. """ # Standard import enum import inspect import re # Third Party import pytest # First Party import lmcache.python_ops_fallback as fallback try: # First Party import lmcache.c_ops as c_ops HAS_C_OPS = c_ops is not fallback except ImportError: HAS_C_OPS = False # ── Helpers ── def _public_callables(module): """Return {name: obj} for all public, non-dunder, non-enum callables.""" return { name: obj for name, obj in inspect.getmembers(module) if not name.startswith("_") and callable(obj) and not (inspect.isclass(obj) and issubclass(obj, enum.Enum)) and not hasattr(obj, "__members__") # exclude pybind11 enums and getattr(obj, "__module__", None) == getattr(module, "__name__", None) } def _public_enums(module): """Return {name: obj} for all public enum-like classes. Detects both Python enum.Enum subclasses and pybind11 enums. pybind11 enums have a ``__members__`` dict attribute. """ return { name: obj for name, obj in inspect.getmembers(module, inspect.isclass) if not name.startswith("_") and (issubclass(obj, enum.Enum) or hasattr(obj, "__members__")) and getattr(obj, "__module__", None) == getattr(module, "__name__", None) } def _get_enum_members(enum_cls): """Extract {name: value} from a Python enum or pybind11 enum.""" if issubclass(enum_cls, enum.Enum): return {m.name: m.value for m in enum_cls} elif hasattr(enum_cls, "__members__"): return {name: int(val) for name, val in enum_cls.__members__.items()} return {} def _public_descriptor_classes(module: object) -> dict[str, type]: """Return {name: cls} for public classes that are neither enums nor plain functions — e.g. pybind11 ``py::class_<>`` types like ``PageBufferShapeDesc``. """ return { name: obj for name, obj in inspect.getmembers(module, inspect.isclass) if not name.startswith("_") and not (issubclass(obj, enum.Enum) or hasattr(obj, "__members__")) and callable(obj) and getattr(obj, "__module__", None) == getattr(module, "__name__", None) } def _normalize_default(value_str): """Normalize a default value string for comparison. Handles differences between pybind11 docstring representation and Python inspect representation: - 'false'/'False' → False - 'true'/'True' → True - numeric strings → float/int - quoted strings → unquoted """ s = value_str.strip() if s.lower() == "false": return False if s.lower() == "true": return True if s == "None" or s == "none": return None # Strip quotes: 'foo' or "foo" → foo if (s.startswith("'") and s.endswith("'")) or ( s.startswith('"') and s.endswith('"') ): return s[1:-1] # Try numeric try: return int(s) except ValueError: pass try: return float(s) except ValueError: pass # Return as-is for anything else (e.g. enum values) return s def _parse_docstring_params(func): """Parse pybind11 docstring to extract [(name, has_default, default_value)]. pybind11 generates docstrings like: func_name(arg1: type1, arg2: type2 = default) -> ret Returns None if parsing fails. default_value is the normalized value if present, or None if no default. """ doc = getattr(func, "__doc__", None) if not doc: return None first_line = doc.strip().split("\n")[0] match = re.match(r"\w+\((.*)\)\s*->", first_line) if not match: return None params_str = match.group(1) if not params_str.strip(): return [] # Split on commas that are NOT inside brackets/parens params = [] depth = 0 current = [] for char in params_str: if char in "([": depth += 1 current.append(char) elif char in ")]": depth -= 1 current.append(char) elif char == "," and depth == 0: params.append("".join(current).strip()) current = [] else: current.append(char) if current: params.append("".join(current).strip()) result = [] for param in params: colon_idx = param.find(":") if colon_idx == -1: name = param.split("=")[0].strip() else: name = param[:colon_idx].strip() has_default = "=" in param default_value = None if has_default: default_value = _normalize_default(param.split("=", 1)[1]) result.append((name, has_default, default_value)) return result def _get_python_params(func): """Extract [(name, has_default, default_value)] via inspect.signature.""" try: sig = inspect.signature(func) except ValueError as err: raise ValueError("no signature found for {!r}".format(func)) from err result = [] for p in sig.parameters.values(): has_default = p.default is not inspect.Parameter.empty default_value = None if has_default: default_value = _normalize_default(repr(p.default)) result.append((p.name, has_default, default_value)) return result def _has_real_names(params): """Check if parameter names are real (not arg0, arg1, ...).""" return params and not any(re.match(r"^arg\d+$", name) for name, _, _ in params) # ── Discover the intersection automatically ── # Functions intentionally excluded from parity checks. _EXCLUDED_FUNCS: set[str] = set() _fallback_callables = _public_callables(fallback) _c_ops_callables = _public_callables(c_ops) if HAS_C_OPS else {} _shared_func_names = sorted( (set(_fallback_callables) & set(_c_ops_callables)) - _EXCLUDED_FUNCS ) _fallback_enums = _public_enums(fallback) _c_ops_enums = _public_enums(c_ops) if HAS_C_OPS else {} _shared_enum_names = sorted(set(_fallback_enums) & set(_c_ops_enums)) _fallback_descs = _public_descriptor_classes(fallback) _c_ops_descs = _public_descriptor_classes(c_ops) if HAS_C_OPS else {} _shared_desc_names = sorted(set(_fallback_descs) & set(_c_ops_descs)) # ── Tests ── @pytest.mark.skipif(not HAS_C_OPS, reason="c_ops not available (no CUDA)") @pytest.mark.parametrize( "func_name", _shared_func_names if _shared_func_names else ["__placeholder__"], ) def test_function_signature_parity(func_name): """For every function that python_ops_fallback chose to implement, its signature must match c_ops exactly. When c_ops has real py::arg() names → check names, count, defaults. When c_ops only has arg0/arg1/... → check count and defaults only. Default rules (relaxed): c_ops has default, fallback does NOT → FAIL (callers will break) c_ops has default, fallback has different → FAIL (silent behavior change) c_ops has NO default, fallback does → OK (backward-compatible) """ if func_name == "__placeholder__": pytest.skip("No shared functions found between c_ops and fallback") c_func = _c_ops_callables[func_name] py_func = _fallback_callables[func_name] # Get c_ops params: try docstring first (more reliable for pybind11), # then inspect.signature c_params = _parse_docstring_params(c_func) if c_params is None: try: c_params = _get_python_params(c_func) except (ValueError, TypeError): pass if c_params is None: pytest.skip(f"Cannot inspect c_ops.{func_name} signature") # Get fallback params: always via inspect.signature try: py_params = _get_python_params(py_func) except (ValueError, TypeError): pytest.skip(f"Cannot inspect fallback.{func_name} signature") c_has_names = _has_real_names(c_params) # 1. Always check parameter count assert len(c_params) == len(py_params), ( f"{func_name}: parameter count mismatch\n" f" c_ops ({len(c_params)}): {[p[0] for p in c_params]}\n" f" fallback ({len(py_params)}): {[p[0] for p in py_params]}" ) # 2. Check each parameter for i, ( (c_name, c_has_def, c_default), (py_name, py_has_def, py_default), ) in enumerate(zip(c_params, py_params, strict=False)): # Check names only when c_ops has real py::arg() names if c_has_names: assert c_name == py_name, ( f"{func_name}: param #{i} name mismatch — " f"c_ops: '{c_name}', fallback: '{py_name}'" ) # Relaxed default check: # Only fail if c_ops has a default but fallback does NOT, # or if both have defaults but values differ. # Fallback having an extra default is fine (backward-compatible). if c_has_def: assert py_has_def, ( f"{func_name}: param #{i} ('{py_name}') — " f"c_ops has default={c_default!r} but fallback has NO default. " f"Callers relying on the default will break." ) assert c_default == py_default, ( f"{func_name}: param #{i} ('{py_name}') — " f"default value mismatch: " f"c_ops={c_default!r}, fallback={py_default!r}" ) @pytest.mark.skipif(not HAS_C_OPS, reason="c_ops not available (no CUDA)") @pytest.mark.parametrize( "enum_name", _shared_enum_names if _shared_enum_names else ["__placeholder__"], ) def test_enum_parity(enum_name): """For every enum that python_ops_fallback defines, its members and values must match c_ops.""" if enum_name == "__placeholder__": pytest.skip("No shared enums found between c_ops and fallback") c_members = _get_enum_members(_c_ops_enums[enum_name]) py_members = _get_enum_members(_fallback_enums[enum_name]) assert c_members == py_members, ( f"{enum_name} mismatch:\n c_ops: {c_members}\n fallback: {py_members}" ) # ── Coverage tests: every c_ops API must have a fallback ── @pytest.mark.skipif(not HAS_C_OPS, reason="c_ops not available (no CUDA)") def test_all_c_ops_callables_have_fallback() -> None: """Every public callable in c_ops must exist in python_ops_fallback.""" missing = sorted(set(_c_ops_callables) - set(_fallback_callables) - _EXCLUDED_FUNCS) assert not missing, f"c_ops callables missing from python_ops_fallback: {missing}" @pytest.mark.skipif(not HAS_C_OPS, reason="c_ops not available (no CUDA)") def test_all_c_ops_enums_have_fallback() -> None: """Every public enum in c_ops must exist in python_ops_fallback.""" missing = sorted(set(_c_ops_enums) - set(_fallback_enums)) assert not missing, f"c_ops enums missing from python_ops_fallback: {missing}" @pytest.mark.skipif(not HAS_C_OPS, reason="c_ops not available (no CUDA)") def test_all_c_ops_descriptors_have_fallback() -> None: """Every public descriptor class in c_ops must exist in python_ops_fallback.""" missing = sorted(set(_c_ops_descs) - set(_fallback_descs)) assert not missing, ( f"c_ops descriptor classes missing from python_ops_fallback: {missing}" ) @pytest.mark.skipif(not HAS_C_OPS, reason="c_ops not available (no CUDA)") @pytest.mark.parametrize( "cls_name", _shared_desc_names if _shared_desc_names else ["__placeholder__"], ) def test_descriptor_class_parity(cls_name: str) -> None: """For every descriptor class shared between c_ops and the fallback, every public, non-callable attribute on the c_ops instance must also be present on the fallback instance. """ if cls_name == "__placeholder__": pytest.skip("No shared descriptor classes found") # Attribute parity requires a no-arg constructor on both sides. Some plan # value structs (e.g. StagingCopy, KernelGroupSpec) only expose an # args-required constructor and no readable fields, so there is nothing to # compare — skip them rather than fail on construction. try: c_inst = _c_ops_descs[cls_name]() except TypeError: pytest.skip(f"{cls_name} has no no-arg constructor in c_ops") py_inst = _fallback_descs[cls_name]() def _public_data_attrs(inst: object) -> set[str]: return { a for a in dir(inst) if not a.startswith("_") and not callable(getattr(inst, a)) } missing = sorted(_public_data_attrs(c_inst) - _public_data_attrs(py_inst)) assert not missing, ( f"{cls_name}: fallback is missing attributes present on c_ops: {missing}" )