Skip to content

Commit

Permalink
Introduce Fortran Test Framework and CTest Integration
Browse files Browse the repository at this point in the history
This PR introduces a Fortran testing framework and CTest integration to improve the testing and validation
process for the library. Key updates include an assertion module, unit tests for core features, and
CMake functions for streamlined test management.
  • Loading branch information
amstokely committed Dec 19, 2024
1 parent 7480929 commit 4177b09
Show file tree
Hide file tree
Showing 12 changed files with 413 additions and 37 deletions.
10 changes: 6 additions & 4 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
# Check CMake version
cmake_minimum_required(VERSION 3.10)
cmake_minimum_required(VERSION 3.12)

include("${CMAKE_SOURCE_DIR}/cmake/Functions/Obs2Ioda_Functions.cmake")
enable_testing()

include("${CMAKE_SOURCE_DIR}/cmake/Obs2Ioda_Functions.cmake")
# Define the project
project(obs2ioda LANGUAGES Fortran)

set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/Modules/")
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/")
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
# Fortran module output directory for build interface
set(OBS2IODA_MODULE_DIR ${PROJECT_NAME}/module/${CMAKE_Fortran_COMPILER_ID}/${CMAKE_Fortran_COMPILER_VERSION})
Expand All @@ -20,4 +22,4 @@ set(NCEP_BUFR_LIB CACHE STRING "" )
find_package(NetCDF REQUIRED COMPONENTS Fortran C)

add_subdirectory("${CMAKE_SOURCE_DIR}/obs2ioda-v2")

add_subdirectory("${CMAKE_SOURCE_DIR}/test")
File renamed without changes.
30 changes: 0 additions & 30 deletions cmake/Functions/Obs2Ioda_Functions.cmake

This file was deleted.

31 changes: 31 additions & 0 deletions cmake/Obs2Ioda_CompilerFlags.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Set Fortran compiler flags specific to the GNU Compiler
# -ffree-line-length-none: Remove the limit on the length of lines in the source file
# -mcmodel=medium: Allow for larger datasets in memory
set(FORTRAN_COMPILER_GNU_FLAGS
$<$<COMPILE_LANGUAGE:Fortran>:-ffree-line-length-none -mcmodel=medium>
)

# Set Debugging Fortran compiler flags specific to the GNU Compiler
# -fbacktrace: Provide a backtrace when an error occurs
# -ffpe-trap=invalid,zero,overflow: Trap floating point exceptions (invalid calculation, divide by zero, overflow)
# -fcheck=all: Execute all types of runtime checks
# -g: Produce debugging information
set(FORTRAN_COMPILER_GNU_DEBUG_FLAGS
$<$<COMPILE_LANGUAGE:Fortran>:-g -fbacktrace -ffpe-trap=invalid,zero,overflow -fcheck=all>
)

# Set Fortran compiler flags for the Intel Compiler
# -mcmodel=medium: Allow for larger datasets in memory
set(FORTRAN_COMPILER_INTEL_FLAGS
$<$<COMPILE_LANGUAGE:Fortran>:-mcmodel=medium>
)

# Set Debugging Fortran compiler flags for the Intel Compiler
# -check uninit: Checks uninitialized variables
# -ftrapuv: Enable trapping of uninitialized variables
# -g: Enable production of debug information
# -traceback: Give symbolic traceback on errors
# -fpe0: Stop execution when a floating-point exception occurs
set(FORTRAN_COMPILER_INTEL_DEBUG_FLAGS
$<$<COMPILE_LANGUAGE:Fortran>:-check uninit -ftrapuv -g -traceback -fpe0>
)
116 changes: 116 additions & 0 deletions cmake/Obs2Ioda_Functions.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
include("${CMAKE_SOURCE_DIR}/cmake/Obs2Ioda_CompilerFlags.cmake")

