diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index 22ddc72..784793a 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -16,6 +16,7 @@ env: jobs: build: + name: Build # The CMake configure and build commands are platform agnostic and should work equally # well on Windows or Mac. You can convert this to a matrix build if you need # cross-platform coverage. @@ -23,40 +24,96 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: submodules: true + - name: Cache pFUnit install + uses: actions/cache@v4 + id: pfunit-cache + with: + path: pFUnit/build/installed + key: pfunit-${{ runner.os }}-${{ env.BUILD_TYPE }}-${{ hashFiles('.gitmodules', 'pFUnit/CMakeLists.txt') }} + restore-keys: | + pfunit-${{ runner.os }}-${{ env.BUILD_TYPE }}- + - name: Configure CMake pFUnit + if: steps.pfunit-cache.outputs.cache-hit != 'true' # Configure pFUnit run: cmake -S pFUnit -B ${{github.workspace}}/pFUnit/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DENABLE_OpenMP_SUPPORT=OFF -DENABLE_POSTGRESQL_SUPPORT=OFF - name: Build pFUnit + if: steps.pfunit-cache.outputs.cache-hit != 'true' # Build your program with the given configuration run: cmake --build ${{github.workspace}}/pFUnit/build --config ${{env.BUILD_TYPE}} - name: Install pFUnit + if: steps.pfunit-cache.outputs.cache-hit != 'true' # install libs and includes in libslam/build/ run: cmake --install ${{github.workspace}}/pFUnit/build --config ${{env.BUILD_TYPE}} - name: Configure CMake # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make. # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type - run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_PREFIX_PATH=${{github.workspace}}/pFUnit/build/installed/ + run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_PREFIX_PATH=${{github.workspace}}/pFUnit/build/installed/ -DENABLE_PFUNIT=ON - name: Build # Build your program with the given configuration run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} - - name: Test + - name: Upload build artifact + uses: actions/upload-artifact@v4 + with: + name: libslam-build + path: build + + test: + name: Test + needs: [build] + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + submodules: true + + - name: Cache pFUnit install + uses: actions/cache@v4 + id: pfunit-cache + with: + path: pFUnit/build/installed + key: pfunit-${{ runner.os }}-${{ env.BUILD_TYPE }}-${{ hashFiles('.gitmodules', 'pFUnit/CMakeLists.txt') }} + restore-keys: | + pfunit-${{ runner.os }}-${{ env.BUILD_TYPE }}- + + - name: Configure CMake pFUnit + if: steps.pfunit-cache.outputs.cache-hit != 'true' + run: cmake -S pFUnit -B ${{github.workspace}}/pFUnit/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DENABLE_OpenMP_SUPPORT=OFF -DENABLE_POSTGRESQL_SUPPORT=OFF + + - name: Build pFUnit + if: steps.pfunit-cache.outputs.cache-hit != 'true' + run: cmake --build ${{github.workspace}}/pFUnit/build --config ${{env.BUILD_TYPE}} + + - name: Install pFUnit + if: steps.pfunit-cache.outputs.cache-hit != 'true' + run: cmake --install ${{github.workspace}}/pFUnit/build --config ${{env.BUILD_TYPE}} + + - name: Download build artifact + uses: actions/download-artifact@v4 + with: + name: libslam-build + path: build + + - name: Restore execute permissions on build artifact + run: chmod -R a+x build + + - name: Run tests working-directory: ${{github.workspace}}/build - # Execute tests defined by the CMake configuration. - # See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail run: ctest -C ${{env.BUILD_TYPE}} --verbose --output-junit ../test_report.xml - name: Publish Test Report uses: mikepenz/action-junit-report@v5 - if: success() || failure() # always run even if the previous step fails + if: success() || failure() with: report_paths: '**/test_report.xml' comment: true diff --git a/build.sh b/build.sh index c7f7cca..da39760 100755 --- a/build.sh +++ b/build.sh @@ -27,13 +27,13 @@ elif [[ "$OSTYPE" == "msys"* ]]; then LIBSUFFIX="dll" GENERATOR_FLAGS="-G MSYS Makefiles" fi -git submodule update --init --recursive ################################################################################ # # # Build pFUnit # # # ################################################################################ if [[ "$ENABLE_PFUNIT" == "ON" ]]; then + git submodule update --init --recursive || exit cd pFUnit || exit # Create the build directory if it does not exist if [[ ! -d "build" ]]; then @@ -43,7 +43,7 @@ if [[ "$ENABLE_PFUNIT" == "ON" ]]; then fi cd build || exit echo "Updating cmake" - export PFUNIT_DIR=..//pFUnit/build/installed + export PFUNIT_DIR=../pFUnit/build/installed export FC=$Fortran_COMPILER cmake -DSKIP_MPI=yes "$GENERATOR_FLAGS" ../ echo "Building pFUnit" @@ -82,4 +82,4 @@ cd ../ || exit ln -sf build/include include || exit ln -sf build/lib lib || exit echo "Leaving libslam" -echo "Done with $BUILD_TYPE build" \ No newline at end of file +echo "Done with $BUILD_TYPE build" diff --git a/pFUnit b/pFUnit index 74e6f4a..581cd32 160000 --- a/pFUnit +++ b/pFUnit @@ -1 +1 @@ -Subproject commit 74e6f4a0c5ff622d3e127b17c29baafe2ca45e5d +Subproject commit 581cd32be937df1922ce50fb06ddbc5522e6ce66 diff --git a/pFUnittests/CMakeLists.txt b/pFUnittests/CMakeLists.txt index c69121d..121e05a 100644 --- a/pFUnittests/CMakeLists.txt +++ b/pFUnittests/CMakeLists.txt @@ -32,3 +32,11 @@ LINK_LIBRARIES slam-Fortran) add_pfunit_ctest (slam_strings_test TEST_SOURCES test_slamstrings.pf LINK_LIBRARIES slam-Fortran) + +add_pfunit_ctest (slam_error_handling_test + TEST_SOURCES test_slam_error_handling.pf + LINK_LIBRARIES slam-Fortran) + +add_pfunit_ctest (slam_io_test + TEST_SOURCES test_slam_io.pf + LINK_LIBRARIES slam-Fortran) diff --git a/pFUnittests/test_slam_error_handling.pf b/pFUnittests/test_slam_error_handling.pf new file mode 100644 index 0000000..59d0b8c --- /dev/null +++ b/pFUnittests/test_slam_error_handling.pf @@ -0,0 +1,106 @@ +!============================================================================== +! +!> @anchor test_slam_error_handling +!! +!> @brief pFUnit tests for slam_error_handling (language/verbosity, setError, compile_log_record) +!! +!------------------------------------------------------------------------ +module test_slam_error_handling + use funit + use slam_error_handling + implicit none + + ! Language constants (match module: 1=ENGLISH, 2=GERMAN) + integer, parameter :: LANG_ENGLISH = 1 + integer, parameter :: LANG_GERMAN = 2 + +contains + + @test + subroutine test_setErrorLanguage_getErrorLanguage() + integer :: prev + prev = setErrorLanguage(LANG_GERMAN) + @assertEqual(LANG_GERMAN, getErrorLanguage()) + prev = setErrorLanguage(LANG_ENGLISH) + @assertEqual(LANG_ENGLISH, getErrorLanguage()) + end subroutine test_setErrorLanguage_getErrorLanguage + + @test + subroutine test_setCliVerbosity_getCliVerbosity() + integer :: junk + junk = setCliVerbosity(WARNINGS) + @assertEqual(WARNINGS, getCliVerbosity()) + junk = setCliVerbosity(ERRORS) + @assertEqual(ERRORS, getCliVerbosity()) + end subroutine test_setCliVerbosity_getCliVerbosity + + @test + subroutine test_setLogVerbosity_getLogVerbosity() + integer :: junk + junk = setLogVerbosity(REMARKS) + @assertEqual(REMARKS, getLogVerbosity()) + junk = setLogVerbosity(QUIET) + @assertEqual(QUIET, getLogVerbosity()) + end subroutine test_setLogVerbosity_getLogVerbosity + + @test + subroutine test_setError_report_updates_latest_error() + call initErrorHandler(errAction='REPORT') + call setError(E_FILE_NOT_FOUND, WARNING, errorMessage='test message') + @assertEqual(E_FILE_NOT_FOUND, getLatestError()) + @assertEqual(WARNING, getLatestErrorType()) + end subroutine test_setError_report_updates_latest_error + + @test + subroutine test_setError_fatal_hasFailed() + call initErrorHandler(errAction='REPORT') + call setError(E_FILE_NOT_FOUND, FATAL, errorMessage='fatal test') + @assertEqual(FATAL, getLatestErrorType()) + @assertTrue(hasFailed()) + end subroutine test_setError_fatal_hasFailed + + @test + subroutine test_setError_remark_type() + call initErrorHandler(errAction='REPORT') + call setError(E_STRING_LENGTH, REMARK, errorMessage='remark') + @assertEqual(E_STRING_LENGTH, getLatestError()) + @assertEqual(REMARK, getLatestErrorType()) + end subroutine test_setError_remark_type + + @test + subroutine test_setError_debug_msg_type() + call initErrorHandler(errAction='REPORT') + call setError(E_UNKNOWN_PARAMETER, DEBUG_MSG, errorMessage='debug') + @assertEqual(E_UNKNOWN_PARAMETER, getLatestError()) + @assertEqual(DEBUG_MSG, getLatestErrorType()) + end subroutine test_setError_debug_msg_type + + @test + subroutine test_resetError_clears_latest() + call initErrorHandler(errAction='REPORT') + call setError(E_FILE_NOT_FOUND, WARNING, errorMessage='x') + call resetError() + @assertEqual(0, getLatestError()) + @assertEqual(ERR_UNDEFINED, getLatestErrorType()) + end subroutine test_resetError_clears_latest + + @test + subroutine test_compile_log_record_format() + character(len=23) :: timestamp + character(len=1024) :: rec + timestamp = '2024-01-15 10:30:00.000' + rec = compile_log_record(timestamp, 2, ' - ', 'test message') + @assertTrue(len_trim(rec) > 0) + @assertTrue(index(trim(rec), '2024-01-15') > 0) + @assertTrue(index(trim(rec), 'WARNING') > 0) + @assertTrue(index(trim(rec), 'test message') > 0) + end subroutine test_compile_log_record_format + + @test + subroutine test_getErrorMessage_returns_message() + character(len=512) :: msg + call getErrorMessage(E_FILE_NOT_FOUND, msg) + @assertTrue(len_trim(msg) > 0) + end subroutine test_getErrorMessage_returns_message + +end module test_slam_error_handling diff --git a/pFUnittests/test_slam_io.pf b/pFUnittests/test_slam_io.pf new file mode 100644 index 0000000..d44a5e3 --- /dev/null +++ b/pFUnittests/test_slam_io.pf @@ -0,0 +1,149 @@ +!============================================================================== +! +!> @anchor test_slam_io +!! +!> @brief pFUnit tests for slam_io (sprint, parse_from_fid) +!! +!------------------------------------------------------------------------ +module test_slam_io + use funit + use slam_io, only: & + openFile, closeFile, parse_from_fid, sprint, message, & + SEQUENTIAL, IN_FORMATTED, OUT_FORMATTED_OVERWRITE, & + STDOUT, LOG_AND_STDOUT, LOGFILE + use slam_error_handling, only: initErrorHandler, getLatestError, resetError + use slam_types, only: dp + implicit none + + character(*), parameter :: TEST_FILE = 'test_slam_io_parse.tmp' + +contains + + !-------- message -------- + @test + subroutine test_message_stdout() + call message('test message to stdout', STDOUT) + end subroutine test_message_stdout + + @test + subroutine test_message_log_and_stdout() + call message('test message to both', LOG_AND_STDOUT) + end subroutine test_message_log_and_stdout + + @test + subroutine test_message_logfile_only() + call message('test message to logfile only', LOGFILE) + end subroutine test_message_logfile_only + + !-------- sprint -------- + @test + subroutine test_sprint_integer() + @assertEqual('7', trim(sprint(7, 'I1'))) + @assertEqual('007', trim(sprint(7, 'I3.3'))) + end subroutine test_sprint_integer + + @test + subroutine test_sprint_real() + @assertEqual('7.1', trim(sprint(7.1, 'F3.1'))) + @assertEqual('7.10', trim(sprint(7.1_dp, 'F4.2'))) + end subroutine test_sprint_real + + @test + subroutine test_sprint_character() + @assertEqual(' x', trim(sprint('x', 'A3'))) + end subroutine test_sprint_character + + @test + subroutine test_sprint_invalid_format_fails() + character(len=20) :: dummy + call initErrorHandler(control='YES', errAction='RETURN') + dummy = sprint(7, 'F4.1') + @assertTrue(getLatestError() /= 0) + call resetError() + end subroutine test_sprint_invalid_format_fails + + @test + subroutine test_sprint_real_as_integer_fails() + character(len=20) :: dummy + call initErrorHandler(control='YES', errAction='RETURN') + dummy = sprint(7.1_dp, 'I3.3') + @assertTrue(getLatestError() /= 0) + call resetError() + end subroutine test_sprint_real_as_integer_fails + + !-------- parse_from_fid -------- + @test + subroutine test_parse_from_fid_real_integer() + integer :: fid + real(dp) :: value_real + integer :: value_int + fid = create_parse_test_file() + fid = openFile(TEST_FILE, SEQUENTIAL, IN_FORMATTED) + call parse_from_fid(fid, '#', value_real, 'real(DP)') + @assertRelativelyEqual(12.50_dp, value_real, tolerance=1.0e-12_dp) + call parse_from_fid(fid, '#', value_int, 'integer') + @assertEqual(7, value_int) + fid = closeFile(fid) + end subroutine test_parse_from_fid_real_integer + + @test + subroutine test_parse_from_fid_logical_string() + integer :: fid + real(dp) :: value_real + integer :: value_int + logical :: value_log + character(len=17) :: value_char + fid = create_parse_test_file() + fid = openFile(TEST_FILE, SEQUENTIAL, IN_FORMATTED) + call parse_from_fid(fid, '#', value_real, 'real(DP)') + call parse_from_fid(fid, '#', value_int, 'integer') + call parse_from_fid(fid, '#', value_log, 'logical true') + @assertTrue(value_log) + call parse_from_fid(fid, '#', value_log, 'logical false') + @assertFalse(value_log) + call parse_from_fid(fid, '#', value_char, 'short string') + @assertEqual('String', trim(value_char)) + fid = closeFile(fid) + end subroutine test_parse_from_fid_logical_string + + @test + subroutine test_parse_from_fid_invalid_raises_error() + integer :: fid + real(dp) :: value_real + integer :: value_int + logical :: value_log + character(len=80) :: value_char + fid = create_parse_test_file() + fid = openFile(TEST_FILE, SEQUENTIAL, IN_FORMATTED) + call initErrorHandler(control='YES', errAction='RETURN') + ! Skip to line "12.50 # real(DP) parsed as integer (should fail)" + call parse_from_fid(fid, '#', value_real, 'real(DP)') + call parse_from_fid(fid, '#', value_int, 'integer') + call parse_from_fid(fid, '#', value_log, 'logical true') + call parse_from_fid(fid, '#', value_log, 'logical false') + call parse_from_fid(fid, '#', value_char, 'short string') + call parse_from_fid(fid, '#', value_char, 'long string') + call parse_from_fid(fid, '#', value_char, 'long string with comment') + call parse_from_fid(fid, '#', value_real, 'integer parsed as real(DP)') + call parse_from_fid(fid, '#', value_int, 'real(DP) parsed as integer') + @assertTrue(getLatestError() /= 0) + call resetError() + fid = closeFile(fid) + end subroutine test_parse_from_fid_invalid_raises_error + + integer function create_parse_test_file() + integer :: u + u = openFile(TEST_FILE, SEQUENTIAL, OUT_FORMATTED_OVERWRITE) + write(u, '(A)') '12.50 # real(DP)' + write(u, '(A)') '7 # integer' + write(u, '(A)') 'T # logical true (string)' + write(u, '(A)') 'F # logical false (string)' + write(u, '(A)') "'String' # short string" + write(u, '(A)') "'This is a string' # long string" + write(u, '(A)') "'This is an entire line # long string with comment'" + write(u, '(A)') '7 # integer parsed as real(DP)' + write(u, '(A)') '12.50 # real(DP) parsed as integer (should fail)' + create_parse_test_file = closeFile(u) + end function create_parse_test_file + +end module test_slam_io diff --git a/pFUnittests/test_slamreduction.pf b/pFUnittests/test_slamreduction.pf index c6ad34a..780607f 100644 --- a/pFUnittests/test_slamreduction.pf +++ b/pFUnittests/test_slamreduction.pf @@ -465,7 +465,7 @@ module test_slamreduction ! Test case 1 : LEO r_ef = (/-3485.799126707284, -5898.652976745232, 835.9701786284777/) v_ef = (/-1.3525457950562447, -0.2804534841971075, -7.4721873681232385/) - mjd = 59868.16721065_dp + mjd = 59868.16721065_dp ! Call the subroutine to be tested call reduction_model%earthFixed2inertial_rv(r_ef, v_ef, mjd, r_inr, v_inr) diff --git a/src/inout/slam_error_handling.f90 b/src/inout/slam_error_handling.f90 index 5a89b0a..e756cbe 100644 --- a/src/inout/slam_error_handling.f90 +++ b/src/inout/slam_error_handling.f90 @@ -50,10 +50,10 @@ module slam_error_handling integer, parameter, public :: REMARK = 1 ! remark, meaning that expected results will still be achieved ! This message/error type can thus be seen as an information (INFO) integer, parameter, public :: DEBUG_MSG = 0 ! debug msg , meaning a message intended to be used for debugging purposes - integer, parameter, public :: ERR_UNDEFINED = -1 ! undefined error type + integer, parameter, public :: ERR_UNDEFINED = -1 ! undefined error type integer :: latestError = 0 ! indicating the latest error (0 = no error) - integer :: latestErrorType = ERR_UNDEFINED ! error type undefined as default (equivalent to 'no error occurred yet') + integer :: latestErrorType = ERR_UNDEFINED ! error type undefined as default (equivalent to 'no error occurred yet') !$omp threadprivate(latestError,latestErrorType) @@ -69,18 +69,17 @@ module slam_error_handling integer :: errorLanguage = ENGLISH ! english language as default - character(len=*), dimension(nlangs), parameter :: C_DEBUG_MSG = (/'DEBUG MSG ', & + character(len=*), dimension(nlangs), parameter, public :: C_DEBUG_MSG = (/'DEBUG ', & 'DEBUG-NACHRICHT'/) - - character(len=*), dimension(nlangs), parameter :: C_FATAL = (/'FATAL ERROR ', & + character(len=*), dimension(nlangs), parameter, public :: C_FATAL = (/'ERROR ', & 'SCHWERER FEHLER'/) - character(len=*), dimension(nlangs), parameter :: C_REMARK = (/'REMARK ', & + character(len=*), dimension(nlangs), parameter, public :: C_REMARK = (/'INFO ', & 'HINWEIS'/) - character(len=*), dimension(nlangs), parameter :: C_WARNING = (/'WARNING', & + character(len=*), dimension(nlangs), parameter, public :: C_WARNING = (/'WARN ', & 'WARNUNG'/) - character(len=*), dimension(nlangs), parameter :: C_TERMINATED = (/'+++ PROGRAM TERMINATED +++', & + character(len=*), dimension(nlangs), parameter, public :: C_TERMINATED = (/'+++ PROGRAM TERMINATED +++', & '+++ PROGRAMM BEENDET +++ '/) - character(len=*), dimension(nlangs), parameter :: C_TRACEBACK = (/'Traceback', & + character(len=*), dimension(nlangs), parameter, public :: C_TRACEBACK = (/'Traceback', & 'Traceback'/) !** verbosity @@ -91,12 +90,12 @@ module slam_error_handling integer, parameter, public :: DEBUG_MSGS = 3 ! print debug msgs, remarks, warnings and errors integer, parameter, public :: ALL_MSG = 4 ! print all messages - integer :: log_verbosity = ALL_MSG ! logfile verbosity level (all messages as default + integer :: log_verbosity = REMARKS ! logfile verbosity level (all messages as default ! 0 = error messages only ! 1 = errors and warnings ! 2 = errors, warnings and remarks ! 3 = all messages (incl. file open/close) - integer :: cli_verbosity = ERRORS ! CLI verbosity level (only error messages as default) + integer :: cli_verbosity = REMARKS ! CLI verbosity level (only error messages as default) ! 0 = error messages only ! 1 = errors and warnings ! 2 = errors, warnings and remarks @@ -219,6 +218,7 @@ module slam_error_handling public :: initErrorHandler public :: isControlled public :: isSetErrorHandling + public :: compile_log_record public :: getErrorMessage public :: setControlled public :: setErrorLanguage @@ -241,6 +241,7 @@ module slam_error_handling public :: printTrace public :: resetError public :: resetTrace + public :: write_log_message contains @@ -353,6 +354,8 @@ subroutine initErrorHandler(control, errAction, language, verbLog, verbCli, logf itemp = setLogVerbosity(WARNINGS) case('REMARKS') itemp = setLogVerbosity(REMARKS) + case('DEBUG_MSGS') + itemp = setLogVerbosity(DEBUG_MSGS) case default itemp = setLogVerbosity(ALL_MSG) end select @@ -371,6 +374,8 @@ subroutine initErrorHandler(control, errAction, language, verbLog, verbCli, logf itemp = setCliVerbosity(WARNINGS) case('REMARKS') itemp = setCliVerbosity(REMARKS) + case('DEBUG_MSGS') + itemp = setCliVerbosity(DEBUG_MSGS) case default itemp = setCliVerbosity(ALL_MSG) end select @@ -1316,20 +1321,22 @@ end subroutine resetTrace !============================================================================== ! !> @brief Set error code -!> @author Vitali Braun +!> @author Vitali Braun (VB) +!> @author Christopher Kebschull (CHK) !! !> @date !! !! -!> @param[in] code Error code -!> @param[in] err_type Error type allowing to decide how to recover -!> @param[in] par (optional) string containing additional information +!> @param[in] code Error code +!> @param[in] err_type Error type allowing to decide how to recover +!> @param[in] par (optional) string containing additional information +!> @param[in] errorMessage (optional) Error message, used when par is not passed !! !-------------------------------------------------------------------------- - subroutine setError(code, err_type, par, errorMessage) integer, intent(in) :: code @@ -1337,8 +1344,8 @@ subroutine setError(code, err_type, par, errorMessage) character(len=*), optional, dimension(:), intent(in) :: par character(len=*), optional, intent(in) :: errorMessage - character(len=128) :: ctemp ! temporary string - character(len=512) :: cmess ! message string + character(len=512) :: cmess ! log string + integer :: i !** return if errors have to be ignored @@ -1374,46 +1381,80 @@ subroutine setError(code, err_type, par, errorMessage) end if - !** get subroutine in which error occurred, if in tracing mode - if((controlled .or. tracing) .and. stackCounter /= 0) then - ctemp = ' in subroutine '//trim(traceStack(stackCounter)) - else - ctemp = '' - end if - if(present(errorMessage)) then cmess = errorMessage else call getErrorMessage(code,cmess) end if + call write_log_message(err_type, cmess, par) - !** report error to logfile or cli if requested - select case(err_type) + if(latestErrorType == FATAL .and. errorAction == ERR_ABORT) then - case(DEBUG_MSG) + !** print calling trace + if(tracing) call printTrace() - latestErrorType = DEBUG_MSG - if (cli_verbosity >= DEBUG_MSGS) then ! CLI output + !** deallocate stack + if(allocated(traceStack)) deallocate(traceStack) + if(allocated(frozenStack)) deallocate(frozenStack) - write(*,'(a)') trim(C_DEBUG_MSG(errorLanguage))//':'//trim(ctemp)//':'//trim(cmess) + !** stop program execution with error indicator + stop -1 - end if + end if - if (log_verbosity >= DEBUG_MSGS .and. flag_ichlog) then ! logfile output +end subroutine setError - write(ichlog,'(a)') trim(C_DEBUG_MSG(errorLanguage))//':'//trim(ctemp)//trim(cmess)//':' +!============================================================================== +! +!> @brief Report error to CLI and logfile according to err_type and verbosity +!! +!> @author Christopher Kebschull (CHK) +!! +!> @param[in] err_type Error type (DEBUG_MSG, REMARK, WARNING, FATAL) +!> @param[in] cmess Log message string +!> @param[in] par (optional) Additional parameters for the message +!! +!-------------------------------------------------------------------------- +subroutine write_log_message(err_type, cmess, par) + + integer, intent(in) :: err_type + character(len=*), intent(in) :: cmess + character(len=*), optional, dimension(:), intent(in) :: par + + character(len=1024) :: log_record + integer :: i + character(len=23) :: timestamp + integer, dimension(8) :: date_time_values + + call date_and_time(VALUES=date_time_values) + write(timestamp,('(i4,2("-",i2.2)," ",2(i2.2,":"),(i2.2,".",i3.3))')) date_time_values(1), & + date_time_values(2), & + date_time_values(3), & + date_time_values(5), & + date_time_values(6), & + date_time_values(7), & + date_time_values(8) + select case(err_type) + case(DEBUG_MSG) + log_record = compile_log_record(timestamp, DEBUG_MSG, ' - ', cmess) + latestErrorType = DEBUG_MSG + if (cli_verbosity >= DEBUG_MSGS) then ! CLI output + write(*,'(a)') trim(log_record) end if - case(REMARK) + if (log_verbosity >= DEBUG_MSGS .and. flag_ichlog) then ! logfile output + write(ichlog,'(a)') trim(log_record) + end if + case(REMARK) latestErrorType = REMARK if (cli_verbosity >= REMARKS) then ! CLI output - - write(*,'(a)', advance = "no") trim(C_REMARK(errorLanguage))//':'//trim(ctemp)//':' ! Prepare the message based on the optional parameter or the predefined message if(present(par)) then + log_record = compile_log_record(timestamp, REMARK, ': ', '') + write(*,'(a)') trim(log_record) do i = 1, SIZE_ERROR_PARAMETER if(i <= size(par)) then write(*,'(a)') ' '//trim(errorParameter(i)) @@ -1422,16 +1463,16 @@ subroutine setError(code, err_type, par, errorMessage) end if end do else - write(*,'(a)') ' '//trim(cmess) + log_record = compile_log_record(timestamp, REMARK, ' - ', cmess) + write(*,'(a)') trim(log_record) end if - end if if (log_verbosity >= REMARKS .and. flag_ichlog) then ! logfile output - - write(ichlog,'(a)', advance = "no") trim(C_REMARK(errorLanguage))//':'//trim(ctemp)//':' ! Prepare the message based on the optional parameter or the predefined message if(present(par)) then + log_record = compile_log_record(timestamp, REMARK, ': ', '') + write(ichlog,'(a)') trim(log_record) do i = 1, SIZE_ERROR_PARAMETER if(i <= size(par)) then write(ichlog,'(a)') ' '//trim(errorParameter(i)) @@ -1440,19 +1481,20 @@ subroutine setError(code, err_type, par, errorMessage) end if end do else - write(ichlog,'(a)') ' '//trim(cmess) + log_record = compile_log_record(timestamp, REMARK, ' - ', cmess) + write(ichlog,'(a)') trim(log_record) end if end if case(WARNING) - latestErrorType = WARNING if (cli_verbosity >= WARNINGS) then ! CLI output - write(*,'(a)', advance = "no") trim(C_WARNING(errorLanguage))//':'//trim(ctemp)//':' ! Prepare the message based on the optional parameter or the predefined message if(present(par)) then + log_record = compile_log_record(timestamp, WARNING, ': ', '') + write(*,'(a)') trim(log_record) do i = 1, SIZE_ERROR_PARAMETER if(i <= size(par)) then write(*,'(a)') ' '//trim(errorParameter(i)) @@ -1461,16 +1503,16 @@ subroutine setError(code, err_type, par, errorMessage) end if end do else - write(*,'(a)') ' '//trim(cmess) + log_record = compile_log_record(timestamp, WARNING, ' - ', cmess) + write(*,'(a)') trim(log_record) end if - end if if (log_verbosity >= WARNINGS .and. flag_ichlog) then ! logfile output - - write(ichlog,'(a)', advance = "no") trim(C_WARNING(errorLanguage))//':'//trim(ctemp)//':' ! Prepare the message based on the optional parameter or the predefined message if(present(par)) then + log_record = compile_log_record(timestamp, WARNING, ': ', '') + write(ichlog,'(a)') trim(log_record) do i = 1, SIZE_ERROR_PARAMETER if(i <= size(par)) then write(ichlog,'(a)') ' '//trim(errorParameter(i)) @@ -1479,50 +1521,73 @@ subroutine setError(code, err_type, par, errorMessage) end if end do else - write(ichlog,'(a)') ' '//trim(cmess) + log_record = compile_log_record(timestamp, WARNING, ' - ', cmess) + write(ichlog,'(a)') trim(log_record) end if - end if case(FATAL) - + log_record = compile_log_record(timestamp, FATAL, ' - ', cmess) latestErrorType = FATAL if (cli_verbosity >= ERRORS) then ! CLI output - - write(*,'(a)', advance = "no") trim(C_FATAL(errorLanguage))//':'//trim(ctemp)//':' - write(*,'(a)') ' '//trim(cmess) + write(*,'(a)') trim(log_record) if(errorAction == ERR_ABORT) write(*,'(a)') ' '//C_TERMINATED(errorLanguage) - end if if (log_verbosity >= ERRORS .and. flag_ichlog) then ! logfile output - - write(ichlog,'(a)', advance = "no") trim(C_FATAL(errorLanguage))//':'//trim(ctemp)//':' - write(ichlog,'(a)') ' '//trim(cmess) + write(ichlog,'(a)') trim(log_record) if(errorAction == ERR_ABORT) write(ichlog,'(a)') ' '//C_TERMINATED(errorLanguage) - end if - end select +end subroutine write_log_message - if(latestErrorType == FATAL .and. errorAction == ERR_ABORT) then - - !** print calling trace - if(tracing) call printTrace() - - !** deallocate stack - if(allocated(traceStack)) deallocate(traceStack) - if(allocated(frozenStack)) deallocate(frozenStack) - - !** stop program execution with error indicator - stop -1 +!============================================================================== +! +!> @brief Compile log record +!> @author Christopher Kebschull +!! +!> @date +!! +!! +!> @param[in] timestamp Date and time as string, e.g. 2023-06-20 10:45:07.329 +!> @param[in] log_type Log type as string, e.g. DEBUG, WARNING, ... +!> @param[in] separator Separator characters between log type and log message +!> @param[in] message Log message string +!! +!> @result log_record Compilation of timestamp, message type and message +!! +!-------------------------------------------------------------------------- +function compile_log_record(timestamp, log_level, separator, message) result(log_record) + + character(len=23),intent(in) :: timestamp ! Date and time as string, e.g. 2023-06-20 10:45:07.329 + integer, intent(in) :: log_level ! Log level as integer + character(len=*), intent(in) :: separator ! Separator characters between log type and log message + character(len=*), intent(in) :: message ! Log message string + + character(LEN=9) :: log_type ! Log type as string, e.g. DEBUG, WARNING, ... + character(len=1024) :: log_record ! Compilation of timestamp, message type and message + + select case(log_level) + case(FATAL) + log_type = 'ERROR' + case(WARNING) + log_type = 'WARNING' + case(REMARK) + log_type = 'INFO' + case(DEBUG_MSG) + log_type = 'DEBUG' + end select + if((controlled .or. tracing) .and. stackCounter /= 0) then + log_record = timestamp//' '//'['//trim(traceStack(stackCounter))//'] '//trim(log_type)//separator//trim(message) + else + log_record = timestamp//' '//trim(log_type)//separator//trim(message) end if - return - -end subroutine setError +end function compile_log_record !============================================================================== ! diff --git a/src/inout/slam_io.f90 b/src/inout/slam_io.f90 index de1956e..94ee937 100644 --- a/src/inout/slam_io.f90 +++ b/src/inout/slam_io.f90 @@ -1134,6 +1134,7 @@ end subroutine nxtxmlcontent !> @brief Managing general messages !! !> @author Vitali Braun +!> @anchor Christopher Kebschull !! @version 1.0 !! !> @param[in] cmess Message text given by calling routine @@ -1143,63 +1144,75 @@ end subroutine nxtxmlcontent !!
  • 1 = both to stdout and logfile
  • !!
  • 2 = to stdout only
  • !! - -subroutine slam_message ( cmess, imode) +!! +!============================================================================== +subroutine slam_message (cmess, imode, err_type) !** declaration of formal parameter list variables !--------------------------------------------------------- - integer, intent(in) :: imode - character(len=*), intent(in) :: cmess + integer, intent(in) :: imode + character(len=*), intent(in) :: cmess + integer, intent(in), optional :: err_type !--------------------------------------------------------- - !** declaration of local parameters character(len=*), parameter :: csubid = 'slam_message' - integer, parameter :: klnw = 260 ! line width (number of characters per line) !** declaration of local variables - character(len=klnw), dimension(mln) :: cln ! line buffer array - integer :: ierr ! error flag - integer :: iline ! line number loop counter - integer :: nline ! number of lines found by function breakLine + integer :: ierr ! error flag + character(len=23) :: timestamp ! Date and time as string, e.g. 2023-06-20 10:45:07.329 + character(len=1024) :: log_record ! log record, containing timestamp, log type and log message + integer, dimension(8) :: date_time_values + integer :: log_level ! log level - !** START if(isControlled()) then if(hasToReturn()) return call checkIn(csubid) end if - !** IF logfile is to be used and is not yet open - if ((imode == LOGFILE .or. imode == LOG_AND_STDOUT) .and. getLogfileChannel() <= 0) then - !** open logfile - ierr = setLogFileChannel(openFile(getLogfileName(),SEQUENTIAL,OUT_FORMATTED_OVERWRITE)) + if (present(err_type)) then + log_level = err_type + else + log_level = REMARK end if - !** break message into multiple lines if necessary - nline = breakLine(cmess,klnw,cln) + if (getLogVerbosity() >= log_level .or. getCliVerbosity() >= log_level) then - !** make output to logfile (if requested) - if ((imode == LOGFILE .or. imode == LOG_AND_STDOUT) .and. getLogVerbosity() /= QUIET) then - do iline = 1,nline - write (getLogfileChannel(),'(A)') trim(cln(iline)) - end do - end if + !** IF logfile is to be used and is not yet open + if ((imode == LOGFILE .or. imode == LOG_AND_STDOUT) .and. getLogfileChannel() <= 0) then + !** open logfile + ierr = setLogFileChannel(openFile(getLogfileName(),SEQUENTIAL,OUT_FORMATTED_OVERWRITE)) + end if - !** make output to stdout (if requested) - if ((imode == LOG_AND_STDOUT .or. imode == STDOUT) .and. getCliVerbosity() /= QUIET) then - do iline = 1,nline - write (*,'(A)') trim(cln(iline)) - end do + call date_and_time(VALUES=date_time_values) + write(timestamp,('(i4,2("-",i2.2)," ",2(i2.2,":"),(i2.2,".",i3.3))')) date_time_values(1), & + date_time_values(2), & + date_time_values(3), & + date_time_values(5), & + date_time_values(6), & + date_time_values(7), & + date_time_values(8) + + log_record = compile_log_record(timestamp, log_level, ': ', cmess) + + !** make output to logfile (if requested) + if ((imode == LOGFILE .or. imode == LOG_AND_STDOUT)) then + write (getLogfileChannel(),'(A)') trim(log_record) + end if + + !** make output to stdout (if requested) + if ((imode == LOG_AND_STDOUT .or. imode == STDOUT)) then + write (*,'(A)') trim(log_record) + end if end if if(isControlled()) then call checkOut(csubid) end if - return - end subroutine slam_message - !============================================================================== + +!============================================================================== ! !> @brief Break string into multiple lines !! diff --git a/src/oop/slam_tehl_class.f90 b/src/oop/slam_tehl_class.f90 index cd60a6c..db7c18d 100644 --- a/src/oop/slam_tehl_class.f90 +++ b/src/oop/slam_tehl_class.f90 @@ -219,7 +219,7 @@ subroutine tehl_log_arbitrary(this, message, slam_message_type) character(len = *), intent(in) :: message integer, intent(in) :: slam_message_type - call setError(E_SPECIAL, slam_message_type, (/ message /)) + call setError(code=E_SPECIAL, err_type=slam_message_type, errorMessage=message) !** save message call this%set_current_state_and_msg(slam_message_type, message)