diff --git a/changes.d/6554.feat.md b/changes.d/6554.feat.md new file mode 100644 index 0000000000..1abebb4e36 --- /dev/null +++ b/changes.d/6554.feat.md @@ -0,0 +1 @@ +`cylc show` now displays when a task has been set to skip mode diff --git a/cylc/flow/config.py b/cylc/flow/config.py index dbf9316218..08653b78ca 100644 --- a/cylc/flow/config.py +++ b/cylc/flow/config.py @@ -81,6 +81,7 @@ is_relative_to, ) from cylc.flow.task_qualifiers import ALT_QUALIFIERS +from cylc.flow.run_modes import WORKFLOW_ONLY_MODES from cylc.flow.run_modes.simulation import configure_sim_mode from cylc.flow.run_modes.skip import skip_mode_validate from cylc.flow.subprocctx import SubFuncContext @@ -2447,6 +2448,13 @@ def _get_taskdef(self, name: str) -> TaskDef: try: rtcfg = self.cfg['runtime'][name] + + # If the workflow mode is simulation or dummy always + # override the task config: + workflow_run_mode = RunMode.get(self.options) + if workflow_run_mode.value in WORKFLOW_ONLY_MODES: + rtcfg['run mode'] = workflow_run_mode.value + except KeyError: raise WorkflowConfigError("Task not defined: %s" % name) from None # We may want to put in some handling for cases of changing the diff --git a/cylc/flow/network/schema.py b/cylc/flow/network/schema.py index 67d3c58ec8..0ab26c33b1 100644 --- a/cylc/flow/network/schema.py +++ b/cylc/flow/network/schema.py @@ -72,7 +72,7 @@ ) from cylc.flow.id import Tokens from cylc.flow.run_modes import ( - TASK_CONFIG_RUN_MODES, WORKFLOW_RUN_MODES, RunMode) + WORKFLOW_RUN_MODES, RunMode) from cylc.flow.task_outputs import SORT_ORDERS from cylc.flow.task_state import ( TASK_STATUS_DESC, @@ -633,7 +633,7 @@ class Meta: # The run mode for the task. TaskRunMode = graphene.Enum( 'TaskRunMode', - [(m.capitalize(), m) for m in TASK_CONFIG_RUN_MODES], + [(k.capitalize(), k.lower()) for k in RunMode.__members__.keys()], description=lambda x: RunMode(x.value).describe() if x else None, ) diff --git a/cylc/flow/run_modes/__init__.py b/cylc/flow/run_modes/__init__.py index 2f09463138..70f0d42ea8 100644 --- a/cylc/flow/run_modes/__init__.py +++ b/cylc/flow/run_modes/__init__.py @@ -85,6 +85,14 @@ def get(options: 'Values') -> "RunMode": return RunMode(run_mode) return RunMode.LIVE + @classmethod + def _missing_(cls, value): + value = value.lower() + for member in cls: + if member.value.lower() == value: + return member + return None + def get_submit_method(self) -> 'Optional[SubmissionInterface]': """Return the job submission method for this run mode. diff --git a/cylc/flow/scripts/show.py b/cylc/flow/scripts/show.py index c387a3f3fc..bb1665a2b8 100755 --- a/cylc/flow/scripts/show.py +++ b/cylc/flow/scripts/show.py @@ -52,6 +52,7 @@ from cylc.flow.id import Tokens from cylc.flow.id_cli import parse_ids from cylc.flow.network.client_factory import get_client +from cylc.flow.run_modes import RunMode from cylc.flow.task_outputs import TaskOutputs from cylc.flow.task_state import ( TASK_STATUSES_ORDERED, @@ -145,6 +146,7 @@ } runtime { completion + runMode } } } @@ -346,9 +348,13 @@ async def prereqs_and_outputs_query( attrs.append("queued") if t_proxy['isRunahead']: attrs.append("runahead") + run_mode = t_proxy['runtime']['runMode'] + if run_mode and RunMode(run_mode) != RunMode.LIVE: + attrs.append(f"run mode={run_mode}") state_msg = state if attrs: state_msg += f" ({','.join(attrs)})" + ansiprint(f'state: {state_msg}') # flow numbers, if not just 1 diff --git a/tests/flakyfunctional/cylc-show/00-simple.t b/tests/flakyfunctional/cylc-show/00-simple.t index d0a700dd86..471515de6b 100644 --- a/tests/flakyfunctional/cylc-show/00-simple.t +++ b/tests/flakyfunctional/cylc-show/00-simple.t @@ -113,7 +113,10 @@ cmp_json "${TEST_NAME}-taskinstance" "${TEST_NAME}-taskinstance" \ } } }, - "runtime": {"completion": "(started and succeeded)"}, + "runtime": { + "completion": "(started and succeeded)", + "runMode": "Live" + }, "prerequisites": [ { "expression": "0", diff --git a/tests/integration/scripts/test_show.py b/tests/integration/scripts/test_show.py index ec000a75a1..7c9befbacc 100644 --- a/tests/integration/scripts/test_show.py +++ b/tests/integration/scripts/test_show.py @@ -16,6 +16,7 @@ import json import pytest +import re from types import SimpleNamespace from colorama import init as colour_init @@ -26,6 +27,9 @@ ) +RE_STATE = re.compile('state:.*') + + @pytest.fixture(scope='module') def mod_my_conf(): """A workflow configuration with some workflow metadata.""" @@ -59,8 +63,8 @@ def mod_my_conf(): 'destroyedtheworldyet.com/' ), 'question': 'mutually exclusive', - } - } + }, + }, }, } @@ -128,6 +132,7 @@ async def test_task_meta_query(mod_my_schd, capsys): ) assert ret == 0 out, err = capsys.readouterr() + assert out.splitlines() == [ 'title: Task Title', 'question: mutually exclusive', @@ -170,9 +175,9 @@ async def test_task_instance_query( 'scheduling': { 'graph': {'R1': 'zed & dog & cat & ant'}, }, - } + }, ), - paused_start=False + paused_start=False, ) async with start(schd): await schd.update_data_structure() @@ -195,20 +200,32 @@ async def test_task_instance_query( ] +@pytest.mark.parametrize( + 'workflow_run_mode, run_mode_info', + ( + ('live', 'Skip'), + ('dummy', 'Dummy'), + ('simulation', 'Simulation'), + ) +) @pytest.mark.parametrize( 'attributes_bool, flow_nums, expected_state, expected_flows', [ pytest.param( - False, [1], 'state: waiting', None, + False, [1], 'state: waiting (run mode={})', None, ), pytest.param( - True, [1, 2], 'state: waiting (held,queued,runahead)', 'flows: [1,2]', + True, + [1, 2], + 'state: waiting (held,queued,runahead,run mode={})', + 'flows: [1,2]', ) ] ) async def test_task_instance_state_flows( flow, scheduler, start, capsys, - attributes_bool, flow_nums, expected_state, expected_flows + workflow_run_mode, run_mode_info, + attributes_bool, flow_nums, expected_state, expected_flows ): """It should print task instance state, attributes, and flows.""" @@ -225,9 +242,13 @@ async def test_task_instance_state_flows( 'scheduling': { 'graph': {'R1': 'a'}, }, - } + 'runtime': { + 'a': {'run mode': 'skip'} + } + }, ), - paused_start=True + paused_start=True, + run_mode=workflow_run_mode, ) async with start(schd): @@ -257,7 +278,7 @@ async def test_task_instance_state_flows( line for line in out.splitlines() if line.startswith("state:") ] == [ - expected_state, + expected_state.format(run_mode_info), ] if expected_flows is not None: assert [ @@ -266,3 +287,47 @@ async def test_task_instance_state_flows( ] == [ expected_flows, ] + + +async def test_task_run_mode_changes(flow, scheduler, start, capsys): + """Broadcasting a change of run mode changes run mode shown by cylc show. + """ + opts = SimpleNamespace( + comms_timeout=5, + json=False, + task_defs=None, + list_prereqs=False, + ) + schd = scheduler( + flow({'scheduling': {'graph': {'R1': 'a'}}}), + run_mode='live' + ) + + async with start(schd): + # Control: No mode set, the Run Mode setting is not shown: + await schd.update_data_structure() + ret = await show( + schd.workflow, + [Tokens('//1/a')], + opts, + ) + assert ret == 0 + out, _ = capsys.readouterr() + state, = RE_STATE.findall(out) + assert 'waiting' in state + + # Broadcast change task to skip mode: + schd.broadcast_mgr.put_broadcast(['1'], ['a'], [{'run mode': 'skip'}]) + await schd.update_data_structure() + + # show now shows skip mode: + ret = await show( + schd.workflow, + [Tokens('//1/a')], + opts, + ) + assert ret == 0 + + out, _ = capsys.readouterr() + state, = RE_STATE.findall(out) + assert 'run mode=Skip' in state diff --git a/tests/integration/tui/screenshots/test_show.success.html b/tests/integration/tui/screenshots/test_show.success.html index c0aaf20a9e..40b4f77924 100644 --- a/tests/integration/tui/screenshots/test_show.success.html +++ b/tests/integration/tui/screenshots/test_show.success.html @@ -13,7 +13,7 @@ description: The first metasyntactic variable. URL: (not given) - state: waiting (queued) + state: waiting (queued,run mode=Simulation) prerequisites: (None) outputs: ('⨯': not completed) ⨯ 1/foo expired diff --git a/tests/unit/run_modes/test_run_modes.py b/tests/unit/run_modes/test_run_modes.py index 57d245016d..6cdd3b0167 100644 --- a/tests/unit/run_modes/test_run_modes.py +++ b/tests/unit/run_modes/test_run_modes.py @@ -16,6 +16,8 @@ """Tests for utilities supporting run modes. """ +import pytest + from cylc.flow.run_modes import RunMode @@ -28,3 +30,9 @@ def test_run_mode_desc(): def test_get_default_live(): """RunMode.get() => live""" assert RunMode.get({}) == RunMode.LIVE + + +@pytest.mark.parametrize('str_', ('LIVE', 'Dummy', 'SkIp', 'siMuLATioN')) +def test__missing_(str_): + """The RunMode enumeration works when fed a string in the wrong case""" + assert RunMode(str_).value == str_.lower()