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
10 changes: 5 additions & 5 deletions benchmark/cellprofiler_benchmark_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,10 @@ def run(self, args: argparse.Namespace) -> int:
MeasuredRunEnded,
SessionMeasuredRun,
)
from openhcs.agent.dto.session import DatasetRunRequest
from openhcs.agent.dto.session import (
DatasetRunRequest,
RuntimeObservationExportScopeName,
)
from openhcs.agent.path_policy import AgentPathPolicy
from openhcs.agent.session_access import AgentPathDatasetAccess
from openhcs.authoring.session.session import CallerThread, Session
Expand All @@ -241,9 +244,6 @@ def run(self, args: argparse.Namespace) -> int:
mirror_input_workspace,
)
from openhcs.runtime.zmq_config import OpenHCSZMQConfig
from openhcs.runtime.zmq_execution_signature import (
ZMQRuntimeObservationExportScope,
)
from python_introspect import to_jsonable

output_dir = args.output_dir.expanduser().resolve()
Expand Down Expand Up @@ -281,7 +281,7 @@ def run(self, args: argparse.Namespace) -> int:
runtime_observation_export_path=str(
MeasuredPipelineRunArtifact.RUNTIME_OBSERVATION.path_in(output_dir)
),
runtime_observation_export_scope=ZMQRuntimeObservationExportScope.named(
runtime_observation_export_scope=RuntimeObservationExportScopeName(
args.observation_scope
),
submit_timeout_ms=args.submit_timeout_ms,
Expand Down
2 changes: 1 addition & 1 deletion openhcs/authoring/session/operations/datasets.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ def run(cls, session, request) -> SessionOperationResult:
tuple(request.scope_ids),
auxiliary_params=session.observation_export_params(
request.runtime_observation_export_path,
request.runtime_observation_export_scope,
request.runtime_observation_export_scope.scope,
),
submit_timeout_ms=request.submit_timeout_ms,
wait_timeout_ms=request.wait_timeout_ms,
Expand Down
21 changes: 15 additions & 6 deletions scripts/smoke_installed_gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@
from zmqruntime import OperationDeadline

from openhcs.agent.dto.execution import RuntimeServerInfo
from openhcs.agent.dto.ui_bridge import UiWindowCatalog, UiWindowSnapshotResult
from openhcs.agent.dto.ui_bridge import (
UiActionInvokeResult,
UiWindowCatalog,
UiWindowSnapshotResult,
)
from openhcs.agent.services.endpoint_function_catalog_service import (
ZMQFunctionCatalogService,
)
Expand Down Expand Up @@ -454,6 +458,15 @@ def _typed_tool_payload(
return payload


def require_ui_action_accepted(action_result: UiActionInvokeResult) -> None:
"""Fail the smoke unless the live GUI took the requested action."""

from openhcs.agent.dto.ui_bridge import ActionAccepted

if action_result.status is not ActionAccepted:
raise AssertionError(f"Installed UI action was not accepted: {action_result}")


def _window_ids(catalog: UiWindowCatalog) -> frozenset[str]:
return frozenset(window.window_id for window in catalog.windows)

Expand Down Expand Up @@ -554,7 +567,6 @@ def run_installed_live_mcp_smoke(
)
from openhcs.agent.dto.mcp import McpServerHealthResult
from openhcs.agent.dto.ui_bridge import (
UiActionInvocationStatus,
UiActionInvokeResult,
UiBridgeCatalog,
UiBridgeStatus,
Expand Down Expand Up @@ -691,10 +703,7 @@ def run_installed_live_mcp_smoke(
capability=agent_capabilities.ui_invoke_action,
payload_type=UiActionInvokeResult,
)
if action_result.status != UiActionInvocationStatus.ACCEPTED.value:
raise AssertionError(
f"Installed UI action was not accepted: {action_result}"
)
require_ui_action_accepted(action_result)

timeout_text = f"{deadline.remaining_seconds():g}"
windows_execution = client.execute(
Expand Down
48 changes: 47 additions & 1 deletion tests/unit/test_benchmark_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,10 @@
RuntimeEnvironmentSnapshot,
)
from openhcs.runtime.zmq_execution_client import OpenHCSExecutionSubmission, ZMQExecutionRequestBuilder
from openhcs.runtime.zmq_execution_signature import ZMQAuxiliaryExecutionParams
from openhcs.runtime.zmq_execution_signature import (
ZMQAuxiliaryExecutionParams,
ZMQRuntimeObservationExportScope,
)


def test_benchmark_command_catalog_is_derived_from_registered_commands() -> None:
Expand Down Expand Up @@ -1308,3 +1311,46 @@ def _run_receipt(
completed_observation_count=completed_observation_count,
updated_at_epoch_seconds=updated_at,
)


@pytest.mark.parametrize(
"scope_name", tuple(ZMQRuntimeObservationExportScope.__registry__)
)
def test_measured_cli_builds_its_run_request_for_every_observation_scope(
scope_name: str,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
class RequestWasBuilt(Exception):
"""Raised by the first step after the run request, to stop before execution."""

def stop_after_request(*_args, **_kwargs):
raise RequestWasBuilt

monkeypatch.setattr(
"openhcs.core.input_workspace.mirror_input_workspace", stop_after_request
)
plate = tmp_path / "plate"
plate.mkdir()
source_file = tmp_path / "pipeline.py"
source_file.write_text("pipeline_steps = []\n", encoding="utf-8")
args = create_benchmark_argument_parser().parse_args(
[
"run-measured",
"--plate",
str(plate),
"--pipeline-source-file",
str(source_file),
"--output-dir",
str(tmp_path / "evidence"),
"--run-id",
"test",
"--observation-scope",
scope_name,
"--wait-timeout-ms",
"1000",
]
)

with pytest.raises(RequestWasBuilt):
args.cli_command.run(args)
89 changes: 89 additions & 0 deletions tests/unit/test_run_request_export_scope.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
"""The run request names an export scope; the session receives the scope class."""

from __future__ import annotations

from pathlib import Path
from types import SimpleNamespace

import pytest

from openhcs.agent.dto.session import (
DatasetRunRequest,
RuntimeObservationExportScopeName,
)
from openhcs.authoring.session.operations.datasets import RunDatasets
from openhcs.authoring.session.session import Session
from openhcs.runtime.zmq_execution_signature import ZMQRuntimeObservationExportScope

SCOPE_NAMES = tuple(ZMQRuntimeObservationExportScope.__registry__)


class _WritableAnywhere:
@staticmethod
def require_writable(path: Path) -> Path:
return path


class _RecordingSession:
"""Only what ``RunDatasets.run`` touches; the export options are the real ones."""

observation_export_params = Session.observation_export_params
dataset_access = _WritableAnywhere()
event_log = SimpleNamespace(last_sequence=0)
run_datasets = object()

def start(self, work, scope_ids, **options) -> None:
self.started = (work, scope_ids, options)


def test_every_registered_scope_has_a_request_name() -> None:
assert {member.value for member in RuntimeObservationExportScopeName} == set(
SCOPE_NAMES
)


@pytest.mark.parametrize("scope_name", SCOPE_NAMES)
def test_run_datasets_gives_the_session_the_scope_class(
scope_name: str, tmp_path: Path
) -> None:
session = _RecordingSession()
export_path = tmp_path / "observation.pkl"

result = RunDatasets.run(
session,
DatasetRunRequest(
scope_ids=("dataset",),
runtime_observation_export_path=str(export_path),
runtime_observation_export_scope=RuntimeObservationExportScopeName(
scope_name
),
),
)

work, scope_ids, options = session.started
assert result.status == "accepted"
assert work is session.run_datasets
assert scope_ids == ("dataset",)
export = options["auxiliary_params"]
assert export.runtime_observation_export_path == export_path
assert export.runtime_observation_export_scope is (
ZMQRuntimeObservationExportScope.named(scope_name)
)


def test_run_datasets_without_an_export_path_runs_with_the_default_scope() -> None:
session = _RecordingSession()

RunDatasets.run(session, DatasetRunRequest(scope_ids=("dataset",)))

assert session.started[2]["auxiliary_params"] is None


def test_run_request_refuses_the_scope_class_itself() -> None:
with pytest.raises(TypeError, match="runtime_observation_export_scope"):
DatasetRunRequest(
scope_ids=(),
runtime_observation_export_scope=ZMQRuntimeObservationExportScope.named(
SCOPE_NAMES[0]
),
)
23 changes: 22 additions & 1 deletion tests/unit/test_smoke_installed_gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import json
from concurrent.futures import Future
from pathlib import Path
from types import SimpleNamespace

import pytest
from pyqt_reactive.services.window_snapshot import WindowSnapshotCaptureScope
Expand All @@ -18,7 +19,11 @@
from openhcs.agent.dto.common import AgentResourceRef
from openhcs.agent.dto.execution import ExecutionConnectionSpec, RuntimeServerInfo
from openhcs.agent.dto.functions import FunctionCatalogEntry, FunctionCatalogPage
from openhcs.agent.dto.ui_bridge import UiWindowSnapshotResult
from openhcs.agent.dto.ui_bridge import (
ActionAccepted,
UiActionInvocationStatus,
UiWindowSnapshotResult,
)
from openhcs.pyqt_gui.config import AgentUiBridgeConfig, UIConfig
from openhcs.runtime.zmq_config import OpenHCSZMQConfig
from scripts.smoke_installed_gui import (
Expand All @@ -30,6 +35,7 @@
InstalledGuiSnapshotEvidenceAuthority,
_StartupObservation,
assert_not_source_checkout_import,
require_ui_action_accepted,
verify_installed_function_catalog,
with_isolated_runtime_topology,
)
Expand Down Expand Up @@ -396,3 +402,18 @@ def test_window_snapshot_cli_projects_declared_output_directory(tmp_path: Path)

assert call.name == "openhcs_ui_snapshot_window"
assert call.arguments["output_dir_path"] == str(tmp_path)


@pytest.mark.parametrize(
"status", tuple(UiActionInvocationStatus.__registry__.values())
)
def test_gui_smoke_accepts_only_an_accepted_ui_action(
status: type[UiActionInvocationStatus],
) -> None:
action_result = SimpleNamespace(status=status)

if status is ActionAccepted:
require_ui_action_accepted(action_result)
else:
with pytest.raises(AssertionError, match="not accepted"):
require_ui_action_accepted(action_result)
Loading