Skip to content
Open
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
3 changes: 1 addition & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@ def _check_module_import(module_name, error_message):
# In CI (indicated by CI env var), fail instead of skip
if os.environ.get("CI"):
raise RuntimeError(
"CFD Python C extension not built. "
"The wheel may be missing the compiled extension."
"CFD Python C extension not built. The wheel may be missing the compiled extension."
)
else:
pytest.skip(
Expand Down
128 changes: 128 additions & 0 deletions tests/test_boundary_conditions.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ def test_bc_type_constants_exist(self):
for const_name in bc_types:
assert hasattr(cfd_python, const_name), f"Missing constant: {const_name}"

def test_bc_type_symmetry_exists(self):
"""Test BC_TYPE_SYMMETRY constant is defined (v0.2.0)"""
assert hasattr(cfd_python, "BC_TYPE_SYMMETRY")
assert isinstance(cfd_python.BC_TYPE_SYMMETRY, int)

def test_bc_type_constants_are_integers(self):
"""Test BC_TYPE_* constants are integers"""
bc_types = [
Expand Down Expand Up @@ -65,6 +70,12 @@ def test_bc_edge_constants_exist(self):
for const_name in bc_edges:
assert hasattr(cfd_python, const_name), f"Missing constant: {const_name}"

def test_bc_3d_edge_constants_exist(self):
"""Test BC_EDGE_FRONT and BC_EDGE_BACK constants (v0.2.0)"""
for name in ["BC_EDGE_FRONT", "BC_EDGE_BACK"]:
assert hasattr(cfd_python, name), f"Missing: {name}"
assert isinstance(getattr(cfd_python, name), int)

def test_bc_edge_constants_are_integers(self):
"""Test BC_EDGE_* constants are integers"""
bc_edges = [
Expand Down Expand Up @@ -178,6 +189,14 @@ def test_bc_backend_available_returns_bool(self):
result, bool
), f"bc_backend_available should return bool for {backend}"

def test_bc_set_backend_invalid_returns_bool(self):
"""Test setting an invalid backend returns a boolean"""
original = cfd_python.bc_get_backend()
result = cfd_python.bc_set_backend(9999)
assert isinstance(result, bool)
# Restore original backend
cfd_python.bc_set_backend(original)


class TestBCApplyScalar:
"""Test bc_apply_scalar function"""
Expand Down Expand Up @@ -236,6 +255,13 @@ def test_bc_apply_velocity_periodic(self):
result = cfd_python.bc_apply_velocity(u, v, nx, ny, cfd_python.BC_TYPE_PERIODIC)
assert result is None

def test_bc_apply_velocity_invalid_size_raises(self):
"""Test bc_apply_velocity with mismatched u size raises error"""
u = [0.0] * 6 # Wrong size for 4x4
v = [0.0] * 16
with pytest.raises(ValueError):
cfd_python.bc_apply_velocity(u, v, 4, 4, cfd_python.BC_TYPE_NEUMANN)


class TestBCApplyDirichlet:
"""Test bc_apply_dirichlet function"""
Expand Down Expand Up @@ -266,6 +292,12 @@ def test_bc_apply_dirichlet_fixed_values(self):
for i in range(nx):
assert field[(ny - 1) * nx + i] == top, f"Top boundary at col {i}"

def test_bc_apply_dirichlet_invalid_size_raises(self):
"""Test bc_apply_dirichlet with mismatched size raises error"""
field = [0.0] * 6 # Wrong size for 4x4
with pytest.raises(ValueError):
cfd_python.bc_apply_dirichlet(field, 4, 4, 1.0, 2.0, 3.0, 4.0)


class TestBCApplyNoslip:
"""Test bc_apply_noslip function"""
Expand Down Expand Up @@ -305,6 +337,13 @@ def test_bc_apply_noslip_zeros_boundaries(self):
assert u[idx] == 0.0, f"u top boundary at {i}"
assert v[idx] == 0.0, f"v top boundary at {i}"

def test_bc_apply_noslip_invalid_size_raises(self):
"""Test bc_apply_noslip with mismatched u size raises error"""
u = [0.0] * 6 # Wrong size for 4x4
v = [0.0] * 16
with pytest.raises(ValueError):
cfd_python.bc_apply_noslip(u, v, 4, 4)


class TestBCApplyInlet:
"""Test inlet boundary condition functions"""
Expand Down Expand Up @@ -350,6 +389,38 @@ def test_bc_apply_inlet_parabolic_left(self):
assert left_u[ny // 2] >= left_u[0], "Parabolic profile should peak near center"
assert left_u[ny // 2] >= left_u[ny - 1], "Parabolic profile should peak near center"

def test_bc_apply_inlet_uniform_invalid_size_raises(self):
"""Test bc_apply_inlet_uniform with mismatched size raises error"""
u = [0.0] * 6 # Wrong size for 4x4
v = [0.0] * 16
with pytest.raises(ValueError):
cfd_python.bc_apply_inlet_uniform(u, v, 4, 4, 1.0, 0.0, cfd_python.BC_EDGE_LEFT)

def test_bc_apply_inlet_parabolic_invalid_size_raises(self):
"""Test bc_apply_inlet_parabolic with mismatched size raises error"""
u = [0.0] * 6 # Wrong size for 4x4
v = [0.0] * 16
with pytest.raises(ValueError):
cfd_python.bc_apply_inlet_parabolic(u, v, 4, 4, 1.0, cfd_python.BC_EDGE_LEFT)

@pytest.mark.parametrize(
"edge",
[
cfd_python.BC_EDGE_LEFT,
cfd_python.BC_EDGE_RIGHT,
cfd_python.BC_EDGE_BOTTOM,
cfd_python.BC_EDGE_TOP,
],
)
def test_bc_apply_inlet_uniform_all_edges(self, edge):
"""Test uniform inlet works on all edges"""
nx, ny = 6, 6
size = nx * ny
u = [0.0] * size
v = [0.0] * size
result = cfd_python.bc_apply_inlet_uniform(u, v, nx, ny, 1.0, 0.0, edge)
assert result is None


class TestBCApplyOutlet:
"""Test outlet boundary condition functions"""
Expand Down Expand Up @@ -389,6 +460,58 @@ def test_bc_apply_outlet_velocity_right(self):
assert u[boundary_idx] == u[interior_idx], f"u outlet should copy interior at row {j}"
assert v[boundary_idx] == v[interior_idx], f"v outlet should copy interior at row {j}"

def test_bc_apply_outlet_scalar_invalid_size_raises(self):
"""Test bc_apply_outlet_scalar with mismatched size raises error"""
field = [0.0] * 6 # Wrong size for 4x4
with pytest.raises(ValueError):
cfd_python.bc_apply_outlet_scalar(field, 4, 4, cfd_python.BC_EDGE_RIGHT)

def test_bc_apply_outlet_velocity_invalid_size_raises(self):
"""Test bc_apply_outlet_velocity with mismatched u size raises error"""
u = [0.0] * 6 # Wrong size for 4x4
v = [0.0] * 16
with pytest.raises(ValueError):
cfd_python.bc_apply_outlet_velocity(u, v, 4, 4, cfd_python.BC_EDGE_RIGHT)

@pytest.mark.parametrize(
"edge",
[
cfd_python.BC_EDGE_LEFT,
cfd_python.BC_EDGE_RIGHT,
cfd_python.BC_EDGE_BOTTOM,
cfd_python.BC_EDGE_TOP,
],
)
def test_bc_apply_outlet_scalar_all_edges(self, edge):
"""Test outlet scalar works on all edges"""
nx, ny = 6, 6
field = [float(i) for i in range(nx * ny)]
result = cfd_python.bc_apply_outlet_scalar(field, nx, ny, edge)
assert result is None


class TestBCStress:
"""Stress tests for boundary condition functions."""

def test_bc_apply_scalar_repeated_calls(self):
"""Verify no crashes or leaks under repeated bc_apply_scalar calls."""
for _ in range(100):
field = [1.0] * 16
cfd_python.bc_apply_scalar(field, 4, 4, cfd_python.BC_TYPE_NEUMANN)

def test_bc_apply_velocity_repeated_calls(self):
"""Verify no crashes or leaks under repeated bc_apply_velocity calls."""
for _ in range(100):
u = [1.0] * 16
v = [0.5] * 16
cfd_python.bc_apply_velocity(u, v, 4, 4, cfd_python.BC_TYPE_NEUMANN)

def test_bc_apply_dirichlet_repeated_calls(self):
"""Verify no crashes or leaks under repeated bc_apply_dirichlet calls."""
for _ in range(100):
field = [0.0] * 16
cfd_python.bc_apply_dirichlet(field, 4, 4, 1.0, 2.0, 3.0, 4.0)


class TestBCFunctionsExported:
"""Test that all BC functions are properly exported"""
Expand Down Expand Up @@ -423,11 +546,16 @@ def test_bc_constants_in_all(self):
"BC_TYPE_NOSLIP",
"BC_TYPE_INLET",
"BC_TYPE_OUTLET",
# v0.2.0
"BC_TYPE_SYMMETRY",
# Edges
"BC_EDGE_LEFT",
"BC_EDGE_RIGHT",
"BC_EDGE_BOTTOM",
"BC_EDGE_TOP",
# v0.2.0
"BC_EDGE_FRONT",
"BC_EDGE_BACK",
# Backends
"BC_BACKEND_AUTO",
"BC_BACKEND_SCALAR",
Expand Down
23 changes: 22 additions & 1 deletion tests/test_cpu_features.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,17 @@ def test_has_simd_matches_arch(self):
)


@pytest.mark.skip(reason=STRETCHED_GRID_BUG_REASON)
def _check_stretched_grid_buggy():
"""Probe for the known stretched grid formula bug.

Returns True if the bug is present, False otherwise.
Raises on unexpected failures (missing symbol, type error, etc.).
"""
grid = cfd_python.create_grid_stretched(5, 5, 0.0, 1.0, 0.0, 1.0, 1.5)
# Bug: x_coords[-1] should be close to xmax, not xmin
return abs(grid["x_coords"][-1] - grid["xmax"]) > 0.1


class TestCreateGridStretched:
"""Test create_grid_stretched function.

Expand All @@ -168,6 +178,11 @@ class TestCreateGridStretched:
- Higher beta clusters points near BOUNDARIES (useful for boundary layers)
"""

@pytest.fixture(autouse=True)
def _skip_if_buggy(self):
if _check_stretched_grid_buggy():
pytest.skip(STRETCHED_GRID_BUG_REASON)

def test_create_grid_stretched_basic(self):
"""Test basic stretched grid creation"""
grid = cfd_python.create_grid_stretched(10, 10, 0.0, 1.0, 0.0, 1.0, 1.0)
Expand Down Expand Up @@ -245,6 +260,12 @@ def test_create_grid_stretched_invalid_beta_raises(self):
with pytest.raises(ValueError):
cfd_python.create_grid_stretched(10, 10, 0.0, 1.0, 0.0, 1.0, -1.0)

def test_create_grid_stretched_repeated_calls(self):
"""Verify no crashes or leaks under repeated calls."""
for _ in range(100):
grid = cfd_python.create_grid_stretched(5, 5, 0.0, 1.0, 0.0, 1.0, 1.5)
assert grid is not None


class TestPhase6Exports:
"""Test that all Phase 6 functions are properly exported"""
Expand Down
28 changes: 28 additions & 0 deletions tests/test_derived_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,34 @@ def test_compute_flow_statistics_wrong_type_raises(self):
cfd_python.compute_flow_statistics([1.0], "not list", [1.0], 1, 1)


class TestDerivedFieldsStress:
"""Stress tests for derived field functions."""

def test_calculate_field_stats_repeated_calls(self):
"""Verify no crashes or leaks under repeated calls."""
data = [float(i) for i in range(100)]
for _ in range(100):
stats = cfd_python.calculate_field_stats(data)
assert stats is not None

def test_compute_velocity_magnitude_repeated_calls(self):
"""Verify no crashes or leaks under repeated calls."""
u = [1.0] * 16
v = [1.0] * 16
for _ in range(100):
result = cfd_python.compute_velocity_magnitude(u, v, 4, 4)
assert result is not None

def test_compute_flow_statistics_repeated_calls(self):
"""Verify no crashes or leaks under repeated calls."""
u = [1.0] * 16
v = [0.5] * 16
p = [100.0] * 16
for _ in range(100):
result = cfd_python.compute_flow_statistics(u, v, p, 4, 4)
assert result is not None


class TestDerivedFieldsExported:
"""Test that all derived fields functions are properly exported"""

Expand Down
88 changes: 88 additions & 0 deletions tests/test_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,20 @@ def test_cfd_max_iter_error(self):
assert isinstance(err, cfd_python.CFDError)
assert err.status_code == -7

def test_cfd_limit_exceeded_error_inheritance(self):
"""Test CFDLimitExceededError inherits from both CFDError and ResourceWarning"""
err = cfd_python.CFDLimitExceededError("limit exceeded", -8)
assert isinstance(err, cfd_python.CFDError)
assert isinstance(err, ResourceWarning)
assert err.status_code == -8

def test_cfd_not_found_error_inheritance(self):
"""Test CFDNotFoundError inherits from both CFDError and LookupError"""
err = cfd_python.CFDNotFoundError("not found", -9)
assert isinstance(err, cfd_python.CFDError)
assert isinstance(err, LookupError)
assert err.status_code == -9


class TestRaiseForStatus:
"""Test raise_for_status function"""
Expand Down Expand Up @@ -168,6 +182,18 @@ def test_max_iter_raises_cfd_max_iter_error(self):
cfd_python.raise_for_status(-7)
assert exc_info.value.status_code == -7

def test_limit_exceeded_raises_cfd_limit_exceeded_error(self):
"""Test that -8 raises CFDLimitExceededError"""
with pytest.raises(cfd_python.CFDLimitExceededError) as exc_info:
cfd_python.raise_for_status(-8)
assert exc_info.value.status_code == -8

def test_not_found_raises_cfd_not_found_error(self):
"""Test that -9 raises CFDNotFoundError"""
with pytest.raises(cfd_python.CFDNotFoundError) as exc_info:
cfd_python.raise_for_status(-9)
assert exc_info.value.status_code == -9

def test_unknown_error_raises_cfd_error(self):
"""Test that unknown negative codes raise CFDError"""
with pytest.raises(cfd_python.CFDError) as exc_info:
Expand All @@ -194,8 +220,70 @@ def test_exceptions_in_all(self):
"CFDUnsupportedError",
"CFDDivergedError",
"CFDMaxIterError",
"CFDLimitExceededError",
"CFDNotFoundError",
"raise_for_status",
]
for name in exceptions:
assert name in cfd_python.__all__, f"{name} should be in __all__"
assert hasattr(cfd_python, name), f"{name} should be accessible"


class TestStatusConstants:
"""Test CFD_SUCCESS and CFD_ERROR_* status code constants."""

_STATUS_CONSTANTS = [
("CFD_SUCCESS", 0),
("CFD_ERROR", -1),
("CFD_ERROR_NOMEM", -2),
("CFD_ERROR_INVALID", -3),
("CFD_ERROR_IO", -4),
("CFD_ERROR_UNSUPPORTED", -5),
("CFD_ERROR_DIVERGED", -6),
("CFD_ERROR_MAX_ITER", -7),
]

_NEW_STATUS_CONSTANTS = [
("CFD_ERROR_LIMIT_EXCEEDED", -8),
("CFD_ERROR_NOT_FOUND", -9),
]

def test_status_constants_exist(self):
for name, _ in self._STATUS_CONSTANTS:
assert hasattr(cfd_python, name), f"Missing constant: {name}"

def test_status_constants_are_integers(self):
for name, _ in self._STATUS_CONSTANTS:
assert isinstance(getattr(cfd_python, name), int)

def test_status_constants_values(self):
for name, expected in self._STATUS_CONSTANTS:
assert getattr(cfd_python, name) == expected, f"{name} should be {expected}"

def test_status_constants_in_all(self):
for name, _ in self._STATUS_CONSTANTS + self._NEW_STATUS_CONSTANTS:
assert name in cfd_python.__all__, f"{name} should be in __all__"

def test_new_status_constants_exist(self):
for name, _ in self._NEW_STATUS_CONSTANTS:
assert hasattr(cfd_python, name), f"Missing constant: {name}"

def test_new_status_constants_values(self):
for name, expected in self._NEW_STATUS_CONSTANTS:
assert getattr(cfd_python, name) == expected, f"{name} should be {expected}"


class TestClearError:
"""Test clear_error function."""

def test_clear_error_does_not_raise(self):
cfd_python.clear_error()

def test_clear_error_resets_last_status(self):
cfd_python.clear_error()
status = cfd_python.get_last_status()
assert status == 0

def test_clear_error_repeated_calls(self):
for _ in range(50):
cfd_python.clear_error()
Loading
Loading