From bc626bbe383e18b382516a2765fb803fbebee52c Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Tue, 21 Jul 2026 14:00:47 +0100 Subject: [PATCH] fix: make Aggregator.from_directory test-mode-aware (interpolate tutorial IndexError) Under PYAUTO_TEST_MODE searches write their results beneath an inserted `test_mode` segment (`output/test_mode/`) rather than the real-run location a caller points at (`output/`). `Aggregator.from_directory` walked only the literal directory, found nothing, and returned an empty aggregator, so the `features/interpolate` tutorial's database section raised `IndexError: Cannot interpolate: no instances`. Refactor the directory walk into a nested `scan()` helper and, when the first scan yields no search-outputs and `is_test_mode()` is active, retry once against the `test_mode` sibling (`.parent / "test_mode" / .name`). The fallback is a no-op outside test mode and for custom-directory aggregator scripts (no `test_mode` sibling exists), so real-run behaviour is unchanged. Fixes PyAutoLabs/PyAutoFit#1401. Co-Authored-By: Claude Opus 4.8 --- autofit/aggregator/aggregator.py | 87 +++++++++++-------- .../aggregator/test_from_directory.py | 31 +++++++ 2 files changed, 84 insertions(+), 34 deletions(-) diff --git a/autofit/aggregator/aggregator.py b/autofit/aggregator/aggregator.py index d949a5006..2793fc81f 100755 --- a/autofit/aggregator/aggregator.py +++ b/autofit/aggregator/aggregator.py @@ -19,6 +19,8 @@ from shutil import rmtree from typing import List, Union, Iterator, Optional +from autonerves.test_mode import is_test_mode + from .predicate import AttributePredicate from .search_output import SearchOutput, GridSearchOutput, GridSearch @@ -168,42 +170,59 @@ def from_directory( """ print("Aggregator loading search_outputs... could take some time.") - search_outputs = [] - grid_search_outputs = [] - - for root, dirs, filenames in os.walk(directory, topdown=True): - for filename in filenames: - if filename.endswith(".zip"): - extracted = Path(root) / filename[:-4] - if extracted.exists(): - continue - try: - with zipfile.ZipFile(Path(root) / filename, "r") as f: - f.extractall(extracted) - except zipfile.BadZipFile: - raise zipfile.BadZipFile( - f"File is not a zip file: \n " f"{root} \n" f"{filename}" + def scan(scan_directory): + search_outputs = [] + grid_search_outputs = [] + + for root, dirs, filenames in os.walk(scan_directory, topdown=True): + for filename in filenames: + if filename.endswith(".zip"): + extracted = Path(root) / filename[:-4] + if extracted.exists(): + continue + try: + with zipfile.ZipFile(Path(root) / filename, "r") as f: + f.extractall(extracted) + except zipfile.BadZipFile: + raise zipfile.BadZipFile( + f"File is not a zip file: \n " f"{root} \n" f"{filename}" + ) + dirs.append(filename[:-4]) + + def should_add(): + return not completed_only or ".completed" in filenames + + if "metadata" in filenames: + if should_add(): + search_outputs.append( + SearchOutput( + Path(root), + reference=reference, + ) ) - dirs.append(filename[:-4]) - - def should_add(): - return not completed_only or ".completed" in filenames - - if "metadata" in filenames: - if should_add(): - search_outputs.append( - SearchOutput( - Path(root), - reference=reference, + if ".is_grid_search" in filenames: + if should_add(): + grid_search_outputs.append( + GridSearchOutput( + Path(root), + ) ) - ) - if ".is_grid_search" in filenames: - if should_add(): - grid_search_outputs.append( - GridSearchOutput( - Path(root), - ) - ) + + return search_outputs, grid_search_outputs + + search_outputs, grid_search_outputs = scan(directory) + + # Under test mode the searches wrote their results beneath an inserted + # ``test_mode`` segment (``output/test_mode/``) rather than the + # real-run location the caller points at (``output/``); see + # ``_test_mode_segment`` in ``non_linear/paths/abstract.py``. If nothing + # was found there, retry once against that sibling so aggregator-based + # tutorials (e.g. features/interpolate) run cleanly under test mode. + if len(search_outputs) == 0 and is_test_mode(): + directory = Path(directory) + test_mode_directory = directory.parent / "test_mode" / directory.name + if test_mode_directory.exists(): + search_outputs, grid_search_outputs = scan(test_mode_directory) if len(search_outputs) == 0: print(f"\nNo search_outputs found in {directory}\n") diff --git a/test_autofit/aggregator/test_from_directory.py b/test_autofit/aggregator/test_from_directory.py index b8fbd3888..92a5bc0fa 100644 --- a/test_autofit/aggregator/test_from_directory.py +++ b/test_autofit/aggregator/test_from_directory.py @@ -68,3 +68,34 @@ def test_samples_summary_cached(): search_output = SearchOutput(directory) assert search_output.samples_summary is search_output.samples_summary + + +@pytest.fixture(name="test_mode_directory") +def make_test_mode_directory(tmp_path): + """ + Mirrors the on-disk layout a search produces under test mode: the results + live beneath an inserted ``test_mode`` segment (``output/test_mode/prefix``) + while the caller points ``from_directory`` at the real-run location + (``output/prefix``), which holds no metadata. + """ + source = Path(__file__).parent / "search_output" + real_directory = tmp_path / "output" / "prefix" + real_directory.mkdir(parents=True) + shutil.copytree(source, real_directory.parent / "test_mode" / "prefix" / "search_output") + return real_directory + + +def test_from_directory_test_mode_fallback(test_mode_directory, monkeypatch): + monkeypatch.setattr( + "autofit.aggregator.aggregator.is_test_mode", lambda: True + ) + aggregator = Aggregator.from_directory(test_mode_directory) + assert len(aggregator) == 1 + + +def test_from_directory_no_fallback_when_not_test_mode(test_mode_directory, monkeypatch): + monkeypatch.setattr( + "autofit.aggregator.aggregator.is_test_mode", lambda: False + ) + aggregator = Aggregator.from_directory(test_mode_directory) + assert len(aggregator) == 0