# This CMake function, `obs2ioda_fortran_target`, configures Fortran targets for obs2ioda.
#
# Its arguments are:
# - target: the name of the target to configure
# - target_main: the main source file for the executable
#
# The function sets the following properties for the target:
# - The directory for Fortran module files
# - The include directories for the target (both build and install interfaces)
# - The install RPATH to enable finding shared libraries at runtime
# - Fortran format as FREE
# - Compiler-specific options and flags, depending on whether the GNU Fortran or Intel Fortran compiler is used,
# and whether the build type is Debug or not
#
# The function also links the public libraries to the target and creates an executable `obs2ioda_${target}` linked
# to the original target.
function(obs2ioda_fortran_target target target_main)
set_target_properties(${target} PROPERTIES Fortran_MODULE_DIRECTORY ${CMAKE_BINARY_DIR}/${OBS2IODA_MODULE_DIR})
target_include_directories(${target} INTERFACE $<BUILD_INTERFACE:${CMAKE_BINARY_DIR}/${OBS2IODA_MODULE_DIR}>
$<INSTALL_INTERFACE:${OBS2IODA_MODULE_DIR}>)
#Relocatable, portable, runtime dynamic linking
set_target_properties(${target} PROPERTIES INSTALL_RPATH "\$ORIGIN/../${CMAKE_INSTALL_LIBDIR}")
# Global Fortran configuration
set(public_link_libraries_name ${target}_PUBLIC_LINK_LIBRARIES)
set(public_link_libraries ${${public_link_libraries_name}})
set_target_properties(${target} PROPERTIES Fortran_FORMAT FREE)

# Compiler-specific options and flags
set(OBS2IODA_FORTRAN_TARGET_COMPILE_OPTIONS_PRIVATE "")
if (CMAKE_Fortran_COMPILER_ID MATCHES GNU)
list(APPEND OBS2IODA_FORTRAN_TARGET_COMPILE_OPTIONS_PRIVATE
${FORTRAN_COMPILER_GNU_FLAGS}
)
if (CMAKE_BUILD_TYPE MATCHES Debug)
list(APPEND OBS2IODA_FORTRAN_TARGET_COMPILE_OPTIONS_PRIVATE
${FORTRAN_COMPILER_GNU_DEBUG_FLAGS}
)
endif ()
elseif (CMAKE_Fortran_COMPILER_ID MATCHES Intel)
list(APPEND OBS2IODA_FORTRAN_TARGET_COMPILE_OPTIONS_PRIVATE
${FORTRAN_COMPILER_INTEL_FLAGS}
)
if (CMAKE_BUILD_TYPE MATCHES Debug)
list(APPEND OBS2IODA_FORTRAN_TARGET_COMPILE_OPTIONS_PRIVATE
${FORTRAN_COMPILER_INTEL_DEBUG_FLAGS}
)
endif ()
endif ()
target_compile_options(${target} PRIVATE ${OBS2IODA_FORTRAN_TARGET_COMPILE_OPTIONS_PRIVATE})
target_link_libraries(${target} PUBLIC ${public_link_libraries})
add_executable(obs2ioda_${target} ${target_main})
target_link_libraries(obs2ioda_${target} PUBLIC ${target})

endfunction()

# Function: add_memcheck_ctest
# Adds a memory check test for a given target using Valgrind.
#
# Arguments:
# - target (string): The name of the target to check for memory issues.
#
# Behavior:
# - If Valgrind is found, it adds a CTest named `<target>_memcheck` that runs the target
# with Valgrind's memory checking options (`--leak-check=full`, `--error-exitcode=1`).
# - If Valgrind is not found, it outputs a status message and does not add the memory check.
#
# Example Usage:
# add_memcheck_ctest(my_target)
#
# Notes:
# - Ensure Valgrind is installed and accessible in the system's PATH for this function to work.
function(add_memcheck_ctest target)
find_program(VALGRIND "valgrind")
if (VALGRIND)
message(STATUS "Valgrind found: ${VALGRIND}")
message(STATUS "Adding memory check for test: ${target}")
set(VALGRIND_COMMAND valgrind --leak-check=full --error-exitcode=1 --undef-value-errors=no)
add_test(NAME ${target}_memcheck
COMMAND ${VALGRIND_COMMAND} $<TARGET_FILE:${target}>)
else ()
message(STATUS "Valgrind not found")
message(STATUS "Memory check for test: ${target} will not be added")
endif ()
endfunction()

