1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327
|
# THIS FILE IS PART OF THE CYLC WORKFLOW ENGINE.
# Copyright (C) NIWA & British Crown (Met Office) & Contributors.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from contextlib import contextmanager
from secrets import token_hex
import sqlite3
import pytest
from cylc.flow.exceptions import HostSelectException, ServiceFileError
from cylc.flow.scheduler_cli import (
RunOptions,
_distribute,
_version_check,
)
from .conftest import MonkeyMock
@pytest.fixture
def stopped_workflow_db(tmp_path):
"""Returns a workflow DB with the `cylc_version` set to the provided
string.
def test_x(stopped_workflow_db):
db_file = stopped_workflow_db(version)
"""
def _stopped_workflow_db(version):
db_file = tmp_path / 'db'
conn = sqlite3.connect(db_file)
conn.execute('''
CREATE TABLE
workflow_params(key TEXT, value TEXT, PRIMARY KEY(key))
''')
conn.execute(
'''
INSERT INTO
workflow_params
VALUES (?, ?)
''',
('cylc_version', version)
)
conn.commit()
conn.close()
return db_file
return _stopped_workflow_db
@pytest.fixture
def set_cylc_version(monkeypatch):
"""Set the cylc.flow.__version__ attribute.
def test_x(set_cylc_version):
set_cylc_version('1.2.3')
"""
def _set_cylc_version(version):
monkeypatch.setattr(
'cylc.flow.scheduler_cli.__version__',
version,
)
return _set_cylc_version
@pytest.fixture
def answer(monkeypatch):
"""Answer a `cylc play` CLI prompt.
def test_x(answer):
answer(users_response)
It also adds an assert on the number of times the prompt interface was
called. 0 if response is None, else 1.
"""
@contextmanager
def _answer(response):
calls = 0
def prompt(*args, **kwargs):
nonlocal calls
calls += 1
return response
monkeypatch.setattr(
'cylc.flow.scheduler_cli.prompt',
prompt,
)
yield
expected_calls = 1
if response is None:
expected_calls = 0
assert calls == expected_calls
return _answer
@pytest.fixture
def interactive(monkeypatch):
monkeypatch.setattr(
'cylc.flow.scheduler_cli.is_terminal',
lambda: True,
)
@pytest.fixture
def non_interactive(monkeypatch):
monkeypatch.setattr(
'cylc.flow.scripts.reinstall.is_terminal',
lambda: False,
)
@pytest.mark.parametrize(
'before, after, downgrade, response, outcome', [
# no change
('8.0.0', '8.0.0', False, None, True),
# upgrading
('8.0rc4.dev', '8.0.0', False, None, True),
('8.0.0', '8.0.1', False, None, True),
('8.0.0', '8.1.0', False, False, False),
('8.0.0', '8.1.0', False, True, True),
('8.0.0', '9.0.0', False, False, False),
('8.0.0', '9.0.0', False, True, True),
# downgrading
('8.1.1', '8.1.0', False, None, False),
('8.1.1', '8.1.0', True, None, True),
('8.1.0', '8.0.0', False, None, False),
('8.1.0', '8.0.0', True, None, True),
('8.1.0', '8.0rc4.dev', True, None, True),
('9.1.0', '8.0.0', False, None, False),
('9.1.0', '8.0.0', True, None, True),
# truncated versions
('8.1.1', '8', False, None, False),
('9.1.1', '8', True, None, True),
],
)
def test_version_check_interactive(
stopped_workflow_db,
set_cylc_version,
interactive,
answer,
before,
after,
response,
downgrade,
outcome,
):
"""It should check compatibility with the Cylc version of the prior run.
When workflows are restarted we need to perform some checks to make sure
it is safe and sensible to restart with this version of Cylc.
Pytest Params:
before:
The Cylc version the workflow ran with previously.
after:
The version of Cylc being used to restart the workflow.
downgrade:
The --downgrade option of `cylc play`.
response:
The user's response the any CLI prompts.
If `None` it will assert that no prompts were raised.
outcome:
The response of _version_check, True means safe to restart.
"""
db_file = stopped_workflow_db(before)
set_cylc_version(after)
with answer(response):
assert (
_version_check(
db_file, RunOptions(downgrade=downgrade)
)
is outcome
)
def test_version_check_interactive_upgrade(
stopped_workflow_db,
set_cylc_version,
interactive,
answer,
):
"""If a user interactively upgrades, it should set the upgrade option."""
db_file = stopped_workflow_db('8.0.0')
set_cylc_version('8.1.0')
opts = RunOptions()
assert opts.upgrade is False
with answer(True):
assert _version_check(db_file, opts) is True
assert opts.upgrade is True
def test_version_check_non_interactive(
stopped_workflow_db,
set_cylc_version,
non_interactive,
):
"""It should not prompt in non-interactive mode.
* The --upgrade argument should permit upgrade.
* The --downgrade argument should permit downgrade.
"""
# upgrade
db_file = stopped_workflow_db('8.0.0')
set_cylc_version('8.1.0')
assert _version_check(db_file, RunOptions()) is False
assert (
_version_check(db_file, RunOptions(upgrade=True)) is True
) # CLI --upgrade
# downgrade
db_file.unlink()
db_file = stopped_workflow_db('8.1.0')
set_cylc_version('8.0.0')
assert _version_check(db_file, RunOptions()) is False
assert (
_version_check(db_file, RunOptions(downgrade=True)) is True
) # CLI --downgrade
def test_version_check_incompat(tmp_path):
"""It should fail for a corrupted or invalid database file."""
db_file = tmp_path / 'db' # invalid DB file
db_file.touch()
with pytest.raises(ServiceFileError):
_version_check(db_file, RunOptions())
def test_version_check_no_db(tmp_path):
"""It should pass if there is no DB file (e.g. on workflow first start)."""
db_file = tmp_path / 'db' # non-existent file
assert _version_check(db_file, RunOptions())
@pytest.mark.parametrize(
'cli_colour, is_terminal, distribute_colour',
[
('never', True, '--color=never'),
('auto', True, '--color=always'),
('always', True, '--color=always'),
('never', False, '--color=never'),
('auto', False, '--color=never'),
('always', False, '--color=never'),
]
)
def test_distribute_colour(
monkeymock,
cli_colour,
is_terminal,
distribute_colour,
):
"""It should start detached workflows with the correct --colour option.
The is_terminal test will fail for detached scheduler processes which means
that the colour formatting will be stripped for startup. This includes
the Cylc header logo and any warnings/errors raised during config parsing.
In order to preserver colour formatting we must set the `--colour` arg to
`always` when we want the detached process to start in colour mode.
See https://github.com/cylc/cylc-flow/issues/5159
"""
_is_terminal = monkeymock('cylc.flow.scheduler_cli.is_terminal')
_is_terminal.return_value = is_terminal
_cylc_server_cmd = monkeymock('cylc.flow.scheduler_cli.cylc_server_cmd')
_cylc_server_cmd.return_value = 0
opts = RunOptions(host='myhost', color=cli_colour)
with pytest.raises(SystemExit) as excinfo:
_distribute('foo', 'foo/run1', opts)
assert excinfo.value.code == 0
assert distribute_colour in _cylc_server_cmd.call_args[0][0]
def test_distribute_upgrade(
monkeymock: MonkeyMock, monkeypatch: pytest.MonkeyPatch
):
"""It should start detached workflows with the --upgrade option if the user
has interactively chosen to upgrade (typed 'y' at prompt).
"""
monkeypatch.setattr(
'sys.argv', ['cylc', 'play', 'foo'] # no upgrade option here
)
_cylc_server_cmd = monkeymock('cylc.flow.scheduler_cli.cylc_server_cmd')
_cylc_server_cmd.return_value = 0
opts = RunOptions(
host='myhost',
upgrade=True, # added by interactive upgrade
)
with pytest.raises(SystemExit) as excinfo:
_distribute('foo', 'foo/run1', opts)
assert excinfo.value.code == 0
assert '--upgrade' in _cylc_server_cmd.call_args[0][0]
def test_distribute_invalid_host(
mock_glbl_cfg, caplog: pytest.LogCaptureFixture
):
"""It handles a socket error when the host is invalid."""
mock_glbl_cfg(
'cylc.flow.host_select.glbl_cfg',
f'''
[scheduler]
[[run hosts]]
available = non_exist_{token_hex(4)}
'''
)
with pytest.raises(HostSelectException):
_distribute('foo', 'foo/run1', RunOptions())
|