Skip to content

Commit

Permalink
improve test coverage (#227)
Browse files Browse the repository at this point in the history
Improve test coverage
Co-authored-by: Oliver Sanders <[email protected]>
  • Loading branch information
wxtim authored Oct 23, 2023
1 parent 2a0963a commit 08d8e11
Show file tree
Hide file tree
Showing 7 changed files with 188 additions and 90 deletions.
17 changes: 17 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,20 @@ source=./cylc

[report]
precision=2

exclude_lines =
pragma: no cover

# Don't complain if tests don't hit defensive assertion code:
raise NotImplementedError
return NotImplemented

# Ignore type checking code:
if (typing\.)?TYPE_CHECKING:
@overload( |$)

# Don't complain about ellipsis (exception classes, typing overloads etc):
\.\.\.

# Ignore abstract methods
@(abc\.)?abstractmethod
7 changes: 2 additions & 5 deletions cylc/rose/entry_points.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,9 +177,7 @@ def record_cylc_install_options(
"""
# Create a config based on command line options:
cli_config = get_cli_opts_node(opts, srcdir)
# Leave now if there is nothing to do:
if not cli_config:
return False

# raise error if CLI config has multiple templating sections
identify_templating_section(cli_config)

Expand Down Expand Up @@ -218,8 +216,7 @@ def record_cylc_install_options(
dumper.dump(cli_config, str(conf_filepath))

# Merge the opts section of the rose-suite.conf with those set by CLI:
if not rose_conf_filepath.is_file():
rose_conf_filepath.touch()
rose_conf_filepath.touch()
rose_suite_conf = loader.load(str(rose_conf_filepath))
rose_suite_conf = add_cylc_install_to_rose_conf_node_opts(
rose_suite_conf, cli_config
Expand Down
43 changes: 21 additions & 22 deletions cylc/rose/stem.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,21 +101,13 @@ def __repr__(self):
__str__ = __repr__


class ConfigSourceTreeSetEvent(Event):

"""Event to report a source tree for config files."""

LEVEL = Event.V

def __repr__(self):
return "Using config files from source %s" % (self.args[0])

__str__ = __repr__


class NameSetEvent(Event):

"""Event to report a name for the suite being set."""
"""Event to report a name for the suite being set.
Simple parser of output expected to be in the format:
Key: Value.
"""

LEVEL = Event.V

Expand Down Expand Up @@ -437,6 +429,21 @@ def _prepend_localhost(self, url):
url = self.host_selector.get_local_host() + ':' + url
return url

def _parse_auto_opts(self):
"""Load the site config file and return any automatic-options.
Parse options in the form of a space separated list of key=value
pairs.
"""
auto_opts = self._read_auto_opts()
if auto_opts:
automatic_options = auto_opts.split()
for option in automatic_options:
elements = option.split("=")
if len(elements) == 2:
self._add_define_option(
elements[0], '"' + elements[1] + '"')

def process(self):
"""Process STEM options into 'rose suite-run' options."""
# Generate options for source trees
Expand Down Expand Up @@ -496,15 +503,7 @@ def process(self):
self.opts.defines.append(
f"{self.template_section}RUN_NAMES={str(expanded_groups)}")

# Load the config file and return any automatic-options
auto_opts = self._read_auto_opts()
if auto_opts:
automatic_options = auto_opts.split()
for option in automatic_options:
elements = option.split("=")
if len(elements) == 2:
self._add_define_option(
elements[0], '"' + elements[1] + '"')
self._parse_auto_opts()

# Change into the suite directory
if getattr(self.opts, 'workflow_conf_dir', None):
Expand Down
89 changes: 38 additions & 51 deletions cylc/rose/utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,6 @@ def get_rose_vars_from_config_node(config, config_node, environ):
Dictionary of environment variables
"""
templating = None

# Don't allow multiple templating sections.
templating = identify_templating_section(config_node)

Expand Down Expand Up @@ -130,54 +128,43 @@ def get_rose_vars_from_config_node(config, config_node, environ):

# For each of the template language sections extract items to a simple
# dict to be returned.
if 'env' in config_node.value:

config['env'] = {
item[0][1]: item[1].value for item in
config_node.value['env'].walk()
if item[1].state == ConfigNode.STATE_NORMAL
}
if templating in config_node.value:
config['template_variables'] = {
item[0][1]: item[1].value for item in
config_node.value[templating].walk()
if item[1].state == ConfigNode.STATE_NORMAL
}
elif 'template variables' in config_node.value:
config['template_variables'] = {
item[0][1]: item[1].value for item in
config_node.value['template variables'].walk()
if item[1].state == ConfigNode.STATE_NORMAL
}
config['env'] = {
item[0][1]: item[1].value for item in
config_node.value['env'].walk()
if item[1].state == ConfigNode.STATE_NORMAL
}
config['template_variables'] = {
item[0][1]: item[1].value for item in
config_node.value[templating].walk()
if item[1].state == ConfigNode.STATE_NORMAL
}

# Add the entire config to ROSE_SUITE_VARIABLES to allow for programatic
# access.
if templating is not None:
with patch_jinja2_leading_zeros():
# BACK COMPAT: patch_jinja2_leading_zeros
# back support zero-padded integers for a limited time to help
# users migrate before upgrading cylc-flow to Jinja2>=3.1
parser = Parser()
for key, value in config['template_variables'].items():
# The special variables are already Python variables.
if key not in ['ROSE_ORIG_HOST', 'ROSE_VERSION', 'ROSE_SITE']:
try:
config['template_variables'][key] = (
parser.literal_eval(value)
)
except Exception:
raise ConfigProcessError(
[templating, key],
value,
f'Invalid template variable: {value}'
'\nMust be a valid Python or Jinja2 literal'
' (note strings "must be quoted").'
) from None
with patch_jinja2_leading_zeros():
# BACK COMPAT: patch_jinja2_leading_zeros
# back support zero-padded integers for a limited time to help
# users migrate before upgrading cylc-flow to Jinja2>=3.1
parser = Parser()
for key, value in config['template_variables'].items():
# The special variables are already Python variables.
if key not in ['ROSE_ORIG_HOST', 'ROSE_VERSION', 'ROSE_SITE']:
try:
config['template_variables'][key] = (
parser.literal_eval(value)
)
except Exception:
raise ConfigProcessError(
[templating, key],
value,
f'Invalid template variable: {value}'
'\nMust be a valid Python or Jinja2 literal'
' (note strings "must be quoted").'
) from None

# Add ROSE_SUITE_VARIABLES to config of templating engines in use.
if templating is not None:
config['template_variables'][
'ROSE_SUITE_VARIABLES'] = config['template_variables']
config['template_variables'][
'ROSE_SUITE_VARIABLES'] = config['template_variables']


def identify_templating_section(config_node):
Expand Down Expand Up @@ -246,7 +233,7 @@ def rose_config_tree_loader(srcdir=None, opts=None):
if opts and 'opt_conf_keys' in dir(opts) and opts.opt_conf_keys:
if isinstance(opts.opt_conf_keys, str):
opt_conf_keys += opts.opt_conf_keys.split()
elif isinstance(opts.opt_conf_keys, list):
else:
opt_conf_keys += opts.opt_conf_keys

# Optional definitions
Expand Down Expand Up @@ -430,6 +417,9 @@ def get_cli_opts_node(opts=None, srcdir=None):

# Construct new config node representing CLI config items:
newconfig = ConfigNode()
newconfig.set(['opts'], ConfigNode())

# For each __define__ determine whether it is an env or template define.
for define in defines:
parsed_define = parse_cli_defines(define)
if parsed_define:
Expand All @@ -454,9 +444,7 @@ def get_cli_opts_node(opts=None, srcdir=None):
)

# Specialised treatement of optional configs.
if 'opts' not in newconfig:
newconfig['opts'] = ConfigNode()
newconfig['opts'].value = ''
newconfig['opts'].value = ''
newconfig['opts'].value = merge_opts(newconfig, opt_conf_keys)
newconfig['opts'].state = '!'

Expand Down Expand Up @@ -539,8 +527,7 @@ def merge_opts(config, opt_conf_keys):
'aleph bet gimmel'
"""
all_opt_conf_keys = []
if 'opts' in config:
all_opt_conf_keys.append(config['opts'].value)
all_opt_conf_keys.append(config['opts'].value)
if "ROSE_SUITE_OPT_CONF_KEYS" in os.environ:
all_opt_conf_keys.append(os.environ["ROSE_SUITE_OPT_CONF_KEYS"])
if opt_conf_keys and isinstance(opt_conf_keys, str):
Expand Down
39 changes: 32 additions & 7 deletions tests/unit/test_functional_post_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,18 @@

from metomi.isodatetime.datetimeoper import DateTimeOperator

import cylc
from cylc.flow.hostuserutil import get_host
from cylc.rose.entry_points import (
record_cylc_install_options, rose_fileinstall, post_install
record_cylc_install_options, rose_fileinstall, post_install,
copy_config_file
)
from cylc.rose.utilities import (
ROSE_ORIG_HOST_INSTALLED_OVERRIDE_STRING,
MultipleTemplatingEnginesError
)
from metomi.rose.config import ConfigLoader
from metomi.rose.config_tree import ConfigTree


HOST = get_host()
Expand Down Expand Up @@ -346,17 +349,39 @@ def test_template_section_conflict(


def test_rose_fileinstall_exception(tmp_path, monkeypatch):
def broken():
raise FileNotFoundError('Any Old Error')
import os
monkeypatch.setattr(os, 'getcwd', broken)
(tmp_path / 'rose-suite.conf').touch()
"""It throws an exception if you try to install files to a non existent
destination.
(And returns to the directory you started at)
"""
def fakenode(_, __):
tree = ConfigTree()
tree.node.value = {'file': ''}
return tree

monkeypatch.setattr(
cylc.rose.entry_points, 'rose_config_tree_loader',
fakenode
)
monkeypatch.setattr(
cylc.rose.entry_points, "rose_config_exists", lambda x, y: True)
with pytest.raises(FileNotFoundError):
rose_fileinstall(srcdir=tmp_path, rundir=tmp_path)
rose_fileinstall(srcdir=tmp_path, rundir='/oiruhgaqhnaigujhj')


def test_cylc_no_rose(tmp_path):
"""A Cylc workflow that contains no ``rose-suite.conf`` installs OK.
"""
from cylc.rose.entry_points import post_install
assert post_install(srcdir=tmp_path, rundir=tmp_path) is False


def test_copy_config_file_fails():
"""It fails when source or rundir not specified."""
with pytest.raises(FileNotFoundError, match='both source and rundir'):
copy_config_file()


def test_copy_config_file_fails2():
"""It fails if source not a rose suite."""
copy_config_file(srcdir='/foo', rundir='/bar')
3 changes: 1 addition & 2 deletions tests/unit/test_rose_opts.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,7 @@ def test_rose_fileinstall_validate(fixture_provide_flow, cylc_validate_cli):
def test_rose_fileinstall_run(fixture_install_flow):
"""Workflow installs:
"""
_, _, _, result, _ = fixture_install_flow
assert result.ret == 0
pass # this tests the fixture itself


def test_rose_fileinstall_rose_conf(fixture_install_flow):
Expand Down
Loading

0 comments on commit 08d8e11

Please sign in to comment.