Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve rose stem coverage #181

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 21 additions & 15 deletions cylc/rose/stem.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,9 @@ class RoseStemVersionException(Exception):
def __init__(self, version):
Exception.__init__(self, version)
if version is None:
self.suite_version = "not rose-stem compatible"
self.suite_version = (
"does not have ROSE_VERSION set in the rose-suite.conf"
)
else:
self.suite_version = "at version %s" % (version)

Expand Down Expand Up @@ -225,32 +227,40 @@ class StemRunner:

def __init__(self, opts, reporter=None, popen=None, fs_util=None):
self.opts = opts

if reporter is None:
self.reporter = Reporter(opts.verbosity - opts.quietness)
else:
self.reporter = reporter

if popen is None:
self.popen = RosePopener(event_handler=self.reporter)
else:
self.popen = popen

if fs_util is None:
self.fs_util = FileSystemUtil(event_handler=self.reporter)
else:
self.fs_util = fs_util

self.host_selector = HostSelector(event_handler=self.reporter,
popen=self.popen)

def _add_define_option(self, var, val):
"""Add a define option passed to the SuiteRunner."""
"""Add a define option passed to the SuiteRunner.

Args:
var: Name of variable to set
val: Value of variable to set
"""
if self.opts.defines:
self.opts.defines.append(SUITE_RC_PREFIX + var + '=' + val)
else:
self.opts.defines = [SUITE_RC_PREFIX + var + '=' + val]
self.reporter(ConfigVariableSetEvent(var, val))
return

def _get_base_dir(self, item):
def _get_fcm_loc_layout_info(self, src_tree):
"""Given a source tree return the following from 'fcm loc-layout':
* url
* sub_tree
Expand All @@ -259,15 +269,17 @@ def _get_base_dir(self, item):
* project
"""

ret_code, output, stderr = self.popen.run('fcm', 'loc-layout', item)
ret_code, output, stderr = self.popen.run(
'fcm', 'loc-layout', src_tree)
if ret_code != 0:
raise ProjectNotFoundException(item, stderr)
raise ProjectNotFoundException(src_tree, stderr)

ret = {}
for line in output.splitlines():
if ":" not in line:
continue
key, value = line.split(":", 1)

if key and value:
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dpmatthews - This is splitting lines from fcm loc-layout. Will it ever be false?

ret[key] = value.strip()

Expand All @@ -289,7 +301,8 @@ def _get_project_from_url(self, source_dict):
break
return project

def _deduce_mirror(self, source_dict, project):
@staticmethod
def _deduce_mirror(source_dict, project):
"""Deduce the mirror location of this source tree."""

# Root location for project
Expand Down Expand Up @@ -331,7 +344,7 @@ def _ascertain_project(self, item):
print(f"[WARN] Forcing project for '{item}' to be '{project}'")
return project, item, item, '', ''

source_dict = self._get_base_dir(item)
source_dict = self._get_fcm_loc_layout_info(item)
project = self._get_project_from_url(source_dict)
if not project:
raise ProjectNotFoundException(item)
Expand Down Expand Up @@ -587,10 +600,7 @@ def rose_stem(parser, opts):
opts = StemRunner(opts).process()

# call cylc install
if hasattr(opts, 'source'):
cylc_install(parser, opts, opts.source)
else:
cylc_install(parser, opts)
cylc_install(parser, opts, opts.source)

except CylcError as exc:
if opts.verbosity > 1:
Expand All @@ -602,7 +612,3 @@ def rose_stem(parser, opts):
),
file=sys.stderr
)


if __name__ == "__main__":
main()
18 changes: 18 additions & 0 deletions tests/functional/test_rose_stem.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,24 @@ def _inner_fn(rose_stem_opts, verbosity=verbosity):
yield _inner_fn


@pytest.fixture(scope='class')
def rose_stem_run_really_basic(rose_stem_run_template, setup_stem_repo):
rose_stem_opts = {
'stem_groups': [],
'stem_sources': [
str(setup_stem_repo['workingcopy']), "fcm:foo.x_tr@head"
],
}
yield rose_stem_run_template(rose_stem_opts)