# Function: add_fortran_ctest
# Creates and registers a CTest for a Fortran test executable, handling mixed Fortran and C sources.
#
# Arguments:
# - test_name (string): The name of the test.
# - test_sources (list): List of source files for the test, including Fortran and optional C sources.
# - library_deps (list): List of library dependencies to link with the test executable.
#
# Behavior:
# - Identifies C source files (`*.c`) from the `test_sources` list and compiles them into shared libraries.
# - Updates the `library_deps` list to include the created shared libraries for C sources.
# - Creates a Fortran executable target named `Test_<test_name>` using the remaining Fortran sources.
# - Links the test executable with the specified libraries and any generated C libraries.
# - Registers the test with CTest, with the executable's path resolved to `${CMAKE_BINARY_DIR}/bin`.
function(add_fortran_ctest test_name test_sources library_deps)
foreach(test_source ${test_sources})
if (${test_source} MATCHES ".*\\.c$")
get_filename_component(test_source_name ${test_source} NAME_WE)
message(STATUS "Adding C test: ${test_source_name}")
add_library("c_${test_source_name}" SHARED ${test_source})
list(APPEND library_deps "c_${test_source_name}")
list(REMOVE_ITEM test_sources ${test_source})
endif ()
endforeach ()
add_executable("Test_${test_name}" ${test_sources})
target_link_libraries("Test_${test_name}" ${library_deps})
add_test(NAME ${test_name}
COMMAND ${CMAKE_BINARY_DIR}/bin/Test_${test_name})
endfunction()
1 change: 1 addition & 0 deletions obs2ioda-v2/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ set(v2_SOURCES
radiance_mod.f90
ufo_variables_mod.F90
utils_mod.f90
test/fortran_test_framework_mod.f90
)
list(TRANSFORM v2_SOURCES PREPEND "src/")
set(v2_MAIN_SOURCE
Expand Down
6 changes: 3 additions & 3 deletions obs2ioda-v2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ If you have an environment preconfigured for `mpas-jedi`, simply source that env
```bash
find . -name *libbufr*
```
4. Next, run CMake to configure the build. Remember to specify the path to the NCEP BUFR library:
4. Next, run CMake to configure the build. Specify the `CMAKE_BUILD_TYPE` option to set the build type. Currently, the supported types are `Release`, `RelWithDebInfo`, and `Debug`. Don't forget to include the path to the NCEP BUFR library:
```bash
cmake <OBS2IODA_ROOT_DIR> -DNCEP_BUFR_LIB=<NCEP_BUFR_LIB_PATH>
cmake <OBS2IODA_ROOT_DIR> -DNCEP_BUFR_LIB=<NCEP_BUFR_LIB_PATH> -DCMAKE_BUILD_TYPE=<BUILD_TYPE>
```
5. Finally, build the project using this command:
5. Finally, build `obs2ioda` using `CMake`'s build tool. In this case, we use `GNU Make`, but other build tools supported by `CMake` can be used:
```bash
make
```
Expand Down
158 changes: 158 additions & 0 deletions obs2ioda-v2/src/test/fortran_test_framework_mod.f90
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
module fortran_test_framework_mod
implicit none

public :: assertEqual

interface assertEqual
module procedure assertEqual_integer
module procedure assertEqual_string
module procedure assertEqual_logical
end interface assertEqual

abstract interface
subroutine assert_interface(condition, message, status)
logical, intent(in) :: condition
character(len = *), intent(in) :: message
integer, intent(out) :: status
end subroutine assert_interface
end interface

contains

! Subroutine: assert
! Performs a basic assertion by evaluating a condition and printing a message.
! Exits the program with an error status if the condition is not met.
!
! Arguments:
! - condition (logical, in): The condition to evaluate.
! - message (character, in): Message to print indicating the assertion result.
! - status (integer, out): Status code (0 for success, 1 for failure).
subroutine assert(condition, message, status)
implicit none
logical, intent(in) :: condition
character(len = *), intent(in) :: message
integer, intent(out) :: status
if (.not. condition) then
status = 1
write(*, '(A)') "Failed: " // message
call exit(1)
else
status = 0
write(*, '(A)') "Success: " // message
end if
end subroutine assert

! Subroutine: assertEqual_integer
! Asserts that two integer values are equal, using a custom or default assertion handler.
!
! Arguments:
! - expected (integer, in): The expected value.
! - actual (integer, in): The actual value.
! - status (integer, out): Status code (0 for success, 1 for failure).
! - assert_procedure (procedure, optional): Custom procedure to handle assertion logic.
subroutine assertEqual_integer(expected, actual, status, assert_procedure)
implicit none
integer, intent(in) :: expected, actual
integer, intent(out) :: status
procedure(assert_interface), optional :: assert_procedure
procedure(assert_interface), pointer :: assert_handler => assert

if (present(assert_procedure)) then
assert_handler => assert_procedure
end if

call assert_handler(&
expected == actual, "expected=" // trim(adjustl(itoa(expected))) // &
" actual=" // trim(adjustl(itoa(actual))), &
status &
)
end subroutine assertEqual_integer

! Subroutine: assertEqual_string
! Asserts that two strings are equal, using a custom or default assertion handler.
!
! Arguments:
! - expected (character, in): The expected string value.
! - actual (character, in): The actual string value.
! - status (integer, out): Status code (0 for success, 1 for failure).
! - assert_procedure (procedure, optional): Custom procedure to handle assertion logic.
subroutine assertEqual_string(expected, actual, status, assert_procedure)
implicit none
character(len = *), intent(in) :: expected, actual
integer, intent(out) :: status
procedure(assert_interface), optional :: assert_procedure
procedure(assert_interface), pointer :: assert_handler => assert

if (present(assert_procedure)) then
assert_handler => assert_procedure
end if

call assert_handler(&
expected == actual, "expected='" // trim(expected) // "' actual='" // &
trim(actual) // "'", &
status &
)
end subroutine assertEqual_string

! Subroutine: assertEqual_logical
! Asserts that two logical values are equivalent, using a custom or default assertion handler.
!
! Arguments:
! - expected (logical, in): The expected logical value.
! - actual (logical, in): The actual logical value.
! - status (integer, out): Status code (0 for success, 1 for failure).
! - assert_procedure (procedure, optional): Custom procedure to handle assertion logic.
subroutine assertEqual_logical(expected, actual, status, assert_procedure)
implicit none
logical, intent(in) :: expected, actual
integer, intent(out) :: status
procedure(assert_interface), optional :: assert_procedure
procedure(assert_interface), pointer :: assert_handler => assert

if (present(assert_procedure)) then
assert_handler => assert_procedure
end if

call assert_handler(&
expected .eqv. actual, "expected=" // trim(logical_to_string(expected)) // &
" actual=" // trim(logical_to_string(actual)), &
status &
)
end subroutine assertEqual_logical

! Function: itoa
! Converts an integer to a string representation.
!
! Arguments:
! - value (integer, in): The integer to convert.
!
! Returns:
! - str (character): String representation of the integer.
function itoa(value) result(str)
implicit none
integer, intent(in) :: value
character(len = 32) :: str
write(str, '(I0)') value
end function itoa

! Function: logical_to_string
! Converts a logical value to a string representation (".true." or ".false.").
!
! Arguments:
! - value (logical, in): The logical value to convert.
!
! Returns:
! - str (character): String representation of the logical value.
function logical_to_string(value) result(str)
implicit none
logical, intent(in) :: value
character(len = 6) :: str

if (value) then
str = ".true."
else
str = ".false."
end if
end function logical_to_string

end module fortran_test_framework_mod
1 change: 1 addition & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/fortran)
Loading

0 comments on commit 4177b09

Please sign in to comment.