Skip to content

Commit 5518b48

Browse files
committed
Validate cylc clean --timeout option more thoroughly
1 parent 6f34d15 commit 5518b48

File tree

5 files changed

+86
-33
lines changed

5 files changed

+86
-33
lines changed

cylc/flow/clean.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,6 @@
4444
Union,
4545
)
4646

47-
from metomi.isodatetime.exceptions import ISO8601SyntaxError
48-
from metomi.isodatetime.parsers import DurationParser
49-
5047
from cylc.flow import LOG
5148
from cylc.flow.cfgspec.glbl_cfg import glbl_cfg
5249
from cylc.flow.exceptions import (
@@ -363,9 +360,6 @@ def remote_clean(
363360
f"Cannot clean {id_} on remote platforms as the workflow database "
364361
f"is out of date/inconsistent with the global config - {exc}")
365362

366-
with suppress(ISO8601SyntaxError):
367-
timeout = str(DurationParser().parse(timeout).get_seconds())
368-
369363
queue: Deque[RemoteCleanQueueTuple] = deque()
370364
remote_clean_cmd = partial(
371365
_remote_clean_cmd, id_=id_, rm_dirs=rm_dirs, timeout=timeout

cylc/flow/scripts/clean.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@
6464
import sys
6565
from typing import TYPE_CHECKING, Iterable, List, Tuple
6666

67+
from metomi.isodatetime.exceptions import ISO8601SyntaxError
68+
from metomi.isodatetime.parsers import DurationParser
69+
6770
from cylc.flow import LOG
6871
from cylc.flow.clean import init_clean, get_contained_workflows
6972
from cylc.flow.exceptions import CylcError, InputError
@@ -140,6 +143,24 @@ def get_option_parser():
140143
CleanOptions = Options(get_option_parser())
141144

142145

146+
def parse_timeout(opts: 'Values') -> None:
147+
"""Parse timeout as ISO 8601 duration or number of seconds."""
148+
if opts.remote_timeout:
149+
try:
150+
timeout = int(
151+
DurationParser().parse(opts.remote_timeout).get_seconds()
152+
)
153+
except ISO8601SyntaxError:
154+
try:
155+
timeout = int(opts.remote_timeout)
156+
except ValueError:
157+
raise InputError(
158+
f"Invalid timeout: {opts.remote_timeout}. Must be "
159+
"an ISO 8601 duration or number of seconds."
160+
)
161+
opts.remote_timeout = str(timeout)
162+
163+
143164
def prompt(workflows: Iterable[str]) -> None:
144165
"""Ask user if they want to clean the given set of workflows."""
145166
print("Would clean the following workflows:")
@@ -220,7 +241,15 @@ async def run(*ids: str, opts: 'Values') -> None:
220241

221242

222243
@cli_function(get_option_parser)
223-
def main(_, opts: 'Values', *ids: str):
244+
def main(_parser, opts: 'Values', *ids: str):
245+
_main(opts, *ids)
246+
247+
248+
def _main(opts: 'Values', *ids: str):
249+
"""Run the clean command.
250+
251+
This is a separate function for ease of testing.
252+
"""
224253
if cylc.flow.flags.verbosity < 2:
225254
set_timestamps(LOG, False)
226255

@@ -229,4 +258,6 @@ def main(_, opts: 'Values', *ids: str):
229258
"--local and --remote options are mutually exclusive"
230259
)
231260

261+
parse_timeout(opts)
262+
232263
asyncio.run(run(*ids, opts=opts))

tests/functional/cylc-clean/01-remote.t

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ SSH_CMD="$(cylc config -d -i "[platforms][${CYLC_TEST_PLATFORM}]ssh command") ${
2525
if ! $SSH_CMD command -v 'tree' > '/dev/null'; then
2626
skip_all "'tree' command not available on remote host ${CYLC_TEST_HOST}"
2727
fi
28-
set_test_number 12
28+
set_test_number 10
2929

3030
# Generate random name for symlink dirs to avoid any clashes with other tests
3131
SYM_NAME="$(mktemp -u)"
@@ -108,14 +108,9 @@ __TREE__
108108

109109
# -----------------------------------------------------------------------------
110110

111-
TEST_NAME="cylc-clean-timeout"
112-
run_fail "$TEST_NAME" cylc clean --timeout 'PT0,1S' "$WORKFLOW_NAME"
113-
dump_std "$TEST_NAME"
114-
grep_ok 'cylc clean timed out after 0.1s' "${TEST_NAME}.stderr"
115-
116-
117111
TEST_NAME="cylc-clean-ok"
118-
run_ok "$TEST_NAME" cylc clean "$WORKFLOW_NAME"
112+
run_ok "$TEST_NAME" cylc clean "$WORKFLOW_NAME" --timeout PT2M
113+
# (timeout opt is covered by unit tests but no harm double-checking here)
119114
dump_std "$TEST_NAME"
120115

121116
TEST_NAME="run-dir-not-exist-post-clean.local"

tests/unit/scripts/test_clean.py

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,14 @@
1616
# You should have received a copy of the GNU General Public License
1717
# along with this program. If not, see <http://www.gnu.org/licenses/>.
1818

19-
from typing import Callable, List
19+
from typing import Callable, List, Type, Union
2020

2121
import pytest
2222

23-
from cylc.flow.scripts.clean import CleanOptions, scan, run
23+
from cylc.flow.exceptions import InputError
24+
from cylc.flow.scripts.clean import (
25+
CleanOptions, _main, parse_timeout, scan, run
26+
)
2427

2528

2629
async def test_scan(tmp_run_dir):
@@ -88,3 +91,40 @@ async def test_multi(tmp_run_dir: Callable, mute: List[str]):
8891
mute.clear()
8992
await run('*', opts=opts)
9093
assert mute == ['bar/pub/beer', 'baz/run1', 'foo']
94+
95+
96+
@pytest.mark.parametrize(
97+
'timeout, expected',
98+
[('100', '100'),
99+
('PT1M2S', '62'),
100+
('', ''),
101+
('oopsie', InputError),
102+
(' ', InputError)]
103+
)
104+
def test_parse_timeout(
105+
timeout: str,
106+
expected: Union[str, Type[InputError]]
107+
):
108+
"""It should accept ISO 8601 format or number of seconds."""
109+
opts = CleanOptions(remote_timeout=timeout)
110+
111+
if expected is InputError:
112+
with pytest.raises(expected):
113+
parse_timeout(opts)
114+
else:
115+
parse_timeout(opts)
116+
assert opts.remote_timeout == expected
117+
118+
119+
@pytest.mark.parametrize(
120+
'opts, expected_msg',
121+
[
122+
({'local_only': True, 'remote_only': True}, "mutually exclusive"),
123+
({'remote_timeout': 'oops'}, "Invalid timeout"),
124+
]
125+
)
126+
def test_bad_user_input(opts: dict, expected_msg: str, mute):
127+
"""It should raise an InputError for bad user input."""
128+
with pytest.raises(InputError) as exc_info:
129+
_main(CleanOptions(**opts), 'blah')
130+
assert expected_msg in str(exc_info.value)

tests/unit/test_clean.py

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -964,25 +964,15 @@ def mocked_remote_clean_cmd_side_effect(id_, platform, timeout, rm_dirs):
964964
assert f"{p_name} - {PlatformError.MSG_TIDY}" in caplog.text
965965

966966

967-
@pytest.mark.parametrize(
968-
'timeout, expected',
969-
[('100', '100'),
970-
('PT1M2S', '62.0')]
971-
)
972967
def test_remote_clean__timeout(
973968
monkeymock: MonkeyMock,
974969
monkeypatch: pytest.MonkeyPatch,
975970
caplog: pytest.LogCaptureFixture,
976-
timeout: str,
977-
expected: str,
978971
):
979-
"""Test remote_clean() timeout.
980-
981-
- It should accept ISO 8601 format or number of seconds.
982-
- It should give a sensible error message for return code 124.
972+
"""Test remote_clean() gives a sensible error message for return code 124.
983973
"""
984974
caplog.set_level(logging.ERROR, CYLC_LOG)
985-
mock_remote_clean_cmd = monkeymock(
975+
monkeymock(
986976
'cylc.flow.clean._remote_clean_cmd',
987977
spec=_remote_clean_cmd,
988978
return_value=mock.Mock(
@@ -995,10 +985,13 @@ def test_remote_clean__timeout(
995985
)
996986

997987
with pytest.raises(CylcError):
998-
cylc_clean.remote_clean('blah', 'blah', timeout)
999-
_, kwargs = mock_remote_clean_cmd.call_args
1000-
assert kwargs['timeout'] == expected
1001-
assert f"cylc clean timed out after {expected}s" in caplog.text
988+
cylc_clean.remote_clean(
989+
'blah', platform_names=['blah'], timeout='blah'
990+
)
991+
assert "cylc clean timed out" in caplog.text
992+
# No need to log the remote clean cmd etc. for timeout
993+
assert "ssh" not in caplog.text.lower()
994+
assert "stderr" not in caplog.text.lower()
1002995

1003996

1004997
@pytest.mark.parametrize(

0 commit comments

Comments
 (0)