Skip to content

Commit 88a4f2a

Browse files
author
Sergio García Prado
committed
ISSUE #306
* Improve `CronTab` class. * Increase coverage.
1 parent 9cdef7a commit 88a4f2a

File tree

4 files changed

+199
-14
lines changed

4 files changed

+199
-14
lines changed

packages/core/minos-microservice-networks/minos/networks/scheduling/crontab.py

Lines changed: 78 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,98 @@
22
annotations,
33
)
44

5+
import asyncio
6+
from datetime import (
7+
datetime,
8+
)
9+
from itertools import (
10+
count,
11+
)
12+
from math import (
13+
inf,
14+
)
515
from typing import (
616
Any,
17+
AsyncIterator,
18+
Optional,
719
Union,
820
)
921

1022
from crontab import CronTab as CrontTabImpl
1123

24+
from minos.common import (
25+
current_datetime,
26+
)
27+
1228

1329
class CronTab:
14-
"""TODO"""
30+
"""CronTab class."""
1531

1632
def __init__(self, pattern: Union[str, CrontTabImpl]):
17-
if not isinstance(pattern, CrontTabImpl):
33+
if isinstance(pattern, str) and pattern == "@reboot":
34+
pattern = None
35+
elif not isinstance(pattern, CrontTabImpl):
1836
pattern = CrontTabImpl(pattern)
19-
self.impl = pattern
37+
self._impl = pattern
38+
39+
@property
40+
def impl(self) -> Optional[CrontTabImpl]:
41+
"""Get the crontab implementation.
42+
43+
:return: A ``crontab.CronTab`` or ``None``.
44+
"""
45+
return self._impl
2046

2147
def __hash__(self):
22-
return hash(self.impl.matchers)
48+
return hash((type(self), self._impl_matchers))
2349

2450
def __eq__(self, other: Any) -> bool:
25-
return isinstance(other, type(self)) and self.impl.matchers == other.impl.matchers
51+
return isinstance(other, type(self)) and self._impl_matchers == other._impl_matchers
52+
53+
@property
54+
def _impl_matchers(self):
55+
if self._impl is None:
56+
return None
57+
return self._impl.matchers
58+
59+
async def __aiter__(self) -> AsyncIterator[datetime]:
60+
counter = count()
61+
now = current_datetime()
62+
while next(counter) < self.repetitions:
63+
await self.sleep_until_next(now)
64+
now = current_datetime()
65+
yield now
66+
67+
await asyncio.sleep(inf)
68+
69+
async def sleep_until_next(self, *args, **kwargs) -> None:
70+
"""Sleep until next matching.
71+
72+
:param args: Additional positional arguments.
73+
:param kwargs: Additional named arguments.
74+
:return: This method does not return anything.
75+
"""
76+
await asyncio.sleep(self.get_delay_until_next(*args, **kwargs))
77+
78+
def get_delay_until_next(self, now: Optional[datetime] = None) -> float:
79+
"""Get the time to wait for next matching.
80+
81+
:param now: Current time.
82+
:return:
83+
"""
84+
if self._impl is None:
85+
return 0
86+
87+
if now is None:
88+
now = current_datetime()
89+
return self._impl.next(now)
90+
91+
@property
92+
def repetitions(self) -> Union[int, float]:
93+
"""Get the number of repetitions.
2694
27-
def next(self, *args, **kwargs):
28-
"""TODO"""
29-
self.impl.next(*args, **kwargs)
95+
:return: A ``float`` value.
96+
"""
97+
if self._impl is None:
98+
return 1
99+
return inf

