Skip to content

Commit 91f57f3

Browse files
committed
Test: Verify edited image creation cascades
Edited image creation needs a pure non-GUI regression contract to ensure cascade recomputation preserves existing workspace objects while propagating regenerated data to downstream processing. * [CHG] : Verify the exact edited Gauss2D parameters reach production image creation * [CHG] : Verify source identity, UUID and metadata are preserved in place * [CHG] : Verify downstream 1-to-1 processing consumes regenerated data and updates its existing output in place
1 parent a9838d1 commit 91f57f3

1 file changed

Lines changed: 199 additions & 0 deletions

File tree

datalab/tests/features/common/history_model_unit_test.py

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,28 @@
66

77
import os
88
import tempfile
9+
from types import SimpleNamespace
10+
from unittest.mock import patch
911

12+
import numpy as np
13+
from sigima.objects import Gauss2DParam, ImageObj
14+
15+
from datalab.gui.creation import (
16+
create_image_from_param,
17+
extract_creation_parameters,
18+
)
19+
from datalab.gui.panel.history import recompute as hrec
20+
from datalab.gui.processor.base import (
21+
ProcessingParameters,
22+
extract_processing_parameters,
23+
insert_processing_parameters,
24+
)
1025
from datalab.h5.native import NativeH5Writer
1126
from datalab.history.action import HistoryAction
1227
from datalab.history.core import HISTORY_ACTION_SCHEMA_VERSION, HISTORY_SCHEMA_VERSION
1328
from datalab.history.session import HistorySession
29+
from datalab.history.workspace_state import WorkspaceState
30+
from datalab.objectmodel import get_uuid, set_uuid
1431
from datalab.tests.features.common.history_test_helpers import (
1532
build_history_action,
1633
build_replay_map,
@@ -20,6 +37,25 @@
2037
)
2138

2239

