ExecutionTimeCron4jIntegrationTest.java

/*
 * Copyright 2015 jmrozanec Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You
 * may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
 * for the specific language governing permissions and limitations under the License.
 */

package com.cronutils.model.time;

import com.cronutils.model.CronType;
import com.cronutils.model.definition.CronDefinitionBuilder;
import com.cronutils.parser.CronParser;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.time.DayOfWeek;
import java.time.Month;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Optional;
import java.util.Random;

import static org.junit.jupiter.api.Assertions.*;

public class ExecutionTimeCron4jIntegrationTest {
    private CronParser cron4jCronParser;
    private static final String EVERY_MONDAY_AT_18 = "0 18 * * 1";
    private static final String EVERY_15_MINUTES = "0/15 * * * *";
    private static final String EVERY_2_HOURS = "0 0/2 * * *";
    private static final String EVERY_WEEKDAY_AT_6 = "0 6 * * MON-FRI";
    private static final Logger log = LoggerFactory.getLogger(ExecutionTimeCron4jIntegrationTest.class);
    private static final String NEXT_EXECUTION_NOT_PRESENT_ERROR = "next execution not present";
    private static final String LOG_LAST_RUN = "LastRun = [{}]";
    private static final String LOG_NEXT_RUN = "NextRun = [{}]";

    @BeforeEach
    public void setUp() {
        cron4jCronParser = new CronParser(CronDefinitionBuilder.instanceDefinitionFor(CronType.CRON4J));
    }

    @Test
    public void testForCron() {
        assertEquals(SingleExecutionTime.class, ExecutionTime.forCron(cron4jCronParser.parse(EVERY_MONDAY_AT_18)).getClass());
    }

    /**
     * Issue #37: nextExecution not calculating correct time.
     */
    @Test
    public void testEveryWeekdayAt6() {
        ZonedDateTime lastRun = ZonedDateTime.now();
        final ExecutionTime executionTime = ExecutionTime.forCron(cron4jCronParser.parse(EVERY_WEEKDAY_AT_6));

        // iterate through the next 8 days so we roll over for a week
        // and make sure the next run time is always in the future from the prior run time
        for (int i = 0; i < 8; i++) {

            final Optional<ZonedDateTime> nextExecution = executionTime.nextExecution(lastRun);
            if (nextExecution.isPresent()) {
                final ZonedDateTime nextRun = nextExecution.get();
                log.info(LOG_LAST_RUN, lastRun);
                log.info(LOG_NEXT_RUN, nextRun);

                assertNotEquals(DayOfWeek.SATURDAY, nextRun.getDayOfWeek());
                assertNotEquals(DayOfWeek.SUNDAY, nextRun.getDayOfWeek());
                assertTrue(lastRun.isBefore(nextRun));
                lastRun = lastRun.plusDays(1);
            } else {
                fail(NEXT_EXECUTION_NOT_PRESENT_ERROR);
            }
        }
    }

    /**
     * Issue #37: nextExecution not calculating correct time.
     */
    @Test
    public void testEvery2Hours() {
        ZonedDateTime lastRun = ZonedDateTime.now();
        final ExecutionTime executionTime = ExecutionTime.forCron(cron4jCronParser.parse(EVERY_2_HOURS));

        // iterate through the next 36 hours so we roll over the to the next day
        // and make sure the next run time is always in the future from the prior run time
        for (int i = 0; i < 36; i++) {

            final Optional<ZonedDateTime> nextExecution = executionTime.nextExecution(lastRun);
            if (nextExecution.isPresent()) {
                final ZonedDateTime nextRun = nextExecution.get();
                log.info(LOG_LAST_RUN, lastRun);
                log.info(LOG_NEXT_RUN, nextRun);

                assertTrue(nextRun.getHour() % 2 == 0, String.format("Hour is %s", nextRun.getHour()));
                assertTrue(lastRun.isBefore(nextRun), String.format("Last run is before next one: %s", lastRun.isBefore(nextRun)));
                lastRun = lastRun.plusHours(1);
            } else {
                fail(NEXT_EXECUTION_NOT_PRESENT_ERROR);
            }
        }
    }

    @Test
    public void testQuick() {
        ZonedDateTime lastRun = ZonedDateTime.of(2017, 3, 12, 0, 55, 50, 630, ZoneId.of("America/Los_Angeles"));
        final ExecutionTime executionTime = ExecutionTime.forCron(cron4jCronParser.parse(EVERY_2_HOURS));

        // iterate through the next 36 hours so we roll over the to the next day
        // and make sure the next run time is always in the future from the prior run time
        for (int i = 0; i < 1; i++) {

            final Optional<ZonedDateTime> nextExecution = executionTime.nextExecution(lastRun);
            if (nextExecution.isPresent()) {
                final ZonedDateTime nextRun = nextExecution.get();
                log.info(LOG_LAST_RUN, lastRun);
                log.info(LOG_NEXT_RUN, nextRun);

                assertTrue(nextRun.getHour() % 2 == 0, String.format("Hour is %s", nextRun.getHour()));
                assertTrue(lastRun.isBefore(nextRun), String.format("Last run is before next one: %s", lastRun.isBefore(nextRun)));
                lastRun = lastRun.plusHours(1);
            } else {
                fail(NEXT_EXECUTION_NOT_PRESENT_ERROR);
            }
        }
    }

    /**
     * Issue #37: nextExecution not calculating correct time.
     */
    @Test
    public void testEvery15Minutes() {
        ZonedDateTime lastRun = ZonedDateTime.now();
        final ExecutionTime executionTime = ExecutionTime.forCron(cron4jCronParser.parse(EVERY_15_MINUTES));

        // iterate through the next 75 minutes so we roll over the top of the hour
        // and make sure the next run time is always in the future from the prior run time
        for (int i = 0; i < 75; i++) {

            final Optional<ZonedDateTime> nextExecution = executionTime.nextExecution(lastRun);
            if (nextExecution.isPresent()) {
                final ZonedDateTime nextRun = nextExecution.get();
                log.debug(LOG_LAST_RUN, lastRun);
                log.debug(LOG_NEXT_RUN, nextRun);

                assertTrue(nextRun.getMinute() % 15 == 0);
                assertTrue(lastRun.isBefore(nextRun));
                lastRun = lastRun.plusMinutes(1);
            } else {
                fail(NEXT_EXECUTION_NOT_PRESENT_ERROR);
            }
        }
    }

    /**
     * Issue #26: bug 1: if day of week specified, always from day of month is not considered.
     */
    @Test
    public void testDayOfWeekOverridesAlwaysAtDayOfMonth() {
        final ZonedDateTime now = ZonedDateTime.now();
        final ExecutionTime executionTime = ExecutionTime.forCron(cron4jCronParser.parse(EVERY_MONDAY_AT_18));
        final Optional<ZonedDateTime> nextExecution = executionTime.nextExecution(now);
        if (nextExecution.isPresent()) {
            final ZonedDateTime next = nextExecution.get();
            assertEquals(1, next.getDayOfWeek().getValue());
            assertTrue(now.isBefore(next));
        } else {
            fail(NEXT_EXECUTION_NOT_PRESENT_ERROR);
        }
    }

    /**
     * Issue #26: bug 1: if day of week specified, always from day of month is not considered.
     */
    @Test
    public void testDayOfMonthOverridesAlwaysAtDayOfWeek() {
        final ZonedDateTime now = ZonedDateTime.now();
        final ExecutionTime executionTime = ExecutionTime.forCron(cron4jCronParser.parse("0 18 1 * *"));
        final Optional<ZonedDateTime> nextExecution = executionTime.nextExecution(now);
        if (nextExecution.isPresent()) {
            final ZonedDateTime next = nextExecution.get();
            assertEquals(1, next.getDayOfMonth());
            assertTrue(now.isBefore(next));
        } else {
            fail(NEXT_EXECUTION_NOT_PRESENT_ERROR);
        }
    }

    /**
     * Issue #26: bug 2: nextNext should be greater than next, not the same value.
     */
    @Test
    public void testNextExecutionOverNextExecution() {
        final ZonedDateTime now = ZonedDateTime.now();
        final ExecutionTime executionTime = ExecutionTime.forCron(cron4jCronParser.parse(EVERY_MONDAY_AT_18));
        final Optional<ZonedDateTime> nextExecution = executionTime.nextExecution(now);
        if (nextExecution.isPresent()) {
            final Optional<ZonedDateTime> nextNextExecution = executionTime.nextExecution(nextExecution.get());
            if (nextNextExecution.isPresent()) {
                assertTrue(now.isBefore(nextExecution.get()));
                assertTrue(nextExecution.get().isBefore(nextNextExecution.get()));
                return;
            }
        }
        fail("one of the asserted values was not present");
    }

