1# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import datetime, logging
6
7import common
8from autotest_lib.client.common_lib import priorities
9
10import base_event, task
11
12
13class TimedEvent(base_event.BaseEvent):
14    """Base class for events that trigger based on time/day.
15
16    @var _deadline: If this time has passed, ShouldHandle() returns True.
17    """
18
19
20    def __init__(self, keyword, manifest_versions, always_handle, deadline):
21        """Constructor.
22
23        @param keyword: the keyword/name of this event, e.g. nightly.
24        @param manifest_versions: ManifestVersions instance to use for querying.
25        @param always_handle: If True, make ShouldHandle() always return True.
26        @param deadline: This instance's initial |_deadline|.
27        """
28        super(TimedEvent, self).__init__(keyword, manifest_versions,
29                                         always_handle)
30        self._deadline = deadline
31
32
33    def __ne__(self, other):
34        return self._deadline != other._deadline or self.tasks != other.tasks
35
36
37    def __eq__(self, other):
38        return self._deadline == other._deadline and self.tasks == other.tasks
39
40
41    @staticmethod
42    def _now():
43        return datetime.datetime.now()
44
45
46    def Prepare(self):
47        pass
48
49
50    def ShouldHandle(self):
51        """Return True if self._deadline has passed; False if not."""
52        if super(TimedEvent, self).ShouldHandle():
53            return True
54        else:
55            logging.info('Checking deadline %s for event %s',
56                         self._deadline, self.keyword)
57            return self._now() >= self._deadline
58
59
60    def _LatestPerBranchBuildsSince(self, board, days_ago):
61        """Get latest per-branch, per-board builds from last |days_ago| days.
62
63        @param board: the board whose builds we want.
64        @param days_ago: how many days back to look for manifests.
65        @return {branch: [build-name]}
66        """
67        since_date = self._deadline - datetime.timedelta(days=days_ago)
68        all_branch_manifests = self._mv.ManifestsSinceDate(since_date, board)
69        latest_branch_builds = {}
70        for (type, milestone), manifests in all_branch_manifests.iteritems():
71            build = base_event.BuildName(board, type, milestone, manifests[-1])
72            latest_branch_builds[task.PickBranchName(type, milestone)] = [build]
73        logging.info('%s event found candidate builds: %r',
74                     self.keyword, latest_branch_builds)
75        return latest_branch_builds
76
77
78class Nightly(TimedEvent):
79    """A TimedEvent that allows a task to be triggered at every night. Each task
80    can set the hour when it should be triggered, through `hour` setting.
81
82    @var KEYWORD: the keyword to use in a run_on option to associate a task
83                  with the Nightly event.
84    @var _DEFAULT_HOUR: the default hour to trigger the nightly event.
85    """
86
87    KEYWORD = 'nightly'
88    # Each task may have different setting of `hour`. Therefore, nightly tasks
89    # can run at each hour. The default is set to 9PM.
90    _DEFAULT_HOUR = 21
91    PRIORITY = priorities.Priority.DAILY
92    TIMEOUT = 24  # Kicked off once a day, so they get the full day to run
93
94    def __init__(self, manifest_versions, always_handle):
95        """Constructor.
96
97        @param manifest_versions: ManifestVersions instance to use for querying.
98        @param always_handle: If True, make ShouldHandle() always return True.
99        """
100        # Set the deadline to the next even hour.
101        now = self._now()
102        now_hour = datetime.datetime(now.year, now.month, now.day, now.hour)
103        extra_hour = 0 if now == now_hour else 1
104        deadline = now_hour + datetime.timedelta(hours=extra_hour)
105        super(Nightly, self).__init__(self.KEYWORD, manifest_versions,
106                                      always_handle, deadline)
107
108
109    def GetBranchBuildsForBoard(self, board):
110        return self._LatestPerBranchBuildsSince(board, 1)
111
112
113    def UpdateCriteria(self):
114        self._deadline = self._deadline + datetime.timedelta(hours=1)
115
116
117    def FilterTasks(self):
118        """Filter the tasks to only return tasks that should run now.
119
120        Nightly task can run at each hour. This function only return the tasks
121        set to run in current hour.
122
123        @return: A list of tasks that can run now.
124        """
125        current_hour = self._now().hour
126        return [task for task in self.tasks
127                if ((task.hour is not None and task.hour == current_hour) or
128                    (task.hour is None and
129                     current_hour == self._DEFAULT_HOUR))]
130
131
132class Weekly(TimedEvent):
133    """A TimedEvent that allows a task to be triggered at every week. Each task
134    can set the day when it should be triggered, through `day` setting.
135
136    @var KEYWORD: the keyword to use in a run_on option to associate a task
137                  with the Weekly event.
138    @var _DEFAULT_DAY: The default day to run a weekly task.
139    @var _DEFAULT_HOUR: can be overridden in the "weekly_params" config section.
140    """
141
142    KEYWORD = 'weekly'
143    _DEFAULT_DAY = 5  # Saturday
144    _DEFAULT_HOUR = 23
145    PRIORITY = priorities.Priority.WEEKLY
146    TIMEOUT = 7 * 24  # 7 days
147
148
149    @classmethod
150    def _ParseConfig(cls, config):
151        """Create args to pass to __init__ by parsing |config|.
152
153        Calls super class' _ParseConfig() method, then parses these additonal
154        options:
155          hour: Integer hour, on a 24 hour clock.
156        """
157        from_base = super(Weekly, cls)._ParseConfig(config)
158
159        section = base_event.SectionName(cls.KEYWORD)
160        event_time = config.getint(section, 'hour') or cls._DEFAULT_HOUR
161
162        from_base.update({'event_time': event_time})
163        return from_base
164
165
166    def __init__(self, manifest_versions, always_handle, event_time):
167        """Constructor.
168
169        @param manifest_versions: ManifestVersions instance to use for querying.
170        @param always_handle: If True, make ShouldHandle() always return True.
171        @param event_time: The hour of the day to set |self._deadline| at.
172        """
173        # determine if we're past this week's event and set the
174        # next deadline for this suite appropriately.
175        now = self._now()
176        this_week_deadline = datetime.datetime.combine(
177                now, datetime.time(event_time))
178        if this_week_deadline >= now:
179            deadline = this_week_deadline
180        else:
181            deadline = this_week_deadline + datetime.timedelta(days=1)
182        super(Weekly, self).__init__(self.KEYWORD, manifest_versions,
183                                     always_handle, deadline)
184
185
186    def Merge(self, to_merge):
187        """Merge this event with to_merge, changing some mutable properties.
188
189        keyword remains unchanged; the following take on values from to_merge:
190          _deadline iff the time of day in to_merge._deadline is different.
191
192        @param to_merge: A TimedEvent instance to merge into this instance.
193        """
194        super(Weekly, self).Merge(to_merge)
195        if self._deadline.time() != to_merge._deadline.time():
196            self._deadline = to_merge._deadline
197
198
199    def GetBranchBuildsForBoard(self, board):
200        return self._LatestPerBranchBuildsSince(board, 7)
201
202
203    def UpdateCriteria(self):
204        self._deadline = self._deadline + datetime.timedelta(days=1)
205
206
207    def FilterTasks(self):
208        """Filter the tasks to only return tasks that should run now.
209
210        Weekly task can be scheduled at any day of the week. This function only
211        return the tasks set to run in current day.
212
213        @return: A list of tasks that can run now.
214        """
215        current_day = self._now().weekday()
216        return [task for task in self.tasks
217                if ((task.day is not None and task.day == current_day) or
218                    (task.day is None and current_day == self._DEFAULT_DAY))]
219