From c682bfeb6be0b9acc5b240cd64c1241757f52505 Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Fri, 22 May 2026 18:16:36 +0100 Subject: [PATCH] Namespace PYAUTO_TEST_MODE output under output/test_mode/ When PYAUTO_TEST_MODE is set, AutoFit now writes search output to output/test_mode/... instead of output/.... Without this, a smoke-test run cached its result at the same path as a later real run, and the real run silently short-circuited with "Fit Already Completed". Adds a private _test_mode_segment() helper in autofit.non_linear.paths.abstract and threads it through the three sites that compose paths from conf.instance.output_path: - AbstractPaths.output_path (paths/abstract.py) - DirectoryPaths._make_path (paths/directory.py) - open_database with a relative filename (database/__init__.py) SubDirectoryPaths inherits the fix via self.parent.output_path. Regression tests added covering output_path and _make_path with/without the env var. Co-Authored-By: Claude Opus 4.7 (1M context) --- autofit/database/__init__.py | 4 +++ autofit/non_linear/paths/abstract.py | 23 ++++++++++++++++ autofit/non_linear/paths/directory.py | 8 ++++-- test_autofit/non_linear/paths/test_paths.py | 30 +++++++++++++++++++++ 4 files changed, 63 insertions(+), 2 deletions(-) diff --git a/autofit/database/__init__.py b/autofit/database/__init__.py index a54c3beb0..a142db5ba 100644 --- a/autofit/database/__init__.py +++ b/autofit/database/__init__.py @@ -7,6 +7,7 @@ from .aggregator import * from .migration.steps import migrator from .model import * +from autofit.non_linear.paths.abstract import _test_mode_segment def open_database( @@ -60,6 +61,9 @@ def open_database( output_path = conf.instance.output_path if not filename.startswith("/"): + segment = _test_mode_segment() + if segment: + output_path = f"{output_path}/{segment}" filename = f"{output_path}/{filename}" os.makedirs( diff --git a/autofit/non_linear/paths/abstract.py b/autofit/non_linear/paths/abstract.py index edccecb4c..e2a26c2e3 100644 --- a/autofit/non_linear/paths/abstract.py +++ b/autofit/non_linear/paths/abstract.py @@ -22,6 +22,19 @@ pattern = re.compile(r"(? Optional[str]: + """ + Returns ``"test_mode"`` when ``PYAUTO_TEST_MODE`` is set in the + environment, else ``None``. + + Inserted into the output-path composition so that smoke runs land + under ``output/test_mode/...`` instead of sharing a directory with + real runs. Without this, a cached test-mode result short-circuits + a later real run at the same paths ("Fit Already Completed"). + """ + return "test_mode" if os.environ.get("PYAUTO_TEST_MODE") else None + + class AbstractPaths(ABC): def __init__( self, @@ -53,6 +66,15 @@ def __init__( /path/to/output/folder_0/folder_1/name + If the ``PYAUTO_TEST_MODE`` environment variable is set, a + ``test_mode`` segment is inserted directly after the output + root: + + /path/to/output/test_mode/folder_0/folder_1/name + + This keeps smoke-test artefacts in a sibling tree so they + cannot be picked up by a later real run with the same paths. + Parameters ---------- name @@ -246,6 +268,7 @@ def output_path(self) -> Path: None, [ str(conf.instance.output_path), + _test_mode_segment(), str(self.path_prefix), self.unique_tag, str(self.name), diff --git a/autofit/non_linear/paths/directory.py b/autofit/non_linear/paths/directory.py index db2c6d6ae..c55790924 100644 --- a/autofit/non_linear/paths/directory.py +++ b/autofit/non_linear/paths/directory.py @@ -16,7 +16,7 @@ from autofit.tools.util import open_ from autofit.non_linear.samples.samples import Samples -from .abstract import AbstractPaths +from .abstract import AbstractPaths, _test_mode_segment from ..samples import load_from_table from autofit.non_linear.samples.pdf import SamplesPDF @@ -530,7 +530,11 @@ def _make_path(self) -> str: The path terminates with the identifier, unless the identifier has already been added to the path. """ - path_ = Path(conf.instance.output_path) / self.path_prefix / self.name + path_ = Path(conf.instance.output_path) + segment = _test_mode_segment() + if segment: + path_ = path_ / segment + path_ = path_ / self.path_prefix / self.name if self.is_identifier_in_paths: path_ = path_ / self.identifier return path_ diff --git a/test_autofit/non_linear/paths/test_paths.py b/test_autofit/non_linear/paths/test_paths.py index e36cf5ed4..4738e73e4 100644 --- a/test_autofit/non_linear/paths/test_paths.py +++ b/test_autofit/non_linear/paths/test_paths.py @@ -75,3 +75,33 @@ def test_serialize(model): def test_unique_tag(): paths = af.DirectoryPaths(unique_tag="unique_tag") assert "unique_tag" in paths.output_path.parts + + +class TestTestModeOutputPath: + """ + PYAUTO_TEST_MODE must namespace the AutoFit output path so smoke + runs cannot poison the cache for a later real run at the same + paths. Regression for the "Fit Already Completed" silent skip. + """ + + def test_output_path_contains_test_mode_segment_when_env_set( + self, monkeypatch + ): + monkeypatch.setenv("PYAUTO_TEST_MODE", "2") + paths = af.DirectoryPaths(name="name", path_prefix="prefix") + assert "test_mode" in paths.output_path.parts + + def test_output_path_excludes_segment_when_env_unset(self, monkeypatch): + monkeypatch.delenv("PYAUTO_TEST_MODE", raising=False) + paths = af.DirectoryPaths(name="name", path_prefix="prefix") + assert "test_mode" not in paths.output_path.parts + + def test_make_path_contains_segment_when_env_set(self, monkeypatch): + monkeypatch.setenv("PYAUTO_TEST_MODE", "2") + paths = af.DirectoryPaths(name="name", path_prefix="prefix") + assert "test_mode" in paths._make_path().parts + + def test_make_path_excludes_segment_when_env_unset(self, monkeypatch): + monkeypatch.delenv("PYAUTO_TEST_MODE", raising=False) + paths = af.DirectoryPaths(name="name", path_prefix="prefix") + assert "test_mode" not in paths._make_path().parts