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