From 64eb82329f586dd6a927bae5cc7cc9d303fe7a91 Mon Sep 17 00:00:00 2001 From: Joerg Henrichs Date: Tue, 19 May 2026 13:25:33 +1000 Subject: [PATCH 01/12] #567 Added first (untested) draft. --- source/fab/steps/psyclone_transmute.py | 283 +++++++++++++++++++++++++ 1 file changed, 283 insertions(+) create mode 100644 source/fab/steps/psyclone_transmute.py diff --git a/source/fab/steps/psyclone_transmute.py b/source/fab/steps/psyclone_transmute.py new file mode 100644 index 00000000..73686252 --- /dev/null +++ b/source/fab/steps/psyclone_transmute.py @@ -0,0 +1,283 @@ +# ############################################################################## +# (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 +# ############################################################################## +""" +A preprocessor and code generation step using PSyclone's transmute (Fortran to +Fortran) ability. . +https://github.com/stfc/PSyclone + +""" +from dataclasses import dataclass +import logging +import shutil +import warnings +from itertools import chain +from pathlib import Path +from typing import Callable, cast, Iterable, Optional, Union + +from fab.build_config import BuildConfig + +from fab.artefacts import (ArtefactSet, ArtefactsGetter, SuffixFilter) +from fab.steps import run_mp, check_for_errors, step +from fab.tools.category import Category +from fab.tools.psyclone import Psyclone +from fab.util import (log_or_dot, input_to_output_fpath, file_checksum, + file_walk, TimerLogger, string_checksum, + by_type, log_or_dot_finish) + +logger = logging.getLogger(__name__) + + +@dataclass +class MpCommonArgs: + """ + Runtime data for child processes to read. + + Contains data used to calculate the prebuild hash. + + """ + config: BuildConfig + suffix: str + transformation_script: Optional[Callable[[Path, BuildConfig], Path]] + cli_args: list[str] + overrides_folder: Optional[Path] + # filenames (not paths) of hand crafted overrides + override_files: list[str] + + +# any already preprocessed x90 we pulled in +DEFAULT_SOURCE_GETTER = SuffixFilter(ArtefactSet.FORTRAN_COMPILER_FILES, + '.f90') + + +@step +def psyclone_transmute( + config: BuildConfig, + transformation_script: Optional[Callable[[Path, + BuildConfig], Path]] = None, + cli_args: Optional[list[str]] = None, + source_getter: Optional[ArtefactsGetter] = None, + overrides_folder: Optional[Path] = None, + ignore_dependencies: Optional[Iterable[str]] = None, + suffix: Optional[str] = None, + ): + """ + PSyclone runner step. + + .. note:: + + This step reads pre-processed Fortran files and produces replacement + Fortran files. So it must be run before the + :class:`~fab.steps.analyse.Analyse` step. + + This step stores results as prebuilds to speed up subsequent builds. + To generate the prebuild hashes, it analyses the files, storing prebuilt + results for these also. + + :param config: + The :class:`fab.build_config.BuildConfig` object where we can read + settings such as the project workspace folder or the multiprocessing + flag. + :param transformation_script: + The function to get Python transformation script. + It takes in a file path and the config object, and returns the path + of the transformation script or None. If no function is given or the + function returns None, no script will be applied and PSyclone still + runs. + :param cli_args: + Passed through to the psyclone cli tool. + :param source_getter: + Optional override for getting input files from the artefact store. + :param overrides_folder: + Optional folder containing hand-crafted override files. + Must be part of the subsequently analysed source code. + Any file produced by psyclone will be deleted if there is a + corresponding file in this folder. + :param ignore_dependencies: + Third party Fortran module names in USE statements, 'DEPENDS ON' files + and modules to be ignored. + :param suffix: a suffix to be added to create the new filename. + """ + + if not suffix: + suffix = "_transmute" + + cli_args = cli_args or [] + + source_getter = source_getter or DEFAULT_SOURCE_GETTER + fortran_files = source_getter(config.artefact_store) + + # get the data in a payload object for child processes to calculate + # prebuild hashes + mp_payload = _generate_mp_payload(config, overrides_folder, + transformation_script, cli_args, suffix) + + # Run PSyclone. For every file, we get back a tuple of the output file and + # the prebuild + mp_arg = [(fortran_file, mp_payload) for fortran_file in fortran_files] + with TimerLogger(f"running PSyclone transmute on {len(fortran_files)} " + f"Fortran files"): + results = run_mp(config, mp_arg, transmute_one_file) + log_or_dot_finish(logger) + outputs, prebuilds = zip(*results) if results else ((), ()) + check_for_errors(outputs, caller_label='psyclone') + + # flatten the list of lists we got back from run_mp + output_files: set[Path] = set(chain(*by_type(outputs, list))) + prebuild_files: list[Path] = list(chain(*by_type(prebuilds, list))) + + # record the output files in the artefact store for further processing + config.artefact_store.add(ArtefactSet.FORTRAN_COMPILER_FILES, output_files) + outputs_str = "\n".join(map(str, output_files)) + logger.debug(f'psyclone outputs:\n{outputs_str}\n') + + # Mark the prebuilds as being current so the + # cleanup step doesn't delete them + config.add_current_prebuilds(prebuild_files) + prebuilds_str = "\n".join(map(str, prebuild_files)) + logger.debug(f'psyclone prebuilds:\n{prebuilds_str}\n') + + +def _generate_mp_payload(config, + overrides_folder, + transformation_script, + cli_args, + suffix: str) -> MpCommonArgs: + override_files: list[str] = [] + if overrides_folder: + override_files = [f.name for f in file_walk(overrides_folder)] + + return MpCommonArgs( + config=config, + transformation_script=transformation_script, + cli_args=cli_args, + overrides_folder=overrides_folder, + override_files=override_files, + suffix=suffix, + ) + + +def transmute_one_file( + arg: tuple[Path, MpCommonArgs]) -> Union[tuple[Path, Path], + tuple[Exception, None]]: + """ + Transmutes a single file. This function is called in parallel + from psyclone_transmute. + + :param arg: all required data, stored in MpCommonArgs + """ + input_file, mp_payload = arg + config = mp_payload.config + prebuild_folder = config.prebuild_folder + prebuild_hash = _gen_prebuild_hash(input_file, + config, + mp_payload.cli_args, + mp_payload.transformation_script) + + # Create the output file name (with the suffix, and in the output + # folder of Fab) + output_file = input_to_output_fpath(config=config, input_path=input_file) + output_file = input_file.with_stem(output_file.stem + mp_payload.suffix) + output_file.parent.mkdir(parents=True, exist_ok=True) + + prebuild_out = (prebuild_folder / f'{output_file.stem}.{prebuild_hash}.' + f'{output_file.suffix}') + + # First check if we have an override file. If so, copy the override + # file as the expected output file, and delete the prebuild file. + if output_file.name in mp_payload.override_files: + # Help mypy to know that overrides_folder is not None + assert mp_payload.overrides_folder + # there is an override so delete this output file... + logger.warning(f"\nOverride found for '{output_file}'.") + shutil.copy2(mp_payload.overrides_folder / output_file.name, + output_file) + # Delete a prebuild, we do not want to store them + prebuild_out.unlink(missing_ok=True) + + elif prebuild_out.exists(): + msg = f'Found prebuild for {input_file}: {prebuild_out}' + log_or_dot(logger=logger, msg=msg) + shutil.copy2(prebuild_out, output_file) + else: + psyclone = config.tool_box.get_tool(Category.PSYCLONE) + psyclone = cast(Psyclone, psyclone) + try: + transformation_script = mp_payload.transformation_script + logger.info(f"Running PSyclone on '{input_file}'," + f" creating '{output_file}'.") + psyclone.process(config=mp_payload.config, + api=None, + x90_file=input_file, + transformed_file=output_file, + transformation_script=transformation_script, + additional_parameters=mp_payload.cli_args) + + shutil.copy2(output_file, prebuild_out) + msg = f'Created prebuilds for {input_file}: {prebuild_out}' + log_or_dot(logger=logger, msg=msg) + + except Exception as err: + logger.error(err) + return err, None + + return output_file, prebuild_out + + +def _gen_prebuild_hash(input_file: Path, + config: BuildConfig, + cli_args: list[str], + script_func): + """ + Calculate the prebuild hash for this Fortran input file, based on + the source file and the transformation script. + + Changes which must trigger reprocessing of an x90 file: + - input_file source: + - transformation script + - cli args + + :param input_file: Fortran input file. + :param mp_payload: provides config file and command line args + """ + + input_hash = file_checksum(input_file).file_hash + # calculate the transformation script hash for this file + script_hash = 0 + if script_func: + script = script_func(input_file, config) + if script: + script_hash = file_checksum(script).file_hash + if script_hash == 0: + # Only a warning. Running PSyclone without script can be used to + # remove e.g. openmp directives (which PSyclone by default will do). + warnings.warn(f'No transformation script specified for {input_file}.') + + # hash everything which should trigger re-processing + # todo: hash the psyclone version? + return sum([input_hash, + string_checksum(str(cli_args)), + script_hash]) + + +def _check_override(check_path: Path, mp_payload: MpCommonArgs): + """ + Delete the file if there's an override for it. + + Assumes `self.overrides_folder` is not None, and is a flat folder. + + Returns either the override or original path. + + """ + + if check_path.name in mp_payload.override_files: + # there is an override so delete this output file... + logger.warning(f"\nOverride found for '{check_path}'") + check_path.unlink() + # ... and return the override path instead + return mp_payload.overrides_folder / check_path.name # type: ignore + + # we didn't have an override, so continue using this file + return check_path From 1d9822693eb705ac2d49534784c047a592cb4d52 Mon Sep 17 00:00:00 2001 From: Joerg Henrichs Date: Wed, 20 May 2026 00:08:02 +1000 Subject: [PATCH 02/12] #567 More cleanup on transmute step. --- source/fab/steps/psyclone_transmute.py | 67 +++++++++++--------------- 1 file changed, 28 insertions(+), 39 deletions(-) diff --git a/source/fab/steps/psyclone_transmute.py b/source/fab/steps/psyclone_transmute.py index 73686252..15878ddd 100644 --- a/source/fab/steps/psyclone_transmute.py +++ b/source/fab/steps/psyclone_transmute.py @@ -47,21 +47,16 @@ class MpCommonArgs: override_files: list[str] -# any already preprocessed x90 we pulled in -DEFAULT_SOURCE_GETTER = SuffixFilter(ArtefactSet.FORTRAN_COMPILER_FILES, - '.f90') - - @step def psyclone_transmute( config: BuildConfig, + fortran_files: list[Path], transformation_script: Optional[Callable[[Path, BuildConfig], Path]] = None, cli_args: Optional[list[str]] = None, - source_getter: Optional[ArtefactsGetter] = None, - overrides_folder: Optional[Path] = None, - ignore_dependencies: Optional[Iterable[str]] = None, suffix: Optional[str] = None, + overrides_folder: Optional[Path] = None, + artefact_set: Optional[ArtefactSet] = None, ): """ PSyclone runner step. @@ -80,6 +75,7 @@ def psyclone_transmute( The :class:`fab.build_config.BuildConfig` object where we can read settings such as the project workspace folder or the multiprocessing flag. + :param fortran_files: list of files to transform. :param transformation_script: The function to get Python transformation script. It takes in a file path and the config object, and returns the path @@ -88,17 +84,14 @@ def psyclone_transmute( runs. :param cli_args: Passed through to the psyclone cli tool. - :param source_getter: - Optional override for getting input files from the artefact store. :param overrides_folder: Optional folder containing hand-crafted override files. Must be part of the subsequently analysed source code. Any file produced by psyclone will be deleted if there is a corresponding file in this folder. - :param ignore_dependencies: - Third party Fortran module names in USE statements, 'DEPENDS ON' files - and modules to be ignored. :param suffix: a suffix to be added to create the new filename. + :param artefact_set: an optional artefact set. If specified, the + input files names will be replaced with the newly transmuted ones. """ if not suffix: @@ -106,14 +99,13 @@ def psyclone_transmute( cli_args = cli_args or [] - source_getter = source_getter or DEFAULT_SOURCE_GETTER - fortran_files = source_getter(config.artefact_store) - # get the data in a payload object for child processes to calculate # prebuild hashes mp_payload = _generate_mp_payload(config, overrides_folder, transformation_script, cli_args, suffix) + config.prebuild_folder.mkdir(parents=True, exist_ok=True) + # Run PSyclone. For every file, we get back a tuple of the output file and # the prebuild mp_arg = [(fortran_file, mp_payload) for fortran_file in fortran_files] @@ -122,8 +114,17 @@ def psyclone_transmute( results = run_mp(config, mp_arg, transmute_one_file) log_or_dot_finish(logger) outputs, prebuilds = zip(*results) if results else ((), ()) + print("XXX", outputs) + print("XXX", prebuilds) check_for_errors(outputs, caller_label='psyclone') + if artefact_set: + print("REPLACING", fortran_files, "WITH", outputs) + config.artefact_store.replace( + artefact_set, + remove_files=fortran_files, + add_files=outputs) + # flatten the list of lists we got back from run_mp output_files: set[Path] = set(chain(*by_type(outputs, list))) prebuild_files: list[Path] = list(chain(*by_type(prebuilds, list))) @@ -178,11 +179,20 @@ def transmute_one_file( # Create the output file name (with the suffix, and in the output # folder of Fab) - output_file = input_to_output_fpath(config=config, input_path=input_file) + try: + relative_path = input_file.relative_to(config.source_root) + except ValueError: + # Remove leading / to be able to concatenate the input path + # to the output path: + relative_path = input_file.relative_to(Path("/")) + + output_file = config.build_output / relative_path + #output_file = input_to_output_fpath(config=config, input_path=input_file) + print("YYY", input_file,"->", output_file) output_file = input_file.with_stem(output_file.stem + mp_payload.suffix) output_file.parent.mkdir(parents=True, exist_ok=True) - prebuild_out = (prebuild_folder / f'{output_file.stem}.{prebuild_hash}.' + prebuild_out = (prebuild_folder / f'{output_file.stem}.{prebuild_hash}' f'{output_file.suffix}') # First check if we have an override file. If so, copy the override @@ -260,24 +270,3 @@ def _gen_prebuild_hash(input_file: Path, return sum([input_hash, string_checksum(str(cli_args)), script_hash]) - - -def _check_override(check_path: Path, mp_payload: MpCommonArgs): - """ - Delete the file if there's an override for it. - - Assumes `self.overrides_folder` is not None, and is a flat folder. - - Returns either the override or original path. - - """ - - if check_path.name in mp_payload.override_files: - # there is an override so delete this output file... - logger.warning(f"\nOverride found for '{check_path}'") - check_path.unlink() - # ... and return the override path instead - return mp_payload.overrides_folder / check_path.name # type: ignore - - # we didn't have an override, so continue using this file - return check_path From 37abb2e864b7a5254a88d5f87357334efae469f9 Mon Sep 17 00:00:00 2001 From: Joerg Henrichs Date: Wed, 20 May 2026 11:24:48 +1000 Subject: [PATCH 03/12] #567 Small fixed, first testing added. --- source/fab/artefacts.py | 6 +- source/fab/steps/psyclone_transmute.py | 41 +++--- .../steps/test_psyclone_transmute.py | 123 ++++++++++++++++++ 3 files changed, 142 insertions(+), 28 deletions(-) create mode 100644 tests/unit_tests/steps/test_psyclone_transmute.py diff --git a/source/fab/artefacts.py b/source/fab/artefacts.py index 5a66febc..8553e409 100644 --- a/source/fab/artefacts.py +++ b/source/fab/artefacts.py @@ -18,7 +18,7 @@ from collections import defaultdict from enum import auto, Enum from pathlib import Path -from typing import Iterable, Optional, Union +from typing import Iterable, Optional, Sequence, Union from fab.dep_tree import filter_source_tree, AnalysedDependent from fab.util import suffix_filter @@ -115,8 +115,8 @@ def copy_artefacts(self, source: Union[str, ArtefactSet], self.add(dest, self[source]) def replace(self, artefact: Union[str, ArtefactSet], - remove_files: list[Union[str, Path]], - add_files: Union[list[Union[str, Path]], dict]): + remove_files: Union[Sequence[str], Sequence[Path]], + add_files: Union[Sequence[str], Sequence[Path]]): '''Replaces artefacts in one artefact set with other artefacts. This can be used e.g to replace files that have been preprocessed and renamed. There is no requirement for these lists to have the diff --git a/source/fab/steps/psyclone_transmute.py b/source/fab/steps/psyclone_transmute.py index 15878ddd..9ba28d9c 100644 --- a/source/fab/steps/psyclone_transmute.py +++ b/source/fab/steps/psyclone_transmute.py @@ -15,11 +15,12 @@ import warnings from itertools import chain from pathlib import Path -from typing import Callable, cast, Iterable, Optional, Union +from typing import Callable, cast, Optional, Sequence, Union + from fab.build_config import BuildConfig -from fab.artefacts import (ArtefactSet, ArtefactsGetter, SuffixFilter) +from fab.artefacts import ArtefactSet from fab.steps import run_mp, check_for_errors, step from fab.tools.category import Category from fab.tools.psyclone import Psyclone @@ -50,7 +51,7 @@ class MpCommonArgs: @step def psyclone_transmute( config: BuildConfig, - fortran_files: list[Path], + fortran_files: Union[Sequence[Path], Sequence[Path]], transformation_script: Optional[Callable[[Path, BuildConfig], Path]] = None, cli_args: Optional[list[str]] = None, @@ -114,20 +115,19 @@ def psyclone_transmute( results = run_mp(config, mp_arg, transmute_one_file) log_or_dot_finish(logger) outputs, prebuilds = zip(*results) if results else ((), ()) - print("XXX", outputs) - print("XXX", prebuilds) - check_for_errors(outputs, caller_label='psyclone') + output_list = cast(list[str], outputs) + prebuild_list = cast(list[str], prebuilds) + check_for_errors(output_list, caller_label='psyclone') if artefact_set: - print("REPLACING", fortran_files, "WITH", outputs) config.artefact_store.replace( artefact_set, remove_files=fortran_files, - add_files=outputs) + add_files=output_list) # flatten the list of lists we got back from run_mp - output_files: set[Path] = set(chain(*by_type(outputs, list))) - prebuild_files: list[Path] = list(chain(*by_type(prebuilds, list))) + output_files: set[Path] = set(chain(*by_type(output_list, list))) + prebuild_files: list[Path] = list(chain(*by_type(prebuild_list, list))) # record the output files in the artefact store for further processing config.artefact_store.add(ArtefactSet.FORTRAN_COMPILER_FILES, output_files) @@ -171,7 +171,7 @@ def transmute_one_file( """ input_file, mp_payload = arg config = mp_payload.config - prebuild_folder = config.prebuild_folder + prebuild_hash = _gen_prebuild_hash(input_file, config, mp_payload.cli_args, @@ -179,21 +179,12 @@ def transmute_one_file( # Create the output file name (with the suffix, and in the output # folder of Fab) - try: - relative_path = input_file.relative_to(config.source_root) - except ValueError: - # Remove leading / to be able to concatenate the input path - # to the output path: - relative_path = input_file.relative_to(Path("/")) - - output_file = config.build_output / relative_path - #output_file = input_to_output_fpath(config=config, input_path=input_file) - print("YYY", input_file,"->", output_file) - output_file = input_file.with_stem(output_file.stem + mp_payload.suffix) + output_file = input_to_output_fpath(config=config, input_path=input_file) output_file.parent.mkdir(parents=True, exist_ok=True) + output_file = output_file.with_stem(output_file.stem + mp_payload.suffix) - prebuild_out = (prebuild_folder / f'{output_file.stem}.{prebuild_hash}' - f'{output_file.suffix}') + prebuild_out = (config.prebuild_folder / + f'{output_file.stem}.{prebuild_hash}{output_file.suffix}') # First check if we have an override file. If so, copy the override # file as the expected output file, and delete the prebuild file. @@ -229,7 +220,7 @@ def transmute_one_file( msg = f'Created prebuilds for {input_file}: {prebuild_out}' log_or_dot(logger=logger, msg=msg) - except Exception as err: + except RuntimeError as err: logger.error(err) return err, None diff --git a/tests/unit_tests/steps/test_psyclone_transmute.py b/tests/unit_tests/steps/test_psyclone_transmute.py new file mode 100644 index 00000000..02561ba2 --- /dev/null +++ b/tests/unit_tests/steps/test_psyclone_transmute.py @@ -0,0 +1,123 @@ +# ############################################################################## +# (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 PSyclone transmutation step in Fab. It requires PSyclone to +be available (otherwise the tests will be skipped). +""" + +from pathlib import Path +import shutil +from unittest.mock import MagicMock, patch +import warnings + +from pytest import fixture, mark, warns + +from fab.build_config import BuildConfig +from fab.artefacts import ArtefactStore, ArtefactSet +from fab.steps.psyclone_transmute import psyclone_transmute, MpCommonArgs +from fab.tools.psyclone import Psyclone +from fab.tools.tool_box import ToolBox + + +@fixture +def config(tmp_path): + """ + Create a fake workspace with input Fortran files. + """ + src = tmp_path / "src" + src.mkdir() + f1 = src / "a.f90" + f2 = src / "b.f90" + f1.write_text("program a\nend program") + f2.write_text("program b\nend program") + cfg = BuildConfig(project_label="test", + fab_workspace=tmp_path, + tool_box=ToolBox()) + cfg.artefact_store.add(ArtefactSet.FORTRAN_COMPILER_FILES, [f1, f2]) + return cfg + +@mark.skipif(not Psyclone().is_available, reason="psyclone cli tool not available") +def test_psyclone_transmute_basic(config): + """ + Test basic behaviour, without changing any artefact set + """ + + input_files = config.artefact_store[ArtefactSet.FORTRAN_COMPILER_FILES] + # Make a copy to ensure changes to artefact store will be detected + input_files = input_files.copy() + + # Expected files will be in the build output directory and + # have the new suffix `_transmute` added. + expected = {config.build_output / '/'.join(i.parts[1:]) + for i in input_files} + expected = {i.with_stem(i.stem + "_transmute") for i in expected} + + with warns(UserWarning, + match="_metric_send_conn not set, cannot send metrics"): + psyclone_transmute( + config, + input_files) + assert config.artefact_store[ArtefactSet.FORTRAN_COMPILER_FILES] == input_files + + + +@mark.skipif(not Psyclone().is_available, reason="psyclone cli tool not available") +def test_psyclone_transmute_artefact_set(config): + + input_files = config.artefact_store[ArtefactSet.FORTRAN_COMPILER_FILES] + + # Expected files will be in the build output directory and + # have the new suffix `_transmute` added. + expected = {config.build_output / '/'.join(i.parts[1:]) + for i in input_files} + expected = {i.with_stem(i.stem + "_transmute") for i in expected} + + with warns(UserWarning, + match="_metric_send_conn not set, cannot send metrics"): + psyclone_transmute( + config, + config.artefact_store[ArtefactSet.FORTRAN_COMPILER_FILES], + artefact_set=ArtefactSet.FORTRAN_COMPILER_FILES) + output_files = config.artefact_store[ArtefactSet.FORTRAN_COMPILER_FILES] + + assert expected == output_files + #transmuted_input_files = set(i.) + return + + + + + # Fake output directory + out_dir = tmp / "build" + out_dir.mkdir() + + + psyclone_transmute(config=config) + + # --- Assertions --- + + # run_mp called with correct number of jobs + assert mp_mock.called + args, kwargs = mp_mock.call_args + _, mp_arg, func = args + assert func.__name__ == "transmute_one_file" + assert len(mp_arg) == len(fortran_files) + + # Output files added to artefact store + stored = config.artefact_store.get(ArtefactSet.FORTRAN_COMPILER_FILES) + assert len(stored) == len(fortran_files) + for f in stored: + assert f.suffix == ".f90" + assert f.stem.endswith("_transmute") + + # Prebuilds recorded + assert len(config.current_prebuilds) == len(fortran_files) + + # check_for_errors called + assert check_mock.called + + From 3eec703c072d138b10b08935c3f038aef30e0ee5 Mon Sep 17 00:00:00 2001 From: Joerg Henrichs Date: Wed, 20 May 2026 13:40:19 +1000 Subject: [PATCH 04/12] #567 Code cleanup. --- source/fab/steps/psyclone_transmute.py | 16 ++++------ .../steps/test_psyclone_transmute.py | 29 +++++++++++++++++++ 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/source/fab/steps/psyclone_transmute.py b/source/fab/steps/psyclone_transmute.py index 9ba28d9c..d624fc52 100644 --- a/source/fab/steps/psyclone_transmute.py +++ b/source/fab/steps/psyclone_transmute.py @@ -13,7 +13,6 @@ import logging import shutil import warnings -from itertools import chain from pathlib import Path from typing import Callable, cast, Optional, Sequence, Union @@ -26,7 +25,7 @@ from fab.tools.psyclone import Psyclone from fab.util import (log_or_dot, input_to_output_fpath, file_checksum, file_walk, TimerLogger, string_checksum, - by_type, log_or_dot_finish) + log_or_dot_finish) logger = logging.getLogger(__name__) @@ -117,6 +116,7 @@ def psyclone_transmute( outputs, prebuilds = zip(*results) if results else ((), ()) output_list = cast(list[str], outputs) prebuild_list = cast(list[str], prebuilds) + # This call will abort in case of an error check_for_errors(output_list, caller_label='psyclone') if artefact_set: @@ -125,19 +125,15 @@ def psyclone_transmute( remove_files=fortran_files, add_files=output_list) - # flatten the list of lists we got back from run_mp - output_files: set[Path] = set(chain(*by_type(output_list, list))) - prebuild_files: list[Path] = list(chain(*by_type(prebuild_list, list))) - # record the output files in the artefact store for further processing - config.artefact_store.add(ArtefactSet.FORTRAN_COMPILER_FILES, output_files) - outputs_str = "\n".join(map(str, output_files)) + config.artefact_store.add(ArtefactSet.FORTRAN_COMPILER_FILES, output_list) + outputs_str = "\n".join(map(str, output_list)) logger.debug(f'psyclone outputs:\n{outputs_str}\n') # Mark the prebuilds as being current so the # cleanup step doesn't delete them - config.add_current_prebuilds(prebuild_files) - prebuilds_str = "\n".join(map(str, prebuild_files)) + config.add_current_prebuilds(prebuild_list) + prebuilds_str = "\n".join(map(str, prebuild_list)) logger.debug(f'psyclone prebuilds:\n{prebuilds_str}\n') diff --git a/tests/unit_tests/steps/test_psyclone_transmute.py b/tests/unit_tests/steps/test_psyclone_transmute.py index 02561ba2..579df2dc 100644 --- a/tests/unit_tests/steps/test_psyclone_transmute.py +++ b/tests/unit_tests/steps/test_psyclone_transmute.py @@ -34,6 +34,8 @@ def config(tmp_path): f2 = src / "b.f90" f1.write_text("program a\nend program") f2.write_text("program b\nend program") + override = tmp_path / "override" + f1_override = override / "a_override.f90" cfg = BuildConfig(project_label="test", fab_workspace=tmp_path, tool_box=ToolBox()) @@ -89,6 +91,33 @@ def test_psyclone_transmute_artefact_set(config): return +@mark.skipif(not Psyclone().is_available, reason="psyclone cli tool not available") +def test_psyclone_transmute_artefact_override(config): + """ + Test that override directices work. + """ + + input_files = config.artefact_store[ArtefactSet.FORTRAN_COMPILER_FILES] + + # Expected files will be in the build output directory and + # have the new suffix `_transmute` added. + expected = {config.build_output / '/'.join(i.parts[1:]) + for i in input_files} + expected = {i.with_stem(i.stem + "_transmute") for i in expected} + + with warns(UserWarning, + match="_metric_send_conn not set, cannot send metrics"): + psyclone_transmute( + config, + config.artefact_store[ArtefactSet.FORTRAN_COMPILER_FILES], + artefact_set=ArtefactSet.FORTRAN_COMPILER_FILES) + output_files = config.artefact_store[ArtefactSet.FORTRAN_COMPILER_FILES] + + assert expected == output_files + #transmuted_input_files = set(i.) + return + + # Fake output directory From a92cd247a6c6e5be5f8b665801b265c60ec4fd6d Mon Sep 17 00:00:00 2001 From: Joerg Henrichs Date: Thu, 21 May 2026 00:23:31 +1000 Subject: [PATCH 05/12] #567 Cover all lines. --- source/fab/steps/psyclone_transmute.py | 4 +- .../steps/test_psyclone_transmute.py | 146 +++++++++++------- 2 files changed, 91 insertions(+), 59 deletions(-) diff --git a/source/fab/steps/psyclone_transmute.py b/source/fab/steps/psyclone_transmute.py index d624fc52..04c51408 100644 --- a/source/fab/steps/psyclone_transmute.py +++ b/source/fab/steps/psyclone_transmute.py @@ -184,12 +184,12 @@ def transmute_one_file( # First check if we have an override file. If so, copy the override # file as the expected output file, and delete the prebuild file. - if output_file.name in mp_payload.override_files: + if input_file.name in mp_payload.override_files: # Help mypy to know that overrides_folder is not None assert mp_payload.overrides_folder # there is an override so delete this output file... logger.warning(f"\nOverride found for '{output_file}'.") - shutil.copy2(mp_payload.overrides_folder / output_file.name, + shutil.copy2(mp_payload.overrides_folder / input_file.name, output_file) # Delete a prebuild, we do not want to store them prebuild_out.unlink(missing_ok=True) diff --git a/tests/unit_tests/steps/test_psyclone_transmute.py b/tests/unit_tests/steps/test_psyclone_transmute.py index 579df2dc..363bbf3a 100644 --- a/tests/unit_tests/steps/test_psyclone_transmute.py +++ b/tests/unit_tests/steps/test_psyclone_transmute.py @@ -9,22 +9,17 @@ be available (otherwise the tests will be skipped). """ -from pathlib import Path -import shutil -from unittest.mock import MagicMock, patch -import warnings - -from pytest import fixture, mark, warns +from pytest import fixture, mark, raises, warns from fab.build_config import BuildConfig -from fab.artefacts import ArtefactStore, ArtefactSet -from fab.steps.psyclone_transmute import psyclone_transmute, MpCommonArgs +from fab.artefacts import ArtefactSet +from fab.steps.psyclone_transmute import psyclone_transmute from fab.tools.psyclone import Psyclone from fab.tools.tool_box import ToolBox -@fixture -def config(tmp_path): +@fixture(name="config") +def config_fixture(tmp_path): """ Create a fake workspace with input Fortran files. """ @@ -35,14 +30,20 @@ def config(tmp_path): f1.write_text("program a\nend program") f2.write_text("program b\nend program") override = tmp_path / "override" - f1_override = override / "a_override.f90" + override.mkdir() + f1_override = override / "a.f90" + f1_override.write_text("program overwrite_a\nend program") cfg = BuildConfig(project_label="test", fab_workspace=tmp_path, - tool_box=ToolBox()) + tool_box=ToolBox(), + multiprocessing=False, + ) cfg.artefact_store.add(ArtefactSet.FORTRAN_COMPILER_FILES, [f1, f2]) return cfg -@mark.skipif(not Psyclone().is_available, reason="psyclone cli tool not available") + +@mark.skipif(not Psyclone().is_available, + reason="psyclone cli tool not available") def test_psyclone_transmute_basic(config): """ Test basic behaviour, without changing any artefact set @@ -63,11 +64,22 @@ def test_psyclone_transmute_basic(config): psyclone_transmute( config, input_files) - assert config.artefact_store[ArtefactSet.FORTRAN_COMPILER_FILES] == input_files + input_files = config.artefact_store[ArtefactSet.FORTRAN_COMPILER_FILES] + + # Expected files will be in the build output directory and + # have the new suffix `_transmute` added. + expected = {config.build_output / '/'.join(i.parts[1:]) + for i in input_files} + expected = {i.with_stem(i.stem + "_transmute") for i in expected} + + # Since we didn't specify ... XXXXXXXXXX + assert (config.artefact_store[ArtefactSet.FORTRAN_COMPILER_FILES] == + input_files) -@mark.skipif(not Psyclone().is_available, reason="psyclone cli tool not available") +@mark.skipif(not Psyclone().is_available, + reason="psyclone cli tool not available") def test_psyclone_transmute_artefact_set(config): input_files = config.artefact_store[ArtefactSet.FORTRAN_COMPILER_FILES] @@ -80,22 +92,16 @@ def test_psyclone_transmute_artefact_set(config): with warns(UserWarning, match="_metric_send_conn not set, cannot send metrics"): - psyclone_transmute( - config, - config.artefact_store[ArtefactSet.FORTRAN_COMPILER_FILES], - artefact_set=ArtefactSet.FORTRAN_COMPILER_FILES) + psyclone_transmute(config, input_files, + artefact_set=ArtefactSet.FORTRAN_COMPILER_FILES) output_files = config.artefact_store[ArtefactSet.FORTRAN_COMPILER_FILES] assert expected == output_files - #transmuted_input_files = set(i.) - return -@mark.skipif(not Psyclone().is_available, reason="psyclone cli tool not available") -def test_psyclone_transmute_artefact_override(config): - """ - Test that override directices work. - """ +@mark.skipif(not Psyclone().is_available, + reason="psyclone cli tool not available") +def test_psyclone_transmute_script(tmp_path, config): input_files = config.artefact_store[ArtefactSet.FORTRAN_COMPILER_FILES] @@ -105,48 +111,74 @@ def test_psyclone_transmute_artefact_override(config): for i in input_files} expected = {i.with_stem(i.stem + "_transmute") for i in expected} - with warns(UserWarning, - match="_metric_send_conn not set, cannot send metrics"): - psyclone_transmute( - config, - config.artefact_store[ArtefactSet.FORTRAN_COMPILER_FILES], - artefact_set=ArtefactSet.FORTRAN_COMPILER_FILES) - output_files = config.artefact_store[ArtefactSet.FORTRAN_COMPILER_FILES] + script = tmp_path / "script" + script.write_text("invalid python\n") + with raises(RuntimeError) as err: + psyclone_transmute(config, input_files, + transformation_script=lambda _a, _b: script, + artefact_set=ArtefactSet.FORTRAN_COMPILER_FILES) + assert ("expected the script file \\'script\\' to have the \\'.py\\' " + "extension" in str(err)) - assert expected == output_files - #transmuted_input_files = set(i.) - return +@mark.skipif(not Psyclone().is_available, + reason="psyclone cli tool not available") +def test_psyclone_transmute_prebuilt(config): + input_files = \ + config.artefact_store[ArtefactSet.FORTRAN_COMPILER_FILES].copy() + + # Expected files will be in the build output directory and + # have the new suffix `_transmute` added. + expected = {config.build_output / '/'.join(i.parts[1:]) + for i in input_files} + expected = {i.with_stem(i.stem + "_transmute") for i in expected} + with warns(UserWarning, + match="_metric_send_conn not set, cannot send metrics"): + psyclone_transmute(config, input_files, + artefact_set=ArtefactSet.FORTRAN_COMPILER_FILES) - # Fake output directory - out_dir = tmp / "build" - out_dir.mkdir() + output_files = config.artefact_store[ArtefactSet.FORTRAN_COMPILER_FILES] + assert expected == output_files + # Now rerun - remove the preprocessed filed from the previous step + config.artefact_store[ArtefactSet.FORTRAN_COMPILER_FILES] = \ + input_files.copy() - psyclone_transmute(config=config) + # Now it should find prebuilds: + with warns(UserWarning, + match="_metric_send_conn not set, cannot send metrics"): + psyclone_transmute(config, input_files, + artefact_set=ArtefactSet.FORTRAN_COMPILER_FILES) + output_files = config.artefact_store[ArtefactSet.FORTRAN_COMPILER_FILES] - # --- Assertions --- + assert expected == output_files - # run_mp called with correct number of jobs - assert mp_mock.called - args, kwargs = mp_mock.call_args - _, mp_arg, func = args - assert func.__name__ == "transmute_one_file" - assert len(mp_arg) == len(fortran_files) - # Output files added to artefact store - stored = config.artefact_store.get(ArtefactSet.FORTRAN_COMPILER_FILES) - assert len(stored) == len(fortran_files) - for f in stored: - assert f.suffix == ".f90" - assert f.stem.endswith("_transmute") +@mark.skipif(not Psyclone().is_available, + reason="psyclone cli tool not available") +def test_psyclone_transmute_override(tmp_path, config): + """ + Test that override directices work. + """ - # Prebuilds recorded - assert len(config.current_prebuilds) == len(fortran_files) + input_files = config.artefact_store[ArtefactSet.FORTRAN_COMPILER_FILES] - # check_for_errors called - assert check_mock.called + # Expected files will be in the build output directory and + # have the new suffix `_transmute` added. + expected = {config.build_output / '/'.join(i.parts[1:]) + for i in input_files} + expected = {i.with_stem(i.stem + "_transmute") for i in expected} + overrides_folder = tmp_path / "override" + with warns(UserWarning, + match="_metric_send_conn not set, cannot send metrics"): + psyclone_transmute( + config, + config.artefact_store[ArtefactSet.FORTRAN_COMPILER_FILES], + artefact_set=ArtefactSet.FORTRAN_COMPILER_FILES, + overrides_folder=overrides_folder) + output_files = config.artefact_store[ArtefactSet.FORTRAN_COMPILER_FILES] + assert expected == output_files From e797291ae5eb2d591358fd91be53b3160a6c1de8 Mon Sep 17 00:00:00 2001 From: Joerg Henrichs Date: Wed, 27 May 2026 12:51:06 +1000 Subject: [PATCH 06/12] #567 Typing updates. --- source/fab/api.py | 2 ++ source/fab/steps/psyclone.py | 2 +- source/fab/steps/psyclone_transmute.py | 9 +++++---- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/source/fab/api.py b/source/fab/api.py index e447f0b2..435ea4d1 100644 --- a/source/fab/api.py +++ b/source/fab/api.py @@ -26,6 +26,7 @@ from fab.steps.link import link_exe, link_shared_object from fab.steps.preprocess import preprocess_c, preprocess_fortran from fab.steps.psyclone import preprocess_x90, psyclone +from fab.steps.psyclone_transmute import psyclone_transmute from fab.steps.root_inc_files import root_inc_files from fab.tools.category import Category from fab.tools.compiler import Compiler, Ifort @@ -73,6 +74,7 @@ "preprocess_fortran", "preprocess_x90", "psyclone", + "psyclone_transmute", "root_inc_files", "run_mp", "step", diff --git a/source/fab/steps/psyclone.py b/source/fab/steps/psyclone.py index 8a1e2515..e87361eb 100644 --- a/source/fab/steps/psyclone.py +++ b/source/fab/steps/psyclone.py @@ -317,7 +317,7 @@ def do_one_file(arg: tuple[Path, MpCommonArgs]): psyclone = config.tool_box.get_tool(Category.PSYCLONE) if not isinstance(psyclone, Psyclone): raise RuntimeError(f"Unexpected tool '{psyclone.name}' of type " - f"'{type(psyclone)}' instead of Psyclone") + f"'{type(psyclone)}' instead of PSyclone") try: transformation_script = mp_payload.transformation_script logger.info(f"running psyclone on '{x90_file}'.") diff --git a/source/fab/steps/psyclone_transmute.py b/source/fab/steps/psyclone_transmute.py index 04c51408..a29c786e 100644 --- a/source/fab/steps/psyclone_transmute.py +++ b/source/fab/steps/psyclone_transmute.py @@ -94,7 +94,8 @@ def psyclone_transmute( input files names will be replaced with the newly transmuted ones. """ - if not suffix: + # We need to allow "" as suffix indicating to overwrite the source file. + if suffix is None: suffix = "_transmute" cli_args = cli_args or [] @@ -114,10 +115,10 @@ def psyclone_transmute( results = run_mp(config, mp_arg, transmute_one_file) log_or_dot_finish(logger) outputs, prebuilds = zip(*results) if results else ((), ()) - output_list = cast(list[str], outputs) - prebuild_list = cast(list[str], prebuilds) + output_list = cast(list[Path], outputs) + prebuild_list = cast(list[Path], prebuilds) # This call will abort in case of an error - check_for_errors(output_list, caller_label='psyclone') + check_for_errors([str(i) for i in output_list], caller_label='psyclone') if artefact_set: config.artefact_store.replace( From 411e1fd8d84815c4a7d5e7574a69d55e69209e37 Mon Sep 17 00:00:00 2001 From: Joerg Henrichs Date: Wed, 10 Jun 2026 11:44:06 +1000 Subject: [PATCH 07/12] #567 Fixed failing tests, simplified typing. --- source/fab/steps/psyclone_transmute.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/source/fab/steps/psyclone_transmute.py b/source/fab/steps/psyclone_transmute.py index a29c786e..0e7da4b5 100644 --- a/source/fab/steps/psyclone_transmute.py +++ b/source/fab/steps/psyclone_transmute.py @@ -115,26 +115,24 @@ def psyclone_transmute( results = run_mp(config, mp_arg, transmute_one_file) log_or_dot_finish(logger) outputs, prebuilds = zip(*results) if results else ((), ()) - output_list = cast(list[Path], outputs) - prebuild_list = cast(list[Path], prebuilds) # This call will abort in case of an error - check_for_errors([str(i) for i in output_list], caller_label='psyclone') + check_for_errors(outputs, caller_label='psyclone') if artefact_set: config.artefact_store.replace( artefact_set, remove_files=fortran_files, - add_files=output_list) + add_files=outputs) # record the output files in the artefact store for further processing - config.artefact_store.add(ArtefactSet.FORTRAN_COMPILER_FILES, output_list) - outputs_str = "\n".join(map(str, output_list)) + config.artefact_store.add(ArtefactSet.FORTRAN_COMPILER_FILES, outputs) + outputs_str = "\n".join(map(str, outputs)) logger.debug(f'psyclone outputs:\n{outputs_str}\n') # Mark the prebuilds as being current so the # cleanup step doesn't delete them - config.add_current_prebuilds(prebuild_list) - prebuilds_str = "\n".join(map(str, prebuild_list)) + config.add_current_prebuilds(prebuilds) + prebuilds_str = "\n".join(map(str, prebuilds)) logger.debug(f'psyclone prebuilds:\n{prebuilds_str}\n') From 898ad5df7f2d243a6c0cd9ff1ad60faf6f8e1164 Mon Sep 17 00:00:00 2001 From: Joerg Henrichs Date: Wed, 10 Jun 2026 12:07:38 +1000 Subject: [PATCH 08/12] #567 Checked more warnings, added comments and improved testing for prebuilds. --- .../steps/test_psyclone_transmute.py | 51 ++++++++++++++----- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/tests/unit_tests/steps/test_psyclone_transmute.py b/tests/unit_tests/steps/test_psyclone_transmute.py index 363bbf3a..5929645c 100644 --- a/tests/unit_tests/steps/test_psyclone_transmute.py +++ b/tests/unit_tests/steps/test_psyclone_transmute.py @@ -9,7 +9,7 @@ be available (otherwise the tests will be skipped). """ -from pytest import fixture, mark, raises, warns +from pytest import CaptureFixture, fixture, mark, raises, warns from fab.build_config import BuildConfig from fab.artefacts import ArtefactSet @@ -59,8 +59,10 @@ def test_psyclone_transmute_basic(config): for i in input_files} expected = {i.with_stem(i.stem + "_transmute") for i in expected} - with warns(UserWarning, - match="_metric_send_conn not set, cannot send metrics"): + with (warns(UserWarning, + match="_metric_send_conn not set, cannot send metrics"), + warns(UserWarning, + match="No transformation script specified")): psyclone_transmute( config, input_files) @@ -81,6 +83,9 @@ def test_psyclone_transmute_basic(config): @mark.skipif(not Psyclone().is_available, reason="psyclone cli tool not available") def test_psyclone_transmute_artefact_set(config): + """ + Verifies that we get the expected updated artefact set. + """ input_files = config.artefact_store[ArtefactSet.FORTRAN_COMPILER_FILES] @@ -90,8 +95,10 @@ def test_psyclone_transmute_artefact_set(config): for i in input_files} expected = {i.with_stem(i.stem + "_transmute") for i in expected} - with warns(UserWarning, - match="_metric_send_conn not set, cannot send metrics"): + with (warns(UserWarning, + match="_metric_send_conn not set, cannot send metrics"), + warns(UserWarning, + match="No transformation script specified")): psyclone_transmute(config, input_files, artefact_set=ArtefactSet.FORTRAN_COMPILER_FILES) output_files = config.artefact_store[ArtefactSet.FORTRAN_COMPILER_FILES] @@ -102,6 +109,10 @@ def test_psyclone_transmute_artefact_set(config): @mark.skipif(not Psyclone().is_available, reason="psyclone cli tool not available") def test_psyclone_transmute_script(tmp_path, config): + """ + Check that we catch the error if the transformation script does not have + a .py extension (which is a PSyclone requirement). + """ input_files = config.artefact_store[ArtefactSet.FORTRAN_COMPILER_FILES] @@ -123,7 +134,10 @@ def test_psyclone_transmute_script(tmp_path, config): @mark.skipif(not Psyclone().is_available, reason="psyclone cli tool not available") -def test_psyclone_transmute_prebuilt(config): +def test_psyclone_transmute_prebuilt(config, capsys: CaptureFixture): + """ + Tests the handling of existing prebuild files. + """ input_files = \ config.artefact_store[ArtefactSet.FORTRAN_COMPILER_FILES].copy() @@ -134,33 +148,42 @@ def test_psyclone_transmute_prebuilt(config): for i in input_files} expected = {i.with_stem(i.stem + "_transmute") for i in expected} - with warns(UserWarning, - match="_metric_send_conn not set, cannot send metrics"): + with (warns(UserWarning, + match="_metric_send_conn not set, cannot send metrics"), + warns(UserWarning, + match="No transformation script specified")): psyclone_transmute(config, input_files, artefact_set=ArtefactSet.FORTRAN_COMPILER_FILES) output_files = config.artefact_store[ArtefactSet.FORTRAN_COMPILER_FILES] assert expected == output_files + captured = capsys.readouterr() + assert "Found prebuild for" not in captured.out + # Now rerun - remove the preprocessed filed from the previous step config.artefact_store[ArtefactSet.FORTRAN_COMPILER_FILES] = \ input_files.copy() # Now it should find prebuilds: - with warns(UserWarning, - match="_metric_send_conn not set, cannot send metrics"): + with (warns(UserWarning, + match="_metric_send_conn not set, cannot send metrics"), + warns(UserWarning, + match="No transformation script specified")): psyclone_transmute(config, input_files, artefact_set=ArtefactSet.FORTRAN_COMPILER_FILES) output_files = config.artefact_store[ArtefactSet.FORTRAN_COMPILER_FILES] assert expected == output_files + captured = capsys.readouterr() + assert "Found prebuild for" in captured.out @mark.skipif(not Psyclone().is_available, reason="psyclone cli tool not available") def test_psyclone_transmute_override(tmp_path, config): """ - Test that override directices work. + Test that the override directive works. """ input_files = config.artefact_store[ArtefactSet.FORTRAN_COMPILER_FILES] @@ -172,8 +195,10 @@ def test_psyclone_transmute_override(tmp_path, config): expected = {i.with_stem(i.stem + "_transmute") for i in expected} overrides_folder = tmp_path / "override" - with warns(UserWarning, - match="_metric_send_conn not set, cannot send metrics"): + with (warns(UserWarning, + match="_metric_send_conn not set, cannot send metrics"), + warns(UserWarning, + match="No transformation script specified")): psyclone_transmute( config, config.artefact_store[ArtefactSet.FORTRAN_COMPILER_FILES], From 975de826059187de8a21536dcc756077c632d92a Mon Sep 17 00:00:00 2001 From: Joerg Henrichs Date: Fri, 26 Jun 2026 23:14:09 +1000 Subject: [PATCH 09/12] #567 Fixed failing test by ensuring debug logging is enabled. --- .../steps/test_psyclone_transmute.py | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/tests/unit_tests/steps/test_psyclone_transmute.py b/tests/unit_tests/steps/test_psyclone_transmute.py index 5929645c..27fc2df1 100644 --- a/tests/unit_tests/steps/test_psyclone_transmute.py +++ b/tests/unit_tests/steps/test_psyclone_transmute.py @@ -9,7 +9,9 @@ be available (otherwise the tests will be skipped). """ -from pytest import CaptureFixture, fixture, mark, raises, warns +import logging + +from pytest import fixture, mark, raises, warns from fab.build_config import BuildConfig from fab.artefacts import ArtefactSet @@ -134,7 +136,7 @@ def test_psyclone_transmute_script(tmp_path, config): @mark.skipif(not Psyclone().is_available, reason="psyclone cli tool not available") -def test_psyclone_transmute_prebuilt(config, capsys: CaptureFixture): +def test_psyclone_transmute_prebuilt(config, caplog): """ Tests the handling of existing prebuild files. """ @@ -148,18 +150,20 @@ def test_psyclone_transmute_prebuilt(config, capsys: CaptureFixture): for i in input_files} expected = {i.with_stem(i.stem + "_transmute") for i in expected} + # Make sure debug messages are enabled so we can check for + # messages about prebuilds. logging.DEBUG is required to ensure that. with (warns(UserWarning, match="_metric_send_conn not set, cannot send metrics"), warns(UserWarning, match="No transformation script specified")): - psyclone_transmute(config, input_files, - artefact_set=ArtefactSet.FORTRAN_COMPILER_FILES) + with caplog.at_level(logging.DEBUG, + logger="fab.steps.psyclone_transmute"): + psyclone_transmute(config, input_files, + artefact_set=ArtefactSet.FORTRAN_COMPILER_FILES) output_files = config.artefact_store[ArtefactSet.FORTRAN_COMPILER_FILES] assert expected == output_files - - captured = capsys.readouterr() - assert "Found prebuild for" not in captured.out + assert "Found prebuild for" not in caplog.text # Now rerun - remove the preprocessed filed from the previous step config.artefact_store[ArtefactSet.FORTRAN_COMPILER_FILES] = \ @@ -170,13 +174,14 @@ def test_psyclone_transmute_prebuilt(config, capsys: CaptureFixture): match="_metric_send_conn not set, cannot send metrics"), warns(UserWarning, match="No transformation script specified")): - psyclone_transmute(config, input_files, - artefact_set=ArtefactSet.FORTRAN_COMPILER_FILES) + with caplog.at_level(logging.DEBUG, + logger="fab.steps.psyclone_transmute"): + psyclone_transmute(config, input_files, + artefact_set=ArtefactSet.FORTRAN_COMPILER_FILES) output_files = config.artefact_store[ArtefactSet.FORTRAN_COMPILER_FILES] assert expected == output_files - captured = capsys.readouterr() - assert "Found prebuild for" in captured.out + assert "Found prebuild for" in caplog.text @mark.skipif(not Psyclone().is_available, From 2517a315540cd8c9865823aa4842e39e23c0265c Mon Sep 17 00:00:00 2001 From: Joerg Henrichs Date: Fri, 26 Jun 2026 23:58:58 +1000 Subject: [PATCH 10/12] #567 Removed unused code. --- .../steps/test_psyclone_transmute.py | 20 +------------------ 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/tests/unit_tests/steps/test_psyclone_transmute.py b/tests/unit_tests/steps/test_psyclone_transmute.py index 27fc2df1..d2a41b0c 100644 --- a/tests/unit_tests/steps/test_psyclone_transmute.py +++ b/tests/unit_tests/steps/test_psyclone_transmute.py @@ -55,12 +55,6 @@ def test_psyclone_transmute_basic(config): # Make a copy to ensure changes to artefact store will be detected input_files = input_files.copy() - # Expected files will be in the build output directory and - # have the new suffix `_transmute` added. - expected = {config.build_output / '/'.join(i.parts[1:]) - for i in input_files} - expected = {i.with_stem(i.stem + "_transmute") for i in expected} - with (warns(UserWarning, match="_metric_send_conn not set, cannot send metrics"), warns(UserWarning, @@ -71,13 +65,7 @@ def test_psyclone_transmute_basic(config): input_files = config.artefact_store[ArtefactSet.FORTRAN_COMPILER_FILES] - # Expected files will be in the build output directory and - # have the new suffix `_transmute` added. - expected = {config.build_output / '/'.join(i.parts[1:]) - for i in input_files} - expected = {i.with_stem(i.stem + "_transmute") for i in expected} - - # Since we didn't specify ... XXXXXXXXXX + # Since we didn't specify a suffix, we should still have the same file assert (config.artefact_store[ArtefactSet.FORTRAN_COMPILER_FILES] == input_files) @@ -118,12 +106,6 @@ def test_psyclone_transmute_script(tmp_path, config): input_files = config.artefact_store[ArtefactSet.FORTRAN_COMPILER_FILES] - # Expected files will be in the build output directory and - # have the new suffix `_transmute` added. - expected = {config.build_output / '/'.join(i.parts[1:]) - for i in input_files} - expected = {i.with_stem(i.stem + "_transmute") for i in expected} - script = tmp_path / "script" script.write_text("invalid python\n") with raises(RuntimeError) as err: From 6af691f2744f954b4453bd9287adc5af57ef2082 Mon Sep 17 00:00:00 2001 From: Joerg Henrichs Date: Tue, 28 Jul 2026 14:53:27 +1000 Subject: [PATCH 11/12] #567 Update contributors. --- CONTRIBUTORS.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 04815c18..9476d6e8 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -10,10 +10,11 @@ To indicate your agreement, add your details to the the following table. If you are not submitting contributions on behalf of an organisation please use "n/a" for your affiliation. -| GitHub Username | Real Name | Affiliation | -|-----------------|-----------------|-------------| -| MatthewHambley | Matthew Hambley | Met Office | -| yaswant | Yaswant Pradhan | Met Office | +| GitHub Username | Real Name | Affiliation | +|-----------------|-----------------|----------------------------------| +| MatthewHambley | Matthew Hambley | Met Office | +| yaswant | Yaswant Pradhan | Met Office | +| hiker | Joerg Henrichs | Bureau of Meteorology, Australia | --- From e83ed6097bfb00402fec158d2ac8440329a7317a Mon Sep 17 00:00:00 2001 From: Joerg Henrichs Date: Mon, 24 Aug 2026 15:32:07 +1000 Subject: [PATCH 12/12] #567 Fixed typo. --- source/fab/steps/psyclone_transmute.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/fab/steps/psyclone_transmute.py b/source/fab/steps/psyclone_transmute.py index 0e7da4b5..682acfe1 100644 --- a/source/fab/steps/psyclone_transmute.py +++ b/source/fab/steps/psyclone_transmute.py @@ -5,7 +5,7 @@ # ############################################################################## """ A preprocessor and code generation step using PSyclone's transmute (Fortran to -Fortran) ability. . +Fortran) ability. https://github.com/stfc/PSyclone """