From 6fc3ca3517cce37da8940d92ed7c76775bdeb18b Mon Sep 17 00:00:00 2001
From: Tim Pillinger <26465611+wxtim@users.noreply.github.com>
Date: Wed, 14 Dec 2022 09:39:37 +0000
Subject: [PATCH] add Cylc VRO
---
cylc/flow/option_parsers.py | 25 ++-
cylc/flow/pathutil.py | 29 ++++
cylc/flow/scripts/config.py | 2 +-
cylc/flow/scripts/cylc.py | 3 +-
cylc/flow/scripts/graph.py | 2 +-
cylc/flow/scripts/list.py | 2 +-
cylc/flow/scripts/psutil.py | 2 +-
cylc/flow/scripts/reinstall.py | 23 ++-
cylc/flow/scripts/reload.py | 4 +
cylc/flow/scripts/validate.py | 2 +-
cylc/flow/scripts/validate_install_play.py | 9 +-
.../flow/scripts/validate_reinstall_reload.py | 150 ++++++++++++++++++
cylc/flow/scripts/view.py | 1 -
cylc/flow/templatevars.py | 13 +-
cylc/flow/workflow_files.py | 10 +-
setup.cfg | 1 +
.../cylc-combination-scripts/01-vro-reload.t | 53 +++++++
.../cylc-combination-scripts/02-vro-restart.t | 54 +++++++
.../cylc-combination-scripts/03-vro-resume.t | 56 +++++++
.../04-vro-fail-validate.t | 53 +++++++
.../05-vro-fail-is-running.t | 58 +++++++
.../vro_workflow/flow.cylc | 8 +
tests/integration/conftest.py | 6 +
tests/unit/scripts/test_validate.py | 0
tests/unit/test_get_old_tvars_units.py | 65 ++++++++
tests/unit/test_pathutil.py | 41 +++++
26 files changed, 643 insertions(+), 29 deletions(-)
create mode 100644 cylc/flow/scripts/validate_reinstall_reload.py
create mode 100644 tests/functional/cylc-combination-scripts/01-vro-reload.t
create mode 100644 tests/functional/cylc-combination-scripts/02-vro-restart.t
create mode 100644 tests/functional/cylc-combination-scripts/03-vro-resume.t
create mode 100644 tests/functional/cylc-combination-scripts/04-vro-fail-validate.t
create mode 100644 tests/functional/cylc-combination-scripts/05-vro-fail-is-running.t
create mode 100644 tests/functional/cylc-combination-scripts/vro_workflow/flow.cylc
create mode 100644 tests/unit/scripts/test_validate.py
create mode 100644 tests/unit/test_get_old_tvars_units.py
diff --git a/cylc/flow/option_parsers.py b/cylc/flow/option_parsers.py
index 3bb6b2e568a..542d9fbc9a9 100644
--- a/cylc/flow/option_parsers.py
+++ b/cylc/flow/option_parsers.py
@@ -80,9 +80,16 @@ def __init__(self, argslist, sources=None, useif=None, **kwargs):
self.kwargs.update({kwarg: value})
def __eq__(self, other):
- """Args and Kwargs, but not other props equal."""
+ """Args and Kwargs, but not other props equal.
+
+ (Also make an exception for kwargs['help'] to allow lists of sources
+ prepended to 'help' to be passed through.)
+ """
return (
- self.kwargs == other.kwargs
+ (
+ {k: v for k, v in self.kwargs.items() if k != 'help'}
+ == {k: v for k, v in other.kwargs.items() if k != 'help'}
+ )
and self.args == other.args
)
@@ -729,9 +736,17 @@ def combine_options_pair(first_list, second_list):
and first & second
):
# if any of the args are different:
- if first.args == second.args:
- # if none of the arg names are different.
- raise Exception(f'Clashing Options \n{first}\n{second}')
+
+ if (
+ first.args == second.args
+ # and (
+ # first.kwargs['help'].split('\x1b[0m ')[-1] !=
+ # second.kwargs['help'].split('\x1b[0m')[-1]
+ # )
+ ):
+ # if none of the arg names are different
+ raise Exception(
+ f'Clashing Options \n{first.args}\n{second.args}')
else:
first_args = first - second
second.args = second - first
diff --git a/cylc/flow/pathutil.py b/cylc/flow/pathutil.py
index 0d62ef37ad4..9d25cc4e757 100644
--- a/cylc/flow/pathutil.py
+++ b/cylc/flow/pathutil.py
@@ -456,3 +456,32 @@ def get_workflow_name_from_id(workflow_id: str) -> str:
name_path = id_path
return str(name_path.relative_to(cylc_run_dir))
+
+
+def get_source_conf_from_id(workflow_id):
+ """Give a workflow id, get the flow.cylc file of the source.
+
+ Additionally Check
+ 1. Flow.cylc or suite.rc exists at source.
+ """
+ # Avoid circular import:
+ from cylc.flow.workflow_files import WorkflowFiles
+ # Get path of source:
+ run_dir = Path(get_workflow_run_dir(workflow_id))
+ relative_path = run_dir.relative_to(get_cylc_run_dir())
+ src = (
+ Path(get_cylc_run_dir())
+ / relative_path.parts[0]
+ / WorkflowFiles.Install.DIRNAME
+ / WorkflowFiles.Install.SOURCE
+ )
+
+ # Test whether there is a config file at source:
+ if (src / WorkflowFiles.FLOW_FILE).exists():
+ return src.resolve() / WorkflowFiles.FLOW_FILE
+ elif (src / WorkflowFiles.SUITE_RC).exists():
+ return src.resolve() / WorkflowFiles.SUITE_RC
+ else:
+ raise WorkflowFilesError(
+ f'Source file not present at: {src.resolve()}'
+ )
diff --git a/cylc/flow/scripts/config.py b/cylc/flow/scripts/config.py
index aa7144a934d..2b3b3d134e6 100755
--- a/cylc/flow/scripts/config.py
+++ b/cylc/flow/scripts/config.py
@@ -206,7 +206,7 @@ async def _main(
workflow_id,
flow_file,
options,
- get_template_vars(options)
+ get_template_vars(options, flow_file)
)
config.pcfg.idump(
diff --git a/cylc/flow/scripts/cylc.py b/cylc/flow/scripts/cylc.py
index 74a37119043..4c474e4dd6d 100644
--- a/cylc/flow/scripts/cylc.py
+++ b/cylc/flow/scripts/cylc.py
@@ -202,7 +202,8 @@ def get_version(long=False):
'shutdown': 'stop',
'task-message': 'message',
'unhold': 'release',
- 'validate-install-play': 'vip'
+ 'validate-install-play': 'vip',
+ 'validate-reinstall-reload': 'vro',
}
diff --git a/cylc/flow/scripts/graph.py b/cylc/flow/scripts/graph.py
index ce22cfdc2ed..fa24fb3e827 100644
--- a/cylc/flow/scripts/graph.py
+++ b/cylc/flow/scripts/graph.py
@@ -199,7 +199,7 @@ def _get_inheritance_nodes_and_edges(
def get_config(workflow_id: str, opts: 'Values', flow_file) -> WorkflowConfig:
"""Return a WorkflowConfig object for the provided reg / path."""
- template_vars = get_template_vars(opts)
+ template_vars = get_template_vars(opts, flow_file)
return WorkflowConfig(
workflow_id, flow_file, opts, template_vars=template_vars
)
diff --git a/cylc/flow/scripts/list.py b/cylc/flow/scripts/list.py
index c0f1ed18ab7..5b715831dfe 100755
--- a/cylc/flow/scripts/list.py
+++ b/cylc/flow/scripts/list.py
@@ -116,7 +116,7 @@ async def _main(parser: COP, options: 'Values', workflow_id: str) -> None:
src=True,
constraint='workflows',
)
- template_vars = get_template_vars(options)
+ template_vars = get_template_vars(options, flow_file)
if options.all_tasks and options.all_namespaces:
parser.error("Choose either -a or -n")
diff --git a/cylc/flow/scripts/psutil.py b/cylc/flow/scripts/psutil.py
index 8747acc9a5a..47ff7ec1dbd 100644
--- a/cylc/flow/scripts/psutil.py
+++ b/cylc/flow/scripts/psutil.py
@@ -22,7 +22,7 @@
the `psutil` on remote platforms.
Exits:
- 0 - If successfull.
+ 0 - If successful.
2 - For errors in extracting results from psutil
1 - For all other errors.
"""
diff --git a/cylc/flow/scripts/reinstall.py b/cylc/flow/scripts/reinstall.py
index c199c912508..65716b685ab 100644
--- a/cylc/flow/scripts/reinstall.py
+++ b/cylc/flow/scripts/reinstall.py
@@ -83,6 +83,7 @@
from cylc.flow.id_cli import parse_id
from cylc.flow.option_parsers import (
CylcOptionParser as COP,
+ OptionSettings,
Options,
WORKFLOW_ID_ARG_DOC,
)
@@ -100,6 +101,18 @@
_input = input # to enable testing
+REINSTALL_CYLC_ROSE_OPTIONS = [
+ OptionSettings(
+ ['--clear-rose-install-options'],
+ help="Clear options previously set by cylc-rose.",
+ action='store_true',
+ default=False,
+ dest="clear_rose_install_opts",
+ sources={'reinstall'}
+ )
+]
+
+
def get_option_parser() -> COP:
parser = COP(
__doc__, comms=True, argdoc=[WORKFLOW_ID_ARG_DOC]
@@ -112,14 +125,8 @@ def get_option_parser() -> COP:
except ImportError:
pass
else:
- parser.add_option(
- "--clear-rose-install-options",
- help="Clear options previously set by cylc-rose.",
- action='store_true',
- default=False,
- dest="clear_rose_install_opts"
- )
-
+ for option in REINSTALL_CYLC_ROSE_OPTIONS:
+ parser.add_option(*option.args, **option.kwargs)
return parser
diff --git a/cylc/flow/scripts/reload.py b/cylc/flow/scripts/reload.py
index 125e7d40fac..f399cc948cb 100755
--- a/cylc/flow/scripts/reload.py
+++ b/cylc/flow/scripts/reload.py
@@ -104,6 +104,10 @@ async def run(options: 'Values', workflow_id: str) -> None:
@cli_function(get_option_parser)
def main(parser: COP, options: 'Values', *ids) -> None:
+ reload_cli(options, *ids)
+
+
+def reload_cli(options: 'Values', *ids) -> None:
call_multi(
partial(run, options),
*ids,
diff --git a/cylc/flow/scripts/validate.py b/cylc/flow/scripts/validate.py
index 0db83d1bdfa..2b47d073b39 100755
--- a/cylc/flow/scripts/validate.py
+++ b/cylc/flow/scripts/validate.py
@@ -155,7 +155,7 @@ async def wrapped_main(
workflow_id,
flow_file,
options,
- get_template_vars(options),
+ get_template_vars(options, flow_file),
output_fname=options.output,
mem_log_func=profiler.log_memory
)
diff --git a/cylc/flow/scripts/validate_install_play.py b/cylc/flow/scripts/validate_install_play.py
index 25f6e805b5e..02be6039cd1 100644
--- a/cylc/flow/scripts/validate_install_play.py
+++ b/cylc/flow/scripts/validate_install_play.py
@@ -30,7 +30,7 @@
from cylc.flow.scripts.validate import (
VALIDATE_OPTIONS,
- _main as validate_main
+ _main as cylc_validate
)
from cylc.flow.scripts.install import (
INSTALL_OPTIONS, install_cli as cylc_install, get_source_location
@@ -75,10 +75,7 @@ def get_option_parser() -> COP:
]
)
for option in VIP_OPTIONS:
- # Make a special exception for option against_source which makes
- # no sense in a VIP context.
- if option.kwargs.get('dest') != 'against_source':
- parser.add_option(*option.args, **option.kwargs)
+ parser.add_option(*option.args, **option.kwargs)
return parser
@@ -92,7 +89,7 @@ def main(parser: COP, options: 'Values', workflow_id: Optional[str] = None):
source = get_source_location(workflow_id)
log_subcommand('validate', source)
- validate_main(parser, options, str(source))
+ cylc_validate(parser, options, str(source))
log_subcommand('install', source)
workflow_id = cylc_install(options, workflow_id)
diff --git a/cylc/flow/scripts/validate_reinstall_reload.py b/cylc/flow/scripts/validate_reinstall_reload.py
new file mode 100644
index 00000000000..b65a5e4da77
--- /dev/null
+++ b/cylc/flow/scripts/validate_reinstall_reload.py
@@ -0,0 +1,150 @@
+#!/usr/bin/env python3
+
+# 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 .
+
+"""cylc validate-reinstall-reload/play [OPTIONS] ARGS
+
+Validate and install a single workflow. Then if:
+- Workflow running => reload.
+- Workflow paused => resume.
+- Workflow stopped => play.
+
+This script is equivalent to:
+
+ $ cylc validate myworkflow --against-source # See note 1
+ $ cylc reinstall myworkflow
+
+ $ if [[ my workflow is running ]];
+ cylc reload myworkflow
+ else
+ cylc play myworkflow
+
+Note 1:
+
+Cylc validate myworkflow --against-source is eqivelent of (It doesn't write
+any temporary files though):
+
+ # Install from run directory
+ $ cylc install ~/cylc-run/myworkflow -n temporary
+ # Install from source directory over the top
+ $ cylc install /path/to/myworkflow -n temporary
+ # Validate combined config
+ $ cylc validate ~/cylc-run/temporary
+"""
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from optparse import Values
+
+from cylc.flow.exceptions import ServiceFileError, CylcError
+from cylc.flow.scheduler_cli import PLAY_OPTIONS, scheduler_cli
+from cylc.flow.scripts.validate import (
+ VALIDATE_OPTIONS,
+ _main as cylc_validate
+)
+from cylc.flow.scripts.reinstall import (
+ REINSTALL_CYLC_ROSE_OPTIONS, reinstall_cli as cylc_reinstall
+)
+from cylc.flow.scripts.reload import (
+ reload_cli as cylc_reload
+)
+from cylc.flow.option_parsers import (
+ WORKFLOW_ID_ARG_DOC,
+ CylcOptionParser as COP,
+ combine_options,
+ log_subcommand,
+ cleanup_sysargv
+)
+from cylc.flow.terminal import cli_function
+from cylc.flow.workflow_files import detect_old_contact_file
+
+
+CYLC_ROSE_OPTIONS = COP.get_cylc_rose_options()
+VRO_OPTIONS = combine_options(
+ VALIDATE_OPTIONS,
+ REINSTALL_CYLC_ROSE_OPTIONS,
+ PLAY_OPTIONS,
+ CYLC_ROSE_OPTIONS,
+ modify={'cylc-rose': 'validate, install'}
+)
+
+
+def get_option_parser() -> COP:
+ parser = COP(
+ __doc__,
+ comms=True,
+ jset=True,
+ argdoc=[WORKFLOW_ID_ARG_DOC],
+ )
+ for option in VRO_OPTIONS:
+ parser.add_option(*option.args, **option.kwargs)
+ return parser
+
+
+@cli_function(get_option_parser)
+def main(parser: COP, options: 'Values', workflow_id: str):
+ vro_cli(parser, options, workflow_id)
+
+
+def vro_cli(parser: COP, options: 'Values', workflow_id: str):
+ """Run Cylc (re)validate - reinstall - reload in sequence."""
+ # Attempt to work out whether the workflow is running.
+ # We are trying to avoid reinstalling a subsequently being
+ # unable to play or reload because we cannot identify workflow state.
+ try:
+ detect_old_contact_file(workflow_id, quiet=True)
+ except ServiceFileError:
+ # Workflow is definately still running:
+ workflow_running = True
+ except CylcError as exc:
+ # We can't tell whether the workflow is running.
+ # TODO - consider a more helpful error
+ raise exc
+ exit(1)
+ else:
+ # Workflow is definately stopped:
+ workflow_running = False
+
+ # Force on the against_source option:
+ options.against_source = True # Make validate check against source.
+ log_subcommand('validate --against-source', workflow_id)
+ cylc_validate(parser, options, workflow_id)
+
+ log_subcommand('reinstall', workflow_id)
+ cylc_reinstall(options, workflow_id)
+
+ # Run reload if workflow is running, else play:
+ if workflow_running:
+ log_subcommand('reload', workflow_id)
+ cylc_reload(options, workflow_id)
+
+ # run play anyway, to resume a paused workflow:
+ else:
+ cleanup_sysargv(
+ 'play',
+ workflow_id,
+ options,
+ compound_script_opts=VRO_OPTIONS,
+ script_opts=(
+ PLAY_OPTIONS + CYLC_ROSE_OPTIONS
+ + parser.get_std_options()
+ ),
+ source='', # Intentionally blank
+ )
+ log_subcommand('play', workflow_id)
+ scheduler_cli(options, workflow_id)
diff --git a/cylc/flow/scripts/view.py b/cylc/flow/scripts/view.py
index dc5abf6dafe..e52b0a578ac 100644
--- a/cylc/flow/scripts/view.py
+++ b/cylc/flow/scripts/view.py
@@ -109,7 +109,6 @@ def main(parser: COP, options: 'Values', workflow_id: str) -> None:
async def _main(parser: COP, options: 'Values', workflow_id: str) -> None:
workflow_id, _, flow_file = await parse_id_async(
-
workflow_id,
src=True,
constraint='workflows',
diff --git a/cylc/flow/templatevars.py b/cylc/flow/templatevars.py
index f09e367d25a..2773264bbc3 100644
--- a/cylc/flow/templatevars.py
+++ b/cylc/flow/templatevars.py
@@ -82,9 +82,18 @@ def eval_var(var):
) from None
-def load_template_vars(template_vars=None, template_vars_file=None):
+def load_template_vars(
+ template_vars=None, template_vars_file=None, flow_file=None
+):
"""Load template variables from key=value strings."""
res = {}
+ if flow_file is not None:
+ srcdir = str(Path(flow_file).parent)
+ db_tvars = OldTemplateVars(srcdir).template_vars
+ if db_tvars:
+ for key, val in db_tvars.items():
+ res[key] = val
+
if template_vars_file:
with open(template_vars_file, 'r') as handle:
for line in handle:
@@ -101,7 +110,7 @@ def load_template_vars(template_vars=None, template_vars_file=None):
return res
-def get_template_vars(options: Values) -> Dict[str, Any]:
+def get_template_vars(options: Values, flow_file) -> Dict[str, Any]:
"""Convienence wrapper for ``load_template_vars``.
Args:
diff --git a/cylc/flow/workflow_files.py b/cylc/flow/workflow_files.py
index e96cf662521..cd40fc7a650 100644
--- a/cylc/flow/workflow_files.py
+++ b/cylc/flow/workflow_files.py
@@ -463,7 +463,9 @@ def _is_process_running(
return cli_format(process['cmdline']) == command
-def detect_old_contact_file(reg: str, contact_data=None) -> None:
+def detect_old_contact_file(
+ reg: str, contact_data=None, quiet=False
+) -> None:
"""Check if the workflow process is still running.
As a side-effect this should detect and rectify the situation
@@ -479,6 +481,10 @@ def detect_old_contact_file(reg: str, contact_data=None) -> None:
Args:
reg: workflow name
+ contact_date:
+ quiet: Controls whether to return already running message -
+ this is not required if Cylc VRO is using this function to
+ decide whether to resume or reload.
Raises:
CylcError:
@@ -514,6 +520,8 @@ def detect_old_contact_file(reg: str, contact_data=None) -> None:
fname = get_contact_file_path(reg)
if process_is_running:
# ... the process is running, raise an exception
+ if quiet:
+ raise ServiceFileError()
raise ServiceFileError(
CONTACT_FILE_EXISTS_MSG % {
"host": old_host,
diff --git a/setup.cfg b/setup.cfg
index f6ee306837f..4acd9a3b0ba 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -200,6 +200,7 @@ cylc.command =
validate = cylc.flow.scripts.validate:main
view = cylc.flow.scripts.view:main
vip = cylc.flow.scripts.validate_install_play:main
+ vro = cylc.flow.scripts.validate_reinstall_reload:main
# async functions to run within the scheduler main loop
cylc.main_loop =
health_check = cylc.flow.main_loop.health_check
diff --git a/tests/functional/cylc-combination-scripts/01-vro-reload.t b/tests/functional/cylc-combination-scripts/01-vro-reload.t
new file mode 100644
index 00000000000..38fcba2c27c
--- /dev/null
+++ b/tests/functional/cylc-combination-scripts/01-vro-reload.t
@@ -0,0 +1,53 @@
+#!/usr/bin/env bash
+# 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 vro` (Validate Reinstall relOad)
+# In this case the target workflow is running so cylc reload is run.
+
+. "$(dirname "$0")/test_header"
+set_test_number 6
+
+
+# Setup (Must be a running workflow, note the unusual absence of --no-detach)
+WORKFLOW_NAME="cylctb-x$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c6)"
+cp "${TEST_SOURCE_DIR}/vro_workflow/flow.cylc" .
+run_ok "setup (vip)" \
+ cylc vip --debug \
+ --workflow-name "${WORKFLOW_NAME}" \
+ --no-run-name
+export WORKFLOW_RUN_DIR="${RUN_DIR}/${WORKFLOW_NAME}"
+poll_workflow_running
+
+
+# It validates and reloads:
+
+# Add allow implicit tasks our flow.cylc so that reload will validate:
+sed -i 's@P1Y@P5Y@' flow.cylc
+run_ok "${TEST_NAME_BASE}-runs" cylc vro "${WORKFLOW_NAME}"
+
+# Grep for VRO reporting revalidation, reinstallation and reloading
+grep "\$" "${TEST_NAME_BASE}-runs.stdout" > VIPOUT.txt
+named_grep_ok "${TEST_NAME_BASE}-it-revalidated" "$ cylc validate --against-source" "VIPOUT.txt"
+named_grep_ok "${TEST_NAME_BASE}-it-installed" "$ cylc reinstall" "VIPOUT.txt"
+named_grep_ok "${TEST_NAME_BASE}-it-reloaded" "$ cylc reload" "VIPOUT.txt"
+
+
+# Clean Up.
+run_ok "teardown (stop workflow)" cylc stop "${WORKFLOW_NAME}" --now --now
+purge
+exit 0
diff --git a/tests/functional/cylc-combination-scripts/02-vro-restart.t b/tests/functional/cylc-combination-scripts/02-vro-restart.t
new file mode 100644
index 00000000000..66b537cbe73
--- /dev/null
+++ b/tests/functional/cylc-combination-scripts/02-vro-restart.t
@@ -0,0 +1,54 @@
+#!/usr/bin/env bash
+# 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 vro` (Validate Reinstall restart)
+# In this case the target workflow is stopped so cylc play is run.
+
+
+. "$(dirname "$0")/test_header"
+set_test_number 6
+
+# Setup
+WORKFLOW_NAME="cylctb-x$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c6)"
+cp "${TEST_SOURCE_DIR}/vro_workflow/flow.cylc" .
+run_ok "setup (vip)" \
+ cylc vip --debug \
+ --workflow-name "${WORKFLOW_NAME}" \
+ --no-run-name \
+# Get the workflow into a stopped state
+cylc stop --now --now "${WORKFLOW_NAME}"
+export WORKFLOW_RUN_DIR="${RUN_DIR}/${WORKFLOW_NAME}"
+poll_workflow_stopped
+
+# It validates and restarts:
+
+# Change source workflow and run vro:
+sed -i 's@P1Y@P5Y@' flow.cylc
+run_ok "${TEST_NAME_BASE}-runs" cylc vro "${WORKFLOW_NAME}"
+
+# Grep for VRO reporting revalidation, reinstallation and playing the workflow.
+grep "\$" "${TEST_NAME_BASE}-runs.stdout" > VIPOUT.txt
+named_grep_ok "${TEST_NAME_BASE}-it-revalidated" "$ cylc validate --against-source" "VIPOUT.txt"
+named_grep_ok "${TEST_NAME_BASE}-it-installed" "$ cylc reinstall" "VIPOUT.txt"
+named_grep_ok "${TEST_NAME_BASE}-it-played" "$ cylc play" "VIPOUT.txt"
+
+
+# Clean Up.
+run_ok "teardown (stop workflow)" cylc stop "${WORKFLOW_NAME}" --now --now
+purge
+exit 0
diff --git a/tests/functional/cylc-combination-scripts/03-vro-resume.t b/tests/functional/cylc-combination-scripts/03-vro-resume.t
new file mode 100644
index 00000000000..4a1eb0916d2
--- /dev/null
+++ b/tests/functional/cylc-combination-scripts/03-vro-resume.t
@@ -0,0 +1,56 @@
+#!/usr/bin/env bash
+# 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 vro` (Validate Reinstall restart)
+# In this case the target workflow is paused so cylc reload & cylc play are run.
+
+. "$(dirname "$0")/test_header"
+set_test_number 6
+
+# Setup
+WORKFLOW_NAME="cylctb-x$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c6)"
+cp "${TEST_SOURCE_DIR}/vro_workflow/flow.cylc" .
+run_ok "setup (vip)" \
+ cylc vip --debug \
+ --workflow-name "${WORKFLOW_NAME}" \
+ --no-run-name \
+# Get the workflow into a paused state
+cylc pause "${WORKFLOW_NAME}"
+
+while [[ ! -n $(cylc scan --name "${WORKFLOW_NAME}" --states=paused) ]]; do
+ sleep 1
+done
+
+
+# It validates, reloads and resumes:
+
+# Change source workflow and run vro:
+sed -i 's@P1Y@P5Y@' flow.cylc
+run_ok "${TEST_NAME_BASE}-runs" cylc vro "${WORKFLOW_NAME}"
+
+# Grep for reporting of revalidation, reinstallation, reloading and playing:
+grep "\$" "${TEST_NAME_BASE}-runs.stdout" > VIPOUT.txt
+named_grep_ok "${TEST_NAME_BASE}-it-revalidated" "$ cylc validate --against-source" "VIPOUT.txt"
+named_grep_ok "${TEST_NAME_BASE}-it-installed" "$ cylc reinstall" "VIPOUT.txt"
+named_grep_ok "${TEST_NAME_BASE}-it-reloaded" "$ cylc reload" "VIPOUT.txt"
+
+
+# Clean Up:
+run_ok "teardown (stop workflow)" cylc stop "${WORKFLOW_NAME}" --now --now
+purge
+exit 0
diff --git a/tests/functional/cylc-combination-scripts/04-vro-fail-validate.t b/tests/functional/cylc-combination-scripts/04-vro-fail-validate.t
new file mode 100644
index 00000000000..42fb459d913
--- /dev/null
+++ b/tests/functional/cylc-combination-scripts/04-vro-fail-validate.t
@@ -0,0 +1,53 @@
+#!/usr/bin/env bash
+# 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 vro` (Validate Reinstall restart)
+# Changes to the source cause VRO to bail on validation.
+
+. "$(dirname "$0")/test_header"
+set_test_number 5
+
+# Setup
+WORKFLOW_NAME="cylctb-x$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c6)"
+cp "${TEST_SOURCE_DIR}/vro_workflow/flow.cylc" .
+run_ok "setup (vip)" \
+ cylc vip --debug \
+ --workflow-name "${WORKFLOW_NAME}" \
+ --no-run-name \
+
+
+# Change source workflow and run vro:
+
+# Cut the runtime section out of the source flow.
+cat flow.cylc | head -n 5 > tmp
+cat tmp > flow.cylc
+
+TEST_NAME="${TEST_NAME_BASE}"
+run_fail "${TEST_NAME}" cylc vro "${WORKFLOW_NAME}"
+
+# Grep for reporting of revalidation, reinstallation, reloading and playing:
+named_grep_ok "${TEST_NAME_BASE}-it-tried" \
+ "$ cylc validate --against-source" "${TEST_NAME}.stdout"
+named_grep_ok "${TEST_NAME_BASE}-it-failed" \
+ "WorkflowConfigError" "${TEST_NAME}.stderr"
+
+
+# Clean Up:
+run_ok "teardown (stop workflow)" cylc stop "${WORKFLOW_NAME}" --now --now
+purge
+exit 0
diff --git a/tests/functional/cylc-combination-scripts/05-vro-fail-is-running.t b/tests/functional/cylc-combination-scripts/05-vro-fail-is-running.t
new file mode 100644
index 00000000000..12c40eb33cc
--- /dev/null
+++ b/tests/functional/cylc-combination-scripts/05-vro-fail-is-running.t
@@ -0,0 +1,58 @@
+#!/usr/bin/env bash
+# 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 vro` (Validate Reinstall restart)
+# In this case the target workflow is in an abiguous state: We cannot tell
+# Whether it's running, paused or stopped. Cylc VRO should validate before
+# reinstall:
+
+. "$(dirname "$0")/test_header"
+set_test_number 4
+
+create_test_global_config "" """
+[scheduler]
+ [[main loop]]
+ plugins = reset bad hosts
+"""
+
+# Setup
+WORKFLOW_NAME="cylctb-x$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c6)"
+cp "${TEST_SOURCE_DIR}/vro_workflow/flow.cylc" .
+run_ok "setup (vip)" \
+ cylc vip --debug \
+ --workflow-name "${WORKFLOW_NAME}" \
+ --no-run-name
+# Get the workflow into an unreachable state
+
+CONTACTFILE="${RUN_DIR}/${WORKFLOW_NAME}/.service/contact"
+
+sed -i 's@CYLC_WORKFLOW_HOST=.*@CYLC_WORKFLOW_HOST=elephantshrew@' "${CONTACTFILE}"
+
+
+# It can't figure out whether the workflow is running:
+
+# Change source workflow and run vro:
+run_fail "${TEST_NAME_BASE}-runs" cylc vro "${WORKFLOW_NAME}"
+
+grep_ok "on elephantshrew." "${TEST_NAME_BASE}-runs.stderr"
+
+# Clean Up:
+sed -i "s@CYLC_WORKFLOW_HOST=elephantshrew@CYLC_WORKFLOW_HOST=$HOSTNAME@" "${CONTACTFILE}"
+run_ok "teardown (stop workflow)" cylc stop "${WORKFLOW_NAME}" --now --now
+purge
+exit 0
diff --git a/tests/functional/cylc-combination-scripts/vro_workflow/flow.cylc b/tests/functional/cylc-combination-scripts/vro_workflow/flow.cylc
new file mode 100644
index 00000000000..bda3da45ff3
--- /dev/null
+++ b/tests/functional/cylc-combination-scripts/vro_workflow/flow.cylc
@@ -0,0 +1,8 @@
+[scheduling]
+ initial cycle point = 1500
+ [[graph]]
+ P1Y = foo
+
+[runtime]
+ [[foo]]
+ script = sleep 500
\ No newline at end of file
diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py
index 65fdab9713a..c2ace181ede 100644
--- a/tests/integration/conftest.py
+++ b/tests/integration/conftest.py
@@ -149,6 +149,12 @@ def flow(run_dir, test_dir):
yield partial(_make_flow, run_dir, test_dir)
+@pytest.fixture
+def flow_src(tmp_path):
+ """A function for creating function-level flows."""
+ yield partial(_make_src_flow, tmp_path)
+
+
@pytest.fixture(scope='module')
def mod_scheduler():
"""Return a Scheduler object for a flow.
diff --git a/tests/unit/scripts/test_validate.py b/tests/unit/scripts/test_validate.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/tests/unit/test_get_old_tvars_units.py b/tests/unit/test_get_old_tvars_units.py
new file mode 100644
index 00000000000..29d6cc85b8d
--- /dev/null
+++ b/tests/unit/test_get_old_tvars_units.py
@@ -0,0 +1,65 @@
+# 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 .
+
+
+from cylc.flow.templatevars import OldTemplateVars
+import sqlite3
+import pytest
+
+
+@pytest.fixture(scope='module')
+def _setup_db(tmp_path_factory):
+ tmp_path = tmp_path_factory.mktemp('test_get_old_tvars')
+ logfolder = tmp_path / "log/"
+ logfolder.mkdir()
+ db_path = logfolder / 'db'
+ conn = sqlite3.connect(db_path)
+ conn.execute(
+ r'''
+ CREATE TABLE workflow_template_vars (
+ key,
+ value
+ )
+ '''
+ )
+ conn.execute(
+ r'''
+ INSERT INTO workflow_template_vars
+ VALUES
+ ("FOO", "42"),
+ ("BAR", "'hello world'"),
+ ("BAZ", "'foo', 'bar', 48"),
+ ("QUX", "['foo', 'bar', 21]")
+ '''
+ )
+ conn.commit()
+ conn.close()
+ yield OldTemplateVars(tmp_path)
+
+
+@pytest.mark.parametrize(
+ 'key, expect',
+ (
+ ('FOO', 42),
+ ('BAR', 'hello world'),
+ ('BAZ', ('foo', 'bar', 48)),
+ ('QUX', ['foo', 'bar', 21])
+ )
+)
+def test_OldTemplateVars(key, expect, _setup_db):
+ """It can extract a variety of items from a workflow database.
+ """
+ assert _setup_db.template_vars[key] == expect
diff --git a/tests/unit/test_pathutil.py b/tests/unit/test_pathutil.py
index d42b26a2dbe..14bf06c46d6 100644
--- a/tests/unit/test_pathutil.py
+++ b/tests/unit/test_pathutil.py
@@ -31,6 +31,7 @@
get_next_rundir_number,
get_remote_workflow_run_dir,
get_remote_workflow_run_job_dir,
+ get_source_conf_from_id,
get_workflow_run_dir,
get_workflow_run_job_dir,
get_workflow_run_scheduler_log_dir,
@@ -576,3 +577,43 @@ def test_get_workflow_name_from_id(
result = get_workflow_name_from_id(id_)
assert result == name
+
+
+@pytest.fixture
+def _setup_get_source_conf_from_id(tmp_path, monkeypatch):
+ run = tmp_path / 'cylc-run/run'
+ src = tmp_path / 'cylc-src/src'
+ src.mkdir(parents=True)
+ (run / '_cylc-install').mkdir(parents=True)
+ (run / '_cylc-install/source').symlink_to(src)
+ monkeypatch.setattr(
+ 'cylc.flow.pathutil.get_workflow_run_dir',
+ lambda workflow_id: tmp_path / 'cylc-run/run'
+ )
+ monkeypatch.setattr(
+ 'cylc.flow.pathutil.get_cylc_run_dir',
+ lambda: tmp_path / 'cylc-run'
+ )
+ yield tmp_path
+
+
+@pytest.mark.parametrize(
+ 'conf_file',
+ (
+ param('flow.cylc', id='flow.cylc'),
+ param('suite.rc', id='flow.cylc'),
+ param(None, id='no file'),
+ )
+)
+def test_get_source_conf_from_id(
+ _setup_get_source_conf_from_id, conf_file
+):
+ """It locates a flow.cylc, suite.rc from a run dir, or fails nicely.
+ """
+ if conf_file:
+ expect = _setup_get_source_conf_from_id / f'cylc-src/src/{conf_file}'
+ expect.touch()
+ assert get_source_conf_from_id('run') == expect
+ else:
+ with pytest.raises(WorkflowFilesError):
+ get_source_conf_from_id('run')