Skip to content

Commit 8021ce9

Browse files
vr: restart the workflow with no changes according to user prompt
* Closes #6261 * If there are no changes to reinstall AND the workflow is stopped, prompt the user to see whether they want to restart it anyway. * This makes `cylc vr` more useful as the "I want to restart my workflow" command. * But it also ensures that they are aware if no changes are present as they might have forgotten to press save or run the command on the wrong workflow or whatever.
1 parent 8bb1f44 commit 8021ce9

File tree

6 files changed

+205
-40
lines changed

6 files changed

+205
-40
lines changed

changes.d/6354.feat.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix an issue that could cause clock-expired tasks to be erroneously retried if "execution retry delays" were configured.

cylc/flow/scripts/validate_reinstall.py

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@
4646
if TYPE_CHECKING:
4747
from optparse import Values
4848

49+
from ansimarkup import parse as cparse
50+
4951
from cylc.flow import LOG
5052
from cylc.flow.exceptions import (
5153
ContactFileExists,
@@ -74,7 +76,7 @@
7476
from cylc.flow.scripts.reload import (
7577
run as cylc_reload
7678
)
77-
from cylc.flow.terminal import cli_function
79+
from cylc.flow.terminal import cli_function, is_terminal
7880
from cylc.flow.workflow_files import detect_old_contact_file
7981

8082
CYLC_ROSE_OPTIONS = COP.get_cylc_rose_options()
@@ -87,6 +89,10 @@
8789
modify={'cylc-rose': 'validate, install'}
8890
)
8991

92+
_input = input # to enable testing
93+
94+
NO_CHANGES_STR = 'No changes to reinstall'
95+
9096

