diff --git a/NEWS.md b/NEWS.md index fed5fa7..b5ffe9f 100644 --- a/NEWS.md +++ b/NEWS.md @@ -24,6 +24,15 @@ - `fzd` docstring corrected: `analysis_dir` defaults to `"analysis"` (the CLI uses `results_fzd`). +### Directory-tree inputs reach the calculator intact + +- Per-case staging now copies subdirectories recursively in **both** directions + (compiled inputs → run dir, and run outputs → results dir). Previously only top-level + files were copied, so any code whose input is a directory tree (e.g. an OpenFOAM case + with `system/`, `constant/`, `0/`) ran without its subdirectories and any output written + into a subdirectory never reached the output parser. Added a regression test that runs a + case with an input subdirectory and an output subdirectory. + ### fzd CLI execution fix - The `fzd` command and the `fz design` subcommand were unusable: both passed diff --git a/fz/helpers.py b/fz/helpers.py index 1a78f67..8199083 100644 --- a/fz/helpers.py +++ b/fz/helpers.py @@ -1003,12 +1003,20 @@ def run_single_case(case_info: Dict) -> Dict[str, Any]: file_names = [f.name for f in all_files if f.is_file()] log_debug(f"🔍 [Thread {thread_id}] {case_name}: Files in tmp_dir before copying: {file_names}") - # Copy each file from the already-retrieved list + # Copy each item from the already-retrieved list for item in all_files: + dest_file = result_dir / item.name try: - if item.is_file() and item.name != ".fz_hash": # Don't overwrite our hash - dest_file = result_dir / item.name - + if item.name == ".fz_hash": # Don't overwrite our hash + files_skipped += 1 + elif item.is_dir(): + # Recursively copy output subdirectories back (e.g. an + # OpenFOAM case's time dirs and postProcessing/), so output + # parsers run against the full case, not just top-level files. + shutil.copytree(item, dest_file, dirs_exist_ok=True) + files_copied += 1 + log_debug(f"📁 [Thread {thread_id}] {case_name}: Copied dir {item.name}: {item} → {dest_file}") + elif item.is_file(): # Additional validation: ensure we're copying to the right place if not dest_file.parent == result_dir: log_error(f"❌ [Thread {thread_id}] {case_name}: DIRECTORY MISMATCH! dest_file.parent={dest_file.parent}, expected result_dir={result_dir}") @@ -1643,16 +1651,23 @@ def prepare_temp_directories(var_combinations: List[Dict], temp_path: Path, resu tmp_dir.mkdir(parents=True, exist_ok=True) - # Copy files from result directory to temp directory (excluding .fz_hash) + # Copy files from result directory to temp directory (excluding .fz_hash). + # Subdirectories are copied recursively so directory-tree inputs (e.g. an + # OpenFOAM case with system/, constant/, 0/) reach the calculator intact. try: if result_dir.exists(): files_copied = 0 for item in result_dir.iterdir(): - if item.is_file() and item.name != ".fz_hash": + if item.name == ".fz_hash": + continue + if item.is_dir(): + shutil.copytree(item, tmp_dir / item.name, dirs_exist_ok=True) + files_copied += 1 + elif item.is_file(): shutil.copy2(item, tmp_dir) files_copied += 1 - log_debug(f"Prepared temp directory: {tmp_dir} ({files_copied} files copied from {result_dir})") + log_debug(f"Prepared temp directory: {tmp_dir} ({files_copied} items copied from {result_dir})") except Exception as e: log_warning(f"Warning: Could not copy files to temp directory for case {var_combo}: {e}") diff --git a/tests/test_multiple_input_files.py b/tests/test_multiple_input_files.py index b479e4c..d860b0d 100644 --- a/tests/test_multiple_input_files.py +++ b/tests/test_multiple_input_files.py @@ -431,5 +431,49 @@ def test_multiple_input_files_with_variables(): f"Expected {expected_cases} cases, got {actual_cases}" assert all_cases_verified, "Not all cases verified successfully" +def test_directory_input_with_subdirectories(): + """Regression: a directory-tree input must reach the calculator with its + subdirectories intact, and output subdirectories must be copied back. + + fz staged result->temp and temp->result by copying only top-level *files* + (`if item.is_file()`), silently dropping subdirectories. Any code whose input + is a directory tree (e.g. an OpenFOAM case with system/, constant/, 0/) then + failed: the calculator ran without its input subdirs, and outputs written into + a subdirectory never reached the output parser. Both copies now recurse. + """ + from fz import fzr + + case = Path.cwd() / "case_tree" + (case / "data").mkdir(parents=True) + # top-level entry file + a parameterized file inside a subdirectory + (case / "input.txt").write_text("x = ${x}\n") + (case / "data" / "factor.txt").write_text("${x}\n") + + # Runner lives OUTSIDE the input dir so it is not itself parameterized/staged. + # It reads the subdir INPUT (must be staged in) and writes into a NEW output + # subdirectory (must be copied back for the parser to see it). + runner = Path.cwd() / "runner.sh" + runner.write_text("#!/bin/bash\nmkdir -p out && cat data/factor.txt > out/result.txt\n") + + model = { + "varprefix": "$", + "delim": "{}", + "output": {"result": "cat out/result.txt"}, + } + + result = fzr( + input_path=str(case), + input_variables={"x": [2, 5]}, + model=model, + calculators=f"sh://bash {runner}", + results_dir=str(Path.cwd() / "res_tree"), + ) + + assert set(result["status"]) == {"done"}, f"statuses: {list(result['status'])}" + by_x = {x: r for x, r in zip(result["x"], result["result"])} + assert by_x == {2: 2, 5: 5}, f"expected results from subdir I/O, got {by_x}" + + if __name__ == "__main__": test_multiple_input_files_with_variables() + test_directory_input_with_subdirectories()