Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions changelog
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
7 changes: 4 additions & 3 deletions src/psyclone/domain/lfric/lfric_invoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
'''
Expand Down
4 changes: 2 additions & 2 deletions src/psyclone/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,17 +84,17 @@ 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'. '''
# pylint: disable=import-outside-toplevel
import graphviz
try:
graphviz.version()
return True
except graphviz.ExecutableNotFound:
return False
return True


@pytest.fixture(scope="session", autouse=True)
Expand Down
33 changes: 33 additions & 0 deletions src/psyclone/tests/conftest_test.py
Original file line number Diff line number Diff line change
@@ -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
8 changes: 8 additions & 0 deletions src/psyclone/tests/lfric_lma_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
117 changes: 116 additions & 1 deletion src/psyclone/tests/lfric_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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. '''
Expand Down
5 changes: 0 additions & 5 deletions src/psyclone/tests/psyir/frontend/fparser2_save_stmts_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,15 +219,10 @@ 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:
if isinstance(sym.datatype, UnsupportedFortranType):
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")
62 changes: 62 additions & 0 deletions src/psyclone/tests/psyir/nodes/node_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@

''' Performs py.test tests on the Node PSyIR node. '''

import builtins
import runpy
import sys
import os
import re
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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,
Expand Down
55 changes: 47 additions & 8 deletions src/psyclone/tests/psyir/nodes/omp_directives_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading
Loading