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

pkarwasz pushed a commit to branch fix/2.x/3660_cron-triggering-policy
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git

commit f7e462ec07f116732ffc6436b2204c01a73ad9f1
Author: Piotr P. Karwasz <pkarwasz-git...@apache.org>
AuthorDate: Tue Jul 29 13:05:02 2025 +0200

    test: Add DST Fall Back cases for CronExpression behavior
    
    Adds two test cases to verify that `CronExpression` correctly handles
    ambiguous local times during the end of Daylight Saving Time (Fall Back).
    
    These tests ensure accurate computation of:
    - the previous fire time (used to name archived log files)
    - the next fire time (used to determine the rollover instant)
    
    Correct handling is critical to avoid overwriting log files when:
    - rotation occurs twice on the same local day, or
    - the same filename is generated due to repeated local times (even across 
different calendar days).
---
 .../log4j/core/util/CronExpressionTest.java        | 93 ++++++++++++++++++++++
 .../logging/log4j/core/util/CronExpression.java    |  2 +-
 2 files changed, 94 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..69497dc6b2 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,88 @@ 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);
+    }
+
+    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..934b1ff19a 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
@@ -1163,7 +1163,7 @@ public final class CronExpression {
 
         // move ahead one second, since we're computing the time *after* the
         // given time
-        afterTime = new Date(afterTime.getTime() + 1000);
+        afterTime = new Date(afterTime.getTime() + 1);
         // CronTrigger does not deal with milliseconds
         cl.setTime(afterTime);
         cl.set(Calendar.MILLISECOND, 0);

Reply via email to