40+
class CascadeObjectModel:
41+
"""Minimal object model for pure cascade recomputation tests."""
42+
43+
def __init__(self, objects: list[ImageObj]) -> None:
44+
self.objects = {get_uuid(obj): obj for obj in objects}
45+
46+
def __getitem__(self, uuid: str) -> ImageObj:
47+
"""Return the image identified by ``uuid``."""
48+
return self.objects[uuid]
49+
50+
def has_uuid(self, uuid: str) -> bool:
51+
"""Return whether ``uuid`` exists in the model."""
52+
return uuid in self.objects
53+
54+
def get_object_ids(self) -> list[str]:
55+
"""Return all object UUIDs in insertion order."""
56+
return list(self.objects)
57+
58+
2359
def test_action_hdf5_current_and_legacy_contract() -> None:
2460
"""Round-trip current fields and apply all legacy defaults."""
2561
action = build_history_action()
@@ -143,3 +179,166 @@ def test_replay_uuid_map_preserves_operands_and_tracks_changes() -> None:
143179
replay_map.capture_changes(HistoryAction(), before)
144180
assert "old-new" not in replay_map.mapping["signal"]
145181
assert image_model.get_object_ids() == ["image-old"]
182+
183+
184+
def test_edited_image_creation_recomputes_downstream_in_place() -> None:
185+
"""Regenerate an edited image before recomputing its existing descendant."""
186+
initial_param = Gauss2DParam.create(
187+
title="Initial Gaussian",
188+
height=24,
189+
width=28,
190+
x0=-4.0,
191+
y0=2.0,
192+
sigma=1.2,
193+
a=25.0,
194+
)
195+
edited_param = Gauss2DParam.create(
196+
title="Edited Gaussian",
197+
height=24,
198+
width=28,
199+
x0=3.0,
200+
y0=-2.0,
201+
sigma=3.5,
202+
a=80.0,
203+
)
204+
source = create_image_from_param(initial_param)
205+
source_uuid = get_uuid(source)
206+
source_identity = id(source)
207+
initial_source_data = source.data.copy()
208+
expected_source = create_image_from_param(edited_param)
209+
210+
downstream = source.copy()
211+
set_uuid(downstream)
212+
downstream.title = "Initial downstream"
213+
downstream.data = np.full(source.data.shape, -1.0)
214+
downstream_uuid = get_uuid(downstream)
215+
downstream_identity = id(downstream)
216+
initial_downstream_data = downstream.data.copy()
217+
insert_processing_parameters(
218+
downstream,
219+
ProcessingParameters(
220+
func_name="unit_transform",
221+
pattern="1-to-1",
222+
source_uuid=source_uuid,
223+
),
224+
)
225+
226+
creation_action = HistoryAction(
227+
title="Create edited Gaussian",
228+
kind=HistoryAction.KIND_UI,
229+
target="imagepanel",
230+
method_name="new_object",
231+
kwargs={"param": edited_param},
232+
)
233+
creation_action.output_uuids = [source_uuid]
234+
downstream_state = WorkspaceState()
235+
downstream_state.selection = {"image": [source_uuid]}
236+
downstream_action = HistoryAction(
237+
title="Transform edited Gaussian",
238+
kind=HistoryAction.KIND_COMPUTE,
239+
panel_str="image",
240+
func_name="unit_transform",
241+
pattern="1_to_1",
242+
state=downstream_state,
243+
)
244+
downstream_action.output_uuids = [downstream_uuid]
245+
session = HistorySession(number=1)
246+
session.add_action(creation_action)
247+
session.add_action(downstream_action)
248+
249+
processor_source_objects: list[ImageObj] = []
250+
processor_source_data: list[np.ndarray] = []
251+
252+
def recompute_1_to_1(
253+
func_name: str | None,
254+
source_obj: ImageObj,
255+
param: object,
256+
*,
257+
plugin_origin: dict[str, object] | None,
258+
) -> SimpleNamespace:
259+
assert func_name == "unit_transform"
260+
assert param is None
261+
assert plugin_origin is None
262+
processor_source_objects.append(source_obj)
263+
processor_source_data.append(source_obj.data.copy())
264+
new_obj = source_obj.copy()
265+
new_obj.title = f"unit_transform({source_obj.title})"
266+
new_obj.data = source_obj.data.astype(float) * 2.0 + 3.0
267+
return SimpleNamespace(cancelled=False, error_msg=None, result=new_obj)
268+
269+
def apply_recomputed_object_in_place(
270+
obj: ImageObj,
271+
new_obj: ImageObj,
272+
proc_params: ProcessingParameters,
273+
) -> None:
274+
hrec.update_obj_in_place(obj, new_obj)
275+
insert_processing_parameters(obj, proc_params)
276+
277+
object_model = CascadeObjectModel([source, downstream])
278+
data_panel = SimpleNamespace(
279+
PANEL_STR_ID="image",
280+
objmodel=object_model,
281+
processor=SimpleNamespace(recompute_1_to_1=recompute_1_to_1),
282+
objprop=SimpleNamespace(
283+
apply_recomputed_object_in_place=apply_recomputed_object_in_place
284+
),
285+
)
286+
runtime = SimpleNamespace(
287+
objects=SimpleNamespace(
288+
action_output_uuids={
289+
creation_action.uuid: [source_uuid],
290+
downstream_action.uuid: [downstream_uuid],
291+
}
292+
),
293+
execution=SimpleNamespace(cascade_warnings=[], broken_actions=set()),
294+
)
295+
history_panel = SimpleNamespace(
296+
runtime=runtime,
297+
history_sessions=[session],
298+
)
299+
refreshed_uuids: list[str] = []
300+
301+
def record_refresh(panel_data: object, output_uuid: str) -> None:
302+
assert panel_data is data_panel
303+
refreshed_uuids.append(output_uuid)
304+
305+
descendants = hrec.hchain.get_downstream_actions(history_panel, creation_action)
306+
assert descendants == [downstream_action]
307+
308+
with (
309+
patch.object(hrec.hchain, "resolve_panel_for_action", return_value=data_panel),
310+
patch.object(
311+
hrec, "create_image_from_param", wraps=create_image_from_param
312+
) as create_image_mock,
313+
patch.object(hrec, "refresh_target", side_effect=record_refresh),
314+
):
315+
assert hrec.recompute_creation_in_place(history_panel, creation_action)
316+
assert hrec.recompute_1_to_1_in_place(history_panel, downstream_action)
317+
318+
create_image_mock.assert_called_once()
319+
assert create_image_mock.call_args.args[0] is edited_param
320+
321+
assert id(source) == source_identity
322+
assert object_model[source_uuid] is source
323+
assert get_uuid(source) == source_uuid
324+
np.testing.assert_allclose(source.data, expected_source.data)
325+
assert not np.array_equal(source.data, initial_source_data)
326+
creation_param = extract_creation_parameters(source)
327+
assert isinstance(creation_param, Gauss2DParam)
328+
for name in ("height", "width", "x0", "y0", "sigma", "a"):
329+
assert getattr(creation_param, name) == getattr(edited_param, name)
330+
331+
assert processor_source_objects == [source]
332+
np.testing.assert_allclose(processor_source_data[0], expected_source.data)
333+
assert id(downstream) == downstream_identity
334+
assert object_model[downstream_uuid] is downstream
335+
assert get_uuid(downstream) == downstream_uuid
336+
np.testing.assert_allclose(
337+
downstream.data, expected_source.data.astype(float) * 2.0 + 3.0
338+
)
339+
assert not np.array_equal(downstream.data, initial_downstream_data)
340+
downstream_params = extract_processing_parameters(downstream)
341+
assert downstream_params is not None
342+
assert downstream_params.source_uuid == source_uuid
343+
assert refreshed_uuids == [source_uuid, downstream_uuid]
344+
assert runtime.execution.cascade_warnings == []

0 commit comments

Comments
 (0)