diff --git a/cylc/flow/scripts/workflow_state.py b/cylc/flow/scripts/workflow_state.py
index 8a49eaa6a4c..16bafa90322 100755
--- a/cylc/flow/scripts/workflow_state.py
+++ b/cylc/flow/scripts/workflow_state.py
@@ -74,6 +74,7 @@
if TYPE_CHECKING:
from optparse import Values
+ from typing import Tuple, Optional
class WorkflowPoller(Poller):
@@ -194,6 +195,19 @@ def get_option_parser() -> COP:
return parser
+def get_workflow(w_id: str, alt_run_dir: Optional[str] = None) -> Tuple[str, str]:
+ """Infer run number for a workflow ID, for alternate run dir if nec."""
+ if alt_run_dir:
+ run_dir = alt_run_dir = expand_path(alt_run_dir)
+ else:
+ run_dir = get_cylc_run_dir()
+ alt_run_dir = None
+ workflow_id, *_ = parse_id(
+ w_id, constraint='workflows', alt_run_dir=alt_run_dir
+ )
+ return workflow_id, run_dir
+
+
@cli_function(get_option_parser, remove_opts=["--db"])
def main(parser: COP, options: 'Values', workflow_id: str) -> None:
@@ -227,18 +241,8 @@ def main(parser: COP, options: 'Values', workflow_id: str) -> None:
options.status not in CylcWorkflowDBChecker.STATE_ALIASES):
raise InputError(f"invalid status '{options.status}'")
- # this only runs locally
- if options.alt_run_dir:
- run_dir = alt_run_dir = expand_path(options.alt_run_dir)
- else:
- run_dir = get_cylc_run_dir()
- alt_run_dir = None
-
- workflow_id, *_ = parse_id(
- workflow_id,
- constraint='workflows',
- alt_run_dir=alt_run_dir
- )
+ # Infer workflow run number if necessary.
+ workflow_id, run_dir = get_workflow(workflow_id, options.alt_run_dir)
pollargs = {
'workflow_id': workflow_id,
diff --git a/tests/unit/scripts/test_workflow_state.py b/tests/unit/scripts/test_workflow_state.py
new file mode 100644
index 00000000000..6e4e4e6719b
--- /dev/null
+++ b/tests/unit/scripts/test_workflow_state.py
@@ -0,0 +1,49 @@
+# 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 aspects of the `cylc workflow-state` command.
+
+import pytest
+from shutil import copytree, rmtree
+
+from cylc.flow.exceptions import InputError
+from cylc.flow.pathutil import get_cylc_run_dir
+from cylc.flow.scripts.workflow_state import get_workflow
+
+
+def test_get_workflow(tmp_run_dir):
+ """It should infer the run name for auto-numbered installations."""
+ # it doesn't do anything for a named run
+ tmp_run_dir('foo/run1', installed=True)
+ cylc_run_dir = get_cylc_run_dir()
+
+ assert get_workflow('foo') == ('foo/run1', cylc_run_dir)
+
+ # Now test we can see workflows in alternate cylc-run directories
+ # e.g. for `cylc workflow-state` or xtriggers targetting another user.
+ alt_cylc_run_dir = cylc_run_dir + "_alt"
+
+ # copy the cylc-run dir to alt location and delete the original.
+ copytree(cylc_run_dir, alt_cylc_run_dir, symlinks=True)
+ rmtree(cylc_run_dir)
+
+ # It can no longer parse IDs in the original cylc-run location.
+ with pytest.raises(InputError):
+ get_workflow('foo')
+
+ # But it can if we specify the alternate location.
+ assert get_workflow('foo', alt_cylc_run_dir) == (
+ 'foo/run1', alt_cylc_run_dir)
diff --git a/tests/unit/test_id_cli.py b/tests/unit/test_id_cli.py
index 3db329436bf..5ae11ef1a65 100644
--- a/tests/unit/test_id_cli.py
+++ b/tests/unit/test_id_cli.py
@@ -18,6 +18,7 @@
import os
from pathlib import Path
import pytest
+from shutil import copytree, rmtree
from cylc.flow import CYLC_LOG
from cylc.flow.async_util import pipe
@@ -248,6 +249,32 @@ async def test_parse_ids_infer_run_name(tmp_run_dir):
)
assert list(workflows) == ['bar']
+ # Now test we can see workflows in alternate cylc-run directories
+ # e.g. for `cylc workflow-state` or xtriggers targetting another user.
+ cylc_run_dir = get_cylc_run_dir()
+ alt_cylc_run_dir = cylc_run_dir + "_alt"
+
+ # copy the cylc-run dir to alt location and delete the original.
+ copytree(cylc_run_dir, alt_cylc_run_dir, symlinks=True)
+ rmtree(cylc_run_dir)
+
+ # It can no longer parse IDs in the original cylc-run location.
+ with pytest.raises(InputError):
+ workflows, *_ = await parse_ids_async(
+ 'bar//',
+ constraint='workflows',
+ infer_latest_runs=True,
+ )
+
+ # But it can if we specify the alternate location.
+ workflows, *_ = await parse_ids_async(
+ 'bar//',
+ constraint='workflows',
+ infer_latest_runs=True,
+ alt_run_dir=alt_cylc_run_dir
+ )
+ assert list(workflows) == ['bar/run2']
+
@pytest.fixture
def patch_expand_workflow_tokens(monkeypatch):
diff --git a/tests/unit/xtriggers/test_workflow_state.py b/tests/unit/xtriggers/test_workflow_state.py
index b3d25737cc2..40321ab4108 100644
--- a/tests/unit/xtriggers/test_workflow_state.py
+++ b/tests/unit/xtriggers/test_workflow_state.py
@@ -15,15 +15,18 @@
# along with this program. If not, see .
from pathlib import Path
+import pytest
import sqlite3
from typing import Callable
from unittest.mock import Mock
+from shutil import copytree, rmtree
+from cylc.flow.exceptions import InputError
+from cylc.flow.pathutil import get_cylc_run_dir
from cylc.flow.workflow_files import WorkflowFiles
from cylc.flow.xtriggers.workflow_state import workflow_state
from ..conftest import MonkeyMock
-
def test_inferred_run(tmp_run_dir: Callable, monkeymock: MonkeyMock):
"""Test that the workflow_state xtrigger infers the run number"""
id_ = 'isildur'
@@ -42,6 +45,24 @@ def test_inferred_run(tmp_run_dir: Callable, monkeymock: MonkeyMock):
assert results['workflow'] == expected_workflow_id
+ # Now test we can see workflows in alternate cylc-run directories
+ # e.g. for `cylc workflow-state` or xtriggers targetting another user.
+ alt_cylc_run_dir = cylc_run_dir + "_alt"
+
+ # copy the cylc-run dir to alt location and delete the original.
+ copytree(cylc_run_dir, alt_cylc_run_dir, symlinks=True)
+ rmtree(cylc_run_dir)
+
+ # It can no longer parse IDs in the original cylc-run location.
+ with pytest.raises(InputError):
+ _, results = workflow_state(id_, task='precious', point='3000')
+
+ # But it can via an explicit alternate run directory.
+ mock_db_checker.reset_mock()
+ _, results = workflow_state(id_, task='precious', point='3000', cylc_run_dir=alt_cylc_run_dir)
+ mock_db_checker.assert_called_once_with(alt_cylc_run_dir, expected_workflow_id)
+ assert results['workflow'] == expected_workflow_id
+
def test_back_compat(tmp_run_dir):
"""Test workflow_state xtrigger backwards compatibility with Cylc 7
database."""