cafut.f90 Source File


Contents

Source Code


Source Code

module cafut

use iso_fortran_env, only: real64

implicit none

!> Define real number kind.
integer, private, parameter :: wp = real64

!> Define maximum name length.
integer, private, parameter :: NAME_LENGTH = 20

!> Define margin of floating point error for real value comparissons.
real(kind=real64), private, parameter :: eps = 1.0d-5

!> Format of the start of a unit test.
character(len=*), private, parameter :: TEST_START = &
    '("==================== ", A, " ====================")'

!> Format of the end of a unit test.
character(len=*), private, parameter :: TEST_END = &
    '(A, " FINISHED WITH ", I3, "/", I3, " TESTS PASSED")'

!> Format of the start of a unit test.
character(len=*), private, parameter :: SUBTEST_START = &
    '("> ", A20)'

!> Format of the end of a unit test.
character(len=*), private, parameter :: SUBTEST_END = &
    '("finished with" , I3, "/", I3, " images passed")'

!> Format of the failure description of an image comparing real values.
character(len=*), private, parameter :: SINGLE_VAL_FMT = &
    '(">> TEST FAILED | Image: ", I3, " | Got: ", F5.2, " | Expected: ", F5.2)'

!> Format of the header for a failed test of an image comparring arrays.
character(len=*), private, parameter :: ARR_VAL_IMG = &
    '(">> Image: ", I3)'

!> String preceeding result array.
character(len=*), private, parameter :: ARR_VAL_RES = &
    ">>> Got: "

!> String preceeding expected array.
character(len=*), private, parameter :: ARR_VAL_EXP = &
    ">>> Expected: "

private :: rootToAll, maxToRoot, maxToAll

type, public :: TestSuite
    !! Holds tests and manages their executtion.
    !! Represents a set of procedures to test a certain feature.
    !! First node of the test linked list.

    integer, private :: n_tests
        !! Number of tests in a test suite.
    character(len=NAME_LENGTH), private :: test_suite_name
        !! Name of the test suite.
    class(Test), public, pointer :: test
        !! Current test whose attributes are available to be set.

contains

    procedure, public, pass :: addUnitTest
    procedure, private, pass :: addTestRealVal
    procedure, private, pass :: addTestRealArrVal
    generic, public :: add => addUnitTest, addTestRealVal, addTestRealArrVal

    procedure, public, pass :: runTests
    final :: deleteTestSuite

end type TestSuite

interface TestSuite
    !! Constructor interface for a TestSuite object.

    module procedure newTestSuite
end interface TestSuite

type, abstract, private :: Test
    !! Abstract class for a single test case. A node in the test linked list.

    character(len=NAME_LENGTH), public :: test_name
        !! Name of the test case.
    class(Test), private, pointer :: next
        !! Next test case or null() if this is the first test inserted (last 
        !! test in the linked list).
contains
    procedure(runInterface), deferred, pass :: run
end type Test

abstract interface 
    function runInterface(self) result(tests_passed)
        !! Abstract function interface for running a test.

        import Test
        class(Test), intent(in) :: self
            !! The test itself. The Test object should contain all information 
            !! needed to run the test.
        integer :: tests_passed
            !! Return total number of tests which passed in the linked list
            !! up to and including this test.
    end function runInterface
end interface 

type, public, extends(Test) :: TestRealVal
    !! Test performed on single real values.

    procedure(realCompInterface), public, nopass, pointer :: compare
        !! Pointer to a comparisson function used to perform the test.
    real(kind=wp), public :: res
        !! Real value result from some process.
    real(kind=wp), public :: tgt
        !! Target real value result for some process.
contains
    procedure, public, pass :: run => runTestRealVal
    procedure, private, nopass :: printFail => printFailTestRealVal
    final :: deleteTestRealVal
end type TestRealVal

interface TestRealVal
    !! Constructor interface for a TestRealVal object.

    module procedure newTestRealVal_name
end interface TestRealVal

interface
    function realCompInterface(res, tgt) result(comp)
        !! Abstract function interface for a value comparisson function.

        import wp
        real(kind=wp), intent(in) :: res
            !! Result being tested.
        real(kind=wp), intent(in) :: tgt
            !! Target value used to compare result to.
        logical :: comp
            !! Return whether or not the test succeeded based on a comparrison.
    end function realCompInterface
