Skip to content

Commit

Permalink
Added shell tool.
Browse files Browse the repository at this point in the history
  • Loading branch information
hiker committed Oct 23, 2024
1 parent 3c569bd commit 2c298b9
Show file tree
Hide file tree
Showing 7 changed files with 124 additions and 8 deletions.
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 @@ -55,7 +55,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
44 changes: 44 additions & 0 deletions source/fab/tools/shell.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
##############################################################################
# (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):
'''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.
'''
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)
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,
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
9 changes: 8 additions & 1 deletion source/fab/tools/tool_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,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 @@ -71,6 +71,13 @@ 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))
self.get_tool(Category.SHELL, shell_name)

# Now create the potential mpif90 and Cray ftn wrapper
all_fc = self[Category.FORTRAN_COMPILER][:]
for fc in all_fc:
Expand Down
61 changes: 61 additions & 0 deletions tests/unit_tests/tools/test_shell.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
##############################################################################
# (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
##############################################################################

'''Tests the shell implementation.
'''

from unittest import mock

from fab.tools import Category, Shell


def test_shell_constructor():
'''Test the Shell constructor.'''
bash = Shell("bash")
assert bash.category == Category.SHELL
assert bash.name == "bash"
assert bash.exec_name == "bash"


def test_shell_check_available():
'''Tests the is_available functionality.'''
bash = Shell("bash")
mock_result = mock.Mock(returncode=0)
with mock.patch('fab.tools.tool.subprocess.run',
return_value=mock_result) as tool_run:
assert bash.check_available()
tool_run.assert_called_once_with(
["bash", "-c", "echo hello"], capture_output=True, env=None,
cwd=None, check=False)

# Test behaviour if a runtime error happens:
with mock.patch("fab.tools.tool.Tool.run",
side_effect=RuntimeError("")) as tool_run:
assert not bash.check_available()


def test_shell_exec_single_arg():
'''Test running a shell script without additional parameters.'''
bash = Shell("ksh")
mock_result = mock.Mock(returncode=0)
with mock.patch('fab.tools.tool.subprocess.run',
return_value=mock_result) as tool_run:
bash.exec("echo")
tool_run.assert_called_with(['ksh', '-c', 'echo'],
capture_output=True, env=None, cwd=None,
check=False)


def test_shell_exec_multiple_args():
'''Test running a shell script with parameters.'''
bash = Shell("ksh")
mock_result = mock.Mock(returncode=0)
with mock.patch('fab.tools.tool.subprocess.run',
return_value=mock_result) as tool_run:
bash.exec(["some", "shell", "function"])
tool_run.assert_called_with(['ksh', '-c', 'some', 'shell', 'function'],
capture_output=True, env=None, cwd=None,
check=False)

0 comments on commit 2c298b9

Please sign in to comment.