Skip to content

Commit 03e8bdb

Browse files
committed
Fix reinstall not detecting changed permissions by itself
1 parent 207984e commit 03e8bdb

File tree

5 files changed

+105
-37
lines changed

5 files changed

+105
-37
lines changed

cylc/flow/install.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -213,19 +213,16 @@ def reinstall_workflow(
213213
dry_run=dry_run,
214214
)
215215

216-
# Add '+++' to -out-format to mark lines passed through formatter.
217-
rsync_cmd.append('--out-format=+++%o %n%L+++')
216+
rsync_cmd.append('--out-format=%i %o %n%L')
217+
# %i: itemized changes - needed for rsync to report files with
218+
# changed permissions
218219

219220
# Run rsync command:
220221
reinstall_log.info(cli_format(rsync_cmd))
221222
LOG.debug(cli_format(rsync_cmd))
222223
proc = Popen(rsync_cmd, stdout=PIPE, stderr=PIPE, text=True) # nosec
223224
# * command is constructed via internal interface
224-
stdout, stderr = proc.communicate()
225-
226-
# Strip unwanted output.
227-
stdout = ('\n'.join(re.findall(r'\+\+\+(.*)\+\+\+', stdout))).strip()
228-
stderr = stderr.strip()
225+
stdout, stderr = (i.strip() for i in proc.communicate())
229226

230227
if proc.returncode != 0:
231228
raise WorkflowFilesError(

cylc/flow/remote.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,9 @@ def get_includes_to_rsync(rsync_includes=None):
183183
'--out-format=%o %n%L',
184184
'--no-t'
185185
]
186+
# %o: the operation (send or del.)
187+
# %n: filename
188+
# %L: "-> symlink_target" if applicable
186189