9197
def get_option_parser() -> COP:
9298
parser = COP(
@@ -189,34 +195,54 @@ async def vr_cli(
189195
# Force on the against_source option:
190196
options.against_source = True
191197

192-
# Run cylc validate
198+
# Run "cylc validate"
193199
log_subcommand('validate --against-source', workflow_id)
194200
await cylc_validate(parser, options, workflow_id)
195201

196202
# Unset options that do not apply after validation:
197203
delattr(options, 'against_source')
198204
delattr(options, 'is_validate')
199205

206+
# Run "cylc reinstall"
200207
log_subcommand('reinstall', workflow_id)
201208
reinstall_ok = await cylc_reinstall(
202-
options, workflow_id,
209+
options,
210+
workflow_id,
203211
[],
204212
print_reload_tip=False
205213
)
206214
if not reinstall_ok:
207-
LOG.warning(
208-
'No changes to source: No reinstall or'
209-
f' {"reload" if workflow_running else "play"} required.'
210-
)
211-
return False
212-
213-
# Run reload if workflow is running or paused:
215+
if (
216+
not workflow_running
217+
and is_terminal()
218+
and not options.skip_interactive
219+
):
220+
# there are no changes to install but the workflow isn't running
221+
# => ask the user if they want to restart it anyway
222+
usr = None
223+
while usr not in ['y', 'n']:
224+
LOG.warning(NO_CHANGES_STR)
225+
usr = _input(
226+
cparse('<bold>Restart anyway?</bold> [y/n]: ')
227+
).lower()
228+
if usr == 'n':
229+
return False
230+
else:
231+
# the are no changes to install and the workflow is running
232+
# => there is nothing for us to do here
233+
LOG.warning(
234+
f'{NO_CHANGES_STR}: No reinstall or'
235+
f' {"reload" if workflow_running else "play"} required.'
236+
)
237+
return False
238+
239+
# Run "cylc reload" (if workflow is running or paused)
214240
if workflow_running:
215241
log_subcommand('reload', workflow_id)
216242
await cylc_reload(options, workflow_id)
217243
return True
218244

219-
# run play anyway, to play a stopped workflow:
245+
# Run "cylc play" (if workflow is stopped)
220246
else:
221247
set_timestamps(LOG, options.log_timestamp)
222248
cleanup_sysargv(

tests/integration/scripts/__init__.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# THIS FILE IS PART OF THE CYLC WORKFLOW ENGINE.
2+
# Copyright (C) NIWA & British Crown (Met Office) & Contributors.
3+
#
4+
# This program is free software: you can redistribute it and/or modify
5+
# it under the terms of the GNU General Public License as published by
6+
# the Free Software Foundation, either version 3 of the License, or
7+
# (at your option) any later version.
8+
#
9+
# This program is distributed in the hope that it will be useful,
10+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
# GNU General Public License for more details.
13+
#
14+
# You should have received a copy of the GNU General Public License
15+
# along with this program. If not, see <http://www.gnu.org/licenses/>.

tests/integration/scripts/conftest.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
#!/usr/bin/env python3
2+
3+
# THIS FILE IS PART OF THE CYLC WORKFLOW ENGINE.
4+
# Copyright (C) NIWA & British Crown (Met Office) & Contributors.
5+
#
6+
# This program is free software: you can redistribute it and/or modify
7+
# it under the terms of the GNU General Public License as published by
8+
# the Free Software Foundation, either version 3 of the License, or
9+
# (at your option) any later version.
10+
#
11+
# This program is distributed in the hope that it will be useful,
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
# GNU General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU General Public License
17+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
18+
19+
from types import SimpleNamespace
20+
from uuid import uuid1
21+
22+
import pytest
23+
24+
from cylc.flow.workflow_files import WorkflowFiles
25+
26+
from ..utils.flow_writer import flow_config_str
27+
28+
29+
@pytest.fixture
30+
def one_src(tmp_path, one_conf):
31+
src_dir = tmp_path
32+
(src_dir / 'flow.cylc').write_text(flow_config_str(one_conf))
33+
(src_dir / 'rose-suite.conf').touch()
34+
return SimpleNamespace(path=src_dir)
35+
36+
37+
@pytest.fixture
38+
def one_run(one_src, test_dir, run_dir):
39+
w_run_dir = test_dir / str(uuid1())
40+
w_run_dir.mkdir()
41+
(w_run_dir / 'flow.cylc').write_text(
42+
(one_src.path / 'flow.cylc').read_text()
43+
)
44+
(w_run_dir / 'rose-suite.conf').write_text(
45+
(one_src.path / 'rose-suite.conf').read_text()
46+
)
47+
install_dir = (w_run_dir / WorkflowFiles.Install.DIRNAME)
48+
install_dir.mkdir(parents=True)
49+
(install_dir / WorkflowFiles.Install.SOURCE).symlink_to(
50+
one_src.path,
51+
target_is_directory=True,
52+
)
53+
return SimpleNamespace(
54+
path=w_run_dir,
55+
id=str(w_run_dir.relative_to(run_dir)),
56+
)

tests/integration/test_reinstall.py renamed to tests/integration/scripts/test_reinstall.py

Lines changed: 1 addition & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@
1919
import asyncio
2020
from contextlib import asynccontextmanager
2121
from pathlib import Path
22-
from types import SimpleNamespace
23-
from uuid import uuid1
2422

2523
import pytest
2624

@@ -39,7 +37,7 @@
3937
WorkflowFiles,
4038
)
4139

42-
from .utils.entry_points import EntryPointWrapper
40+
from ..utils.entry_points import EntryPointWrapper
4341

4442
ReInstallOptions = Options(reinstall_gop())
4543

@@ -66,32 +64,6 @@ def non_interactive(monkeypatch):
6664
)
6765

6866

69-
@pytest.fixture
70-
def one_src(tmp_path):
71-
src_dir = tmp_path
72-
(src_dir / 'flow.cylc').touch()
73-
(src_dir / 'rose-suite.conf').touch()
74-
return SimpleNamespace(path=src_dir)
75-
76-
77-
@pytest.fixture
78-
def one_run(one_src, test_dir, run_dir):
79-
w_run_dir = test_dir / str(uuid1())
80-
w_run_dir.mkdir()
81-
(w_run_dir / 'flow.cylc').touch()
82-
(w_run_dir / 'rose-suite.conf').touch()
83-
install_dir = (w_run_dir / WorkflowFiles.Install.DIRNAME)
84-
install_dir.mkdir(parents=True)
85-
(install_dir / WorkflowFiles.Install.SOURCE).symlink_to(
86-
one_src.path,
87-
target_is_directory=True,
88-
)
89-
return SimpleNamespace(
90-
path=w_run_dir,
91-
id=str(w_run_dir.relative_to(run_dir)),
92-
)
93-
94-
9567
async def test_rejects_random_workflows(one, one_run):
9668
"""It should only work with workflows installed by cylc install."""
9769
with pytest.raises(WorkflowFilesError) as exc_ctx:
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
#!/usr/bin/env python3
2+
3+
# THIS FILE IS PART OF THE CYLC WORKFLOW ENGINE.
4+
# Copyright (C) NIWA & British Crown (Met Office) & Contributors.
5+
#
6+
# This program is free software: you can redistribute it and/or modify
7+
# it under the terms of the GNU General Public License as published by
8+
# the Free Software Foundation, either version 3 of the License, or
9+
# (at your option) any later version.
10+
#
11+
# This program is distributed in the hope that it will be useful,
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
# GNU General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU General Public License
17+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
18+
19+
from cylc.flow.option_parsers import Options
20+
from cylc.flow.scripts.validate_reinstall import (
21+
get_option_parser as vr_gop,
22+
vr_cli,
23+
)
24+
25+
ValidateReinstallOptions = Options(vr_gop())
26+
27+
28+
async def test_prompt_for_running_workflow_with_no_changes(
29+
monkeypatch,
30+
caplog,
31+
capsys,
32+
log_filter,
33+
one_run,
34+
capcall,
35+
):
36+
"""It should reinstall and restart the workflow with no changes.
37+
38+
See: https://github.com/cylc/cylc-flow/issues/6261
39+
40+
We hope to get users into the habbit of "cylc vip" to create a new run,
41+
and "cylc vr" to contine an old one (picking up any new changes in the
42+
process).
43+
44+
This works fine, unless there are no changes to reinstall, in which case
45+
the "cylc vr" command exits (nothing to do).
46+
47+
The "nothing to reinstall" situation can be interpretted two ways:
48+
1. Unexpected error, the user expected there to be something to reinstall,
49+
but there wasn't. E.g, they forgot to press save.
50+
2. Unexpected annoyance, I wanted to restart the workflow, just do it.
51+
52+
To handle this we explain that there are no changes to reinstall and
53+
prompt the user to see if they want to press save or restart the workflow.
54+
"""
55+
# disable the clean_sysargv logic (this interferes with other tests)
56+
cleanup_sysargv_calls = capcall(
57+
'cylc.flow.scripts.validate_reinstall.cleanup_sysargv'
58+
)
59+
60+
# answer "y" to all prompts
61+
def _input(prompt):
62+
print(prompt)
63+
return 'y'
64+
65+
monkeypatch.setattr(
66+
'cylc.flow.scripts.validate_reinstall._input',
67+
_input,
68+
)
69+
70+
# make it look like we are running this command in a terminal
71+
monkeypatch.setattr(
72+
'cylc.flow.scripts.validate_reinstall.is_terminal',
73+
lambda: True
74+
)
75+
monkeypatch.setattr(
76+
'cylc.flow.scripts.reinstall.is_terminal',
77+
lambda: True
78+
)
79+
80+
# attempt to restart it with "cylc vr"
81+
ret = await vr_cli(
82+
vr_gop(), ValidateReinstallOptions(), one_run.id
83+
)
84+
# the workflow should reinstall
85+
assert ret
86+
87+
# the user should have been warned that there were no changes to reinstall
88+
assert log_filter(caplog, contains='No changes to reinstall')
89+
90+
# they should have been presented with a prompt
91+
# (to which we have hardcoded the response "y")
92+
assert 'Restart anyway?' in capsys.readouterr()[0]
93+
94+
# the workflow should have restarted
95+
assert len(cleanup_sysargv_calls) == 1

0 commit comments

Comments
 (0)