Skip to content

Commit

Permalink
ToolRepository now only returns default that are available. Updated t…
Browse files Browse the repository at this point in the history
…ests to make tools as available.
  • Loading branch information
hiker committed Nov 21, 2024
1 parent 6e280d9 commit 6c3f1c2
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 38 deletions.
28 changes: 17 additions & 11 deletions source/fab/tools/tool_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def __init__(self):
# Add the FAB default tools:
# TODO: sort the defaults so that they actually work (since not all
# tools FAB knows about are available). For now, disable Fpp (by not
# adding it). IF someone actually uses it it can added.
# adding it). If someone actually uses it it can added.
for cls in [Craycc, Crayftn,
Gcc, Gfortran,
Icc, Icx, Ifort, Ifx,
Expand Down Expand Up @@ -101,9 +101,10 @@ def __init__(self):

def add_tool(self, tool: Tool):
'''Creates an instance of the specified class and adds it
to the tool repository.
to the tool repository. If the tool is a compiler, it automatically
adds the compiler as a linker as well (named "linker-{tool.name}").
:param cls: the tool to instantiate.
:param tool: the tool to add.
'''

# We do not test if a tool is actually available. The ToolRepository
Expand Down Expand Up @@ -161,15 +162,15 @@ def set_default_compiler_suite(self, suite: str):
def get_default(self, category: Category,
mpi: Optional[bool] = None,
openmp: Optional[bool] = None):
'''Returns the default tool for a given category. For most tools
that will be the first entry in the list of tools. The exception
are compilers and linker: in this case it must be specified if
MPI support is required or not. And the default return will be
'''Returns the default tool for a given category that is available.
For most tools that will be the first entry in the list of tools. The
exception are compilers and linker: in this case it must be specified
if MPI support is required or not. And the default return will be
the first tool that either supports MPI or not.
:param category: the category for which to return the default tool.
:param mpi: if a compiler or linker is required that supports MPI.
:param open: if a compiler or linker is required that supports OpenMP.
:param openmp: if a compiler or linker is required that supports OpenMP.
:raises KeyError: if the category does not exist.
:raises RuntimeError: if no compiler/linker is found with the
Expand All @@ -182,7 +183,12 @@ def get_default(self, category: Category,

# If not a compiler or linker, return the first tool
if not category.is_compiler and category != Category.LINKER:
return self[category][0]
for tool in self[category]:
if tool.is_available:
return tool
tool_names = ",".join(i.name for i in self[category])
raise RuntimeError(f"Can't find available '{category}' tool. "
f"Tools are '{tool_names}'.")

if not isinstance(mpi, bool):
raise RuntimeError(f"Invalid or missing mpi specification "
Expand All @@ -197,8 +203,8 @@ def get_default(self, category: Category,
# ignore it.
if openmp and not tool.openmp:
continue
# If the tool supports/does not support MPI, return it.
if mpi == tool.mpi:
# If the tool supports/does not support MPI, return the first one
if tool.is_available and mpi == tool.mpi:
return tool

# Don't bother returning an MPI enabled tool if no-MPI is requested -
Expand Down
60 changes: 47 additions & 13 deletions tests/unit_tests/steps/test_grab.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
# For further details please refer to the file COPYRIGHT
# which you should have received as part of this distribution
##############################################################################

'''Test various grab implementation - folders and fcm.
'''

from pathlib import Path
from types import SimpleNamespace
from unittest import mock
Expand All @@ -15,24 +19,37 @@


class TestGrabFolder:
'''Test grab folder functionality.'''

def test_trailing_slash(self):
with pytest.warns(UserWarning, match="_metric_send_conn not set, cannot send metrics"):
self._common(grab_src='/grab/source/', expect_grab_src='/grab/source/')
'''Test folder grabbing with a trailing slash.'''
with pytest.warns(UserWarning, match="_metric_send_conn not set, "
"cannot send metrics"):
self._common(grab_src='/grab/source/',
expect_grab_src='/grab/source/')

def test_no_trailing_slash(self):
with pytest.warns(UserWarning, match="_metric_send_conn not set, cannot send metrics"):
self._common(grab_src='/grab/source', expect_grab_src='/grab/source/')
'''Test folder grabbing without a trailing slash.'''
with pytest.warns(UserWarning, match="_metric_send_conn not set, "
"cannot send metrics"):
self._common(grab_src='/grab/source',
expect_grab_src='/grab/source/')

def _common(self, grab_src, expect_grab_src):
source_root = Path('/workspace/source')
dst = 'bar'

mock_config = SimpleNamespace(source_root=source_root,
tool_box=ToolBox())
# Since is_available calls run, in order to test a single run call,
# we patch is_available to be always true.
with mock.patch('pathlib.Path.mkdir'):
with mock.patch('fab.tools.tool.Tool.run') as mock_run:
grab_folder(mock_config, src=grab_src, dst_label=dst)
with mock.patch(
'fab.tools.tool.Tool.is_available',
new_callable=mock.PropertyMock) as is_available:
is_available.return_value = True
grab_folder(mock_config, src=grab_src, dst_label=dst)

expect_dst = mock_config.source_root / dst
mock_run.assert_called_once_with(
Expand All @@ -41,24 +58,34 @@ def _common(self, grab_src, expect_grab_src):


class TestGrabFcm:
'''Test FCM functionality.'''

def test_no_revision(self):
'''Test FCM without specifying a revision.'''
source_root = Path('/workspace/source')
source_url = '/www.example.com/bar'
dst_label = 'bar'

mock_config = SimpleNamespace(source_root=source_root,
tool_box=ToolBox())
with mock.patch('pathlib.Path.mkdir'):
with mock.patch('fab.tools.tool.Tool.run') as mock_run, \
pytest.warns(UserWarning, match="_metric_send_conn not set, cannot send metrics"):
fcm_export(config=mock_config, src=source_url, dst_label=dst_label)
with mock.patch('fab.tools.tool.Tool.is_available',
new_callable=mock.PropertyMock) as is_available:
is_available.return_value = True
with mock.patch('fab.tools.tool.Tool.run') as mock_run, \
pytest.warns(UserWarning,
match="_metric_send_conn not "
"set, cannot send metrics"):
fcm_export(config=mock_config, src=source_url,
dst_label=dst_label)

mock_run.assert_called_once_with(['export', '--force', source_url,
str(source_root / dst_label)],
env=None, cwd=None, capture_output=True)
env=None, cwd=None,
capture_output=True)

def test_revision(self):
'''Test that the revision is passed on correctly in fcm export.'''
source_root = Path('/workspace/source')
source_url = '/www.example.com/bar'
dst_label = 'bar'
Expand All @@ -67,12 +94,19 @@ def test_revision(self):
mock_config = SimpleNamespace(source_root=source_root,
tool_box=ToolBox())
with mock.patch('pathlib.Path.mkdir'):
with mock.patch('fab.tools.tool.Tool.run') as mock_run, \
pytest.warns(UserWarning, match="_metric_send_conn not set, cannot send metrics"):
fcm_export(mock_config, src=source_url, dst_label=dst_label, revision=revision)
with mock.patch('fab.tools.tool.Tool.is_available',
new_callable=mock.PropertyMock) as is_available:
is_available.return_value = True
with mock.patch('fab.tools.tool.Tool.run') as mock_run, \
pytest.warns(
UserWarning, match="_metric_send_conn not set, "
"cannot send metrics"):
fcm_export(mock_config, src=source_url,
dst_label=dst_label, revision=revision)

mock_run.assert_called_once_with(
['export', '--force', '--revision', '42', f'{source_url}', str(source_root / dst_label)],
['export', '--force', '--revision', '42', f'{source_url}',
str(source_root / dst_label)],
env=None, cwd=None, capture_output=True)

# todo: test missing repo
Expand Down
47 changes: 33 additions & 14 deletions tests/unit_tests/tools/test_tool_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,17 +151,36 @@ def test_tool_repository_default_compiler_suite():
'''Tests the setting of default suite for compiler and linker.'''
tr = ToolRepository()
tr.set_default_compiler_suite("gnu")
for cat in [Category.C_COMPILER, Category.FORTRAN_COMPILER,
Category.LINKER]:
def_tool = tr.get_default(cat, mpi=False, openmp=False)
assert def_tool.suite == "gnu"

tr.set_default_compiler_suite("intel-classic")
for cat in [Category.C_COMPILER, Category.FORTRAN_COMPILER,
Category.LINKER]:
def_tool = tr.get_default(cat, mpi=False, openmp=False)
assert def_tool.suite == "intel-classic"
with pytest.raises(RuntimeError) as err:
tr.set_default_compiler_suite("does-not-exist")
assert ("Cannot find 'FORTRAN_COMPILER' in the suite 'does-not-exist'"
in str(err.value))

# Mark all compiler and linker as available.
with mock.patch('fab.tools.tool.Tool.is_available',
new_callable=mock.PropertyMock) as is_available:
is_available.return_value = True
for cat in [Category.C_COMPILER, Category.FORTRAN_COMPILER,
Category.LINKER]:
def_tool = tr.get_default(cat, mpi=False, openmmp=False)
assert def_tool.suite == "gnu"

tr.set_default_compiler_suite("intel-classic")
for cat in [Category.C_COMPILER, Category.FORTRAN_COMPILER,
Category.LINKER]:
def_tool = tr.get_default(cat, mpi=False, openmp=False)
assert def_tool.suite == "intel-classic"
with pytest.raises(RuntimeError) as err:
tr.set_default_compiler_suite("does-not-exist")
assert ("Cannot find 'FORTRAN_COMPILER' in the suite 'does-not-exist'"
in str(err.value))


def test_tool_repository_no_tool_available():
'''Tests error handling if no tool is available.'''

tr = ToolRepository()
tr.set_default_compiler_suite("gnu")
with mock.patch('fab.tools.tool.Tool.is_available',
new_callable=mock.PropertyMock) as is_available:
is_available.return_value = False
with pytest.raises(RuntimeError) as err:
def_tool = tr.get_default(Category.SHELL)
assert ("Can't find available 'SHELL' tool. Tools are 'bash,sh,ksh,"
"dash'" in str(err.value))

0 comments on commit 6c3f1c2

Please sign in to comment.