end interface

type, public, extends(Test) :: TestRealArrVal
        !! Test performed on an array of real values.

    procedure(realArrCompInterface), nopass, pointer :: compare
        !! Pointer to a comparisson function used to perform the test.

    real(kind=wp), public, allocatable, dimension(:) :: res
        !! Real array result from some process.
    real(kind=wp), public, allocatable, dimension(:) :: tgt
        !! Target real array result for some process.
contains
    procedure, public, pass :: run => runTestRealArrVal
    procedure, private, nopass :: printFail => printFailTestRealArrVal
    final :: deleteTestRealArrVal
end type TestRealArrVal

interface TestRealArrVal
    !! Constructor interface for a TestRealArrVal object.

    module procedure newTestRealArrVal_name
end interface TestRealArrVal

interface
    function realArrCompInterface(res, tgt) result(comp)
        !! Abstract function interface for an array comparisson function.
        import wp
        real(kind=wp), dimension(:), intent(in) :: res, tgt
            !! See res and tgt in TestRealArrVal class.
        logical :: comp
            !! Return whether or not the test succeeded based on a comparrison.
    end function realArrCompInterface
end interface

contains

! Private Helper Methods
! ======================

function rootToAll(x) result(s)
    !! Helper method to spread a value from the root image (1) to all other
    !! images.

    integer, intent(in) :: x
        !! Value being spread. The only value actually spread is the one
        !! input by the root image (1). The values input by all other 
        !! images will be ignored (and not modified).
    integer :: s
        !! Return the value input by the root image (1). This will be the same
        !! for all images.

    integer, allocatable, codimension[:]  :: y
    integer :: L, me, p

        me = this_image()
        p = num_images()

        allocate(y[*])
        !TODO: add error checking to all allocate statements.

        if (me == 1) y[1] = x
        sync all

        L = 1
        do while (L < p)
            L = 2*L
        end do
        do while (L > 0)
            if ((me+L <= p).and.(mod(me-1,2*L)==0)) y[me+L] = y
            L = L/2
            sync all
        end do
        s = y
end function rootToAll

function maxToRoot(x) result(s)
    !! Helper method to return the maximum value from the inputs of each image
    !! to the root image (1) only.

    integer, intent(in) :: x
        !! Input by each image. This value will be compared to the values 
        !! input by every other image.
    integer :: s
        !! Return the maximum value from all `x` inputs from all images. This
        !! will only be significant to the root image (1); the return to every
        !! other image will not be significant and thus should not be used by
        !! them in any way.

    integer, allocatable, codimension[:]  :: y
    integer :: L, me, p

        me = this_image()
        p = num_images()

        allocate(y[*])
        y = x
        sync all

        L = 1
        do while (L < p)
            if ((me+L <= p).and.(mod(me-1,2*L)==0)) y = max(y, y[me+L])
            L = 2*L
            sync all
        end do

        s = 0.0
        if (me == 1) s = y[1]
end function maxToRoot

function maxToAll(x) result(s)
    !! Helper method to collect inputs from every image and return the largest
    !! of these inputs back to each image.

    integer, intent(in) :: x
        !! Value input by each image, which will be compared to the inputs of 
        !! all other images.
    integer :: s
        !! The maximum value from the ones input into `x`.

        s = rootToAll(maxToRoot(x))
end function maxToAll

! TestSuite
! =========

function newTestSuite(ts_name) result(new_ts)
    !! Construct a new test suite.

    character(len=*), intent(in) :: ts_name
        !! Name of the test suite.
    type(TestSuite) :: new_ts
        !! Return the new test suite.

    new_ts%n_tests = 0
    new_ts%test => null()

    new_ts%test_suite_name = ts_name
end function newTestSuite

subroutine deleteTestSuite(self)
    !! Destruct a test suite by deallocating its test pointer attribute.
    type(TestSuite), intent(inout) :: self

    deallocate(self%test)
end subroutine deleteTestSuite

subroutine runTests(self)
    !! Run all tests contained in a test suite.

    class(TestSuite), intent(in) :: self

    integer :: tot_passed

    if (this_image() == 1) then
        print TEST_START, trim(self%test_suite_name)
    end if

    tot_passed = 0
    if (associated(self%test)) tot_passed = self%test%run()

    if (this_image() == 1) then
        print TEST_END, trim(self%test_suite_name), tot_passed, self%n_tests
    end if
