diff --git a/source/fab/tools/__init__.py b/source/fab/tools/__init__.py index eadabc83..baa06c01 100644 --- a/source/fab/tools/__init__.py +++ b/source/fab/tools/__init__.py @@ -10,8 +10,8 @@ from fab.tools.ar import Ar from fab.tools.category import Category from fab.tools.compiler import (CCompiler, Compiler, FortranCompiler, Gcc, - Gfortran, Icc, Ifort, MpiGcc, - MpiGfortran, MpiIcc, MpiIfort) + Gfortran, Icc, Ifort) +from fab.tools.compiler_wrapper import CompilerWrapper, Mpicc, Mpif90 from fab.tools.flags import Flags from fab.tools.linker import Linker from fab.tools.psyclone import Psyclone @@ -28,6 +28,7 @@ "CCompiler", "Compiler", "CompilerSuiteTool", + "CompilerWrapper", "Cpp", "CppFortran", "Fcm", @@ -40,10 +41,8 @@ "Icc", "Ifort", "Linker", - "MpiGcc", - "MpiGfortran", - "MpiIcc", - "MpiIfort", + "Mpif90", + "Mpicc", "Preprocessor", "Psyclone", "Rsync", diff --git a/source/fab/tools/compiler.py b/source/fab/tools/compiler.py index 2f8b92c4..f62d7ce2 100644 --- a/source/fab/tools/compiler.py +++ b/source/fab/tools/compiler.py @@ -30,12 +30,14 @@ class Compiler(CompilerSuiteTool): :param exec_name: name of the executable to start. :param suite: name of the compiler suite this tool belongs to. :param category: the Category (C_COMPILER or FORTRAN_COMPILER). - :param mpi: whether the compiler or linker support MPI. :param compile_flag: the compilation flag to use when only requesting compilation (not linking). :param output_flag: the compilation flag to use to indicate the name of the output file :param openmp_flag: the flag to use to enable OpenMP + :param availability_option: a command line option for the tool to test + if the tool is available on the current system. Defaults to + `--version`. ''' # pylint: disable=too-many-arguments @@ -43,11 +45,11 @@ def __init__(self, name: str, exec_name: Union[str, Path], suite: str, category: Category, - mpi: bool = False, compile_flag: Optional[str] = None, output_flag: Optional[str] = None, - openmp_flag: Optional[str] = None): - super().__init__(name, exec_name, suite, mpi=mpi, category=category) + openmp_flag: Optional[str] = None, + availablility_option: Optional[str] = None): + super().__init__(name, exec_name, suite, category=category) self._version = None self._compile_flag = compile_flag if compile_flag else "-c" self._output_flag = output_flag if output_flag else "-o" @@ -182,7 +184,6 @@ class CCompiler(Compiler): :param name: name of the compiler. :param exec_name: name of the executable to start. :param suite: name of the compiler suite. - :param mpi: whether the compiler or linker support MPI. :param category: the Category (C_COMPILER or FORTRAN_COMPILER). :param compile_flag: the compilation flag to use when only requesting compilation (not linking). @@ -193,9 +194,9 @@ class CCompiler(Compiler): # pylint: disable=too-many-arguments def __init__(self, name: str, exec_name: str, suite: str, - mpi: bool = False, compile_flag=None, output_flag=None, + compile_flag=None, output_flag=None, openmp_flag: Optional[str] = None): - super().__init__(name, exec_name, suite, Category.C_COMPILER, mpi=mpi, + super().__init__(name, exec_name, suite, Category.C_COMPILER, compile_flag=compile_flag, output_flag=output_flag, openmp_flag=openmp_flag) @@ -211,7 +212,6 @@ class FortranCompiler(Compiler): :param suite: name of the compiler suite. :param module_folder_flag: the compiler flag to indicate where to store created module files. - :param mpi: whether the compiler or linker support MPI. :param openmp_flag: the flag to use to enable OpenMP :param syntax_only_flag: flag to indicate to only do a syntax check. The side effect is that the module files are created. @@ -223,13 +223,13 @@ class FortranCompiler(Compiler): # pylint: disable=too-many-arguments def __init__(self, name: str, exec_name: str, suite: str, - module_folder_flag: str, mpi: bool = False, + module_folder_flag: str, openmp_flag: Optional[str] = None, syntax_only_flag: Optional[str] = None, compile_flag: Optional[str] = None, output_flag: Optional[str] = None): - super().__init__(name=name, exec_name=exec_name, suite=suite, mpi=mpi, + super().__init__(name=name, exec_name=exec_name, suite=suite, category=Category.FORTRAN_COMPILER, compile_flag=compile_flag, output_flag=output_flag, openmp_flag=openmp_flag) @@ -253,11 +253,12 @@ def compile_file(self, input_file: Path, output_file: Path, openmp: bool, add_flags: Union[None, List[str]] = None, - syntax_only: bool = False): + syntax_only: Optional[bool] = False): '''Compiles a file. :param input_file: the name of the input file. :param output_file: the name of the output file. + :param openmp: if compilation should be done with OpenMP. :param add_flags: additional flags for the compiler. :param syntax_only: if set, the compiler will only do a syntax check @@ -287,26 +288,9 @@ class Gcc(CCompiler): :param name: name of this compiler. :param exec_name: name of the executable. - :param mpi: whether the compiler supports MPI. - ''' - def __init__(self, - name: str = "gcc", - exec_name: str = "gcc", - mpi: bool = False): - super().__init__(name, exec_name, suite="gnu", mpi=mpi, - openmp_flag="-fopenmp") - - -# ============================================================================ -class MpiGcc(Gcc): - '''Class for a simple wrapper around gcc that supports MPI. - It calls `mpicc`. ''' - - def __init__(self): - super().__init__(name="mpicc-gcc", - exec_name="mpicc", - mpi=True) + def __init__(self, name: str = "gcc", exec_name: str = "gcc"): + super().__init__(name, exec_name, suite="gnu", openmp_flag="-fopenmp") # ============================================================================ @@ -315,57 +299,26 @@ class Gfortran(FortranCompiler): :param name: name of this compiler. :param exec_name: name of the executable. - :param mpi: whether the compiler supports MPI. ''' - def __init__(self, - name: str = "gfortran", - exec_name: str = "gfortran", - mpi: bool = False): - super().__init__(name, exec_name, suite="gnu", mpi=mpi, + def __init__(self, name: str = "gfortran", exec_name: str = "gfortran"): + super().__init__(name, exec_name, suite="gnu", module_folder_flag="-J", openmp_flag="-fopenmp", syntax_only_flag="-fsyntax-only") -# ============================================================================ -class MpiGfortran(Gfortran): - '''Class for a simple wrapper around gfortran that supports MPI. - It calls `mpif90`. - ''' - - def __init__(self): - super().__init__(name="mpif90-gfortran", - exec_name="mpif90", - mpi=True) - - # ============================================================================ class Icc(CCompiler): '''Class for the Intel's icc compiler. :param name: name of this compiler. :param exec_name: name of the executable. - :param mpi: whether the compiler supports MPI. - ''' - def __init__(self, - name: str = "icc", - exec_name: str = "icc", - mpi: bool = False): - super().__init__(name, exec_name, suite="intel-classic", mpi=mpi, - openmp_flag="-qopenmp") - - -# ============================================================================ -class MpiIcc(Icc): - '''Class for a simple wrapper around icc that supports MPI. - It calls `mpicc`. ''' - def __init__(self): - super().__init__(name="mpicc-icc", - exec_name="mpicc", - mpi=True) + def __init__(self, name: str = "icc", exec_name: str = "icc"): + super().__init__(name, exec_name, suite="intel-classic", + openmp_flag="-qopenmp") # ============================================================================ @@ -374,26 +327,10 @@ class Ifort(FortranCompiler): :param name: name of this compiler. :param exec_name: name of the executable. - :param mpi: whether the compiler supports MPI. ''' - def __init__(self, - name: str = "ifort", - exec_name: str = "ifort", - mpi: bool = False): - super().__init__(name, exec_name, suite="intel-classic", mpi=mpi, + def __init__(self, name: str = "ifort", exec_name: str = "ifort"): + super().__init__(name, exec_name, suite="intel-classic", module_folder_flag="-module", openmp_flag="-qopenmp", syntax_only_flag="-syntax-only") - - -# ============================================================================ -class MpiIfort(Ifort): - '''Class for a simple wrapper around ifort that supports MPI. - It calls `mpif90`. - ''' - - def __init__(self): - super().__init__(name="mpif90-ifort", - exec_name="mpif90", - mpi=True) diff --git a/source/fab/tools/compiler_wrapper.py b/source/fab/tools/compiler_wrapper.py new file mode 100644 index 00000000..40337219 --- /dev/null +++ b/source/fab/tools/compiler_wrapper.py @@ -0,0 +1,192 @@ +############################################################################## +# (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 the base class for any compiler, and derived +classes for gcc, gfortran, icc, ifort +""" + +from pathlib import Path +from typing import cast, List, Optional, Union +import warnings + +from fab.tools.category import Category +from fab.tools.compiler import Compiler, FortranCompiler +from fab.tools.flags import Flags + + +class CompilerWrapper(Compiler): + '''A decorator-based compiler wrapper. It basically uses a different + executable name when compiling, but otherwise behaves like the wrapped + compiler. An example of a compiler wrapper is `mpif90` (which can + internally call e.g. gfortran, icc, ...) + + :param name: name of the wrapper. + :param exec_name: name of the executable to call. + :param compiler: the compiler that is decorated. + ''' + + def __init__(self, name: str, exec_name: str, + compiler: Compiler): + self._compiler = compiler + super().__init__( + name=name, exec_name=exec_name, + category=self._compiler.category, + suite=self._compiler.suite, + availablility_option=self._compiler.availablility_option) + + def __str__(self): + return f"{type(self).__name__}({self._compiler.name})" + + @property + def is_available(self) -> bool: + '''Checks if the tool is available or not. It will call a tool-specific + function check_available to determine this, but will cache the results + to avoid testing a tool more than once. + + :returns: whether the tool is available (i.e. installed and + working). + ''' + if self._is_available is not None: + return self._is_available + + # We need to check that both the wrapper and the original compiler are + # available: + if not self._compiler.is_available: + print("Orig compiler not available)") + self._is_available = False + return False + + if not super().is_available: + # This will set self._is_available, so need to set it here. + return False + + # Both the compiler and wrapper are available. Make sure they are + # consistent, i.e. both report the same version number: + + if super().get_version() != self._compiler.get_version(): + warnings.warn(f"Compiler wrapper '{self}' is inconsistent: " + f"compiler version is " + f"'{self._compiler.get_version()}', but wrapper " + f"reports '{super().get_version()}'.") + return False + return True + + @property + def category(self) -> Category: + ''':returns: the category of this tool.''' + return self._compiler.category + + @property + def flags(self) -> Flags: + ''':returns: the flags to be used with this tool.''' + return self._compiler.flags + + @property + def suite(self) -> str: + ''':returns: the compiler suite of this tool.''' + return self._compiler.suite + + @property + def mpi(self) -> bool: + ''':returns: whether this tool supports MPI or not.''' + return False + + @property + def has_syntax_only(self) -> bool: + ''':returns: whether this compiler supports a syntax-only feature. + + :raises RuntimeError: if this function is called for a non-Fortran + wrapped compiler. + ''' + + if self._compiler.category == Category.FORTRAN_COMPILER: + return cast(FortranCompiler, self._compiler).has_syntax_only + + raise RuntimeError(f"Compiler '{self._compiler}' has " + f"no has_syntax_only.") + + def set_module_output_path(self, path: Path): + '''Sets the output path for modules. + + :params path: the path to the output directory. + + :raises RuntimeError: if this function is called for a non-Fortran + wrapped compiler. + ''' + + if self._compiler.category != Category.FORTRAN_COMPILER: + raise RuntimeError(f"Compiler '{self._compiler}' has no" + f"'set_module_output_path' function.") + cast(FortranCompiler, self._compiler).set_module_output_path(path) + + def compile_file(self, input_file: Path, + output_file: Path, + openmp: bool, + add_flags: Union[None, List[str]] = None, + syntax_only: Optional[bool] = False): + # pylint: disable=too-many-arguments + '''Compiles a file using the wrapper compiler. It will temporarily + change the name of the wrapped compiler, and then calls the original + compiler (to get all its parameters) + + :param input_file: the name of the input file. + :param output_file: the name of the output file. + :param openmp: if compilation should be done with OpenMP. + :param add_flags: additional flags for the compiler. + :param syntax_only: if set, the compiler will only do + a syntax check + ''' + + orig_compiler_name = self._compiler.exec_name + self._compiler.change_exec_name(self.exec_name) + if isinstance(self._compiler, FortranCompiler): + self._compiler.compile_file(input_file, output_file, openmp=openmp, + add_flags=add_flags, + syntax_only=syntax_only, + ) + else: + self._compiler.compile_file(input_file, output_file, openmp=openmp, + add_flags=add_flags + ) + self._compiler.change_exec_name(orig_compiler_name) + + +# ============================================================================ +class Mpif90(CompilerWrapper): + '''Class for a simple wrapper for using a compiler driver (like mpif90) + It will be using the name "mpif90-COMPILER_NAME" and calls `mpif90`. + All flags from the original compiler will be used when using the wrapper + as compiler. + + :param compiler: the compiler that the mpif90 wrapper will use. + ''' + + def __init__(self, compiler: Compiler): + super().__init__(name=f"mpif90-{compiler.name}", + exec_name="mpif90", compiler=compiler) + + @property + def mpi(self) -> bool: + return True + + +# ============================================================================ +class Mpicc(CompilerWrapper): + '''Class for a simple wrapper for using a compiler driver (like mpicc) + It will be using the name "mpicc-COMPILER_NAME" and calls `mpicc`. + All flags from the original compiler will be used when using the wrapper + as compiler. +s + :param compiler: the compiler that the mpicc wrapper will use. + ''' + + def __init__(self, compiler: Compiler): + super().__init__(name=f"mpicc-{compiler.name}", + exec_name="mpicc", compiler=compiler) + + @property + def mpi(self) -> bool: + return True diff --git a/source/fab/tools/tool.py b/source/fab/tools/tool.py index a4b49a5e..56c57ca8 100644 --- a/source/fab/tools/tool.py +++ b/source/fab/tools/tool.py @@ -91,11 +91,26 @@ def exec_name(self) -> str: ''':returns: the name of the executable.''' return self._exec_name + def change_exec_name(self, exec_name: str): + '''Changes the name of the executable This function should in general + not be used (typically it is better to create a new tool instead). The + function is only provided to support CompilerWrapper (like mpif90), + which need all parameters from the original compiler, but call the + wrapper. The name of the compiler will be changed just before + compilation, and then set back to its original value + ''' + self._exec_name = exec_name + @property def name(self) -> str: ''':returns: the name of the tool.''' return self._name + @property + def availablility_option(self) -> str: + ''':returns: the option to use to check if the tool is available.''' + return self._availability_option + @property def category(self) -> Category: ''':returns: the category of this tool.''' @@ -188,13 +203,16 @@ class CompilerSuiteTool(Tool): :param exec_name: name of the executable to start. :param suite: name of the compiler suite. :param category: the Category to which this tool belongs. - :param mpi: whether the compiler or linker support MPI. + :param availability_option: a command line option for the tool to test + if the tool is available on the current system. Defaults to + `--version`. ''' def __init__(self, name: str, exec_name: Union[str, Path], suite: str, - category: Category, mpi: bool = False): - super().__init__(name, exec_name, category) + category: Category, + availablility_option: Optional[str] = None): + super().__init__(name, exec_name, category, + availablility_option=availablility_option) self._suite = suite - self._mpi = mpi @property def suite(self) -> str: @@ -204,4 +222,4 @@ def suite(self) -> str: @property def mpi(self) -> bool: ''':returns: whether this tool supports MPI or not.''' - return self._mpi + return False diff --git a/source/fab/tools/tool_repository.py b/source/fab/tools/tool_repository.py index 944b421c..7d3aa754 100644 --- a/source/fab/tools/tool_repository.py +++ b/source/fab/tools/tool_repository.py @@ -12,10 +12,11 @@ from __future__ import annotations import logging -from typing import Any, Optional, Type +from typing import cast, Optional from fab.tools.tool import Tool from fab.tools.category import Category +from fab.tools.compiler import Compiler from fab.tools.linker import Linker from fab.tools.versioning import Fcm, Git, Subversion @@ -60,26 +61,30 @@ def __init__(self): # We get circular dependencies if imported at top of the file: # pylint: disable=import-outside-toplevel from fab.tools import (Ar, Cpp, CppFortran, Gcc, Gfortran, - Icc, Ifort, MpiGcc, MpiGfortran, - MpiIcc, MpiIfort, Psyclone, Rsync) + Icc, Ifort, Psyclone, Rsync) for cls in [Gcc, Icc, Gfortran, Ifort, Cpp, CppFortran, - MpiGcc, MpiGfortran, MpiIcc, MpiIfort, Fcm, Git, Subversion, Ar, Psyclone, Rsync]: - self.add_tool(cls) + self.add_tool(cls()) - def add_tool(self, cls: Type[Any]): + from fab.tools.compiler_wrapper import Mpif90, Mpicc + all_fc = self[Category.FORTRAN_COMPILER][:] + for fc in all_fc: + mpif90 = Mpif90(fc) + self.add_tool(mpif90) + + all_cc = self[Category.C_COMPILER][:] + for cc in all_cc: + mpicc = Mpicc(cc) + self.add_tool(mpicc) + + def add_tool(self, tool: Tool): '''Creates an instance of the specified class and adds it to the tool repository. :param cls: the tool to instantiate. ''' - # Note that we cannot declare `cls` to be `Type[Tool]`, since the - # Tool constructor requires arguments, but the classes used here are - # derived from Tool which do not require any arguments (e.g. Ifort) - - tool = cls() # We do not test if a tool is actually available. The ToolRepository # contains the tools that FAB knows about. It is the responsibility # of the ToolBox to make sure only available tools are added. @@ -87,6 +92,7 @@ def add_tool(self, cls: Type[Any]): # If we have a compiler, add the compiler as linker as well if tool.is_compiler: + tool = cast(Compiler, tool) linker = Linker(name=f"linker-{tool.name}", compiler=tool) self[linker.category].append(linker) diff --git a/tests/unit_tests/tools/test_compiler.py b/tests/unit_tests/tools/test_compiler.py index bb7e2695..cbb55b8e 100644 --- a/tests/unit_tests/tools/test_compiler.py +++ b/tests/unit_tests/tools/test_compiler.py @@ -15,8 +15,7 @@ import pytest from fab.tools import (Category, CCompiler, Compiler, FortranCompiler, - Gcc, Gfortran, Icc, Ifort, MpiGcc, MpiGfortran, - MpiIcc, MpiIfort) + Gcc, Gfortran, Icc, Ifort) def test_compiler(): @@ -25,6 +24,7 @@ def test_compiler(): assert cc.category == Category.C_COMPILER assert cc._compile_flag == "-c" assert cc._output_flag == "-o" + # pylint: disable-next=use-implicit-booleaness-not-comparison assert cc.flags == [] assert cc.suite == "gnu" assert not cc.mpi @@ -36,6 +36,7 @@ def test_compiler(): assert fc._output_flag == "-o" assert fc.category == Category.FORTRAN_COMPILER assert fc.suite == "gnu" + # pylint: disable-next=use-implicit-booleaness-not-comparison assert fc.flags == [] assert not fc.mpi assert fc.openmp_flag == "-fopenmp" @@ -321,15 +322,6 @@ def test_gcc(): assert not gcc.mpi -def test_mpi_gcc(): - '''Tests the MPI enables gcc class.''' - mpi_gcc = MpiGcc() - assert mpi_gcc.name == "mpicc-gcc" - assert isinstance(mpi_gcc, CCompiler) - assert mpi_gcc.category == Category.C_COMPILER - assert mpi_gcc.mpi - - def test_gfortran(): '''Tests the gfortran class.''' gfortran = Gfortran() @@ -339,15 +331,6 @@ def test_gfortran(): assert not gfortran.mpi -def test_mpi_gfortran(): - '''Tests the MPI enabled gfortran class.''' - mpi_gfortran = MpiGfortran() - assert mpi_gfortran.name == "mpif90-gfortran" - assert isinstance(mpi_gfortran, FortranCompiler) - assert mpi_gfortran.category == Category.FORTRAN_COMPILER - assert mpi_gfortran.mpi - - def test_icc(): '''Tests the icc class.''' icc = Icc() @@ -357,15 +340,6 @@ def test_icc(): assert not icc.mpi -def test_mpi_icc(): - '''Tests the MPI enabled icc class.''' - mpi_icc = MpiIcc() - assert mpi_icc.name == "mpicc-icc" - assert isinstance(mpi_icc, CCompiler) - assert mpi_icc.category == Category.C_COMPILER - assert mpi_icc.mpi - - def test_ifort(): '''Tests the ifort class.''' ifort = Ifort() @@ -373,32 +347,3 @@ def test_ifort(): assert isinstance(ifort, FortranCompiler) assert ifort.category == Category.FORTRAN_COMPILER assert not ifort.mpi - - -def test_mpi_ifort(): - '''Tests the MPI enabled ifort class.''' - mpi_ifort = MpiIfort() - assert mpi_ifort.name == "mpif90-ifort" - assert isinstance(mpi_ifort, FortranCompiler) - assert mpi_ifort.category == Category.FORTRAN_COMPILER - assert mpi_ifort.mpi - - -def test_compiler_wrapper(): - '''Make sure we can easily create a compiler wrapper.''' - class MpiF90(Ifort): - '''A simple compiler wrapper''' - def __init__(self): - super().__init__(name="mpif90-intel", - exec_name="mpif90") - - @property - def mpi(self): - return True - - mpif90 = MpiF90() - assert mpif90.suite == "intel-classic" - assert mpif90.category == Category.FORTRAN_COMPILER - assert mpif90.name == "mpif90-intel" - assert mpif90.exec_name == "mpif90" - assert mpif90.mpi diff --git a/tests/unit_tests/tools/test_compiler_wrapper.py b/tests/unit_tests/tools/test_compiler_wrapper.py new file mode 100644 index 00000000..adf33970 --- /dev/null +++ b/tests/unit_tests/tools/test_compiler_wrapper.py @@ -0,0 +1,171 @@ +############################################################################## +# (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 compiler wrapper implementation. +''' + +from pathlib import Path, PosixPath +from unittest import mock + +import pytest + +from fab.tools import (Category, CCompiler, FortranCompiler, + Gcc, Gfortran, Icc, Ifort, + CompilerWrapper, Mpicc, Mpif90) + + +def test_compiler_check_available(): + '''Check if check_available works as expected. The compiler class + uses internally get_version to test if a compiler works or not. + ''' + cc = CCompiler("gcc", "gcc", "gnu") + # The compiler uses get_version to check if it is available. + # First simulate a successful run: + with mock.patch.object(cc, "get_version", returncode=123): + assert cc.check_available() + + # Now test if get_version raises an error + with mock.patch.object(cc, "get_version", side_effect=RuntimeError("")): + assert not cc.check_available() + + +def test_compiler_hash(): + '''Test the hash functionality.''' + cc = CCompiler("gcc", "gcc", "gnu") + with mock.patch.object(cc, "_version", 567): + hash1 = cc.get_hash() + assert hash1 == 4646426180 + + # A change in the version number must change the hash: + with mock.patch.object(cc, "_version", 89): + hash2 = cc.get_hash() + assert hash2 != hash1 + + # A change in the name must change the hash, again: + cc._name = "new_name" + hash3 = cc.get_hash() + assert hash3 not in (hash1, hash2) + + +def test_compiler_syntax_only(): + '''Tests handling of syntax only flags.''' + fc = FortranCompiler("gfortran", "gfortran", "gnu", + openmp_flag="-fopenmp", module_folder_flag="-J") + assert not fc.has_syntax_only + fc = FortranCompiler("gfortran", "gfortran", "gnu", openmp_flag="-fopenmp", + module_folder_flag="-J", syntax_only_flag=None) + assert not fc.has_syntax_only + # Empty since no flag is defined + assert fc.openmp_flag == "-fopenmp" + + fc = FortranCompiler("gfortran", "gfortran", "gnu", + openmp_flag="-fopenmp", + module_folder_flag="-J", + syntax_only_flag="-fsyntax-only") + fc.set_module_output_path("/tmp") + assert fc.has_syntax_only + assert fc._syntax_only_flag == "-fsyntax-only" + fc.run = mock.Mock() + fc.compile_file(Path("a.f90"), "a.o", openmp=False, syntax_only=True) + fc.run.assert_called_with(cwd=Path('.'), + additional_parameters=['-c', '-fsyntax-only', + "-J", '/tmp', 'a.f90', + '-o', 'a.o', ]) + fc.compile_file(Path("a.f90"), "a.o", openmp=True, syntax_only=True) + fc.run.assert_called_with(cwd=Path('.'), + additional_parameters=['-c', '-fopenmp', '-fsyntax-only', + "-J", '/tmp', 'a.f90', + '-o', 'a.o', ]) + + +def test_compiler_module_output(): + '''Tests handling of module output_flags.''' + fc = FortranCompiler("gfortran", "gfortran", suite="gnu", + module_folder_flag="-J") + fc.set_module_output_path("/module_out") + assert fc._module_output_path == "/module_out" + fc.run = mock.MagicMock() + fc.compile_file(Path("a.f90"), "a.o", openmp=False, syntax_only=True) + fc.run.assert_called_with(cwd=PosixPath('.'), + additional_parameters=['-c', '-J', '/module_out', + 'a.f90', '-o', 'a.o']) + + +def test_compiler_with_add_args(): + '''Tests that additional arguments are handled as expected.''' + fc = FortranCompiler("gfortran", "gfortran", suite="gnu", + openmp_flag="-fopenmp", + module_folder_flag="-J") + fc.set_module_output_path("/module_out") + assert fc._module_output_path == "/module_out" + fc.run = mock.MagicMock() + with pytest.warns(UserWarning, match="Removing managed flag"): + fc.compile_file(Path("a.f90"), "a.o", add_flags=["-J/b", "-O3"], + openmp=False, syntax_only=True) + # Notice that "-J/b" has been removed + fc.run.assert_called_with(cwd=PosixPath('.'), + additional_parameters=['-c', "-O3", + '-J', '/module_out', + 'a.f90', '-o', 'a.o']) + with pytest.warns(UserWarning, + match="explicitly provided. OpenMP should be enabled in " + "the BuildConfiguration"): + fc.compile_file(Path("a.f90"), "a.o", + add_flags=["-fopenmp", "-O3"], + openmp=True, syntax_only=True) + + +def test_flags(): + '''Tests that flags set in the base compiler will be accessed in the + wrapper.''' + gcc = Gcc() + mpicc = Mpicc(gcc) + assert gcc.flags == [] + assert mpicc.flags == [] + # Setting flags in gcc must become visible in the wrapper compiler: + gcc.add_flags(["-a", "-b"]) + assert gcc.flags == ["-a", "-b"] + assert mpicc.flags == ["-a", "-b"] + + +def test_mpi_gcc(): + '''Tests the MPI enables gcc class.''' + mpi_gcc = Mpicc(Gcc()) + assert mpi_gcc.name == "mpicc-gcc" + assert str(mpi_gcc) == "Mpicc(gcc)" + assert isinstance(mpi_gcc, CompilerWrapper) + assert mpi_gcc.category == Category.C_COMPILER + assert mpi_gcc.mpi + + +def test_mpi_gfortran(): + '''Tests the MPI enabled gfortran class.''' + mpi_gfortran = Mpif90(Gfortran()) + assert mpi_gfortran.name == "mpif90-gfortran" + assert str(mpi_gfortran) == "Mpif90(gfortran)" + assert isinstance(mpi_gfortran, CompilerWrapper) + assert mpi_gfortran.category == Category.FORTRAN_COMPILER + assert mpi_gfortran.mpi + + +def test_mpi_icc(): + '''Tests the MPI enabled icc class.''' + mpi_icc = Mpicc(Icc()) + assert mpi_icc.name == "mpicc-icc" + assert str(mpi_icc) == "Mpicc(icc)" + assert isinstance(mpi_icc, CompilerWrapper) + assert mpi_icc.category == Category.C_COMPILER + assert mpi_icc.mpi + + +def test_mpi_ifort(): + '''Tests the MPI enabled ifort class.''' + mpi_ifort = Mpif90(Ifort()) + assert mpi_ifort.name == "mpif90-ifort" + assert str(mpi_ifort) == "Mpif90(ifort)" + assert isinstance(mpi_ifort, CompilerWrapper) + assert mpi_ifort.category == Category.FORTRAN_COMPILER + assert mpi_ifort.mpi diff --git a/tests/unit_tests/tools/test_tool.py b/tests/unit_tests/tools/test_tool.py index ea4b8ee6..0f714444 100644 --- a/tests/unit_tests/tools/test_tool.py +++ b/tests/unit_tests/tools/test_tool.py @@ -50,15 +50,21 @@ def test_tool_constructor(): assert misc.category == Category.MISC +def test_tool_chance_exec_name(): + '''Test that we can change the name of the executable. + ''' + tool = Tool("gfortran", "gfortran", Category.FORTRAN_COMPILER) + assert tool.exec_name == "gfortran" + tool.change_exec_name("start_me_instead") + assert tool.exec_name == "start_me_instead" + + def test_tool_is_available(): '''Test that is_available works as expected.''' tool = Tool("gfortran", "gfortran", Category.FORTRAN_COMPILER) with mock.patch.object(tool, "check_available", return_value=True): assert tool.is_available - # Test the getter tool._is_available = False - assert not tool.is_available - assert tool.is_compiler # Test the exception when trying to use in a non-existent tool: with pytest.raises(RuntimeError) as err: @@ -66,9 +72,16 @@ def test_tool_is_available(): assert ("Tool 'gfortran' is not available to run '['gfortran', '--ops']'" in str(err.value)) + # Test setting the option and the getter + tool = Tool("gfortran", "gfortran", Category.FORTRAN_COMPILER, + availablility_option="am_i_here") + assert tool.availablility_option == "am_i_here" + + def test_tool_flags(): '''Test that flags work as expected''' tool = Tool("gfortran", "gfortran", Category.FORTRAN_COMPILER) + # pylint: disable-next=use-implicit-booleaness-not-comparison assert tool.flags == [] tool.add_flags("-a") assert tool.flags == ["-a"] diff --git a/tests/unit_tests/tools/test_tool_repository.py b/tests/unit_tests/tools/test_tool_repository.py index b72f85a9..4601be06 100644 --- a/tests/unit_tests/tools/test_tool_repository.py +++ b/tests/unit_tests/tools/test_tool_repository.py @@ -76,6 +76,7 @@ def test_tool_repository_get_default(): def test_tool_repository_get_default_error(): '''Tests error handling in get_default.''' + tr = ToolRepository() with pytest.raises(RuntimeError) as err: tr.get_default("unknown-category")