Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 22 additions & 7 deletions fz/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines 1003 to +1006
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}")
Expand Down Expand Up @@ -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():
Comment on lines 1659 to +1663
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}")

Expand Down
44 changes: 44 additions & 0 deletions tests/test_multiple_input_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Loading