end subroutine runTests

subroutine addUnitTest(self, ut)
    !! Add a Test object to the test suite and make it available for setup.

    class(TestSuite), intent(inout) :: self
    class(Test), target, intent(inout) :: ut
        !! Object derived from the Test abstract type.

    class(Test), pointer :: next
        
    if (this_image() == 1) self%n_tests = self%n_tests + 1

    allocate(next)
    next => self%test

    ut%next => next
    self%test => ut
end subroutine addUnitTest

subroutine addTestRealVal(self, ut, res, tgt)
    !! Compact alternative to add a TestRealVal object to the test suite.

    class(TestSuite), intent(inout) :: self
    type(TestRealVal), intent(in) :: ut
        !! An initialized TestRealVal object with the desired name.
    real(kind=wp), intent(in) :: res, tgt
        !! See TestRealVal.

    class(Test), pointer :: next
        
    if (this_image() == 1) self%n_tests = self%n_tests + 1

    allocate(next)
    next => self%test

    allocate(self%test, source=ut)

    associate (t => self%test)
    select type(t)
    type is (TestRealVal)
        t%next => next
        t%test_name = ut%test_name
        t%res = res
        t%tgt = tgt
    end select
    end associate
end subroutine addTestRealVal

subroutine addTestRealArrVal(self, ut, res, tgt)
    !! Compact alternative to add a TestRealArrVal object to the test suite.

    class(TestSuite), intent(inout) :: self
    type(TestRealArrVal), intent(in) :: ut
        !! An initialized TestRealArrVal object with the desired name.
    real(kind=wp), allocatable, dimension(:) :: res, tgt
        !! See TestRealArrVal.

    class(Test), pointer :: next
        
    if (this_image() == 1) self%n_tests = self%n_tests + 1

    allocate(next)
    next => self%test

    allocate(self%test, source=ut)

    associate (t => self%test)
    select type(t)
    type is (TestRealArrVal)
        t%next => next
        t%test_name = ut%test_name
        t%res = res
        t%tgt = tgt
    end select
    end associate
end subroutine addTestRealArrVal

! Test
! ====

! Comparison Functions

function realEq(res, tgt) result(comp)
    !! Test if two real values are equal. Uses an epsilon value to account
    !! for floating point error.

    real(kind=wp), intent(in) :: res
        !! Real value result being tested. 
    real(kind=wp), intent(in) :: tgt
        !! Target real value to compare result to.
    logical :: comp
        !! Return whether both values are equal.

    comp = abs(res-tgt) < eps
end function realEq

function realArrEq(res, tgt) result(comp)
    !! Test if two real arrays are _exactly_ equal. Arrays must be of the
    !! same length and have the same values in the same positions. Uses
    !! epsilon value to account for floating point error.

    real(kind=wp), dimension(:), intent(in) :: res
        !! Real value result being tested. 
    real(kind=wp), dimension(:), intent(in) :: tgt
        !! Target real value to compare result to.
    logical :: comp

    if (size(res) /= size(tgt)) then
        comp = .false.
        return
    end if

    comp = all(abs(res-tgt) < eps)
end function realArrEq

! Constructors

function newTestRealVal_name(ts_name) result(new_ts)
    !! Construct new TestRealVal given a name.

    character(len=*), intent(in) :: ts_name
        !! Name of the new TestRealVal object.
    type(TestRealVal) :: new_ts
        !! Return new TestRealVal object.

    new_ts%test_name = ts_name
    new_ts%next => null()
    new_ts%compare => realEq
    !TODO: create a bunch of subclasses with different comparisson
    ! operators.
    new_ts%res = 0
    new_ts%tgt = 0
end function newTestRealVal_name

function newTestRealArrVal_name(ts_name) result(new_ts)
    !! Construct new TestRealArrVal given a name.

    character(len=*), intent(in) :: ts_name
        !! Name of the new TestRealArrVal object.
    type(TestRealArrVal) :: new_ts
        !! Return new TestRealArrVal object.

    new_ts%test_name = ts_name
    new_ts%next => null()
    new_ts%compare => realArrEq
    !TODO: create a bunch of subclasses with different comparisson
    ! operators.
end function newTestRealArrVal_name

! Destructors

