From ddd5db1f5c8c86c5588029b9e10abcdc5664e271 Mon Sep 17 00:00:00 2001 From: shaia Date: Fri, 5 Dec 2025 12:24:12 +0200 Subject: [PATCH 1/2] chore: add ruff linter with pre-commit hooks Replace black, isort, and flake8 with ruff for faster linting. Configure ruff with sensible defaults and per-file ignores for re-exports and test fixtures. Fix all existing lint violations. --- .pre-commit-config.yaml | 7 ++ cfd_python/__init__.py | 41 ++++----- dev_build.py | 14 ++- examples/basic_example.py | 30 +++---- examples/lid_driven_cavity.py | 53 +++++------ examples/output_formats.py | 74 ++++++++-------- examples/parameter_study.py | 38 +++----- examples/solver_discovery.py | 26 +++--- examples/visualization_numpy.py | 38 ++++---- pyproject.toml | 41 ++++++++- scripts/setup_distribution.py | 43 ++++----- tests/conftest.py | 19 ++-- tests/test_abi_compatibility.py | 116 ++++++++++++++---------- tests/test_errors.py | 7 +- tests/test_import_handling.py | 27 +++--- tests/test_integration.py | 56 ++++++------ tests/test_module.py | 86 +++++++++--------- tests/test_output.py | 74 +++++++++++----- tests/test_simulation.py | 99 +++++++++------------ tests/test_solvers.py | 37 ++++---- tests/test_vtk_output.py | 151 +++++++++++++++++++++----------- 21 files changed, 598 insertions(+), 479 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..c6d19f4 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,7 @@ +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.8.2 + hooks: + - id: ruff + args: [--fix] + - id: ruff-format diff --git a/cfd_python/__init__.py b/cfd_python/__init__.py index 04ca5ee..9452db5 100644 --- a/cfd_python/__init__.py +++ b/cfd_python/__init__.py @@ -18,7 +18,8 @@ # Get version from package metadata (setuptools-scm) or fall back to C module try: - from importlib.metadata import version, PackageNotFoundError + from importlib.metadata import PackageNotFoundError, version + try: __version__ = version("cfd-python") except PackageNotFoundError: @@ -54,36 +55,35 @@ ] try: + # Import the C extension module to access dynamic solver constants + from . import cfd_python as _cfd_module from .cfd_python import ( - # Simulation functions - run_simulation, + OUTPUT_CSV_CENTERLINE, + OUTPUT_CSV_STATISTICS, + OUTPUT_CSV_TIMESERIES, + OUTPUT_FULL_FIELD, + # Output type constants + OUTPUT_PRESSURE, + OUTPUT_VELOCITY, create_grid, get_default_solver_params, - run_simulation_with_params, + get_solver_info, + has_solver, # Solver functions list_solvers, - has_solver, - get_solver_info, + # Simulation functions + run_simulation, + run_simulation_with_params, # Output functions set_output_dir, + write_csv_timeseries, write_vtk_scalar, write_vtk_vector, - write_csv_timeseries, - # Output type constants - OUTPUT_PRESSURE, - OUTPUT_VELOCITY, - OUTPUT_FULL_FIELD, - OUTPUT_CSV_TIMESERIES, - OUTPUT_CSV_CENTERLINE, - OUTPUT_CSV_STATISTICS, ) - # Import the C extension module to access dynamic solver constants - from . import cfd_python as _cfd_module - # Fall back to C module version if metadata lookup failed if __version__ is None: - __version__ = getattr(_cfd_module, '__version__', '0.0.0') + __version__ = getattr(_cfd_module, "__version__", "0.0.0") # Dynamically export all SOLVER_* constants from the C module # This allows new solvers to be automatically available without @@ -101,11 +101,12 @@ # Check if this is a development environment (source checkout without built extension) # vs a broken installation (extension exists but fails to load) import os as _os + _package_dir = _os.path.dirname(__file__) # Look for compiled extension files _extension_exists = any( - f.startswith('cfd_python') and (f.endswith('.pyd') or f.endswith('.so')) + f.startswith("cfd_python") and (f.endswith(".pyd") or f.endswith(".so")) for f in _os.listdir(_package_dir) ) @@ -120,4 +121,4 @@ # Development mode - module not yet built __all__ = _CORE_EXPORTS if __version__ is None: - __version__ = "0.0.0-dev" \ No newline at end of file + __version__ = "0.0.0-dev" diff --git a/dev_build.py b/dev_build.py index 64561bf..e0d5e0d 100644 --- a/dev_build.py +++ b/dev_build.py @@ -10,10 +10,11 @@ python build.py clean # Clean build artifacts python build.py all # Clean, build, install, and test """ + +import argparse +import shutil import subprocess import sys -import shutil -import argparse from pathlib import Path # Project paths @@ -116,7 +117,12 @@ def clean(): def verify(): """Verify the installation works""" print("\n=== Verifying Installation ===") - result = run(f'{sys.executable} -c "import cfd_python; print(f\'Version: {{cfd_python.__version__}}\'); print(f\'Solvers: {{cfd_python.list_solvers()}}\')"', check=False) + verify_cmd = ( + f'{sys.executable} -c "import cfd_python; ' + f"print(f'Version: {{cfd_python.__version__}}'); " + f"print(f'Solvers: {{cfd_python.list_solvers()}}')\"" + ) + result = run(verify_cmd, check=False) return result.returncode == 0 @@ -127,7 +133,7 @@ def main(): nargs="?", default="develop", choices=["build", "install", "develop", "test", "clean", "all", "verify", "cfd"], - help="Command to run (default: develop)" + help="Command to run (default: develop)", ) args = parser.parse_args() diff --git a/examples/basic_example.py b/examples/basic_example.py index e2577d2..47ab09c 100644 --- a/examples/basic_example.py +++ b/examples/basic_example.py @@ -6,13 +6,15 @@ for running fluid dynamics simulations. """ -import sys import os -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) try: - import cfd_python import numpy as np + + import cfd_python except ImportError as e: print(f"Import error: {e}") print("Make sure to build the package first:") @@ -36,7 +38,7 @@ def main(): ymin, ymax = 0.0, 1.0 steps = 50 - print(f"\nSimulation Setup:") + print("\nSimulation Setup:") print(f" Grid: {nx} x {ny}") print(f" Domain: [{xmin}, {xmax}] x [{ymin}, {ymax}]") print(f" Steps: {steps}") @@ -47,10 +49,7 @@ def main(): print("-" * 50) vel_mag = cfd_python.run_simulation( - nx=nx, ny=ny, - steps=steps, - xmin=xmin, xmax=xmax, - ymin=ymin, ymax=ymax + nx=nx, ny=ny, steps=steps, xmin=xmin, xmax=xmax, ymin=ymin, ymax=ymax ) vel_array = np.array(vel_mag) @@ -64,9 +63,7 @@ def main(): print("-" * 50) vel_mag_vtk = cfd_python.run_simulation( - nx=nx, ny=ny, - steps=steps, - output_file="basic_output.vtk" + nx=nx, ny=ny, steps=steps, output_file="basic_output.vtk" ) print(f" Output: basic_output.vtk ({len(vel_mag_vtk)} points)") @@ -76,17 +73,12 @@ def main(): print("-" * 50) result = cfd_python.run_simulation_with_params( - nx=nx, ny=ny, - xmin=xmin, xmax=xmax, - ymin=ymin, ymax=ymax, - steps=25, - dt=0.0005, - cfl=0.2 + nx=nx, ny=ny, xmin=xmin, xmax=xmax, ymin=ymin, ymax=ymax, steps=25, dt=0.0005, cfl=0.2 ) print(f" Grid: {result['nx']} x {result['ny']}") - if 'stats' in result: - stats = result['stats'] + if "stats" in result: + stats = result["stats"] print(f" Max velocity: {stats.get('max_velocity', 'N/A')}") print(f" Iterations: {stats.get('iterations', 'N/A')}") diff --git a/examples/lid_driven_cavity.py b/examples/lid_driven_cavity.py index 49e02aa..c8085c0 100644 --- a/examples/lid_driven_cavity.py +++ b/examples/lid_driven_cavity.py @@ -11,33 +11,36 @@ - Comparing solver performance """ -import sys import os -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) -import cfd_python -import numpy as np import tempfile from pathlib import Path +import numpy as np + +import cfd_python + def run_cavity_simulation(nx, ny, steps, solver_type=None, output_dir=None): """Run a lid-driven cavity simulation.""" kwargs = { - 'nx': nx, - 'ny': ny, - 'steps': steps, - 'xmin': 0.0, - 'xmax': 1.0, - 'ymin': 0.0, - 'ymax': 1.0, + "nx": nx, + "ny": ny, + "steps": steps, + "xmin": 0.0, + "xmax": 1.0, + "ymin": 0.0, + "ymax": 1.0, } if solver_type: - kwargs['solver_type'] = solver_type + kwargs["solver_type"] = solver_type if output_dir: - kwargs['output_file'] = str(output_dir / f"cavity_{nx}x{ny}_{steps}steps.vtk") + kwargs["output_file"] = str(output_dir / f"cavity_{nx}x{ny}_{steps}steps.vtk") result = cfd_python.run_simulation(**kwargs) return np.array(result).reshape((ny, nx)) @@ -46,22 +49,22 @@ def run_cavity_simulation(nx, ny, steps, solver_type=None, output_dir=None): def analyze_results(vel_mag, nx, ny): """Analyze the velocity field.""" # Find vortex center (location of minimum velocity in central region) - center_region = vel_mag[ny//4:3*ny//4, nx//4:3*nx//4] + center_region = vel_mag[ny // 4 : 3 * ny // 4, nx // 4 : 3 * nx // 4] min_idx = np.unravel_index(np.argmin(center_region), center_region.shape) # Adjust indices for full grid - vortex_y = min_idx[0] + ny//4 - vortex_x = min_idx[1] + nx//4 + vortex_y = min_idx[0] + ny // 4 + vortex_x = min_idx[1] + nx // 4 # Normalize to [0,1] domain vortex_x_norm = vortex_x / (nx - 1) vortex_y_norm = vortex_y / (ny - 1) return { - 'max_velocity': np.max(vel_mag), - 'min_velocity': np.min(vel_mag), - 'mean_velocity': np.mean(vel_mag), - 'vortex_center': (vortex_x_norm, vortex_y_norm), + "max_velocity": np.max(vel_mag), + "min_velocity": np.min(vel_mag), + "mean_velocity": np.mean(vel_mag), + "vortex_center": (vortex_x_norm, vortex_y_norm), } @@ -88,13 +91,11 @@ def main(): vel_mag = run_cavity_simulation(n, n, steps, output_dir=output_dir) analysis = analyze_results(vel_mag, n, n) - results.append({ - 'grid': n, - **analysis - }) + results.append({"grid": n, **analysis}) print(f" Max velocity: {analysis['max_velocity']:.6f}") - print(f" Vortex center: ({analysis['vortex_center'][0]:.3f}, {analysis['vortex_center'][1]:.3f})") + vx, vy = analysis["vortex_center"] + print(f" Vortex center: ({vx:.3f}, {vy:.3f})") # Reference values (Ghia et al., 1982 for Re=100) print("\n2. Comparison with Reference Data") @@ -104,7 +105,7 @@ def main(): print() for r in results: - vx, vy = r['vortex_center'] + vx, vy = r["vortex_center"] print(f" Grid {r['grid']:3d}x{r['grid']:<3d}: vortex at ({vx:.3f}, {vy:.3f})") # Solver comparison diff --git a/examples/output_formats.py b/examples/output_formats.py index f832b63..f4c56f4 100644 --- a/examples/output_formats.py +++ b/examples/output_formats.py @@ -7,14 +7,16 @@ - CSV files for data analysis """ -import sys import os -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) -import cfd_python import tempfile from pathlib import Path +import cfd_python + def main(): print("CFD Python - Output Formats Example") @@ -51,11 +53,7 @@ def main(): vtk_scalar_file = output_dir / "pressure_field.vtk" cfd_python.write_vtk_scalar( - str(vtk_scalar_file), - "pressure", - p_data, - nx, ny, - xmin, xmax, ymin, ymax + str(vtk_scalar_file), "pressure", p_data, nx, ny, xmin, xmax, ymin, ymax ) print(f" Written: {vtk_scalar_file.name}") print(f" Size: {vtk_scalar_file.stat().st_size} bytes") @@ -63,11 +61,7 @@ def main(): # VTK with velocity magnitude vtk_velmag_file = output_dir / "velocity_magnitude.vtk" cfd_python.write_vtk_scalar( - str(vtk_velmag_file), - "velocity_magnitude", - result, - nx, ny, - xmin, xmax, ymin, ymax + str(vtk_velmag_file), "velocity_magnitude", result, nx, ny, xmin, xmax, ymin, ymax ) print(f" Written: {vtk_velmag_file.name}") @@ -77,11 +71,7 @@ def main(): vtk_vector_file = output_dir / "velocity_field.vtk" cfd_python.write_vtk_vector( - str(vtk_vector_file), - "velocity", - u_data, v_data, - nx, ny, - xmin, xmax, ymin, ymax + str(vtk_vector_file), "velocity", u_data, v_data, nx, ny, xmin, xmax, ymin, ymax ) print(f" Written: {vtk_vector_file.name}") print(f" Size: {vtk_vector_file.stat().st_size} bytes") @@ -95,11 +85,16 @@ def main(): # Write initial state (create new file) cfd_python.write_csv_timeseries( str(csv_file), - step=0, time=0.0, - u_data=u_data, v_data=v_data, p_data=p_data, - nx=nx, ny=ny, - dt=0.001, iterations=0, - create_new=True + step=0, + time=0.0, + u_data=u_data, + v_data=v_data, + p_data=p_data, + nx=nx, + ny=ny, + dt=0.001, + iterations=0, + create_new=True, ) print(f" Created: {csv_file.name}") @@ -108,13 +103,18 @@ def main(): time = step * 0.001 cfd_python.write_csv_timeseries( str(csv_file), - step=step, time=time, - u_data=u_data, v_data=v_data, p_data=p_data, - nx=nx, ny=ny, - dt=0.001, iterations=step * 5, - create_new=False + step=step, + time=time, + u_data=u_data, + v_data=v_data, + p_data=p_data, + nx=nx, + ny=ny, + dt=0.001, + iterations=step * 5, + create_new=False, ) - print(f" Appended 5 timesteps") + print(" Appended 5 timesteps") print(f" Final size: {csv_file.stat().st_size} bytes") # Output Type Constants @@ -123,12 +123,12 @@ def main(): print(" Available output types:") output_constants = [ - 'OUTPUT_PRESSURE', - 'OUTPUT_VELOCITY', - 'OUTPUT_FULL_FIELD', - 'OUTPUT_CSV_TIMESERIES', - 'OUTPUT_CSV_CENTERLINE', - 'OUTPUT_CSV_STATISTICS', + "OUTPUT_PRESSURE", + "OUTPUT_VELOCITY", + "OUTPUT_FULL_FIELD", + "OUTPUT_CSV_TIMESERIES", + "OUTPUT_CSV_CENTERLINE", + "OUTPUT_CSV_STATISTICS", ] for const_name in output_constants: if hasattr(cfd_python, const_name): @@ -146,8 +146,8 @@ def main(): print("\n" + "=" * 50) print("Output formats example completed!") - print(f"\nVTK files can be opened with ParaView or VisIt.") - print(f"CSV files can be opened with Excel, Python pandas, etc.") + print("\nVTK files can be opened with ParaView or VisIt.") + print("CSV files can be opened with Excel, Python pandas, etc.") if __name__ == "__main__": diff --git a/examples/parameter_study.py b/examples/parameter_study.py index bc4a497..b2a423b 100644 --- a/examples/parameter_study.py +++ b/examples/parameter_study.py @@ -6,13 +6,15 @@ to study the effect of different settings on the solution. """ -import sys import os -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) -import cfd_python import numpy as np +import cfd_python + def run_grid_convergence_study(): """Study the effect of grid resolution on the solution.""" @@ -24,9 +26,7 @@ def run_grid_convergence_study(): for n in grid_sizes: result = cfd_python.run_simulation( - nx=n, ny=n, steps=20, - xmin=0.0, xmax=1.0, - ymin=0.0, ymax=1.0 + nx=n, ny=n, steps=20, xmin=0.0, xmax=1.0, ymin=0.0, ymax=1.0 ) vel_array = np.array(result) max_vel = np.max(vel_array) @@ -49,18 +49,13 @@ def run_timestep_study(): for dt in dt_values: try: result = cfd_python.run_simulation_with_params( - nx=nx, ny=ny, - xmin=0.0, xmax=1.0, - ymin=0.0, ymax=1.0, - steps=10, - dt=dt, - cfl=0.5 + nx=nx, ny=ny, xmin=0.0, xmax=1.0, ymin=0.0, ymax=1.0, steps=10, dt=dt, cfl=0.5 ) - vel_mag = result.get('velocity_magnitude', []) + vel_mag = result.get("velocity_magnitude", []) max_vel = max(vel_mag) if vel_mag else 0 status = "OK" except Exception as e: - max_vel = float('nan') + max_vel = float("nan") status = f"Failed: {e}" results.append((dt, max_vel, status)) @@ -81,17 +76,14 @@ def run_solver_comparison(): for solver_name in solvers: try: - result = cfd_python.run_simulation( - nx=nx, ny=ny, steps=steps, - solver_type=solver_name - ) + result = cfd_python.run_simulation(nx=nx, ny=ny, steps=steps, solver_type=solver_name) vel_array = np.array(result) max_vel = np.max(vel_array) avg_vel = np.mean(vel_array) status = "OK" - except Exception as e: - max_vel = avg_vel = float('nan') - status = f"Failed" + except Exception: + max_vel = avg_vel = float("nan") + status = "Failed" results.append((solver_name, max_vel, avg_vel, status)) print(f" {solver_name:30s}: max={max_vel:.6f}, avg={avg_vel:.6f} [{status}]") @@ -115,9 +107,7 @@ def run_domain_size_study(): for xsize, ysize in domain_sizes: result = cfd_python.run_simulation( - nx=nx, ny=ny, steps=20, - xmin=0.0, xmax=xsize, - ymin=0.0, ymax=ysize + nx=nx, ny=ny, steps=20, xmin=0.0, xmax=xsize, ymin=0.0, ymax=ysize ) vel_array = np.array(result) max_vel = np.max(vel_array) diff --git a/examples/solver_discovery.py b/examples/solver_discovery.py index 815af76..5561f81 100644 --- a/examples/solver_discovery.py +++ b/examples/solver_discovery.py @@ -7,9 +7,10 @@ available in Python. """ -import sys import os -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) import cfd_python @@ -44,11 +45,11 @@ def main(): print("-" * 50) solvers_to_check = [ - 'explicit_euler', - 'explicit_euler_optimized', - 'projection', - 'explicit_euler_gpu', - 'nonexistent_solver' + "explicit_euler", + "explicit_euler_optimized", + "projection", + "explicit_euler_gpu", + "nonexistent_solver", ] for name in solvers_to_check: @@ -63,13 +64,13 @@ def main(): print(" Solver constants are auto-generated from the registry:\n") # These constants are dynamically created when the module loads - if hasattr(cfd_python, 'SOLVER_EXPLICIT_EULER'): + if hasattr(cfd_python, "SOLVER_EXPLICIT_EULER"): print(f" SOLVER_EXPLICIT_EULER = '{cfd_python.SOLVER_EXPLICIT_EULER}'") - if hasattr(cfd_python, 'SOLVER_EXPLICIT_EULER_OPTIMIZED'): + if hasattr(cfd_python, "SOLVER_EXPLICIT_EULER_OPTIMIZED"): print(f" SOLVER_EXPLICIT_EULER_OPTIMIZED = '{cfd_python.SOLVER_EXPLICIT_EULER_OPTIMIZED}'") - if hasattr(cfd_python, 'SOLVER_PROJECTION'): + if hasattr(cfd_python, "SOLVER_PROJECTION"): print(f" SOLVER_PROJECTION = '{cfd_python.SOLVER_PROJECTION}'") # Run simulation with different solvers @@ -79,10 +80,7 @@ def main(): for solver_name in solvers[:3]: # Test first 3 solvers print(f"\n Testing {solver_name}...") try: - result = cfd_python.run_simulation( - nx=10, ny=10, steps=5, - solver_type=solver_name - ) + result = cfd_python.run_simulation(nx=10, ny=10, steps=5, solver_type=solver_name) max_vel = max(result) if result else 0 print(f" Success! Max velocity: {max_vel:.6f}") except Exception as e: diff --git a/examples/visualization_numpy.py b/examples/visualization_numpy.py index 948083d..1e391ef 100644 --- a/examples/visualization_numpy.py +++ b/examples/visualization_numpy.py @@ -9,13 +9,15 @@ you would use matplotlib (see visualization_matplotlib.py). """ -import sys import os -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) -import cfd_python import numpy as np +import cfd_python + def main(): print("CFD Python - NumPy Analysis Example") @@ -27,9 +29,7 @@ def main(): print("-" * 50) result = cfd_python.run_simulation( - nx=nx, ny=ny, steps=50, - xmin=0.0, xmax=1.0, - ymin=0.0, ymax=1.0 + nx=nx, ny=ny, steps=50, xmin=0.0, xmax=1.0, ymin=0.0, ymax=1.0 ) # Convert to NumPy array and reshape to 2D grid @@ -82,13 +82,12 @@ def main(): print("\n5. Quadrant Analysis") print("-" * 50) - q1 = vel_mag[:ny//2, :nx//2] # Bottom-left - q2 = vel_mag[:ny//2, nx//2:] # Bottom-right - q3 = vel_mag[ny//2:, :nx//2] # Top-left - q4 = vel_mag[ny//2:, nx//2:] # Top-right + q1 = vel_mag[: ny // 2, : nx // 2] # Bottom-left + q2 = vel_mag[: ny // 2, nx // 2 :] # Bottom-right + q3 = vel_mag[ny // 2 :, : nx // 2] # Top-left + q4 = vel_mag[ny // 2 :, nx // 2 :] # Top-right - quadrants = [("Bottom-left", q1), ("Bottom-right", q2), - ("Top-left", q3), ("Top-right", q4)] + quadrants = [("Bottom-left", q1), ("Bottom-right", q2), ("Top-left", q3), ("Top-right", q4)] for name, q in quadrants: print(f" {name:12s}: mean={np.mean(q):.6f}, max={np.max(q):.6f}") @@ -113,8 +112,8 @@ def main(): print("-" * 50) grid_info = cfd_python.create_grid(nx, ny, 0.0, 1.0, 0.0, 1.0) - x = np.array(grid_info['x_coords']) - y = np.array(grid_info['y_coords']) + x = np.array(grid_info["x_coords"]) + y = np.array(grid_info["y_coords"]) X, Y = np.meshgrid(x, y) print(f" X range: [{X.min():.3f}, {X.max():.3f}]") @@ -126,15 +125,8 @@ def main(): # Save as CSV for external tools output_file = "velocity_data.csv" - flat_data = np.column_stack([ - X.flatten(), - Y.flatten(), - vel_mag.flatten() - ]) - np.savetxt(output_file, flat_data, - delimiter=',', - header='x,y,velocity_magnitude', - comments='') + flat_data = np.column_stack([X.flatten(), Y.flatten(), vel_mag.flatten()]) + np.savetxt(output_file, flat_data, delimiter=",", header="x,y,velocity_magnitude", comments="") print(f" Saved to: {output_file}") print(f" Shape: {flat_data.shape}") diff --git a/pyproject.toml b/pyproject.toml index fb54f4f..1961565 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,9 +47,7 @@ docs = [ "myst-parser", ] dev = [ - "black", - "isort", - "flake8", + "ruff", "mypy", "pre-commit", ] @@ -75,3 +73,40 @@ provider = "scikit_build_core.metadata.setuptools_scm" [tool.pytest.ini_options] testpaths = ["tests"] python_files = ["test_*.py"] + +[tool.ruff] +target-version = "py38" +line-length = 100 +exclude = [ + "build", + "dist", + ".venv", + "__pycache__", + "*.egg-info", +] + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # Pyflakes + "I", # isort + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "UP", # pyupgrade +] +fixable = ["ALL"] +unfixable = [] + +[tool.ruff.lint.per-file-ignores] +"__init__.py" = ["F401"] # Allow unused imports in __init__.py (re-exports) +"tests/*" = ["F841", "B007"] # Allow unused variables in tests +"scripts/*" = ["F401"] # Allow unused imports in setup scripts +"examples/*" = ["B007"] # Allow unused loop variables in examples + +[tool.ruff.lint.isort] +known-first-party = ["cfd_python"] + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" diff --git a/scripts/setup_distribution.py b/scripts/setup_distribution.py index ad49e9e..4042844 100644 --- a/scripts/setup_distribution.py +++ b/scripts/setup_distribution.py @@ -6,12 +6,13 @@ """ import os -import sys -import subprocess import shlex import shutil +import subprocess +import sys from pathlib import Path + def run_command(cmd, cwd=None, check=True, env=None): """Run a command and return the result. @@ -30,8 +31,7 @@ def run_command(cmd, cwd=None, check=True, env=None): print(f"Running: {' '.join(args)}") try: - result = subprocess.run(args, cwd=cwd, check=check, - capture_output=True, text=True, env=env) + result = subprocess.run(args, cwd=cwd, check=check, capture_output=True, text=True, env=env) if result.stdout: print(result.stdout) return result @@ -41,6 +41,7 @@ def run_command(cmd, cwd=None, check=True, env=None): print(f"stderr: {e.stderr}") raise + def embed_c_library(): """Embed the C library source code into the Python package""" print("Embedding C library source code...") @@ -75,7 +76,7 @@ def embed_c_library(): print(f"OK: Copied sources: {src_src} -> {src_dst}") # Create embedded CMakeLists.txt - cmake_content = ''' + cmake_content = """ # Embedded CFD Library cmake_minimum_required(VERSION 3.15) @@ -96,7 +97,7 @@ def embed_c_library(): C_STANDARD 11 POSITION_INDEPENDENT_CODE ON ) -''' +""" cmake_file = target_dir / "CMakeLists.txt" cmake_file.write_text(cmake_content.strip()) @@ -104,11 +105,12 @@ def embed_c_library(): return True + def update_main_cmake(): """Update the main CMakeLists.txt to use embedded library""" print("Updating main CMakeLists.txt...") - cmake_content = '''cmake_minimum_required(VERSION 3.15...3.27) + cmake_content = """cmake_minimum_required(VERSION 3.15...3.27) project(cfd_python LANGUAGES C CXX) @@ -170,7 +172,7 @@ def update_main_cmake(): # Install the extension module in the cfd_python package directory install(TARGETS cfd_python DESTINATION cfd_python) -''' +""" cmake_file = Path("CMakeLists.txt") cmake_file.write_text(cmake_content.strip()) @@ -178,6 +180,7 @@ def update_main_cmake(): return True + def build_wheels_locally(): """Build wheels locally for testing""" print("Building wheels locally...") @@ -192,12 +195,10 @@ def build_wheels_locally(): # Build wheels env = os.environ.copy() # Build only for current Python version for testing - env['CIBW_BUILD'] = f'cp{sys.version_info.major}{sys.version_info.minor}-*' + env["CIBW_BUILD"] = f"cp{sys.version_info.major}{sys.version_info.minor}-*" result = run_command( - ["python", "-m", "cibuildwheel", "--output-dir", "wheelhouse"], - check=False, - env=env + ["python", "-m", "cibuildwheel", "--output-dir", "wheelhouse"], check=False, env=env ) if result.returncode == 0: @@ -214,6 +215,7 @@ def build_wheels_locally(): print("ERROR: Wheel building failed") return False + def test_wheel_installation(): """Test wheel installation in a clean environment""" print("Testing wheel installation...") @@ -245,7 +247,7 @@ def test_wheel_installation(): # Test the installation by writing script to a temp file # (avoids shell quoting issues with multi-line strings) - test_script = '''\ + test_script = """\ import cfd_python print(f"CFD Python version: {cfd_python.__version__}") @@ -263,16 +265,14 @@ def test_wheel_installation(): print("OK: Solver params test passed") print("All tests passed!") -''' +""" # Write test script to a temporary file and execute it test_script_path = test_env / "test_install.py" test_script_path.write_text(test_script) result = subprocess.run( - [str(python_exe), str(test_script_path)], - capture_output=True, - text=True + [str(python_exe), str(test_script_path)], capture_output=True, text=True ) if result.stdout: print(result.stdout) @@ -289,12 +289,13 @@ def test_wheel_installation(): print("ERROR: Wheel testing failed") return False + def setup_pypi_config(): """Set up PyPI configuration files""" print("Setting up PyPI configuration...") # Create .pypirc template - pypirc_content = '''[distutils] + pypirc_content = """[distutils] index-servers = pypi testpypi @@ -307,7 +308,7 @@ def setup_pypi_config(): repository = https://test.pypi.org/legacy/ username = __token__ password = REPLACE_WITH_YOUR_TEST_PYPI_TOKEN -''' +""" print("PyPI configuration template:") print("1. Create ~/.pypirc with your PyPI tokens") @@ -318,6 +319,7 @@ def setup_pypi_config(): return True + def main(): """Main setup function""" print("CFD Python Distribution Setup") @@ -354,5 +356,6 @@ def main(): print("\nERROR: Distribution setup incomplete") return False + if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/tests/conftest.py b/tests/conftest.py index f6d88e9..ef2f072 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,12 +1,15 @@ """ Pytest configuration for CFD Python tests """ -import pytest -import sys + import os +import sys + +import pytest # Add the build directory to the path for testing -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + # Check each required module separately for clearer error messages def _check_module_import(module_name, error_message): @@ -18,13 +21,15 @@ def _check_module_import(module_name, error_message): pytest.skip(f"{error_message} (ImportError: {e})") return False + # Verify cfd_python C extension is available and functional try: import cfd_python + # Check if extension actually loaded (not just dev mode stub) - if not hasattr(cfd_python, 'list_solvers'): + if not hasattr(cfd_python, "list_solvers"): # In CI (indicated by CI env var), fail instead of skip - if os.environ.get('CI'): + if os.environ.get("CI"): raise RuntimeError( "CFD Python C extension not built. " "The wheel may be missing the compiled extension." @@ -33,7 +38,7 @@ def _check_module_import(module_name, error_message): pytest.skip( "CFD Python C extension not built (development mode). " "Run 'pip install -e .' to build the extension.", - allow_module_level=True + allow_module_level=True, ) except ImportError as e: error_str = str(e) @@ -44,6 +49,6 @@ def _check_module_import(module_name, error_message): else: reason = f"CFD Python import failed: {e}" # In CI, fail instead of skip for ImportError - if os.environ.get('CI'): + if os.environ.get("CI"): raise RuntimeError(reason) from e pytest.skip(reason, allow_module_level=True) diff --git a/tests/test_abi_compatibility.py b/tests/test_abi_compatibility.py index 1c660f6..610df11 100644 --- a/tests/test_abi_compatibility.py +++ b/tests/test_abi_compatibility.py @@ -7,6 +7,7 @@ """ import sys + import pytest @@ -16,32 +17,34 @@ class TestStableABICompliance: def test_module_imports(self): """Test that the module can be imported.""" import cfd_python + assert cfd_python is not None def test_module_has_version(self): """Test that module has version attribute.""" import cfd_python - assert hasattr(cfd_python, '__version__') + + assert hasattr(cfd_python, "__version__") assert isinstance(cfd_python.__version__, str) - @pytest.mark.skipif(sys.platform != 'win32', reason="Windows-specific test") + @pytest.mark.skipif(sys.platform != "win32", reason="Windows-specific test") def test_extension_suffix_windows(self): """On Windows, extensions use .pyd suffix or are Python packages.""" + import cfd_python - import os + ext_path = cfd_python.__file__ # Can be a .pyd extension, abi3 extension, or Python package __init__.py - assert (ext_path.endswith('.pyd') or 'abi3' in ext_path or - ext_path.endswith('__init__.py')) + assert ext_path.endswith(".pyd") or "abi3" in ext_path or ext_path.endswith("__init__.py") - @pytest.mark.skipif(sys.platform == 'win32', reason="Unix-specific test") + @pytest.mark.skipif(sys.platform == "win32", reason="Unix-specific test") def test_extension_suffix_unix(self): """On Unix, extensions should have .so suffix or be Python packages.""" import cfd_python + ext_path = cfd_python.__file__ # Can be .so extension, abi3 extension, or Python package __init__.py - assert ('.so' in ext_path or '.abi3' in ext_path or - ext_path.endswith('__init__.py')) + assert ".so" in ext_path or ".abi3" in ext_path or ext_path.endswith("__init__.py") class TestListReturnTypes: @@ -50,6 +53,7 @@ class TestListReturnTypes: def test_list_solvers_returns_list(self): """list_solvers() should return a proper Python list.""" import cfd_python + solvers = cfd_python.list_solvers() assert isinstance(solvers, list) # Each element should be a string @@ -59,12 +63,14 @@ def test_list_solvers_returns_list(self): def test_list_solvers_not_empty(self): """list_solvers() should return at least one solver.""" import cfd_python + solvers = cfd_python.list_solvers() assert len(solvers) > 0 def test_list_solvers_repeated_calls(self): """Repeated calls to list_solvers() should work without memory issues.""" import cfd_python + for _ in range(100): solvers = cfd_python.list_solvers() assert isinstance(solvers, list) @@ -73,6 +79,7 @@ def test_list_solvers_repeated_calls(self): def test_run_simulation_returns_list(self): """run_simulation() should return a proper Python list.""" import cfd_python + result = cfd_python.run_simulation(nx=8, ny=8, steps=5) assert isinstance(result, list) # Each element should be a float @@ -82,6 +89,7 @@ def test_run_simulation_returns_list(self): def test_run_simulation_correct_size(self): """run_simulation() should return nx*ny elements.""" import cfd_python + nx, ny = 10, 12 result = cfd_python.run_simulation(nx=nx, ny=ny, steps=5) assert len(result) == nx * ny @@ -89,6 +97,7 @@ def test_run_simulation_correct_size(self): def test_run_simulation_repeated_calls(self): """Repeated calls to run_simulation() should work without memory issues.""" import cfd_python + for _ in range(20): result = cfd_python.run_simulation(nx=8, ny=8, steps=2) assert isinstance(result, list) @@ -101,76 +110,79 @@ class TestDictReturnTypes: def test_create_grid_returns_dict(self): """create_grid() should return a proper Python dict.""" import cfd_python + grid = cfd_python.create_grid(10, 10, 0.0, 1.0, 0.0, 1.0) assert isinstance(grid, dict) def test_create_grid_has_coordinate_lists(self): """create_grid() should have x_coords and y_coords as lists.""" import cfd_python + nx, ny = 10, 12 grid = cfd_python.create_grid(nx, ny, 0.0, 1.0, 0.0, 1.0) - assert 'x_coords' in grid - assert 'y_coords' in grid - assert isinstance(grid['x_coords'], list) - assert isinstance(grid['y_coords'], list) - assert len(grid['x_coords']) == nx - assert len(grid['y_coords']) == ny + assert "x_coords" in grid + assert "y_coords" in grid + assert isinstance(grid["x_coords"], list) + assert isinstance(grid["y_coords"], list) + assert len(grid["x_coords"]) == nx + assert len(grid["y_coords"]) == ny # Each coordinate should be a float - for x in grid['x_coords']: + for x in grid["x_coords"]: assert isinstance(x, float) - for y in grid['y_coords']: + for y in grid["y_coords"]: assert isinstance(y, float) def test_create_grid_repeated_calls(self): """Repeated calls to create_grid() should work without memory issues.""" import cfd_python + for _ in range(50): grid = cfd_python.create_grid(8, 8, 0.0, 1.0, 0.0, 1.0) assert isinstance(grid, dict) - assert len(grid['x_coords']) == 8 - assert len(grid['y_coords']) == 8 + assert len(grid["x_coords"]) == 8 + assert len(grid["y_coords"]) == 8 def test_get_default_solver_params_returns_dict(self): """get_default_solver_params() should return a proper Python dict.""" import cfd_python + params = cfd_python.get_default_solver_params() assert isinstance(params, dict) # Check expected keys - expected_keys = ['dt', 'cfl', 'gamma', 'mu', 'k', 'max_iter', 'tolerance'] + expected_keys = ["dt", "cfl", "gamma", "mu", "k", "max_iter", "tolerance"] for key in expected_keys: assert key in params def test_run_simulation_with_params_returns_dict(self): """run_simulation_with_params() should return a proper Python dict.""" import cfd_python + result = cfd_python.run_simulation_with_params( - nx=8, ny=8, - xmin=0.0, xmax=1.0, - ymin=0.0, ymax=1.0, - steps=5 + nx=8, ny=8, xmin=0.0, xmax=1.0, ymin=0.0, ymax=1.0, steps=5 ) assert isinstance(result, dict) # Should have velocity_magnitude as a list - if 'velocity_magnitude' in result: - assert isinstance(result['velocity_magnitude'], list) - for val in result['velocity_magnitude']: + if "velocity_magnitude" in result: + assert isinstance(result["velocity_magnitude"], list) + for val in result["velocity_magnitude"]: assert isinstance(val, float) def test_get_solver_info_returns_dict(self): """get_solver_info() should return a proper Python dict.""" import cfd_python + solvers = cfd_python.list_solvers() if solvers: info = cfd_python.get_solver_info(solvers[0]) assert isinstance(info, dict) - assert 'name' in info - assert 'description' in info - assert 'capabilities' in info - assert isinstance(info['capabilities'], list) + assert "name" in info + assert "description" in info + assert "capabilities" in info + assert isinstance(info["capabilities"], list) class TestReferenceCountingStress: @@ -179,6 +191,7 @@ class TestReferenceCountingStress: def test_large_list_creation(self): """Test creating and destroying large lists doesn't leak memory.""" import cfd_python + # Run a larger simulation to create bigger lists for _ in range(10): result = cfd_python.run_simulation(nx=50, ny=50, steps=2) @@ -188,6 +201,7 @@ def test_large_list_creation(self): def test_rapid_dict_creation(self): """Test rapid dict creation and destruction.""" import cfd_python + for _ in range(100): grid = cfd_python.create_grid(20, 20, 0.0, 1.0, 0.0, 1.0) params = cfd_python.get_default_solver_params() @@ -197,6 +211,7 @@ def test_rapid_dict_creation(self): def test_mixed_operations_stress(self): """Test mixed operations don't cause memory issues.""" import cfd_python + for i in range(50): # Mix different operations solvers = cfd_python.list_solvers() @@ -205,7 +220,7 @@ def test_mixed_operations_stress(self): # Use the results assert len(solvers) > 0 - assert len(grid['x_coords']) == 10 + assert len(grid["x_coords"]) == 10 assert len(result) == 100 # Clean up @@ -214,6 +229,7 @@ def test_mixed_operations_stress(self): def test_list_modification_after_return(self): """Test that returned lists can be modified without issues.""" import cfd_python + result = cfd_python.run_simulation(nx=8, ny=8, steps=2) # Modify the list @@ -228,12 +244,13 @@ def test_list_modification_after_return(self): def test_dict_modification_after_return(self): """Test that returned dicts can be modified without issues.""" import cfd_python + grid = cfd_python.create_grid(8, 8, 0.0, 1.0, 0.0, 1.0) # Modify the dict - grid['custom_key'] = 'custom_value' - del grid['nx'] - grid['x_coords'].append(999.0) + grid["custom_key"] = "custom_value" + del grid["nx"] + grid["x_coords"].append(999.0) grid.clear() assert len(grid) == 0 @@ -245,12 +262,14 @@ class TestErrorHandlingWithABI: def test_invalid_solver_raises_error(self): """Invalid solver should raise ValueError, not crash.""" import cfd_python + with pytest.raises(ValueError): cfd_python.get_solver_info("nonexistent_solver_xyz") def test_invalid_grid_params(self): """Invalid grid parameters should be handled gracefully.""" import cfd_python + # These should either work or raise proper Python exceptions # They should NOT cause segfaults try: @@ -263,6 +282,7 @@ def test_invalid_grid_params(self): def test_zero_steps_simulation(self): """Zero steps simulation should be handled gracefully.""" import cfd_python + try: result = cfd_python.run_simulation(nx=8, ny=8, steps=0) # Either returns empty or valid list @@ -276,9 +296,10 @@ class TestMemoryLeakPrevention: def test_dict_values_have_correct_refcount(self): """Values added to dicts should have correct reference counts.""" - import cfd_python import sys + import cfd_python + # Get a fresh dict params = cfd_python.get_default_solver_params() @@ -296,9 +317,10 @@ def test_dict_values_have_correct_refcount(self): def test_list_elements_have_correct_refcount(self): """Elements in returned lists should have correct reference counts.""" - import cfd_python import sys + import cfd_python + result = cfd_python.run_simulation(nx=4, ny=4, steps=2) # Check first few elements @@ -310,16 +332,17 @@ def test_list_elements_have_correct_refcount(self): def test_solver_info_no_leak(self): """get_solver_info should not leak references.""" - import cfd_python import sys + import cfd_python + solvers = cfd_python.list_solvers() if solvers: # Call multiple times for _ in range(50): info = cfd_python.get_solver_info(solvers[0]) # Check capabilities list - caps = info['capabilities'] + caps = info["capabilities"] for cap in caps: refcount = sys.getrefcount(cap) assert refcount < 100, f"High refcount for capability: {refcount}" @@ -332,8 +355,8 @@ def test_grid_coords_no_leak(self): for _ in range(100): grid = cfd_python.create_grid(20, 20, 0.0, 1.0, 0.0, 1.0) # Access all coordinates to ensure they're valid - x_sum = sum(grid['x_coords']) - y_sum = sum(grid['y_coords']) + x_sum = sum(grid["x_coords"]) + y_sum = sum(grid["y_coords"]) assert x_sum > 0 assert y_sum > 0 del grid @@ -344,15 +367,12 @@ def test_simulation_with_params_no_leak(self): for _ in range(20): result = cfd_python.run_simulation_with_params( - nx=10, ny=10, - xmin=0.0, xmax=1.0, - ymin=0.0, ymax=1.0, - steps=3 + nx=10, ny=10, xmin=0.0, xmax=1.0, ymin=0.0, ymax=1.0, steps=3 ) # Verify result structure to ensure it's properly formed - assert 'velocity_magnitude' in result - assert len(result['velocity_magnitude']) == 100 + assert "velocity_magnitude" in result + assert len(result["velocity_magnitude"]) == 100 # Access nested dict if present - if 'stats' in result: - assert isinstance(result['stats'], dict) + if "stats" in result: + assert isinstance(result["stats"], dict) del result diff --git a/tests/test_errors.py b/tests/test_errors.py index 1f5c7fa..80adea6 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -1,7 +1,9 @@ """ Tests for error handling """ + import pytest + import cfd_python @@ -28,8 +30,7 @@ def test_write_vtk_scalar_invalid_data_type(self, tmp_path): output_file = tmp_path / "invalid.vtk" with pytest.raises(TypeError): cfd_python.write_vtk_scalar( - str(output_file), "test", "not a list", - 5, 5, 0.0, 1.0, 0.0, 1.0 + str(output_file), "test", "not a list", 5, 5, 0.0, 1.0, 0.0, 1.0 ) def test_has_solver_invalid_type(self): @@ -40,7 +41,7 @@ def test_has_solver_invalid_type(self): def test_get_solver_info_empty_string(self): """Test error handling for empty solver name""" with pytest.raises(ValueError): - cfd_python.get_solver_info('') + cfd_python.get_solver_info("") def test_create_grid_zero_dimensions(self): """Test error handling for zero grid dimensions.""" diff --git a/tests/test_import_handling.py b/tests/test_import_handling.py index 089f090..8e942c3 100644 --- a/tests/test_import_handling.py +++ b/tests/test_import_handling.py @@ -5,10 +5,9 @@ 1. Extension exists but fails to load (broken installation) 2. No extension exists (development mode) """ -import os + import sys -import tempfile -import shutil + import pytest @@ -24,7 +23,7 @@ def test_broken_extension_raises_import_error(self, tmp_path): # Create __init__.py with the same logic as the real one # but importing from fake_cfd_broken submodule - init_content = ''' + init_content = """ import os as _os _CORE_EXPORTS = ["run_simulation", "list_solvers"] @@ -48,7 +47,7 @@ def test_broken_extension_raises_import_error(self, tmp_path): __all__ = _CORE_EXPORTS if __version__ is None: __version__ = "0.0.0-dev" -''' +""" (fake_package / "__init__.py").write_text(init_content) # Create a fake broken extension file @@ -61,7 +60,7 @@ def test_broken_extension_raises_import_error(self, tmp_path): sys.path.insert(0, str(tmp_path)) try: with pytest.raises(ImportError) as exc_info: - import fake_cfd_broken + pass # Verify the error message is helpful assert "Failed to load C extension" in str(exc_info.value) @@ -79,7 +78,7 @@ def test_dev_mode_no_extension(self, tmp_path): fake_package.mkdir() # Create __init__.py with the same logic - init_content = ''' + init_content = """ import os as _os _CORE_EXPORTS = ["run_simulation", "list_solvers"] @@ -105,7 +104,7 @@ def test_dev_mode_no_extension(self, tmp_path): __all__ = _CORE_EXPORTS if __version__ is None: __version__ = "0.0.0-dev" -''' +""" (fake_package / "__init__.py").write_text(init_content) # Add to path and import @@ -128,11 +127,11 @@ def test_real_module_loads_successfully(self): import cfd_python # Should have version - assert hasattr(cfd_python, '__version__') + assert hasattr(cfd_python, "__version__") assert cfd_python.__version__ is not None # Should have core functions - assert hasattr(cfd_python, 'list_solvers') + assert hasattr(cfd_python, "list_solvers") assert callable(cfd_python.list_solvers) # list_solvers should return a non-empty list @@ -148,7 +147,7 @@ def test_extension_detection_logic(self, tmp_path): # Test with no extension files files = list(test_dir.iterdir()) has_extension = any( - f.name.startswith('cfd_python') and (f.name.endswith('.pyd') or f.name.endswith('.so')) + f.name.startswith("cfd_python") and (f.name.endswith(".pyd") or f.name.endswith(".so")) for f in files ) assert not has_extension @@ -157,7 +156,7 @@ def test_extension_detection_logic(self, tmp_path): (test_dir / "cfd_python.cp311-win_amd64.pyd").touch() files = list(test_dir.iterdir()) has_extension = any( - f.name.startswith('cfd_python') and (f.name.endswith('.pyd') or f.name.endswith('.so')) + f.name.startswith("cfd_python") and (f.name.endswith(".pyd") or f.name.endswith(".so")) for f in files ) assert has_extension @@ -167,7 +166,7 @@ def test_extension_detection_logic(self, tmp_path): (test_dir / "cfd_python.cpython-311-x86_64-linux-gnu.so").touch() files = list(test_dir.iterdir()) has_extension = any( - f.name.startswith('cfd_python') and (f.name.endswith('.pyd') or f.name.endswith('.so')) + f.name.startswith("cfd_python") and (f.name.endswith(".pyd") or f.name.endswith(".so")) for f in files ) assert has_extension @@ -177,7 +176,7 @@ def test_extension_detection_logic(self, tmp_path): (test_dir / "other_module.so").touch() files = list(test_dir.iterdir()) has_extension = any( - f.name.startswith('cfd_python') and (f.name.endswith('.pyd') or f.name.endswith('.so')) + f.name.startswith("cfd_python") and (f.name.endswith(".pyd") or f.name.endswith(".so")) for f in files ) assert not has_extension diff --git a/tests/test_integration.py b/tests/test_integration.py index 012d697..0a11f25 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -1,11 +1,13 @@ """ Integration tests for complete workflows """ + import pytest + import cfd_python # Check if extension is available (not in dev mode) -_EXTENSION_AVAILABLE = hasattr(cfd_python, 'list_solvers') +_EXTENSION_AVAILABLE = hasattr(cfd_python, "list_solvers") # Get solver list at module load time for parametrization # Returns empty list if extension not built (dev mode) @@ -13,8 +15,7 @@ # Skip all tests in this module if extension not available pytestmark = pytest.mark.skipif( - not _EXTENSION_AVAILABLE, - reason="C extension not built (development mode)" + not _EXTENSION_AVAILABLE, reason="C extension not built (development mode)" ) @@ -29,28 +30,25 @@ def test_full_simulation_workflow(self, tmp_path): # 2. Get solver info solver_info = cfd_python.get_solver_info(solvers[0]) - assert 'name' in solver_info + assert "name" in solver_info # 3. Get default parameters params = cfd_python.get_default_solver_params() - assert 'dt' in params + assert "dt" in params # 4. Create grid grid = cfd_python.create_grid(10, 10, 0.0, 1.0, 0.0, 1.0) - assert grid['nx'] == 10 + assert grid["nx"] == 10 # 5. Run simulation vtk_file = tmp_path / "workflow_test.vtk" result = cfd_python.run_simulation_with_params( - 10, 10, 0.0, 1.0, 0.0, 1.0, - steps=5, - solver_type=solvers[0], - output_file=str(vtk_file) + 10, 10, 0.0, 1.0, 0.0, 1.0, steps=5, solver_type=solvers[0], output_file=str(vtk_file) ) # 6. Verify results - assert 'velocity_magnitude' in result - assert len(result['velocity_magnitude']) == 100 + assert "velocity_magnitude" in result + assert len(result["velocity_magnitude"]) == 100 assert vtk_file.exists() def test_solver_constant_matches_list(self): @@ -59,7 +57,7 @@ def test_solver_constant_matches_list(self): for solver_name in solvers: # Use the constant to run a simulation - const_name = 'SOLVER_' + solver_name.upper() + const_name = "SOLVER_" + solver_name.upper() if hasattr(cfd_python, const_name): solver_type = getattr(cfd_python, const_name) # Should be able to use this to check solver exists @@ -71,9 +69,7 @@ def test_multiple_simulations_same_grid(self): results = [] for i in range(3): - result = cfd_python.run_simulation( - grid['nx'], grid['ny'], steps=2 - ) + result = cfd_python.run_simulation(grid["nx"], grid["ny"], steps=2) results.append(result) # All results should have same size @@ -83,12 +79,11 @@ def test_multiple_simulations_same_grid(self): @pytest.mark.parametrize("solver_name", _AVAILABLE_SOLVERS) def test_solver_produces_valid_output(self, solver_name): """Test that each available solver produces valid simulation output.""" - result = cfd_python.run_simulation( - 5, 5, steps=3, solver_type=solver_name - ) + result = cfd_python.run_simulation(5, 5, steps=3, solver_type=solver_name) assert len(result) == 25, f"Solver {solver_name} returned wrong size" - assert all(isinstance(v, float) for v in result), f"Solver {solver_name} returned non-float values" + all_floats = all(isinstance(v, float) for v in result) + assert all_floats, f"Solver {solver_name} returned non-float values" def test_output_workflow(self, tmp_path): """Test complete output workflow""" @@ -102,8 +97,7 @@ def test_output_workflow(self, tmp_path): # Write VTK scalar output vtk_file = tmp_path / "velocity_mag.vtk" cfd_python.write_vtk_scalar( - str(vtk_file), "velocity_magnitude", result, - nx, ny, 0.0, 1.0, 0.0, 1.0 + str(vtk_file), "velocity_magnitude", result, nx, ny, 0.0, 1.0, 0.0, 1.0 ) assert vtk_file.exists() @@ -112,8 +106,7 @@ def test_output_workflow(self, tmp_path): v_data = [0.05] * (nx * ny) vtk_vector_file = tmp_path / "velocity.vtk" cfd_python.write_vtk_vector( - str(vtk_vector_file), "velocity", u_data, v_data, - nx, ny, 0.0, 1.0, 0.0, 1.0 + str(vtk_vector_file), "velocity", u_data, v_data, nx, ny, 0.0, 1.0, 0.0, 1.0 ) assert vtk_vector_file.exists() @@ -121,9 +114,16 @@ def test_output_workflow(self, tmp_path): csv_file = tmp_path / "timeseries.csv" p_data = [1.0] * (nx * ny) cfd_python.write_csv_timeseries( - str(csv_file), step=0, time=0.0, - u_data=u_data, v_data=v_data, p_data=p_data, - nx=nx, ny=ny, dt=0.001, iterations=10, - create_new=True + str(csv_file), + step=0, + time=0.0, + u_data=u_data, + v_data=v_data, + p_data=p_data, + nx=nx, + ny=ny, + dt=0.001, + iterations=10, + create_new=True, ) assert csv_file.exists() diff --git a/tests/test_module.py b/tests/test_module.py index a735b65..4f699b2 100644 --- a/tests/test_module.py +++ b/tests/test_module.py @@ -1,7 +1,9 @@ """ Tests for module-level attributes and exports """ + import re + import cfd_python @@ -10,7 +12,7 @@ class TestModuleAttributes: def test_version_exists_and_valid_format(self): """Test version string exists and follows a valid version format""" - assert hasattr(cfd_python, '__version__') + assert hasattr(cfd_python, "__version__") version = cfd_python.__version__ assert isinstance(version, str) assert len(version) > 0, "Version string should not be empty" @@ -18,43 +20,45 @@ def test_version_exists_and_valid_format(self): # - Semantic versioning: X.Y.Z (with optional pre-release/build metadata) # - setuptools-scm dev version: X.Y.devN+gHASH (for development builds) # Examples: "0.3.0", "1.0.0", "0.1.0.dev1", "0.3.0+g1234567", "0.1.dev6+g4bf1eb6" - version_pattern = r'^\d+\.\d+(\.\d+)?.*$' - assert re.match(version_pattern, version), \ - f"Version '{version}' should follow versioning format (X.Y or X.Y.Z)" + version_pattern = r"^\d+\.\d+(\.\d+)?.*$" + assert re.match( + version_pattern, version + ), f"Version '{version}' should follow versioning format (X.Y or X.Y.Z)" def test_output_constants_exist(self): """Test OUTPUT_* constants are defined""" - assert hasattr(cfd_python, 'OUTPUT_PRESSURE') - assert hasattr(cfd_python, 'OUTPUT_VELOCITY') - assert hasattr(cfd_python, 'OUTPUT_FULL_FIELD') - assert hasattr(cfd_python, 'OUTPUT_CSV_TIMESERIES') - assert hasattr(cfd_python, 'OUTPUT_CSV_CENTERLINE') - assert hasattr(cfd_python, 'OUTPUT_CSV_STATISTICS') + assert hasattr(cfd_python, "OUTPUT_PRESSURE") + assert hasattr(cfd_python, "OUTPUT_VELOCITY") + assert hasattr(cfd_python, "OUTPUT_FULL_FIELD") + assert hasattr(cfd_python, "OUTPUT_CSV_TIMESERIES") + assert hasattr(cfd_python, "OUTPUT_CSV_CENTERLINE") + assert hasattr(cfd_python, "OUTPUT_CSV_STATISTICS") def test_output_constants_are_integers(self): """Test OUTPUT_* constants are integers""" output_constants = [ - 'OUTPUT_PRESSURE', - 'OUTPUT_VELOCITY', - 'OUTPUT_FULL_FIELD', - 'OUTPUT_CSV_TIMESERIES', - 'OUTPUT_CSV_CENTERLINE', - 'OUTPUT_CSV_STATISTICS', + "OUTPUT_PRESSURE", + "OUTPUT_VELOCITY", + "OUTPUT_FULL_FIELD", + "OUTPUT_CSV_TIMESERIES", + "OUTPUT_CSV_CENTERLINE", + "OUTPUT_CSV_STATISTICS", ] for const_name in output_constants: if hasattr(cfd_python, const_name): - assert isinstance(getattr(cfd_python, const_name), int), \ - f"{const_name} should be an integer" + assert isinstance( + getattr(cfd_python, const_name), int + ), f"{const_name} should be an integer" def test_output_constants_unique(self): """Test OUTPUT_* constants have unique values""" output_constants = [ - 'OUTPUT_PRESSURE', - 'OUTPUT_VELOCITY', - 'OUTPUT_FULL_FIELD', - 'OUTPUT_CSV_TIMESERIES', - 'OUTPUT_CSV_CENTERLINE', - 'OUTPUT_CSV_STATISTICS', + "OUTPUT_PRESSURE", + "OUTPUT_VELOCITY", + "OUTPUT_FULL_FIELD", + "OUTPUT_CSV_TIMESERIES", + "OUTPUT_CSV_CENTERLINE", + "OUTPUT_CSV_STATISTICS", ] values = [] for const_name in output_constants: @@ -69,17 +73,17 @@ class TestAllExports: def test_core_functions_exported(self): """Test core functions are in __all__ and accessible""" core_functions = [ - 'run_simulation', - 'create_grid', - 'get_default_solver_params', - 'run_simulation_with_params', - 'list_solvers', - 'has_solver', - 'get_solver_info', - 'set_output_dir', - 'write_vtk_scalar', - 'write_vtk_vector', - 'write_csv_timeseries', + "run_simulation", + "create_grid", + "get_default_solver_params", + "run_simulation_with_params", + "list_solvers", + "has_solver", + "get_solver_info", + "set_output_dir", + "write_vtk_scalar", + "write_vtk_vector", + "write_csv_timeseries", ] for func_name in core_functions: assert hasattr(cfd_python, func_name), f"Missing function: {func_name}" @@ -88,12 +92,12 @@ def test_core_functions_exported(self): def test_output_constants_in_all(self): """Test OUTPUT_* constants are exported""" output_constants = [ - 'OUTPUT_PRESSURE', - 'OUTPUT_VELOCITY', - 'OUTPUT_FULL_FIELD', - 'OUTPUT_CSV_TIMESERIES', - 'OUTPUT_CSV_CENTERLINE', - 'OUTPUT_CSV_STATISTICS', + "OUTPUT_PRESSURE", + "OUTPUT_VELOCITY", + "OUTPUT_FULL_FIELD", + "OUTPUT_CSV_TIMESERIES", + "OUTPUT_CSV_CENTERLINE", + "OUTPUT_CSV_STATISTICS", ] for const_name in output_constants: assert const_name in cfd_python.__all__, f"{const_name} should be in __all__" diff --git a/tests/test_output.py b/tests/test_output.py index cf3ddac..408b0c4 100644 --- a/tests/test_output.py +++ b/tests/test_output.py @@ -1,7 +1,9 @@ """ Tests for VTK and CSV output functions """ + import pytest + import cfd_python @@ -15,8 +17,7 @@ def test_write_vtk_scalar(self, tmp_path): data = [float(i) for i in range(nx * ny)] cfd_python.write_vtk_scalar( - str(output_file), "test_field", data, - nx, ny, 0.0, 1.0, 0.0, 1.0 + str(output_file), "test_field", data, nx, ny, 0.0, 1.0, 0.0, 1.0 ) assert output_file.exists() @@ -27,8 +28,15 @@ def test_write_vtk_scalar_validates_size(self, tmp_path): output_file = tmp_path / "test.vtk" with pytest.raises(ValueError): cfd_python.write_vtk_scalar( - str(output_file), "test", [1.0, 2.0, 3.0], # 3 elements - 5, 5, 0.0, 1.0, 0.0, 1.0 # expects 25 elements + str(output_file), + "test", + [1.0, 2.0, 3.0], # 3 elements + 5, + 5, + 0.0, + 1.0, + 0.0, + 1.0, # expects 25 elements ) def test_write_vtk_vector(self, tmp_path): @@ -39,8 +47,7 @@ def test_write_vtk_vector(self, tmp_path): v_data = [float(i) * 0.5 for i in range(nx * ny)] cfd_python.write_vtk_vector( - str(output_file), "velocity", u_data, v_data, - nx, ny, 0.0, 1.0, 0.0, 1.0 + str(output_file), "velocity", u_data, v_data, nx, ny, 0.0, 1.0, 0.0, 1.0 ) assert output_file.exists() @@ -51,10 +58,16 @@ def test_write_vtk_vector_validates_size(self, tmp_path): output_file = tmp_path / "test.vtk" with pytest.raises(ValueError): cfd_python.write_vtk_vector( - str(output_file), "test", + str(output_file), + "test", [1.0, 2.0], # 2 elements [1.0, 2.0, 3.0], # 3 elements - 5, 5, 0.0, 1.0, 0.0, 1.0 + 5, + 5, + 0.0, + 1.0, + 0.0, + 1.0, ) @@ -71,10 +84,17 @@ def test_write_csv_timeseries_creates_file(self, tmp_path): p_data = [1.0] * size cfd_python.write_csv_timeseries( - str(output_file), step=0, time=0.0, - u_data=u_data, v_data=v_data, p_data=p_data, - nx=nx, ny=ny, dt=0.001, iterations=10, - create_new=True + str(output_file), + step=0, + time=0.0, + u_data=u_data, + v_data=v_data, + p_data=p_data, + nx=nx, + ny=ny, + dt=0.001, + iterations=10, + create_new=True, ) assert output_file.exists() @@ -91,19 +111,33 @@ def test_write_csv_timeseries_append(self, tmp_path): # Create new file cfd_python.write_csv_timeseries( - str(output_file), step=0, time=0.0, - u_data=u_data, v_data=v_data, p_data=p_data, - nx=nx, ny=ny, dt=0.001, iterations=10, - create_new=True + str(output_file), + step=0, + time=0.0, + u_data=u_data, + v_data=v_data, + p_data=p_data, + nx=nx, + ny=ny, + dt=0.001, + iterations=10, + create_new=True, ) initial_size = output_file.stat().st_size # Append to file cfd_python.write_csv_timeseries( - str(output_file), step=1, time=0.001, - u_data=u_data, v_data=v_data, p_data=p_data, - nx=nx, ny=ny, dt=0.001, iterations=10, - create_new=False + str(output_file), + step=1, + time=0.001, + u_data=u_data, + v_data=v_data, + p_data=p_data, + nx=nx, + ny=ny, + dt=0.001, + iterations=10, + create_new=False, ) # File should be larger after append diff --git a/tests/test_simulation.py b/tests/test_simulation.py index 2548fe1..2f6aa3a 100644 --- a/tests/test_simulation.py +++ b/tests/test_simulation.py @@ -1,7 +1,9 @@ """ Tests for simulation functions (grid, params, run_simulation) """ + import pytest + import cfd_python @@ -16,24 +18,24 @@ def test_create_grid_returns_dict(self): def test_create_grid_has_dimensions(self): """Test create_grid result contains dimensions""" grid = cfd_python.create_grid(10, 20, 0.0, 1.0, 0.0, 2.0) - assert grid['nx'] == 10 - assert grid['ny'] == 20 + assert grid["nx"] == 10 + assert grid["ny"] == 20 def test_create_grid_has_bounds(self): """Test create_grid result contains domain bounds""" grid = cfd_python.create_grid(10, 10, -1.0, 1.0, -2.0, 2.0) - assert grid['xmin'] == -1.0 - assert grid['xmax'] == 1.0 - assert grid['ymin'] == -2.0 - assert grid['ymax'] == 2.0 + assert grid["xmin"] == -1.0 + assert grid["xmax"] == 1.0 + assert grid["ymin"] == -2.0 + assert grid["ymax"] == 2.0 def test_create_grid_has_coordinates(self): """Test create_grid result contains coordinate arrays""" grid = cfd_python.create_grid(5, 3, 0.0, 1.0, 0.0, 1.0) - assert 'x_coords' in grid - assert 'y_coords' in grid - assert len(grid['x_coords']) == 5 - assert len(grid['y_coords']) == 3 + assert "x_coords" in grid + assert "y_coords" in grid + assert len(grid["x_coords"]) == 5 + assert len(grid["y_coords"]) == 3 class TestSolverParams: @@ -47,21 +49,21 @@ def test_get_default_solver_params_returns_dict(self): def test_default_params_has_dt(self): """Test default params contains dt""" params = cfd_python.get_default_solver_params() - assert 'dt' in params - assert isinstance(params['dt'], float) - assert params['dt'] > 0 + assert "dt" in params + assert isinstance(params["dt"], float) + assert params["dt"] > 0 def test_default_params_has_cfl(self): """Test default params contains cfl""" params = cfd_python.get_default_solver_params() - assert 'cfl' in params - assert isinstance(params['cfl'], float) - assert 0 < params['cfl'] <= 1 # CFL=1.0 is valid for some schemes + assert "cfl" in params + assert isinstance(params["cfl"], float) + assert 0 < params["cfl"] <= 1 # CFL=1.0 is valid for some schemes def test_default_params_has_all_keys(self): """Test default params contains all expected keys""" params = cfd_python.get_default_solver_params() - expected_keys = ['dt', 'cfl', 'gamma', 'mu', 'k', 'max_iter', 'tolerance'] + expected_keys = ["dt", "cfl", "gamma", "mu", "k", "max_iter", "tolerance"] for key in expected_keys: assert key in params, f"Missing parameter: {key}" @@ -88,33 +90,26 @@ def test_run_simulation_non_negative_values(self): def test_run_simulation_with_solver_type(self): """Test run_simulation with explicit solver type""" - result = cfd_python.run_simulation(5, 5, steps=3, solver_type='explicit_euler') + result = cfd_python.run_simulation(5, 5, steps=3, solver_type="explicit_euler") assert isinstance(result, list) assert len(result) == 25 def test_run_simulation_with_domain_bounds(self): """Test run_simulation with custom domain bounds""" - result = cfd_python.run_simulation( - 5, 5, steps=3, - xmin=-1.0, xmax=1.0, - ymin=-1.0, ymax=1.0 - ) + result = cfd_python.run_simulation(5, 5, steps=3, xmin=-1.0, xmax=1.0, ymin=-1.0, ymax=1.0) assert isinstance(result, list) def test_run_simulation_with_output_file(self, tmp_path): """Test run_simulation with VTK output""" output_file = tmp_path / "test_output.vtk" - result = cfd_python.run_simulation( - 5, 5, steps=3, - output_file=str(output_file) - ) + result = cfd_python.run_simulation(5, 5, steps=3, output_file=str(output_file)) assert output_file.exists() assert output_file.stat().st_size > 0 def test_run_simulation_invalid_solver(self): """Test run_simulation with invalid solver raises error""" with pytest.raises(RuntimeError): - cfd_python.run_simulation(5, 5, steps=3, solver_type='nonexistent_solver') + cfd_python.run_simulation(5, 5, steps=3, solver_type="nonexistent_solver") class TestRunSimulationWithParams: @@ -122,50 +117,39 @@ class TestRunSimulationWithParams: def test_returns_dict(self): """Test run_simulation_with_params returns a dictionary""" - result = cfd_python.run_simulation_with_params( - 5, 5, 0.0, 1.0, 0.0, 1.0, steps=3 - ) + result = cfd_python.run_simulation_with_params(5, 5, 0.0, 1.0, 0.0, 1.0, steps=3) assert isinstance(result, dict) def test_result_has_velocity_magnitude(self): """Test result contains velocity_magnitude""" - result = cfd_python.run_simulation_with_params( - 5, 5, 0.0, 1.0, 0.0, 1.0, steps=3 - ) - assert 'velocity_magnitude' in result - assert isinstance(result['velocity_magnitude'], list) + result = cfd_python.run_simulation_with_params(5, 5, 0.0, 1.0, 0.0, 1.0, steps=3) + assert "velocity_magnitude" in result + assert isinstance(result["velocity_magnitude"], list) def test_result_has_dimensions(self): """Test result contains grid dimensions""" - result = cfd_python.run_simulation_with_params( - 5, 7, 0.0, 1.0, 0.0, 1.0, steps=3 - ) - assert result['nx'] == 5 - assert result['ny'] == 7 + result = cfd_python.run_simulation_with_params(5, 7, 0.0, 1.0, 0.0, 1.0, steps=3) + assert result["nx"] == 5 + assert result["ny"] == 7 def test_result_has_solver_info(self): """Test result contains solver information""" - result = cfd_python.run_simulation_with_params( - 5, 5, 0.0, 1.0, 0.0, 1.0, steps=3 - ) - assert 'solver_name' in result + result = cfd_python.run_simulation_with_params(5, 5, 0.0, 1.0, 0.0, 1.0, steps=3) + assert "solver_name" in result def test_result_has_stats(self): """Test result contains solver statistics""" - result = cfd_python.run_simulation_with_params( - 5, 5, 0.0, 1.0, 0.0, 1.0, steps=3 - ) - assert 'stats' in result - stats = result['stats'] - assert 'iterations' in stats - assert 'max_velocity' in stats - assert 'max_pressure' in stats + result = cfd_python.run_simulation_with_params(5, 5, 0.0, 1.0, 0.0, 1.0, steps=3) + assert "stats" in result + stats = result["stats"] + assert "iterations" in stats + assert "max_velocity" in stats + assert "max_pressure" in stats def test_custom_dt_and_cfl(self): """Test custom dt and cfl parameters""" result = cfd_python.run_simulation_with_params( - 5, 5, 0.0, 1.0, 0.0, 1.0, - steps=3, dt=0.0005, cfl=0.1 + 5, 5, 0.0, 1.0, 0.0, 1.0, steps=3, dt=0.0005, cfl=0.1 ) assert isinstance(result, dict) @@ -173,8 +157,7 @@ def test_with_output_file(self, tmp_path): """Test with VTK output file""" output_file = tmp_path / "detailed_output.vtk" result = cfd_python.run_simulation_with_params( - 5, 5, 0.0, 1.0, 0.0, 1.0, - steps=3, output_file=str(output_file) + 5, 5, 0.0, 1.0, 0.0, 1.0, steps=3, output_file=str(output_file) ) - assert 'output_file' in result + assert "output_file" in result assert output_file.exists() diff --git a/tests/test_solvers.py b/tests/test_solvers.py index 993c15a..b5e4c9a 100644 --- a/tests/test_solvers.py +++ b/tests/test_solvers.py @@ -1,7 +1,9 @@ """ Tests for solver discovery and dynamic solver constants """ + import pytest + import cfd_python @@ -21,38 +23,38 @@ def test_list_solvers_not_empty(self): def test_list_solvers_contains_explicit_euler(self): """Test explicit_euler solver is available""" solvers = cfd_python.list_solvers() - assert 'explicit_euler' in solvers, "explicit_euler should be a built-in solver" + assert "explicit_euler" in solvers, "explicit_euler should be a built-in solver" def test_has_solver_true(self): """Test has_solver returns True for existing solver""" - assert cfd_python.has_solver('explicit_euler') is True + assert cfd_python.has_solver("explicit_euler") is True def test_has_solver_false(self): """Test has_solver returns False for non-existing solver""" - assert cfd_python.has_solver('nonexistent_solver_xyz') is False + assert cfd_python.has_solver("nonexistent_solver_xyz") is False def test_get_solver_info_returns_dict(self): """Test get_solver_info returns a dictionary""" - info = cfd_python.get_solver_info('explicit_euler') + info = cfd_python.get_solver_info("explicit_euler") assert isinstance(info, dict) def test_get_solver_info_has_required_keys(self): """Test get_solver_info contains expected keys""" - info = cfd_python.get_solver_info('explicit_euler') - assert 'name' in info - assert 'description' in info - assert 'version' in info - assert 'capabilities' in info + info = cfd_python.get_solver_info("explicit_euler") + assert "name" in info + assert "description" in info + assert "version" in info + assert "capabilities" in info def test_get_solver_info_capabilities_is_list(self): """Test solver capabilities is a list""" - info = cfd_python.get_solver_info('explicit_euler') - assert isinstance(info['capabilities'], list) + info = cfd_python.get_solver_info("explicit_euler") + assert isinstance(info["capabilities"], list) def test_get_solver_info_invalid_solver(self): """Test get_solver_info raises error for invalid solver""" with pytest.raises(ValueError): - cfd_python.get_solver_info('nonexistent_solver_xyz') + cfd_python.get_solver_info("nonexistent_solver_xyz") class TestDynamicSolverConstants: @@ -63,7 +65,7 @@ def test_solver_constants_exist(self): solvers = cfd_python.list_solvers() for solver_name in solvers: - const_name = 'SOLVER_' + solver_name.upper() + const_name = "SOLVER_" + solver_name.upper() assert hasattr(cfd_python, const_name), f"Missing constant: {const_name}" def test_solver_constants_values(self): @@ -71,12 +73,13 @@ def test_solver_constants_values(self): solvers = cfd_python.list_solvers() for solver_name in solvers: - const_name = 'SOLVER_' + solver_name.upper() + const_name = "SOLVER_" + solver_name.upper() const_value = getattr(cfd_python, const_name) assert const_value == solver_name, f"{const_name} should equal '{solver_name}'" def test_explicit_euler_constant(self): """Test SOLVER_EXPLICIT_EULER constant specifically""" - assert hasattr(cfd_python, 'SOLVER_EXPLICIT_EULER'), \ - "SOLVER_EXPLICIT_EULER constant should be defined" - assert getattr(cfd_python, 'SOLVER_EXPLICIT_EULER') == 'explicit_euler' + assert hasattr( + cfd_python, "SOLVER_EXPLICIT_EULER" + ), "SOLVER_EXPLICIT_EULER constant should be defined" + assert cfd_python.SOLVER_EXPLICIT_EULER == "explicit_euler" diff --git a/tests/test_vtk_output.py b/tests/test_vtk_output.py index 4f2641a..349b491 100644 --- a/tests/test_vtk_output.py +++ b/tests/test_vtk_output.py @@ -6,6 +6,7 @@ """ import pytest + import cfd_python @@ -22,9 +23,12 @@ def test_write_vtk_scalar_basic(self, tmp_path): filename=str(filename), field_name="pressure", data=data, - nx=nx, ny=ny, - xmin=0.0, xmax=1.0, - ymin=0.0, ymax=1.0 + nx=nx, + ny=ny, + xmin=0.0, + xmax=1.0, + ymin=0.0, + ymax=1.0, ) assert filename.exists() assert filename.stat().st_size > 0 @@ -39,9 +43,12 @@ def test_write_vtk_scalar_with_floats(self, tmp_path): filename=str(filename), field_name="temperature", data=data, - nx=nx, ny=ny, - xmin=0.0, xmax=2.0, - ymin=0.0, ymax=2.0 + nx=nx, + ny=ny, + xmin=0.0, + xmax=2.0, + ymin=0.0, + ymax=2.0, ) assert filename.exists() @@ -57,9 +64,12 @@ def test_write_vtk_scalar_wrong_size_raises(self, tmp_path): filename=str(filename), field_name="pressure", data=data, - nx=nx, ny=ny, - xmin=0.0, xmax=1.0, - ymin=0.0, ymax=1.0 + nx=nx, + ny=ny, + xmin=0.0, + xmax=1.0, + ymin=0.0, + ymax=1.0, ) def test_write_vtk_scalar_repeated_calls(self, tmp_path): @@ -73,9 +83,12 @@ def test_write_vtk_scalar_repeated_calls(self, tmp_path): filename=str(filename), field_name="density", data=data, - nx=nx, ny=ny, - xmin=0.0, xmax=1.0, - ymin=0.0, ymax=1.0 + nx=nx, + ny=ny, + xmin=0.0, + xmax=1.0, + ymin=0.0, + ymax=1.0, ) assert filename.exists() @@ -100,9 +113,12 @@ def test_write_vtk_vector_basic(self, tmp_path): field_name="velocity", u_data=u_data, v_data=v_data, - nx=nx, ny=ny, - xmin=0.0, xmax=1.0, - ymin=0.0, ymax=1.0 + nx=nx, + ny=ny, + xmin=0.0, + xmax=1.0, + ymin=0.0, + ymax=1.0, ) assert filename.exists() assert filename.stat().st_size > 0 @@ -120,9 +136,12 @@ def test_write_vtk_vector_with_zeros(self, tmp_path): field_name="velocity", u_data=u_data, v_data=v_data, - nx=nx, ny=ny, - xmin=0.0, xmax=1.0, - ymin=0.0, ymax=1.0 + nx=nx, + ny=ny, + xmin=0.0, + xmax=1.0, + ymin=0.0, + ymax=1.0, ) assert filename.exists() @@ -141,9 +160,12 @@ def test_write_vtk_vector_wrong_u_size_raises(self, tmp_path): field_name="velocity", u_data=u_data, v_data=v_data, - nx=nx, ny=ny, - xmin=0.0, xmax=1.0, - ymin=0.0, ymax=1.0 + nx=nx, + ny=ny, + xmin=0.0, + xmax=1.0, + ymin=0.0, + ymax=1.0, ) def test_write_vtk_vector_wrong_v_size_raises(self, tmp_path): @@ -161,9 +183,12 @@ def test_write_vtk_vector_wrong_v_size_raises(self, tmp_path): field_name="velocity", u_data=u_data, v_data=v_data, - nx=nx, ny=ny, - xmin=0.0, xmax=1.0, - ymin=0.0, ymax=1.0 + nx=nx, + ny=ny, + xmin=0.0, + xmax=1.0, + ymin=0.0, + ymax=1.0, ) def test_write_vtk_vector_repeated_calls(self, tmp_path): @@ -180,9 +205,12 @@ def test_write_vtk_vector_repeated_calls(self, tmp_path): field_name="velocity", u_data=u_data, v_data=v_data, - nx=nx, ny=ny, - xmin=0.0, xmax=1.0, - ymin=0.0, ymax=1.0 + nx=nx, + ny=ny, + xmin=0.0, + xmax=1.0, + ymin=0.0, + ymax=1.0, ) assert filename.exists() @@ -199,9 +227,12 @@ def test_write_vtk_vector_large_grid(self, tmp_path): field_name="velocity", u_data=u_data, v_data=v_data, - nx=nx, ny=ny, - xmin=0.0, xmax=1.0, - ymin=0.0, ymax=1.0 + nx=nx, + ny=ny, + xmin=0.0, + xmax=1.0, + ymin=0.0, + ymax=1.0, ) assert filename.exists() # File should be substantial size @@ -259,10 +290,11 @@ def test_write_csv_timeseries_basic(self, tmp_path): u_data=u_data, v_data=v_data, p_data=p_data, - nx=nx, ny=ny, + nx=nx, + ny=ny, dt=0.001, iterations=100, - create_new=True + create_new=True, ) assert filename.exists() assert filename.stat().st_size > 0 @@ -284,10 +316,11 @@ def test_write_csv_timeseries_append(self, tmp_path): u_data=u_data, v_data=v_data, p_data=p_data, - nx=nx, ny=ny, + nx=nx, + ny=ny, dt=0.001, iterations=100, - create_new=True + create_new=True, ) initial_size = filename.stat().st_size @@ -299,10 +332,11 @@ def test_write_csv_timeseries_append(self, tmp_path): u_data=u_data, v_data=v_data, p_data=p_data, - nx=nx, ny=ny, + nx=nx, + ny=ny, dt=0.001, iterations=101, - create_new=False + create_new=False, ) # File should be larger after append assert filename.stat().st_size > initial_size @@ -324,10 +358,11 @@ def test_write_csv_timeseries_repeated_calls(self, tmp_path): u_data=u_data, v_data=v_data, p_data=p_data, - nx=nx, ny=ny, + nx=nx, + ny=ny, dt=0.001, iterations=100 + step, - create_new=(step == 0) + create_new=(step == 0), ) @@ -338,7 +373,7 @@ def test_vtk_scalar_non_float_in_list(self, tmp_path): """Test that non-float values in list are handled.""" nx, ny = 4, 4 # Mix of ints and floats - should be converted - data = [i for i in range(nx * ny)] # integers + data = list(range(nx * ny)) # integers filename = tmp_path / "test.vtk" # Should work - ints are convertible to float @@ -346,9 +381,12 @@ def test_vtk_scalar_non_float_in_list(self, tmp_path): filename=str(filename), field_name="test", data=data, - nx=nx, ny=ny, - xmin=0.0, xmax=1.0, - ymin=0.0, ymax=1.0 + nx=nx, + ny=ny, + xmin=0.0, + xmax=1.0, + ymin=0.0, + ymax=1.0, ) assert filename.exists() @@ -364,9 +402,12 @@ def test_vtk_scalar_invalid_type_raises(self, tmp_path): filename=str(filename), field_name="test", data=data, - nx=nx, ny=ny, - xmin=0.0, xmax=1.0, - ymin=0.0, ymax=1.0 + nx=nx, + ny=ny, + xmin=0.0, + xmax=1.0, + ymin=0.0, + ymax=1.0, ) def test_csv_timeseries_non_list_u_raises(self, tmp_path): @@ -385,10 +426,11 @@ def test_csv_timeseries_non_list_u_raises(self, tmp_path): u_data="not a list", # Invalid type v_data=v_data, p_data=p_data, - nx=nx, ny=ny, + nx=nx, + ny=ny, dt=0.001, iterations=100, - create_new=True + create_new=True, ) def test_csv_timeseries_non_list_v_raises(self, tmp_path): @@ -407,10 +449,11 @@ def test_csv_timeseries_non_list_v_raises(self, tmp_path): u_data=u_data, v_data=(1, 2, 3), # Tuple instead of list p_data=p_data, - nx=nx, ny=ny, + nx=nx, + ny=ny, dt=0.001, iterations=100, - create_new=True + create_new=True, ) def test_csv_timeseries_non_list_p_raises(self, tmp_path): @@ -429,10 +472,11 @@ def test_csv_timeseries_non_list_p_raises(self, tmp_path): u_data=u_data, v_data=v_data, p_data=None, # None instead of list - nx=nx, ny=ny, + nx=nx, + ny=ny, dt=0.001, iterations=100, - create_new=True + create_new=True, ) def test_csv_timeseries_wrong_size_raises(self, tmp_path): @@ -452,8 +496,9 @@ def test_csv_timeseries_wrong_size_raises(self, tmp_path): u_data=u_data, v_data=v_data, p_data=p_data, - nx=nx, ny=ny, + nx=nx, + ny=ny, dt=0.001, iterations=100, - create_new=True + create_new=True, ) From 2bf293ce5c213e509da37c1e543499f538ba8db9 Mon Sep 17 00:00:00 2001 From: shaia Date: Fri, 5 Dec 2025 14:08:50 +0200 Subject: [PATCH 2/2] fix: add missing import statement in broken extension test The test was missing the actual import statement inside the pytest.raises block, causing it to never raise ImportError. --- tests/test_import_handling.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_import_handling.py b/tests/test_import_handling.py index 8e942c3..4837e5d 100644 --- a/tests/test_import_handling.py +++ b/tests/test_import_handling.py @@ -60,7 +60,7 @@ def test_broken_extension_raises_import_error(self, tmp_path): sys.path.insert(0, str(tmp_path)) try: with pytest.raises(ImportError) as exc_info: - pass + import fake_cfd_broken # noqa: F401 # Verify the error message is helpful assert "Failed to load C extension" in str(exc_info.value)