!> Define some procedures to automate collecting and launching of tests module testsuite use fpm_error, only : error_t, test_failed => fatal_error implicit none private public :: run_testsuite, run_selected, new_unittest, new_testsuite, test_failed public :: select_test, select_suite public :: check_string public :: unittest_t, testsuite_t, error_t abstract interface !> Entry point for tests subroutine test_interface(error) import :: error_t !> Error handling type(error_t), allocatable, intent(out) :: error end subroutine test_interface end interface !> Declaration of a unit test type :: unittest_t !> Name of the test character(len=:), allocatable :: name !> Entry point of the test procedure(test_interface), pointer, nopass :: test => null() !> Whether test is supposed to fail logical :: should_fail = .false. end type unittest_t abstract interface !> Collect all tests subroutine collect_interface(testsuite) import :: unittest_t !> Collection of tests type(unittest_t), allocatable, intent(out) :: testsuite(:) end subroutine collect_interface end interface !> Collection of unit tests type :: testsuite_t !> Name of the testsuite character(len=:), allocatable :: name !> Entry point of the test procedure(collect_interface), pointer, nopass :: collect => null() end type testsuite_t character(len=*), parameter :: fmt = '("#", *(1x, a))' character(len=*), parameter :: indent = repeat(" ", 5) // repeat(".", 3) contains !> Driver for testsuite subroutine run_testsuite(collect, unit, stat) !> Collect tests procedure(collect_interface) :: collect !> Unit for IO integer, intent(in) :: unit !> Number of failed tests integer, intent(inout) :: stat type(unittest_t), allocatable :: testsuite(:) integer :: ii call collect(testsuite) do ii = 1, size(testsuite) write(unit, '("#", 3(1x, a), 1x, "(", i0, "/", i0, ")")') & & "Starting", testsuite(ii)%name, "...", ii, size(testsuite) call run_unittest(testsuite(ii), unit, stat) end do end subroutine run_testsuite !> Driver for selective testing subroutine run_selected(collect, name, unit, stat) !> Collect tests procedure(collect_interface) :: collect !> Name of the selected test character(len=*), intent(in) :: name !> Unit for IO integer, intent(in) :: unit !> Number of failed tests integer, intent(inout) :: stat type(unittest_t), allocatable :: testsuite(:) integer :: ii call collect(testsuite) ii = select_test(testsuite, name) if (ii > 0 .and. ii <= size(testsuite)) then call run_unittest(testsuite(ii), unit, stat) else write(unit, fmt) "Available tests:" do ii = 1, size(testsuite) write(unit, fmt) "-", testsuite(ii)%name end do stat = -huge(ii) end if end subroutine run_selected !> Run a selected unit test subroutine run_unittest(test, unit, stat) !> Unit test type(unittest_t), intent(in) :: test !> Unit for IO integer, intent(in) :: unit !> Number of failed tests integer, intent(inout) :: stat type(error_t), allocatable :: error call test%test(error) if (allocated(error) .neqv. test%should_fail) then if (test%should_fail) then write(unit, fmt) indent, test%name, "[UNEXPECTED PASS]" else write(unit, fmt) indent, test%name, "[FAILED]" end if stat = stat + 1 else if (test%should_fail) then write(unit, fmt) indent, test%name, "[EXPECTED FAIL]" else write(unit, fmt) indent, test%name, "[PASSED]" end if end if if (allocated(error)) then write(unit, fmt) "Message:", error%message end if end subroutine run_unittest !> Select a unit test from all available tests function select_test(tests, name) result(pos) !> Name identifying the test suite character(len=*), intent(in) :: name !> Available unit tests type(unittest_t) :: tests(:) !> Selected test suite integer :: pos integer :: it pos = 0 do it = 1, size(tests) if (name == tests(it)%name) then pos = it exit end if end do end function select_test !> Select a test suite from all available suites function select_suite(suites, name) result(pos) !> Name identifying the test suite character(len=*), intent(in) :: name !> Available test suites type(testsuite_t) :: suites(:) !> Selected test suite integer :: pos integer :: it pos = 0 do it = 1, size(suites) if (name == suites(it)%name) then pos = it exit end if end do end function select_suite !> Register a new unit test function new_unittest(name, test, should_fail) result(self) !> Name of the test character(len=*), intent(in) :: name !> Entry point for the test procedure(test_interface) :: test !> Whether test is supposed to error or not logical, intent(in), optional :: should_fail !> Newly registered test type(unittest_t) :: self self%name = name self%test => test if (present(should_fail)) self%should_fail = should_fail end function new_unittest !> Register a new testsuite function new_testsuite(name, collect) result(self) !> Name of the testsuite character(len=*), intent(in) :: name !> Entry point to collect tests procedure(collect_interface) :: collect !> Newly registered testsuite type(testsuite_t) :: self self%name = name self%collect => collect end function new_testsuite !> Check a deferred length character variable against a reference value subroutine check_string(error, actual, expected, name) !> Error handling type(error_t), allocatable, intent(out) :: error !> Actual string value character(len=:), allocatable, intent(in) :: actual !> Expected string value character(len=*), intent(in) :: expected !> Name of the string to check character(len=*), intent(in) :: name if (.not.allocated(actual)) then call test_failed(error, name//" is not set correctly") return end if if (actual /= expected) then call test_failed(error, name//" is "//actual// & & " but should be "//expected) end if end subroutine check_string end module testsuite