class TestReallyBasic():
def test_really_basic(self, rose_stem_run_really_basic):
"""Check that assorted variables have been exported.
"""
assert rose_stem_run_really_basic['run_stem'].returncode == 0


@pytest.fixture(scope='class')
def rose_stem_run_basic(rose_stem_run_template, setup_stem_repo):
rose_stem_opts = {
Expand Down
239 changes: 238 additions & 1 deletion tests/unit/test_rose_stem_units.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,30 @@
"""Functional tests for top-level function record_cylc_install_options and
"""

import cylc
import pytest
from pytest import param
from types import SimpleNamespace

from cylc.rose.stem import get_source_opt_from_args
from cylc.rose.stem import (
ProjectNotFoundException,
RoseStemVersionException,
RoseSuiteConfNotFoundException,
StemRunner, SUITE_RC_PREFIX,
get_source_opt_from_args
)

from metomi.rose.reporter import Reporter
from metomi.rose.popen import RosePopener
from metomi.rose.fs_util import FileSystemUtil


class MockPopen:
def __init__(self, mocked_return):
self.mocked_return = mocked_return

def run(self, *args):
return self.mocked_return


@pytest.mark.parametrize(
Expand Down Expand Up @@ -54,3 +74,220 @@ def test_get_source_opt_from_args(tmp_path, monkeypatch, args, expect):
assert result == expect
else:
assert result == expect.format(tmp_path=str(tmp_path))


@pytest.fixture
def get_StemRunner():
def _inner(kwargs, options=None):
if options is None:
options = {}
"""Create a StemRunner objects with some options set."""
opts = SimpleNamespace(verbosity=1, quietness=1, **options)
stemrunner = StemRunner(opts, **kwargs)
return stemrunner
return _inner


def test_StemRunner_init_kwargs_set(get_StemRunner):
"""It handles __init__ with different kwargs."""
stemrunner = get_StemRunner({
'reporter': 'foo', 'popen': 'foo', 'fs_util': 'foo'
})
assert isinstance(stemrunner.reporter, str)
assert isinstance(stemrunner.popen, str)
assert isinstance(stemrunner.popen, str)


def test_StemRunner_init_defaults(get_StemRunner):
"""It handles __init__ with different kwargs."""
stemrunner = get_StemRunner({})
assert isinstance(stemrunner.reporter, Reporter)
assert isinstance(stemrunner.popen, RosePopener)
assert isinstance(stemrunner.fs_util, FileSystemUtil)


@pytest.mark.parametrize(
'exisiting_defines',
[
param([], id='no existing defines'),
param(['opts=(cylc-install)'], id='existing defines')
]
)
def test__add_define_option(get_StemRunner, capsys, exisiting_defines):
"""It adds to defines, rather than replacing any."""
stemrunner = get_StemRunner(
{'reporter': print}, {'defines': exisiting_defines})
assert stemrunner._add_define_option('FOO', '"bar"') is None
assert f'{SUITE_RC_PREFIX}FOO="bar"' in stemrunner.opts.defines
assert 'Variable FOO set to "bar"' in capsys.readouterr().out


@pytest.mark.parametrize(
'mocked_return',
[
param((1, 'foo', 'SomeError'), id='it fails if fcm-loc-layout fails'),
param(
(
0,
'url: file:///worthwhile/foo/bar/baz/trunk@1\n'
'project: \n'
'some waffle which ought to be ignored',
''
),
id='Good fcm output'
)

]
)
def test__get_fcm_loc_layout_info(get_StemRunner, capsys, mocked_return):
"""It parses information from fcm loc layout"""

stemrunner = get_StemRunner({'popen': MockPopen(mocked_return)})

if mocked_return[0] == 0:
expect = {
'url': 'file:///worthwhile/foo/bar/baz/trunk@1',
'project': ''
}
assert expect == stemrunner._get_fcm_loc_layout_info('foo')
else:
with pytest.raises(ProjectNotFoundException) as exc:
stemrunner._get_fcm_loc_layout_info('foo')
assert mocked_return[2] in str(exc.value)


@pytest.mark.parametrize(
'source_dict, mockreturn, expect',
[
param(
{
'root': 'svn://subversive',
'project': 'waltheof',
'url': 'Irrelevent, it\'s mocked away, but required.'
},
(
0,
(
"location{primary}[mortimer] = "
"svn://subversive/rogermortimer\n"
"location{primary}[fenwick] = "
"svn://subversive/johnfenwick\n"
"location{primary}[waltheof] = "
"svn://subversive/waltheof\n"
),
),
'waltheof',
id='all paths true'
),
param(
{
'root': 'svn://subversive',
'project': 'waltheof',
'url': 'Irrelevent, it\'s mocked away, but required.'
},
(0, "location{primary} = svn://subversive/waltheof\n"),
None,
id='no kp result'
)
]
)
def test__get_project_from_url(
get_StemRunner, source_dict, mockreturn, expect
):
stemrunner = get_StemRunner({'popen': MockPopen(mockreturn)})
project = stemrunner._get_project_from_url(source_dict)
assert project == expect


@pytest.mark.parametrize(
'source, expect',
(
(None, 'cwd'),
('foo/bar', 'some_dir'),
)
)
def test__generate_name(get_StemRunner, monkeypatch, tmp_path, source, expect):
"""It generates a name if StemRunner._ascertain_project fails.

(This happens if the workflow source is not controlled with FCM)
"""
monkeypatch.chdir(tmp_path)

# Case: we've set source:
source = (tmp_path / source / expect) if expect == 'some_dir' else None
# Case: we've not set source:
expect = tmp_path.name if expect == 'cwd' else expect

stemrunner = get_StemRunner({}, {'source': source})
assert stemrunner._generate_name() == expect


@pytest.mark.parametrize(
'stem_sources, expect',
(
('given', True),
('given', False),
('infer', True),
('infer', False),
)
)
def test__this_suite(
get_StemRunner, monkeypatch, tmp_path, stem_sources, expect
):
"""It returns a sensible suite-dir."""
stem_suite_subdir = tmp_path / 'rose-stem'
stem_suite_subdir.mkdir()

if stem_sources == 'infer':
stem_sources = []
monkeypatch.setattr(
cylc.rose.stem.StemRunner,
'_ascertain_project',
lambda x, y: [0, str(tmp_path)]
)
else:
stem_sources = [tmp_path]

if expect:
(stem_suite_subdir / 'rose-suite.conf').write_text(
'ROSE_STEM_VERSION=1')
stemrunner = get_StemRunner({}, {'stem_sources': stem_sources})
assert stemrunner._this_suite() == str(stem_suite_subdir)
else:
stemrunner = get_StemRunner({}, {'stem_sources': stem_sources})
with pytest.raises(RoseSuiteConfNotFoundException):
stemrunner._this_suite()


def test__check_suite_version_fails_if_no_stem_source(
get_StemRunner, tmp_path
):
"""It fails if path of first stem source is not a file"""
stemrunner = get_StemRunner(
{}, {'stem_sources': str(tmp_path), 'source': None})
stem_suite_subdir = tmp_path / 'rose-stem'
stem_suite_subdir.mkdir()
with pytest.raises(RoseSuiteConfNotFoundException, match='^\nCannot'):
stemrunner._check_suite_version(str(tmp_path))


def test__check_suite_version_incompatible(get_StemRunner, tmp_path):
"""It fails if path of first stem source is not a file"""
(tmp_path / 'rose-suite.conf').write_text('')
stemrunner = get_StemRunner(
{}, {'stem_sources': [], 'source': str(tmp_path)})
with pytest.raises(
RoseStemVersionException, match='ROSE_VERSION'
):
stemrunner._check_suite_version(str(tmp_path / 'rose-suite.conf'))


def test__deduce_mirror():
source_dict = {
'root': 'svn://lab/spaniel.xm',
'project': 'myproject.xm',
'url': 'svn://lab/spaniel.xm/myproject/trunk@123',
'sub_tree': 'foo'
}
project = 'someproject'
StemRunner._deduce_mirror(source_dict, project)