This is an automated email from the ASF dual-hosted git repository.

vy pushed a commit to branch 2.x
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git


The following commit(s) were added to refs/heads/2.x by this push:
     new 841efff2a9 Improve `CronExpression` tests to cover daylight saving and 
scheduling logic (#4081)
841efff2a9 is described below

commit 841efff2a9f7a288c2467522a7299d9927bd4bfd
Author: Ramanathan <[email protected]>
AuthorDate: Fri May 22 00:58:55 2026 +0530

    Improve `CronExpression` tests to cover daylight saving and scheduling 
logic (#4081)
    
    Co-authored-by: Piotr P. Karwasz <[email protected]>
    Co-authored-by: Volkan Yazıcı <[email protected]>
---
 .../log4j/core/util/CronExpressionTest.java        | 126 +++++++++++++++++++++
 .../logging/log4j/core/util/CronExpression.java    |   9 +-
 ..._cover_daylight_saving_and_scheduling_logic.xml |  13 +++
 3 files changed, 147 insertions(+), 1 deletion(-)

diff --git 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/CronExpressionTest.java
 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/CronExpressionTest.java
index 8a93e2b58c..e5f19c992c 100644
--- 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/CronExpressionTest.java
+++ 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/CronExpressionTest.java
@@ -16,12 +16,21 @@
  */
 package org.apache.logging.log4j.core.util;
 
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
 import java.text.SimpleDateFormat;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.time.temporal.ChronoUnit;
 import java.util.Calendar;
 import java.util.Date;
 import java.util.GregorianCalendar;
+import java.util.TimeZone;
+import org.assertj.core.presentation.Representation;
+import org.assertj.core.presentation.StandardRepresentation;
 import org.junit.jupiter.api.Test;
 
 /**
@@ -171,4 +180,121 @@ class CronExpressionTest {
         final Date expected = new GregorianCalendar(2015, 10, 1, 0, 0, 
0).getTime();
         assertEquals(expected, fireDate, "Dates not equal.");
     }
+
+    /**
+     * Test that the next valid time after a fallback at 2:00 am from Daylight 
Saving Time
+     */
+    @Test
+    void daylightSavingChangeAtTwoAm() throws Exception {
+        ZoneId zoneId = ZoneId.of("Australia/Sydney");
+        Representation representation = new 
ZoneOffsetRepresentation(ZoneOffset.ofHours(11));
+        // The beginning of the day when daylight saving time ends in 
Australia in 2025 (switch from UTC+11 to UTC+10).
+        Instant april5 =
+                ZonedDateTime.of(2025, 4, 4, 13, 0, 0, 0, 
ZoneOffset.UTC).toInstant();
+        Instant april6 = april5.plus(24, ChronoUnit.HOURS);
+        Instant april7 = april6.plus(25, ChronoUnit.HOURS);
+
+        final CronExpression expression = new CronExpression("0 0 0 * * ?");
+        expression.setTimeZone(TimeZone.getTimeZone(zoneId));
+        // Check the next valid time after 23:59:59.999 on the day before DST 
ends.
+        Date currentTime = Date.from(april6.minusMillis(1));
+        Instant previousTime = 
expression.getPrevFireTime(currentTime).toInstant();
+        
assertThat(previousTime).withRepresentation(representation).isEqualTo(april5);
+        Instant nextTime = 
expression.getNextValidTimeAfter(currentTime).toInstant();
+        
assertThat(nextTime).withRepresentation(representation).isEqualTo(april6);
+        // Check the next valid time after 00:00:00.001 on the day DST ends.
+        currentTime = Date.from(april6.plusMillis(1));
+        previousTime = expression.getPrevFireTime(currentTime).toInstant();
+        
assertThat(previousTime).withRepresentation(representation).isEqualTo(april6);
+        nextTime = expression.getNextValidTimeAfter(currentTime).toInstant();
+        
assertThat(nextTime).withRepresentation(representation).isEqualTo(april7);
+    }
+
+    /**
+     * Test that the next valid time after a fallback at 0:00 am from Daylight 
Saving Time
+     */
+    @Test
+    void daylightSavingChangeAtMidnight() throws Exception {
+        ZoneId zoneId = ZoneId.of("America/Santiago");
+        Representation representation = new 
ZoneOffsetRepresentation(ZoneOffset.ofHours(-3));
+        // The beginning of the day when daylight saving time ends in Chile in 
2025 (switch from UTC-3 to UTC-4).
+        Instant april5 =
+                ZonedDateTime.of(2025, 4, 5, 3, 0, 0, 0, 
ZoneOffset.UTC).toInstant();
+        // Midnight according to Daylight Saving Time.
+        Instant april6Dst = april5.plus(24, ChronoUnit.HOURS);
+        // Midnight according to Standard Time.
+        Instant april6 = april6Dst.plus(1, ChronoUnit.HOURS);
+        Instant april7 = april6.plus(24, ChronoUnit.HOURS);
+
+        final CronExpression expression = new CronExpression("0 0 0 * * ?");
+        expression.setTimeZone(TimeZone.getTimeZone(zoneId));
+        // Check the next valid time after 23:59:59.999 DST (22:59.59.999 
standard) on the day before DST ends.
+        Date currentTime = Date.from(april6Dst.minusMillis(1));
+        Instant previousTime = 
expression.getPrevFireTime(currentTime).toInstant();
+        
assertThat(previousTime).withRepresentation(representation).isEqualTo(april5);
+        Instant nextTime = 
expression.getNextValidTimeAfter(currentTime).toInstant();
+        
assertThat(nextTime).withRepresentation(representation).isEqualTo(april6);
+        // Check the next valid time after 23:59:59.999 on the day before DST 
ends.
+        currentTime = Date.from(april6.minusMillis(1));
+        previousTime = expression.getPrevFireTime(currentTime).toInstant();
+        
assertThat(previousTime).withRepresentation(representation).isEqualTo(april5);
+        nextTime = expression.getNextValidTimeAfter(currentTime).toInstant();
+        
assertThat(nextTime).withRepresentation(representation).isEqualTo(april6);
+        // Check the next valid time after 00:00:00.001 on the day DST ends.
+        currentTime = Date.from(april6.plusMillis(1));
+        previousTime = expression.getPrevFireTime(currentTime).toInstant();
+        
assertThat(previousTime).withRepresentation(representation).isEqualTo(april6);
+        nextTime = expression.getNextValidTimeAfter(currentTime).toInstant();
+        
assertThat(nextTime).withRepresentation(representation).isEqualTo(april7);
+    }
+
+    /**
+     * Test that the next valid time after a spring forward (23-hour day) is 
correct.
+     * Sydney spring-forward: Oct 5 2025 02:00 UTC+10 → 03:00 UTC+11. Day is 
only 23 hours long.
+     */
+    @Test
+    void daylightSavingSpringForward() throws Exception {
+        ZoneId zoneId = ZoneId.of("Australia/Sydney");
+        Representation representation = new 
ZoneOffsetRepresentation(ZoneOffset.ofHours(10));
+        // Midnight UTC+10 on Oct 5 (the spring-forward day; clocks jump at 
2AM → day is 23h).
+        Instant oct5 =
+                ZonedDateTime.of(2025, 10, 4, 14, 0, 0, 0, 
ZoneOffset.UTC).toInstant();
+        // Next midnight is only 23 hours later (UTC+11 offset after the jump).
+        Instant oct6 = oct5.plus(23, ChronoUnit.HOURS);
+        Instant oct7 = oct6.plus(24, ChronoUnit.HOURS);
+
+        final CronExpression expression = new CronExpression("0 0 0 * * ?");
+        expression.setTimeZone(TimeZone.getTimeZone(zoneId));
+
+        // Check the next valid time after 23:59:59.999 on the spring-forward 
day.
+        Date currentTime = Date.from(oct6.minusMillis(1));
+        Instant previousTime = 
expression.getPrevFireTime(currentTime).toInstant();
+        
assertThat(previousTime).withRepresentation(representation).isEqualTo(oct5);
+        Instant nextTime = 
expression.getNextValidTimeAfter(currentTime).toInstant();
+        
assertThat(nextTime).withRepresentation(representation).isEqualTo(oct6);
+
+        // Check the next valid time after 00:00:00.001 on the day after 
spring-forward.
+        currentTime = Date.from(oct6.plusMillis(1));
+        previousTime = expression.getPrevFireTime(currentTime).toInstant();
+        
assertThat(previousTime).withRepresentation(representation).isEqualTo(oct6);
+        nextTime = expression.getNextValidTimeAfter(currentTime).toInstant();
+        
assertThat(nextTime).withRepresentation(representation).isEqualTo(oct7);
+    }
+
+    private static class ZoneOffsetRepresentation extends 
StandardRepresentation {
+
+        private final ZoneOffset zoneOffset;
+
+        private ZoneOffsetRepresentation(final ZoneOffset zoneOffset) {
+            this.zoneOffset = zoneOffset;
+        }
+
+        @Override
+        public String toStringOf(final Object object) {
+            if (object instanceof Instant) {
+                return ZonedDateTime.ofInstant((Instant) object, 
zoneOffset).toString();
+            }
+            return super.toStringOf(object);
+        }
+    }
 }
diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/CronExpression.java
 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/CronExpression.java
index 96fd555153..6e1433c660 100644
--- 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/CronExpression.java
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/CronExpression.java
@@ -1591,6 +1591,11 @@ public final class CronExpression {
     }
 
     public Date getPrevFireTime(final Date targetDate) {
+        // Cron expressions have second precision. If the input has a 
millisecond fraction,
+        // include fire times at that same wall-clock second by shifting into 
the next second.
+        if (targetDate.getTime() % 1000 != 0) {
+            return getTimeBefore(new Date(targetDate.getTime() + 999));
+        }
         return getTimeBefore(targetDate);
     }
 
@@ -1610,7 +1615,9 @@ public final class CronExpression {
         } else if (hours.first() == ALL_SPEC_INT) {
             return 3600000;
         }
-        return 86400000;
+        // DST spring-forward days can be 23 hours, so using 24 hours here can 
skip
+        // a valid previous fire time around midnight transitions.
+        return 23L * 60L * 60L * 1000L;
     }
 
     private int minInSet(final TreeSet<Integer> set) {
diff --git 
a/src/changelog/.2.x.x/LOG4J2-3660_Improve_CronExpression_tests_to_cover_daylight_saving_and_scheduling_logic.xml
 
b/src/changelog/.2.x.x/LOG4J2-3660_Improve_CronExpression_tests_to_cover_daylight_saving_and_scheduling_logic.xml
new file mode 100644
index 0000000000..60f2cf065c
--- /dev/null
+++ 
b/src/changelog/.2.x.x/LOG4J2-3660_Improve_CronExpression_tests_to_cover_daylight_saving_and_scheduling_logic.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<entry xmlns="https://logging.apache.org/xml/ns";
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+       xsi:schemaLocation="
+           https://logging.apache.org/xml/ns
+           https://logging.apache.org/xml/ns/log4j-changelog-0.xsd";
+       type="fixed">
+  <issue id="3660" 
link="https://github.com/apache/logging-log4j2/issues/3660"/>
+  <issue id="4081" link="https://github.com/apache/logging-log4j2/pull/4081"/>
+  <description format="asciidoc">
+    Improve DST handling in `CronExpression`, including boundary and short-day 
behavior, with added DST transition tests
+  </description>
+</entry>
\ No newline at end of file

Reply via email to