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:26 UTC
(logging-log4j2) 01/01: test: Add DST Fall Back cases for CronExpression behavior
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);