packages/core/minos-microservice-networks/minos/networks/scheduling/schedulers.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -162,12 +162,9 @@ async def run_forever(self) -> NoReturn:
162162
163163
:return: This method never returns.
164164
"""
165-
now = current_datetime()
166-
await asyncio.sleep(self._crontab.next(now))
167165

168-
while True:
169-
now = current_datetime()
170-
await asyncio.gather(asyncio.sleep(self._crontab.next(now)), self.run_once(now))
166+
async for now in self._crontab:
167+
await self.run_once(now)
171168

172169
@property
173170
def running(self) -> bool:
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import unittest
2+
from datetime import (
3+
datetime,
4+
time,
5+
timedelta,
6+
timezone,
7+
)
8+
from math import (
9+
inf,
10+
)
11+
from unittest.mock import (
12+
MagicMock,
13+
call,
14+
patch,
15+
)
16+
17+
from crontab import CronTab as CrontabImpl
18+
19+
from minos.common import (
20+
current_datetime,
21+
)
22+
from minos.networks import (
23+
CronTab,
24+
)
25+
26+
27+
class TestCronTab(unittest.IsolatedAsyncioTestCase):
28+
def test_constructor(self):
29+
crontab = CronTab("@daily")
30+
self.assertEqual(CrontabImpl("@daily").matchers, crontab.impl.matchers)
31+
32+
def test_constructor_reboot(self):
33+
crontab = CronTab("@reboot")
34+
self.assertEqual(None, crontab.impl)
35+
36+
def test_constructor_raises(self):
37+
with self.assertRaises(ValueError):
38+
CronTab("foo")
39+
40+
def test_repetitions(self):
41+
crontab = CronTab("@daily")
42+
self.assertEqual(inf, crontab.repetitions)
43+
44+
def test_repetitions_reboot(self):
45+
crontab = CronTab("@reboot")
46+
self.assertEqual(1, crontab.repetitions)
47+
48+
def test_get_delay_until_next(self):
49+
crontab = CronTab("@daily")
50+
now = current_datetime()
51+
52+
expected = (
53+
datetime.combine(now.date() + timedelta(days=1), time.min, tzinfo=timezone.utc) - now
54+
).total_seconds()
55+
self.assertAlmostEqual(expected, crontab.get_delay_until_next(), places=1)
56+
57+
def test_get_delay_until_next_reboot(self):
58+
crontab = CronTab("@reboot")
59+
self.assertEqual(0, crontab.get_delay_until_next())
60+
61+
def test_hash(self):
62+
crontab = CronTab("@daily")
63+
self.assertIsInstance(hash(crontab), int)
64+
65+
def test_hash_reboot(self):
66+
crontab = CronTab("@reboot")
67+
self.assertIsInstance(hash(crontab), int)
68+
69+
def test_eq(self):
70+
base = CronTab("@daily")
71+
one = CronTab("@daily")
72+
self.assertEqual(base, one)
73+
74+
two = CronTab("@hourly")
75+
self.assertNotEqual(base, two)
76+
77+
three = CronTab("@reboot")
78+
self.assertNotEqual(base, three)
79+
80+
async def test_sleep_until_next(self):
81+
crontab = CronTab("@reboot")
82+
83+
mock = MagicMock(return_value=1234)
84+
crontab.get_delay_until_next = mock
85+
86+
with patch("asyncio.sleep") as mock_sleep:
87+
await crontab.sleep_until_next()
88+
89+
self.assertEqual([call(1234)], mock_sleep.call_args_list)
90+
91+
async def test_aiter(self):
92+
crontab = CronTab("@reboot")
93+
94+
with patch("asyncio.sleep") as mock_sleep:
95+
count = 0
96+
async for now in crontab:
97+
count += 1
98+
self.assertAlmostEqual(current_datetime(), now, delta=timedelta(seconds=1))
99+
self.assertEqual(1, count)
100+
101+
self.assertEqual([call(0), call(inf)], mock_sleep.call_args_list)
102+
103+
104+
if __name__ == "__main__":
105+
unittest.main()

packages/core/minos-microservice-networks/tests/test_networks/test_scheduling/test_schedulers.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import asyncio
2+
import math
23
import unittest
34
import warnings
45
from unittest.mock import (
@@ -107,13 +108,25 @@ async def test_stop(self) -> None:
107108

108109
async def test_run_forever(self) -> None:
109110
with patch("asyncio.sleep") as mock_sleep:
110-
run_once_mock = AsyncMock(side_effect=ValueError)
111+
run_once_mock = AsyncMock(side_effect=[int, ValueError])
111112
self.periodic.run_once = run_once_mock
112113

113114
with self.assertRaises(ValueError):
114115
await self.periodic.run_forever()
115116

116117
self.assertEqual(2, mock_sleep.call_count)
118+
self.assertEqual(2, run_once_mock.call_count)
119+
120+
async def test_run_forever_once(self) -> None:
121+
periodic = PeriodicTask("@reboot", self.fn_mock)
122+
with patch("asyncio.sleep", AsyncMock(side_effect=[int, ValueError])) as mock_sleep:
123+
run_once_mock = AsyncMock()
124+
periodic.run_once = run_once_mock
125+
126+
with self.assertRaises(ValueError):
127+
await periodic.run_forever()
128+
129+
self.assertEqual([call(0), call(math.inf)], mock_sleep.call_args_list)
117130
self.assertEqual(1, run_once_mock.call_count)
118131

119132
async def test_run_once(self) -> None:

0 commit comments

Comments
 (0)