subroutine deleteTestRealVal(self) 
    !! Destruct TestRealVal object by deallocating its next object pointer.

    type(TestRealVal), intent(inout) :: self
    deallocate(self%next)
end subroutine deleteTestRealVal

subroutine deleteTestRealArrVal(self) 
    !! Destruct TestRealVal object by deallocating its next object pointer
    !! as well as its res and tgt arrays.

    type(TestRealArrVal), intent(inout) :: self
    deallocate(self%next)
    deallocate(self%res)
    deallocate(self%tgt)
end subroutine deleteTestRealArrVal

! Print Fail Functions

subroutine printFailTestRealVal(img, res, tgt)
    !! Print failure message of a real value comparrison.

    integer, intent(in) :: img
        !! Image where the failure occured.
    real(kind=wp), intent(in) :: res
        !! (Incorrect) result value of some procedure.
    real(kind=wp), intent(in) :: tgt
        !! (Correct) target result value of some procedure.

    print SINGLE_VAL_FMT, img, res, tgt
end subroutine printFailTestRealVal

subroutine printFailTestRealArrVal(img, res, tgt)
    !! Print failure message of a real array comparrison.

    integer, intent(in) :: img
        !! Image where the failure occured.
    real(kind=wp), dimension(:), intent(in) :: res
        !! (Incorrect) result array of some procedure.
    real(kind=wp), dimension(:), intent(in) :: tgt
        !! (Correct) target result array of some procedure.

    print ARR_VAL_IMG, img
    print '(A)', ARR_VAL_RES 
    print *, res
    print '(A)', ARR_VAL_EXP
    print *, tgt
end subroutine printFailTestRealArrVal

! Run Functions

function runTestRealVal(self) result(tests_passed)
    !! Run test on real values and print summary report for images.

    class(TestRealVal), intent(in) :: self
    integer :: tests_passed
        !! Return the tests that passed up to and including this one in
        !! the linked list.

    real(kind=wp), allocatable, codimension[:] :: res, tgt
    integer :: img_passed, i

    if (associated(self%next)) then
        tests_passed = self%next%run()
    else
        tests_passed = 0
    end if

    allocate(res[*], tgt[*])
    res = self%res
    tgt = self%tgt

    sync all
    img_passed = 0
    if (this_image() == 1) then
        print SUBTEST_START, self%test_name
        do i=1, num_images()
            if (self%compare(res[i], tgt[i])) then
                img_passed = img_passed + 1
            else
                call self%printFail(i, res[i], tgt[i])
            end if
        end do
        print SUBTEST_END, img_passed, num_images()
    end if

    if (img_passed == num_images()) tests_passed = tests_passed + 1
end function runTestRealVal

function runTestRealArrVal(self) result(tests_passed)
    !! Run test on real arrays and print summary report for images.

    class(TestRealArrVal), intent(in) :: self
    integer :: tests_passed
        !! Return the tests that passed up to and including this one in
        !! the linked list.

    real(kind=wp), allocatable, dimension(:), codimension[:] :: res, tgt
    integer, allocatable, codimension[:] :: res_n, tgt_n

    integer :: max_res_n, max_tgt_n
    integer :: img_passed, i

    if (associated(self%next)) then
        tests_passed = self%next%run()
    else
        tests_passed = 0
    end if

    allocate(res_n[*], tgt_n[*])
    res_n = size(self%res)
    tgt_n = size(self%tgt)

    max_res_n = maxToAll(res_n)
    max_tgt_n = maxToAll(tgt_n)

    allocate(res(max_res_n)[*], tgt(max_tgt_n)[*])
    res(:res_n) = self%res
    tgt(:res_n) = self%tgt

    sync all
    img_passed = 0
    if (this_image() == 1) then
        print SUBTEST_START, self%test_name
        do i=1, num_images()
            if (self%compare(res(:res_n[i])[i], tgt(:tgt_n[i])[i])) then
                img_passed = img_passed + 1
            else
                call self%printFail(i, res(:res_n[i])[i], tgt(:tgt_n[i])[i])
            end if
        end do
        print SUBTEST_END, img_passed, num_images()
    end if

    if (img_passed == num_images()) tests_passed = tests_passed + 1

    deallocate(res_n, tgt_n, res, tgt)
end function runTestRealArrVal

end module cafut