diff --git a/source/fab/tools/category.py b/source/fab/tools/category.py index 3b4223d6..fa9e8711 100644 --- a/source/fab/tools/category.py +++ b/source/fab/tools/category.py @@ -116,6 +116,7 @@ def is_compiler(self) -> bool: FORTRAN_PREPROCESSOR: Category GIT: Category LINKER: Category + PFUNIT: Category PSYCLONE: Category RSYNC: Category SHELL: Category @@ -125,7 +126,10 @@ def is_compiler(self) -> bool: CATEGORY_FOR_UNIT_TESTS: Category -# Now create the default categories that Fab needs +# Now create the default categories for all categories that +# have more than one implementation. All tools that have only +# one class here, the corresponding class will create the +# required category. Category.add("C_COMPILER") Category.add("C_PREPROCESSOR") Category.add("FORTRAN_COMPILER") diff --git a/source/fab/tools/pfunit.py b/source/fab/tools/pfunit.py new file mode 100644 index 00000000..0bc42d53 --- /dev/null +++ b/source/fab/tools/pfunit.py @@ -0,0 +1,72 @@ +############################################################################## +# (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 Rsync class for synchronising file trees. +""" + +import logging +import os +from pathlib import Path + +from fab.tools.tool import Tool +from fab.tools.category import Category + + +logger = logging.getLogger(__name__) + + +class PfUnit(Tool): + """ + This is a class to encapsulate pFUnit. It relies on the environment + variable $PFUNIT to indicate the location of the source code. + This is required since besides .mod files and executable, it also + contains the source code for a Fortran driver program . + It assumes that pFUnit's preprocessor `funitproc` is in $PFUNIT/bin. + """ + Category.add("PFUNIT") + + def __init__(self): + pfunit_home = os.environ.get("PFUNIT", "") + if not pfunit_home: + logger.error("$PFUNIT not defined in environment, pFUnit will " + "likely not work.") + self._pfunit_home = Path(pfunit_home) + + exec_name = self._pfunit_home / "bin" / "funitproc" + super().__init__("funitproc", exec_name=exec_name, + category=Category.PFUNIT, + availability_option="-v") + + def get_root_path(self) -> Path: + """ + :returns: the root path of pFUnit. + """ + return self._pfunit_home + + def get_include_path(self) -> Path: + """ + :returns: the include directory for PFUnit. + """ + return self._pfunit_home / "include" + + def get_driver_f90(self) -> str: + """ + :returns: the content of pFUnit's driver.F90 file. + """ + driver_path = self._pfunit_home / "include" / "driver.F90" + with driver_path.open("r", encoding='utf-8') as f: + driver_f90 = f.read() + return driver_f90 + + def process(self, pf_path: Path, + f90_out_path: Path): + """ + Processes the .pf file to create an output f90 file. + + :param pf_path: the input path. + :param f90_out_path: destination path. + """ + return self.run(additional_parameters=[pf_path, f90_out_path]) diff --git a/source/fab/tools/tool_repository.py b/source/fab/tools/tool_repository.py index b4eddde0..d476f957 100644 --- a/source/fab/tools/tool_repository.py +++ b/source/fab/tools/tool_repository.py @@ -26,6 +26,7 @@ from fab.tools.preprocessor import Cpp, CppFortran from fab.tools.compiler import (Craycc, Crayftn, Gcc, Gfortran, Icc, Icx, Ifort, Ifx, Nvc, Nvfortran) +from fab.tools.pfunit import PfUnit from fab.tools.psyclone import Psyclone from fab.tools.rsync import Rsync from fab.tools.shell import Shell @@ -74,7 +75,7 @@ def __init__(self): Icc, Icx, Ifort, Ifx, Nvc, Nvfortran, Cpp, CppFortran, - Ar, Fcm, Git, Psyclone, Rsync, Subversion]: + Ar, Fcm, Git, PfUnit, Psyclone, Rsync, Subversion]: self.add_tool(cls()) # Add a standard shell. Additional shells (bash, ksh, dash) diff --git a/tests/unit_tests/tools/test_pfunit.py b/tests/unit_tests/tools/test_pfunit.py new file mode 100644 index 00000000..48f0644e --- /dev/null +++ b/tests/unit_tests/tools/test_pfunit.py @@ -0,0 +1,135 @@ +############################################################################## +# (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 'pfunit' tool. +""" + +import logging +from pathlib import Path + +from pytest_subprocess.fake_process import FakeProcess + + +from fab.tools.category import Category +from fab.tools.pfunit import PfUnit + +from tests.conftest import ExtendedRecorder, call_list + + +def test_pfunit_constructor_no_env(monkeypatch, caplog) -> None: + """ + Tests constructor when $PFUNIT is not defined + """ + # Make sure the environment variable PFUNIT is not defined: + monkeypatch.delenv("PFUNIT", raising=False) + + with caplog.at_level(logging.ERROR): + pfunit = PfUnit() + assert ("$PFUNIT not defined in environment, pFUnit will likely " + "not work." in caplog.text) + assert len(caplog.records) == 1 + assert caplog.records[0].levelname == "ERROR" + + assert pfunit.category == Category.PFUNIT + assert pfunit.name == "funitproc" + assert pfunit.exec_name == "funitproc" + assert pfunit.get_flags() == [] + + +def test_pfunit_constructor_with_env(monkeypatch, caplog) -> None: + """ + Tests constructor when $PFUNIT is defined + """ + + # Make sure the environment variable PFUNIT is defined: + monkeypatch.setenv("PFUNIT", "/tmp") + + with caplog.at_level(logging.ERROR): + pfunit = PfUnit() + assert len(caplog.records) == 0 + assert pfunit.category == Category.PFUNIT + assert pfunit.name == "funitproc" + assert pfunit.exec_name == "funitproc" + assert pfunit.get_flags() == [] + assert pfunit.get_root_path() == Path("/tmp") + + +def test_pfunit_paths(monkeypatch) -> None: + """ + Tests root and include paths. + """ + + # Make sure the environment variable PFUNIT is defined: + monkeypatch.setenv("PFUNIT", "/tmp") + + pfunit = PfUnit() + assert pfunit.get_root_path() == Path("/tmp") + assert pfunit.get_include_path() == Path("/tmp/include") + + +def test_pfunit_driver(monkeypatch, tmp_path: Path) -> None: + """ + Tests that pfunit reads the driver.F90 file: + """ + + # Make sure the environment variable PFUNIT is defined: + monkeypatch.setenv("PFUNIT", str(tmp_path)) + include_path = tmp_path / "include" + include_path.mkdir() + (include_path / "driver.F90").write_text("DRIVER\n") + + pfunit = PfUnit() + assert pfunit.get_driver_f90() == "DRIVER\n" + + +def test_pfunit_check_available(monkeypatch, + subproc_record: ExtendedRecorder) -> None: + """ + Tests availability functionality. + """ + monkeypatch.setenv("PFUNIT", "/tmp") + pfunit = PfUnit() + assert pfunit.check_available() + assert subproc_record.invocations() == [["/tmp/bin/funitproc", "-v"]] + assert subproc_record.extras() == [{'cwd': None, + 'env': None, + 'stdout': None, + 'stderr': None}] + + +def test_pfunit_check_unavailable(monkeypatch, + fake_process: FakeProcess) -> None: + """ + Tests availability failure. + """ + monkeypatch.setenv("PFUNIT", "/tmp") + fake_process.register(['/tmp/bin/funitproc', '-v'], + returncode=1, + stderr="Something went wrong.") + pfunit = PfUnit() + assert not pfunit.check_available() + assert call_list(fake_process) == [["/tmp/bin/funitproc", "-v"]] + + +def test_pfunit_process(monkeypatch, + tmp_path: Path, + subproc_record: ExtendedRecorder) -> None: + """ + Tests processing a file + """ + monkeypatch.setenv("PFUNIT", str(tmp_path)) + pfunit = PfUnit() + pfunit.process(pf_path=tmp_path / "file.pf", + f90_out_path=tmp_path / "file.f90") + + assert subproc_record.invocations() \ + == [[str(tmp_path / "bin" / "funitproc"), + str(tmp_path / "file.pf"), + str(tmp_path / "file.f90")]] + assert subproc_record.extras() == [{'cwd': None, + 'env': None, + 'stderr': None, + 'stdout': None}]