From 4c3effd23e3c644b094eb5ccb1e5cda6911ea3d6 Mon Sep 17 00:00:00 2001 From: Bennett Meares <38741257+bmeares@users.noreply.github.com> Date: Wed, 15 May 2024 03:01:56 -0400 Subject: [PATCH] Fixed AndTrigger skipping run times (#914) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removed redundant `self._next_triggers` construction and added `AndTrigger` test. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Alex Grönholm --- docs/versionhistory.rst | 2 + src/apscheduler/triggers/combining.py | 1 - tests/triggers/test_combining.py | 101 +++++++++++++++++++++++++- 3 files changed, 102 insertions(+), 2 deletions(-) diff --git a/docs/versionhistory.rst b/docs/versionhistory.rst index d579d41c7..58407a3a2 100644 --- a/docs/versionhistory.rst +++ b/docs/versionhistory.rst @@ -30,6 +30,8 @@ APScheduler, see the :doc:`migration section `. (PR by MohammadAmin Vahedinia) - Fixed the shutdown procedure of the Redis event broker - Fixed ``SQLAlchemyDataStore`` not respecting custom schema name when creating enums +- Fixed skipped intervals with overlapping schedules in ``AndTrigger`` + (#911 _; PR by Bennett Meares) **4.0.0a4** diff --git a/src/apscheduler/triggers/combining.py b/src/apscheduler/triggers/combining.py index 2b271e719..1f8cee447 100644 --- a/src/apscheduler/triggers/combining.py +++ b/src/apscheduler/triggers/combining.py @@ -87,7 +87,6 @@ def next(self) -> datetime | None: # If all the fire times were within the threshold, return the earliest one if latest_fire_time - earliest_fire_time <= self.threshold: - self._next_fire_times = [t.next() for t in self.triggers] return earliest_fire_time else: raise MaxIterationsReached diff --git a/tests/triggers/test_combining.py b/tests/triggers/test_combining.py index 01c73e25d..03df3a713 100644 --- a/tests/triggers/test_combining.py +++ b/tests/triggers/test_combining.py @@ -1,11 +1,13 @@ from __future__ import annotations -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone import pytest from apscheduler import MaxIterationsReached +from apscheduler.triggers.calendarinterval import CalendarIntervalTrigger from apscheduler.triggers.combining import AndTrigger, OrTrigger +from apscheduler.triggers.cron import CronTrigger from apscheduler.triggers.date import DateTrigger from apscheduler.triggers.interval import IntervalTrigger @@ -62,6 +64,103 @@ def test_repr(self, timezone, serializer): "threshold=1.0, max_iterations=10000)" ) + @pytest.mark.parametrize( + "left_trigger,right_trigger,expected_datetimes", + [ + ( + IntervalTrigger( + hours=6, start_time=datetime(2024, 5, 1, tzinfo=timezone.utc) + ), + IntervalTrigger( + hours=12, start_time=datetime(2024, 5, 1, tzinfo=timezone.utc) + ), + [ + datetime(2024, 5, 1, 0, tzinfo=timezone.utc), + datetime(2024, 5, 1, 12, tzinfo=timezone.utc), + datetime(2024, 5, 2, 0, tzinfo=timezone.utc), + ], + ), + ( + IntervalTrigger( + days=1, start_time=datetime(2024, 5, 1, tzinfo=timezone.utc) + ), + IntervalTrigger( + weeks=1, start_time=datetime(2024, 5, 1, tzinfo=timezone.utc) + ), + [ + datetime(2024, 5, 1, tzinfo=timezone.utc), + datetime(2024, 5, 8, tzinfo=timezone.utc), + datetime(2024, 5, 15, tzinfo=timezone.utc), + ], + ), + ( + CronTrigger( + day_of_week="mon-fri", + hour="*", + timezone=timezone.utc, + start_time=datetime(2024, 5, 3, tzinfo=timezone.utc), + ), + IntervalTrigger( + hours=12, start_time=datetime(2024, 5, 3, tzinfo=timezone.utc) + ), + [ + datetime(2024, 5, 3, 0, tzinfo=timezone.utc), + datetime(2024, 5, 3, 12, tzinfo=timezone.utc), + datetime(2024, 5, 6, 0, tzinfo=timezone.utc), + ], + ), + ( + CronTrigger( + day_of_week="mon-fri", + timezone=timezone.utc, + start_time=datetime(2024, 5, 13, tzinfo=timezone.utc), + ), + IntervalTrigger( + days=4, start_time=datetime(2024, 5, 13, tzinfo=timezone.utc) + ), + [ + datetime(2024, 5, 13, tzinfo=timezone.utc), + datetime(2024, 5, 17, tzinfo=timezone.utc), + datetime(2024, 5, 21, tzinfo=timezone.utc), + datetime(2024, 5, 29, tzinfo=timezone.utc), + ], + ), + ( + CalendarIntervalTrigger( + months=1, + timezone=timezone.utc, + start_date=datetime(2024, 1, 1, tzinfo=timezone.utc), + ), + CronTrigger( + day_of_week="mon-fri", + timezone=timezone.utc, + start_time=datetime(2024, 1, 1, tzinfo=timezone.utc), + ), + [ + datetime(2024, 1, 1, tzinfo=timezone.utc), + datetime(2024, 2, 1, tzinfo=timezone.utc), + datetime(2024, 3, 1, tzinfo=timezone.utc), + datetime(2024, 4, 1, tzinfo=timezone.utc), + datetime(2024, 5, 1, tzinfo=timezone.utc), + datetime(2024, 7, 1, tzinfo=timezone.utc), + datetime(2024, 8, 1, tzinfo=timezone.utc), + datetime(2024, 10, 1, tzinfo=timezone.utc), + datetime(2024, 11, 1, tzinfo=timezone.utc), + ], + ), + ], + ) + def test_overlapping_triggers( + self, left_trigger, right_trigger, expected_datetimes + ): + """ + Verify that the `AndTrigger` fires at the intersection of two triggers. + """ + and_trigger = AndTrigger([left_trigger, right_trigger]) + for expected_datetime in expected_datetimes: + next_datetime = and_trigger.next() + assert next_datetime == expected_datetime + class TestOrTrigger: def test_two_datetriggers(self, timezone, serializer):