Skip to content

Commit 6d1908a

Browse files
committed
Propagate and show exec_hosts in run_dialog if present
1 parent 8fef8f4 commit 6d1908a

File tree

13 files changed

+170
-6
lines changed

13 files changed

+170
-6
lines changed

src/_ert/events.py

+1
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ class RealizationBaseEvent(BaseEvent):
110110
real: str
111111
ensemble: Union[str, None] = None
112112
queue_event_type: Union[str, None] = None
113+
exec_hosts: Union[str, None] = None
113114

114115

115116
class RealizationPending(RealizationBaseEvent):

src/ert/ensemble_evaluator/snapshot.py

+6
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,7 @@ def update_realization(
252252
status: str,
253253
start_time: Optional[datetime] = None,
254254
end_time: Optional[datetime] = None,
255+
exec_hosts: Optional[str] = None,
255256
callback_status_message: Optional[str] = None,
256257
) -> "EnsembleSnapshot":
257258
self._realization_snapshots[real_id].update(
@@ -260,6 +261,7 @@ def update_realization(
260261
status=status,
261262
start_time=start_time,
262263
end_time=end_time,
264+
exec_hosts=exec_hosts,
263265
callback_status_message=callback_status_message,
264266
)
265267
)
@@ -279,6 +281,7 @@ def update_from_event(
279281
status = _FM_TYPE_EVENT_TO_STATUS[type(event)]
280282
start_time = None
281283
end_time = None
284+
exec_hosts = event.exec_hosts
282285
callback_status_message = None
283286

284287
if e_type is RealizationRunning:
@@ -296,6 +299,7 @@ def update_from_event(
296299
status,
297300
start_time,
298301
end_time,
302+
exec_hosts,
299303
callback_status_message,
300304
)
301305

@@ -397,6 +401,7 @@ class RealizationSnapshot(TypedDict, total=False):
397401
active: Optional[bool]
398402
start_time: Optional[datetime]
399403
end_time: Optional[datetime]
404+
exec_hosts: Optional[str]
400405
fm_steps: Dict[str, FMStepSnapshot]
401406
callback_status_message: Optional[str]
402407

@@ -409,6 +414,7 @@ def _realization_dict_to_realization_snapshot(
409414
active=source.get("active"),
410415
start_time=source.get("start_time"),
411416
end_time=source.get("end_time"),
417+
exec_hosts=source.get("exec_hosts"),
412418
callback_status_message=source.get("callback_status_message"),
413419
fm_steps=source.get("fm_steps", {}),
414420
)

src/ert/gui/model/node.py

+1
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ class RealNodeData:
7474
real_status_color: Optional[QColor] = None
7575
current_memory_usage: Optional[int] = None
7676
max_memory_usage: Optional[int] = None
77+
exec_hosts: Optional[str] = None
7778
stderr: Optional[str] = None
7879
callback_status_message: Optional[str] = None
7980

src/ert/gui/model/snapshot.py

+3
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,8 @@ def _update_snapshot(self, snapshot: EnsembleSnapshot, iter_: str) -> None:
168168
data = real_node.data
169169
if real_status := real.get("status"):
170170
data.status = real_status
171+
if real_exec_hosts := real.get("exec_hosts"):
172+
data.exec_hosts = real_exec_hosts
171173
for real_fm_step_id, color in (
172174
metadata["aggr_fm_step_status_colors"].get(real_id, {}).items()
173175
):
@@ -240,6 +242,7 @@ def _add_snapshot(self, snapshot: EnsembleSnapshot, iter_: str) -> None:
240242
data=RealNodeData(
241243
status=real.get("status"),
242244
active=real.get("active"),
245+
exec_hosts=real.get("exec_hosts"),
243246
fm_step_status_color_by_id=metadata.get(
244247
"aggr_fm_step_status_colors", defaultdict(None)
245248
)[real_id],

src/ert/gui/simulation/run_dialog.py

+13-1
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ def __init__(
219219
self._snapshot_model.rowsInserted.connect(self.on_snapshot_new_iteration)
220220

221221
self._fm_step_label = QLabel(self)
222+
self._fm_step_label.setObjectName("fm_step_label")
222223
self._fm_step_overview = FMStepOverview(self._snapshot_model, self)
223224

224225
self.running_time = QLabel("")
@@ -335,10 +336,21 @@ def on_snapshot_new_iteration(
335336
def _select_real(self, index: QModelIndex) -> None:
336337
real = index.row()
337338
iter_ = index.model().get_iter() # type: ignore
339+
exec_hosts = None
340+
341+
iter_node = self._snapshot_model.root.children.get(str(iter_), None)
342+
if iter_node:
343+
real_node = iter_node.children.get(str(real), None)
344+
if real_node:
345+
exec_hosts = real_node.data.exec_hosts
346+
338347
self._fm_step_overview.set_realization(iter_, real)
339-
self._fm_step_label.setText(
348+
text = (
340349
f"Realization id {index.data(RealIens)} in iteration {index.data(IterNum)}"
341350
)
351+
if exec_hosts and exec_hosts != "-":
352+
text += f", assigned to host: {exec_hosts}"
353+
self._fm_step_label.setText(text)
342354

343355
def closeEvent(self, a0: Optional[QCloseEvent]) -> None:
344356
if not self._notifier.is_simulation_running:

src/ert/scheduler/event.py

+2
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@
77
@dataclass
88
class StartedEvent:
99
iens: int
10+
exec_hosts: str = "-"
1011

1112

1213
@dataclass
1314
class FinishedEvent:
1415
iens: int
1516
returncode: int
17+
exec_hosts: str = "-"
1618

1719

1820
Event = Union[StartedEvent, FinishedEvent]

src/ert/scheduler/job.py

+2
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ def __init__(self, scheduler: Scheduler, real: Realization) -> None:
6262
self.real = real
6363
self.state = JobState.WAITING
6464
self.started = asyncio.Event()
65+
self.exec_hosts: str = "-"
6566
self.returncode: asyncio.Future[int] = asyncio.Future()
6667
self._aborted = False
6768
self._scheduler: Scheduler = scheduler
@@ -263,6 +264,7 @@ async def _send(self, state: JobState) -> None:
263264
"event_type": _queue_jobstate_event_type[state],
264265
"queue_event_type": state,
265266
"real": str(self.iens),
267+
"exec_hosts": self.exec_hosts,
266268
}
267269
self.state = state
268270
if state == JobState.FAILED:

src/ert/scheduler/lsf_driver.py

+9-3
Original file line numberDiff line numberDiff line change
@@ -500,16 +500,22 @@ async def _process_job_update(self, job_id: str, new_state: AnyJob) -> None:
500500
event: Optional[Event] = None
501501
if isinstance(new_state, RunningJob):
502502
logger.debug(f"Realization {iens} is running")
503-
event = StartedEvent(iens=iens)
503+
event = StartedEvent(iens=iens, exec_hosts=self._jobs[job_id].exec_hosts)
504504
elif isinstance(new_state, FinishedJobFailure):
505505
logger.info(f"Realization {iens} (LSF-id: {self._iens2jobid[iens]}) failed")
506506
exit_code = await self._get_exit_code(job_id)
507-
event = FinishedEvent(iens=iens, returncode=exit_code)
507+
event = FinishedEvent(
508+
iens=iens,
509+
returncode=exit_code,
510+
exec_hosts=self._jobs[job_id].exec_hosts,
511+
)
508512
elif isinstance(new_state, FinishedJobSuccess):
509513
logger.info(
510514
f"Realization {iens} (LSF-id: {self._iens2jobid[iens]}) succeeded"
511515
)
512-
event = FinishedEvent(iens=iens, returncode=0)
516+
event = FinishedEvent(
517+
iens=iens, returncode=0, exec_hosts=self._jobs[job_id].exec_hosts
518+
)
513519

514520
if event:
515521
if isinstance(event, FinishedEvent):

src/ert/scheduler/scheduler.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
from ert.constant_filenames import CERT_FILE
2929

3030
from .driver import Driver
31-
from .event import FinishedEvent
31+
from .event import FinishedEvent, StartedEvent
3232
from .job import Job, JobState
3333

3434
if TYPE_CHECKING:
@@ -308,6 +308,9 @@ async def _process_event_queue(self) -> None:
308308
# Any event implies the job has at least started
309309
job.started.set()
310310

311+
if isinstance(event, (StartedEvent, FinishedEvent)) and event.exec_hosts:
312+
self._jobs[event.iens].exec_hosts = event.exec_hosts
313+
311314
if (
312315
isinstance(event, FinishedEvent)
313316
and not self._cancelled

tests/ert/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ def build(
3434
self,
3535
real_ids: Sequence[str],
3636
status: Optional[str],
37+
exec_hosts: Optional[str] = None,
3738
start_time: Optional[datetime] = None,
3839
end_time: Optional[datetime] = None,
3940
) -> EnsembleSnapshot:
@@ -49,6 +50,7 @@ def build(
4950
fm_steps=deepcopy(self.fm_steps),
5051
start_time=start_time,
5152
end_time=end_time,
53+
exec_hosts=exec_hosts,
5254
status=status,
5355
),
5456
)

tests/ert/unit_tests/gui/conftest.py

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ def full_snapshot() -> EnsembleSnapshot:
2424
real = RealizationSnapshot(
2525
status=REALIZATION_STATE_RUNNING,
2626
active=True,
27+
exec_hosts="COMP-01",
2728
fm_steps={
2829
"0": FMStepSnapshot(
2930
start_time=dt.now(),

tests/ert/unit_tests/gui/model/test_snapshot.py

+26
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,29 @@ def test_snapshot_model_data_intact_on_full_update(full_snapshot, fail_snapshot)
6262
first_real = model.index(0, 0, model.index(0, 0))
6363

6464
assert first_real.internalPointer().children["0"].data["status"] == "Finished"
65+
66+
67+
@pytest.mark.parametrize(
68+
"do_update, expected_value",
69+
[
70+
pytest.param(
71+
True,
72+
"COMP-01",
73+
id="Host assigned",
74+
),
75+
pytest.param(
76+
False,
77+
None,
78+
id="No host assigned",
79+
),
80+
],
81+
)
82+
def test_snapshot_model_exec_hosts_propagated(full_snapshot, do_update, expected_value):
83+
model = SnapshotModel()
84+
model._add_snapshot(SnapshotModel.prerender(full_snapshot), "0")
85+
86+
if do_update:
87+
model._update_snapshot(SnapshotModel.prerender(full_snapshot), "0")
88+
89+
first_real = model.index(0, 0, model.index(0, 0))
90+
assert first_real.internalPointer().data.exec_hosts == expected_value

tests/ert/unit_tests/gui/simulation/test_run_dialog.py

+100-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,14 @@
66
from pytestqt.qtbot import QtBot
77
from qtpy import QtWidgets
88
from qtpy.QtCore import Qt, QTimer
9-
from qtpy.QtWidgets import QApplication, QComboBox, QPushButton, QToolButton, QWidget
9+
from qtpy.QtWidgets import (
10+
QApplication,
11+
QComboBox,
12+
QLabel,
13+
QPushButton,
14+
QToolButton,
15+
QWidget,
16+
)
1017

1118
import ert
1219
from ert.config import ErtConfig
@@ -459,6 +466,98 @@ def test_run_dialog_memory_usage_showing(
459466
assert max_memory_value == "60.00 KB"
460467

461468

469+
@pytest.mark.parametrize(
470+
"events, tab_widget_count, expected_host_info",
471+
[
472+
pytest.param(
473+
[
474+
FullSnapshotEvent(
475+
snapshot=(
476+
SnapshotBuilder()
477+
.add_fm_step(
478+
fm_step_id="0",
479+
index="0",
480+
name="fm_step_0",
481+
status=state.FORWARD_MODEL_STATE_START,
482+
)
483+
.build(
484+
["0"],
485+
status=state.REALIZATION_STATE_UNKNOWN,
486+
exec_hosts="COMP_01",
487+
)
488+
),
489+
iteration_label="Foo",
490+
current_iteration=0,
491+
total_iterations=1,
492+
progress=0.25,
493+
realization_count=4,
494+
status_count={"Finished": 1, "Pending": 1, "Unknown": 2},
495+
iteration=0,
496+
),
497+
EndEvent(failed=False, msg=""),
498+
],
499+
1,
500+
", assigned to host: COMP_01",
501+
id="Simulation where exec_host present",
502+
),
503+
pytest.param(
504+
[
505+
FullSnapshotEvent(
506+
snapshot=(
507+
SnapshotBuilder()
508+
.add_fm_step(
509+
fm_step_id="0",
510+
index="0",
511+
name="fm_step_0",
512+
status=state.FORWARD_MODEL_STATE_START,
513+
)
514+
.build(["0"], status=state.REALIZATION_STATE_UNKNOWN)
515+
),
516+
iteration_label="Foo",
517+
current_iteration=0,
518+
total_iterations=1,
519+
progress=0.25,
520+
realization_count=4,
521+
status_count={"Finished": 1, "Pending": 1, "Unknown": 2},
522+
iteration=0,
523+
),
524+
EndEvent(failed=False, msg=""),
525+
],
526+
1,
527+
"",
528+
id="Simulation where exec_host not present",
529+
),
530+
],
531+
)
532+
def test_run_dialog_fm_label_show_correct_info(
533+
events, tab_widget_count, expected_host_info, qtbot: QtBot, event_queue, run_dialog
534+
):
535+
run_dialog.run_experiment()
536+
for event in events:
537+
event_queue.put(event)
538+
539+
qtbot.waitUntil(
540+
lambda: run_dialog._tab_widget.count() == tab_widget_count, timeout=5000
541+
)
542+
qtbot.waitUntil(lambda: not run_dialog.done_button.isHidden(), timeout=5000)
543+
544+
# This is the container of realization boxes
545+
realization_box = run_dialog._tab_widget.widget(0)
546+
assert type(realization_box) == RealizationWidget
547+
# Click the first realization box
548+
qtbot.mouseClick(realization_box, Qt.LeftButton)
549+
fm_step_model = run_dialog._fm_step_overview.model()
550+
assert fm_step_model._real == 0
551+
552+
fm_step_label = run_dialog.findChild(QLabel, name="fm_step_label")
553+
assert not fm_step_label.text()
554+
555+
realization_box._item_clicked(run_dialog._fm_step_overview.model().index(0, 0))
556+
assert (
557+
fm_step_label.text() == f"Realization id 0 in iteration 0{expected_host_info}"
558+
)
559+
560+
462561
@pytest.mark.integration_test
463562
@pytest.mark.usefixtures("use_tmpdir")
464563
def test_that_exception_in_base_run_model_is_handled(qtbot: QtBot, storage):

0 commit comments

Comments
 (0)