187190
DEFAULT_INCLUDES = [
188191
'/ana/***', # Rose ana analysis modules

cylc/flow/scripts/reinstall.py

Lines changed: 40 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -68,33 +68,43 @@
6868
"cylc install" even if present in the source directory.
6969
"""
7070

71+
from functools import partial
7172
from pathlib import Path
73+
import re
7274
import sys
73-
from typing import Optional, TYPE_CHECKING, List, Callable
74-
from functools import partial
75+
from typing import (
76+
TYPE_CHECKING,
77+
Callable,
78+
List,
79+
Optional,
80+
)
7581

7682
from ansimarkup import parse as cparse
7783

7884
from cylc.flow.exceptions import (
7985
ServiceFileError,
8086
WorkflowFilesError,
8187
)
82-
from cylc.flow.install import (
83-
reinstall_workflow,
84-
)
88+
import cylc.flow.flags
89+
from cylc.flow.install import reinstall_workflow
8590
from cylc.flow.network.multi import call_multi
8691
from cylc.flow.option_parsers import (
92+
ID_MULTI_ARG_DOC,
8793
CylcOptionParser as COP,
8894
OptionSettings,
89-
ID_MULTI_ARG_DOC
9095
)
9196
from cylc.flow.pathutil import get_workflow_run_dir
9297
from cylc.flow.plugins import run_plugins_async
98+
from cylc.flow.terminal import (
99+
DIM,
100+
cli_function,
101+
is_terminal,
102+
)
93103
from cylc.flow.workflow_files import (
94104
get_workflow_source_dir,
95105
load_contact_file,
96106
)
97-
from cylc.flow.terminal import cli_function, DIM, is_terminal
107+
98108

99109
if TYPE_CHECKING:
100110
from optparse import Values
@@ -269,9 +279,8 @@ async def reinstall(
269279
update anything, else returns True.
270280
271281
"""
272-
# run pre_configure plugins
273282
if not dry_run:
274-
# don't run plugins in dry-mode
283+
# run pre_configure plugins
275284
async for _entry_point, _plugin_result in run_plugins_async(
276285
'cylc.pre_configure',
277286
srcdir=src_dir,
@@ -287,18 +296,17 @@ async def reinstall(
287296
dry_run=dry_run,
288297
)
289298

290-
# display changes
291299
if dry_run:
292-
if not stdout or stdout == 'send ./':
293-
# no rsync output == no changes => exit
300+
# display changes
301+
changes = format_reinstall_output(stdout)
302+
if not changes:
294303
return False
295304

296305
# display rsync output
297-
write('\n'.join(format_rsync_out(stdout)), file=sys.stderr)
306+
write('\n'.join(changes), file=sys.stderr)
298307

299-
# run post_install plugins
300308
if not dry_run:
301-
# don't run plugins in dry-mode
309+
# run post_install plugins
302310
async for _entry_point, _plugin_result in run_plugins_async(
303311
'cylc.post_install',
304312
srcdir=src_dir,
@@ -311,15 +319,15 @@ async def reinstall(
311319
return True
312320

313321

314-
def format_rsync_out(out: str) -> List[str]:
322+
def format_reinstall_output(out: str) -> List[str]:
315323
r"""Format rsync stdout for presenting to users.
316324
317325
Note: Output formats of different rsync implementations may differ so keep
318326
this code simple and robust.
319327
320328
Example:
321-
>>> format_rsync_out(
322-
... 'send foo\ndel. bar\nbaz'
329+
>>> format_reinstall_output(
330+
... '>f+++++++++ send foo\n*deleting del. bar\nbaz'
323331
... '\ncannot delete non-empty directory: opt'
324332
... ) == [
325333
... cparse('<green>send foo</green>'),
@@ -331,20 +339,23 @@ def format_rsync_out(out: str) -> List[str]:
331339
"""
332340
lines = []
333341
for line in out.splitlines():
334-
if line[0:4] == 'send':
335-
# file added or updated
336-
lines.append(cparse(f'<green>{line}</green>'))
337-
elif line[0:4] == 'del.':
338-
# file deleted
339-
lines.append(cparse(f'<red>{line}</red>'))
340-
elif line == 'cannot delete non-empty directory: opt':
341-
# These "cannot delete non-empty directory" messages can arise
342-
# as a result of excluding files within sub-directories.
343-
# This opt dir message is likely to occur when a rose-suit.conf
342+
if not line or line.startswith('cannot delete non-empty directory:'):
343+
# This can arise as a result of deleting a subdir when we are
344+
# excluding files within the subdir.
345+
# This is likely to occur for the opt dir when a rose-suite.conf
344346
# file is present.
347+
# Skip this line as nothing will happen to this dir.
345348
continue
349+
match = re.match(r'^(.{11}) (send|del\.) (.*)$', line)
350+
if match:
351+
summary, operation, file = match.groups()
352+
color = 'green' if operation == 'send' else 'red'
353+
formatted_line = f"<{color}>{operation} {file}</{color}>"
354+
if cylc.flow.flags.verbosity > 0:
355+
formatted_line = f"<{DIM}>{summary}</{DIM}> {formatted_line}"
356+
lines.append(cparse(formatted_line))
346357
else:
347-
# other uncategorised log line
358+
# shouldn't happen; tolerate unknown rsync implementation?
348359
lines.append(line)
349360
return lines
350361

tests/integration/test_reinstall.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@ def non_interactive(monkeypatch):
6868

6969
@pytest.fixture
7070
def one_src(tmp_path):
71-
src_dir = tmp_path
71+
src_dir = tmp_path / 'src'
72+
src_dir.mkdir()
7273
(src_dir / 'flow.cylc').touch()
7374
(src_dir / 'rose-suite.conf').touch()
7475
return SimpleNamespace(path=src_dir)
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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/>.
16+
17+
from textwrap import dedent
18+
19+
from ansimarkup import parse as cparse
20+
import pytest
21+
22+
from cylc.flow.scripts.reinstall import format_reinstall_output
23+
from cylc.flow.terminal import DIM
24+
25+
26+
@pytest.mark.parametrize('verbose', [True, False])
27+
def test_format_reinstall_output(verbose, monkeypatch: pytest.MonkeyPatch):
28+
"""It should:
29+
- colorize the output
30+
- remove the itemized changes summary if not in verbose mode
31+
- remove the "cannot delete non-empty directory" message
32+
"""
33+
output = dedent("""
34+
*deleting del. Cloud.jpg
35+
>f+++++++++ send cloud.jpg
36+
.f...p..... send foo
37+
>fcsTp..... send bar
38+
cannot delete non-empty directory: scarf
39+
>f+++++++++ send meow.txt
40+
cL+++++++++ send garage -> foo
41+
""").strip()
42+
expected = [
43+
f"<{DIM}>*deleting </{DIM}> <red>del. Cloud.jpg</red>",
44+
f"<{DIM}>>f+++++++++</{DIM}> <green>send cloud.jpg</green>",
45+
f"<{DIM}>.f...p.....</{DIM}> <green>send foo</green>",
46+
f"<{DIM}>>fcsTp.....</{DIM}> <green>send bar</green>",
47+
f"<{DIM}>>f+++++++++</{DIM}> <green>send meow.txt</green>",
48+
f"<{DIM}>cL+++++++++</{DIM}> <green>send garage -> foo</green>",
49+
]
50+
if verbose:
51+
monkeypatch.setattr('cylc.flow.flags.verbosity', 1)
52+
else:
53+
# itemized changes summary should not be in output
54+
shift = len(f'<{DIM}></{DIM}> ') + 11
55+
expected = [i[shift:] for i in expected]
56+
assert format_reinstall_output(output) == [cparse(i) for i in expected]

0 commit comments

Comments
 (0)