From 03548efa7ad6431c1a4cf923be40d70d31d3bcd0 Mon Sep 17 00:00:00 2001 From: Joerg Henrichs Date: Thu, 13 Feb 2025 22:15:23 +1100 Subject: [PATCH] Support additional compilers (#385) * Cray * Icx (LLVM Intel) * nVidia --- source/fab/tools/__init__.py | 19 +- source/fab/tools/compiler.py | 244 +++++++----- source/fab/tools/compiler_wrapper.py | 31 +- source/fab/tools/tool_repository.py | 33 +- tests/conftest.py | 4 +- tests/unit_tests/tools/test_compiler.py | 350 ++++++++++++++++-- .../unit_tests/tools/test_compiler_wrapper.py | 47 ++- tests/unit_tests/tools/test_tool_box.py | 6 +- .../unit_tests/tools/test_tool_repository.py | 2 +- 9 files changed, 596 insertions(+), 140 deletions(-) diff --git a/source/fab/tools/__init__.py b/source/fab/tools/__init__.py index 45eb666f..bc7430b7 100644 --- a/source/fab/tools/__init__.py +++ b/source/fab/tools/__init__.py @@ -9,10 +9,11 @@ from fab.tools.ar import Ar from fab.tools.category import Category -from fab.tools.compiler import (CCompiler, Compiler, FortranCompiler, Gcc, - Gfortran, GnuVersionHandling, Icc, Ifort, - IntelVersionHandling) -from fab.tools.compiler_wrapper import CompilerWrapper, Mpicc, Mpif90 +from fab.tools.compiler import (CCompiler, Compiler, Craycc, Crayftn, + FortranCompiler, Gcc, Gfortran, Icc, + Icx, Ifort, Ifx, Nvc, Nvfortran) +from fab.tools.compiler_wrapper import (CompilerWrapper, CrayCcWrapper, + CrayFtnWrapper, Mpicc, Mpif90) from fab.tools.flags import Flags from fab.tools.linker import Linker from fab.tools.psyclone import Psyclone @@ -32,6 +33,10 @@ "CompilerWrapper", "Cpp", "CppFortran", + "Craycc", + "CrayCcWrapper", + "Crayftn", + "CrayFtnWrapper", "Fcm", "Flags", "FortranCompiler", @@ -39,13 +44,15 @@ "Gcc", "Gfortran", "Git", - "GnuVersionHandling", "Icc", + "Icx", "Ifort", - "IntelVersionHandling", + "Ifx", "Linker", "Mpif90", "Mpicc", + "Nvc", + "Nvfortran", "Preprocessor", "Psyclone", "Rsync", diff --git a/source/fab/tools/compiler.py b/source/fab/tools/compiler.py index 7a635d78..992301b4 100644 --- a/source/fab/tools/compiler.py +++ b/source/fab/tools/compiler.py @@ -30,6 +30,9 @@ class Compiler(CompilerSuiteTool): :param name: name of the compiler. :param exec_name: name of the executable to start. :param suite: name of the compiler suite this tool belongs to. + :param version_regex: A regular expression that allows extraction of + the version number from the version output of the compiler. The + version is taken from the first group of a match. :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 @@ -47,6 +50,7 @@ class Compiler(CompilerSuiteTool): def __init__(self, name: str, exec_name: Union[str, Path], suite: str, + version_regex: str, category: Category, mpi: bool = False, compile_flag: Optional[str] = None, @@ -61,6 +65,7 @@ def __init__(self, name: str, self._output_flag = output_flag if output_flag else "-o" self._openmp_flag = openmp_flag if openmp_flag else "" self.flags.extend(os.getenv("FFLAGS", "").split()) + self._version_regex = version_regex @property def mpi(self) -> bool: @@ -156,8 +161,14 @@ def get_version(self) -> Tuple[int, ...]: # Run the compiler to get the version and parse the output # The implementations depend on vendor output = self.run_version_command() - version_string = self.parse_version_output(self.category, output) + # Multiline is required in case that the version number is the end + # of the string, otherwise the $ would not match the end of line + matches = re.search(self._version_regex, output, re.MULTILINE) + if not matches: + raise RuntimeError(f"Unexpected version output format for " + f"compiler '{self.name}': {output}") + version_string = matches.groups()[0] # Expect the version to be dot-separated integers. try: version = tuple(int(x) for x in version_string.split('.')) @@ -195,15 +206,6 @@ def run_version_command( raise RuntimeError(f"Error asking for version of compiler " f"'{self.name}'") from err - def parse_version_output(self, category: Category, - version_output: str) -> str: - ''' - Extract the numerical part from the version output. - Implemented in specific compilers. - ''' - raise NotImplementedError("The method `parse_version_output` must be " - "provided using a mixin.") - def get_version_string(self) -> str: """ Get a string representing the version of the given compiler. @@ -226,6 +228,8 @@ 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 version_regex: A regular expression that allows extraction of + the version number from the version output of the compiler. :param mpi: whether the compiler or linker support MPI. :param compile_flag: the compilation flag to use when only requesting compilation (not linking). @@ -236,6 +240,7 @@ class CCompiler(Compiler): # pylint: disable=too-many-arguments def __init__(self, name: str, exec_name: str, suite: str, + version_regex: str, mpi: bool = False, compile_flag: Optional[str] = None, output_flag: Optional[str] = None, @@ -243,7 +248,8 @@ def __init__(self, name: str, exec_name: str, suite: str, super().__init__(name, exec_name, suite, category=Category.C_COMPILER, mpi=mpi, compile_flag=compile_flag, output_flag=output_flag, - openmp_flag=openmp_flag) + openmp_flag=openmp_flag, + version_regex=version_regex) # ============================================================================ @@ -255,6 +261,8 @@ class FortranCompiler(Compiler): :param name: name of the compiler. :param exec_name: name of the executable to start. :param suite: name of the compiler suite. + :param version_regex: A regular expression that allows extraction of + the version number from the version output of the compiler. :param mpi: whether MPI is supported by this compiler or not. :param compile_flag: the compilation flag to use when only requesting compilation (not linking). @@ -269,6 +277,7 @@ class FortranCompiler(Compiler): # pylint: disable=too-many-arguments def __init__(self, name: str, exec_name: str, suite: str, + version_regex: str, mpi: bool = False, compile_flag: Optional[str] = None, output_flag: Optional[str] = None, @@ -280,7 +289,8 @@ def __init__(self, name: str, exec_name: str, suite: str, super().__init__(name=name, exec_name=exec_name, suite=suite, category=Category.FORTRAN_COMPILER, mpi=mpi, compile_flag=compile_flag, - output_flag=output_flag, openmp_flag=openmp_flag) + output_flag=output_flag, openmp_flag=openmp_flag, + version_regex=version_regex) self._module_folder_flag = (module_folder_flag if module_folder_flag else "") self._syntax_only_flag = syntax_only_flag @@ -334,45 +344,9 @@ def compile_file(self, input_file: Path, # ============================================================================ -class GnuVersionHandling(): - '''Mixin to handle version information from GNU compilers''' - - def parse_version_output(self, category: Category, - version_output: str) -> str: - ''' - Extract the numerical part from a GNU compiler's version output - - :param name: the compiler's name - :param category: the compiler's Category - :param version_output: the full version output from the compiler - :returns: the actual version as a string - - :raises RuntimeError: if the output is not in an expected format. - ''' - - # Expect the version to appear after some in parentheses, e.g. - # "GNU Fortran (...) n.n[.n, ...]" or # "gcc (...) n.n[.n, ...]" - if category is Category.FORTRAN_COMPILER: - name = "GNU Fortran" - else: - name = "gcc" - # A version number is a digit, followed by a sequence of digits and - # '.'', ending with a digit. It must then be followed by either the - # end of the string, or a space (e.g. "... 5.6 123456"). We can't use - # \b to determine the end, since then "1.2." would be matched - # excluding the dot (so it would become a valid 1.2) - exp = name + r" \(.*?\) (\d[\d\.]+\d)(?:$| )" - # Multiline is required in case that the version number is the - # end of the string, otherwise the $ would not match the end of line - matches = re.search(exp, version_output, re.MULTILINE) - if not matches: - raise RuntimeError(f"Unexpected version output format for " - f"compiler '{name}': {version_output}") - return matches.groups()[0] - - +# Gnu # ============================================================================ -class Gcc(GnuVersionHandling, CCompiler): +class Gcc(CCompiler): '''Class for GNU's gcc compiler. :param name: name of this compiler. @@ -383,12 +357,18 @@ def __init__(self, name: str = "gcc", exec_name: str = "gcc", mpi: bool = False): + # A version number is a digit, followed by a sequence of digits and + # '.'', ending with a digit. It must then be followed by either the + # end of the string, or a space (e.g. "... 5.6 123456"). We can't use + # \b to determine the end, since then "1.2." would be matched + # excluding the dot (so it would become a valid 1.2) super().__init__(name, exec_name, suite="gnu", mpi=mpi, - openmp_flag="-fopenmp") + openmp_flag="-fopenmp", + version_regex=r"gcc \(.*?\) (\d[\d\.]+\d)(?:$| )") # ============================================================================ -class Gfortran(GnuVersionHandling, FortranCompiler): +class Gfortran(FortranCompiler): '''Class for GNU's gfortran compiler. :param name: name of this compiler. @@ -401,45 +381,15 @@ def __init__(self, name: str = "gfortran", super().__init__(name, exec_name, suite="gnu", openmp_flag="-fopenmp", module_folder_flag="-J", - syntax_only_flag="-fsyntax-only") + syntax_only_flag="-fsyntax-only", + version_regex=(r"GNU Fortran \(.*?\) " + r"(\d[\d\.]+\d)(?:$| )")) # ============================================================================ -class IntelVersionHandling(): - '''Mixin to handle version information from Intel compilers''' - - def parse_version_output(self, category: Category, - version_output: str) -> str: - ''' - Extract the numerical part from an Intel compiler's version output - - :param name: the compiler's name - :param version_output: the full version output from the compiler - :returns: the actual version as a string - - :raises RuntimeError: if the output is not in an expected format. - ''' - - # Expect the version to appear after some in parentheses, e.g. - # "icc (...) n.n[.n, ...]" or "ifort (...) n.n[.n, ...]" - if category == Category.C_COMPILER: - name = "icc" - else: - name = "ifort" - - # A version number is a digit, followed by a sequence of digits and - # '.'', ending with a digit. It must then be followed by a space. - exp = name + r" \(.*?\) (\d[\d\.]+\d) " - matches = re.search(exp, version_output) - - if not matches: - raise RuntimeError(f"Unexpected version output format for " - f"compiler '{name}': {version_output}") - return matches.groups()[0] - - +# intel-classic # ============================================================================ -class Icc(IntelVersionHandling, CCompiler): +class Icc(CCompiler): '''Class for the Intel's icc compiler. :param name: name of this compiler. @@ -449,11 +399,12 @@ class Icc(IntelVersionHandling, CCompiler): def __init__(self, name: str = "icc", exec_name: str = "icc"): super().__init__(name, exec_name, suite="intel-classic", - openmp_flag="-qopenmp") + openmp_flag="-qopenmp", + version_regex=r"icc \(ICC\) (\d[\d\.]+\d) ") # ============================================================================ -class Ifort(IntelVersionHandling, FortranCompiler): +class Ifort(FortranCompiler): '''Class for Intel's ifort compiler. :param name: name of this compiler. @@ -465,4 +416,117 @@ 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") + syntax_only_flag="-syntax-only", + version_regex=r"ifort \(IFORT\) (\d[\d\.]+\d) ") + + +# ============================================================================ +# intel-llvm +# ============================================================================ +class Icx(CCompiler): + '''Class for the Intel's new llvm based icx compiler. + + :param name: name of this compiler. + :param exec_name: name of the executable. + ''' + def __init__(self, name: str = "icx", exec_name: str = "icx"): + super().__init__(name, exec_name, suite="intel-llvm", + openmp_flag="-qopenmp", + version_regex=(r"Intel\(R\) oneAPI DPC\+\+/C\+\+ " + r"Compiler (\d[\d\.]+\d) ")) + + +# ============================================================================ +class Ifx(FortranCompiler): + '''Class for Intel's new ifx compiler. + + :param name: name of this compiler. + :param exec_name: name of the executable. + ''' + + def __init__(self, name: str = "ifx", exec_name: str = "ifx"): + super().__init__(name, exec_name, suite="intel-llvm", + module_folder_flag="-module", + openmp_flag="-qopenmp", + syntax_only_flag="-syntax-only", + version_regex=r"ifx \(IFORT\) (\d[\d\.]+\d) ") + + +# ============================================================================ +# nvidia +# ============================================================================ +class Nvc(CCompiler): + '''Class for Nvidia's nvc compiler. Nvc has a '-' in the + version number. In order to get this, we overwrite run_version_command + and replace any '-' with a '.' + + :param name: name of this compiler. + :param exec_name: name of the executable. + ''' + + def __init__(self, name: str = "nvc", exec_name: str = "nvc"): + super().__init__(name, exec_name, suite="nvidia", + openmp_flag="-mp", + version_regex=r"nvc (\d[\d\.]+\d)") + + +# ============================================================================ +class Nvfortran(FortranCompiler): + '''Class for Nvidia's nvfortran compiler. Nvfortran has a '-' in the + version number. In order to get this, we overwrite run_version_command + and replace any '-' with a '.' + + :param name: name of this compiler. + :param exec_name: name of the executable. + ''' + + def __init__(self, name: str = "nvfortran", exec_name: str = "nvfortran"): + super().__init__(name, exec_name, suite="nvidia", + module_folder_flag="-module", + openmp_flag="-mp", + syntax_only_flag="-Msyntax-only", + version_regex=r"nvfortran (\d[\d\.]+\d)") + + +# ============================================================================ +# Cray compiler +# ============================================================================ +class Craycc(CCompiler): + '''Class for the native Cray C compiler. Since cc is actually a compiler + wrapper, follow the naming scheme of a compiler wrapper and call it: + craycc-cc. + + Cray has two different compilers. Older ones have as version number: + Cray C : Version 8.7.0 Tue Jul 23, 2024 07:39:46 + Newer compiler (several lines, the important one): + Cray clang version 15.0.1 (66f7391d6a03cf932f321b9f6b1d8612ef5f362c) + We use the beginning ("cray c") to identify the compiler, which works for + both cray c and cray clang. Then we ignore non-numbers, to reach the + version number which is then extracted. + + :param name: name of this compiler. + :param exec_name: name of the executable. + ''' + def __init__(self, name: str = "craycc-cc", exec_name: str = "cc"): + super().__init__(name, exec_name, suite="cray", mpi=True, + openmp_flag="-homp", + version_regex=r"Cray [Cc][^\d]* (\d[\d\.]+\d)") + + +# ============================================================================ +class Crayftn(FortranCompiler): + '''Class for the native Cray Fortran compiler. Since ftn is actually a + compiler wrapper, follow the naming scheme of Cray compiler wrapper + and call it crayftn-ftn. + + :param name: name of this compiler. + :param exec_name: name of the executable. + ''' + + def __init__(self, name: str = "crayftn-ftn", exec_name: str = "ftn"): + super().__init__(name, exec_name, suite="cray", mpi=True, + module_folder_flag="-J", + openmp_flag="-homp", + syntax_only_flag="-syntax-only", + version_regex=(r"Cray Fortran : Version " + r"(\d[\d\.]+\d)")) diff --git a/source/fab/tools/compiler_wrapper.py b/source/fab/tools/compiler_wrapper.py index 19186b2d..3c5d2997 100644 --- a/source/fab/tools/compiler_wrapper.py +++ b/source/fab/tools/compiler_wrapper.py @@ -36,12 +36,9 @@ def __init__(self, name: str, exec_name: str, name=name, exec_name=exec_name, category=self._compiler.category, suite=self._compiler.suite, + version_regex=self._compiler._version_regex, mpi=mpi, availability_option=self._compiler.availability_option) - # We need to have the right version to parse the version output - # So we set this function based on the function that the - # wrapped compiler uses: - setattr(self, "parse_version_output", compiler.parse_version_output) def __str__(self): return f"{type(self).__name__}({self._compiler.name})" @@ -207,3 +204,29 @@ class Mpicc(CompilerWrapper): def __init__(self, compiler: Compiler): super().__init__(name=f"mpicc-{compiler.name}", exec_name="mpicc", compiler=compiler, mpi=True) + + +# ============================================================================ +class CrayFtnWrapper(CompilerWrapper): + '''Class for the Cray Fortran compiler wrapper. We add 'wrapper' to the + class name to make this class distinct from the Crayftn compiler class. + + :param compiler: the compiler that the ftn wrapper will use. + ''' + + def __init__(self, compiler: Compiler): + super().__init__(name=f"crayftn-{compiler.name}", + exec_name="ftn", compiler=compiler, mpi=True) + + +# ============================================================================ +class CrayCcWrapper(CompilerWrapper): + '''Class for the Cray C compiler wrapper. We add 'wrapper' to the class + name to make this class distinct from the Craycc compiler class + + :param compiler: the compiler that the mpicc wrapper will use. + ''' + + def __init__(self, compiler: Compiler): + super().__init__(name=f"craycc-{compiler.name}", + exec_name="cc", compiler=compiler, mpi=True) diff --git a/source/fab/tools/tool_repository.py b/source/fab/tools/tool_repository.py index 6a077b67..d699574a 100644 --- a/source/fab/tools/tool_repository.py +++ b/source/fab/tools/tool_repository.py @@ -17,8 +17,13 @@ from fab.tools.tool import Tool from fab.tools.category import Category from fab.tools.compiler import Compiler +from fab.tools.compiler_wrapper import (CrayCcWrapper, CrayFtnWrapper, + Mpif90, Mpicc) from fab.tools.linker import Linker 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) class ToolRepository(dict): @@ -57,26 +62,36 @@ 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: - # 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, Psyclone, Rsync) - - for cls in [Gcc, Icc, Gfortran, Ifort, Cpp, CppFortran, - Fcm, Git, Subversion, Ar, Psyclone, Rsync]: + # tools FAB knows about are available). For now, disable Fpp (by not + # adding it). IF someone actually uses it it can added. + for cls in [Craycc, Crayftn, + Gcc, Gfortran, + Icc, Icx, Ifort, Ifx, + Nvc, Nvfortran, + Cpp, CppFortran, + Ar, Fcm, Git, Psyclone, Rsync, Subversion]: self.add_tool(cls()) - from fab.tools.compiler_wrapper import Mpif90, Mpicc + # Now create the potential mpif90 and Cray ftn wrapper all_fc = self[Category.FORTRAN_COMPILER][:] for fc in all_fc: mpif90 = Mpif90(fc) self.add_tool(mpif90) + # I assume cray has (besides cray) only support for Intel and GNU + if fc.name in ["gfortran", "ifort"]: + crayftn = CrayFtnWrapper(fc) + print("NEW NAME", crayftn, crayftn.name) + self.add_tool(crayftn) + # Now create the potential mpicc and Cray cc wrapper all_cc = self[Category.C_COMPILER][:] for cc in all_cc: mpicc = Mpicc(cc) self.add_tool(mpicc) + # I assume cray has (besides cray) only support for Intel and GNU + if cc.name in ["gcc", "icc"]: + craycc = CrayCcWrapper(cc) + self.add_tool(craycc) def add_tool(self, tool: Tool): '''Creates an instance of the specified class and adds it diff --git a/tests/conftest.py b/tests/conftest.py index 559d4f3b..86de6476 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -18,7 +18,8 @@ @pytest.fixture(name="mock_c_compiler") def fixture_mock_c_compiler(): '''Provides a mock C-compiler.''' - mock_compiler = CCompiler("mock_c_compiler", "mock_exec", "suite") + mock_compiler = CCompiler("mock_c_compiler", "mock_exec", "suite", + version_regex="something") mock_compiler.run = mock.Mock() mock_compiler._version = (1, 2, 3) mock_compiler._name = "mock_c_compiler" @@ -32,6 +33,7 @@ def fixture_mock_fortran_compiler(): '''Provides a mock Fortran-compiler.''' mock_compiler = FortranCompiler("mock_fortran_compiler", "mock_exec", "suite", module_folder_flag="", + version_regex="something", syntax_only_flag=None, compile_flag=None, output_flag=None, openmp_flag=None) mock_compiler.run = mock.Mock() diff --git a/tests/unit_tests/tools/test_compiler.py b/tests/unit_tests/tools/test_compiler.py index c31e480e..eaadbbd9 100644 --- a/tests/unit_tests/tools/test_compiler.py +++ b/tests/unit_tests/tools/test_compiler.py @@ -14,13 +14,15 @@ import pytest -from fab.tools import (Category, CCompiler, Compiler, FortranCompiler, - Gcc, Gfortran, Icc, Ifort) +from fab.tools import (Category, CCompiler, Compiler, Craycc, Crayftn, + FortranCompiler, Gcc, Gfortran, Icc, Icx, Ifort, Ifx, + Nvc, Nvfortran) def test_compiler(): '''Test the compiler constructor.''' - cc = Compiler("gcc", "gcc", "gnu", category=Category.C_COMPILER, openmp_flag="-fopenmp") + cc = Compiler("gcc", "gcc", "gnu", version_regex="some_regex", + category=Category.C_COMPILER, openmp_flag="-fopenmp") assert cc.category == Category.C_COMPILER assert cc._compile_flag == "-c" assert cc._output_flag == "-o" @@ -29,13 +31,9 @@ def test_compiler(): assert cc.suite == "gnu" assert not cc.mpi assert cc.openmp_flag == "-fopenmp" - with pytest.raises(NotImplementedError) as err: - cc.parse_version_output(Category.FORTRAN_COMPILER, "NOT NEEDED") - assert ("The method `parse_version_output` must be provided using a mixin." - in str(err.value)) fc = FortranCompiler("gfortran", "gfortran", "gnu", openmp_flag="-fopenmp", - module_folder_flag="-J") + version_regex="something", module_folder_flag="-J") assert fc._compile_flag == "-c" assert fc._output_flag == "-o" assert fc.category == Category.FORTRAN_COMPILER @@ -44,35 +42,32 @@ def test_compiler(): assert fc.flags == [] assert not fc.mpi assert fc.openmp_flag == "-fopenmp" - with pytest.raises(NotImplementedError) as err: - fc.parse_version_output(Category.FORTRAN_COMPILER, "NOT NEEDED") - assert ("The method `parse_version_output` must be provided using a mixin." - in str(err.value)) def test_compiler_openmp(): '''Test that the openmp flag is correctly reflected in the test if a compiler supports OpenMP or not.''' - cc = CCompiler("gcc", "gcc", "gnu", openmp_flag="-fopenmp") + cc = CCompiler("gcc", "gcc", "gnu", openmp_flag="-fopenmp", + version_regex=None) assert cc.openmp_flag == "-fopenmp" assert cc.openmp - cc = CCompiler("gcc", "gcc", "gnu", openmp_flag=None) + cc = CCompiler("gcc", "gcc", "gnu", openmp_flag=None, version_regex=None) assert cc.openmp_flag == "" assert not cc.openmp - cc = CCompiler("gcc", "gcc", "gnu") + cc = CCompiler("gcc", "gcc", "gnu", version_regex=None) assert cc.openmp_flag == "" assert not cc.openmp fc = FortranCompiler("gfortran", "gfortran", "gnu", openmp_flag="-fopenmp", - module_folder_flag="-J") + module_folder_flag="-J", version_regex=None) assert fc.openmp_flag == "-fopenmp" assert fc.openmp fc = FortranCompiler("gfortran", "gfortran", "gnu", openmp_flag=None, - module_folder_flag="-J") + module_folder_flag="-J", version_regex=None) assert fc.openmp_flag == "" assert not fc.openmp fc = FortranCompiler("gfortran", "gfortran", "gnu", - module_folder_flag="-J") + module_folder_flag="-J", version_regex=None) assert fc.openmp_flag == "" assert not fc.openmp @@ -148,16 +143,19 @@ def test_compiler_with_env_fflags(): def test_compiler_syntax_only(): '''Tests handling of syntax only flags.''' fc = FortranCompiler("gfortran", "gfortran", "gnu", + version_regex="something", openmp_flag="-fopenmp", module_folder_flag="-J") # Empty since no flag is defined assert not fc.has_syntax_only fc = FortranCompiler("gfortran", "gfortran", "gnu", openmp_flag="-fopenmp", - module_folder_flag="-J", syntax_only_flag=None) + version_regex="something", module_folder_flag="-J", + syntax_only_flag=None) # Empty since no flag is defined assert not fc.has_syntax_only fc = FortranCompiler("gfortran", "gfortran", "gnu", + version_regex="something", openmp_flag="-fopenmp", module_folder_flag="-J", syntax_only_flag="-fsyntax-only") @@ -168,6 +166,7 @@ def test_compiler_syntax_only(): def test_compiler_without_openmp(): '''Tests that the openmp flag is not used when openmp is not enabled. ''' fc = FortranCompiler("gfortran", "gfortran", "gnu", + version_regex="something", openmp_flag="-fopenmp", module_folder_flag="-J", syntax_only_flag="-fsyntax-only") @@ -184,6 +183,7 @@ def test_compiler_with_openmp(): '''Tests that the openmp flag is used as expected if openmp is enabled. ''' fc = FortranCompiler("gfortran", "gfortran", "gnu", + version_regex="something", openmp_flag="-fopenmp", module_folder_flag="-J", syntax_only_flag="-fsyntax-only") @@ -199,7 +199,7 @@ def test_compiler_with_openmp(): def test_compiler_module_output(): '''Tests handling of module output_flags.''' fc = FortranCompiler("gfortran", "gfortran", suite="gnu", - module_folder_flag="-J") + version_regex="something", module_folder_flag="-J") fc.set_module_output_path("/module_out") assert fc._module_output_path == "/module_out" fc.run = mock.MagicMock() @@ -212,6 +212,7 @@ def test_compiler_module_output(): def test_compiler_with_add_args(): '''Tests that additional arguments are handled as expected.''' fc = FortranCompiler("gfortran", "gfortran", suite="gnu", + version_regex="something", openmp_flag="-fopenmp", module_folder_flag="-J") fc.set_module_output_path("/module_out") @@ -233,6 +234,9 @@ def test_compiler_with_add_args(): openmp=True, syntax_only=True) +# ============================================================================ +# Test version number handling +# ============================================================================ def test_get_version_string(): '''Tests the get_version_string() method. ''' @@ -393,6 +397,8 @@ def test_get_version_bad_result_is_not_cached(): assert c.run.called +# ============================================================================ +# gcc # ============================================================================ def test_gcc(): '''Tests the gcc class.''' @@ -428,6 +434,8 @@ def test_gcc_get_version_with_icc_string(): assert "Unexpected version output format for compiler" in str(err.value) +# ============================================================================ +# gfortran # ============================================================================ def test_gfortran(): '''Tests the gfortran class.''' @@ -512,7 +520,8 @@ def test_gfortran_get_version_12(): """) gfortran = Gfortran() - with mock.patch.object(gfortran, "run", mock.Mock(return_value=full_output)): + with mock.patch.object(gfortran, "run", + mock.Mock(return_value=full_output)): assert gfortran.get_version() == (12, 1, 0) @@ -524,12 +533,16 @@ def test_gfortran_get_version_with_ifort_string(): """) gfortran = Gfortran() - with mock.patch.object(gfortran, "run", mock.Mock(return_value=full_output)): + with mock.patch.object(gfortran, "run", + mock.Mock(return_value=full_output)): with pytest.raises(RuntimeError) as err: gfortran.get_version() - assert "Unexpected version output format for compiler" in str(err.value) + assert ("Unexpected version output format for compiler" + in str(err.value)) +# ============================================================================ +# icc # ============================================================================ def test_icc(): '''Tests the icc class.''' @@ -562,9 +575,12 @@ def test_icc_get_version_with_gcc_string(): with mock.patch.object(icc, "run", mock.Mock(return_value=full_output)): with pytest.raises(RuntimeError) as err: icc.get_version() - assert "Unexpected version output format for compiler" in str(err.value) + assert ("Unexpected version output format for compiler" + in str(err.value)) +# ============================================================================ +# ifort # ============================================================================ def test_ifort(): '''Tests the ifort class.''' @@ -634,7 +650,8 @@ def test_ifort_get_version_with_icc_string(): with mock.patch.object(ifort, "run", mock.Mock(return_value=full_output)): with pytest.raises(RuntimeError) as err: ifort.get_version() - assert "Unexpected version output format for compiler" in str(err.value) + assert ("Unexpected version output format for compiler" + in str(err.value)) @pytest.mark.parametrize("version", ["5.15f.2", @@ -653,4 +670,285 @@ def test_ifort_get_version_invalid_version(version): with mock.patch.object(ifort, "run", mock.Mock(return_value=full_output)): with pytest.raises(RuntimeError) as err: ifort.get_version() - assert "Unexpected version output format for compiler" in str(err.value) + assert ("Unexpected version output format for compiler" + in str(err.value)) + + +# ============================================================================ +# icx +# ============================================================================ +def test_icx(): + '''Tests the icx class.''' + icx = Icx() + assert icx.name == "icx" + assert isinstance(icx, CCompiler) + assert icx.category == Category.C_COMPILER + assert not icx.mpi + + +def test_icx_get_version_2023(): + '''Test icx 2023.0.0 version detection.''' + full_output = dedent(""" +Intel(R) oneAPI DPC++/C++ Compiler 2023.0.0 (2023.0.0.20221201) +Target: x86_64-unknown-linux-gnu +Thread model: posix +InstalledDir: /opt/intel/oneapi/compiler/2023.0.0/linux/bin-llvm +Configuration file: /opt/intel/oneapi/compiler/2023.0.0/linux/bin-llvm/""" + """../bin/icx.cfg + + """) + icx = Icx() + with mock.patch.object(icx, "run", mock.Mock(return_value=full_output)): + assert icx.get_version() == (2023, 0, 0) + + +def test_icx_get_version_with_icc_string(): + '''Tests the icx class with an icc version output.''' + full_output = dedent(""" + icc (ICC) 2021.10.0 20230609 + Copyright (C) 1985-2023 Intel Corporation. All rights reserved. + + """) + icx = Icx() + with mock.patch.object(icx, "run", mock.Mock(return_value=full_output)): + with pytest.raises(RuntimeError) as err: + icx.get_version() + assert ("Unexpected version output format for compiler" + in str(err.value)) + + +# ============================================================================ +# ifx +# ============================================================================ +def test_ifx(): + '''Tests the ifx class.''' + ifx = Ifx() + assert ifx.name == "ifx" + assert isinstance(ifx, FortranCompiler) + assert ifx.category == Category.FORTRAN_COMPILER + assert not ifx.mpi + + +def test_ifx_get_version_2023(): + '''Test ifx 2023.0.0 version detection.''' + full_output = dedent(""" +ifx (IFORT) 2023.0.0 20221201 +Copyright (C) 1985-2022 Intel Corporation. All rights reserved. + + """) + ifx = Ifx() + with mock.patch.object(ifx, "run", mock.Mock(return_value=full_output)): + assert ifx.get_version() == (2023, 0, 0) + + +def test_ifx_get_version_with_ifort_string(): + '''Tests the ifx class with an icc version output.''' + full_output = dedent(""" + ifort (IFORT) 19.0.0.117 20180804 + Copyright (C) 1985-2018 Intel Corporation. All rights reserved. + + """) + ifx = Ifx() + with mock.patch.object(ifx, "run", mock.Mock(return_value=full_output)): + with pytest.raises(RuntimeError) as err: + ifx.get_version() + assert ("Unexpected version output format for compiler" + in str(err.value)) + + +# ============================================================================ +# nvc +# ============================================================================ +def test_nvc(): + '''Tests the nvc class.''' + nvc = Nvc() + assert nvc.name == "nvc" + assert isinstance(nvc, CCompiler) + assert nvc.category == Category.C_COMPILER + assert not nvc.mpi + + +def test_nvc_get_version_23_5_0(): + '''Test nvc 23.5.0 version detection.''' + full_output = dedent(""" + +nvc 23.5-0 64-bit target on x86-64 Linux -tp icelake-server +NVIDIA Compilers and Tools +Copyright (c) 2022, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + """) + nvc = Nvc() + with mock.patch.object(nvc, "run", mock.Mock(return_value=full_output)): + assert nvc.get_version() == (23, 5) + + +def test_nvc_get_version_with_icc_string(): + '''Tests the nvc class with an icc version output.''' + full_output = dedent(""" + icc (ICC) 2021.10.0 20230609 + Copyright (C) 1985-2023 Intel Corporation. All rights reserved. + + """) + nvc = Nvc() + with mock.patch.object(nvc, "run", mock.Mock(return_value=full_output)): + with pytest.raises(RuntimeError) as err: + nvc.get_version() + assert ("Unexpected version output format for compiler" + in str(err.value)) + + +# ============================================================================ +# nvfortran +# ============================================================================ +def test_nvfortran(): + '''Tests the nvfortran class.''' + nvfortran = Nvfortran() + assert nvfortran.name == "nvfortran" + assert isinstance(nvfortran, FortranCompiler) + assert nvfortran.category == Category.FORTRAN_COMPILER + assert not nvfortran.mpi + + +def test_nvfortran_get_version_23_5_0(): + '''Test nvfortran 23.5 version detection.''' + full_output = dedent(""" + +nvfortran 23.5-0 64-bit target on x86-64 Linux -tp icelake-server +NVIDIA Compilers and Tools +Copyright (c) 2022, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + """) + nvfortran = Nvfortran() + with mock.patch.object(nvfortran, "run", + mock.Mock(return_value=full_output)): + assert nvfortran.get_version() == (23, 5) + + +def test_nvfortran_get_version_with_ifort_string(): + '''Tests the nvfortran class with an icc version output.''' + full_output = dedent(""" + ifort (IFORT) 19.0.0.117 20180804 + Copyright (C) 1985-2018 Intel Corporation. All rights reserved. + + """) + nvfortran = Nvfortran() + with mock.patch.object(nvfortran, "run", + mock.Mock(return_value=full_output)): + with pytest.raises(RuntimeError) as err: + nvfortran.get_version() + assert ("Unexpected version output format for compiler" + in str(err.value)) + + +# ============================================================================ +# Craycc +# ============================================================================ +def test_craycc(): + '''Tests the Craycc class.''' + craycc = Craycc() + assert craycc.name == "craycc-cc" + assert isinstance(craycc, CCompiler) + assert craycc.category == Category.C_COMPILER + assert craycc.mpi + + +def test_craycc_get_version_8_7_0(): + '''Test craycc .23.5 version detection.''' + full_output = dedent(""" +Cray C : Version 8.7.0 Tue Jul 23, 2024 07:39:46 + + """) + craycc = Craycc() + with mock.patch.object(craycc, "run", mock.Mock(return_value=full_output)): + assert craycc.get_version() == (8, 7, 0) + + +def test_craycc_get_version_2023(): + '''Test craycc .23.5 version detection.''' + full_output = dedent(""" +Cray clang version 15.0.1 (66f7391d6a03cf932f321b9f6b1d8612ef5f362c) + +Target: x86_64-unknown-linux-gnu + +Thread model: posix + +InstalledDir: /opt/cray/pe/cce/15.0.1/cce-clang/x86_64/share/../bin + +Found candidate GCC installation: /opt/gcc/10.3.0/snos/lib/gcc/x86_64-""" + """suse-linux/10.3.0 + +Selected GCC installation: /opt/gcc/10.3.0/snos/lib/gcc/x86_64-suse-""" + """linux/10.3.0 + +Candidate multilib: .;@m64 + +Selected multilib: .;@m64 + +OFFICIAL + """) + craycc = Craycc() + with mock.patch.object(craycc, "run", mock.Mock(return_value=full_output)): + assert craycc.get_version() == (15, 0, 1) + + +def test_craycc_get_version_with_icc_string(): + '''Tests the Craycc class with an icc version output.''' + full_output = dedent(""" + icc (ICC) 2021.10.0 20230609 + Copyright (C) 1985-2023 Intel Corporation. All rights reserved. + + """) + craycc = Craycc() + with mock.patch.object(craycc, "run", mock.Mock(return_value=full_output)): + with pytest.raises(RuntimeError) as err: + craycc.get_version() + assert ("Unexpected version output format for compiler" + in str(err.value)) + + +# ============================================================================ +# Crayftn +# ============================================================================ +def test_crayftn(): + '''Tests the Crayftn class.''' + crayftn = Crayftn() + assert crayftn.name == "crayftn-ftn" + assert isinstance(crayftn, FortranCompiler) + assert crayftn.category == Category.FORTRAN_COMPILER + assert crayftn.mpi + + +def test_crayftn_get_version_8_7_0(): + '''Test crayftn .23.5 version detection.''' + full_output = dedent(""" +Cray Fortran : Version 8.7.0 Tue Jul 23, 2024 07:39:25 + """) + crayftn = Crayftn() + with mock.patch.object(crayftn, "run", + mock.Mock(return_value=full_output)): + assert crayftn.get_version() == (8, 7, 0) + + +def test_crayftn_get_version_15_0_1(): + '''Test Crayftn 15.0.1 version detection.''' + full_output = dedent(""" +Cray Fortran : Version 15.0.1 Tue Jul 23, 2024 07:39:25 + """) + crayftn = Crayftn() + with mock.patch.object(crayftn, "run", + mock.Mock(return_value=full_output)): + assert crayftn.get_version() == (15, 0, 1) + + +def test_crayftn_get_version_with_ifort_string(): + '''Tests the crayftn class with an icc version output.''' + full_output = dedent(""" + ifort (IFORT) 19.0.0.117 20180804 + Copyright (C) 1985-2018 Intel Corporation. All rights reserved. + + """) + crayftn = Crayftn() + with mock.patch.object(crayftn, "run", + mock.Mock(return_value=full_output)): + with pytest.raises(RuntimeError) as err: + crayftn.get_version() + assert ("Unexpected version output format for compiler" + in str(err.value)) diff --git a/tests/unit_tests/tools/test_compiler_wrapper.py b/tests/unit_tests/tools/test_compiler_wrapper.py index 93b26f14..bcf8d4a7 100644 --- a/tests/unit_tests/tools/test_compiler_wrapper.py +++ b/tests/unit_tests/tools/test_compiler_wrapper.py @@ -12,7 +12,8 @@ import pytest -from fab.tools import (Category, CompilerWrapper, Gcc, Gfortran, Icc, Ifort, +from fab.tools import (Category, CompilerWrapper, CrayCcWrapper, + CrayFtnWrapper, Gcc, Gfortran, Icc, Ifort, Mpicc, Mpif90, ToolRepository) @@ -346,3 +347,47 @@ def test_compiler_wrapper_mpi_ifort(): assert mpi_ifort.category == Category.FORTRAN_COMPILER assert mpi_ifort.mpi assert mpi_ifort.suite == "intel-classic" + + +def test_compiler_wrapper_cray_icc(): + '''Tests the Cray wrapper for icc.''' + craycc = CrayCcWrapper(Icc()) + assert craycc.name == "craycc-icc" + assert str(craycc) == "CrayCcWrapper(icc)" + assert isinstance(craycc, CompilerWrapper) + assert craycc.category == Category.C_COMPILER + assert craycc.mpi + assert craycc.suite == "intel-classic" + + +def test_compiler_wrapper_cray_ifort(): + '''Tests the Cray wrapper for ifort.''' + crayftn = CrayFtnWrapper(Ifort()) + assert crayftn.name == "crayftn-ifort" + assert str(crayftn) == "CrayFtnWrapper(ifort)" + assert isinstance(crayftn, CompilerWrapper) + assert crayftn.category == Category.FORTRAN_COMPILER + assert crayftn.mpi + assert crayftn.suite == "intel-classic" + + +def test_compiler_wrapper_cray_gcc(): + '''Tests the Cray wrapper for gcc.''' + craycc = CrayCcWrapper(Gcc()) + assert craycc.name == "craycc-gcc" + assert str(craycc) == "CrayCcWrapper(gcc)" + assert isinstance(craycc, CompilerWrapper) + assert craycc.category == Category.C_COMPILER + assert craycc.mpi + assert craycc.suite == "gnu" + + +def test_compiler_wrapper_cray_gfortran(): + '''Tests the Cray wrapper for gfortran.''' + crayftn = CrayFtnWrapper(Gfortran()) + assert crayftn.name == "crayftn-gfortran" + assert str(crayftn) == "CrayFtnWrapper(gfortran)" + assert isinstance(crayftn, CompilerWrapper) + assert crayftn.category == Category.FORTRAN_COMPILER + assert crayftn.mpi + assert crayftn.suite == "gnu" diff --git a/tests/unit_tests/tools/test_tool_box.py b/tests/unit_tests/tools/test_tool_box.py index 29bedf30..2e886ea5 100644 --- a/tests/unit_tests/tools/test_tool_box.py +++ b/tests/unit_tests/tools/test_tool_box.py @@ -44,9 +44,11 @@ def test_tool_box_add_tool_replacement(): warning can be disabled.''' tb = ToolBox() - mock_compiler1 = CCompiler("mock_c_compiler1", "mock_exec1", "suite") + mock_compiler1 = CCompiler("mock_c_compiler1", "mock_exec1", "suite", + version_regex="something") mock_compiler1._is_available = True - mock_compiler2 = CCompiler("mock_c_compiler2", "mock_exec2", "suite") + mock_compiler2 = CCompiler("mock_c_compiler2", "mock_exec2", "suite", + version_regex="something") mock_compiler2._is_available = True tb.add_tool(mock_compiler1) diff --git a/tests/unit_tests/tools/test_tool_repository.py b/tests/unit_tests/tools/test_tool_repository.py index 8369668e..e9bfb0c1 100644 --- a/tests/unit_tests/tools/test_tool_repository.py +++ b/tests/unit_tests/tools/test_tool_repository.py @@ -137,7 +137,7 @@ def test_tool_repository_get_default_error_missing_openmp_compiler(): ToolRepository.''' tr = ToolRepository() fc = FortranCompiler("gfortran", "gfortran", "gnu", openmp_flag=None, - module_folder_flag="-J") + module_folder_flag="-J", version_regex=None) with mock.patch.dict(tr, {Category.FORTRAN_COMPILER: [fc]}), \ pytest.raises(RuntimeError) as err: