From b0912bdd5dfce09a433f31109e4f8722d31f1d6d Mon Sep 17 00:00:00 2001 From: Dirk Lattermann Date: Thu, 8 Feb 2024 12:59:43 +0100 Subject: [PATCH 1/3] Implement test for problematic non-zero nanos time --- .../model/time/ExecutionTimeNanosTest.java | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 src/test/java/com/cronutils/model/time/ExecutionTimeNanosTest.java diff --git a/src/test/java/com/cronutils/model/time/ExecutionTimeNanosTest.java b/src/test/java/com/cronutils/model/time/ExecutionTimeNanosTest.java new file mode 100644 index 00000000..22204290 --- /dev/null +++ b/src/test/java/com/cronutils/model/time/ExecutionTimeNanosTest.java @@ -0,0 +1,79 @@ +package com.cronutils.model.time; + +import com.cronutils.model.Cron; +import com.cronutils.model.definition.CronDefinition; +import com.cronutils.model.definition.CronDefinitionBuilder; +import com.cronutils.parser.CronParser; +import org.junit.jupiter.api.Test; + +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ExecutionTimeNanosTest { + @Test + public void timeWithNanosMatchingSecondsTest() { + CronDefinition cronDefinition = CronDefinitionBuilder.defineCron() + .withSeconds().and() + .withMinutes().and() + .withHours().and() + .withDayOfMonth().and() + .withMonth().and() + .instance(); + Cron cron = new CronParser(cronDefinition) + .parse("0,1,3/1 * * * *").validate(); + ExecutionTime executionTime = ExecutionTime.forCron(cron); + + ZonedDateTime now0 = ZonedDateTime.of(2024, 2, 7, 13, 46, 0, 999999999, ZoneOffset.UTC); + ZonedDateTime expected0 = ZonedDateTime.of(2024, 2, 7, 13, 46, 1, 0, ZoneOffset.UTC); + Optional nextTime0 = executionTime.nextExecution(now0); + assertTrue(nextTime0.isPresent()); + assertEquals(expected0, nextTime0.get()); + + ZonedDateTime now1 = ZonedDateTime.of(2024, 2, 7, 13, 46, 1, 123456789, ZoneOffset.UTC); + ZonedDateTime expected1 = ZonedDateTime.of(2024, 2, 7, 13, 46, 3, 0, ZoneOffset.UTC); + Optional nextTime1 = executionTime.nextExecution(now1); + assertTrue(nextTime1.isPresent()); + assertEquals(expected1, nextTime1.get()); + + ZonedDateTime now2 = ZonedDateTime.of(2024, 2, 7, 13, 46, 2, 123456789, ZoneOffset.UTC); + ZonedDateTime expected2 = ZonedDateTime.of(2024, 2, 7, 13, 46, 3, 0, ZoneOffset.UTC); + Optional nextTime2 = executionTime.nextExecution(now2); + assertTrue(nextTime2.isPresent()); + assertEquals(expected2, nextTime2.get()); + } + + @Test + public void minutimeWithNanosMatchingMinutesTest() { + CronDefinition cronDefinition = CronDefinitionBuilder.defineCron() + .withMinutes().and() + .withHours().and() + .withDayOfMonth().and() + .withMonth().and() + .instance(); + Cron cron = new CronParser(cronDefinition) + .parse("0,1,3/1 * * * "); + ExecutionTime executionTime = ExecutionTime.forCron(cron); + + ZonedDateTime now0 = ZonedDateTime.of(2024, 2, 7, 13, 0, 7, 123456789, ZoneOffset.UTC); + ZonedDateTime expected0 = ZonedDateTime.of(2024, 2, 7, 13, 1, 0, 0, ZoneOffset.UTC); + Optional nextTime0 = executionTime.nextExecution(now0); + assertTrue(nextTime0.isPresent()); + assertEquals(expected0, nextTime0.get()); + + ZonedDateTime now1 = ZonedDateTime.of(2024, 2, 7, 13, 1, 7, 123456789, ZoneOffset.UTC); + ZonedDateTime expected1 = ZonedDateTime.of(2024, 2, 7, 13, 3, 0, 0, ZoneOffset.UTC); + Optional nextTime1 = executionTime.nextExecution(now1); + assertTrue(nextTime1.isPresent()); + assertEquals(expected1, nextTime1.get()); + + ZonedDateTime now2 = ZonedDateTime.of(2024, 2, 7, 13, 2, 7, 123456789, ZoneOffset.UTC); + ZonedDateTime expected2 = ZonedDateTime.of(2024, 2, 7, 13, 3, 0, 0, ZoneOffset.UTC); + Optional nextTime2 = executionTime.nextExecution(now2); + assertTrue(nextTime2.isPresent()); + assertEquals(expected2, nextTime2.get()); + } +} From 983e0d8c241acabae13fff30e66097a6a028df7c Mon Sep 17 00:00:00 2001 From: Dirk Lattermann Date: Thu, 8 Feb 2024 13:08:04 +0100 Subject: [PATCH 2/3] Set nanos part to 0 for next seconds field match --- .../com/cronutils/model/time/SingleExecutionTime.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/cronutils/model/time/SingleExecutionTime.java b/src/main/java/com/cronutils/model/time/SingleExecutionTime.java index c10c7312..05701f7d 100755 --- a/src/main/java/com/cronutils/model/time/SingleExecutionTime.java +++ b/src/main/java/com/cronutils/model/time/SingleExecutionTime.java @@ -182,11 +182,15 @@ private ExecutionTimeResult potentialNextClosestMatch(final ZonedDateTime date) if (!minutes.getValues().contains(date.getMinute())) { return getNextPotentialMinute(date); } - if (!seconds.getValues().contains(date.getSecond())) { - return getNextPotentialSecond(date); + // Rationale for rounding up the nanos: + // If nanos != 0, then the matched seconds field time is already in the past. + // Additionally, all other fields return the next match with nanos set to zero + ZonedDateTime dateWithRoundedUpNanos = date.getNano() == 0 ? date : date.plusNanos(1_000_000_000 - date.getNano()); + if (!seconds.getValues().contains(dateWithRoundedUpNanos.getSecond())) { + return getNextPotentialSecond(dateWithRoundedUpNanos); } - return new ExecutionTimeResult(date, true); + return new ExecutionTimeResult(dateWithRoundedUpNanos, true); } private ExecutionTimeResult getNextPotentialYear(final ZonedDateTime date, From 43492ccd2bb054194fef16b5afe9802fd9c83c66 Mon Sep 17 00:00:00 2001 From: Dirk Lattermann Date: Fri, 9 Feb 2024 13:37:27 +0100 Subject: [PATCH 3/3] Cleanup and extend test to isMatch and lastExecution --- .../model/time/ExecutionTimeNanosTest.java | 225 ++++++++++++++---- 1 file changed, 180 insertions(+), 45 deletions(-) diff --git a/src/test/java/com/cronutils/model/time/ExecutionTimeNanosTest.java b/src/test/java/com/cronutils/model/time/ExecutionTimeNanosTest.java index 22204290..de4bce7b 100644 --- a/src/test/java/com/cronutils/model/time/ExecutionTimeNanosTest.java +++ b/src/test/java/com/cronutils/model/time/ExecutionTimeNanosTest.java @@ -4,18 +4,20 @@ import com.cronutils.model.definition.CronDefinition; import com.cronutils.model.definition.CronDefinitionBuilder; import com.cronutils.parser.CronParser; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.util.Optional; +import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; public class ExecutionTimeNanosTest { - @Test - public void timeWithNanosMatchingSecondsTest() { + public static ExecutionTime createExecutionTimeWithSeconds() { CronDefinition cronDefinition = CronDefinitionBuilder.defineCron() .withSeconds().and() .withMinutes().and() @@ -24,30 +26,12 @@ public void timeWithNanosMatchingSecondsTest() { .withMonth().and() .instance(); Cron cron = new CronParser(cronDefinition) - .parse("0,1,3/1 * * * *").validate(); - ExecutionTime executionTime = ExecutionTime.forCron(cron); - - ZonedDateTime now0 = ZonedDateTime.of(2024, 2, 7, 13, 46, 0, 999999999, ZoneOffset.UTC); - ZonedDateTime expected0 = ZonedDateTime.of(2024, 2, 7, 13, 46, 1, 0, ZoneOffset.UTC); - Optional nextTime0 = executionTime.nextExecution(now0); - assertTrue(nextTime0.isPresent()); - assertEquals(expected0, nextTime0.get()); - - ZonedDateTime now1 = ZonedDateTime.of(2024, 2, 7, 13, 46, 1, 123456789, ZoneOffset.UTC); - ZonedDateTime expected1 = ZonedDateTime.of(2024, 2, 7, 13, 46, 3, 0, ZoneOffset.UTC); - Optional nextTime1 = executionTime.nextExecution(now1); - assertTrue(nextTime1.isPresent()); - assertEquals(expected1, nextTime1.get()); - - ZonedDateTime now2 = ZonedDateTime.of(2024, 2, 7, 13, 46, 2, 123456789, ZoneOffset.UTC); - ZonedDateTime expected2 = ZonedDateTime.of(2024, 2, 7, 13, 46, 3, 0, ZoneOffset.UTC); - Optional nextTime2 = executionTime.nextExecution(now2); - assertTrue(nextTime2.isPresent()); - assertEquals(expected2, nextTime2.get()); + .parse("0,1,3 * * * *"); + + return ExecutionTime.forCron(cron); } - @Test - public void minutimeWithNanosMatchingMinutesTest() { + public static ExecutionTime createExecutionTimeWithoutSeconds() { CronDefinition cronDefinition = CronDefinitionBuilder.defineCron() .withMinutes().and() .withHours().and() @@ -55,25 +39,176 @@ public void minutimeWithNanosMatchingMinutesTest() { .withMonth().and() .instance(); Cron cron = new CronParser(cronDefinition) - .parse("0,1,3/1 * * * "); - ExecutionTime executionTime = ExecutionTime.forCron(cron); - - ZonedDateTime now0 = ZonedDateTime.of(2024, 2, 7, 13, 0, 7, 123456789, ZoneOffset.UTC); - ZonedDateTime expected0 = ZonedDateTime.of(2024, 2, 7, 13, 1, 0, 0, ZoneOffset.UTC); - Optional nextTime0 = executionTime.nextExecution(now0); - assertTrue(nextTime0.isPresent()); - assertEquals(expected0, nextTime0.get()); - - ZonedDateTime now1 = ZonedDateTime.of(2024, 2, 7, 13, 1, 7, 123456789, ZoneOffset.UTC); - ZonedDateTime expected1 = ZonedDateTime.of(2024, 2, 7, 13, 3, 0, 0, ZoneOffset.UTC); - Optional nextTime1 = executionTime.nextExecution(now1); - assertTrue(nextTime1.isPresent()); - assertEquals(expected1, nextTime1.get()); - - ZonedDateTime now2 = ZonedDateTime.of(2024, 2, 7, 13, 2, 7, 123456789, ZoneOffset.UTC); - ZonedDateTime expected2 = ZonedDateTime.of(2024, 2, 7, 13, 3, 0, 0, ZoneOffset.UTC); - Optional nextTime2 = executionTime.nextExecution(now2); - assertTrue(nextTime2.isPresent()); - assertEquals(expected2, nextTime2.get()); + .parse("0,1,3 * * *"); + + return ExecutionTime.forCron(cron); + } + + public static Stream cronExpressionsWithSeconds() { + // args are "now", "isMatch", "expectedNext", "expectedLast" + // for cron "0,1,3 * * * *" + return Stream.of( + Arguments.of( + ZonedDateTime.of(2024, 2, 7, 13, 46, 0, 0, ZoneOffset.UTC), + true, + ZonedDateTime.of(2024, 2, 7, 13, 46, 1, 0, ZoneOffset.UTC), + ZonedDateTime.of(2024, 2, 7, 13, 45, 3, 0, ZoneOffset.UTC) + ), + Arguments.of( + ZonedDateTime.of(2024, 2, 7, 13, 46, 0, 999999999, ZoneOffset.UTC), + false, + ZonedDateTime.of(2024, 2, 7, 13, 46, 1, 0, ZoneOffset.UTC), + ZonedDateTime.of(2024, 2, 7, 13, 46, 0, 0, ZoneOffset.UTC) + ), + Arguments.of( + ZonedDateTime.of(2024, 2, 7, 13, 46, 1, 0, ZoneOffset.UTC), + true, + ZonedDateTime.of(2024, 2, 7, 13, 46, 3, 0, ZoneOffset.UTC), + ZonedDateTime.of(2024, 2, 7, 13, 46, 0, 0, ZoneOffset.UTC) + ), + Arguments.of( + ZonedDateTime.of(2024, 2, 7, 13, 46, 1, 999999999, ZoneOffset.UTC), + false, + ZonedDateTime.of(2024, 2, 7, 13, 46, 3, 0, ZoneOffset.UTC), + ZonedDateTime.of(2024, 2, 7, 13, 46, 1, 0, ZoneOffset.UTC) + ), + Arguments.of( + ZonedDateTime.of(2024, 2, 7, 13, 46, 2, 0, ZoneOffset.UTC), + false, + ZonedDateTime.of(2024, 2, 7, 13, 46, 3, 0, ZoneOffset.UTC), + ZonedDateTime.of(2024, 2, 7, 13, 46, 1, 0, ZoneOffset.UTC) + ), + Arguments.of( + ZonedDateTime.of(2024, 2, 7, 13, 46, 2, 999999999, ZoneOffset.UTC), + false, + ZonedDateTime.of(2024, 2, 7, 13, 46, 3, 0, ZoneOffset.UTC), + ZonedDateTime.of(2024, 2, 7, 13, 46, 1, 0, ZoneOffset.UTC) + ), + Arguments.of( + ZonedDateTime.of(2024, 2, 7, 13, 46, 3, 0, ZoneOffset.UTC), + true, + ZonedDateTime.of(2024, 2, 7, 13, 47, 0, 0, ZoneOffset.UTC), + ZonedDateTime.of(2024, 2, 7, 13, 46, 1, 0, ZoneOffset.UTC) + ), + Arguments.of( + ZonedDateTime.of(2024, 2, 7, 13, 46, 3, 999999999, ZoneOffset.UTC), + false, + ZonedDateTime.of(2024, 2, 7, 13, 47, 0, 0, ZoneOffset.UTC), + ZonedDateTime.of(2024, 2, 7, 13, 46, 3, 0, ZoneOffset.UTC) + ) + ); + } + + public static Stream cronExpressionsWithoutSeconds() { + // args are "now", "isMatch", "expectedNext", "expectedLast" + // for cron "0,1,3 * * *" + return Stream.of( + Arguments.of( + ZonedDateTime.of(2024, 2, 7, 13, 0, 0, 0, ZoneOffset.UTC), + true, + ZonedDateTime.of(2024, 2, 7, 13, 1, 0, 0, ZoneOffset.UTC), + ZonedDateTime.of(2024, 2, 7, 12, 3, 0, 0, ZoneOffset.UTC) + ), + Arguments.of( + ZonedDateTime.of(2024, 2, 7, 13, 0, 0, 999999999, ZoneOffset.UTC), + false, + ZonedDateTime.of(2024, 2, 7, 13, 1, 0, 0, ZoneOffset.UTC), + ZonedDateTime.of(2024, 2, 7, 13, 0, 0, 0, ZoneOffset.UTC) + ), + Arguments.of( + ZonedDateTime.of(2024, 2, 7, 13, 1, 0, 0, ZoneOffset.UTC), + true, + ZonedDateTime.of(2024, 2, 7, 13, 3, 0, 0, ZoneOffset.UTC), + ZonedDateTime.of(2024, 2, 7, 13, 0, 0, 0, ZoneOffset.UTC) + ), + Arguments.of( + ZonedDateTime.of(2024, 2, 7, 13, 1, 0, 999999999, ZoneOffset.UTC), + false, + ZonedDateTime.of(2024, 2, 7, 13, 3, 0, 0, ZoneOffset.UTC), + ZonedDateTime.of(2024, 2, 7, 13, 1, 0, 0, ZoneOffset.UTC) + ), + Arguments.of( + ZonedDateTime.of(2024, 2, 7, 13, 2, 0, 0, ZoneOffset.UTC), + false, + ZonedDateTime.of(2024, 2, 7, 13, 3, 0, 0, ZoneOffset.UTC), + ZonedDateTime.of(2024, 2, 7, 13, 1, 0, 0, ZoneOffset.UTC) + ), + Arguments.of( + ZonedDateTime.of(2024, 2, 7, 13, 2, 0, 999999999, ZoneOffset.UTC), + false, + ZonedDateTime.of(2024, 2, 7, 13, 3, 0, 0, ZoneOffset.UTC), + ZonedDateTime.of(2024, 2, 7, 13, 1, 0, 0, ZoneOffset.UTC) + ), + Arguments.of( + ZonedDateTime.of(2024, 2, 7, 13, 3, 0, 0, ZoneOffset.UTC), + true, + ZonedDateTime.of(2024, 2, 7, 14, 0, 0, 0, ZoneOffset.UTC), + ZonedDateTime.of(2024, 2, 7, 13, 1, 0, 0, ZoneOffset.UTC) + ), + Arguments.of( + ZonedDateTime.of(2024, 2, 7, 13, 3, 0, 999999999, ZoneOffset.UTC), + true, + ZonedDateTime.of(2024, 2, 7, 14, 0, 0, 0, ZoneOffset.UTC), + ZonedDateTime.of(2024, 2, 7, 13, 3, 0, 0, ZoneOffset.UTC) + ) + ); + } + + @ParameterizedTest + @MethodSource("cronExpressionsWithSeconds") + public void testIsMatchWithSeconds(ZonedDateTime now, boolean expectedIsMatch) { + ExecutionTime executionTime = createExecutionTimeWithSeconds(); + + boolean isMatch = executionTime.isMatch(now); + assertEquals(expectedIsMatch, isMatch); + } + + @ParameterizedTest + @MethodSource("cronExpressionsWithSeconds") + public void testNextExecutionWithSeconds(ZonedDateTime now, boolean ignored1, ZonedDateTime expectedNext) { + ExecutionTime executionTime = createExecutionTimeWithSeconds(); + + Optional nextTime = executionTime.nextExecution(now); + assertTrue(nextTime.isPresent()); + assertEquals(expectedNext, nextTime.get()); + } + + @ParameterizedTest + @MethodSource("cronExpressionsWithSeconds") + public void testLastExecutionWithSeconds(ZonedDateTime now, boolean ignored1, ZonedDateTime ignored2, ZonedDateTime expectedLast) { + ExecutionTime executionTime = createExecutionTimeWithSeconds(); + + Optional lastTime = executionTime.lastExecution(now); + assertTrue(lastTime.isPresent()); + assertEquals(expectedLast, lastTime.get()); + } + + @ParameterizedTest + @MethodSource("cronExpressionsWithoutSeconds") + public void testIsMatchWithoutSeconds(ZonedDateTime now, boolean expectedIsMatch) { + ExecutionTime executionTime = createExecutionTimeWithoutSeconds(); + + boolean isMatch = executionTime.isMatch(now); + assertEquals(expectedIsMatch, isMatch); + } + + @ParameterizedTest + @MethodSource("cronExpressionsWithoutSeconds") + public void testNextExecutionWithoutSeconds(ZonedDateTime now, boolean ignored, ZonedDateTime expectedNext) { + ExecutionTime executionTime = createExecutionTimeWithoutSeconds(); + + Optional nextTime = executionTime.nextExecution(now); + assertTrue(nextTime.isPresent()); + assertEquals(expectedNext, nextTime.get()); + } + + @ParameterizedTest + @MethodSource("cronExpressionsWithoutSeconds") + public void testLastExecutionWithoutSeconds(ZonedDateTime now, boolean ignored1, ZonedDateTime ignored2, ZonedDateTime expectedLast) { + ExecutionTime executionTime = createExecutionTimeWithoutSeconds(); + + Optional lastTime = executionTime.lastExecution(now); + assertTrue(lastTime.isPresent()); + assertEquals(expectedLast, lastTime.get()); } }