    /**
     * Issue #203: cron4j definition should generate next execution times matching both the day of month and day of week when both are restricted.
     */
    @Test
    public void testFixedDayOfMonthAndDayOfWeek() {
        // Run only on January 1st if it is a Tuesday, at 9:00AM
        final ExecutionTime executionTime = ExecutionTime.forCron(cron4jCronParser.parse("0 9 1 1 tue"));
        // The next four Tuesday January 1 after January 1, 2017 are in 2019, 2030, 2036, and 2041
        final int[] expectedYears = { 2019, 2030, 2036, 2041 };
        ZonedDateTime next = ZonedDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault());
        for (final int expectedYear : expectedYears) {
            final Optional<ZonedDateTime> nextExecution = executionTime.nextExecution(next);
            assert nextExecution.isPresent();
            next = nextExecution.get();
            final ZonedDateTime expectedDate = ZonedDateTime.of(expectedYear, 1, 1, 9, 0, 0, 0, ZoneId.systemDefault());
            final String expectedMessage = String.format("Expected next execution time: %s, Actual next execution time: %s", expectedDate, next);
            assertEquals(DayOfWeek.TUESDAY, next.getDayOfWeek(), expectedMessage);
            assertEquals(1, next.getDayOfMonth(), expectedMessage);
            assertEquals(expectedYear, next.getYear(), expectedMessage);
            assertEquals(9, next.getHour(), expectedMessage);
            assertEquals(expectedDate, next, expectedMessage);
        }
    }

    /**
     * Issue #203: cron4j definition should generate next execution times matching both the day of month and day of week when both are restricted.
     */
    @Test
    public void testRandomDayOfMonthAndDayOfWeek() {
        // pick a random day of week and day of month
        // DayOfWeek uses 1 (Mon) to 7 (Sun) while cron4j allows 0 (Sun) to 6 (Sat)
        final Random random = new Random();
        final DayOfWeek dayOfWeek = DayOfWeek.of(random.nextInt(7) + 1);
        int dayOfWeekValue = dayOfWeek.getValue();
        if (dayOfWeekValue == 7) {
            dayOfWeekValue = 0;
        }
        final int month = random.nextInt(12) + 1;
        // using max length so it is possible to use February 29 (leap-year)
        final int dayOfMonth = random.nextInt(Month.of(month).maxLength()) + 1;
        final String expression = String.format("0 0 %d %d %d", dayOfMonth, month, dayOfWeekValue);
        final ExecutionTime executionTime = ExecutionTime.forCron(cron4jCronParser.parse(expression));
        log.debug("cron4j expression: {}", expression);
        ZonedDateTime next = ZonedDateTime.now();
        log.debug("Start date: {}", next);
        for (int i = 1; i <= 20; i++) {
            final Optional<ZonedDateTime> nextExecution = executionTime.nextExecution(next);
            assert (nextExecution.isPresent());
            next = nextExecution.get();
            log.debug("Execution #{} date: {}", i, next);
            assertEquals(dayOfWeek, next.getDayOfWeek(), "Incorrect day of the week");
            assertEquals(dayOfMonth, next.getDayOfMonth(), "Incorrect day of the month");
            assertEquals(month, next.getMonthValue(), "Incorrect month");
        }
    }

    @Test
    public void testEvery4HoursPast10Hours() {
        ExecutionTime execTime = ExecutionTime.forCron(cron4jCronParser.parse("0 10/4 * * *"));
        final ZonedDateTime startDate = ZonedDateTime.parse("2019-11-08T00:00Z");
        Optional<ZonedDateTime> next = execTime.nextExecution(startDate);

        assertTrue(next.isPresent());
        assertEquals(ZonedDateTime.parse("2019-11-08T10:00Z"), next.get());
    }

    @Test
    public void testAt20MinutesEvery1HourPast23Hours() {
        ExecutionTime execTime = ExecutionTime.forCron(cron4jCronParser.parse("20 23/1 * * *"));
        final ZonedDateTime startDate = ZonedDateTime.parse("2019-11-08T23:20Z");
        Optional<ZonedDateTime> next = execTime.nextExecution(startDate);

        assertTrue(next.isPresent());
        assertEquals(ZonedDateTime.parse("2019-11-09T23:20Z"), next.get());
    }
}