diff --git a/cylc/flow/scripts/install.py b/cylc/flow/scripts/install.py
index bdf339fcc38..4bce90fabb2 100755
--- a/cylc/flow/scripts/install.py
+++ b/cylc/flow/scripts/install.py
@@ -89,11 +89,16 @@
from cylc.flow.scripts.scan import (
get_pipe,
_format_plain,
+ FLOW_STATE_SYMBOLS,
+ FLOW_STATE_CMAP
)
from cylc.flow import iter_entry_points
from cylc.flow.exceptions import PluginError, InputError
from cylc.flow.loggingutil import CylcLogFormatter
-from cylc.flow.option_parsers import CylcOptionParser as COP
+from cylc.flow.option_parsers import (
+ CylcOptionParser as COP,
+ Options
+)
from cylc.flow.pathutil import EXPLICIT_RELATIVE_PATH_REGEX, expand_path
from cylc.flow.workflow_files import (
install_workflow, search_install_source_dirs, parse_cli_sym_dirs
@@ -167,7 +172,7 @@ def get_source_location(path: Optional[str]) -> Path:
"""
if path is None:
return Path.cwd()
- path = path.strip()
+ path = str(path).strip()
expanded_path = Path(expand_path(path))
if expanded_path.is_absolute():
return expanded_path
@@ -182,33 +187,68 @@ async def scan(wf_name: str) -> None:
'name': [f'{wf_name}/*'],
'states': {'running', 'paused', 'stopping'},
'source': False,
- 'ping': False,
+ 'ping': True, # get status of scanned workflows
})
active = [
item async for item in get_pipe(opts, None, scan_dir=None)
]
if active:
+ n = len(active)
+ grammar = (
+ ["s", "are", "them"]
+ if n > 1 else
+ ["", "is", "it"]
+ )
print(
CylcLogFormatter.COLORS['WARNING'].format(
- f'Instance(s) of "{wf_name}" are already active:'
+ f'WARNING: {n} run%s of "{wf_name}"'
+ ' %s already active:' % tuple(grammar[:2])
)
)
for item in active:
+ status = item['status']
+ tag = FLOW_STATE_CMAP[status]
+ symbol = f"<{tag}>{FLOW_STATE_SYMBOLS[status]}{tag}>"
cprint(
- _format_plain(item, opts)
+ " ", symbol, _format_plain(item, opts)
)
+ pattern = (
+ f"'{wf_name}/*'"
+ if n > 1 else
+ f"{item['name']}"
+ )
+ print(
+ f'You can stop %s with "cylc stop [options] {pattern}".\n'
+ 'See "cylc stop --help" for options.' % grammar[-1]
+ )
+
+
+InstallOptions = Options(get_option_parser())
@cli_function(get_option_parser)
-def main(parser, opts, reg=None):
- wf_name = install(parser, opts, reg)
+def main(
+ _parser: COP,
+ opts: 'Values',
+ reg: Optional[str] = None
+) -> None:
+ """CLI wrapper."""
+ install_cli(opts, reg)
+
+
+def install_cli(
+ opts: 'Values',
+ reg: Optional[str] = None
+) -> None:
+ """Install workflow and scan for already-running instances."""
+ wf_name = install(opts, reg)
asyncio.run(
scan(wf_name)
)
def install(
- parser: COP, opts: 'Values', reg: Optional[str] = None
+ opts: 'Values', reg: Optional[str] = None
) -> str:
if opts.no_run_name and opts.run_name:
raise InputError(
diff --git a/tests/integration/test_install.py b/tests/integration/test_install.py
new file mode 100644
index 00000000000..4c9023347e0
--- /dev/null
+++ b/tests/integration/test_install.py
@@ -0,0 +1,84 @@
+# 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 .
+
+"""Test cylc install."""
+
+from pathlib import Path
+from shutil import rmtree
+from tempfile import TemporaryDirectory
+
+import pytest
+
+from .test_scan import init_flows
+
+from cylc.flow.workflow_files import WorkflowFiles
+from cylc.flow.scripts.install import (
+ InstallOptions,
+ install_cli
+)
+
+
+SRV_DIR = Path(WorkflowFiles.Service.DIRNAME)
+CONTACT = Path(WorkflowFiles.Service.CONTACT)
+RUN_N = Path(WorkflowFiles.RUN_N)
+INSTALL = Path(WorkflowFiles.Install.DIRNAME)
+
+
+@pytest.fixture()
+def src_run_dirs(mock_glbl_cfg, monkeypatch):
+ """Create some workflow source and run dirs for testing.
+
+ Source dirs:
+ /w1
+ /w2
+
+ Run dir:
+ /w1/run1
+
+ """
+ tmp_src_path = Path(TemporaryDirectory().name)
+ tmp_run_path = Path(TemporaryDirectory().name)
+ tmp_src_path.mkdir()
+ tmp_run_path.mkdir()
+
+ init_flows(
+ tmp_run_path=tmp_run_path,
+ running=('w1/run1',),
+ tmp_src_path=tmp_src_path,
+ src=('w1', 'w2')
+ )
+ mock_glbl_cfg(
+ 'cylc.flow.workflow_files.glbl_cfg',
+ f'''
+ [install]
+ source dirs = {tmp_src_path}
+ '''
+ )
+ monkeypatch.setattr('cylc.flow.pathutil._CYLC_RUN_DIR', tmp_run_path)
+
+ yield tmp_src_path, tmp_run_path
+ rmtree(tmp_src_path)
+ rmtree(tmp_run_path)
+
+
+def test_install_scan(src_run_dirs, capsys):
+ """At install, any running intances should be reported."""
+
+ install_cli(opts=InstallOptions(), reg='w1')
+ assert '1 run of "w1" is already active:' in capsys.readouterr().out
+
+ install_cli(opts=InstallOptions(), reg='w2')
+ assert '1 run of "w2" is already active:' not in capsys.readouterr().out
diff --git a/tests/integration/test_scan.py b/tests/integration/test_scan.py
index 94edbfc9b29..6cb405cea69 100644
--- a/tests/integration/test_scan.py
+++ b/tests/integration/test_scan.py
@@ -42,17 +42,20 @@
INSTALL = Path(WorkflowFiles.Install.DIRNAME)
-def init_flows(tmp_path, running=None, registered=None, un_registered=None):
+def init_flows(tmp_run_path=None, running=None, registered=None,
+ un_registered=None, tmp_src_path=None, src=None):
"""Create some dummy workflows for scan to discover.
Assume "run1, run2, ..., runN" structure if flow name constains "run".
+ Optionally create workflow source dirs in a give location too.
+
"""
def make_registered(name, running=False):
- run_d = Path(tmp_path, name)
+ run_d = Path(tmp_run_path, name)
run_d.mkdir(parents=True, exist_ok=True)
(run_d / "flow.cylc").touch()
if "run" in name:
- root = Path(tmp_path, name).parent
+ root = Path(tmp_run_path, name).parent
with suppress(FileExistsError):
(root / "runN").symlink_to(run_d, target_is_directory=True)
else:
@@ -63,12 +66,19 @@ def make_registered(name, running=False):
if running:
(srv_d / CONTACT).touch()
+ def make_src(name):
+ src_d = Path(tmp_src_path, name)
+ src_d.mkdir(parents=True, exist_ok=True)
+ (src_d / "flow.cylc").touch()
+
for name in (running or []):
make_registered(name, running=True)
for name in (registered or []):
make_registered(name)
for name in (un_registered or []):
- Path(tmp_path, name).mkdir(parents=True, exist_ok=True)
+ Path(tmp_run_path, name).mkdir(parents=True, exist_ok=True)
+ for name in (src or []):
+ make_src(name)
@pytest.fixture(scope='session')
@@ -157,14 +167,14 @@ def source_dirs(mock_glbl_cfg):
src1 = src / '1'
src1.mkdir()
init_flows(
- src1,
- registered=('a', 'b/c')
+ tmp_src_path=src1,
+ src=('a', 'b/c')
)
src2 = src / '2'
src2.mkdir()
init_flows(
- src2,
- registered=('d', 'e/f')
+ tmp_src_path=src2,
+ src=('d', 'e/f')
)
mock_glbl_cfg(
'cylc.flow.scripts.scan.glbl_cfg',