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

Add shell tool clean #368

Merged
merged 47 commits into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from 45 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
282f068
Merge pull request #19 from hiker/linker-lib-flags
hiker Sep 24, 2024
3b6e0bd
Support new and old style of PSyclone command line (no more nemo api …
hiker Sep 26, 2024
16d3ff5
Fix mypy errors.
hiker Sep 26, 2024
71fd1ae
Added missing tests for calling psyclone, and converting old style to…
hiker Sep 30, 2024
ec4c0f6
Updated comment.
hiker Sep 30, 2024
b9aabf8
Removed mixing, use a simple regex instead.
hiker Oct 17, 2024
8ee10e8
Added support for ifx/icx compiler as intel-llvm class.
hiker Oct 18, 2024
d7b2008
Added support for nvidia compiler.
hiker Oct 18, 2024
9005b3b
Add preliminary support for Cray compiler.
hiker Oct 18, 2024
8771e80
Added Cray compiler wrapper ftn and cc.
hiker Oct 18, 2024
0188050
Follow a more consistent naming scheme for crays, even though the nat…
hiker Oct 22, 2024
3c569bd
Changed names again.
hiker Oct 22, 2024
edc5fcd
Renamed cray compiler wrapper to be CrayCcWrapper and CrayFtnWrapper,…
hiker Nov 11, 2024
f6a70c8
Fixed incorrect name in comments.
hiker Nov 11, 2024
4f0e70f
Merge pull request #28 from hiker/additional_compilers
lukehoffmann Nov 11, 2024
58caecf
Merge branch 'compiler_wrapper' into bom_master
hiker Nov 11, 2024
5452d70
Merge branch 'linker-lib-flags' into additional_compilers
hiker Nov 12, 2024
d54d94c
Merge branch 'linker-lib-flags' into bom_master
hiker Nov 12, 2024
605e7e5
Merge branch 'additional_compilers' into bom_master
hiker Nov 12, 2024
70ad4b1
Merge branch 'linker-lib-flags' into additional_compilers
hiker Nov 12, 2024
b70f98f
Merge branch 'linker-lib-flags' into additional_compilers
hiker Nov 12, 2024
2f7e3ba
Merge branch 'linker-lib-flags' into additional_compilers
hiker Nov 12, 2024
7a2eb59
Additional compilers (#349)
hiker Nov 12, 2024
bd1d318
Merge branch 'dev' into additional_compilers
hiker Nov 12, 2024
2148fb7
Merge branch 'compiler_wrapper' into update_psyclone_to_support_next_…
hiker Nov 19, 2024
20fe928
Merge branch 'update_psyclone_to_support_next_release_syntax' into ad…
hiker Nov 19, 2024
f7b49e0
Merge branch 'linker-lib-flags' into additional_compilers
hiker Nov 21, 2024
a493c53
Support new and old style of PSyclone command line (no more nemo api …
hiker Sep 26, 2024
824851d
Fix mypy errors.
hiker Sep 26, 2024
16a125c
Added missing tests for calling psyclone, and converting old style to…
hiker Sep 30, 2024
fc19283
Added shell tool.
hiker Oct 23, 2024
730a824
Try to make mypy happy.
hiker Oct 23, 2024
6e280d9
Removed debug code.
hiker Oct 23, 2024
6c3f1c2
ToolRepository now only returns default that are available. Updated t…
hiker Oct 23, 2024
ae61d4a
Fixed typos and coding style.
hiker Nov 21, 2024
e7c2c83
Support new and old style of PSyclone command line (no more nemo api …
hiker Sep 26, 2024
e2051f2
Fix mypy errors.
hiker Sep 26, 2024
0ad85ee
Added missing tests for calling psyclone, and converting old style to…
hiker Sep 30, 2024
890b50d
Updated comment.
hiker Sep 30, 2024
032ab26
Fixed failing tests.
hiker Nov 21, 2024
7168f42
Merge branch 'additional_compilers_clean' into psyclone_3_support_clean
hiker Nov 21, 2024
70c083e
Merge branch 'psyclone_3_support_clean' into add_shell_tool_clean
hiker Nov 21, 2024
fa0cb5d
Fixed double _.
hiker Nov 29, 2024
63e77e5
Merge branch 'psyclone_3_support_clean' into add_shell_tool_clean
hiker Nov 29, 2024
bbdb380
Merge branch 'develop' into add_shell_tool_clean
hiker Dec 2, 2024
0e6e41c
Updated documentation and small issues raised in review.
hiker Dec 9, 2024
72fb659
Fixed failing test.
hiker Dec 9, 2024
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
2 changes: 2 additions & 0 deletions source/fab/tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from fab.tools.psyclone import Psyclone
from fab.tools.rsync import Rsync
from fab.tools.preprocessor import Cpp, CppFortran, Fpp, Preprocessor
from fab.tools.shell import Shell
from fab.tools.tool import Tool, CompilerSuiteTool
# Order here is important to avoid a circular import
from fab.tools.tool_repository import ToolRepository
Expand Down Expand Up @@ -56,6 +57,7 @@
"Preprocessor",
"Psyclone",
"Rsync",
"Shell",
"Subversion",
"Tool",
"ToolBox",
Expand Down
1 change: 1 addition & 0 deletions source/fab/tools/category.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class Category(Enum):
SUBVERSION = auto()
AR = auto()
RSYNC = auto()
SHELL = auto()
MISC = auto()

def __str__(self):
Expand Down
2 changes: 1 addition & 1 deletion source/fab/tools/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def __init__(self, name: str,
compile_flag: Optional[str] = None,
output_flag: Optional[str] = None,
openmp_flag: Optional[str] = None,
availability_option: Optional[str] = None):
availability_option: Optional[Union[str, List[str]]] = None):
super().__init__(name, exec_name, suite, category=category,
availability_option=availability_option)
self._version: Union[Tuple[int, ...], None] = None
Expand Down
2 changes: 1 addition & 1 deletion source/fab/tools/psyclone.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ def process(self,
# New version: no API, parameter, but -o for output name:
parameters.extend(["-o", transformed_file])
else:
# 2.5.0 or earlier: needs api nemo, output name is -oalg
# 2.5.0 or earlier: needs api nemo, output name is -opsy
parameters.extend(["-api", "nemo", "-opsy", transformed_file])
parameters.extend(["-l", "all"])

Expand Down
46 changes: 46 additions & 0 deletions source/fab/tools/shell.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
##############################################################################
# (c) Crown copyright Met Office. All rights reserved.
# For further details please refer to the file COPYRIGHT
# which you should have received as part of this distribution
##############################################################################

"""This file contains a base class for shells. This can be used to execute
other scripts.
"""

from pathlib import Path
from typing import List, Union

from fab.tools.category import Category
from fab.tools.tool import Tool


class Shell(Tool):
hiker marked this conversation as resolved.
Show resolved Hide resolved
'''A simple wrapper that runs a shell script. There seems to be no
consistent way to simply check if a shell is working - not only support
a version command (e.g. sh and dash don't). Instead, availability
is tested by running a simple 'echo' command.

:name: the path to the script to run.
'''
def __init__(self, name: str):
super().__init__(name=name, exec_name=name,
availability_option=["-c", "echo hello"],
category=Category.SHELL)

def exec(self, command: Union[str, List[Union[Path, str]]]) -> str:
'''Executes the specified command.

:param command: the command and potential parameters to execute.

:returns: stdout of the result.
'''
# Make mypy happy:
params: List[Union[str, Path]]
if isinstance(command, str):
params = ["-c", command]
else:
params = ["-c"]
params.extend(command)
return super().run(additional_parameters=params,
capture_output=True)
13 changes: 7 additions & 6 deletions source/fab/tools/tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import logging
from pathlib import Path
import subprocess
from typing import Dict, List, Optional, Union
from typing import Dict, List, Optional, Sequence, Union

from fab.tools.category import Category
from fab.tools.flags import Flags
Expand All @@ -36,7 +36,7 @@ class Tool:

def __init__(self, name: str, exec_name: Union[str, Path],
category: Category = Category.MISC,
availability_option: Optional[str] = None):
availability_option: Optional[Union[str, List[str]]] = None):
self._logger = logging.getLogger(__name__)
self._name = name
self._exec_name = str(exec_name)
Expand All @@ -63,7 +63,8 @@ def check_available(self) -> bool:
:returns: whether the tool is working (True) or not.
'''
try:
self.run(self._availability_option)
op = self._availability_option
self.run(op)
hiker marked this conversation as resolved.
Show resolved Hide resolved
except (RuntimeError, FileNotFoundError):
return False
return True
Expand Down Expand Up @@ -107,7 +108,7 @@ def name(self) -> str:
return self._name

@property
def availability_option(self) -> str:
def availability_option(self) -> Union[str, List[str]]:
''':returns: the option to use to check if the tool is available.'''
return self._availability_option

Expand Down Expand Up @@ -139,7 +140,7 @@ def __str__(self):

def run(self,
additional_parameters: Optional[
Union[str, List[Union[Path, str]]]] = None,
Union[str, Sequence[Union[Path, str]]]] = None,
jasonjunweilyu marked this conversation as resolved.
Show resolved Hide resolved
env: Optional[Dict[str, str]] = None,
cwd: Optional[Union[Path, str]] = None,
capture_output=True) -> str:
Expand Down Expand Up @@ -210,7 +211,7 @@ class CompilerSuiteTool(Tool):
'''
def __init__(self, name: str, exec_name: Union[str, Path], suite: str,
category: Category,
availability_option: Optional[str] = None):
availability_option: Optional[Union[str, List[str]]] = None):
super().__init__(name, exec_name, category,
availability_option=availability_option)
self._suite = suite
Expand Down
37 changes: 25 additions & 12 deletions source/fab/tools/tool_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from fab.tools.versioning import Fcm, Git, Subversion
from fab.tools import (Ar, Cpp, CppFortran, Craycc, Crayftn,
Gcc, Gfortran, Icc, Icx, Ifort, Ifx,
Nvc, Nvfortran, Psyclone, Rsync)
Nvc, Nvfortran, Psyclone, Rsync, Shell)


class ToolRepository(dict):
Expand Down 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 All @@ -72,6 +72,12 @@ def __init__(self):
Ar, Fcm, Git, Psyclone, Rsync, Subversion]:
self.add_tool(cls())

# Add the common shells. While Fab itself does not need this,
# it is a very convenient tool for user configuration (e.g. to
# query nc-config etc)
for shell_name in ["bash", "sh", "ksh", "dash"]:
self.add_tool(Shell(shell_name))
hiker marked this conversation as resolved.
Show resolved Hide resolved

# Now create the potential mpif90 and Cray ftn wrapper
all_fc = self[Category.FORTRAN_COMPILER][:]
for fc in all_fc:
Expand All @@ -95,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 @@ -155,15 +162,16 @@ 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 @@ -176,7 +184,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}'.")
hiker marked this conversation as resolved.
Show resolved Hide resolved

if not isinstance(mpi, bool):
raise RuntimeError(f"Invalid or missing mpi specification "
Expand All @@ -191,8 +204,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
1 change: 0 additions & 1 deletion tests/unit_tests/steps/test_archive_objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@ def test_for_library(self):
def test_incorrect_tool(self, tool_box):
'''Test that an incorrect archive tool is detected
'''

config = BuildConfig('proj', tool_box)
cc = tool_box.get_tool(Category.C_COMPILER, config.mpi, config.openmp)
# And set its category to be AR
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
hiker marked this conversation as resolved.
Show resolved Hide resolved
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
Loading