diff --git a/pyproject.toml b/pyproject.toml index 7243809..099bc51 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,7 @@ classifiers = [ requires-python = ">=3.9,<3.12" dependencies = [ "py_wake>=2.6.5", - "foxes>=1.6.2", + "foxes>=1.7.0", "windIO @ git+https://github.com/EUFlow/windIO.git", "wayve @ git+https://gitlab.kuleuven.be/TFSO-software/wayve@dev_foxes", #"numpy<2", diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..bdaf9c1 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,122 @@ +""" +Pytest configuration and fixtures for WIFA tests. + +Provides: +- Pre-test cleanup of leftover output directories +- Output directory fixtures with conditional cleanup (preserved on failure) +- FOXES engine fixture with proper initialization and teardown +""" + +import shutil +from pathlib import Path + +import pytest + +# Handle different foxes versions - reset_engine location varies +try: + from foxes import reset_engine +except ImportError: + try: + from foxes.core import reset_engine + except ImportError: + try: + from foxes.core.engine import reset_engine + except ImportError: + # Older foxes versions don't have reset_engine - use no-op + def reset_engine(): + pass + + +# Store test outcomes for conditional cleanup +_test_outcomes = {} + + +@pytest.hookimpl(hookwrapper=True) +def pytest_runtest_makereport(item, call): + """Track test outcomes to conditionally preserve output on failure.""" + outcome = yield + report = outcome.get_result() + if report.when == "call": + _test_outcomes[item.nodeid] = report.failed + + +def pytest_configure(config): + """Register custom markers.""" + config.addinivalue_line( + "markers", "slow: marks tests as slow (deselect with '-m \"not slow\"')" + ) + + +@pytest.fixture(scope="session", autouse=True) +def cleanup_old_outputs(): + """Remove output directories from previous test runs at session start.""" + patterns = [ + "output_pywake_*", + "output_test_*", + "output", + ] + for pattern in patterns: + for path in Path(".").glob(pattern): + if path.is_dir(): + shutil.rmtree(path) + elif path.is_file(): + path.unlink() + yield + + +@pytest.fixture +def output_dir(request, tmp_path): + """ + Provide a unique temporary output directory for tests. + + Cleans up automatically on test success, preserves on failure for debugging. + """ + yield tmp_path + + # Only clean up if test passed + test_failed = _test_outcomes.get(request.node.nodeid, False) + if not test_failed: + if tmp_path.exists(): + shutil.rmtree(tmp_path) + + +@pytest.fixture +def named_output_dir(request): + """ + Provide a named output directory based on test name. + + Use this when tests need output in the current working directory + (e.g., for Code Saturne integration). + + Cleans up automatically on test success, preserves on failure. + """ + test_name = request.node.name.replace("[", "_").replace("]", "_").rstrip("_") + output_path = Path(f"output_test_{test_name}") + output_path.mkdir(parents=True, exist_ok=True) + + yield output_path + + # Only clean up if test passed + test_failed = _test_outcomes.get(request.node.nodeid, False) + if not test_failed: + if output_path.exists(): + shutil.rmtree(output_path) + + +@pytest.fixture +def cleanup_output_dir(request): + """ + Cleanup fixture for tests that write to the default 'output/' directory. + + Use this when tests can't control their output location (e.g., when YAML + specifies output_folder). Cleans up on success, preserves on failure. + """ + output_path = Path("output") + + yield output_path + + # Only clean up if test passed + test_failed = _test_outcomes.get(request.node.nodeid, False) + if not test_failed: + if output_path.exists(): + shutil.rmtree(output_path) diff --git a/tests/test_cs.py b/tests/test_cs.py index 622164d..8aa51fb 100644 --- a/tests/test_cs.py +++ b/tests/test_cs.py @@ -11,51 +11,52 @@ def _run_cs(wes_dir, output_dir): + """Run Code Saturne on all system* files in the given directory.""" i = 1 for yaml_input in wes_dir.glob("system*"): print("\nRUNNING CODE_SATURNE ON", yaml_input, "\n") validate_yaml(yaml_input, Path("plant/wind_energy_system")) + # Pass subdirectory path - run_code_saturne will create it + sub_output_dir = output_dir / f"run_{i}" run_code_saturne( yaml_input, test_mode=False, - output_dir="output_test_" + output_dir + "_" + str(i), + output_dir=str(sub_output_dir), ) i += 1 -def test_cs_KUL(): +def test_cs_KUL(output_dir): wes_dir = test_path / "../examples/cases/KUL_LES/wind_energy_system/" - _run_cs(wes_dir, "KUL") + _run_cs(wes_dir, output_dir) -def test_cs_4wts(): +def test_cs_4wts(output_dir): wes_dir = test_path / "../examples/cases/windio_4turbines/wind_energy_system/" - _run_cs(wes_dir, "4wts") + _run_cs(wes_dir, output_dir) -def test_cs_abl(): +def test_cs_abl(output_dir): wes_dir = test_path / "../examples/cases/windio_4turbines_ABL/wind_energy_system/" - _run_cs(wes_dir, "abl") + _run_cs(wes_dir, output_dir) -def test_cs_abl_stable(): +def test_cs_abl_stable(output_dir): wes_dir = ( test_path / "../examples/cases/windio_4turbines_ABL_stable/wind_energy_system/" ) - _run_cs(wes_dir, "abl_stable") + _run_cs(wes_dir, output_dir) -def test_cs_profiles(): +def test_cs_profiles(output_dir): wes_dir = ( test_path / "../examples/cases/windio_4turbines_profiles_stable/wind_energy_system/" ) - _run_cs(wes_dir, "profiles") + _run_cs(wes_dir, output_dir) if __name__ == "__main__": - test_cs_KUL() - test_cs_4wts() - test_cs_abl() - test_cs_abl_stable() - test_cs_profiles() + import pytest + + pytest.main([__file__, "-v"]) diff --git a/tests/test_foxes.py b/tests/test_foxes.py index ac22881..1f2c1a0 100644 --- a/tests/test_foxes.py +++ b/tests/test_foxes.py @@ -1,7 +1,7 @@ import os from pathlib import Path +from shutil import rmtree -from foxes import Engine, reset_engine from windIO import __path__ as wiop from windIO import validate as validate_yaml @@ -10,96 +10,77 @@ test_path = Path(os.path.dirname(__file__)) windIO_path = Path(wiop[0]) -engine = None - -def _run_foxes(wes_dir): +def _run_foxes(wes_dir, output_dir): + """Run FOXES on all system.yaml files in the given directory.""" assert wes_dir.is_dir(), f"{wes_dir} is not a directory" - global engine - if engine is None: - engine = Engine.new("default", verbosity=0) - engine.initialize() - print("SETTING ENGINE:", engine) - for yaml_input in wes_dir.glob("system.yaml"): if "_noXYgrid" not in str(yaml_input): print("\nRUNNING FOXES ON", yaml_input, "\n") validate_yaml(yaml_input, Path("plant/wind_energy_system")) - output_dir_name = Path("output_test_foxes") - output_dir_name.mkdir(parents=True, exist_ok=True) - try: - run_foxes(yaml_input, output_dir=output_dir_name, engine=None) - except Exception as e: - reset_engine() - engine = None - raise e + run_foxes(yaml_input, output_dir=output_dir, engine=None) -def test_foxes_KUL(): +def test_foxes_KUL(output_dir): wes_dir = test_path / "../examples/cases/KUL_LES/wind_energy_system/" - _run_foxes(wes_dir) + _run_foxes(wes_dir, output_dir) -def test_foxes_4wts(): +def test_foxes_4wts(output_dir): wes_dir = test_path / "../examples/cases/windio_4turbines/wind_energy_system/" - _run_foxes(wes_dir) + _run_foxes(wes_dir, output_dir) -def test_foxes_abl(): +def test_foxes_abl(output_dir): wes_dir = test_path / "../examples/cases/windio_4turbines_ABL/wind_energy_system/" - _run_foxes(wes_dir) + _run_foxes(wes_dir, output_dir) -def test_foxes_abl_stable(): +def test_foxes_abl_stable(output_dir): wes_dir = ( test_path / "../examples/cases/windio_4turbines_ABL_stable/wind_energy_system/" ) - _run_foxes(wes_dir) + _run_foxes(wes_dir, output_dir) -def test_foxes_profiles(): +def test_foxes_profiles(output_dir): wes_dir = ( test_path / "../examples/cases/windio_4turbines_profiles_stable/wind_energy_system/" ) - _run_foxes(wes_dir) + _run_foxes(wes_dir, output_dir) -def test_foxes_heterogeneous_wind_rose_at_turbines(): +def test_foxes_heterogeneous_wind_rose_at_turbines(output_dir): wes_dir = ( test_path / "../examples/cases/heterogeneous_wind_rose_at_turbines/wind_energy_system/" ) - _run_foxes(wes_dir) + _run_foxes(wes_dir, output_dir) -def test_foxes_heterogeneous_wind_rose_map(): +def test_foxes_heterogeneous_wind_rose_map(output_dir): wes_dir = ( test_path / "../examples/cases/heterogeneous_wind_rose_map/wind_energy_system/" ) - _run_foxes(wes_dir) + _run_foxes(wes_dir, output_dir) -def test_foxes_simple_wind_rose(): +def test_foxes_simple_wind_rose(output_dir): wes_dir = test_path / "../examples/cases/simple_wind_rose/wind_energy_system/" - _run_foxes(wes_dir) + _run_foxes(wes_dir, output_dir) -def test_foxes_timeseries_with_operating_flag(): +def test_foxes_timeseries_with_operating_flag(output_dir): wes_dir = ( test_path / "../examples/cases/timeseries_with_operating_flag/wind_energy_system/" ) - _run_foxes(wes_dir) + _run_foxes(wes_dir, output_dir) if __name__ == "__main__": - test_foxes_KUL() - test_foxes_4wts() - test_foxes_abl() - test_foxes_abl_stable() - test_foxes_profiles() - test_foxes_heterogeneous_wind_rose_at_turbines() - test_foxes_heterogeneous_wind_rose_map() - test_foxes_simple_wind_rose() + import pytest + + pytest.main([__file__, "-v"]) diff --git a/tests/test_pywake.py b/tests/test_pywake.py index 3c01eb2..77bb790 100644 --- a/tests/test_pywake.py +++ b/tests/test_pywake.py @@ -50,7 +50,7 @@ def four_turbine_site(config_params): return wfm(x, y, ws=ws, wd=wd, time=True), config_name -def test_pywake_KUL(): +def test_pywake_KUL(output_dir): yaml_input = ( test_path / "../examples/cases/KUL_LES/wind_energy_system/system_pywake.yaml" ) @@ -59,10 +59,7 @@ def test_pywake_KUL(): validate_yaml(yaml_input, Path("plant/wind_energy_system")) # compute AEP (next step is to return a richer set of outputs) - output_dir_name = "output_pywake_4wts" - Path(output_dir_name).mkdir(parents=True, exist_ok=True) - pywake_aep = run_pywake(yaml_input, output_dir=output_dir_name) - # print(pywake_aep) + pywake_aep = run_pywake(yaml_input, output_dir=output_dir) # Check result pywake_aep_expected = 7515.2 @@ -87,7 +84,7 @@ def config_params(request): return request.param -def test_pywake_4wts(four_turbine_site): +def test_pywake_4wts(four_turbine_site, output_dir): wfm, config_name = four_turbine_site yaml_input = ( @@ -98,17 +95,14 @@ def test_pywake_4wts(four_turbine_site): validate_yaml(yaml_input, Path("plant/wind_energy_system")) # compute AEP (next step is to return a richer set of outputs) - output_dir_name = "output_pywake_4wts" - Path(output_dir_name).mkdir(parents=True, exist_ok=True) - pywake_aep = run_pywake(yaml_input, output_dir=output_dir_name) - # print(pywake_aep) + pywake_aep = run_pywake(yaml_input, output_dir=output_dir) # Check result pywake_aep_expected = wfm.aep().sum() npt.assert_array_almost_equal(pywake_aep, pywake_aep_expected, 0) -def test_pywake_4wts_operating_flag(): +def test_pywake_4wts_operating_flag(output_dir): x = [0, 1248.1, 2496.2, 3744.3] y = [0, 0, 0, 0] ws = [10.0910225, 10.233016, 8.797999, 9.662098, 9.78371, 10.307792] @@ -148,10 +142,7 @@ def test_pywake_4wts_operating_flag(): validate_yaml(yaml_input, Path("plant/wind_energy_system")) # compute AEP (next step is to return a richer set of outputs) - output_dir_name = "output_pywake_4wts" - Path(output_dir_name).mkdir(parents=True, exist_ok=True) - pywake_aep = run_pywake(yaml_input, output_dir=output_dir_name) - # print(pywake_aep) + pywake_aep = run_pywake(yaml_input, output_dir=output_dir) # Check result pywake_aep_expected = res.aep().sum() @@ -181,7 +172,9 @@ def test_pywake_4wts_operating_flag(): # fmt: on -def test_simple_wind_rose(): +def test_simple_wind_rose(cleanup_output_dir): + # Note: This test uses the output_folder from the YAML ("output/"), not a fixture. + # The cleanup_output_dir fixture handles cleanup of "output/". _ = run_pywake( test_path / "../examples/cases/simple_wind_rose/wind_energy_system/system.yaml" ) diff --git a/tests/test_wayve.py b/tests/test_wayve.py index 902edf0..c418b06 100644 --- a/tests/test_wayve.py +++ b/tests/test_wayve.py @@ -1,6 +1,7 @@ import os from pathlib import Path +import pytest from windIO import __path__ as wiop from windIO import validate as validate_yaml @@ -10,16 +11,16 @@ windIO_path = Path(wiop[0]) -# @pytest.mark.skip() -def test_wayve_4wts(): +@pytest.mark.slow +def test_wayve_4wts(output_dir): yaml_input = ( test_path / "../examples/cases/windio_4turbines/wind_energy_system/system.yaml" ) validate_yaml(yaml_input, Path("plant/wind_energy_system")) - output_dir_name = Path("output_test_wayve") - output_dir_name.mkdir(parents=True, exist_ok=True) - run_wayve(yaml_input, output_dir=output_dir_name, debug_mode=True) + run_wayve(yaml_input, output_dir=output_dir, debug_mode=True) if __name__ == "__main__": - test_wayve_4wts() + import pytest + + pytest.main([__file__, "-v"]) diff --git a/wifa/pywake_api.py b/wifa/pywake_api.py index 1c42edb..584d32c 100644 --- a/wifa/pywake_api.py +++ b/wifa/pywake_api.py @@ -97,7 +97,7 @@ def weighted_quantile( return np.interp(quantiles, weighted_quantiles, values) -def run_pywake(yamlFile, output_dir="output"): +def run_pywake(yamlFile, output_dir=None): from py_wake import NOJ, BastankhahGaussian, HorizontalGrid from py_wake.deficit_models import SelfSimilarityDeficit2020 from py_wake.deficit_models.fuga import FugaDeficit @@ -179,12 +179,15 @@ def dict_to_site(resource_dict): else: system_dat = yamlFile - # output_dir priority: 1) yaml file, 2) function argument, 3) default - output_dir = str( - system_dat["attributes"] - .get("model_outputs_specification", {}) - .get("output_folder", output_dir) - ) + # output_dir priority: 1) function argument, 2) yaml file, 3) default "output" + if output_dir is None: + output_dir = str( + system_dat["attributes"] + .get("model_outputs_specification", {}) + .get("output_folder", "output") + ) + else: + output_dir = str(output_dir) Path(output_dir).mkdir(parents=True, exist_ok=True)