Merge ~dashmage/charm-logrotated:bug-2019990/random-time-cronjob into charm-logrotated:master
- Git
- lp:~dashmage/charm-logrotated
- bug-2019990/random-time-cronjob
- Merge into master
Status: | Merged |
---|---|
Approved by: | Eric Chen |
Approved revision: | af917de4e101a494ca13550fbe29e9f882cde377 |
Merged at revision: | b5d0cd927b51e2741af3b967b7cae8fd0393f8ef |
Proposed branch: | ~dashmage/charm-logrotated:bug-2019990/random-time-cronjob |
Merge into: | charm-logrotated:master |
Diff against target: |
421 lines (+190/-58) 4 files modified
src/config.yaml (+5/-2) src/lib/lib_cron.py (+77/-34) src/tests/unit/conftest.py (+2/-7) src/tests/unit/test_logrotate.py (+106/-15) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
🤖 prod-jenkaas-bootstack (community) | continuous-integration | Approve | |
Eric Chen | Approve | ||
JamesLin | Approve | ||
BootStack Reviewers | Pending | ||
Review via email:
|
Commit message
Implement random time functionality for daily cronjob.
Description of the change
This branch contains changes to fix the random time setting for the `update-
It also fixes the unit tests that have been written assuming that the start, end times were directly added to the crontab file.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Ashley James (dashmage) wrote : | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Mert Kirpici (mertkirpici) : | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Eric Chen (eric-chen) wrote : | # |
As we discovered last time, cronhelper.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Eric Chen (eric-chen) wrote (last edit ): | # |
About test_random_time. Sorry that I do not have specific answer here but just leave some questions.
1. Should we verify "randominess" here?
2. If yes, should we execute it over 100000+ times to approve it? or how to approve it?
3. If no, mock the randInt function?
Please share your finding and conclusion. thanks!
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
JamesLin (jneo8) wrote : | # |
Please see my inline comments.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote : | # |
This merge proposal is being monitored by mergebot. Change the status to Approved to merge.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : | # |
PASSED: Continuous integration, rev:57f55f50274
https:/
Executed test runs:
SUCCESS: https:/
None: https:/
Click here to trigger a rebuild:
https:/
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Ashley James (dashmage) wrote (last edit ): | # |
> As we discovered last time, cronhelper.
- Edited response -
The charm's cronjob will be triggered according to the schedule provided in `logrotate-
In order to resolve this, we could remove the `install_cronjob` function call from the `main` function in lib_cron.py so that the charm's cronjob wouldn't set another random time except for install/config changed hook event or running juju action. As per what I understand, the charm's cronjob would only need to override the logrotate configs at a regular time, and it wouldn't need to install the cronjob as well.
But otherwise as well, even if we leave things as it is, the cron.daily jobs would still run only once a day (at a random time which might change) and I feel it's not a big problem.
---
> About test_random_time. Sorry that I do not have specific answer here but just leave some questions.
1. Should we verify "randominess" here?
2. If yes, should we execute it over 100000+ times to approve it? or how to approve it?
3. If no, mock the randInt function?
So in the unit test for the `get_random_time` method, I'm not actually verifying the "randomness" because we can safely assume that `random.randint` will generate a random number for us. Instead I'm testing whether the random time generated with a variety of inputs is actually within the time range (between start and end time).
For the other unit test `test_cron_
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Mert Kirpici (mertkirpici) : | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : | # |
PASSED: Continuous integration, rev:fc8d83446a0
https:/
Executed test runs:
SUCCESS: https:/
None: https:/
Click here to trigger a rebuild:
https:/
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
JamesLin (jneo8) wrote : | # |
Please see my inline comments.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Eric Chen (eric-chen) wrote : | # |
> ---
> > About test_random_time. Sorry that I do not have specific answer here but
> just leave some questions.
>
> 1. Should we verify "randominess" here?
> 2. If yes, should we execute it over 100000+ times to approve it? or how to
> approve it?
> 3. If no, mock the randInt function?
>
>
> So in the unit test for the `get_random_time` method, I'm not actually
> verifying the "randomness" because we can safely assume that `random.randint`
> will generate a random number for us. Instead I'm testing whether the random
> time generated with a variety of inputs is actually within the time range
> (between start and end time).
From my prospective, it's testing the "randomness".
Or May I ask another question from another direction:
After the unit test pass, can we 100% confirm the return value is actually within the time range? I don't think it was been proofed. We only test the "get_random_time" less than 10 times.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Eric Chen (eric-chen) wrote : | # |
I prefer to remove "install_cronjob" in the charm's cronjob if it doesn't voliate the intention in this issue.
https:/
Please review the code and confirm the impact again.
Thanks!
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : | # |
PASSED: Continuous integration, rev:e2d3b058266
https:/
Executed test runs:
SUCCESS: https:/
None: https:/
Click here to trigger a rebuild:
https:/
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Ashley James (dashmage) wrote : | # |
> From my prospective, it's testing the "randomness".
Or May I ask another question from another direction:
After the unit test pass, can we 100% confirm the return value is actually within the time range? I don't think it was been proofed. We only test the "get_random_time" less than 10 times.
In the logic that I've written, the random number is generated between 0 and the total number of minutes within the provided time range and added to the start time. We can trust that random.randint will only return a number between 0 and the total minutes within the range. This will 100% return a random time between the start and end time.
But if you're asking about how "random" is the random.randint function that ships with Python -- I believe we wouldn't need to test that and we can assume that the randomness provided by the random module in Python is good enough for our purposes.
I was only testing it with a handful of test cases just for sanity. I can add few more test cases if you'd like but I'm not sure how else we can test the `get_random_time` method. I'm open to any suggestions for this.
---
> I prefer to remove "install_cronjob" in the charm's cronjob if it doesn't voliate the intention in this issue.
https:/
Thanks for sharing the bug report. After taking some time to review it and thinking about the overall intention for the charm, I agree in removing the "install_cronjob". I've pushed another commit for this and provided ample reasoning as part of the commit message.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : | # |
PASSED: Continuous integration, rev:f6611640754
https:/
Executed test runs:
SUCCESS: https:/
None: https:/
Click here to trigger a rebuild:
https:/
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Eric Chen (eric-chen) wrote : | # |
> In the logic that I've written, the random number is generated between 0 and the
> total number of minutes within the provided time range and added to the start
> time. We can trust that random.randint will only return a number between 0 and
> the total minutes within the range. This will 100% return a random time between
> the start and end time.
When imeplemnting unit test, we should not have assumption how will the function been implemented. Because the implementation may be changed one day but the unit test should still work.
For example, I can pass current unit test very easily by this implementation
+ def get_random_
+ # Convert start and end times to integers
+ start_hour, start_minute, end_hour, end_minute = [int(t) for t in time_range]
+ return str(start_hour), str(start_minute)+1
Therefore, I don't agree the unit test of "get_random_time" proof the function work here.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Ashley James (dashmage) wrote : | # |
In the latest commit, I've modified the unit test for "get_random_time" so that it executes the function 5 times and captures the output each time in a set. Each output is checked for whether it lies within the provided time range and after that, the set is checked for whether there is atleast 2 distinct elements so we can make sure the "get_random_time" function isn't returning some static output.
True, we can have a better random check with all outputs of the function but that would not be fruitful since we aren't trying to test `random.randint` here but only:
a. The function outputs a time within the provided range
b. The time is random enough for the logrotate charm's purposes
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : | # |
PASSED: Continuous integration, rev:cf6dd13f12f
https:/
Executed test runs:
SUCCESS: https:/
None: https:/
Click here to trigger a rebuild:
https:/
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Eric Chen (eric-chen) wrote (last edit ): | # |
Please check my inline comment.
I also put my thought in summary:
1. unit test of a function should cover the corner case, no matter it is possible or impossible under current program logic. We should clear define the expected behavior under normal situation and abnormal situation.
2. prove "all the possibility values could be generated from the functions" is valuable and the implementation is not too complicated compare to current version
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Ashley James (dashmage) wrote : | # |
> 1. unit test of a function should cover the corner case, no matter it is possible or impossible under current program logic. We should clear define the expected behavior under normal situation and abnormal situation.
> I suggest to add corner cases. What is the expected behavior under error parameters in this function?
1) start_time = end_time
2) start_time < end_time
3) any wrong format of time_range
All the three mentioned "corner cases" or invalid inputs are addressed in the `validate_
But considering the code logic, this validation is already done beforehand and only then does the code execution reach the `get_random_time` method. So I strongly feel that the unit test for `get_random_time` does not need to have test cases for invalid inputs and there are already other unit tests (test_{
The unit tests for `CronHelper.
---
> 2. prove "all the possibility values could be generated from the functions" is valuable and the implementation is not too complicated compare to current version
I have implemented this.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : | # |
PASSED: Continuous integration, rev:187f902d59b
https:/
Executed test runs:
SUCCESS: https:/
None: https:/
Click here to trigger a rebuild:
https:/
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Eric Chen (eric-chen) wrote : | # |
1.
_check_
2.
How long will test_get_
- validate the return value is valid under different start_time / end_time.
- validate all possible datetime would be generated from the fucntion.
3. The corner case is mandatory for each function. We never know how the code logic would be implemented. In ideal situation, the unit test should be finished before writing the function. I mentioned this concept several times. Therefore, we should avoid to decide the unit test case scope based on the logic in real code.
4.
In get_random_time , it’s okay to skip the parameter validation and error handling. (Even though I think it’s better to have error handling). But we still need to know the expected behavior under invalid parameters and add unit tests.
https:/
5.
Thanks for the reminder. Yes, we need the corner case for write_to_crontab and update_
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Ashley James (dashmage) wrote : | # |
1. I've removed _check_
2. `test_get_
3. Alright, I understand the concept. Will implement this.
4. For `get_random_time`, I've added another unit test to check for the invalid parameters as well now.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Eric Chen (eric-chen) wrote (last edit ): | # |
LGTM for the code.
But I just realize these three functions can be staticmethod.
get_random_time
_validate_
_valid_timestamp
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Ashley James (dashmage) wrote (last edit ): | # |
>
> LGTM for the code.
>
> But I just realize these three functions can be staticmethod.
>
> get_random_time
> _validate_
> _valid_timestamp
For some reason, making the methods as staticmethod was breaking many of the unit tests and I wasn't able to find a way to fix it for pytest. I've used classmethods instead. So "self.<method>" is replaced with "self._
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Eric Chen (eric-chen) wrote : | # |
There is no sufficient reason to use classmethod.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
JamesLin (jneo8) wrote : | # |
> For some reason, making the methods as staticmethod was breaking many of the unit tests and I wasn't able to find a way to fix it for pytest. I've used classmethods instead. So "self.<method>" is replaced with "self._
I believe the major use case of classmethod is not for access the other methods in the same class. I mean it's workable but not that suitable. The purpose of get_random_time is very simple and it's make sense to become a static like method. You can try to find a way to resolve this function dependency issue and fix the unit tests.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Ashley James (dashmage) wrote (last edit ): | # |
I've replaced the class methods with static methods now.
The unit tests were breaking because of the monkeypatching of CronHelper done in conftest.py. Instead of monkeypatching the Class, I'm now having it return the Class itself and this will allow for accessing the static methods of the class. The monkeypatching is unnecessary since we want to call the actual methods of the class anyway and if we want to refer to the static methods through the class name, the monkeypatch will not work.
The static methods were breaking while monkeypatching CronHelper because any calls to the static method, for example,
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Eric Chen (eric-chen) wrote : | # |
Please check my inline comment for the validation function
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Ashley James (dashmage) wrote (last edit ): | # |
Latest changes:
- Modified the arguments being used by `_validate_
- The `_validate_
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Eric Chen (eric-chen) wrote : | # |
Overall it's good. Two small things in inline comment
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Ashley James (dashmage) wrote : | # |
Addressed both inline comments!
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : | # |
PASSED: Continuous integration, rev:af917de4e10
https:/
Executed test runs:
SUCCESS: https:/
None: https:/
Click here to trigger a rebuild:
https:/
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote : | # |
Change successfully merged at revision b5d0cd927b51e27
Preview Diff
1 | diff --git a/src/config.yaml b/src/config.yaml |
2 | index 2a14f0b..def9295 100644 |
3 | --- a/src/config.yaml |
4 | +++ b/src/config.yaml |
5 | @@ -29,13 +29,16 @@ options: |
6 | type: string |
7 | default: 'unset' |
8 | description: | |
9 | - If logrotate-cronjob-frequency is set to 'daily' then this value determines whether |
10 | - to update the cron.daily schedule. |
11 | + This value determines the time schedule with which the cron.daily job in |
12 | + /etc/crontab runs. This is done in order to control the execution time for |
13 | + the `/etc/cron.daily/logrotate` job. |
14 | Valid options: |
15 | 'unset': No change to default cron.daily schedule. |
16 | 'set,HOUR:MINUTE': cron.daily schedule will be set to timestamp HOUR:MINUTE (24H) daily. |
17 | 'random,START_HOUR:START_MINUTE,END_HOUR:END_MINUTE': cron.daily schedule will be set |
18 | to some random value between START_HOUR:START_MINUTE,END_HOUR:END_MINUTE (24H) daily. |
19 | + Note that all cron.daily jobs are affected by this config. They still run once a |
20 | + day but according to the time specified here. |
21 | override: |
22 | type: string |
23 | default: '[]' |
24 | diff --git a/src/lib/lib_cron.py b/src/lib/lib_cron.py |
25 | index f2383cc..94352d0 100644 |
26 | --- a/src/lib/lib_cron.py |
27 | +++ b/src/lib/lib_cron.py |
28 | @@ -1,6 +1,8 @@ |
29 | """Cron helper module.""" |
30 | import os |
31 | +import random |
32 | import re |
33 | +from datetime import datetime |
34 | |
35 | from charmhelpers.core import hookenv |
36 | |
37 | @@ -70,8 +72,7 @@ class CronHelper: |
38 | cron_file.write(cron_job) |
39 | os.chmod(cron_file_path, 0o755) |
40 | |
41 | - # update cron.daily schedule if logrotate-cronjob-frequency set to "daily" |
42 | - if self.cronjob_frequency == 1 and self.validate_cron_conf(): |
43 | + if self.validate_cron_daily_schedule_conf(): |
44 | self.update_cron_daily_schedule() |
45 | else: |
46 | self.cleanup_etc_config() |
47 | @@ -108,11 +109,9 @@ class CronHelper: |
48 | |
49 | elif schedule_type == "random": |
50 | cron_daily_start, _, cron_daily_end = schedule_value.partition(",") |
51 | - cron_end_hour, _, cron_end_minute = cron_daily_end.partition(":") |
52 | - cron_start_hour, _, cron_start_minute = cron_daily_start.partition(":") |
53 | - |
54 | - cron_daily_hour = cron_start_hour + "~" + cron_end_hour |
55 | - cron_daily_minute = cron_start_minute + "~" + cron_end_minute |
56 | + cron_daily_hour, cron_daily_minute = CronHelper.get_random_time( |
57 | + cron_daily_start, cron_daily_end |
58 | + ) |
59 | |
60 | elif schedule_type == "unset": |
61 | # Revert to default ubuntu/debian values for daily cron job |
62 | @@ -126,6 +125,31 @@ class CronHelper: |
63 | |
64 | return cron_daily_timestamp |
65 | |
66 | + @staticmethod |
67 | + def get_random_time(start_time, end_time): |
68 | + """Return a random time between the provided time range. |
69 | + |
70 | + start_time and end_time are both timestamps in the format "08:30". |
71 | + A random time between start_time and end_time is returned as |
72 | + two values for the random_hour and random_minute strings. |
73 | + """ |
74 | + # Convert start and end times to integers |
75 | + start_hour, start_minute = [int(t) for t in start_time.split(":")] |
76 | + end_hour, end_minute = [int(t) for t in end_time.split(":")] |
77 | + |
78 | + # Calculate the total number of minutes between the start and end times |
79 | + total_start_minutes = start_hour * 60 + start_minute |
80 | + total_end_minutes = end_hour * 60 + end_minute |
81 | + total_minutes_range = total_end_minutes - total_start_minutes |
82 | + |
83 | + # Generate a random number of minutes within the range |
84 | + random_minutes = random.randint(0, total_minutes_range) |
85 | + |
86 | + # Calculate the hour and minute components of the random time |
87 | + random_hour = (total_start_minutes + random_minutes) // 60 |
88 | + random_minute = (total_start_minutes + random_minutes) % 60 |
89 | + return str(random_hour), str(random_minute) |
90 | + |
91 | def write_to_crontab(self, cron_daily_timestamp): |
92 | """Write daily cronjob with provided timestamp to /etc/crontab.""" |
93 | cron_pattern = re.compile(r".*\/etc\/cron.daily.*") |
94 | @@ -136,7 +160,7 @@ class CronHelper: |
95 | updated_cron_daily = "" |
96 | if cron_daily: |
97 | updated_cron_daily = re.sub( |
98 | - r"\d?\d(~\d\d)?\s\d?\d(~\d\d)?\t", |
99 | + r"\d?\d\s\d?\d\t", |
100 | cron_daily_timestamp + "\t", |
101 | cron_daily[0], |
102 | ) |
103 | @@ -144,8 +168,11 @@ class CronHelper: |
104 | with open(r"/etc/crontab", "w") as crontab: |
105 | crontab.write(updated_data) |
106 | |
107 | - def validate_cron_conf(self): |
108 | - """Block the unit and exit the hook if there is invalid configuration.""" |
109 | + def validate_cron_daily_schedule_conf(self): |
110 | + """Validate configuration for update-cron-daily-schedule. |
111 | + |
112 | + Block the unit and exit the hook in case of invalid configuration. |
113 | + """ |
114 | try: |
115 | conf = self.cron_daily_schedule |
116 | conf = conf.split(",") |
117 | @@ -158,9 +185,9 @@ class CronHelper: |
118 | result = True |
119 | # run additional validation functions |
120 | if operation == "set": |
121 | - result = self._validate_set_schedule(conf) |
122 | + result = CronHelper._validate_set_schedule(conf[1]) |
123 | elif operation == "random": |
124 | - result = self._validate_random_schedule(conf) |
125 | + result = CronHelper._validate_random_schedule(conf[1], conf[2]) |
126 | |
127 | return result |
128 | |
129 | @@ -174,40 +201,58 @@ class CronHelper: |
130 | ) |
131 | raise self.InvalidCronConfig(err) |
132 | |
133 | - def _validate_set_schedule(self, conf): |
134 | - """Validate update-cron-daily-schedule when the "set" keyword exists.""" |
135 | - cron_daily_time = conf[1].split(":") |
136 | - if not self._valid_timestamp(cron_daily_time): |
137 | + @staticmethod |
138 | + def _validate_set_schedule(cron_daily_time): |
139 | + """Validate update-cron-daily-schedule when the "set" keyword exists. |
140 | + |
141 | + cron_daily_time is a time in the format "08:30". |
142 | + """ |
143 | + if not CronHelper._valid_timestamp(cron_daily_time): |
144 | raise ValueError( |
145 | "Invalid value for update-cron-daily-schedule: \ |
146 | {}".format( |
147 | - conf[1] |
148 | + cron_daily_time |
149 | ) |
150 | ) |
151 | else: |
152 | return True |
153 | |
154 | - def _validate_random_schedule(self, conf): |
155 | - """Validate update-cron-daily-schedule when the "random" keyword exists.""" |
156 | - cron_daily_start_time = conf[1].split(":") |
157 | - cron_daily_end_time = conf[2].split(":") |
158 | + @staticmethod |
159 | + def _validate_random_schedule(start_time, end_time): |
160 | + """Validate update-cron-daily-schedule when the "random" keyword exists. |
161 | + |
162 | + start_time and end_time are timestamps in the format "08:30". |
163 | + """ |
164 | if not ( |
165 | - self._valid_timestamp(cron_daily_start_time) |
166 | - and self._valid_timestamp(cron_daily_end_time) |
167 | - and int(cron_daily_start_time[0]) <= int(cron_daily_end_time[0]) |
168 | - and int(cron_daily_start_time[1]) <= int(cron_daily_end_time[1]) |
169 | + CronHelper._valid_timestamp(start_time) |
170 | + and CronHelper._valid_timestamp(end_time) |
171 | ): |
172 | + raise ValueError("Invalid time provided for random schedule.") |
173 | + |
174 | + start_time_datetime = datetime.strptime(start_time, "%H:%M") |
175 | + end_time_datetime = datetime.strptime(end_time, "%H:%M") |
176 | + if start_time_datetime < end_time_datetime: |
177 | + return True |
178 | + elif start_time_datetime == end_time_datetime: |
179 | raise ValueError( |
180 | - "Invalid value for update-cron-daily-schedule: \ |
181 | - {},{}".format( |
182 | - conf[1], conf[2] |
183 | - ) |
184 | + f"Random time range is invalid." |
185 | + f"Start time ({start_time}) and " |
186 | + f"end time ({end_time}) can't be equal." |
187 | ) |
188 | else: |
189 | - return True |
190 | + raise ValueError( |
191 | + f"Random time range is invalid." |
192 | + f"Start time ({start_time}) should not be " |
193 | + f"after end time ({end_time})." |
194 | + ) |
195 | |
196 | - def _valid_timestamp(self, timestamp): |
197 | - """Validate the timestamp.""" |
198 | + @staticmethod |
199 | + def _valid_timestamp(timestamp): |
200 | + """Validate the timestamp. |
201 | + |
202 | + timestamp is in the format "08:30". |
203 | + """ |
204 | + timestamp = timestamp.split(":") |
205 | return int(timestamp[0]) in range(24) and int(timestamp[1]) in range(60) |
206 | |
207 | |
208 | @@ -216,9 +261,7 @@ def main(): |
209 | hookenv.log("Executing cron job.", level=hookenv.INFO) |
210 | hookenv.status_set("maintenance", "Executing cron job.") |
211 | cronhelper = CronHelper() |
212 | - cronhelper.read_config() |
213 | cronhelper.update_logrotate_etc() |
214 | - cronhelper.install_cronjob() |
215 | hookenv.log("Cron job completed.", level=hookenv.INFO) |
216 | hookenv.status_set("active", "Unit is ready.") |
217 | |
218 | diff --git a/src/tests/unit/conftest.py b/src/tests/unit/conftest.py |
219 | index 9eff063..d6bbe2e 100644 |
220 | --- a/src/tests/unit/conftest.py |
221 | +++ b/src/tests/unit/conftest.py |
222 | @@ -80,13 +80,8 @@ def logrotate(tmpdir, mock_hookenv_config, mock_charm_dir, monkeypatch): |
223 | |
224 | |
225 | @pytest.fixture |
226 | -def cron(monkeypatch): |
227 | +def cron(): |
228 | """Cron fixture.""" |
229 | from lib_cron import CronHelper |
230 | |
231 | - helper = CronHelper |
232 | - |
233 | - # Any other functions that load helper will get this version |
234 | - monkeypatch.setattr("lib_cron.CronHelper", lambda: helper) |
235 | - |
236 | - return helper |
237 | + return CronHelper |
238 | diff --git a/src/tests/unit/test_logrotate.py b/src/tests/unit/test_logrotate.py |
239 | index 3af2c13..207661f 100644 |
240 | --- a/src/tests/unit/test_logrotate.py |
241 | +++ b/src/tests/unit/test_logrotate.py |
242 | @@ -233,35 +233,98 @@ class TestCronHelper: |
243 | cron_config.cronjob_frequency = 1 |
244 | cron_config.cron_daily_schedule = "unset" |
245 | |
246 | - assert cron_config.validate_cron_conf() |
247 | + assert cron_config.validate_cron_daily_schedule_conf() |
248 | |
249 | @pytest.mark.parametrize( |
250 | - ("cron_schedule, exp_pattern"), |
251 | + ("cron_schedule, random_hour, random_minute, exp_pattern"), |
252 | [ |
253 | - ("random,06:00,07:50", "00~50 06~07"), |
254 | - ("random,06:00,07:00", "00~00 06~07"), |
255 | - ("random,07:00,07:45", "00~45 07~07"), |
256 | - ("set,08:00", "00 08"), |
257 | - ("unset", "25 6"), |
258 | + ("random,06:00,07:50", "6", "30", "30 6"), |
259 | + ("random,07:00,09:00", "9", "5", "5 9"), |
260 | + ("random,07:10,10:45", "10", "10", "10 10"), |
261 | + ("set,08:00", "0", "0", "00 08"), |
262 | + ("unset", "0", "0", "25 6"), |
263 | ], |
264 | ) |
265 | - def test_cron_daily_schedule(self, cron, cron_schedule, exp_pattern, mocker): |
266 | + def test_update_cron_daily_schedule( |
267 | + self, cron, cron_schedule, random_hour, random_minute, exp_pattern, mocker |
268 | + ): |
269 | """Test the validate and update random cron.daily schedule.""" |
270 | cron_config = cron() |
271 | cron_config.cronjob_enabled = True |
272 | cron_config.cronjob_frequency = 1 |
273 | cron_config.cron_daily_schedule = cron_schedule |
274 | |
275 | - mock_method = mocker.Mock() |
276 | - mocker.patch.object(cron, "write_to_crontab", new=mock_method) |
277 | + mock_write_to_crontab = mocker.Mock() |
278 | + mocker.patch.object(cron, "write_to_crontab", new=mock_write_to_crontab) |
279 | + |
280 | + mock_get_random_time = mocker.Mock() |
281 | + mocker.patch.object(cron, "get_random_time", new=mock_get_random_time) |
282 | + mock_get_random_time.return_value = random_hour, random_minute |
283 | |
284 | updated_cron_daily = cron_config.update_cron_daily_schedule() |
285 | |
286 | - assert cron_config.validate_cron_conf() |
287 | + assert cron_config.validate_cron_daily_schedule_conf() |
288 | assert updated_cron_daily.split("\t")[0] == exp_pattern |
289 | - mock_method.assert_called_once_with(exp_pattern) |
290 | + mock_write_to_crontab.assert_called_once_with(exp_pattern) |
291 | + |
292 | + @pytest.mark.parametrize( |
293 | + ("start_time", "end_time"), |
294 | + [ |
295 | + ("06:30", "09:45"), |
296 | + ("7:42", "12:35"), |
297 | + ("18:10", "21:45"), |
298 | + ("2:8", "4:6"), |
299 | + ("7:30", "7:30"), |
300 | + ("30:25", "32:40"), |
301 | + ], |
302 | + ) |
303 | + def test_get_random_time(self, cron, start_time, end_time): |
304 | + """Test random time setting for update-cron-daily-schedule.""" |
305 | + cron_config = cron() |
306 | |
307 | - @pytest.mark.parametrize("cron_daily_timestamp", ["00 08", "25 6", "00~50 06~07"]) |
308 | + # generate set containing all possible minutes within time_range |
309 | + start_hour, start_minute = [int(t) for t in start_time.split(":")] |
310 | + end_hour, end_minute = [int(t) for t in end_time.split(":")] |
311 | + all_minutes_in_range = set() |
312 | + current_hour = start_hour |
313 | + current_minute = start_minute |
314 | + while current_hour < end_hour or ( |
315 | + current_hour == end_hour and current_minute <= end_minute |
316 | + ): |
317 | + all_minutes_in_range.add(f"{current_hour}:{current_minute}") |
318 | + current_minute += 1 |
319 | + if current_minute == 60: |
320 | + current_minute = 0 |
321 | + current_hour += 1 |
322 | + |
323 | + # run `get_random_time` multiple times, collect output and |
324 | + # see if we're able to obtain all possible minutes in range. |
325 | + random_time_set = set() |
326 | + for _ in range(5000): |
327 | + random_hour, random_minute = cron_config.get_random_time( |
328 | + start_time, end_time |
329 | + ) |
330 | + random_time = f"{random_hour}:{random_minute}" |
331 | + random_time_set.add(random_time) |
332 | + |
333 | + assert all_minutes_in_range == random_time_set |
334 | + |
335 | + @pytest.mark.parametrize( |
336 | + ("start_time", "end_time"), |
337 | + [ |
338 | + ("09:30", "06:45"), # start_time > end_time |
339 | + ("130:45", "18:125"), # invalid time |
340 | + ], |
341 | + ) |
342 | + def test_invalid_get_random_time(self, cron, start_time, end_time): |
343 | + """Test invalid time range arguments for get_random_time.""" |
344 | + cron_config = cron() |
345 | + with pytest.raises(ValueError): |
346 | + cron_config.get_random_time(start_time, end_time) |
347 | + |
348 | + @pytest.mark.parametrize( |
349 | + "cron_daily_timestamp", ["00 08", "25 6", "40 18", "5 7", "20 4", "5 35"] |
350 | + ) |
351 | def test_write_to_crontab(self, cron, cron_daily_timestamp, mocker): |
352 | """Test function that writes updated data to /etc/crontab.""" |
353 | cron_config = cron() |
354 | @@ -297,23 +360,48 @@ class TestCronHelper: |
355 | @pytest.mark.parametrize( |
356 | ("cron_schedule"), |
357 | [ |
358 | + ("random,06:00,07:00"), |
359 | + ("random,7:50,9:30"), |
360 | + ("random,15:00,19:30"), |
361 | + ("random,08:40,09:20"), |
362 | + ("set,8:00"), |
363 | + ("set,12:10"), |
364 | + ("unset"), |
365 | + ], |
366 | + ) |
367 | + def test_valid_cron_daily_schedule(self, cron, cron_schedule): |
368 | + """Test valid configuration for cron.daily schedule.""" |
369 | + cron_config = cron() |
370 | + cron_config.cronjob_enabled = True |
371 | + cron_config.cronjob_frequency = 1 |
372 | + cron_config.cron_daily_schedule = cron_schedule |
373 | + try: |
374 | + cron_config.validate_cron_daily_schedule_conf() |
375 | + except cron_config.InvalidCronConfig: |
376 | + pytest.fail("InvalidCronConfig should not be raised.") |
377 | + |
378 | + @pytest.mark.parametrize( |
379 | + ("cron_schedule"), |
380 | + [ |
381 | ("random,07:00,06:50"), |
382 | ("random,07:50,07:00"), |
383 | ("random,59:50,07:00"), |
384 | ("random,07:00,39:00"), |
385 | + ("random,09:20,08:40"), |
386 | ("set,28:00"), |
387 | ("set,02:80"), |
388 | + ("invalid_setting"), |
389 | ], |
390 | ) |
391 | def test_invalid_cron_daily_schedule(self, cron, cron_schedule): |
392 | - """Test the validate and update random cron.daily schedule.""" |
393 | + """Test invalid configuration for cron.daily schedule.""" |
394 | cron_config = cron() |
395 | cron_config.cronjob_enabled = True |
396 | cron_config.cronjob_frequency = 1 |
397 | cron_config.cron_daily_schedule = cron_schedule |
398 | |
399 | with pytest.raises(cron_config.InvalidCronConfig) as err: |
400 | - cron_config.validate_cron_conf() |
401 | + cron_config.validate_cron_daily_schedule_conf() |
402 | |
403 | assert err.type == cron_config.InvalidCronConfig |
404 | |
405 | @@ -330,6 +418,8 @@ class TestCronHelper: |
406 | mocker.patch("lib_cron.os.getcwd", return_value=mock_charm_dir) |
407 | mock_open = mocker.patch("lib_cron.open") |
408 | mock_handle = mock_open.return_value.__enter__.return_value |
409 | + mock_write_to_crontab = mocker.Mock() |
410 | + mocker.patch.object(cron, "write_to_crontab", new=mock_write_to_crontab) |
411 | expected_files_to_be_removed = [ |
412 | "/etc/cron.hourly/charm-logrotate", |
413 | "/etc/cron.daily/charm-logrotate", |
414 | @@ -340,6 +430,7 @@ class TestCronHelper: |
415 | cron_config = cron() |
416 | cron_config.cronjob_enabled = True |
417 | cron_config.cronjob_frequency = 2 |
418 | + cron_config.cron_daily_schedule = "unset" |
419 | cron_config.install_cronjob() |
420 | |
421 | mock_exists.assert_has_calls( |
I found out that the existing logic [1] to validate the random time is also incorrect.
For example, "update- cron-daily- schedule= random, 08:40,09: 20" would be treated as an invalid entry but it is actually correct since end_time > start_time.
I will be fixing that and pushing it along with this MR since it's also related to the random time functionality.
[1]: https:/ /git.launchpad. net/charm- logrotated/ tree/src/ lib/lib_ cron.py# n198