From 74bf5e93d99ce84a911a783d53a327069aa051a0 Mon Sep 17 00:00:00 2001 From: shaia Date: Sat, 7 Mar 2026 17:56:41 +0200 Subject: [PATCH 1/7] feat: Add and update tests for v0.2.0 Python bindings Cover new v0.2.0 APIs: 3D grids, symmetry BCs, front/back edges, Poisson solver, logging, library lifecycle, GPU management, status constants, and new exception types. Convert hard skips to conditional skipif for CSV timeseries and stretched grid tests. Add stress tests for boundary conditions and derived fields. --- tests/conftest.py | 3 +- tests/test_boundary_conditions.py | 128 +++++++++++++++++++ tests/test_cpu_features.py | 18 ++- tests/test_derived_fields.py | 28 +++++ tests/test_errors.py | 88 ++++++++++++++ tests/test_gpu.py | 63 ++++++++++ tests/test_lifecycle.py | 87 +++++++++++++ tests/test_logging.py | 60 +++++++++ tests/test_module.py | 18 +++ tests/test_output.py | 37 +++++- tests/test_poisson_solver.py | 196 ++++++++++++++++++++++++++++++ tests/test_simulation.py | 37 ++++++ tests/test_vtk_output.py | 32 ++++- 13 files changed, 786 insertions(+), 9 deletions(-) create mode 100644 tests/test_gpu.py create mode 100644 tests/test_lifecycle.py create mode 100644 tests/test_logging.py create mode 100644 tests/test_poisson_solver.py diff --git a/tests/conftest.py b/tests/conftest.py index ef2f072..1a24d20 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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( diff --git a/tests/test_boundary_conditions.py b/tests/test_boundary_conditions.py index 64eada1..b854074 100644 --- a/tests/test_boundary_conditions.py +++ b/tests/test_boundary_conditions.py @@ -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 = [ @@ -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 = [ @@ -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""" @@ -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""" @@ -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""" @@ -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""" @@ -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""" @@ -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""" @@ -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", diff --git a/tests/test_cpu_features.py b/tests/test_cpu_features.py index feb341d..bcfde4a 100644 --- a/tests/test_cpu_features.py +++ b/tests/test_cpu_features.py @@ -150,7 +150,17 @@ def test_has_simd_matches_arch(self): ) -@pytest.mark.skip(reason=STRETCHED_GRID_BUG_REASON) +_STRETCHED_GRID_BUGGY = False +try: + _test_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 + if abs(_test_grid["x_coords"][-1] - _test_grid["xmax"]) > 0.1: + _STRETCHED_GRID_BUGGY = True +except Exception: + _STRETCHED_GRID_BUGGY = True + + +@pytest.mark.skipif(_STRETCHED_GRID_BUGGY, reason=STRETCHED_GRID_BUG_REASON) class TestCreateGridStretched: """Test create_grid_stretched function. @@ -245,6 +255,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""" diff --git a/tests/test_derived_fields.py b/tests/test_derived_fields.py index 2f4a0c8..3f09150 100644 --- a/tests/test_derived_fields.py +++ b/tests/test_derived_fields.py @@ -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""" diff --git a/tests/test_errors.py b/tests/test_errors.py index dcf53db..0e0a414 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -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""" @@ -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: @@ -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() diff --git a/tests/test_gpu.py b/tests/test_gpu.py new file mode 100644 index 0000000..b2a0704 --- /dev/null +++ b/tests/test_gpu.py @@ -0,0 +1,63 @@ +"""Tests for GPU device management functions (v0.2.0).""" + +import pytest + +import cfd_python + + +class TestGPUFunctions: + """Tests for gpu_is_available(), gpu_get_device_info(), etc.""" + + def test_gpu_is_available_returns_bool(self): + result = cfd_python.gpu_is_available() + assert isinstance(result, bool) + + def test_gpu_get_device_info_returns_list(self): + result = cfd_python.gpu_get_device_info() + assert isinstance(result, list) + + def test_gpu_get_device_info_entries_are_dicts(self): + devices = cfd_python.gpu_get_device_info() + for device in devices: + assert isinstance(device, dict) + + def test_gpu_get_default_config_returns_dict(self): + result = cfd_python.gpu_get_default_config() + assert isinstance(result, dict) + + def test_gpu_is_available_consistent_with_device_info(self): + available = cfd_python.gpu_is_available() + devices = cfd_python.gpu_get_device_info() + if available: + assert len(devices) > 0 + else: + assert len(devices) == 0 + + def test_gpu_functions_repeated_calls(self): + """Verify no crashes or leaks under repeated calls.""" + for _ in range(50): + cfd_python.gpu_is_available() + cfd_python.gpu_get_device_info() + cfd_python.gpu_get_default_config() + + def test_gpu_select_device_invalid_id(self): + """Test gpu_select_device with invalid device ID.""" + # If no GPU available, selecting any device should fail or be a no-op + if not cfd_python.gpu_is_available(): + with pytest.raises((ValueError, RuntimeError)): + cfd_python.gpu_select_device(9999) + else: + # With GPU, very high ID should fail + devices = cfd_python.gpu_get_device_info() + with pytest.raises((ValueError, RuntimeError)): + cfd_python.gpu_select_device(len(devices) + 100) + + def test_gpu_functions_in_all(self): + funcs = [ + "gpu_is_available", + "gpu_get_device_info", + "gpu_select_device", + "gpu_get_default_config", + ] + for name in funcs: + assert name in cfd_python.__all__, f"{name} not in __all__" diff --git a/tests/test_lifecycle.py b/tests/test_lifecycle.py new file mode 100644 index 0000000..87767c7 --- /dev/null +++ b/tests/test_lifecycle.py @@ -0,0 +1,87 @@ +"""Tests for library lifecycle and version functions (v0.2.0).""" + +import cfd_python + + +class TestLibraryLifecycle: + """Tests for init(), finalize(), is_initialized().""" + + def test_is_initialized_returns_bool(self): + result = cfd_python.is_initialized() + assert isinstance(result, bool) + + def test_init_succeeds(self): + cfd_python.init() + assert cfd_python.is_initialized() + + def test_finalize_succeeds(self): + cfd_python.init() + cfd_python.finalize() + + def test_repeated_init_safe(self): + """Verify repeated init calls do not crash.""" + for _ in range(10): + cfd_python.init() + assert cfd_python.is_initialized() + + def test_init_finalize_cycle(self): + """Verify init/finalize can be cycled without errors.""" + for _ in range(5): + cfd_python.init() + cfd_python.finalize() + + def test_double_finalize_safe(self): + """Verify calling finalize twice does not crash.""" + cfd_python.init() + cfd_python.finalize() + cfd_python.finalize() # Should not crash + + +class TestCFDVersion: + """Tests for get_cfd_version() and version constants.""" + + def test_get_cfd_version_returns_string(self): + result = cfd_python.get_cfd_version() + assert isinstance(result, str) + assert len(result) > 0 + + def test_get_cfd_version_format(self): + version = cfd_python.get_cfd_version() + parts = version.split(".") + assert len(parts) >= 2, f"Version '{version}' should have at least major.minor" + + def test_get_cfd_version_repeated_calls(self): + """Verify no leaks or crashes under repeated calls.""" + for _ in range(100): + result = cfd_python.get_cfd_version() + assert isinstance(result, str) + + def test_version_constants_exist(self): + for name in ["CFD_VERSION_MAJOR", "CFD_VERSION_MINOR", "CFD_VERSION_PATCH"]: + assert hasattr(cfd_python, name), f"Missing constant: {name}" + + def test_version_constants_are_integers(self): + for name in ["CFD_VERSION_MAJOR", "CFD_VERSION_MINOR", "CFD_VERSION_PATCH"]: + assert isinstance(getattr(cfd_python, name), int) + + def test_version_constants_non_negative(self): + for name in ["CFD_VERSION_MAJOR", "CFD_VERSION_MINOR", "CFD_VERSION_PATCH"]: + assert getattr(cfd_python, name) >= 0 + + def test_version_constants_match_string(self): + """Check version string format and constant consistency. + + Note: get_cfd_version() returns the runtime library version which may + differ from the compile-time CFD_VERSION_* constants if headers and + libraries are from different builds. + """ + version_str = cfd_python.get_cfd_version() + parts = version_str.split(".") + # Version string should have numeric parts + assert all(p.isdigit() for p in parts), f"Non-numeric version parts in '{version_str}'" + # Constants should form a valid version + major = cfd_python.CFD_VERSION_MAJOR + minor = cfd_python.CFD_VERSION_MINOR + patch = cfd_python.CFD_VERSION_PATCH + constructed = f"{major}.{minor}.{patch}" + assert len(constructed) > 0 diff --git a/tests/test_logging.py b/tests/test_logging.py new file mode 100644 index 0000000..db1303a --- /dev/null +++ b/tests/test_logging.py @@ -0,0 +1,60 @@ +"""Tests for logging functions and constants (v0.2.0).""" + +import pytest + +import cfd_python + + +class TestLogConstants: + """Tests for CFD_LOG_LEVEL_* constants.""" + + _LEVELS = ["CFD_LOG_LEVEL_INFO", "CFD_LOG_LEVEL_WARNING", "CFD_LOG_LEVEL_ERROR"] + + def test_log_level_constants_exist(self): + for name in self._LEVELS: + assert hasattr(cfd_python, name), f"Missing: {name}" + + def test_log_level_constants_are_integers(self): + for name in self._LEVELS: + assert isinstance(getattr(cfd_python, name), int) + + def test_log_level_constants_ordered(self): + assert cfd_python.CFD_LOG_LEVEL_INFO < cfd_python.CFD_LOG_LEVEL_WARNING + assert cfd_python.CFD_LOG_LEVEL_WARNING < cfd_python.CFD_LOG_LEVEL_ERROR + + def test_log_level_constants_in_all(self): + for name in self._LEVELS: + assert name in cfd_python.__all__, f"{name} not in __all__" + + +class TestSetLogCallback: + """Tests for set_log_callback() function.""" + + def test_set_log_callback_none_clears(self): + cfd_python.set_log_callback(None) # Should not raise + + def test_set_log_callback_callable(self): + messages = [] + + def callback(level, msg): + messages.append((level, msg)) + + cfd_python.set_log_callback(callback) + cfd_python.set_log_callback(None) # Cleanup + + def test_set_log_callback_lambda(self): + cfd_python.set_log_callback(lambda level, msg: None) + cfd_python.set_log_callback(None) # Cleanup + + def test_set_log_callback_invalid_raises(self): + with pytest.raises(TypeError): + cfd_python.set_log_callback("not_callable") + + def test_set_log_callback_repeated(self): + """Verify repeated callback registration does not leak.""" + for _ in range(50): + cfd_python.set_log_callback(lambda level, msg: None) + cfd_python.set_log_callback(None) # Cleanup + + def test_set_log_callback_in_all(self): + assert "set_log_callback" in cfd_python.__all__ diff --git a/tests/test_module.py b/tests/test_module.py index b2143f5..a9948c0 100644 --- a/tests/test_module.py +++ b/tests/test_module.py @@ -4,6 +4,8 @@ import re +import pytest + import cfd_python @@ -101,3 +103,19 @@ def test_output_constants_in_all(self): ] for const_name in output_constants: assert const_name in cfd_python.__all__, f"{const_name} should be in __all__" + + +class TestCoreExportsCompleteness: + """Verify every symbol in _CORE_EXPORTS is actually exposed at runtime.""" + + @pytest.mark.parametrize("name", cfd_python._CORE_EXPORTS) + def test_core_export_accessible(self, name): + """Each _CORE_EXPORTS entry must be accessible via getattr.""" + assert hasattr( + cfd_python, name + ), f"{name} is declared in _CORE_EXPORTS but not accessible on the module" + + @pytest.mark.parametrize("name", cfd_python._CORE_EXPORTS) + def test_core_export_in_all(self, name): + """Each _CORE_EXPORTS entry must appear in __all__.""" + assert name in cfd_python.__all__, f"{name} is in _CORE_EXPORTS but missing from __all__" diff --git a/tests/test_output.py b/tests/test_output.py index 485f846..694ed64 100644 --- a/tests/test_output.py +++ b/tests/test_output.py @@ -6,6 +6,34 @@ import cfd_python +# Check if write_csv_timeseries actually creates files +_CSV_SKIP_REASON = ( + "write_csv_timeseries not creating files - investigate CFD library implementation" +) +_CSV_WORKS = False +try: + import os + import tempfile + + with tempfile.TemporaryDirectory() as _td: + _tf = os.path.join(_td, "_probe.csv") + cfd_python.write_csv_timeseries( + _tf, + step=0, + time=0.0, + u_data=[0.0] * 4, + v_data=[0.0] * 4, + p_data=[0.0] * 4, + nx=2, + ny=2, + dt=0.001, + iterations=1, + create_new=True, + ) + _CSV_WORKS = os.path.exists(_tf) and os.path.getsize(_tf) > 0 +except Exception: + pass + class TestVTKOutput: """Test VTK output functions""" @@ -71,9 +99,7 @@ def test_write_vtk_vector_validates_size(self, tmp_path): ) -@pytest.mark.skip( - reason="write_csv_timeseries not creating files - investigate CFD library implementation" -) +@pytest.mark.skipif(not _CSV_WORKS, reason=_CSV_SKIP_REASON) class TestCSVOutput: """Test CSV output functions""" @@ -158,3 +184,8 @@ def test_set_output_dir_returns_none(self, tmp_path): """Test set_output_dir returns None""" result = cfd_python.set_output_dir(str(tmp_path)) assert result is None + + def test_set_output_dir_invalid_type_raises(self): + """Test set_output_dir with non-string raises TypeError""" + with pytest.raises(TypeError): + cfd_python.set_output_dir(123) diff --git a/tests/test_poisson_solver.py b/tests/test_poisson_solver.py new file mode 100644 index 0000000..0cd83ff --- /dev/null +++ b/tests/test_poisson_solver.py @@ -0,0 +1,196 @@ +"""Tests for Poisson solver functions and constants (v0.2.0).""" + +import cfd_python + + +class TestPoissonMethodConstants: + """Tests for POISSON_METHOD_* constants.""" + + _METHODS = [ + "POISSON_METHOD_JACOBI", + "POISSON_METHOD_GAUSS_SEIDEL", + "POISSON_METHOD_SOR", + "POISSON_METHOD_REDBLACK_SOR", + "POISSON_METHOD_CG", + "POISSON_METHOD_BICGSTAB", + "POISSON_METHOD_MULTIGRID", + ] + + def test_method_constants_exist(self): + for name in self._METHODS: + assert hasattr(cfd_python, name), f"Missing: {name}" + + def test_method_constants_are_integers(self): + for name in self._METHODS: + assert isinstance(getattr(cfd_python, name), int) + + def test_method_constants_unique(self): + values = [getattr(cfd_python, name) for name in self._METHODS] + assert len(values) == len(set(values)), "Duplicate method constant values" + + def test_method_constants_in_all(self): + for name in self._METHODS: + assert name in cfd_python.__all__, f"{name} not in __all__" + + +class TestPoissonBackendConstants: + """Tests for POISSON_BACKEND_* constants.""" + + _BACKENDS = [ + "POISSON_BACKEND_AUTO", + "POISSON_BACKEND_SCALAR", + "POISSON_BACKEND_OMP", + "POISSON_BACKEND_SIMD", + "POISSON_BACKEND_GPU", + ] + + def test_backend_constants_exist(self): + for name in self._BACKENDS: + assert hasattr(cfd_python, name), f"Missing: {name}" + + def test_backend_constants_are_integers(self): + for name in self._BACKENDS: + assert isinstance(getattr(cfd_python, name), int) + + def test_backend_constants_unique(self): + values = [getattr(cfd_python, name) for name in self._BACKENDS] + assert len(values) == len(set(values)), "Duplicate backend constant values" + + def test_backend_constants_in_all(self): + for name in self._BACKENDS: + assert name in cfd_python.__all__, f"{name} not in __all__" + + +class TestPoissonSolverPresets: + """Tests for POISSON_SOLVER_* preset constants.""" + + _PRESETS = [ + "POISSON_SOLVER_SOR_SCALAR", + "POISSON_SOLVER_JACOBI_SIMD", + "POISSON_SOLVER_REDBLACK_SIMD", + "POISSON_SOLVER_REDBLACK_OMP", + "POISSON_SOLVER_REDBLACK_SCALAR", + "POISSON_SOLVER_CG_SCALAR", + "POISSON_SOLVER_CG_SIMD", + "POISSON_SOLVER_CG_OMP", + "POISSON_SOLVER_SOR_SIMD", + ] + + def test_solver_preset_constants_exist(self): + for name in self._PRESETS: + assert hasattr(cfd_python, name), f"Missing: {name}" + + def test_solver_preset_constants_are_integers(self): + for name in self._PRESETS: + assert isinstance(getattr(cfd_python, name), int) + + def test_solver_preset_constants_unique(self): + values = [getattr(cfd_python, name) for name in self._PRESETS] + assert len(values) == len(set(values)), "Duplicate solver preset values" + + +class TestPoissonPrecondConstants: + """Tests for POISSON_PRECOND_* constants.""" + + _PRECONDS = ["POISSON_PRECOND_NONE", "POISSON_PRECOND_JACOBI"] + + def test_precond_constants_exist(self): + for name in self._PRECONDS: + assert hasattr(cfd_python, name), f"Missing: {name}" + + def test_precond_constants_are_integers(self): + for name in self._PRECONDS: + assert isinstance(getattr(cfd_python, name), int) + + def test_precond_constants_unique(self): + values = [getattr(cfd_python, name) for name in self._PRECONDS] + assert len(values) == len(set(values)), "Duplicate preconditioner constant values" + + def test_precond_constants_in_all(self): + for name in self._PRECONDS: + assert name in cfd_python.__all__, f"{name} not in __all__" + + +class TestPoissonFunctions: + """Tests for Poisson solver query and configuration functions.""" + + def test_get_default_poisson_params_returns_dict(self): + result = cfd_python.get_default_poisson_params() + assert isinstance(result, dict) + + def test_default_params_has_expected_keys(self): + params = cfd_python.get_default_poisson_params() + expected = { + "tolerance", + "absolute_tolerance", + "max_iterations", + "omega", + "check_interval", + "verbose", + "preconditioner", + } + for key in expected: + assert key in params, f"Missing key: {key}" + + def test_default_params_tolerance_positive(self): + params = cfd_python.get_default_poisson_params() + assert params["tolerance"] > 0 + + def test_default_params_max_iterations_positive(self): + params = cfd_python.get_default_poisson_params() + assert params["max_iterations"] > 0 + + def test_poisson_get_backend_returns_int(self): + result = cfd_python.poisson_get_backend() + assert isinstance(result, int) + + def test_poisson_get_backend_name_returns_string(self): + result = cfd_python.poisson_get_backend_name() + assert isinstance(result, str) + assert len(result) > 0 + + def test_poisson_backend_available_returns_bool(self): + result = cfd_python.poisson_backend_available(cfd_python.POISSON_BACKEND_SCALAR) + assert isinstance(result, bool) + + def test_poisson_scalar_backend_always_available(self): + assert cfd_python.poisson_backend_available(cfd_python.POISSON_BACKEND_SCALAR) + + def test_poisson_set_backend_scalar(self): + original = cfd_python.poisson_get_backend() + result = cfd_python.poisson_set_backend(cfd_python.POISSON_BACKEND_SCALAR) + assert isinstance(result, bool) + cfd_python.poisson_set_backend(original) # restore + + def test_poisson_simd_available_returns_bool(self): + result = cfd_python.poisson_simd_available() + assert isinstance(result, bool) + + def test_poisson_functions_repeated_calls(self): + """Verify no crashes or leaks under repeated Poisson function calls.""" + for _ in range(50): + cfd_python.get_default_poisson_params() + cfd_python.poisson_get_backend() + cfd_python.poisson_get_backend_name() + + def test_poisson_set_backend_invalid_returns_false(self): + """Test setting an invalid backend returns False.""" + result = cfd_python.poisson_set_backend(9999) + assert result is False + + def test_poisson_backend_available_invalid_returns_false(self): + """Test invalid backend is not available.""" + result = cfd_python.poisson_backend_available(9999) + assert result is False + + def test_poisson_functions_in_all(self): + funcs = [ + "get_default_poisson_params", + "poisson_get_backend", + "poisson_get_backend_name", + "poisson_set_backend", + "poisson_backend_available", + "poisson_simd_available", + ] + for name in funcs: + assert name in cfd_python.__all__, f"{name} not in __all__" diff --git a/tests/test_simulation.py b/tests/test_simulation.py index 2f6aa3a..db35961 100644 --- a/tests/test_simulation.py +++ b/tests/test_simulation.py @@ -37,6 +37,25 @@ def test_create_grid_has_coordinates(self): assert len(grid["x_coords"]) == 5 assert len(grid["y_coords"]) == 3 + def test_create_grid_3d(self): + """Test create_grid with 3D parameters (nz > 1)""" + grid = cfd_python.create_grid(5, 5, 0.0, 1.0, 0.0, 1.0, nz=4, zmin=0.0, zmax=1.0) + assert isinstance(grid, dict) + assert grid["nz"] == 4 + assert grid["zmin"] == 0.0 + assert grid["zmax"] == 1.0 + + def test_create_grid_3d_has_z_coords(self): + """Test 3D grid has z_coords array""" + grid = cfd_python.create_grid(5, 5, 0.0, 1.0, 0.0, 1.0, nz=3, zmin=0.0, zmax=1.0) + assert "z_coords" in grid + assert len(grid["z_coords"]) == 3 + + def test_create_grid_default_nz_is_2d(self): + """Test that default nz=1 creates a 2D grid""" + grid = cfd_python.create_grid(5, 5, 0.0, 1.0, 0.0, 1.0) + assert grid["nz"] == 1 + class TestSolverParams: """Test solver parameters function""" @@ -111,6 +130,12 @@ def test_run_simulation_invalid_solver(self): with pytest.raises(RuntimeError): cfd_python.run_simulation(5, 5, steps=3, solver_type="nonexistent_solver") + def test_run_simulation_minimum_grid(self): + """Test run_simulation with minimum valid grid size""" + result = cfd_python.run_simulation(3, 3, steps=1) + assert isinstance(result, list) + assert len(result) == 9 + class TestRunSimulationWithParams: """Test run_simulation_with_params function""" @@ -161,3 +186,15 @@ def test_with_output_file(self, tmp_path): ) assert "output_file" in result assert output_file.exists() + + def test_invalid_solver_raises(self): + """Test run_simulation_with_params with invalid solver raises error""" + with pytest.raises(RuntimeError): + cfd_python.run_simulation_with_params( + 5, 5, 0.0, 1.0, 0.0, 1.0, solver_type="nonexistent_solver" + ) + + def test_invalid_nx_type_raises(self): + """Test run_simulation_with_params with non-int nx raises TypeError""" + with pytest.raises(TypeError): + cfd_python.run_simulation_with_params("five", 5, 0.0, 1.0, 0.0, 1.0) diff --git a/tests/test_vtk_output.py b/tests/test_vtk_output.py index c78632d..3ae7e3f 100644 --- a/tests/test_vtk_output.py +++ b/tests/test_vtk_output.py @@ -9,6 +9,34 @@ import cfd_python +# Check if write_csv_timeseries actually creates files +_CSV_SKIP_REASON = ( + "write_csv_timeseries not creating files - investigate CFD library implementation" +) +_CSV_WORKS = False +try: + import os + import tempfile + + with tempfile.TemporaryDirectory() as _td: + _tf = os.path.join(_td, "_probe.csv") + cfd_python.write_csv_timeseries( + _tf, + step=0, + time=0.0, + u_data=[0.0] * 4, + v_data=[0.0] * 4, + p_data=[0.0] * 4, + nx=2, + ny=2, + dt=0.001, + iterations=1, + create_new=True, + ) + _CSV_WORKS = os.path.exists(_tf) and os.path.getsize(_tf) > 0 +except Exception: + pass + class TestWriteVtkScalar: """Test the write_vtk_scalar function.""" @@ -271,9 +299,7 @@ def test_has_solver_empty_string(self): assert cfd_python.has_solver("") is False -@pytest.mark.skip( - reason="write_csv_timeseries not creating files - investigate CFD library implementation" -) +@pytest.mark.skipif(not _CSV_WORKS, reason=_CSV_SKIP_REASON) class TestWriteCsvTimeseries: """Test the write_csv_timeseries function.""" From 65c3ff301be73ab2bbf424877753b123f554c8de Mon Sep 17 00:00:00 2001 From: shaia Date: Sat, 7 Mar 2026 18:09:58 +0200 Subject: [PATCH 2/7] fix: Stop swallowing exceptions in CSV capability probe Let unexpected exceptions from write_csv_timeseries propagate instead of silently skipping the entire test class. Only skip when the function succeeds but fails to create a non-empty file (the known issue). --- tests/test_output.py | 40 +++++++++++++++++++--------------------- tests/test_vtk_output.py | 40 +++++++++++++++++++--------------------- 2 files changed, 38 insertions(+), 42 deletions(-) diff --git a/tests/test_output.py b/tests/test_output.py index 694ed64..d249986 100644 --- a/tests/test_output.py +++ b/tests/test_output.py @@ -2,6 +2,9 @@ Tests for VTK and CSV output functions """ +import os +import tempfile + import pytest import cfd_python @@ -11,28 +14,23 @@ "write_csv_timeseries not creating files - investigate CFD library implementation" ) _CSV_WORKS = False -try: - import os - import tempfile - with tempfile.TemporaryDirectory() as _td: - _tf = os.path.join(_td, "_probe.csv") - cfd_python.write_csv_timeseries( - _tf, - step=0, - time=0.0, - u_data=[0.0] * 4, - v_data=[0.0] * 4, - p_data=[0.0] * 4, - nx=2, - ny=2, - dt=0.001, - iterations=1, - create_new=True, - ) - _CSV_WORKS = os.path.exists(_tf) and os.path.getsize(_tf) > 0 -except Exception: - pass +with tempfile.TemporaryDirectory() as _td: + _tf = os.path.join(_td, "_probe.csv") + cfd_python.write_csv_timeseries( + _tf, + step=0, + time=0.0, + u_data=[0.0] * 4, + v_data=[0.0] * 4, + p_data=[0.0] * 4, + nx=2, + ny=2, + dt=0.001, + iterations=1, + create_new=True, + ) + _CSV_WORKS = os.path.exists(_tf) and os.path.getsize(_tf) > 0 class TestVTKOutput: diff --git a/tests/test_vtk_output.py b/tests/test_vtk_output.py index 3ae7e3f..d4c2a35 100644 --- a/tests/test_vtk_output.py +++ b/tests/test_vtk_output.py @@ -5,6 +5,9 @@ particularly the fixed use-after-free bug in write_vtk_vector. """ +import os +import tempfile + import pytest import cfd_python @@ -14,28 +17,23 @@ "write_csv_timeseries not creating files - investigate CFD library implementation" ) _CSV_WORKS = False -try: - import os - import tempfile - with tempfile.TemporaryDirectory() as _td: - _tf = os.path.join(_td, "_probe.csv") - cfd_python.write_csv_timeseries( - _tf, - step=0, - time=0.0, - u_data=[0.0] * 4, - v_data=[0.0] * 4, - p_data=[0.0] * 4, - nx=2, - ny=2, - dt=0.001, - iterations=1, - create_new=True, - ) - _CSV_WORKS = os.path.exists(_tf) and os.path.getsize(_tf) > 0 -except Exception: - pass +with tempfile.TemporaryDirectory() as _td: + _tf = os.path.join(_td, "_probe.csv") + cfd_python.write_csv_timeseries( + _tf, + step=0, + time=0.0, + u_data=[0.0] * 4, + v_data=[0.0] * 4, + p_data=[0.0] * 4, + nx=2, + ny=2, + dt=0.001, + iterations=1, + create_new=True, + ) + _CSV_WORKS = os.path.exists(_tf) and os.path.getsize(_tf) > 0 class TestWriteVtkScalar: From c820e9eeae08db8538f9e159d6bbda6bd2a6c2ea Mon Sep 17 00:00:00 2001 From: shaia Date: Sat, 7 Mar 2026 18:12:28 +0200 Subject: [PATCH 3/7] fix: Let unexpected exceptions propagate in stretched grid probe Only skip TestCreateGridStretched when the known formula bug is detected, not when create_grid_stretched raises an unexpected error. --- tests/test_cpu_features.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_cpu_features.py b/tests/test_cpu_features.py index bcfde4a..932a659 100644 --- a/tests/test_cpu_features.py +++ b/tests/test_cpu_features.py @@ -153,11 +153,13 @@ def test_has_simd_matches_arch(self): _STRETCHED_GRID_BUGGY = False try: _test_grid = cfd_python.create_grid_stretched(5, 5, 0.0, 1.0, 0.0, 1.0, 1.5) +except Exception: + # Unexpected failure (missing symbol, type error, etc.) — let tests surface it + raise +else: # Bug: x_coords[-1] should be close to xmax, not xmin if abs(_test_grid["x_coords"][-1] - _test_grid["xmax"]) > 0.1: _STRETCHED_GRID_BUGGY = True -except Exception: - _STRETCHED_GRID_BUGGY = True @pytest.mark.skipif(_STRETCHED_GRID_BUGGY, reason=STRETCHED_GRID_BUG_REASON) From fe75056f273b2739e14fab6b1c626e25cb5002fb Mon Sep 17 00:00:00 2001 From: shaia Date: Sat, 7 Mar 2026 18:15:27 +0200 Subject: [PATCH 4/7] fix: Restore library init state after lifecycle tests Add autouse fixture to snapshot/restore is_initialized() state so lifecycle tests don't leak side-effects to other test modules. --- tests/test_lifecycle.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/test_lifecycle.py b/tests/test_lifecycle.py index 87767c7..ce4d461 100644 --- a/tests/test_lifecycle.py +++ b/tests/test_lifecycle.py @@ -1,11 +1,23 @@ """Tests for library lifecycle and version functions (v0.2.0).""" +import pytest + import cfd_python class TestLibraryLifecycle: """Tests for init(), finalize(), is_initialized().""" + @pytest.fixture(autouse=True) + def _restore_init_state(self): + """Snapshot and restore library init state so tests don't leak side-effects.""" + was_initialized = cfd_python.is_initialized() + yield + if was_initialized: + cfd_python.init() + else: + cfd_python.finalize() + def test_is_initialized_returns_bool(self): result = cfd_python.is_initialized() assert isinstance(result, bool) From 110d21cf407977b3412623e0120b0fc3e0129fef Mon Sep 17 00:00:00 2001 From: shaia Date: Sat, 7 Mar 2026 18:35:06 +0200 Subject: [PATCH 5/7] fix: Remove POISSON_SOLVER_SOR_SIMD from test presets list The constant was removed from _CORE_EXPORTS in 3a56067 but remained in the test _PRESETS list, causing 3 failures on CI. --- tests/test_poisson_solver.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_poisson_solver.py b/tests/test_poisson_solver.py index 0cd83ff..6338a6a 100644 --- a/tests/test_poisson_solver.py +++ b/tests/test_poisson_solver.py @@ -73,7 +73,6 @@ class TestPoissonSolverPresets: "POISSON_SOLVER_CG_SCALAR", "POISSON_SOLVER_CG_SIMD", "POISSON_SOLVER_CG_OMP", - "POISSON_SOLVER_SOR_SIMD", ] def test_solver_preset_constants_exist(self): From a4a3983843d0b9b5966e7cfa3e365f295498baf0 Mon Sep 17 00:00:00 2001 From: shaia Date: Sat, 7 Mar 2026 18:54:27 +0200 Subject: [PATCH 6/7] fix: Move stretched grid probe from module level into fixture The module-level probe ran create_grid_stretched at import time, which could break test collection for the entire module on unexpected errors. Move the probe into a helper function called from an autouse fixture, so failures surface as test errors rather than import errors. --- tests/test_cpu_features.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/tests/test_cpu_features.py b/tests/test_cpu_features.py index 932a659..974ac69 100644 --- a/tests/test_cpu_features.py +++ b/tests/test_cpu_features.py @@ -150,19 +150,17 @@ def test_has_simd_matches_arch(self): ) -_STRETCHED_GRID_BUGGY = False -try: - _test_grid = cfd_python.create_grid_stretched(5, 5, 0.0, 1.0, 0.0, 1.0, 1.5) -except Exception: - # Unexpected failure (missing symbol, type error, etc.) — let tests surface it - raise -else: +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 - if abs(_test_grid["x_coords"][-1] - _test_grid["xmax"]) > 0.1: - _STRETCHED_GRID_BUGGY = True + return abs(grid["x_coords"][-1] - grid["xmax"]) > 0.1 -@pytest.mark.skipif(_STRETCHED_GRID_BUGGY, reason=STRETCHED_GRID_BUG_REASON) class TestCreateGridStretched: """Test create_grid_stretched function. @@ -180,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) From 22dde72ffbc81c2f305a74a8ae7463541de2aae5 Mon Sep 17 00:00:00 2001 From: shaia Date: Sat, 7 Mar 2026 19:18:00 +0200 Subject: [PATCH 7/7] fix: Expect RuntimeError from gpu_select_device, not ValueError The C extension raises RuntimeError via raise_cfd_error() for invalid device IDs. The test was catching (ValueError, RuntimeError) which would never match ValueError with the current implementation. --- tests/test_gpu.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_gpu.py b/tests/test_gpu.py index b2a0704..7377d40 100644 --- a/tests/test_gpu.py +++ b/tests/test_gpu.py @@ -44,12 +44,12 @@ def test_gpu_select_device_invalid_id(self): """Test gpu_select_device with invalid device ID.""" # If no GPU available, selecting any device should fail or be a no-op if not cfd_python.gpu_is_available(): - with pytest.raises((ValueError, RuntimeError)): + with pytest.raises(RuntimeError): cfd_python.gpu_select_device(9999) else: # With GPU, very high ID should fail devices = cfd_python.gpu_get_device_info() - with pytest.raises((ValueError, RuntimeError)): + with pytest.raises(RuntimeError): cfd_python.gpu_select_device(len(devices) + 100) def test_gpu_functions_in_all(self):