From 55fce8ed54a1f11a2a255a987e0cfe35ae5337ba Mon Sep 17 00:00:00 2001 From: Andy Stokely Date: Wed, 1 Oct 2025 14:44:46 -0600 Subject: [PATCH 1/9] Add support for windowed alarms in MPAS timekeeping This commit adds active windows to recurring alarms. If users do not provide a start or stop time, they default to the clock start and stop, so existing behavior is unchanged. New helpers simplify the logic and are validated indirectly through behavior-level tests, allowing future refactoring without breaking the suite. A new test fixture and 16-case suite verify alarm behavior across boundaries, resets, anchors, and direction changes, documenting the alarm ringing contract and ensuring safe future development of MPAS timekeeping. --- src/core_test/mpas_test_core.F | 12 + .../mpas_test_core_timekeeping_tests.F | 547 +++++++++++++++++- src/framework/mpas_timekeeping.F | 84 ++- src/framework/mpas_timekeeping_types.inc | 2 + 4 files changed, 633 insertions(+), 12 deletions(-) diff --git a/src/core_test/mpas_test_core.F b/src/core_test/mpas_test_core.F index dbaee54bbc..3091c91ea2 100644 --- a/src/core_test/mpas_test_core.F +++ b/src/core_test/mpas_test_core.F @@ -242,6 +242,18 @@ function test_core_run(domain) result(iErr)!{{{ end if call mpas_log_write('') + ! + ! Test functionality of alarms with user-defined active windows + ! + call mpas_log_write('') + call mpas_log_write('Testing mpas_window_alarms:') + iErr = mpas_window_alarm_tests(domain) + if (iErr == 0) then + call mpas_log_write('* mpas_window_alarm tests - all tests passed: SUCCESS') + else + call mpas_log_write('* mpas_window_alarm tests - $i failed tests: FAILURE', intArgs=[iErr]) + end if + deallocate(threadErrs) end function test_core_run!}}} diff --git a/src/core_test/mpas_test_core_timekeeping_tests.F b/src/core_test/mpas_test_core_timekeeping_tests.F index a2b214c6b0..4919c9727c 100644 --- a/src/core_test/mpas_test_core_timekeeping_tests.F +++ b/src/core_test/mpas_test_core_timekeeping_tests.F @@ -23,11 +23,549 @@ module test_core_timekeeping_tests private public :: test_core_test_intervals, & - mpas_adjust_alarm_tests + mpas_adjust_alarm_tests, & + mpas_window_alarm_tests - contains + !----------------------------------------------------------------------- + ! type alarm_fixture_t + ! + !> \brief Fixture type for testing alarm ringing behavior with windows. + !> \author Andy Stokely + !> \date 10/02/2025 + !> \details + !> This type provides all clocks, alarms, and precomputed iteration + !> steps needed to exercise windowed alarm test cases. + !> + !> The test timeline is organized as: + !> + !> A B C D E + !> |----------------|----------|------------|----------------| + !> ^ ^ ^ ^ ^ + !> Clock start Alarm anchor Window Window stop Clock stop + !> start + !> + !> Fields map directly to these points and derived step counts. + !----------------------------------------------------------------------- + type alarm_fixture_t + !> Clock start time (A on the timeline) + type(MPAS_Time_type) :: clock_start_time - !*********************************************************************** + !> Clock stop time (E on the timeline) + type(MPAS_Time_type) :: clock_stop_time + + !> Alarm anchor time (B on the timeline) + type(MPAS_Time_type) :: alarm_time + + !> Alarm window start time (C on the timeline) + type(MPAS_Time_type) :: window_start_time + + !> Alarm window stop time (D on the timeline) + type(MPAS_Time_type) :: window_stop_time + + !> Clock step interval (time advanced per tick) + type(MPAS_TimeInterval_type) :: clock_time_step + + !> Alarm recurrence interval (ring spacing) + type(MPAS_TimeInterval_type) :: alarm_interval + + !> Clock under test + type(MPAS_Clock_type) :: clock + + !> Alarm with a user-defined active window + type(MPAS_Alarm_type), pointer :: alarm + + !> Identifier string for the alarm + character(len=:), allocatable :: alarm_id + + !> Total number of steps the clock will run + integer :: num_clock_steps = 120 + + !> Number of substeps per interval (clock resolution) + integer :: num_steps_per_interval = 6 + + !> Steps from clock start to alarm anchor (A→B) + integer :: num_steps_before_anchor = 12 + + !> Steps from anchor to window start (B→C) + integer :: num_steps_before_window = 36 + + !> Steps covering full window duration (C→D) + integer :: num_steps_window = 36 + + !> Steps from window end to clock end (D→E) + integer :: num_steps_post_window = 36 + + !> Absolute step index at anchor (B) + integer :: steps_to_anchor + + !> Absolute step index at window start (C) + integer :: steps_to_window_start + + !> Absolute step index at window midpoint + integer :: steps_to_window_midpoint + + !> Absolute step index at window end (D) + integer :: steps_to_window_end + + !> Absolute step index in post-window region + integer :: steps_to_post_window + end type alarm_fixture_t + +contains + + + !----------------------------------------------------------------------- + ! subroutine advance_clock_n_times + ! + !> \brief Advance a clock forward by a specified number of steps. + !> \author Andy Stokely + !> \date 10/02/2025 + !> \details This routine repeatedly advances the provided clock + !> forward by calling mpas_advance_clock `n` times. + !----------------------------------------------------------------------- + subroutine advance_clock_n_times(clock, n) + use mpas_derived_types, only : MPAS_Clock_type + implicit none + type(MPAS_Clock_type), intent(inout) :: clock + integer, intent(in) :: n + integer :: i + + do i = 1, n + call mpas_advance_clock(clock) + end do + end subroutine advance_clock_n_times + + + !----------------------------------------------------------------------- + ! subroutine setup_alarm_fixture + ! + !> \brief Initialize a test fixture with a clock and alarms. + !> \author Andy Stokely + !> \date 10/02/2025 + !> \details This routine allocates and initializes an alarm_fixture_t + !> structure. + !----------------------------------------------------------------------- + subroutine setup_alarm_fixture(fixture) + implicit none + type(alarm_fixture_t), pointer :: fixture + integer :: ierr + + allocate(fixture) + + ! Alarm ID + fixture%alarm_id = 'alarm' + + ! Clock setup + call mpas_set_time(fixture%clock_start_time, YYYY=2000, MM=01, DD=01, H=0, & + M=0, S=0, S_n=0, S_d=0, ierr=ierr) + call mpas_set_time(fixture%clock_stop_time, YYYY=2000, MM=01, DD=01, H=20, & + M=0, S=0, S_n=0, S_d=0, ierr=ierr) + + call mpas_set_timeInterval(fixture%clock_time_step, dt=600.0_RKIND, ierr=ierr) + + call mpas_create_clock(fixture%clock, fixture%clock_start_time, & + fixture%clock_time_step, fixture%clock_stop_time, ierr=ierr) + + ! Alarm anchor and interval + call mpas_set_time(fixture%alarm_time, YYYY=2000, MM=01, DD=01, H=2, M=0, S=0, S_n=0, S_d=0, ierr=ierr) + call mpas_set_timeInterval(fixture%alarm_interval, dt=3600.0_RKIND, ierr=ierr) + + ! Alarm window + call mpas_set_time(fixture%window_start_time, YYYY=2000, MM=01, DD=01, H=8, M=0, S=0, S_n=0, S_d=0, ierr=ierr) + call mpas_set_time(fixture%window_stop_time, YYYY=2000, MM=01, DD=01, H=14, M=0, S=0, S_n=0, S_d=0, ierr=ierr) + + ! Add alarms to the clock + call mpas_add_clock_alarm(fixture%clock, fixture%alarm_id, fixture%alarm_time, & + alarmTimeInterval = fixture%alarm_interval, & + alarmStartTime = fixture%window_start_time, & + alarmStopTime = fixture%window_stop_time) + fixture%alarm => fixture%clock%alarmListHead + + ! Derived iteration steps + fixture%steps_to_anchor = fixture%num_steps_before_anchor + fixture%steps_to_window_start = fixture%num_steps_before_anchor + fixture%num_steps_before_window + fixture%steps_to_window_midpoint = fixture%steps_to_window_start + fixture%num_steps_window/2 + fixture%steps_to_window_end = fixture%steps_to_window_start + fixture%num_steps_window + fixture%steps_to_post_window = fixture%steps_to_window_end + fixture%num_steps_post_window/2 + end subroutine setup_alarm_fixture + + + !----------------------------------------------------------------------- + ! subroutine teardown_alarm_fixture + ! + !> \brief Finalize and deallocate a test alarm fixture. + !> \author Andy Stokely + !> \date 10/02/2025 + !> \details This routine removes alarms from the test clock, destroys + !> the clock, nullifies alarm pointers, and deallocates the fixture. + !> It ensures a clean state for subsequent tests. + !----------------------------------------------------------------------- + subroutine teardown_alarm_fixture(fixture) + implicit none + type(alarm_fixture_t), pointer :: fixture + integer :: ierr + + call mpas_remove_clock_alarm(fixture%clock, fixture%alarm_id, ierr=ierr) + + call mpas_destroy_clock(fixture%clock, ierr=ierr) + + nullify(fixture%alarm) + deallocate(fixture) + end subroutine teardown_alarm_fixture + + + !----------------------------------------------------------------------- + ! subroutine test_window_alarm + ! + !> \brief Unit test for alarm ringing behavior with user-defined windows. + !> \author Andy Stokely + !> \date 10/02/2025 + !> \details This test suite verifies that an alarm rings only when expected + !> relative to the defined timeline: + !> + !> A = Clock start time + !> B = Alarm anchor time + !> C = Window start time + !> D = Window end time + !> E = Clock end time + !> + !> Timeline: + !> A ---------------- B ----- C ---------------- D --------- E + !> + !> The test covers 16 cases, including behavior: + !> - Before anchor (A → B) + !> - Between anchor and window start (B → C) + !> - At window boundaries (C, D) + !> - Inside the window (C → D) + !> - After leaving the window (D → E) + !> - After reset operations at various points + !> - With anchor times shifted into the window + !> - When the clock direction changes (forward/backward) + !> + !> Each case checks whether the alarm rings at the correct times and + !> uses mpas_log_write to log PASS/FAIL outcomes. + !----------------------------------------------------------------------- + subroutine test_window_alarm(case_idx, ierr) + implicit none + integer, intent(in) :: case_idx + integer, intent(inout) :: ierr + type(alarm_fixture_t), pointer :: f + logical :: ringing + + call setup_alarm_fixture(f) + ierr = 0 + + select case (case_idx) + + !----------------------------------------------------------------------- + ! Case 1: Before anchor (A → B) + ! Alarm should not ring before the anchor time. + !----------------------------------------------------------------------- + case(1) + ringing = mpas_is_alarm_ringing(f%clock, f%alarm_id) + if (ringing) then + call mpas_log_write('FAIL: Alarm is ringing before anchor time') + ierr = ierr + 1 + else + call mpas_log_write('PASS: Alarm is not ringing before anchor time') + end if + + !----------------------------------------------------------------------- + ! Case 2: Before anchor after reset (A → B) + ! Reset should not cause false ringing before anchor. + !----------------------------------------------------------------------- + case(2) + call mpas_reset_clock_alarm(f%clock, f%alarm_id) + ringing = mpas_is_alarm_ringing(f%clock, f%alarm_id) + if (ringing) then + call mpas_log_write('FAIL: Alarm is ringing before anchor time after reset') + ierr = ierr + 1 + else + call mpas_log_write('PASS: Alarm is not ringing before anchor time after reset') + end if + + !----------------------------------------------------------------------- + ! Case 3: After anchor but before window (B → C) + ! Alarm should not ring after anchor until the window begins. + !----------------------------------------------------------------------- + case(3) + call advance_clock_n_times(f%clock, f%steps_to_anchor + 2*f%num_steps_per_interval) + ringing = mpas_is_alarm_ringing(f%clock, f%alarm_id) + if (ringing) then + call mpas_log_write('FAIL: Alarm is ringing after anchor, before window') + ierr = ierr + 1 + else + call mpas_log_write('PASS: Alarm is not ringing after anchor, before window') + end if + + !----------------------------------------------------------------------- + ! Case 4: At window start (C) + ! Alarm should ring exactly at the start of the window. + !----------------------------------------------------------------------- + case(4) + call advance_clock_n_times(f%clock, f%steps_to_window_start) + ringing = mpas_is_alarm_ringing(f%clock, f%alarm_id) + if (.not. ringing) then + call mpas_log_write('FAIL: Alarm is not ringing at start of window') + ierr = ierr + 1 + else + call mpas_log_write('PASS: Alarm is ringing at start of window') + end if + + !----------------------------------------------------------------------- + ! Case 5: At window start (C) after reset + ! Reset should prevent ringing immediately at window start. + !----------------------------------------------------------------------- + case(5) + call advance_clock_n_times(f%clock, f%steps_to_window_start) + call mpas_reset_clock_alarm(f%clock, f%alarm_id) + ringing = mpas_is_alarm_ringing(f%clock, f%alarm_id) + if (ringing) then + call mpas_log_write('FAIL: Alarm is ringing at start of window after reset') + ierr = ierr + 1 + else + call mpas_log_write('PASS: Alarm is not ringing at start of window after reset') + end if + + !----------------------------------------------------------------------- + ! Case 6: Middle of window (C → D) + ! Alarm should ring inside the window. + !----------------------------------------------------------------------- + case(6) + call advance_clock_n_times(f%clock, f%steps_to_window_midpoint) + ringing = mpas_is_alarm_ringing(f%clock, f%alarm_id) + if (.not. ringing) then + call mpas_log_write('FAIL: Alarm is not ringing in middle of window') + ierr = ierr + 1 + else + call mpas_log_write('PASS: Alarm is ringing in middle of window') + end if + + !----------------------------------------------------------------------- + ! Case 7: Middle of window after reset at window start + ! Alarm should ring again after being reset at C. + !----------------------------------------------------------------------- + case(7) + call advance_clock_n_times(f%clock, f%steps_to_window_start) + call mpas_reset_clock_alarm(f%clock, f%alarm_id) + call advance_clock_n_times(f%clock, f%num_steps_window/2) + ringing = mpas_is_alarm_ringing(f%clock, f%alarm_id) + if (.not. ringing) then + call mpas_log_write('FAIL: Alarm is not ringing in middle of window after reset') + ierr = ierr + 1 + else + call mpas_log_write('PASS: Alarm is ringing in middle of window after reset') + end if + + !----------------------------------------------------------------------- + ! Case 8: Middle of window after reset inside window + ! Reset prevents alarm from ringing again immediately inside window. + !----------------------------------------------------------------------- + case(8) + call advance_clock_n_times(f%clock, f%steps_to_window_midpoint) + call mpas_reset_clock_alarm(f%clock, f%alarm_id) + ringing = mpas_is_alarm_ringing(f%clock, f%alarm_id) + if (ringing) then + call mpas_log_write('FAIL: Alarm is ringing in middle of window after reset') + ierr = ierr + 1 + else + call mpas_log_write('PASS: Alarm is not ringing in middle of window after reset') + end if + + !----------------------------------------------------------------------- + ! Case 9: At window end (D) + ! Alarm should ring at the end of the window. + !----------------------------------------------------------------------- + case(9) + call advance_clock_n_times(f%clock, f%steps_to_window_end) + ringing = mpas_is_alarm_ringing(f%clock, f%alarm_id) + if (.not. ringing) then + call mpas_log_write('FAIL: Alarm is not ringing at end of window') + ierr = ierr + 1 + else + call mpas_log_write('PASS: Alarm is ringing at end of window') + end if + + !----------------------------------------------------------------------- + ! Case 10: At window end (D) after reset + ! Reset should suppress ringing at window end. + !----------------------------------------------------------------------- + case(10) + call advance_clock_n_times(f%clock, f%steps_to_window_end) + call mpas_reset_clock_alarm(f%clock, f%alarm_id) + ringing = mpas_is_alarm_ringing(f%clock, f%alarm_id) + if (ringing) then + call mpas_log_write('FAIL: Alarm is ringing at end of window after reset') + ierr = ierr + 1 + else + call mpas_log_write('PASS: Alarm is not ringing at end of window after reset') + end if + + !----------------------------------------------------------------------- + ! Case 11: After leaving window (D → E) before reset + ! Alarm should ring once more just after window ends. + !----------------------------------------------------------------------- + case(11) + call advance_clock_n_times(f%clock, f%steps_to_window_midpoint) + call mpas_reset_clock_alarm(f%clock, f%alarm_id) + call advance_clock_n_times(f%clock, f%num_steps_window/2 + f%num_steps_post_window/2) + ringing = mpas_is_alarm_ringing(f%clock, f%alarm_id) + if (.not. ringing) then + call mpas_log_write('FAIL: Alarm is not ringing after window') + ierr = ierr + 1 + else + call mpas_log_write('PASS: Alarm is ringing after window') + end if + + !----------------------------------------------------------------------- + ! Case 12: After leaving window (D → E) after reset + ! Alarm should not ring after reset outside window. + !----------------------------------------------------------------------- + case(12) + call advance_clock_n_times(f%clock, f%steps_to_window_end + f%num_steps_post_window/2) + call mpas_reset_clock_alarm(f%clock, f%alarm_id) + ringing = mpas_is_alarm_ringing(f%clock, f%alarm_id) + if (ringing) then + call mpas_log_write('FAIL: Alarm is ringing after window after reset') + ierr = ierr + 1 + else + call mpas_log_write('PASS: Alarm is not ringing after window after reset') + end if + + !----------------------------------------------------------------------- + ! Case 13: Anchor after window start (B > C) + ! Ensures alarm does not ring before a delayed anchor, even inside window. + !----------------------------------------------------------------------- + case(13) + call mpas_remove_clock_alarm(f%clock, f%alarm_id) + call mpas_set_time(f%alarm_time, YYYY=2000, MM=01, DD=01, H=9, M=0, S=0, S_n=0, S_d=0, ierr=ierr) + call mpas_add_clock_alarm(f%clock, f%alarm_id, f%alarm_time, & + alarmTimeInterval = f%alarm_interval, & + alarmStartTime = f%window_start_time, & + alarmStopTime = f%window_stop_time) + call advance_clock_n_times(f%clock, f%steps_to_window_start) + ringing = mpas_is_alarm_ringing(f%clock, f%alarm_id) + if (ringing) then + call mpas_log_write('FAIL: Alarm is ringing before anchor (after window start)') + ierr = ierr + 1 + else + call mpas_log_write('PASS: Alarm is not ringing before anchor (after window start)') + end if + call advance_clock_n_times(f%clock, 2*f%num_steps_per_interval) + ringing = mpas_is_alarm_ringing(f%clock, f%alarm_id) + if (.not. ringing) then + call mpas_log_write('FAIL: Alarm is not ringing after anchor (after window start)') + ierr = ierr + 1 + else + call mpas_log_write('PASS: Alarm is ringing after anchor (after window start)') + end if + + !----------------------------------------------------------------------- + ! Case 14: Clock direction change inside window (forward → backward) + ! Alarm should still ring in window after direction change. + !----------------------------------------------------------------------- + case(14) + call advance_clock_n_times(f%clock, f%steps_to_window_midpoint) + call mpas_set_clock_direction(f%clock, MPAS_BACKWARD) + ringing = mpas_is_alarm_ringing(f%clock, f%alarm_id) + if (.not. ringing) then + call mpas_log_write('FAIL: Alarm is not ringing in window after direction change') + ierr = ierr + 1 + else + call mpas_log_write('PASS: Alarm is ringing in window after direction change') + end if + + !----------------------------------------------------------------------- + ! Case 15: Re-entering window boundary after direction change + ! Alarm should ring again at boundary after leaving and re-entering. + !----------------------------------------------------------------------- + case(15) + call advance_clock_n_times(f%clock, f%steps_to_window_end) + call mpas_reset_clock_alarm(f%clock, f%alarm_id) + call advance_clock_n_times(f%clock, f%num_steps_post_window/2) + ringing = mpas_is_alarm_ringing(f%clock, f%alarm_id) + if (ringing) then + call mpas_log_write('FAIL: Alarm is ringing after leaving window before re-entering') + ierr = ierr + 1 + else + call mpas_log_write('PASS: Alarm is not ringing after leaving window before re-entering') + end if + call mpas_set_clock_direction(f%clock, MPAS_BACKWARD) + call advance_clock_n_times(f%clock, f%num_steps_post_window/2) + ringing = mpas_is_alarm_ringing(f%clock, f%alarm_id) + if (.not. ringing) then + call mpas_log_write('FAIL: Alarm is not ringing at boundary after re-entering') + ierr = ierr + 1 + else + call mpas_log_write('PASS: Alarm is ringing at boundary after re-entering') + end if + + !----------------------------------------------------------------------- + ! Case 16: Reset at window start and reverse direction to exit window + !> Alarm should not ring when moving backward out of the window after + !> being reset at the window start. + !----------------------------------------------------------------------- + case(16) + call advance_clock_n_times(f%clock, f%steps_to_window_midpoint) + call mpas_set_clock_direction(f%clock, MPAS_BACKWARD) + call mpas_reset_clock_alarm(f%clock, f%alarm_id) + call advance_clock_n_times(f%clock, f%num_steps_window / 2) + call mpas_reset_clock_alarm(f%clock, f%alarm_id) + call advance_clock_n_times(f%clock, f%steps_to_window_start / 2) + + ringing = mpas_is_alarm_ringing(f%clock, f%alarm_id) + if (ringing) then + call mpas_log_write('FAIL: Alarm rang after reset when moving backward out of window') + ierr = ierr + 1 + else + call mpas_log_write('PASS: Alarm did not ring after reset when moving backward out of window') + end if + + !----------------------------------------------------------------------- + ! Case 17: Outside window after direction change (D → E → backward) + ! Alarm should not ring outside window when clock direction flips. + !----------------------------------------------------------------------- + case(17) + call advance_clock_n_times(f%clock, f%steps_to_window_end + f%num_steps_post_window/2) + call mpas_set_clock_direction(f%clock, MPAS_BACKWARD) + ringing = mpas_is_alarm_ringing(f%clock, f%alarm_id) + if (ringing) then + call mpas_log_write('FAIL: Alarm is ringing outside of window after direction change') + ierr = ierr + 1 + else + call mpas_log_write('PASS: Alarm is not ringing outside of window after direction change') + end if + + end select + + call teardown_alarm_fixture(f) + end subroutine test_window_alarm + + + !----------------------------------------------------------------------- + ! function mpas_window_alarm_tests + ! + !> \brief Run all unit tests for windowed alarm ringing behavior. + !> \author Andy Stokely + !> \date 10/02/2025 + !> \details This driver function executes the suite of test cases + !> implemented in test_window_alarm, aggregating any errors encountered. + !----------------------------------------------------------------------- + integer function mpas_window_alarm_tests(domain) result(ierr) + type(domain_type), intent(inout) :: domain + + integer :: i, ierr_local + + ierr = 0 + do i = 1, 17 + ierr_local = 0 + call test_window_alarm(i, ierr_local) + ierr = ierr + ierr_local + end do + end function mpas_window_alarm_tests + + + !----------------------------------------------------------------------- ! ! routine test_core_test_intervals ! @@ -252,7 +790,7 @@ subroutine mpas_adjust_alarm_tests(domain, ierr) call mpas_set_time(test_alarmTime, YYYY=2000, MM=01, DD=01, H=0, M=0, S=0, S_n=0, S_d=0, ierr=ierr_local) call mpas_set_timeInterval(test_alarmTimeInterval, dt=86400.0_RKIND, ierr=ierr_local) - call mpas_add_clock_alarm(test_clock, 'foobar', test_alarmTime, test_alarmTimeInterval, ierr_local) + call mpas_add_clock_alarm(test_clock, 'foobar', test_alarmTime, test_alarmTimeInterval, ierr=ierr_local) #ifdef MPAS_ADVANCE_TEST_CLOCK do istep = 1, 24*365 @@ -461,4 +999,5 @@ subroutine mpas_adjust_alarm_tests(domain, ierr) end subroutine mpas_adjust_alarm_tests + end module test_core_timekeeping_tests diff --git a/src/framework/mpas_timekeeping.F b/src/framework/mpas_timekeeping.F index 659d9bb4f4..245426505f 100644 --- a/src/framework/mpas_timekeeping.F +++ b/src/framework/mpas_timekeeping.F @@ -486,8 +486,8 @@ type (MPAS_Time_type) function mpas_get_clock_time(clock, whichTime, ierr) end function mpas_get_clock_time - subroutine mpas_add_clock_alarm(clock, alarmID, alarmTime, alarmTimeInterval, ierr) -! TODO: possibly add a stop time for recurring alarms + subroutine mpas_add_clock_alarm(clock, alarmID, alarmTime, alarmTimeInterval, & + alarmStartTime, alarmStopTime, ierr) implicit none @@ -495,6 +495,8 @@ subroutine mpas_add_clock_alarm(clock, alarmID, alarmTime, alarmTimeInterval, ie character (len=*), intent(in) :: alarmID type (MPAS_Time_type), intent(in) :: alarmTime type (MPAS_TimeInterval_type), intent(in), optional :: alarmTimeInterval + type (MPAS_Time_type), intent(in), optional :: alarmStartTime + type (MPAS_Time_type), intent(in), optional :: alarmStopTime integer, intent(out), optional :: ierr type (MPAS_Alarm_type), pointer :: alarmPtr @@ -541,7 +543,16 @@ subroutine mpas_add_clock_alarm(clock, alarmID, alarmTime, alarmTimeInterval, ie alarmPtr % isSet = .true. alarmPtr % ringTime = alarmTime - + if (present(alarmStartTime)) then + alarmPtr % activeStartTime = alarmStartTime + else + alarmPtr % activeStartTime = mpas_get_clock_time(clock, MPAS_START_TIME) + end if + if (present(alarmStopTime)) then + alarmPtr % activeStopTime = alarmStopTime + else + alarmPtr % activeStopTime = mpas_get_clock_time(clock, MPAS_STOP_TIME) + end if if (present(alarmTimeInterval)) then alarmPtr % isRecurring = .true. @@ -810,6 +821,56 @@ subroutine mpas_print_alarm(clock, alarmID, ierr) end subroutine mpas_print_alarm + !----------------------------------------------------------------------- + ! function mpas_is_alarm_active + ! + !> \brief Determine if an alarm is currently active. + !> \author Andy Stokely + !> \date 10/01/2025 + !> \details This function checks whether the provided time falls + !> within the active start and stop times of the given alarm. If so, + !> it returns `.true.`. + !----------------------------------------------------------------------- + logical function mpas_is_alarm_active(alarm, time) + + implicit none + + type(MPAS_Alarm_type), pointer :: alarm + type(MPAS_Time_type), intent(in) :: time + + mpas_is_alarm_active = (alarm % activeStartTime <= time & + .and. time <= alarm % activeStopTime) + + end function mpas_is_alarm_active + + + !----------------------------------------------------------------------- + ! function mpas_prev_ring_in_window + ! + !> \brief Check if the alarm’s previous ring was inside its window. + !> \author Andy Stokely + !> \date 10/01/2025 + !> \details This function tests whether the alarm’s `prevRingTime` + !> occurred strictly within the defined start and stop times of the + !> alarm’s active window. The check uses an open interval: + !> + !> (start, stop) + !> + !> The boundaries themselves are excluded. If the previous ring time + !> lies inside this open interval, the function returns `.true.`. + !----------------------------------------------------------------------- + logical function mpas_prev_ring_in_window(alarm) + + implicit none + + type(MPAS_Alarm_type), pointer :: alarm + + mpas_prev_ring_in_window = (alarm % activeStartTime < alarm % prevRingTime & + .and. alarm % prevRingTime < alarm % activeStopTime) + + end function mpas_prev_ring_in_window + + logical function mpas_is_alarm_ringing(clock, alarmID, interval, ierr) implicit none @@ -824,7 +885,6 @@ logical function mpas_is_alarm_ringing(clock, alarmID, interval, ierr) if (present(ierr)) ierr = 0 mpas_is_alarm_ringing = .false. - alarmPtr => clock % alarmListHead do while (associated(alarmPtr)) if (trim(alarmPtr % alarmID) == trim(alarmID)) then @@ -882,10 +942,12 @@ logical function mpas_in_ringing_envelope(clock, alarmPtr, interval, ierr) integer, intent(out), optional :: ierr type (MPAS_Time_type) :: alarmNow + type (MPAS_Time_type) :: currentTime type (MPAS_Time_type) :: alarmThreshold - alarmNow = mpas_get_clock_time(clock, MPAS_NOW, ierr) - alarmThreshold = alarmPtr % ringTime + currentTime = mpas_get_clock_time(clock, MPAS_NOW, ierr) + alarmNow = currentTime + alarmThreshold = alarmPtr % ringTime mpas_in_ringing_envelope = .false. @@ -900,7 +962,10 @@ logical function mpas_in_ringing_envelope(clock, alarmPtr, interval, ierr) end if if (alarmThreshold <= alarmNow) then - mpas_in_ringing_envelope = .true. + if (mpas_is_alarm_active(alarmPtr, currentTime) & + .or. mpas_prev_ring_in_window(alarmPtr)) then + mpas_in_ringing_envelope = .true. + end if end if else @@ -913,7 +978,10 @@ logical function mpas_in_ringing_envelope(clock, alarmPtr, interval, ierr) end if if (alarmThreshold >= alarmNow) then - mpas_in_ringing_envelope = .true. + if (mpas_is_alarm_active(alarmPtr, currentTime) & + .or. mpas_prev_ring_in_window(alarmPtr)) then + mpas_in_ringing_envelope = .true. + end if end if end if diff --git a/src/framework/mpas_timekeeping_types.inc b/src/framework/mpas_timekeeping_types.inc index bcabf595fc..3b9405f530 100644 --- a/src/framework/mpas_timekeeping_types.inc +++ b/src/framework/mpas_timekeeping_types.inc @@ -25,6 +25,8 @@ logical :: isSet type (MPAS_Time_type) :: ringTime type (MPAS_Time_type) :: prevRingTime + type (MPAS_Time_type) :: activeStartTime + type (MPAS_Time_type) :: activeStopTime type (MPAS_TimeInterval_type) :: ringTimeInterval type (MPAS_Alarm_type), pointer :: next => null() end type From 2196cd01aeace8df193c199af47db40948f73bc4 Mon Sep 17 00:00:00 2001 From: Andy Stokely Date: Fri, 24 Oct 2025 11:27:50 -0600 Subject: [PATCH 2/9] Updated descriptions for test cases 11 and 12. --- src/core_test/mpas_test_core_timekeeping_tests.F | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core_test/mpas_test_core_timekeeping_tests.F b/src/core_test/mpas_test_core_timekeeping_tests.F index 4919c9727c..48c14f85d3 100644 --- a/src/core_test/mpas_test_core_timekeeping_tests.F +++ b/src/core_test/mpas_test_core_timekeeping_tests.F @@ -402,7 +402,7 @@ subroutine test_window_alarm(case_idx, ierr) end if !----------------------------------------------------------------------- - ! Case 11: After leaving window (D → E) before reset + ! Case 11: After leaving window (D → E) before reset outside of window ! Alarm should ring once more just after window ends. !----------------------------------------------------------------------- case(11) @@ -418,7 +418,7 @@ subroutine test_window_alarm(case_idx, ierr) end if !----------------------------------------------------------------------- - ! Case 12: After leaving window (D → E) after reset + ! Case 12: After leaving window (D → E) after reset outside of window ! Alarm should not ring after reset outside window. !----------------------------------------------------------------------- case(12) From fbc8a239300659c9698bad86a1b9108af81b4024 Mon Sep 17 00:00:00 2001 From: Andy Stokely Date: Fri, 24 Oct 2025 12:43:21 -0600 Subject: [PATCH 3/9] Add validation check and test for invalid alarm window ordering - Added a check in mpas_add_clock_alarm to return an error when activeStartTime occurs after activeStopTime. - Updated test suite (case 18) to verify that alarms with reversed window times are rejected, while alarms with equal start and stop times are allowed. --- .../mpas_test_core_timekeeping_tests.F | 41 ++++++++++++++++++- src/framework/mpas_timekeeping.F | 12 ++++-- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/src/core_test/mpas_test_core_timekeeping_tests.F b/src/core_test/mpas_test_core_timekeeping_tests.F index 48c14f85d3..dad27ca496 100644 --- a/src/core_test/mpas_test_core_timekeeping_tests.F +++ b/src/core_test/mpas_test_core_timekeeping_tests.F @@ -251,9 +251,11 @@ subroutine test_window_alarm(case_idx, ierr) integer, intent(inout) :: ierr type(alarm_fixture_t), pointer :: f logical :: ringing + integer :: local_ierr call setup_alarm_fixture(f) ierr = 0 + local_ierr = 0 select case (case_idx) @@ -536,6 +538,43 @@ subroutine test_window_alarm(case_idx, ierr) call mpas_log_write('PASS: Alarm is not ringing outside of window after direction change') end if + !----------------------------------------------------------------------- + ! Case 18: Invalid window ordering test + !> \details + !> This test verifies that mpas_add_clock_alarm correctly handles + !> window time validation: + !> 1. It must reject alarms where the start time occurs after the stop time. + !> 2. It must allow alarms where the start time equals the stop time. + !----------------------------------------------------------------------- + case(18) + ! Start time after stop time rejected + call mpas_remove_clock_alarm(f%clock, f%alarm_id) + call mpas_add_clock_alarm(f%clock, f%alarm_id, f%alarm_time, & + alarmTimeInterval = f%alarm_interval, & + alarmStartTime = f%window_stop_time, & + alarmStopTime = f%window_start_time, ierr=local_ierr) + + if (local_ierr == 0) then + call mpas_log_write('FAIL: Alarm with start time after stop time did not return error') + ierr = ierr + 1 + else + call mpas_log_write('PASS: Alarm with start time after stop time correctly returned error') + end if + + ! Start time equals stop time accepted + call mpas_remove_clock_alarm(f%clock, f%alarm_id) + call mpas_add_clock_alarm(f%clock, f%alarm_id, f%alarm_time, & + alarmTimeInterval = f%alarm_interval, & + alarmStartTime = f%window_start_time, & + alarmStopTime = f%window_start_time, ierr=local_ierr) + + if (local_ierr /= 0) then + call mpas_log_write('FAIL: Alarm with equal start/stop times incorrectly returned error', MPAS_LOG_ERR) + ierr = ierr + 1 + else + call mpas_log_write('PASS: Alarm with equal start/stop times correctly accepted') + end if + end select call teardown_alarm_fixture(f) @@ -557,7 +596,7 @@ integer function mpas_window_alarm_tests(domain) result(ierr) integer :: i, ierr_local ierr = 0 - do i = 1, 17 + do i = 1, 18 ierr_local = 0 call test_window_alarm(i, ierr_local) ierr = ierr + ierr_local diff --git a/src/framework/mpas_timekeeping.F b/src/framework/mpas_timekeeping.F index 245426505f..e1d1132629 100644 --- a/src/framework/mpas_timekeeping.F +++ b/src/framework/mpas_timekeeping.F @@ -497,11 +497,13 @@ subroutine mpas_add_clock_alarm(clock, alarmID, alarmTime, alarmTimeInterval, & type (MPAS_TimeInterval_type), intent(in), optional :: alarmTimeInterval type (MPAS_Time_type), intent(in), optional :: alarmStartTime type (MPAS_Time_type), intent(in), optional :: alarmStopTime - integer, intent(out), optional :: ierr + integer, intent(out), optional :: ierr type (MPAS_Alarm_type), pointer :: alarmPtr integer :: threadNum + if (present(ierr)) ierr = ESMF_SUCCESS + threadNum = mpas_threading_get_thread_num() if ( len_trim(alarmID) > ShortStrKIND ) then @@ -553,6 +555,11 @@ subroutine mpas_add_clock_alarm(clock, alarmID, alarmTime, alarmTimeInterval, & else alarmPtr % activeStopTime = mpas_get_clock_time(clock, MPAS_STOP_TIME) end if + if (alarmPtr % activeStartTime > alarmPtr % activeStopTime) then + call mpas_log_write('Invalid alarm times: start > stop for ' // trim(alarmID), MPAS_LOG_ERR) + if (present(ierr)) ierr = 1 + end if + if (present(alarmTimeInterval)) then alarmPtr % isRecurring = .true. @@ -566,9 +573,6 @@ subroutine mpas_add_clock_alarm(clock, alarmID, alarmTime, alarmTimeInterval, & alarmPtr % isRecurring = .false. alarmPtr % prevRingTime = alarmTime end if - if (present(ierr)) then - if (ierr == ESMF_SUCCESS) ierr = 0 - end if end if !$omp barrier From 58ad2748ac1975eeaf9df24d612e05ef228e1d80 Mon Sep 17 00:00:00 2001 From: Andy Stokely Date: Fri, 24 Oct 2025 15:05:21 -0600 Subject: [PATCH 4/9] Updated window alarm test suite docs. --- src/core_test/mpas_test_core_timekeeping_tests.F | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core_test/mpas_test_core_timekeeping_tests.F b/src/core_test/mpas_test_core_timekeeping_tests.F index dad27ca496..b52f6a0a71 100644 --- a/src/core_test/mpas_test_core_timekeeping_tests.F +++ b/src/core_test/mpas_test_core_timekeeping_tests.F @@ -206,7 +206,6 @@ subroutine teardown_alarm_fixture(fixture) integer :: ierr call mpas_remove_clock_alarm(fixture%clock, fixture%alarm_id, ierr=ierr) - call mpas_destroy_clock(fixture%clock, ierr=ierr) nullify(fixture%alarm) @@ -232,7 +231,7 @@ end subroutine teardown_alarm_fixture !> Timeline: !> A ---------------- B ----- C ---------------- D --------- E !> - !> The test covers 16 cases, including behavior: + !> The test covers 18 cases, including behavior: !> - Before anchor (A → B) !> - Between anchor and window start (B → C) !> - At window boundaries (C, D) @@ -241,6 +240,8 @@ end subroutine teardown_alarm_fixture !> - After reset operations at various points !> - With anchor times shifted into the window !> - When the clock direction changes (forward/backward) + !> - When the window start time is after the stop time (invalid) + !> - When the window start time equals the stop time (valid) !> !> Each case checks whether the alarm rings at the correct times and !> uses mpas_log_write to log PASS/FAIL outcomes. @@ -574,7 +575,6 @@ subroutine test_window_alarm(case_idx, ierr) else call mpas_log_write('PASS: Alarm with equal start/stop times correctly accepted') end if - end select call teardown_alarm_fixture(f) From 931e0292031d9dfba2d457745560108efdd18d15 Mon Sep 17 00:00:00 2001 From: Andy Stokely Date: Tue, 28 Oct 2025 10:45:44 -0600 Subject: [PATCH 5/9] Refactor tests to use assert utilities and clean up arguments Replaced manual PASS/FAIL checks with assert_true and assert_false subroutines for consistency and readability. Removed unused variables and corrected argument intents to better reflect usage. --- src/core_test/mpas_test_core.F | 2 +- .../mpas_test_core_timekeeping_tests.F | 268 ++++++++---------- 2 files changed, 124 insertions(+), 146 deletions(-) diff --git a/src/core_test/mpas_test_core.F b/src/core_test/mpas_test_core.F index 3091c91ea2..ba9c86b280 100644 --- a/src/core_test/mpas_test_core.F +++ b/src/core_test/mpas_test_core.F @@ -247,7 +247,7 @@ function test_core_run(domain) result(iErr)!{{{ ! call mpas_log_write('') call mpas_log_write('Testing mpas_window_alarms:') - iErr = mpas_window_alarm_tests(domain) + iErr = mpas_window_alarm_tests() if (iErr == 0) then call mpas_log_write('* mpas_window_alarm tests - all tests passed: SUCCESS') else diff --git a/src/core_test/mpas_test_core_timekeeping_tests.F b/src/core_test/mpas_test_core_timekeeping_tests.F index b52f6a0a71..3d078449c0 100644 --- a/src/core_test/mpas_test_core_timekeeping_tests.F +++ b/src/core_test/mpas_test_core_timekeeping_tests.F @@ -77,9 +77,6 @@ module test_core_timekeeping_tests !> Identifier string for the alarm character(len=:), allocatable :: alarm_id - !> Total number of steps the clock will run - integer :: num_clock_steps = 120 - !> Number of substeps per interval (clock resolution) integer :: num_steps_per_interval = 6 @@ -136,6 +133,74 @@ subroutine advance_clock_n_times(clock, n) end subroutine advance_clock_n_times + !----------------------------------------------------------------------- + ! subroutine assert_true + ! + !> \brief Assert that a condition is true; logs PASS/FAIL accordingly. + !> \author Andy Stokely + !> \date 10/28/2025 + !> \details + !> Verifies that the provided logical expression evaluates to `.true.`. + !> If the condition is true, a PASS message is logged; otherwise, a FAIL + !> message is logged and the status is set to 1. + !> + !> This routine is used throughout test cases to simplify repetitive + !> PASS/FAIL logic and ensure consistent logging and error tracking. + !> + !> \param condition Logical expression expected to be `.true.` + !> \param message Description of the test condition + !> \param status Integer status flag (0 = pass, 1 = fail) + !----------------------------------------------------------------------- + subroutine assert_true(condition, message, status) + implicit none + logical, intent(in) :: condition + character(len=*), intent(in) :: message + integer, intent(out) :: status + + if (condition) then + call mpas_log_write('PASS: ' // trim(message)) + status = 0 + else + call mpas_log_write('FAIL: ' // trim(message)) + status = 1 + end if + end subroutine assert_true + + + !----------------------------------------------------------------------- + ! subroutine assert_false + ! + !> \brief Assert that a condition is false; logs PASS/FAIL accordingly. + !> \author Andy Stokely + !> \date 10/28/2025 + !> \details + !> Verifies that the provided logical expression evaluates to `.false.`. + !> If the condition is false, a PASS message is logged; otherwise, a FAIL + !> message is logged and the status is set to 1. + !> + !> This routine complements `assert_true` and is used when the expected + !> behavior requires a logical expression to remain false. + !> + !> \param condition Logical expression expected to be `.false.` + !> \param message Description of the test condition + !> \param status Integer status flag (0 = pass, 1 = fail) + !----------------------------------------------------------------------- + subroutine assert_false(condition, message, status) + implicit none + logical, intent(in) :: condition + character(len=*), intent(in) :: message + integer, intent(out) :: status + + if (.not. condition) then + call mpas_log_write('PASS: ' // trim(message)) + status = 0 + else + call mpas_log_write('FAIL: ' // trim(message)) + status = 1 + end if + end subroutine assert_false + + !----------------------------------------------------------------------- ! subroutine setup_alarm_fixture ! @@ -147,7 +212,7 @@ end subroutine advance_clock_n_times !----------------------------------------------------------------------- subroutine setup_alarm_fixture(fixture) implicit none - type(alarm_fixture_t), pointer :: fixture + type(alarm_fixture_t), intent(out), pointer :: fixture integer :: ierr allocate(fixture) @@ -266,12 +331,8 @@ subroutine test_window_alarm(case_idx, ierr) !----------------------------------------------------------------------- case(1) ringing = mpas_is_alarm_ringing(f%clock, f%alarm_id) - if (ringing) then - call mpas_log_write('FAIL: Alarm is ringing before anchor time') - ierr = ierr + 1 - else - call mpas_log_write('PASS: Alarm is not ringing before anchor time') - end if + call assert_false(ringing, 'Alarm is ringing before anchor time', local_ierr) + ierr = ierr + local_ierr !----------------------------------------------------------------------- ! Case 2: Before anchor after reset (A → B) @@ -280,12 +341,8 @@ subroutine test_window_alarm(case_idx, ierr) case(2) call mpas_reset_clock_alarm(f%clock, f%alarm_id) ringing = mpas_is_alarm_ringing(f%clock, f%alarm_id) - if (ringing) then - call mpas_log_write('FAIL: Alarm is ringing before anchor time after reset') - ierr = ierr + 1 - else - call mpas_log_write('PASS: Alarm is not ringing before anchor time after reset') - end if + call assert_false(ringing, 'Alarm is ringing before anchor time after reset', local_ierr) + ierr = ierr + local_ierr !----------------------------------------------------------------------- ! Case 3: After anchor but before window (B → C) @@ -294,12 +351,8 @@ subroutine test_window_alarm(case_idx, ierr) case(3) call advance_clock_n_times(f%clock, f%steps_to_anchor + 2*f%num_steps_per_interval) ringing = mpas_is_alarm_ringing(f%clock, f%alarm_id) - if (ringing) then - call mpas_log_write('FAIL: Alarm is ringing after anchor, before window') - ierr = ierr + 1 - else - call mpas_log_write('PASS: Alarm is not ringing after anchor, before window') - end if + call assert_false(ringing, 'Alarm is ringing after anchor, before window', local_ierr) + ierr = ierr + local_ierr !----------------------------------------------------------------------- ! Case 4: At window start (C) @@ -308,12 +361,8 @@ subroutine test_window_alarm(case_idx, ierr) case(4) call advance_clock_n_times(f%clock, f%steps_to_window_start) ringing = mpas_is_alarm_ringing(f%clock, f%alarm_id) - if (.not. ringing) then - call mpas_log_write('FAIL: Alarm is not ringing at start of window') - ierr = ierr + 1 - else - call mpas_log_write('PASS: Alarm is ringing at start of window') - end if + call assert_true(ringing, 'Alarm is not ringing at start of window', local_ierr) + ierr = ierr + local_ierr !----------------------------------------------------------------------- ! Case 5: At window start (C) after reset @@ -323,12 +372,8 @@ subroutine test_window_alarm(case_idx, ierr) call advance_clock_n_times(f%clock, f%steps_to_window_start) call mpas_reset_clock_alarm(f%clock, f%alarm_id) ringing = mpas_is_alarm_ringing(f%clock, f%alarm_id) - if (ringing) then - call mpas_log_write('FAIL: Alarm is ringing at start of window after reset') - ierr = ierr + 1 - else - call mpas_log_write('PASS: Alarm is not ringing at start of window after reset') - end if + call assert_false(ringing, 'Alarm is ringing at start of window after reset', local_ierr) + ierr = ierr + local_ierr !----------------------------------------------------------------------- ! Case 6: Middle of window (C → D) @@ -337,12 +382,8 @@ subroutine test_window_alarm(case_idx, ierr) case(6) call advance_clock_n_times(f%clock, f%steps_to_window_midpoint) ringing = mpas_is_alarm_ringing(f%clock, f%alarm_id) - if (.not. ringing) then - call mpas_log_write('FAIL: Alarm is not ringing in middle of window') - ierr = ierr + 1 - else - call mpas_log_write('PASS: Alarm is ringing in middle of window') - end if + call assert_true(ringing, 'Alarm is not ringing in middle of window', local_ierr) + ierr = ierr + local_ierr !----------------------------------------------------------------------- ! Case 7: Middle of window after reset at window start @@ -353,12 +394,8 @@ subroutine test_window_alarm(case_idx, ierr) call mpas_reset_clock_alarm(f%clock, f%alarm_id) call advance_clock_n_times(f%clock, f%num_steps_window/2) ringing = mpas_is_alarm_ringing(f%clock, f%alarm_id) - if (.not. ringing) then - call mpas_log_write('FAIL: Alarm is not ringing in middle of window after reset') - ierr = ierr + 1 - else - call mpas_log_write('PASS: Alarm is ringing in middle of window after reset') - end if + call assert_true(ringing, 'Alarm is not ringing in middle of window after reset', local_ierr) + ierr = ierr + local_ierr !----------------------------------------------------------------------- ! Case 8: Middle of window after reset inside window @@ -368,12 +405,8 @@ subroutine test_window_alarm(case_idx, ierr) call advance_clock_n_times(f%clock, f%steps_to_window_midpoint) call mpas_reset_clock_alarm(f%clock, f%alarm_id) ringing = mpas_is_alarm_ringing(f%clock, f%alarm_id) - if (ringing) then - call mpas_log_write('FAIL: Alarm is ringing in middle of window after reset') - ierr = ierr + 1 - else - call mpas_log_write('PASS: Alarm is not ringing in middle of window after reset') - end if + call assert_false(ringing, 'Alarm is ringing in middle of window after reset', local_ierr) + ierr = ierr + local_ierr !----------------------------------------------------------------------- ! Case 9: At window end (D) @@ -382,12 +415,8 @@ subroutine test_window_alarm(case_idx, ierr) case(9) call advance_clock_n_times(f%clock, f%steps_to_window_end) ringing = mpas_is_alarm_ringing(f%clock, f%alarm_id) - if (.not. ringing) then - call mpas_log_write('FAIL: Alarm is not ringing at end of window') - ierr = ierr + 1 - else - call mpas_log_write('PASS: Alarm is ringing at end of window') - end if + call assert_true(ringing, 'Alarm is not ringing at end of window', local_ierr) + ierr = ierr + local_ierr !----------------------------------------------------------------------- ! Case 10: At window end (D) after reset @@ -397,12 +426,8 @@ subroutine test_window_alarm(case_idx, ierr) call advance_clock_n_times(f%clock, f%steps_to_window_end) call mpas_reset_clock_alarm(f%clock, f%alarm_id) ringing = mpas_is_alarm_ringing(f%clock, f%alarm_id) - if (ringing) then - call mpas_log_write('FAIL: Alarm is ringing at end of window after reset') - ierr = ierr + 1 - else - call mpas_log_write('PASS: Alarm is not ringing at end of window after reset') - end if + call assert_false(ringing, 'Alarm is ringing at end of window after reset', local_ierr) + ierr = ierr + local_ierr !----------------------------------------------------------------------- ! Case 11: After leaving window (D → E) before reset outside of window @@ -413,12 +438,8 @@ subroutine test_window_alarm(case_idx, ierr) call mpas_reset_clock_alarm(f%clock, f%alarm_id) call advance_clock_n_times(f%clock, f%num_steps_window/2 + f%num_steps_post_window/2) ringing = mpas_is_alarm_ringing(f%clock, f%alarm_id) - if (.not. ringing) then - call mpas_log_write('FAIL: Alarm is not ringing after window') - ierr = ierr + 1 - else - call mpas_log_write('PASS: Alarm is ringing after window') - end if + call assert_true(ringing, 'Alarm is not ringing after window', local_ierr) + ierr = ierr + local_ierr !----------------------------------------------------------------------- ! Case 12: After leaving window (D → E) after reset outside of window @@ -428,12 +449,8 @@ subroutine test_window_alarm(case_idx, ierr) call advance_clock_n_times(f%clock, f%steps_to_window_end + f%num_steps_post_window/2) call mpas_reset_clock_alarm(f%clock, f%alarm_id) ringing = mpas_is_alarm_ringing(f%clock, f%alarm_id) - if (ringing) then - call mpas_log_write('FAIL: Alarm is ringing after window after reset') - ierr = ierr + 1 - else - call mpas_log_write('PASS: Alarm is not ringing after window after reset') - end if + call assert_false(ringing, 'Alarm is ringing after window after reset', local_ierr) + ierr = ierr + local_ierr !----------------------------------------------------------------------- ! Case 13: Anchor after window start (B > C) @@ -443,25 +460,18 @@ subroutine test_window_alarm(case_idx, ierr) call mpas_remove_clock_alarm(f%clock, f%alarm_id) call mpas_set_time(f%alarm_time, YYYY=2000, MM=01, DD=01, H=9, M=0, S=0, S_n=0, S_d=0, ierr=ierr) call mpas_add_clock_alarm(f%clock, f%alarm_id, f%alarm_time, & - alarmTimeInterval = f%alarm_interval, & - alarmStartTime = f%window_start_time, & - alarmStopTime = f%window_stop_time) + alarmTimeInterval = f%alarm_interval, & + alarmStartTime = f%window_start_time, & + alarmStopTime = f%window_stop_time) call advance_clock_n_times(f%clock, f%steps_to_window_start) ringing = mpas_is_alarm_ringing(f%clock, f%alarm_id) - if (ringing) then - call mpas_log_write('FAIL: Alarm is ringing before anchor (after window start)') - ierr = ierr + 1 - else - call mpas_log_write('PASS: Alarm is not ringing before anchor (after window start)') - end if + call assert_false(ringing, 'Alarm is ringing before anchor (after window start)', local_ierr) + ierr = ierr + local_ierr + call advance_clock_n_times(f%clock, 2*f%num_steps_per_interval) ringing = mpas_is_alarm_ringing(f%clock, f%alarm_id) - if (.not. ringing) then - call mpas_log_write('FAIL: Alarm is not ringing after anchor (after window start)') - ierr = ierr + 1 - else - call mpas_log_write('PASS: Alarm is ringing after anchor (after window start)') - end if + call assert_true(ringing, 'Alarm is not ringing after anchor (after window start)', local_ierr) + ierr = ierr + local_ierr !----------------------------------------------------------------------- ! Case 14: Clock direction change inside window (forward → backward) @@ -471,12 +481,8 @@ subroutine test_window_alarm(case_idx, ierr) call advance_clock_n_times(f%clock, f%steps_to_window_midpoint) call mpas_set_clock_direction(f%clock, MPAS_BACKWARD) ringing = mpas_is_alarm_ringing(f%clock, f%alarm_id) - if (.not. ringing) then - call mpas_log_write('FAIL: Alarm is not ringing in window after direction change') - ierr = ierr + 1 - else - call mpas_log_write('PASS: Alarm is ringing in window after direction change') - end if + call assert_true(ringing, 'Alarm is not ringing in window after direction change', local_ierr) + ierr = ierr + local_ierr !----------------------------------------------------------------------- ! Case 15: Re-entering window boundary after direction change @@ -487,21 +493,14 @@ subroutine test_window_alarm(case_idx, ierr) call mpas_reset_clock_alarm(f%clock, f%alarm_id) call advance_clock_n_times(f%clock, f%num_steps_post_window/2) ringing = mpas_is_alarm_ringing(f%clock, f%alarm_id) - if (ringing) then - call mpas_log_write('FAIL: Alarm is ringing after leaving window before re-entering') - ierr = ierr + 1 - else - call mpas_log_write('PASS: Alarm is not ringing after leaving window before re-entering') - end if + call assert_false(ringing, 'Alarm is ringing after leaving window before re-entering', local_ierr) + ierr = ierr + local_ierr + call mpas_set_clock_direction(f%clock, MPAS_BACKWARD) call advance_clock_n_times(f%clock, f%num_steps_post_window/2) ringing = mpas_is_alarm_ringing(f%clock, f%alarm_id) - if (.not. ringing) then - call mpas_log_write('FAIL: Alarm is not ringing at boundary after re-entering') - ierr = ierr + 1 - else - call mpas_log_write('PASS: Alarm is ringing at boundary after re-entering') - end if + call assert_true(ringing, 'Alarm is not ringing at boundary after re-entering', local_ierr) + ierr = ierr + local_ierr !----------------------------------------------------------------------- ! Case 16: Reset at window start and reverse direction to exit window @@ -515,14 +514,9 @@ subroutine test_window_alarm(case_idx, ierr) call advance_clock_n_times(f%clock, f%num_steps_window / 2) call mpas_reset_clock_alarm(f%clock, f%alarm_id) call advance_clock_n_times(f%clock, f%steps_to_window_start / 2) - ringing = mpas_is_alarm_ringing(f%clock, f%alarm_id) - if (ringing) then - call mpas_log_write('FAIL: Alarm rang after reset when moving backward out of window') - ierr = ierr + 1 - else - call mpas_log_write('PASS: Alarm did not ring after reset when moving backward out of window') - end if + call assert_false(ringing, 'Alarm rang after reset when moving backward out of window', local_ierr) + ierr = ierr + local_ierr !----------------------------------------------------------------------- ! Case 17: Outside window after direction change (D → E → backward) @@ -532,12 +526,8 @@ subroutine test_window_alarm(case_idx, ierr) call advance_clock_n_times(f%clock, f%steps_to_window_end + f%num_steps_post_window/2) call mpas_set_clock_direction(f%clock, MPAS_BACKWARD) ringing = mpas_is_alarm_ringing(f%clock, f%alarm_id) - if (ringing) then - call mpas_log_write('FAIL: Alarm is ringing outside of window after direction change') - ierr = ierr + 1 - else - call mpas_log_write('PASS: Alarm is not ringing outside of window after direction change') - end if + call assert_false(ringing, 'Alarm is ringing outside of window after direction change', local_ierr) + ierr = ierr + local_ierr !----------------------------------------------------------------------- ! Case 18: Invalid window ordering test @@ -551,30 +541,19 @@ subroutine test_window_alarm(case_idx, ierr) ! Start time after stop time rejected call mpas_remove_clock_alarm(f%clock, f%alarm_id) call mpas_add_clock_alarm(f%clock, f%alarm_id, f%alarm_time, & - alarmTimeInterval = f%alarm_interval, & - alarmStartTime = f%window_stop_time, & - alarmStopTime = f%window_start_time, ierr=local_ierr) + alarmTimeInterval = f%alarm_interval, & + alarmStartTime = f%window_stop_time, & + alarmStopTime = f%window_start_time, ierr=local_ierr) + call assert_true(local_ierr /= 0, 'Alarm with start time after stop time did not return error', local_ierr) + ierr = ierr + local_ierr - if (local_ierr == 0) then - call mpas_log_write('FAIL: Alarm with start time after stop time did not return error') - ierr = ierr + 1 - else - call mpas_log_write('PASS: Alarm with start time after stop time correctly returned error') - end if - - ! Start time equals stop time accepted call mpas_remove_clock_alarm(f%clock, f%alarm_id) call mpas_add_clock_alarm(f%clock, f%alarm_id, f%alarm_time, & - alarmTimeInterval = f%alarm_interval, & - alarmStartTime = f%window_start_time, & - alarmStopTime = f%window_start_time, ierr=local_ierr) - - if (local_ierr /= 0) then - call mpas_log_write('FAIL: Alarm with equal start/stop times incorrectly returned error', MPAS_LOG_ERR) - ierr = ierr + 1 - else - call mpas_log_write('PASS: Alarm with equal start/stop times correctly accepted') - end if + alarmTimeInterval = f%alarm_interval, & + alarmStartTime = f%window_start_time, & + alarmStopTime = f%window_start_time, ierr=local_ierr) + call assert_false(local_ierr /= 0, 'Alarm with equal start/stop times incorrectly returned error', local_ierr) + ierr = ierr + local_ierr end select call teardown_alarm_fixture(f) @@ -590,8 +569,7 @@ end subroutine test_window_alarm !> \details This driver function executes the suite of test cases !> implemented in test_window_alarm, aggregating any errors encountered. !----------------------------------------------------------------------- - integer function mpas_window_alarm_tests(domain) result(ierr) - type(domain_type), intent(inout) :: domain + integer function mpas_window_alarm_tests() result(ierr) integer :: i, ierr_local From 9d0e6e93f583f40224c3841e74e1a1d132020bd7 Mon Sep 17 00:00:00 2001 From: Andy Stokely Date: Tue, 28 Oct 2025 13:24:04 -0600 Subject: [PATCH 6/9] Removed call to mpas_log_write in assert utils when the assertion is positive. --- src/core_test/mpas_test_core_timekeeping_tests.F | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/core_test/mpas_test_core_timekeeping_tests.F b/src/core_test/mpas_test_core_timekeeping_tests.F index 3d078449c0..1af6996e14 100644 --- a/src/core_test/mpas_test_core_timekeeping_tests.F +++ b/src/core_test/mpas_test_core_timekeeping_tests.F @@ -158,7 +158,6 @@ subroutine assert_true(condition, message, status) integer, intent(out) :: status if (condition) then - call mpas_log_write('PASS: ' // trim(message)) status = 0 else call mpas_log_write('FAIL: ' // trim(message)) @@ -192,7 +191,6 @@ subroutine assert_false(condition, message, status) integer, intent(out) :: status if (.not. condition) then - call mpas_log_write('PASS: ' // trim(message)) status = 0 else call mpas_log_write('FAIL: ' // trim(message)) From ce6d4a11ea638d903d15524e8ae8ec2389e34f17 Mon Sep 17 00:00:00 2001 From: Andy Stokely Date: Wed, 29 Oct 2025 11:56:09 -0600 Subject: [PATCH 7/9] Updated out of date docstrings for assert utility subroutines. --- src/core_test/mpas_test_core_timekeeping_tests.F | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/core_test/mpas_test_core_timekeeping_tests.F b/src/core_test/mpas_test_core_timekeeping_tests.F index 1af6996e14..b555157a14 100644 --- a/src/core_test/mpas_test_core_timekeeping_tests.F +++ b/src/core_test/mpas_test_core_timekeeping_tests.F @@ -141,8 +141,8 @@ end subroutine advance_clock_n_times !> \date 10/28/2025 !> \details !> Verifies that the provided logical expression evaluates to `.true.`. - !> If the condition is true, a PASS message is logged; otherwise, a FAIL - !> message is logged and the status is set to 1. + !> If the condition is false, a FAIL message is logged and the status + ! is set to 1. !> !> This routine is used throughout test cases to simplify repetitive !> PASS/FAIL logic and ensure consistent logging and error tracking. @@ -174,8 +174,8 @@ end subroutine assert_true !> \date 10/28/2025 !> \details !> Verifies that the provided logical expression evaluates to `.false.`. - !> If the condition is false, a PASS message is logged; otherwise, a FAIL - !> message is logged and the status is set to 1. + !> If the condition is true, a FAIL message is logged and the status + !> is set to 1. !> !> This routine complements `assert_true` and is used when the expected !> behavior requires a logical expression to remain false. @@ -572,6 +572,7 @@ integer function mpas_window_alarm_tests() result(ierr) integer :: i, ierr_local ierr = 0 + call mpas_log_write('Running 18 window alarm tests') do i = 1, 18 ierr_local = 0 call test_window_alarm(i, ierr_local) From 94c91f8b8aef8d297101534c0262d3e69ffec293 Mon Sep 17 00:00:00 2001 From: Andy Stokely Date: Wed, 29 Oct 2025 15:49:27 -0600 Subject: [PATCH 8/9] Replaced all Non-ASCII characters with ASCII equivalent characters. --- .../mpas_test_core_timekeeping_tests.F | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/core_test/mpas_test_core_timekeeping_tests.F b/src/core_test/mpas_test_core_timekeeping_tests.F index b555157a14..563eb54fa7 100644 --- a/src/core_test/mpas_test_core_timekeeping_tests.F +++ b/src/core_test/mpas_test_core_timekeeping_tests.F @@ -80,16 +80,16 @@ module test_core_timekeeping_tests !> Number of substeps per interval (clock resolution) integer :: num_steps_per_interval = 6 - !> Steps from clock start to alarm anchor (A→B) + !> Steps from clock start to alarm anchor (A->B) integer :: num_steps_before_anchor = 12 - !> Steps from anchor to window start (B→C) + !> Steps from anchor to window start (B->C) integer :: num_steps_before_window = 36 - !> Steps covering full window duration (C→D) + !> Steps covering full window duration (C->D) integer :: num_steps_window = 36 - !> Steps from window end to clock end (D→E) + !> Steps from window end to clock end (D->E) integer :: num_steps_post_window = 36 !> Absolute step index at anchor (B) @@ -295,11 +295,11 @@ end subroutine teardown_alarm_fixture !> A ---------------- B ----- C ---------------- D --------- E !> !> The test covers 18 cases, including behavior: - !> - Before anchor (A → B) - !> - Between anchor and window start (B → C) + !> - Before anchor (A -> B) + !> - Between anchor and window start (B -> C) !> - At window boundaries (C, D) - !> - Inside the window (C → D) - !> - After leaving the window (D → E) + !> - Inside the window (C -> D) + !> - After leaving the window (D -> E) !> - After reset operations at various points !> - With anchor times shifted into the window !> - When the clock direction changes (forward/backward) @@ -324,7 +324,7 @@ subroutine test_window_alarm(case_idx, ierr) select case (case_idx) !----------------------------------------------------------------------- - ! Case 1: Before anchor (A → B) + ! Case 1: Before anchor (A -> B) ! Alarm should not ring before the anchor time. !----------------------------------------------------------------------- case(1) @@ -333,7 +333,7 @@ subroutine test_window_alarm(case_idx, ierr) ierr = ierr + local_ierr !----------------------------------------------------------------------- - ! Case 2: Before anchor after reset (A → B) + ! Case 2: Before anchor after reset (A -> B) ! Reset should not cause false ringing before anchor. !----------------------------------------------------------------------- case(2) @@ -343,7 +343,7 @@ subroutine test_window_alarm(case_idx, ierr) ierr = ierr + local_ierr !----------------------------------------------------------------------- - ! Case 3: After anchor but before window (B → C) + ! Case 3: After anchor but before window (B -> C) ! Alarm should not ring after anchor until the window begins. !----------------------------------------------------------------------- case(3) @@ -374,7 +374,7 @@ subroutine test_window_alarm(case_idx, ierr) ierr = ierr + local_ierr !----------------------------------------------------------------------- - ! Case 6: Middle of window (C → D) + ! Case 6: Middle of window (C -> D) ! Alarm should ring inside the window. !----------------------------------------------------------------------- case(6) @@ -428,7 +428,7 @@ subroutine test_window_alarm(case_idx, ierr) ierr = ierr + local_ierr !----------------------------------------------------------------------- - ! Case 11: After leaving window (D → E) before reset outside of window + ! Case 11: After leaving window (D -> E) before reset outside of window ! Alarm should ring once more just after window ends. !----------------------------------------------------------------------- case(11) @@ -440,7 +440,7 @@ subroutine test_window_alarm(case_idx, ierr) ierr = ierr + local_ierr !----------------------------------------------------------------------- - ! Case 12: After leaving window (D → E) after reset outside of window + ! Case 12: After leaving window (D -> E) after reset outside of window ! Alarm should not ring after reset outside window. !----------------------------------------------------------------------- case(12) @@ -472,7 +472,7 @@ subroutine test_window_alarm(case_idx, ierr) ierr = ierr + local_ierr !----------------------------------------------------------------------- - ! Case 14: Clock direction change inside window (forward → backward) + ! Case 14: Clock direction change inside window (forward -> backward) ! Alarm should still ring in window after direction change. !----------------------------------------------------------------------- case(14) @@ -517,7 +517,7 @@ subroutine test_window_alarm(case_idx, ierr) ierr = ierr + local_ierr !----------------------------------------------------------------------- - ! Case 17: Outside window after direction change (D → E → backward) + ! Case 17: Outside window after direction change (D -> E -> backward) ! Alarm should not ring outside window when clock direction flips. !----------------------------------------------------------------------- case(17) @@ -529,7 +529,7 @@ subroutine test_window_alarm(case_idx, ierr) !----------------------------------------------------------------------- ! Case 18: Invalid window ordering test - !> \details + !> !> This test verifies that mpas_add_clock_alarm correctly handles !> window time validation: !> 1. It must reject alarms where the start time occurs after the stop time. From 253c4f8f421fdce327cfb021d88634b0004dd324 Mon Sep 17 00:00:00 2001 From: Andy Stokely Date: Thu, 30 Oct 2025 11:17:48 -0600 Subject: [PATCH 9/9] Added test for case when an alarms active start time is before the clock start time. This behavior is valid. --- .../mpas_test_core_timekeeping_tests.F | 42 +++++++++++++++++-- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/src/core_test/mpas_test_core_timekeeping_tests.F b/src/core_test/mpas_test_core_timekeeping_tests.F index 563eb54fa7..57b4cfa5c3 100644 --- a/src/core_test/mpas_test_core_timekeeping_tests.F +++ b/src/core_test/mpas_test_core_timekeeping_tests.F @@ -294,7 +294,7 @@ end subroutine teardown_alarm_fixture !> Timeline: !> A ---------------- B ----- C ---------------- D --------- E !> - !> The test covers 18 cases, including behavior: + !> The test covers 19 cases, including behavior: !> - Before anchor (A -> B) !> - Between anchor and window start (B -> C) !> - At window boundaries (C, D) @@ -305,6 +305,7 @@ end subroutine teardown_alarm_fixture !> - When the clock direction changes (forward/backward) !> - When the window start time is after the stop time (invalid) !> - When the window start time equals the stop time (valid) + !> - When the window start time is before the clock start time (valid) !> !> Each case checks whether the alarm rings at the correct times and !> uses mpas_log_write to log PASS/FAIL outcomes. @@ -456,7 +457,7 @@ subroutine test_window_alarm(case_idx, ierr) !----------------------------------------------------------------------- case(13) call mpas_remove_clock_alarm(f%clock, f%alarm_id) - call mpas_set_time(f%alarm_time, YYYY=2000, MM=01, DD=01, H=9, M=0, S=0, S_n=0, S_d=0, ierr=ierr) + call mpas_set_time(f%alarm_time, YYYY=2000, MM=01, DD=01, H=9, M=0, S=0, S_n=0, S_d=0) call mpas_add_clock_alarm(f%clock, f%alarm_id, f%alarm_time, & alarmTimeInterval = f%alarm_interval, & alarmStartTime = f%window_start_time, & @@ -552,6 +553,39 @@ subroutine test_window_alarm(case_idx, ierr) alarmStopTime = f%window_start_time, ierr=local_ierr) call assert_false(local_ierr /= 0, 'Alarm with equal start/stop times incorrectly returned error', local_ierr) ierr = ierr + local_ierr + + !----------------------------------------------------------------------- + ! Case 19: Alarm starting before clock start test + !> + !> This test verifies that mpas_add_clock_alarm allows alarms whose + !> active window begins before the clock start time: + !> 1. It must not return an error when the activeStartTime occurs + !> before the clock start time. + !> 2. The alarm should ring immediately when the clock begins, + !> since its anchor time equals the clock start time. + !----------------------------------------------------------------------- + case(19) + call mpas_remove_clock_alarm(f%clock, f%alarm_id) + + ! Set the alarm anchor time equal to the clock start time so it can ring immediately + f%alarm_time = f%clock_start_time + + ! Add an alarm whose active window begins before the clock start time + call mpas_add_clock_alarm(f%clock, f%alarm_id, f%alarm_time, & + alarmTimeInterval = f%alarm_interval, & + alarmStartTime = f%clock_start_time - mul_ti_n(f%alarm_interval, 4), & + alarmStopTime = f%window_stop_time, ierr=local_ierr) + + ! Verify that adding the alarm does not return an error + call assert_false(local_ierr /= 0, & + 'Alarm with start time before clock start time incorrectly returned error', local_ierr) + + ! Check whether the alarm rings immediately at clock start + ringing = mpas_is_alarm_ringing(f%clock, f%alarm_id) + call assert_true(ringing, & + 'Alarm is not ringing inside window with start time before clock start', local_ierr) + ierr = ierr + local_ierr + end select call teardown_alarm_fixture(f) @@ -572,8 +606,8 @@ integer function mpas_window_alarm_tests() result(ierr) integer :: i, ierr_local ierr = 0 - call mpas_log_write('Running 18 window alarm tests') - do i = 1, 18 + call mpas_log_write('Running 19 window alarm tests') + do i = 1, 19 ierr_local = 0 call test_window_alarm(i, ierr_local) ierr = ierr + ierr_local