A Python testing framework for validating that C and Python implementations of functions produce identical results. This framework uses CFFI (C Foreign Function Interface) to dynamically load C code and compare outputs with Python implementations.
It provides two flexible loading strategies:
uv package manageruvIf you don’t have uv installed, install it with:
curl -LsSf https://astral.sh/uv/install.sh | sh
Or on macOS using Homebrew:
brew install uv
Verify the installation:
uv --version
This project uses pyproject.toml to manage dependencies. Install them with:
uv sync
This command:
cffi, hypothesis, pytestTest that everything is working:
uv run pytest tests/ -v
You should see the existing tests run successfully.
.
├── README.md # This file
├── pyproject.toml # Project configuration & dependencies
├── src/
│ ├── __init__.py
│ ├── loader.py # CompileLoader & PrecompiledLoader
│ └── equivalence.py # compare_values() for output validation
├── c_funcs/
│ ├── add.c / add.h # Simple integer addition
│ ├── clamp.c / clamp.h # Value clamping function
│ └── windower/
│ ├── windower.c / windower.h # Sliding window data structure
├── tests/
│ ├── test_equivalence_example.py # Basic equivalence test
│ ├── test_clamp_property.py # Property-based tests with Hypothesis
│ ├── test_windower.py # Complex struct comparison
│ ├── test_intentional_fail.py # Demonstrates test failure reporting
│ └── conftest.py # pytest fixtures (if needed)
Use CompileLoader when you’re actively developing C code and want automatic compilation during test runs.
c_funcs/CompileLoader with header and source pathsload() to compile on-the-flyadd() functionCreate a new test file tests/test_add_compile.py:
from equichecker import CompileLoader
from equichecker import compare_values
# Initialize the loader with source and header files
loader = CompileLoader(
header_path="c_funcs/add.h",
source_paths=["c_funcs/add.c"]
)
# Load the C library (triggers compilation)
lib = loader.load()
# Retrieve the C function
c_add = loader.get("add")
def test_add_basic():
"""Test basic addition equivalence."""
result_c = c_add(5, 7)
expected = 5 + 7
assert result_c == expected, f"Expected {expected}, got {result_c}"
def test_add_edge_cases():
"""Test edge cases."""
test_cases = [
(0, 0, 0),
(-5, 5, 0),
(1000000, 1000000, 2000000),
]
for a, b, expected in test_cases:
result = c_add(a, b)
assert result == expected, f"add({a}, {b}) = {result}, expected {expected}"
Run the test:
uv run pytest tests/test_add_compile.py -v
clamp() function with property-based testingCreate tests/test_clamp_compile.py:
from hypothesis import given, strategies as st
from equichecker import CompileLoader
# Load the clamp function
loader = CompileLoader(
header_path="c_funcs/clamp.h",
source_paths=["c_funcs/clamp.c"]
)
lib = loader.load()
c_clamp = loader.get("clamp_int")
def py_clamp(value, lower, upper):
"""Python reference implementation."""
return max(lower, min(upper, value))
@given(
value=st.integers(min_value=-1000, max_value=1000),
lower=st.integers(min_value=-1000, max_value=500),
upper=st.integers(min_value=500, max_value=1000)
)
def test_clamp_equivalence(value, lower, upper):
"""Test that C clamp matches Python implementation across many cases."""
# Adjust so lower <= upper
lower_adj = min(lower, upper)
upper_adj = max(lower, upper)
py_result = py_clamp(value, lower_adj, upper_adj)
c_result = c_clamp(value, lower_adj, upper_adj)
assert py_result == c_result, (
f"Mismatch: clamp({value}, {lower_adj}, {upper_adj}) "
f"Python={py_result}, C={c_result}"
)
Run property-based tests:
uv run pytest tests/test_clamp_compile.py -v
Hypothesis will generate 100+ test cases automatically.
Use PrecompiledLoader when you have a stable, pre-compiled C library (.so on Linux, .dll on Windows, .dylib on macOS).
Your C library must be pre-compiled. For example, using gcc:
# Compile to a shared library
gcc -shared -fPIC -o c_funcs/libadd.so c_funcs/add.c
gcc -shared -fPIC -o c_funcs/libclamp.so c_funcs/clamp.c
gcc -shared -fPIC -o c_funcs/windower/libwindower.so c_funcs/windower/windower.c
Create tests/test_add_precompiled.py:
from equichecker import PrecompiledLoader
from equichecker import compare_values
# Point to the pre-built .so file and the header
loader = PrecompiledLoader(
library_path="c_funcs/libadd.so",
header_path="c_funcs/add.h"
)
# Load the pre-compiled library
lib = loader.load()
# Retrieve the C function
c_add = loader.get("add")
def test_add_with_precompiled():
"""Test add function using pre-compiled library."""
for a in range(-100, 101, 10):
for b in range(-100, 101, 10):
expected = a + b
result = c_add(a, b)
assert result == expected, f"add({a}, {b}) = {result}, expected {expected}"
Run the test:
uv run pytest tests/test_add_precompiled.py -v
Choose CompileLoader if:
Choose PrecompiledLoader if:
Existing test files in this repository demonstrate different patterns:
See tests/test_equivalence_example.py
See tests/test_clamp_property.py
See tests/test_intentional_fail.py
def python_function(x, y):
"""Python implementation (source of truth)."""
return x + y
test_cases = [
(0, 0), # Zero case
(-MAX_INT, MAX_INT), # Boundary
(-1, -1), # Negative
(1, 1), # Positive
]
@given(x=st.integers(), y=st.integers())
def test_property(x, y):
assert c_func(x, y) == py_func(x, y)
Hypothesis generates 100+ test cases automatically. It’s more thorough than manual test cases.
compare_values() for FloatsDon’t use == for floating-point comparisons:
# ❌ Wrong
assert result == 3.14159265858979
# ✅ Correct
matches, reason = compare_values(result, 3.14159265858979, abs_tol=1e-9)
assert matches, reason
If your test file requires pre-compilation:
"""Test the add function (requires: gcc -shared -fPIC -o c_funcs/libadd.so c_funcs/add.c)"""
Cause: pytest can’t find the src package.
Solution: Ensure pyproject.toml has:
[tool.pytest.ini_options]
pythonpath = ["."]
Then run tests from the project root:
cd /path/to/function_equivalence_test
uv run pytest tests/
Cause: C compiler can’t find a required header or library.
Solution:
gcc -c your_header.hCause: The .so file doesn’t exist or path is wrong.
Solution:
Verify the library exists:
ls -la c_funcs/libexample.so
Pre-compile it:
gcc -shared -fPIC -o c_funcs/libexample.so c_funcs/example.c
Use an absolute path if relative fails:
loader = PrecompiledLoader(
library_path="/absolute/path/to/libexample.so",
header_path="c_funcs/example.h"
)
Cause: Dependencies aren’t installed.
Solution: Run uv sync again:
uv sync
uv run pytest tests/