diff --git a/changelog b/changelog index 7828991949..dd1e2a998c 100644 --- a/changelog +++ b/changelog @@ -1,3 +1,5 @@ + 25) PR #3447 for #3250. Add missing tests to achieve 100% code coverage. + 24) PR #3429 for #3412. Generalise hoist_arguments_to_temporaries in NEMO utils to all arguments with array operations. diff --git a/src/psyclone/domain/lfric/lfric_invoke.py b/src/psyclone/domain/lfric/lfric_invoke.py index 852d2449b3..7abd1375cb 100644 --- a/src/psyclone/domain/lfric/lfric_invoke.py +++ b/src/psyclone/domain/lfric/lfric_invoke.py @@ -44,9 +44,6 @@ from psyclone.configuration import Config from psyclone.domain.lfric.lfric_builtins import LFRicBuiltIn -if TYPE_CHECKING: # pragma: no cover - from psyclone.domain.common.psylayer import GlobalReduction - from psyclone.domain.lfric.lfric_invokes import LFRicInvokes from psyclone.domain.lfric.lfric_loop import LFRicLoop from psyclone.errors import FieldNotFoundError, GenerationError, InternalError from psyclone.parse.algorithm import InvokeCall @@ -55,6 +52,10 @@ from psyclone.psyir.symbols import ( ContainerSymbol, RoutineSymbol, ImportInterface, DataSymbol, ScalarType) +if TYPE_CHECKING: + from psyclone.domain.common.psylayer import GlobalReduction + from psyclone.domain.lfric.lfric_invokes import LFRicInvokes + class LFRicInvoke(Invoke): ''' diff --git a/src/psyclone/tests/conftest.py b/src/psyclone/tests/conftest.py index 4cc88b89e8..195128aa48 100644 --- a/src/psyclone/tests/conftest.py +++ b/src/psyclone/tests/conftest.py @@ -84,7 +84,7 @@ def pytest_addoption(parser): @pytest.fixture -def have_graphviz(): # pragma: no-cover +def have_graphviz(): ''' Whether or not the system has graphviz installed. This refers to the underlying system library, not the python bindings that are provided by 'import graphviz'. ''' @@ -92,9 +92,9 @@ def have_graphviz(): # pragma: no-cover import graphviz try: graphviz.version() + return True except graphviz.ExecutableNotFound: return False - return True @pytest.fixture(scope="session", autouse=True) diff --git a/src/psyclone/tests/conftest_test.py b/src/psyclone/tests/conftest_test.py new file mode 100644 index 0000000000..ee6fe5f355 --- /dev/null +++ b/src/psyclone/tests/conftest_test.py @@ -0,0 +1,33 @@ +# ----------------------------------------------------------------------------- +# BSD 3-Clause License +# +# Copyright (c) 2026, Science and Technology Facilities Council. +# All rights reserved. +# ----------------------------------------------------------------------------- + +'''Tests for helpers defined in the test-suite conftest module.''' + +import graphviz + +from psyclone.tests import conftest as psyconftest + + +def test_have_graphviz_missing_executable(monkeypatch): + '''Check that have_graphviz reports False when the graphviz executable is + not available. + + ''' + def existing_graphviz(): + '''Returns nothing.''' + pass + + def missing_graphviz(): + '''Raise the same exception graphviz emits when binaries are absent.''' + raise graphviz.ExecutableNotFound("not found") + + # 'have_graphviz' is a fixture and cannot be called directly, but we can + # do it through its `__wrapped__` method + monkeypatch.setattr(graphviz, "version", existing_graphviz) + assert psyconftest.have_graphviz.__wrapped__() is True + monkeypatch.setattr(graphviz, "version", missing_graphviz) + assert psyconftest.have_graphviz.__wrapped__() is False diff --git a/src/psyclone/tests/lfric_lma_test.py b/src/psyclone/tests/lfric_lma_test.py index 4d64270c3f..939523c514 100644 --- a/src/psyclone/tests/lfric_lma_test.py +++ b/src/psyclone/tests/lfric_lma_test.py @@ -983,6 +983,14 @@ def test_operators(fortran_writer): end module dummy_mod """ == generated_code + # Try with unsupported types + lma_args = args_filter(kernel.arguments.args, arg_types=["gh_operator"]) + lma_args[0]._intrinsic_type = "integer" + generated_code = fortran_writer(kernel.gen_stub) + assert "dimension(op_1_ncell_3d,ndf_w0,ndf_w0), intent(inout) :: op_1" \ + in generated_code + assert "integer(kind=" in generated_code + # Try with unsupported types lma_args = args_filter(kernel.arguments.args, arg_types=["gh_operator"]) lma_args[0]._intrinsic_type = "logical" diff --git a/src/psyclone/tests/lfric_test.py b/src/psyclone/tests/lfric_test.py index 38b806d502..dcb499f3db 100644 --- a/src/psyclone/tests/lfric_test.py +++ b/src/psyclone/tests/lfric_test.py @@ -54,7 +54,8 @@ from psyclone.domain.lfric.transformations import LFRicLoopFuseTrans from psyclone.lfric import ( LFRicACCEnterDataDirective, LFRicBoundaryConditions, - LFRicKernelArgument, LFRicKernelArguments, LFRicProxies, HaloReadAccess, + LFRicKernelArgument, LFRicKernelArguments, LFRicProxies, HaloDepth, + HaloReadAccess, KernCallArgList) from psyclone.errors import FieldNotFoundError, GenerationError, InternalError from psyclone.gen_kernel_stub import generate @@ -1486,6 +1487,91 @@ def test_arg_ref_name_method_error2(): "type 'gh_funky_instigator'" in str(excinfo.value)) +def test_arg_ref_name_method_error3(monkeypatch): + '''Test error handling for an operator argument when the supplied + function-space matches the argument but not either descriptor endpoint. + + ''' + _, invoke_info = parse(os.path.join(BASE_PATH, "10_operator.f90"), + api=TEST_API) + psy = PSyFactory(TEST_API, distributed_memory=True).create(invoke_info) + first_invoke = psy.invokes.invoke_list[0] + first_kernel = first_invoke.schedule.coded_kernels()[0] + first_argument = first_kernel.arguments.args[0] + + descriptor_type = type(first_argument.descriptor) + monkeypatch.setattr(descriptor_type, "function_space_from", + property(lambda self: "w_broken_from")) + monkeypatch.setattr(descriptor_type, "function_space_to", + property(lambda self: "w_broken_to")) + + with pytest.raises(GenerationError) as excinfo: + _ = first_argument.ref_name(first_argument.function_spaces[0]) + assert ("is one of the 'gh_operator' function spaces" in + str(excinfo.value)) + + +def test_arg_proxy_name_indexed_vector(): + '''Check that proxy_name_indexed includes an explicit (1) index for a + vector argument. + + ''' + _, invoke_info = parse(os.path.join(BASE_PATH, "1_single_invoke.f90"), + api=TEST_API) + psy = PSyFactory(TEST_API, distributed_memory=True).create(invoke_info) + first_invoke = psy.invokes.invoke_list[0] + first_kernel = first_invoke.schedule.coded_kernels()[0] + first_argument = first_kernel.arguments.args[1] + first_argument._vector_size = 2 + assert first_argument.proxy_name_indexed == "f1_proxy(1)" + + +def test_mesh_properties_initialise_invalid_property(): + '''Check that unsupported mesh properties are rejected during + initialisation code generation. + + ''' + _, invoke_info = parse( + os.path.join(BASE_PATH, "24.1_mesh_prop_invoke.f90"), + api=TEST_API) + psy = PSyFactory(TEST_API, distributed_memory=False).create(invoke_info) + invoke = psy.invokes.invoke_list[0] + invoke.setup_psy_layer_symbols() + invoke.mesh_properties._properties.append("not-a-property") + + with pytest.raises(InternalError) as err: + invoke.mesh_properties.initialise(0) + assert "Found unsupported mesh property 'not-a-property'" in str(err.value) + + +def test_halo_depth_parent_type_error(): + '''Check validation of the parent argument to HaloDepth.''' + with pytest.raises(TypeError) as err: + _ = HaloDepth(parent="not-a-node") + assert "HaloDepth parent argument must be a Node" in str(err.value) + + +def test_iteration_space_arg_error_empty_args(): + '''Check that iteration_space_arg() raises the expected error when no + field/operator arguments are present. + + ''' + _, invoke_info = parse(os.path.join(BASE_PATH, "1_single_invoke.f90"), + api=TEST_API) + psy = PSyFactory(TEST_API, distributed_memory=True).create(invoke_info) + first_invoke = psy.invokes.invoke_list[0] + first_kernel = first_invoke.schedule.coded_kernels()[0] + arguments = first_kernel.arguments + saved_args = arguments._args + arguments._args = [] + try: + with pytest.raises(GenerationError) as err: + arguments.iteration_space_arg() + assert "None of these were found." in str(err.value) + finally: + arguments._args = saved_args + + def test_arg_intent_error(): ''' Tests that an internal error is raised in LFRicKernelArgument when intent() is called and the argument access property is not one of @@ -3512,6 +3598,35 @@ def test_haloex_not_required(monkeypatch): assert haloex.required() == (False, True) +def test_haloex_required_max_depth_clean_outer(monkeypatch): + '''Check the required() logic branch where the full halo is known clean + due to redundant computation. + + ''' + _, info = parse(os.path.join(BASE_PATH, "1_single_invoke_w3.f90"), + api=TEST_API) + psy = PSyFactory(TEST_API, distributed_memory=True).create(info) + invoke = psy.invokes.invoke_list[0] + haloex = invoke.schedule.children[0] + + class DummyReadInfo: # pylint: disable=too-few-public-methods + '''Minimal read-info object for required().''' + max_depth = False + annexed_only = False + + class DummyWriteInfo: # pylint: disable=too-few-public-methods + '''Minimal write-info object for required().''' + max_depth = True + dirty_outer = False + + monkeypatch.setattr(haloex, "_compute_halo_read_depth_info", + lambda _ignore_hex_dep=False: [DummyReadInfo()]) + monkeypatch.setattr(haloex, "_compute_halo_write_info", + lambda: DummyWriteInfo()) + + assert haloex.required() == (False, True) + + def test_lfriccollection_err1(): ''' Check that the LFRicCollection constructor raises the expected error if it is not provided with an LFRicKern or LFRicInvoke. ''' diff --git a/src/psyclone/tests/psyir/frontend/fparser2_save_stmts_test.py b/src/psyclone/tests/psyir/frontend/fparser2_save_stmts_test.py index a382d19e6c..e790327f5c 100644 --- a/src/psyclone/tests/psyir/frontend/fparser2_save_stmts_test.py +++ b/src/psyclone/tests/psyir/frontend/fparser2_save_stmts_test.py @@ -219,8 +219,6 @@ def test_save_common_module(fortran_reader): assert sym.name.lower().startswith("_psyclone_internal_save") assert sym.datatype._declaration == "SAVE :: /my_common/" break - else: # pragma: no cover - assert False, "No Symbol of UnsupportedFortranType found" sub = psyir.walk(Routine)[0] for sym in sub.symbol_table.symbols: @@ -228,6 +226,3 @@ def test_save_common_module(fortran_reader): assert sym.name.lower().startswith("_psyclone_internal_save") assert sym.datatype._declaration == "SAVE :: /some_other_common/" break - else: # pragma: no cover - assert False, ("No Symbol of UnsupportedFortranType found in nested" - "table") diff --git a/src/psyclone/tests/psyir/nodes/node_test.py b/src/psyclone/tests/psyir/nodes/node_test.py index 4789cf0983..e699d88ada 100644 --- a/src/psyclone/tests/psyir/nodes/node_test.py +++ b/src/psyclone/tests/psyir/nodes/node_test.py @@ -38,6 +38,8 @@ ''' Performs py.test tests on the Node PSyIR node. ''' +import builtins +import runpy import sys import os import re @@ -126,6 +128,24 @@ def dummy(_1, _2): "package." in str(err.value)) +def test_node_colored_fallback_without_termcolor(monkeypatch): + '''Exercise the fallback implementation of ``colored`` when termcolor + cannot be imported. + + ''' + original_import = builtins.__import__ + + def fake_import(name, *args, **kwargs): + '''Raise ImportError only for termcolor.''' + if name == "termcolor": + raise ImportError("termcolor unavailable") + return original_import(name, *args, **kwargs) + + monkeypatch.setattr(builtins, "__import__", fake_import) + module_globals = runpy.run_path(node.__file__) + assert module_globals["colored"]("text", "green") == "text" + + def test_node_str(monkeypatch): ''' Tests for the Node.node_str method. ''' tnode = Node() @@ -646,6 +666,48 @@ def test_node_is_valid_location(): assert not anode.is_valid_location(schedule.children[3], position="after") +def test_node_forward_dependence_selects_closest(): + '''Check that ``forward_dependence`` keeps the closest dependence when + several dependencies are found. + + ''' + class MyNode(Node): + '''Simple Node subclass with configurable arguments.''' + _colour = "green" + + @staticmethod + def _validate_child(position, child): + return True + + @property + def args(self): + return self._args + + class FakeDepArg: + '''Argument object that returns a predefined dependence.''' + def __init__(self, dep_node): + self._dep_node = dep_node + + def forward_dependence(self): + class DepInfo: # pylint: disable=too-few-public-methods + '''Mimic a dependence container with a ``call`` node.''' + dep = DepInfo() + dep.call = self._dep_node + return dep + + parent = MyNode() + target = MyNode() + dep_near = MyNode() + dep_far = MyNode() + parent.addchild(target) + parent.addchild(dep_near) + parent.addchild(dep_far) + target._args = [FakeDepArg(dep_far), FakeDepArg(dep_near)] + + assert dep_far.position > dep_near.position + assert target.forward_dependence() is dep_near + + def test_node_ancestor(): ''' Test the Node.ancestor() method. ''' _, invoke = get_invoke("single_invoke.f90", "gocean", idx=0, diff --git a/src/psyclone/tests/psyir/nodes/omp_directives_test.py b/src/psyclone/tests/psyir/nodes/omp_directives_test.py index 5627d6c281..d10eb03642 100644 --- a/src/psyclone/tests/psyir/nodes/omp_directives_test.py +++ b/src/psyclone/tests/psyir/nodes/omp_directives_test.py @@ -231,15 +231,54 @@ def test_omp_parallel_do_lowering(fortran_reader, monkeypatch, caplog): assert len(pdir.children[3].children) == 1 assert pdir.children[3].children[0].name == 'b' - # Monkeypatch a case with shared variables that need synchronisation + +def test_ompparallel_lowering_in_depend_continue(fortran_reader, monkeypatch): + '''Exercise the branch that ignores IN dependence clauses when checking + synchronisation requirements. + + ''' + code = ''' + subroutine my_subroutine() + integer, dimension(10) :: a + integer :: i + do i = 1, 10 + a(i) = i + end do + end subroutine + ''' + tree = fortran_reader.psyir_from_source(code) + ptrans = OMPParallelTrans() + ptrans.apply(tree.walk(Loop)[0]) + pdir = tree.walk(OMPParallelDirective)[0] + + shared_sym = Symbol("a") monkeypatch.setattr(pdir, "infer_sharing_attributes", - lambda: ({}, {}, {Symbol("a")})) - with caplog.at_level(logging.WARNING, logger=TEST_LOGGER_OMP): - pdir.lower_to_language_level() - assert ("Lowering 'OMPParallelDoDirective' detected a possible race " - "condition for symbol 'a'. Make sure this is a false WaW " - "dependency or the code includes the necessary synchronisations." - "\n" in caplog.text) + lambda: (set(), set(), {shared_sym})) + + task_dir = OMPTaskDirective() + task_dir.addchild(OMPPrivateClause()) + task_dir.addchild(OMPFirstprivateClause()) + task_dir.addchild(OMPSharedClause()) + in_clause = OMPDependClause( + depend_type=OMPDependClause.DependClauseTypes.IN) + in_clause.addchild(Reference(shared_sym)) + task_dir.addchild(in_clause) + pdir.children[0].addchild(task_dir) + + monkeypatch.setattr(OMPDependClause, "operator", + property(lambda self: "in")) + pdir.lower_to_language_level() + assert isinstance(pdir.children[2], OMPPrivateClause) + + +def test_ompparallel_encloses_no_directive_pass_branch(monkeypatch): + '''Exercise the code path where no enclosed OpenMP region directives are + found. + + ''' + pdir = OMPParallelDirective() + monkeypatch.setattr(pdir, "walk", lambda *args, **kwargs: []) + assert pdir._encloses_omp_directive() is None def test_omp_teams_distribute_parallel_do_strings( diff --git a/src/psyclone/tests/psyir/transformations/inline_trans_test.py b/src/psyclone/tests/psyir/transformations/inline_trans_test.py index cda89d5229..f305374efc 100644 --- a/src/psyclone/tests/psyir/transformations/inline_trans_test.py +++ b/src/psyclone/tests/psyir/transformations/inline_trans_test.py @@ -2606,6 +2606,38 @@ def test_validate_call_within_routine(fortran_reader): "sub(a)') is not inside a Routine" in str(err.value)) +def test_validate_unknown_actual_array_arg(fortran_reader): + '''Check that validation rejects inlining when an actual argument has + unknown type but corresponds to an array formal argument. + + ''' + code = ( + "module test_mod\n" + "contains\n" + " subroutine main()\n" + " real, dimension(10) :: a\n" + " call sub(a)\n" + " end subroutine main\n" + " subroutine sub(x)\n" + " real, dimension(:), intent(inout) :: x\n" + " end subroutine sub\n" + "end module test_mod\n" + ) + psyir = fortran_reader.psyir_from_source(code) + call = psyir.walk(Call)[0] + routine = psyir.walk(Routine)[1] + routine_arg = routine.symbol_table.argument_list[0] + # Force unknown type information on the actual argument. + call.arguments[0].symbol.datatype = UnresolvedType() + + inline_trans = InlineTrans() + with pytest.raises(TransformationError) as err: + inline_trans._validate_inline_of_call_and_routine_argument_pairs( + call, call.arguments[0], routine, routine_arg) + assert ("the type of the actual argument 'a' corresponding to an array" + " formal argument ('x') is unknown." in str(err.value)) + + def test_validate_automatic_array_sized_by_arg(fortran_reader, monkeypatch): ''' Check that validate raises the expected error if the dimension of an diff --git a/src/psyclone/tests/psyir/transformations/parallel_loop_trans_test.py b/src/psyclone/tests/psyir/transformations/parallel_loop_trans_test.py index ae7fa823f4..ae6318388b 100644 --- a/src/psyclone/tests/psyir/transformations/parallel_loop_trans_test.py +++ b/src/psyclone/tests/psyir/transformations/parallel_loop_trans_test.py @@ -402,6 +402,41 @@ def test_paralooptrans_collapse_options(fortran_reader, fortran_writer): enddo ''' in fortran_writer(test_loop.parent.parent) + +def test_paralooptrans_collapse_warn_scalar_written_once(monkeypatch, + fortran_reader, + fortran_writer): + '''Exercise the branch that skips dependency issues when all messages are + WARN_SCALAR_WRITTEN_ONCE. + + ''' + psyir = fortran_reader.psyir_from_source(''' + subroutine my_sub() + integer :: i, j + real :: var(10, 10) + do i = 1, 10 + do j = 1, 10 + var(i, j) = var(i, j) + end do + end do + end subroutine my_sub''') + + class DummyMsg: # pylint: disable=too-few-public-methods + '''Mock dependency message carrying just the required code.''' + code = DTCode.WARN_SCALAR_WRITTEN_ONCE + + monkeypatch.setattr( + Loop, "independent_iterations", + lambda self, dep_tools=None, signatures_to_ignore=None, **kwargs: + False) + monkeypatch.setattr(DependencyTools, "get_all_messages", + lambda self: [DummyMsg()]) + + trans = ParaTrans() + trans.apply(psyir.walk(Loop, stop_type=Loop)[0], + {"collapse": True, "verbose": True}) + assert psyir.walk(OMPParallelDoDirective) + # Also it won't collapse if the loop inside is not perfectly nested, # regardless of the force option. psyir = fortran_reader.psyir_from_source(''' @@ -419,7 +454,6 @@ def test_paralooptrans_collapse_options(fortran_reader, fortran_writer): end do end do end subroutine my_sub''') - loop = psyir.walk(Loop)[0] trans = ParaTrans() test_loop = psyir.copy().walk(Loop, stop_type=Loop)[0] trans.apply(test_loop, {"collapse": True, "verbose": True, "force": True}) diff --git a/src/psyclone/tests/psyir/transformations/transformations_test.py b/src/psyclone/tests/psyir/transformations/transformations_test.py index 4daf00cf29..8e4284bff8 100644 --- a/src/psyclone/tests/psyir/transformations/transformations_test.py +++ b/src/psyclone/tests/psyir/transformations/transformations_test.py @@ -185,6 +185,64 @@ def test_accenterdata(): assert str(acct) == "Adds an OpenACC 'enter data' directive" +def test_accenterdata_check_child_async_mismatch(fortran_reader): + '''Check that check_child_async() rejects children with a different + async queue value. + + ''' + code = ''' + subroutine my_subroutine() + integer, dimension(10) :: a + integer :: i + do i = 1, 10 + a(i) = i + end do + end subroutine + ''' + psyir = fortran_reader.psyir_from_source(code) + routine = psyir.walk(Routine)[0] + parallel_trans = ACCParallelTrans() + parallel_trans.apply(routine.walk(Loop)[0], options={"async_queue": 1}) + + enter_trans = ACCEnterDataTrans() + with pytest.raises(TransformationError) as err: + enter_trans.check_child_async(routine, 2) + assert ("Try to make an ACCEnterDataTrans with async_queue different " + "than the one in child kernels" in str(err.value)) + + +def test_ompdeclaretargettrans_detached_scope_fallback(sample_psyir, + monkeypatch): + '''Exercise the fallback path used when an access node has no scope. + + ''' + ompdeclaretargettrans = OMPDeclareTargetTrans() + routine = sample_psyir.walk(Routine)[0] + ref1 = sample_psyir.walk(Reference)[0] + ref1.symbol.interface = ImportInterface(ContainerSymbol('my_mod')) + + class DummySig: # pylint: disable=too-few-public-methods + '''Minimal signature object with a variable name.''' + var_name = "a" + + class DummyAccess: # pylint: disable=too-few-public-methods + '''Minimal access-info object that stores a node.''' + def __init__(self, node): + self.node = node + + class DummyVAM: # pylint: disable=too-few-public-methods + '''Minimal variable-access map replacement for this test.''' + all_signatures = [DummySig()] + + def __getitem__(self, _): + return [DummyAccess(Statement())] + + monkeypatch.setattr(routine, "reference_accesses", lambda: DummyVAM()) + with pytest.raises(TransformationError) as err: + ompdeclaretargettrans.apply(routine) + assert "which is imported" in str(err.value) + + def test_omptaskloop_no_collapse(): ''' Check that the OMPTaskloopTrans.directive() method rejects the collapse argument '''