You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@logging.apache.org by pk...@apache.org on 2025/07/29 11:05:25 UTC

(logging-log4j2) branch fix/2.x/3660_cron-triggering-policy created (now f7e462ec07)

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

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


      at f7e462ec07 test: Add DST Fall Back cases for CronExpression behavior

This branch includes the following new commits:

     new f7e462ec07 test: Add DST Fall Back cases for CronExpression behavior

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(logging-log4j2) 01/01: test: Add DST Fall Back cases for CronExpression behavior

Posted by pk...@apache.org.
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 <pk...@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);