blob: 542d0b6cb9bdfd75cb1ce21c1d1d82ac9958a09f [file] [log] [blame]
James E. Blairee743612012-05-29 14:49:32 -07001# Copyright 2012 Hewlett-Packard Development Company, L.P.
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may
4# not use this file except in compliance with the License. You may obtain
5# a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations
13# under the License.
14
James E. Blair1b265312014-06-24 09:35:21 -070015import copy
James E. Blairce8a2132016-05-19 15:21:52 -070016import os
James E. Blairee743612012-05-29 14:49:32 -070017import re
James E. Blairce8a2132016-05-19 15:21:52 -070018import struct
James E. Blairff986a12012-05-30 14:56:51 -070019import time
James E. Blair4886cc12012-07-18 15:39:41 -070020from uuid import uuid4
James E. Blair5a9918a2013-08-27 10:06:27 -070021import extras
22
23OrderedDict = extras.try_imports(['collections.OrderedDict',
24 'ordereddict.OrderedDict'])
James E. Blair4886cc12012-07-18 15:39:41 -070025
26
K Jonathan Harkerf95e7232015-04-29 13:33:16 -070027EMPTY_GIT_REF = '0' * 40 # git sha of all zeros, used during creates/deletes
28
James E. Blair19deff22013-08-25 13:17:35 -070029MERGER_MERGE = 1 # "git merge"
30MERGER_MERGE_RESOLVE = 2 # "git merge -s resolve"
31MERGER_CHERRY_PICK = 3 # "git cherry-pick"
32
33MERGER_MAP = {
34 'merge': MERGER_MERGE,
35 'merge-resolve': MERGER_MERGE_RESOLVE,
36 'cherry-pick': MERGER_CHERRY_PICK,
37}
James E. Blairee743612012-05-29 14:49:32 -070038
James E. Blair64ed6f22013-07-10 14:07:23 -070039PRECEDENCE_NORMAL = 0
40PRECEDENCE_LOW = 1
41PRECEDENCE_HIGH = 2
42
43PRECEDENCE_MAP = {
44 None: PRECEDENCE_NORMAL,
45 'low': PRECEDENCE_LOW,
46 'normal': PRECEDENCE_NORMAL,
47 'high': PRECEDENCE_HIGH,
48}
49
James E. Blair1e8dd892012-05-30 09:15:05 -070050
James E. Blairc053d022014-01-22 14:57:33 -080051def time_to_seconds(s):
52 if s.endswith('s'):
53 return int(s[:-1])
54 if s.endswith('m'):
55 return int(s[:-1]) * 60
56 if s.endswith('h'):
57 return int(s[:-1]) * 60 * 60
58 if s.endswith('d'):
59 return int(s[:-1]) * 24 * 60 * 60
60 if s.endswith('w'):
61 return int(s[:-1]) * 7 * 24 * 60 * 60
62 raise Exception("Unable to parse time value: %s" % s)
63
64
James E. Blair11041d22014-05-02 14:49:53 -070065def normalizeCategory(name):
66 name = name.lower()
67 return re.sub(' ', '-', name)
68
69
James E. Blair4aea70c2012-07-26 14:23:24 -070070class Pipeline(object):
71 """A top-level pipeline such as check, gate, post, etc."""
72 def __init__(self, name):
73 self.name = name
James E. Blair8dbd56a2012-12-22 10:55:10 -080074 self.description = None
James E. Blair56370192013-01-14 15:47:28 -080075 self.failure_message = None
Joshua Heskethb7179772014-01-30 23:30:46 +110076 self.merge_failure_message = None
James E. Blair56370192013-01-14 15:47:28 -080077 self.success_message = None
Joshua Hesketh3979e3e2014-03-04 11:21:10 +110078 self.footer_message = None
James E. Blair2fa50962013-01-30 21:50:41 -080079 self.dequeue_on_new_patchset = True
James E. Blair17dd6772015-02-09 14:45:18 -080080 self.ignore_dependencies = False
James E. Blair4aea70c2012-07-26 14:23:24 -070081 self.job_trees = {} # project -> JobTree
82 self.manager = None
James E. Blaire0487072012-08-29 17:38:31 -070083 self.queues = []
James E. Blair64ed6f22013-07-10 14:07:23 -070084 self.precedence = PRECEDENCE_NORMAL
James E. Blairc0dedf82014-08-06 09:37:52 -070085 self.source = None
Joshua Hesketh352264b2015-08-11 23:42:08 +100086 self.start_actions = []
87 self.success_actions = []
88 self.failure_actions = []
89 self.merge_failure_actions = []
90 self.disabled_actions = []
Joshua Hesketh89e829d2015-02-10 16:29:45 +110091 self.disable_at = None
92 self._consecutive_failures = 0
93 self._disabled = False
Clark Boylan7603a372014-01-21 11:43:20 -080094 self.window = None
95 self.window_floor = None
96 self.window_increase_type = None
97 self.window_increase_factor = None
98 self.window_decrease_type = None
99 self.window_decrease_factor = None
James E. Blair4aea70c2012-07-26 14:23:24 -0700100
James E. Blaird09c17a2012-08-07 09:23:14 -0700101 def __repr__(self):
102 return '<Pipeline %s>' % self.name
103
James E. Blair4aea70c2012-07-26 14:23:24 -0700104 def setManager(self, manager):
105 self.manager = manager
106
107 def addProject(self, project):
108 job_tree = JobTree(None) # Null job == job tree root
109 self.job_trees[project] = job_tree
110 return job_tree
111
112 def getProjects(self):
Monty Taylor74fa3862016-06-02 07:39:49 +0300113 # cmp is not in python3, applied idiom from
114 # http://python-future.org/compatible_idioms.html#cmp
115 return sorted(
116 self.job_trees.keys(),
117 key=lambda p: p.name)
James E. Blair4aea70c2012-07-26 14:23:24 -0700118
James E. Blaire0487072012-08-29 17:38:31 -0700119 def addQueue(self, queue):
120 self.queues.append(queue)
121
122 def getQueue(self, project):
123 for queue in self.queues:
124 if project in queue.projects:
125 return queue
126 return None
127
James E. Blairbfb8e042014-12-30 17:01:44 -0800128 def removeQueue(self, queue):
129 self.queues.remove(queue)
130
James E. Blair4aea70c2012-07-26 14:23:24 -0700131 def getJobTree(self, project):
132 tree = self.job_trees.get(project)
133 return tree
134
James E. Blair107c3852015-02-07 08:23:10 -0800135 def getJobs(self, item):
136 if not item.live:
137 return []
138 tree = self.getJobTree(item.change.project)
James E. Blair4aea70c2012-07-26 14:23:24 -0700139 if not tree:
140 return []
James E. Blair107c3852015-02-07 08:23:10 -0800141 return item.change.filterJobs(tree.getJobs())
James E. Blair4aea70c2012-07-26 14:23:24 -0700142
James E. Blairaf17a972016-02-03 15:07:18 -0800143 def _findJobsToRun(self, job_trees, item, mutex):
James E. Blair4aea70c2012-07-26 14:23:24 -0700144 torun = []
James E. Blairfee8d652013-06-07 08:57:52 -0700145 if item.item_ahead:
James E. Blair4aea70c2012-07-26 14:23:24 -0700146 # Only run jobs if any 'hold' jobs on the change ahead
147 # have completed successfully.
James E. Blairfee8d652013-06-07 08:57:52 -0700148 if self.isHoldingFollowingChanges(item.item_ahead):
James E. Blair4aea70c2012-07-26 14:23:24 -0700149 return []
150 for tree in job_trees:
151 job = tree.job
152 result = None
153 if job:
James E. Blairfee8d652013-06-07 08:57:52 -0700154 if not job.changeMatches(item.change):
James E. Blair4aea70c2012-07-26 14:23:24 -0700155 continue
James E. Blairfee8d652013-06-07 08:57:52 -0700156 build = item.current_build_set.getBuild(job.name)
James E. Blair4aea70c2012-07-26 14:23:24 -0700157 if build:
158 result = build.result
159 else:
160 # There is no build for the root of this job tree,
161 # so we should run it.
James E. Blairaf17a972016-02-03 15:07:18 -0800162 if mutex.acquire(item, job):
163 # If this job needs a mutex, either acquire it or make
164 # sure that we have it before running the job.
165 torun.append(job)
James E. Blair4aea70c2012-07-26 14:23:24 -0700166 # If there is no job, this is a null job tree, and we should
167 # run all of its jobs.
168 if result == 'SUCCESS' or not job:
James E. Blairaf17a972016-02-03 15:07:18 -0800169 torun.extend(self._findJobsToRun(tree.job_trees, item, mutex))
James E. Blair4aea70c2012-07-26 14:23:24 -0700170 return torun
171
James E. Blairaf17a972016-02-03 15:07:18 -0800172 def findJobsToRun(self, item, mutex):
James E. Blairbfb8e042014-12-30 17:01:44 -0800173 if not item.live:
174 return []
James E. Blairfee8d652013-06-07 08:57:52 -0700175 tree = self.getJobTree(item.change.project)
James E. Blair4aea70c2012-07-26 14:23:24 -0700176 if not tree:
177 return []
James E. Blairaf17a972016-02-03 15:07:18 -0800178 return self._findJobsToRun(tree.job_trees, item, mutex)
James E. Blair4aea70c2012-07-26 14:23:24 -0700179
James E. Blairbea9ef12013-07-15 11:52:23 -0700180 def haveAllJobsStarted(self, item):
James E. Blair107c3852015-02-07 08:23:10 -0800181 for job in self.getJobs(item):
James E. Blairbea9ef12013-07-15 11:52:23 -0700182 build = item.current_build_set.getBuild(job.name)
183 if not build or not build.start_time:
184 return False
185 return True
186
James E. Blairfee8d652013-06-07 08:57:52 -0700187 def areAllJobsComplete(self, item):
James E. Blair107c3852015-02-07 08:23:10 -0800188 for job in self.getJobs(item):
James E. Blairfee8d652013-06-07 08:57:52 -0700189 build = item.current_build_set.getBuild(job.name)
James E. Blair4aea70c2012-07-26 14:23:24 -0700190 if not build or not build.result:
191 return False
192 return True
193
James E. Blairfee8d652013-06-07 08:57:52 -0700194 def didAllJobsSucceed(self, item):
James E. Blair107c3852015-02-07 08:23:10 -0800195 for job in self.getJobs(item):
James E. Blair4ec821f2012-08-23 15:28:28 -0700196 if not job.voting:
197 continue
James E. Blairfee8d652013-06-07 08:57:52 -0700198 build = item.current_build_set.getBuild(job.name)
James E. Blair4aea70c2012-07-26 14:23:24 -0700199 if not build:
200 return False
201 if build.result != 'SUCCESS':
202 return False
203 return True
204
Joshua Heskethb7179772014-01-30 23:30:46 +1100205 def didMergerSucceed(self, item):
206 if item.current_build_set.unable_to_merge:
207 return False
208 return True
209
James E. Blairfee8d652013-06-07 08:57:52 -0700210 def didAnyJobFail(self, item):
James E. Blair107c3852015-02-07 08:23:10 -0800211 for job in self.getJobs(item):
James E. Blair4ec821f2012-08-23 15:28:28 -0700212 if not job.voting:
213 continue
James E. Blairfee8d652013-06-07 08:57:52 -0700214 build = item.current_build_set.getBuild(job.name)
James E. Blair0018a6c2013-02-27 14:11:45 -0800215 if build and build.result and (build.result != 'SUCCESS'):
James E. Blair4aea70c2012-07-26 14:23:24 -0700216 return True
217 return False
218
James E. Blairfee8d652013-06-07 08:57:52 -0700219 def isHoldingFollowingChanges(self, item):
James E. Blairbfb8e042014-12-30 17:01:44 -0800220 if not item.live:
221 return False
James E. Blair107c3852015-02-07 08:23:10 -0800222 for job in self.getJobs(item):
James E. Blair4aea70c2012-07-26 14:23:24 -0700223 if not job.hold_following_changes:
224 continue
James E. Blairfee8d652013-06-07 08:57:52 -0700225 build = item.current_build_set.getBuild(job.name)
James E. Blair4aea70c2012-07-26 14:23:24 -0700226 if not build:
227 return True
228 if build.result != 'SUCCESS':
229 return True
James E. Blair972e3c72013-08-29 12:04:55 -0700230
James E. Blairfee8d652013-06-07 08:57:52 -0700231 if not item.item_ahead:
James E. Blair4aea70c2012-07-26 14:23:24 -0700232 return False
James E. Blairfee8d652013-06-07 08:57:52 -0700233 return self.isHoldingFollowingChanges(item.item_ahead)
James E. Blair4aea70c2012-07-26 14:23:24 -0700234
James E. Blairfee8d652013-06-07 08:57:52 -0700235 def setResult(self, item, build):
James E. Blair4a28a882013-08-23 15:17:33 -0700236 if build.retry:
237 item.removeBuild(build)
238 elif build.result != 'SUCCESS':
James E. Blair4aea70c2012-07-26 14:23:24 -0700239 # Get a JobTree from a Job so we can find only its dependent jobs
James E. Blairfee8d652013-06-07 08:57:52 -0700240 root = self.getJobTree(item.change.project)
James E. Blair4aea70c2012-07-26 14:23:24 -0700241 tree = root.getJobTreeForJob(build.job)
242 for job in tree.getJobs():
243 fakebuild = Build(job, None)
244 fakebuild.result = 'SKIPPED'
James E. Blairfee8d652013-06-07 08:57:52 -0700245 item.addBuild(fakebuild)
James E. Blair4aea70c2012-07-26 14:23:24 -0700246
Joshua Heskethb7179772014-01-30 23:30:46 +1100247 def setUnableToMerge(self, item):
James E. Blairfee8d652013-06-07 08:57:52 -0700248 item.current_build_set.unable_to_merge = True
249 root = self.getJobTree(item.change.project)
James E. Blair973721f2012-08-15 10:19:43 -0700250 for job in root.getJobs():
251 fakebuild = Build(job, None)
252 fakebuild.result = 'SKIPPED'
James E. Blairfee8d652013-06-07 08:57:52 -0700253 item.addBuild(fakebuild)
James E. Blair973721f2012-08-15 10:19:43 -0700254
James E. Blairfee8d652013-06-07 08:57:52 -0700255 def setDequeuedNeedingChange(self, item):
256 item.dequeued_needing_change = True
257 root = self.getJobTree(item.change.project)
James E. Blaircaec0c52012-08-22 14:52:22 -0700258 for job in root.getJobs():
259 fakebuild = Build(job, None)
260 fakebuild.result = 'SKIPPED'
James E. Blairfee8d652013-06-07 08:57:52 -0700261 item.addBuild(fakebuild)
James E. Blaircaec0c52012-08-22 14:52:22 -0700262
James E. Blaire0487072012-08-29 17:38:31 -0700263 def getChangesInQueue(self):
264 changes = []
265 for shared_queue in self.queues:
James E. Blairfee8d652013-06-07 08:57:52 -0700266 changes.extend([x.change for x in shared_queue.queue])
James E. Blaire0487072012-08-29 17:38:31 -0700267 return changes
268
James E. Blairfee8d652013-06-07 08:57:52 -0700269 def getAllItems(self):
270 items = []
James E. Blaire0487072012-08-29 17:38:31 -0700271 for shared_queue in self.queues:
James E. Blairfee8d652013-06-07 08:57:52 -0700272 items.extend(shared_queue.queue)
James E. Blairfee8d652013-06-07 08:57:52 -0700273 return items
James E. Blaire0487072012-08-29 17:38:31 -0700274
James E. Blairb7273ef2016-04-19 08:58:51 -0700275 def formatStatusJSON(self, url_pattern=None):
James E. Blair8dbd56a2012-12-22 10:55:10 -0800276 j_pipeline = dict(name=self.name,
277 description=self.description)
278 j_queues = []
279 j_pipeline['change_queues'] = j_queues
280 for queue in self.queues:
281 j_queue = dict(name=queue.name)
282 j_queues.append(j_queue)
283 j_queue['heads'] = []
Clark Boylanaf2476f2014-01-23 14:47:36 -0800284 j_queue['window'] = queue.window
James E. Blair972e3c72013-08-29 12:04:55 -0700285
286 j_changes = []
287 for e in queue.queue:
288 if not e.item_ahead:
289 if j_changes:
290 j_queue['heads'].append(j_changes)
291 j_changes = []
James E. Blairb7273ef2016-04-19 08:58:51 -0700292 j_changes.append(e.formatJSON(url_pattern))
James E. Blair972e3c72013-08-29 12:04:55 -0700293 if (len(j_changes) > 1 and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000294 (j_changes[-2]['remaining_time'] is not None) and
295 (j_changes[-1]['remaining_time'] is not None)):
James E. Blair972e3c72013-08-29 12:04:55 -0700296 j_changes[-1]['remaining_time'] = max(
297 j_changes[-2]['remaining_time'],
298 j_changes[-1]['remaining_time'])
299 if j_changes:
James E. Blair8dbd56a2012-12-22 10:55:10 -0800300 j_queue['heads'].append(j_changes)
301 return j_pipeline
302
James E. Blair4aea70c2012-07-26 14:23:24 -0700303
James E. Blairee743612012-05-29 14:49:32 -0700304class ChangeQueue(object):
James E. Blair4aea70c2012-07-26 14:23:24 -0700305 """DependentPipelines have multiple parallel queues shared by
306 different projects; this is one of them. For instance, there may
307 a queue shared by interrelated projects foo and bar, and a second
308 queue for independent project baz. Pipelines have one or more
James E. Blairbfb8e042014-12-30 17:01:44 -0800309 ChangeQueues."""
310 def __init__(self, pipeline, window=0, window_floor=1,
Clark Boylan7603a372014-01-21 11:43:20 -0800311 window_increase_type='linear', window_increase_factor=1,
312 window_decrease_type='exponential', window_decrease_factor=2):
James E. Blair4aea70c2012-07-26 14:23:24 -0700313 self.pipeline = pipeline
James E. Blairee743612012-05-29 14:49:32 -0700314 self.name = ''
James E. Blairc8a1e052014-02-25 09:29:26 -0800315 self.assigned_name = None
316 self.generated_name = None
James E. Blairee743612012-05-29 14:49:32 -0700317 self.projects = []
318 self._jobs = set()
319 self.queue = []
Clark Boylan7603a372014-01-21 11:43:20 -0800320 self.window = window
321 self.window_floor = window_floor
322 self.window_increase_type = window_increase_type
323 self.window_increase_factor = window_increase_factor
324 self.window_decrease_type = window_decrease_type
325 self.window_decrease_factor = window_decrease_factor
James E. Blairee743612012-05-29 14:49:32 -0700326
James E. Blair9f9667e2012-06-12 17:51:08 -0700327 def __repr__(self):
James E. Blair4aea70c2012-07-26 14:23:24 -0700328 return '<ChangeQueue %s: %s>' % (self.pipeline.name, self.name)
James E. Blairee743612012-05-29 14:49:32 -0700329
330 def getJobs(self):
331 return self._jobs
332
333 def addProject(self, project):
334 if project not in self.projects:
335 self.projects.append(project)
James E. Blairc8a1e052014-02-25 09:29:26 -0800336 self._jobs |= set(self.pipeline.getJobTree(project).getJobs())
337
James E. Blairee743612012-05-29 14:49:32 -0700338 names = [x.name for x in self.projects]
339 names.sort()
James E. Blairc8a1e052014-02-25 09:29:26 -0800340 self.generated_name = ', '.join(names)
341
342 for job in self._jobs:
343 if job.queue_name:
344 if (self.assigned_name and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000345 job.queue_name != self.assigned_name):
James E. Blairc8a1e052014-02-25 09:29:26 -0800346 raise Exception("More than one name assigned to "
347 "change queue: %s != %s" %
348 (self.assigned_name, job.queue_name))
349 self.assigned_name = job.queue_name
350 self.name = self.assigned_name or self.generated_name
James E. Blairee743612012-05-29 14:49:32 -0700351
352 def enqueueChange(self, change):
James E. Blairbfb8e042014-12-30 17:01:44 -0800353 item = QueueItem(self, change)
James E. Blaircdccd972013-07-01 12:10:22 -0700354 self.enqueueItem(item)
355 item.enqueue_time = time.time()
356 return item
357
358 def enqueueItem(self, item):
James E. Blair4a035d92014-01-23 13:10:48 -0800359 item.pipeline = self.pipeline
James E. Blairbfb8e042014-12-30 17:01:44 -0800360 item.queue = self
361 if self.queue:
James E. Blairfee8d652013-06-07 08:57:52 -0700362 item.item_ahead = self.queue[-1]
James E. Blair972e3c72013-08-29 12:04:55 -0700363 item.item_ahead.items_behind.append(item)
James E. Blairfee8d652013-06-07 08:57:52 -0700364 self.queue.append(item)
James E. Blairee743612012-05-29 14:49:32 -0700365
James E. Blairfee8d652013-06-07 08:57:52 -0700366 def dequeueItem(self, item):
367 if item in self.queue:
368 self.queue.remove(item)
James E. Blairfee8d652013-06-07 08:57:52 -0700369 if item.item_ahead:
James E. Blair972e3c72013-08-29 12:04:55 -0700370 item.item_ahead.items_behind.remove(item)
371 for item_behind in item.items_behind:
372 if item.item_ahead:
373 item.item_ahead.items_behind.append(item_behind)
374 item_behind.item_ahead = item.item_ahead
James E. Blairfee8d652013-06-07 08:57:52 -0700375 item.item_ahead = None
James E. Blair972e3c72013-08-29 12:04:55 -0700376 item.items_behind = []
James E. Blairfee8d652013-06-07 08:57:52 -0700377 item.dequeue_time = time.time()
James E. Blaire0487072012-08-29 17:38:31 -0700378
James E. Blair972e3c72013-08-29 12:04:55 -0700379 def moveItem(self, item, item_ahead):
James E. Blair972e3c72013-08-29 12:04:55 -0700380 if item.item_ahead == item_ahead:
381 return False
382 # Remove from current location
383 if item.item_ahead:
384 item.item_ahead.items_behind.remove(item)
385 for item_behind in item.items_behind:
386 if item.item_ahead:
387 item.item_ahead.items_behind.append(item_behind)
388 item_behind.item_ahead = item.item_ahead
389 # Add to new location
390 item.item_ahead = item_ahead
James E. Blair00451262013-09-20 11:40:17 -0700391 item.items_behind = []
James E. Blair972e3c72013-08-29 12:04:55 -0700392 if item.item_ahead:
393 item.item_ahead.items_behind.append(item)
394 return True
James E. Blairee743612012-05-29 14:49:32 -0700395
396 def mergeChangeQueue(self, other):
397 for project in other.projects:
398 self.addProject(project)
Clark Boylan7603a372014-01-21 11:43:20 -0800399 self.window = min(self.window, other.window)
400 # TODO merge semantics
401
Clark Boylan3d2f7a72014-01-23 11:07:42 -0800402 def isActionable(self, item):
James E. Blairbfb8e042014-12-30 17:01:44 -0800403 if self.window:
Clark Boylan3d2f7a72014-01-23 11:07:42 -0800404 return item in self.queue[:self.window]
Clark Boylan7603a372014-01-21 11:43:20 -0800405 else:
Clark Boylan3d2f7a72014-01-23 11:07:42 -0800406 return True
Clark Boylan7603a372014-01-21 11:43:20 -0800407
408 def increaseWindowSize(self):
James E. Blairbfb8e042014-12-30 17:01:44 -0800409 if self.window:
Clark Boylan7603a372014-01-21 11:43:20 -0800410 if self.window_increase_type == 'linear':
411 self.window += self.window_increase_factor
412 elif self.window_increase_type == 'exponential':
413 self.window *= self.window_increase_factor
414
415 def decreaseWindowSize(self):
James E. Blairbfb8e042014-12-30 17:01:44 -0800416 if self.window:
Clark Boylan7603a372014-01-21 11:43:20 -0800417 if self.window_decrease_type == 'linear':
418 self.window = max(
419 self.window_floor,
420 self.window - self.window_decrease_factor)
421 elif self.window_decrease_type == 'exponential':
422 self.window = max(
423 self.window_floor,
424 self.window / self.window_decrease_factor)
James E. Blairee743612012-05-29 14:49:32 -0700425
James E. Blair1e8dd892012-05-30 09:15:05 -0700426
James E. Blair4aea70c2012-07-26 14:23:24 -0700427class Project(object):
Evgeny Antyshev0deaaad2015-08-03 20:22:56 +0000428 def __init__(self, name, foreign=False):
James E. Blair4aea70c2012-07-26 14:23:24 -0700429 self.name = name
James E. Blair19deff22013-08-25 13:17:35 -0700430 self.merge_mode = MERGER_MERGE_RESOLVE
Evgeny Antyshev0deaaad2015-08-03 20:22:56 +0000431 # foreign projects are those referenced in dependencies
432 # of layout projects, this should matter
433 # when deciding whether to enqueue their changes
434 self.foreign = foreign
James E. Blair4aea70c2012-07-26 14:23:24 -0700435
436 def __str__(self):
437 return self.name
438
439 def __repr__(self):
440 return '<Project %s>' % (self.name)
441
442
James E. Blairee743612012-05-29 14:49:32 -0700443class Job(object):
444 def __init__(self, name):
James E. Blair222d4982012-07-16 09:31:19 -0700445 # If you add attributes here, be sure to add them to the copy method.
James E. Blairee743612012-05-29 14:49:32 -0700446 self.name = name
James E. Blairc8a1e052014-02-25 09:29:26 -0800447 self.queue_name = None
James E. Blairee743612012-05-29 14:49:32 -0700448 self.failure_message = None
449 self.success_message = None
James E. Blair6aea36d2012-12-17 13:03:24 -0800450 self.failure_pattern = None
451 self.success_pattern = None
James E. Blaire5a847f2012-07-10 15:29:14 -0700452 self.parameter_function = None
James E. Blair456f2fb2016-02-09 09:29:33 -0800453 self.tags = set()
James E. Blairaf17a972016-02-03 15:07:18 -0800454 self.mutex = None
Maru Newby79427a42015-02-17 17:54:45 +0000455 # A metajob should only supply values for attributes that have
456 # been explicitly provided, so avoid setting boolean defaults.
457 if self.is_metajob:
458 self.hold_following_changes = None
459 self.voting = None
460 else:
461 self.hold_following_changes = False
462 self.voting = True
James E. Blaire421a232012-07-25 16:59:21 -0700463 self.branches = []
464 self._branches = []
James E. Blair70c71582013-03-06 08:50:50 -0800465 self.files = []
466 self._files = []
Maru Newby3fe5f852015-01-13 04:22:14 +0000467 self.skip_if_matcher = None
Joshua Hesketh36c3fa52014-01-22 11:40:52 +1100468 self.swift = {}
James E. Blairee743612012-05-29 14:49:32 -0700469
470 def __str__(self):
471 return self.name
472
473 def __repr__(self):
474 return '<Job %s>' % (self.name)
475
Maru Newby79427a42015-02-17 17:54:45 +0000476 @property
477 def is_metajob(self):
478 return self.name.startswith('^')
479
James E. Blairb0954652012-06-01 11:32:01 -0700480 def copy(self, other):
James E. Blairc28d1b02013-07-19 11:37:06 -0700481 if other.failure_message:
482 self.failure_message = other.failure_message
483 if other.success_message:
484 self.success_message = other.success_message
485 if other.failure_pattern:
486 self.failure_pattern = other.failure_pattern
487 if other.success_pattern:
488 self.success_pattern = other.success_pattern
489 if other.parameter_function:
490 self.parameter_function = other.parameter_function
491 if other.branches:
492 self.branches = other.branches[:]
493 self._branches = other._branches[:]
494 if other.files:
495 self.files = other.files[:]
496 self._files = other._files[:]
Maru Newby3fe5f852015-01-13 04:22:14 +0000497 if other.skip_if_matcher:
498 self.skip_if_matcher = other.skip_if_matcher.copy()
Joshua Hesketh36c3fa52014-01-22 11:40:52 +1100499 if other.swift:
500 self.swift.update(other.swift)
James E. Blairaf17a972016-02-03 15:07:18 -0800501 if other.mutex:
502 self.mutex = other.mutex
James E. Blair456f2fb2016-02-09 09:29:33 -0800503 # Tags are merged via a union rather than a destructive copy
504 # because they are intended to accumulate as metajobs are
505 # applied.
506 if other.tags:
507 self.tags = self.tags.union(other.tags)
Maru Newby79427a42015-02-17 17:54:45 +0000508 # Only non-None values should be copied for boolean attributes.
509 if other.hold_following_changes is not None:
510 self.hold_following_changes = other.hold_following_changes
511 if other.voting is not None:
512 self.voting = other.voting
James E. Blairb0954652012-06-01 11:32:01 -0700513
James E. Blaire421a232012-07-25 16:59:21 -0700514 def changeMatches(self, change):
James E. Blair70c71582013-03-06 08:50:50 -0800515 matches_branch = False
James E. Blaire421a232012-07-25 16:59:21 -0700516 for branch in self.branches:
James E. Blair45865f32012-10-05 09:39:46 -0700517 if hasattr(change, 'branch') and branch.match(change.branch):
James E. Blair70c71582013-03-06 08:50:50 -0800518 matches_branch = True
James E. Blair45865f32012-10-05 09:39:46 -0700519 if hasattr(change, 'ref') and branch.match(change.ref):
James E. Blair70c71582013-03-06 08:50:50 -0800520 matches_branch = True
521 if self.branches and not matches_branch:
522 return False
523
524 matches_file = False
525 for f in self.files:
526 if hasattr(change, 'files'):
527 for cf in change.files:
528 if f.match(cf):
529 matches_file = True
530 if self.files and not matches_file:
531 return False
532
Maru Newby3fe5f852015-01-13 04:22:14 +0000533 if self.skip_if_matcher and self.skip_if_matcher.matches(change):
534 return False
535
James E. Blair70c71582013-03-06 08:50:50 -0800536 return True
James E. Blaire5a847f2012-07-10 15:29:14 -0700537
James E. Blair1e8dd892012-05-30 09:15:05 -0700538
James E. Blairee743612012-05-29 14:49:32 -0700539class JobTree(object):
540 """ A JobTree represents an instance of one Job, and holds JobTrees
541 whose jobs should be run if that Job succeeds. A root node of a
542 JobTree will have no associated Job. """
543
544 def __init__(self, job):
545 self.job = job
546 self.job_trees = []
547
548 def addJob(self, job):
James E. Blair12a92b12014-03-26 11:54:53 -0700549 if job not in [x.job for x in self.job_trees]:
550 t = JobTree(job)
551 self.job_trees.append(t)
552 return t
James E. Blaire4ad55a2015-06-11 08:22:43 -0700553 for tree in self.job_trees:
554 if tree.job == job:
555 return tree
James E. Blairee743612012-05-29 14:49:32 -0700556
557 def getJobs(self):
558 jobs = []
559 for x in self.job_trees:
560 jobs.append(x.job)
561 jobs.extend(x.getJobs())
562 return jobs
563
564 def getJobTreeForJob(self, job):
565 if self.job == job:
566 return self
567 for tree in self.job_trees:
568 ret = tree.getJobTreeForJob(job)
569 if ret:
570 return ret
571 return None
572
James E. Blair1e8dd892012-05-30 09:15:05 -0700573
James E. Blair4aea70c2012-07-26 14:23:24 -0700574class Build(object):
575 def __init__(self, job, uuid):
576 self.job = job
577 self.uuid = uuid
James E. Blair4aea70c2012-07-26 14:23:24 -0700578 self.url = None
579 self.number = None
580 self.result = None
581 self.build_set = None
582 self.launch_time = time.time()
James E. Blair71e94122012-12-24 17:53:08 -0800583 self.start_time = None
584 self.end_time = None
James E. Blairbea9ef12013-07-15 11:52:23 -0700585 self.estimated_time = None
James E. Blair66eeebf2013-07-27 17:44:32 -0700586 self.pipeline = None
James E. Blair0aac4872013-08-23 14:02:38 -0700587 self.canceled = False
James E. Blair4a28a882013-08-23 15:17:33 -0700588 self.retry = False
James E. Blaird78576a2013-07-09 10:39:17 -0700589 self.parameters = {}
Joshua Heskethba8776a2014-01-12 14:35:40 +0800590 self.worker = Worker()
Timothy Chavezb2332082015-08-07 20:08:04 -0500591 self.node_labels = []
592 self.node_name = None
James E. Blairee743612012-05-29 14:49:32 -0700593
594 def __repr__(self):
Joshua Heskethba8776a2014-01-12 14:35:40 +0800595 return ('<Build %s of %s on %s>' %
596 (self.uuid, self.job.name, self.worker))
597
598
599class Worker(object):
600 """A model of the worker running a job"""
601 def __init__(self):
602 self.name = "Unknown"
603 self.hostname = None
604 self.ips = []
605 self.fqdn = None
606 self.program = None
607 self.version = None
608 self.extra = {}
609
610 def updateFromData(self, data):
611 """Update worker information if contained in the WORK_DATA response."""
612 self.name = data.get('worker_name', self.name)
613 self.hostname = data.get('worker_hostname', self.hostname)
614 self.ips = data.get('worker_ips', self.ips)
615 self.fqdn = data.get('worker_fqdn', self.fqdn)
616 self.program = data.get('worker_program', self.program)
617 self.version = data.get('worker_version', self.version)
618 self.extra = data.get('worker_extra', self.extra)
619
620 def __repr__(self):
621 return '<Worker %s>' % self.name
James E. Blairee743612012-05-29 14:49:32 -0700622
James E. Blair1e8dd892012-05-30 09:15:05 -0700623
James E. Blair7e530ad2012-07-03 16:12:28 -0700624class BuildSet(object):
James E. Blair4076e2b2014-01-28 12:42:20 -0800625 # Merge states:
626 NEW = 1
627 PENDING = 2
628 COMPLETE = 3
629
Antoine Musso9b229282014-08-18 23:45:43 +0200630 states_map = {
631 1: 'NEW',
632 2: 'PENDING',
633 3: 'COMPLETE',
634 }
635
James E. Blairfee8d652013-06-07 08:57:52 -0700636 def __init__(self, item):
637 self.item = item
James E. Blair11700c32012-07-05 17:50:05 -0700638 self.other_changes = []
James E. Blair7e530ad2012-07-03 16:12:28 -0700639 self.builds = {}
James E. Blair11700c32012-07-05 17:50:05 -0700640 self.result = None
641 self.next_build_set = None
642 self.previous_build_set = None
James E. Blair4886cc12012-07-18 15:39:41 -0700643 self.ref = None
James E. Blair81515ad2012-10-01 18:29:08 -0700644 self.commit = None
James E. Blair4076e2b2014-01-28 12:42:20 -0800645 self.zuul_url = None
James E. Blair973721f2012-08-15 10:19:43 -0700646 self.unable_to_merge = False
James E. Blair972e3c72013-08-29 12:04:55 -0700647 self.failing_reasons = []
James E. Blair4076e2b2014-01-28 12:42:20 -0800648 self.merge_state = self.NEW
James E. Blair7e530ad2012-07-03 16:12:28 -0700649
Antoine Musso9b229282014-08-18 23:45:43 +0200650 def __repr__(self):
651 return '<BuildSet item: %s #builds: %s merge state: %s>' % (
652 self.item,
653 len(self.builds),
654 self.getStateName(self.merge_state))
655
James E. Blair4886cc12012-07-18 15:39:41 -0700656 def setConfiguration(self):
James E. Blair11700c32012-07-05 17:50:05 -0700657 # The change isn't enqueued until after it's created
658 # so we don't know what the other changes ahead will be
659 # until jobs start.
660 if not self.other_changes:
James E. Blairfee8d652013-06-07 08:57:52 -0700661 next_item = self.item.item_ahead
662 while next_item:
663 self.other_changes.append(next_item.change)
664 next_item = next_item.item_ahead
James E. Blair4886cc12012-07-18 15:39:41 -0700665 if not self.ref:
666 self.ref = 'Z' + uuid4().hex
667
Antoine Musso9b229282014-08-18 23:45:43 +0200668 def getStateName(self, state_num):
669 return self.states_map.get(
670 state_num, 'UNKNOWN (%s)' % state_num)
671
James E. Blair4886cc12012-07-18 15:39:41 -0700672 def addBuild(self, build):
673 self.builds[build.job.name] = build
674 build.build_set = self
James E. Blair11700c32012-07-05 17:50:05 -0700675
James E. Blair4a28a882013-08-23 15:17:33 -0700676 def removeBuild(self, build):
677 del self.builds[build.job.name]
678
James E. Blair7e530ad2012-07-03 16:12:28 -0700679 def getBuild(self, job_name):
680 return self.builds.get(job_name)
681
James E. Blair11700c32012-07-05 17:50:05 -0700682 def getBuilds(self):
683 keys = self.builds.keys()
684 keys.sort()
685 return [self.builds.get(x) for x in keys]
686
James E. Blair7e530ad2012-07-03 16:12:28 -0700687
James E. Blairfee8d652013-06-07 08:57:52 -0700688class QueueItem(object):
689 """A changish inside of a Pipeline queue"""
James E. Blair32663402012-06-01 10:04:18 -0700690
James E. Blairbfb8e042014-12-30 17:01:44 -0800691 def __init__(self, queue, change):
692 self.pipeline = queue.pipeline
693 self.queue = queue
James E. Blairfee8d652013-06-07 08:57:52 -0700694 self.change = change # a changeish
James E. Blair7e530ad2012-07-03 16:12:28 -0700695 self.build_sets = []
James E. Blaircaec0c52012-08-22 14:52:22 -0700696 self.dequeued_needing_change = False
James E. Blair11700c32012-07-05 17:50:05 -0700697 self.current_build_set = BuildSet(self)
698 self.build_sets.append(self.current_build_set)
James E. Blairfee8d652013-06-07 08:57:52 -0700699 self.item_ahead = None
James E. Blair972e3c72013-08-29 12:04:55 -0700700 self.items_behind = []
James E. Blair8fa16972013-01-15 16:57:20 -0800701 self.enqueue_time = None
702 self.dequeue_time = None
James E. Blairfee8d652013-06-07 08:57:52 -0700703 self.reported = False
James E. Blairbfb8e042014-12-30 17:01:44 -0800704 self.active = False # Whether an item is within an active window
705 self.live = True # Whether an item is intended to be processed at all
James E. Blaire5a847f2012-07-10 15:29:14 -0700706
James E. Blair972e3c72013-08-29 12:04:55 -0700707 def __repr__(self):
708 if self.pipeline:
709 pipeline = self.pipeline.name
710 else:
711 pipeline = None
712 return '<QueueItem 0x%x for %s in %s>' % (
713 id(self), self.change, pipeline)
714
James E. Blairee743612012-05-29 14:49:32 -0700715 def resetAllBuilds(self):
James E. Blair11700c32012-07-05 17:50:05 -0700716 old = self.current_build_set
717 self.current_build_set.result = 'CANCELED'
718 self.current_build_set = BuildSet(self)
719 old.next_build_set = self.current_build_set
720 self.current_build_set.previous_build_set = old
James E. Blair7e530ad2012-07-03 16:12:28 -0700721 self.build_sets.append(self.current_build_set)
James E. Blairee743612012-05-29 14:49:32 -0700722
723 def addBuild(self, build):
James E. Blair7e530ad2012-07-03 16:12:28 -0700724 self.current_build_set.addBuild(build)
James E. Blair66eeebf2013-07-27 17:44:32 -0700725 build.pipeline = self.pipeline
James E. Blairee743612012-05-29 14:49:32 -0700726
James E. Blair4a28a882013-08-23 15:17:33 -0700727 def removeBuild(self, build):
728 self.current_build_set.removeBuild(build)
729
James E. Blairfee8d652013-06-07 08:57:52 -0700730 def setReportedResult(self, result):
731 self.current_build_set.result = result
732
James E. Blairb7273ef2016-04-19 08:58:51 -0700733 def formatJobResult(self, job, url_pattern=None):
734 build = self.current_build_set.getBuild(job.name)
735 result = build.result
736 pattern = url_pattern
737 if result == 'SUCCESS':
738 if job.success_message:
739 result = job.success_message
740 if job.success_pattern:
741 pattern = job.success_pattern
742 elif result == 'FAILURE':
743 if job.failure_message:
744 result = job.failure_message
745 if job.failure_pattern:
746 pattern = job.failure_pattern
747 url = None
748 if pattern:
749 try:
750 url = pattern.format(change=self.change,
751 pipeline=self.pipeline,
752 job=job,
753 build=build)
754 except Exception:
755 pass # FIXME: log this or something?
756 if not url:
757 url = build.url or job.name
758 return (result, url)
759
760 def formatJSON(self, url_pattern=None):
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800761 changeish = self.change
762 ret = {}
763 ret['active'] = self.active
James E. Blair107c3852015-02-07 08:23:10 -0800764 ret['live'] = self.live
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800765 if hasattr(changeish, 'url') and changeish.url is not None:
766 ret['url'] = changeish.url
767 else:
768 ret['url'] = None
769 ret['id'] = changeish._id()
770 if self.item_ahead:
771 ret['item_ahead'] = self.item_ahead.change._id()
772 else:
773 ret['item_ahead'] = None
774 ret['items_behind'] = [i.change._id() for i in self.items_behind]
775 ret['failing_reasons'] = self.current_build_set.failing_reasons
776 ret['zuul_ref'] = self.current_build_set.ref
Ramy Asselin07cc33c2015-06-12 14:06:34 -0700777 if changeish.project:
778 ret['project'] = changeish.project.name
779 else:
780 # For cross-project dependencies with the depends-on
781 # project not known to zuul, the project is None
782 # Set it to a static value
783 ret['project'] = "Unknown Project"
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800784 ret['enqueue_time'] = int(self.enqueue_time * 1000)
785 ret['jobs'] = []
Davanum Srinivasb6bfbcc2014-11-18 13:26:52 -0500786 if hasattr(changeish, 'owner'):
787 ret['owner'] = changeish.owner
788 else:
789 ret['owner'] = None
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800790 max_remaining = 0
James E. Blair107c3852015-02-07 08:23:10 -0800791 for job in self.pipeline.getJobs(self):
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800792 now = time.time()
793 build = self.current_build_set.getBuild(job.name)
794 elapsed = None
795 remaining = None
796 result = None
James E. Blairb7273ef2016-04-19 08:58:51 -0700797 build_url = None
798 report_url = None
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800799 worker = None
800 if build:
801 result = build.result
James E. Blairb7273ef2016-04-19 08:58:51 -0700802 build_url = build.url
803 (unused, report_url) = self.formatJobResult(job, url_pattern)
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800804 if build.start_time:
805 if build.end_time:
806 elapsed = int((build.end_time -
807 build.start_time) * 1000)
808 remaining = 0
809 else:
810 elapsed = int((now - build.start_time) * 1000)
811 if build.estimated_time:
812 remaining = max(
813 int(build.estimated_time * 1000) - elapsed,
814 0)
815 worker = {
816 'name': build.worker.name,
817 'hostname': build.worker.hostname,
818 'ips': build.worker.ips,
819 'fqdn': build.worker.fqdn,
820 'program': build.worker.program,
821 'version': build.worker.version,
822 'extra': build.worker.extra
823 }
824 if remaining and remaining > max_remaining:
825 max_remaining = remaining
826
827 ret['jobs'].append({
828 'name': job.name,
829 'elapsed_time': elapsed,
830 'remaining_time': remaining,
James E. Blairb7273ef2016-04-19 08:58:51 -0700831 'url': build_url,
832 'report_url': report_url,
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800833 'result': result,
834 'voting': job.voting,
835 'uuid': build.uuid if build else None,
836 'launch_time': build.launch_time if build else None,
837 'start_time': build.start_time if build else None,
838 'end_time': build.end_time if build else None,
839 'estimated_time': build.estimated_time if build else None,
840 'pipeline': build.pipeline.name if build else None,
841 'canceled': build.canceled if build else None,
842 'retry': build.retry if build else None,
843 'number': build.number if build else None,
Timothy Chavezb2332082015-08-07 20:08:04 -0500844 'node_labels': build.node_labels if build else [],
845 'node_name': build.node_name if build else None,
846 'worker': worker,
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800847 })
848
849 if self.pipeline.haveAllJobsStarted(self):
850 ret['remaining_time'] = max_remaining
851 else:
852 ret['remaining_time'] = None
853 return ret
854
855 def formatStatus(self, indent=0, html=False):
856 changeish = self.change
857 indent_str = ' ' * indent
858 ret = ''
859 if html and hasattr(changeish, 'url') and changeish.url is not None:
860 ret += '%sProject %s change <a href="%s">%s</a>\n' % (
861 indent_str,
862 changeish.project.name,
863 changeish.url,
864 changeish._id())
865 else:
866 ret += '%sProject %s change %s based on %s\n' % (
867 indent_str,
868 changeish.project.name,
869 changeish._id(),
870 self.item_ahead)
James E. Blair107c3852015-02-07 08:23:10 -0800871 for job in self.pipeline.getJobs(self):
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800872 build = self.current_build_set.getBuild(job.name)
873 if build:
874 result = build.result
875 else:
876 result = None
877 job_name = job.name
878 if not job.voting:
879 voting = ' (non-voting)'
880 else:
881 voting = ''
882 if html:
883 if build:
884 url = build.url
885 else:
886 url = None
887 if url is not None:
888 job_name = '<a href="%s">%s</a>' % (url, job_name)
889 ret += '%s %s: %s%s' % (indent_str, job_name, result, voting)
890 ret += '\n'
891 return ret
892
James E. Blairfee8d652013-06-07 08:57:52 -0700893
894class Changeish(object):
895 """Something like a change; either a change or a ref"""
James E. Blairfee8d652013-06-07 08:57:52 -0700896
897 def __init__(self, project):
898 self.project = project
899
Joshua Hesketh36c3fa52014-01-22 11:40:52 +1100900 def getBasePath(self):
901 base_path = ''
902 if hasattr(self, 'refspec'):
903 base_path = "%s/%s/%s" % (
904 self.number[-2:], self.number, self.patchset)
905 elif hasattr(self, 'ref'):
906 base_path = "%s/%s" % (self.newrev[:2], self.newrev)
907
908 return base_path
909
James E. Blairfee8d652013-06-07 08:57:52 -0700910 def equals(self, other):
911 raise NotImplementedError()
912
913 def isUpdateOf(self, other):
914 raise NotImplementedError()
915
916 def filterJobs(self, jobs):
917 return filter(lambda job: job.changeMatches(self), jobs)
918
919 def getRelatedChanges(self):
920 return set()
921
James E. Blair1e8dd892012-05-30 09:15:05 -0700922
James E. Blair4aea70c2012-07-26 14:23:24 -0700923class Change(Changeish):
James E. Blair4aea70c2012-07-26 14:23:24 -0700924 def __init__(self, project):
925 super(Change, self).__init__(project)
926 self.branch = None
927 self.number = None
928 self.url = None
929 self.patchset = None
930 self.refspec = None
931
James E. Blair70c71582013-03-06 08:50:50 -0800932 self.files = []
James E. Blair6965a4b2014-12-16 17:19:04 -0800933 self.needs_changes = []
James E. Blair4aea70c2012-07-26 14:23:24 -0700934 self.needed_by_changes = []
935 self.is_current_patchset = True
936 self.can_merge = False
937 self.is_merged = False
James E. Blairfee8d652013-06-07 08:57:52 -0700938 self.failed_to_merge = False
James E. Blairc053d022014-01-22 14:57:33 -0800939 self.approvals = []
James E. Blair11041d22014-05-02 14:49:53 -0700940 self.open = None
941 self.status = None
Davanum Srinivasb6bfbcc2014-11-18 13:26:52 -0500942 self.owner = None
James E. Blair4aea70c2012-07-26 14:23:24 -0700943
944 def _id(self):
James E. Blairbe765db2012-08-07 08:36:20 -0700945 return '%s,%s' % (self.number, self.patchset)
James E. Blair4aea70c2012-07-26 14:23:24 -0700946
947 def __repr__(self):
948 return '<Change 0x%x %s>' % (id(self), self._id())
949
950 def equals(self, other):
Zhongyue Luoaa85ebf2012-09-21 16:38:33 +0800951 if self.number == other.number and self.patchset == other.patchset:
James E. Blair4aea70c2012-07-26 14:23:24 -0700952 return True
953 return False
954
James E. Blair2fa50962013-01-30 21:50:41 -0800955 def isUpdateOf(self, other):
Clark Boylan01976242013-02-17 18:41:48 -0800956 if ((hasattr(other, 'number') and self.number == other.number) and
James E. Blair7a192e42013-07-11 14:10:36 -0700957 (hasattr(other, 'patchset') and
958 self.patchset is not None and
959 other.patchset is not None and
960 int(self.patchset) > int(other.patchset))):
James E. Blair2fa50962013-01-30 21:50:41 -0800961 return True
962 return False
963
James E. Blairfee8d652013-06-07 08:57:52 -0700964 def getRelatedChanges(self):
965 related = set()
James E. Blair6965a4b2014-12-16 17:19:04 -0800966 for c in self.needs_changes:
967 related.add(c)
James E. Blairfee8d652013-06-07 08:57:52 -0700968 for c in self.needed_by_changes:
969 related.add(c)
970 related.update(c.getRelatedChanges())
971 return related
James E. Blair4aea70c2012-07-26 14:23:24 -0700972
973
974class Ref(Changeish):
James E. Blair4aea70c2012-07-26 14:23:24 -0700975 def __init__(self, project):
James E. Blairbe765db2012-08-07 08:36:20 -0700976 super(Ref, self).__init__(project)
James E. Blair4aea70c2012-07-26 14:23:24 -0700977 self.ref = None
978 self.oldrev = None
979 self.newrev = None
980
James E. Blairbe765db2012-08-07 08:36:20 -0700981 def _id(self):
982 return self.newrev
983
Antoine Musso68bdcd72013-01-17 12:31:28 +0100984 def __repr__(self):
985 rep = None
986 if self.newrev == '0000000000000000000000000000000000000000':
987 rep = '<Ref 0x%x deletes %s from %s' % (
988 id(self), self.ref, self.oldrev)
989 elif self.oldrev == '0000000000000000000000000000000000000000':
990 rep = '<Ref 0x%x creates %s on %s>' % (
991 id(self), self.ref, self.newrev)
992 else:
993 # Catch all
994 rep = '<Ref 0x%x %s updated %s..%s>' % (
995 id(self), self.ref, self.oldrev, self.newrev)
996
997 return rep
998
James E. Blair4aea70c2012-07-26 14:23:24 -0700999 def equals(self, other):
James E. Blair9358c612012-09-28 08:29:39 -07001000 if (self.project == other.project
1001 and self.ref == other.ref
1002 and self.newrev == other.newrev):
James E. Blair4aea70c2012-07-26 14:23:24 -07001003 return True
1004 return False
1005
James E. Blair2fa50962013-01-30 21:50:41 -08001006 def isUpdateOf(self, other):
1007 return False
1008
James E. Blair4aea70c2012-07-26 14:23:24 -07001009
James E. Blair63bb0ef2013-07-29 17:14:51 -07001010class NullChange(Changeish):
James E. Blaire5910202013-12-27 09:50:31 -08001011 def __repr__(self):
1012 return '<NullChange for %s>' % (self.project)
James E. Blair63bb0ef2013-07-29 17:14:51 -07001013
James E. Blair63bb0ef2013-07-29 17:14:51 -07001014 def _id(self):
Alex Gaynorddb9ef32013-09-16 21:04:58 -07001015 return None
James E. Blair63bb0ef2013-07-29 17:14:51 -07001016
1017 def equals(self, other):
Steve Varnau7b78b312015-04-03 14:49:46 -07001018 if (self.project == other.project
1019 and other._id() is None):
James E. Blair4f6033c2014-03-27 15:49:09 -07001020 return True
James E. Blair63bb0ef2013-07-29 17:14:51 -07001021 return False
1022
1023 def isUpdateOf(self, other):
1024 return False
1025
1026
James E. Blairee743612012-05-29 14:49:32 -07001027class TriggerEvent(object):
1028 def __init__(self):
1029 self.data = None
James E. Blair32663402012-06-01 10:04:18 -07001030 # common
James E. Blairee743612012-05-29 14:49:32 -07001031 self.type = None
1032 self.project_name = None
James E. Blair6c358e72013-07-29 17:06:47 -07001033 self.trigger_name = None
Antoine Mussob4e809e2012-12-06 16:58:06 +01001034 # Representation of the user account that performed the event.
1035 self.account = None
James E. Blair32663402012-06-01 10:04:18 -07001036 # patchset-created, comment-added, etc.
James E. Blairee743612012-05-29 14:49:32 -07001037 self.change_number = None
Clark Boylanfc56df32012-06-28 15:25:57 -07001038 self.change_url = None
James E. Blairee743612012-05-29 14:49:32 -07001039 self.patch_number = None
James E. Blaira03262c2012-05-30 09:41:16 -07001040 self.refspec = None
James E. Blairee743612012-05-29 14:49:32 -07001041 self.approvals = []
1042 self.branch = None
Clark Boylanb9bcb402012-06-29 17:44:05 -07001043 self.comment = None
James E. Blair32663402012-06-01 10:04:18 -07001044 # ref-updated
James E. Blairee743612012-05-29 14:49:32 -07001045 self.ref = None
James E. Blair32663402012-06-01 10:04:18 -07001046 self.oldrev = None
James E. Blair89cae0f2012-07-18 11:18:32 -07001047 self.newrev = None
James E. Blair63bb0ef2013-07-29 17:14:51 -07001048 # timer
1049 self.timespec = None
James E. Blairc494d542014-08-06 09:23:52 -07001050 # zuultrigger
1051 self.pipeline_name = None
James E. Blairad28e912013-11-27 10:43:22 -08001052 # For events that arrive with a destination pipeline (eg, from
1053 # an admin command, etc):
1054 self.forced_pipeline = None
James E. Blairee743612012-05-29 14:49:32 -07001055
James E. Blair9f9667e2012-06-12 17:51:08 -07001056 def __repr__(self):
James E. Blairee743612012-05-29 14:49:32 -07001057 ret = '<TriggerEvent %s %s' % (self.type, self.project_name)
James E. Blair1e8dd892012-05-30 09:15:05 -07001058
James E. Blairee743612012-05-29 14:49:32 -07001059 if self.branch:
1060 ret += " %s" % self.branch
1061 if self.change_number:
1062 ret += " %s,%s" % (self.change_number, self.patch_number)
1063 if self.approvals:
James E. Blair1e8dd892012-05-30 09:15:05 -07001064 ret += ' ' + ', '.join(
1065 ['%s:%s' % (a['type'], a['value']) for a in self.approvals])
James E. Blairee743612012-05-29 14:49:32 -07001066 ret += '>'
1067
1068 return ret
1069
James E. Blair1e8dd892012-05-30 09:15:05 -07001070
James E. Blair9c17dbf2014-06-23 14:21:58 -07001071class BaseFilter(object):
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001072 def __init__(self, required_approvals=[], reject_approvals=[]):
James E. Blair5bf78a32015-07-30 18:08:24 +00001073 self._required_approvals = copy.deepcopy(required_approvals)
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001074 self.required_approvals = self._tidy_approvals(required_approvals)
1075 self._reject_approvals = copy.deepcopy(reject_approvals)
1076 self.reject_approvals = self._tidy_approvals(reject_approvals)
James E. Blair9c17dbf2014-06-23 14:21:58 -07001077
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001078 def _tidy_approvals(self, approvals):
1079 for a in approvals:
James E. Blair9c17dbf2014-06-23 14:21:58 -07001080 for k, v in a.items():
1081 if k == 'username':
1082 pass
James E. Blair1fbfceb2014-06-23 14:42:53 -07001083 elif k in ['email', 'email-filter']:
James E. Blair5bf78a32015-07-30 18:08:24 +00001084 a['email'] = re.compile(v)
James E. Blair9c17dbf2014-06-23 14:21:58 -07001085 elif k == 'newer-than':
James E. Blair5bf78a32015-07-30 18:08:24 +00001086 a[k] = time_to_seconds(v)
James E. Blair9c17dbf2014-06-23 14:21:58 -07001087 elif k == 'older-than':
James E. Blair5bf78a32015-07-30 18:08:24 +00001088 a[k] = time_to_seconds(v)
James E. Blair1fbfceb2014-06-23 14:42:53 -07001089 if 'email-filter' in a:
1090 del a['email-filter']
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001091 return approvals
1092
1093 def _match_approval_required_approval(self, rapproval, approval):
1094 # Check if the required approval and approval match
1095 if 'description' not in approval:
1096 return False
1097 now = time.time()
1098 by = approval.get('by', {})
1099 for k, v in rapproval.items():
1100 if k == 'username':
1101 if (by.get('username', '') != v):
1102 return False
1103 elif k == 'email':
1104 if (not v.search(by.get('email', ''))):
1105 return False
1106 elif k == 'newer-than':
1107 t = now - v
1108 if (approval['grantedOn'] < t):
1109 return False
1110 elif k == 'older-than':
1111 t = now - v
1112 if (approval['grantedOn'] >= t):
1113 return False
1114 else:
1115 if not isinstance(v, list):
1116 v = [v]
1117 if (normalizeCategory(approval['description']) != k or
1118 int(approval['value']) not in v):
1119 return False
1120 return True
1121
1122 def matchesApprovals(self, change):
1123 if (self.required_approvals and not change.approvals
1124 or self.reject_approvals and not change.approvals):
1125 # A change with no approvals can not match
1126 return False
1127
1128 # TODO(jhesketh): If we wanted to optimise this slightly we could
1129 # analyse both the REQUIRE and REJECT filters by looping over the
1130 # approvals on the change and keeping track of what we have checked
1131 # rather than needing to loop on the change approvals twice
1132 return (self.matchesRequiredApprovals(change) and
1133 self.matchesNoRejectApprovals(change))
James E. Blair9c17dbf2014-06-23 14:21:58 -07001134
1135 def matchesRequiredApprovals(self, change):
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001136 # Check if any approvals match the requirements
James E. Blair5bf78a32015-07-30 18:08:24 +00001137 for rapproval in self.required_approvals:
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001138 matches_rapproval = False
James E. Blair9c17dbf2014-06-23 14:21:58 -07001139 for approval in change.approvals:
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001140 if self._match_approval_required_approval(rapproval, approval):
1141 # We have a matching approval so this requirement is
1142 # fulfilled
1143 matches_rapproval = True
James E. Blair5bf78a32015-07-30 18:08:24 +00001144 break
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001145 if not matches_rapproval:
James E. Blair5bf78a32015-07-30 18:08:24 +00001146 return False
James E. Blair9c17dbf2014-06-23 14:21:58 -07001147 return True
1148
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001149 def matchesNoRejectApprovals(self, change):
1150 # Check to make sure no approvals match a reject criteria
1151 for rapproval in self.reject_approvals:
1152 for approval in change.approvals:
1153 if self._match_approval_required_approval(rapproval, approval):
1154 # A reject approval has been matched, so we reject
1155 # immediately
1156 return False
1157 # To get here no rejects can have been matched so we should be good to
1158 # queue
1159 return True
1160
James E. Blair9c17dbf2014-06-23 14:21:58 -07001161
1162class EventFilter(BaseFilter):
James E. Blairc0dedf82014-08-06 09:37:52 -07001163 def __init__(self, trigger, types=[], branches=[], refs=[],
1164 event_approvals={}, comments=[], emails=[], usernames=[],
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001165 timespecs=[], required_approvals=[], reject_approvals=[],
1166 pipelines=[], ignore_deletes=True):
James E. Blair9c17dbf2014-06-23 14:21:58 -07001167 super(EventFilter, self).__init__(
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001168 required_approvals=required_approvals,
1169 reject_approvals=reject_approvals)
James E. Blairc0dedf82014-08-06 09:37:52 -07001170 self.trigger = trigger
James E. Blairee743612012-05-29 14:49:32 -07001171 self._types = types
1172 self._branches = branches
1173 self._refs = refs
James E. Blair1fbfceb2014-06-23 14:42:53 -07001174 self._comments = comments
1175 self._emails = emails
1176 self._usernames = usernames
James E. Blairc494d542014-08-06 09:23:52 -07001177 self._pipelines = pipelines
James E. Blairee743612012-05-29 14:49:32 -07001178 self.types = [re.compile(x) for x in types]
1179 self.branches = [re.compile(x) for x in branches]
1180 self.refs = [re.compile(x) for x in refs]
James E. Blair1fbfceb2014-06-23 14:42:53 -07001181 self.comments = [re.compile(x) for x in comments]
1182 self.emails = [re.compile(x) for x in emails]
1183 self.usernames = [re.compile(x) for x in usernames]
James E. Blairc494d542014-08-06 09:23:52 -07001184 self.pipelines = [re.compile(x) for x in pipelines]
James E. Blairc053d022014-01-22 14:57:33 -08001185 self.event_approvals = event_approvals
James E. Blair63bb0ef2013-07-29 17:14:51 -07001186 self.timespecs = timespecs
K Jonathan Harkerf95e7232015-04-29 13:33:16 -07001187 self.ignore_deletes = ignore_deletes
James E. Blairee743612012-05-29 14:49:32 -07001188
James E. Blair9f9667e2012-06-12 17:51:08 -07001189 def __repr__(self):
James E. Blairee743612012-05-29 14:49:32 -07001190 ret = '<EventFilter'
James E. Blair1e8dd892012-05-30 09:15:05 -07001191
James E. Blairee743612012-05-29 14:49:32 -07001192 if self._types:
1193 ret += ' types: %s' % ', '.join(self._types)
James E. Blairc494d542014-08-06 09:23:52 -07001194 if self._pipelines:
1195 ret += ' pipelines: %s' % ', '.join(self._pipelines)
James E. Blairee743612012-05-29 14:49:32 -07001196 if self._branches:
1197 ret += ' branches: %s' % ', '.join(self._branches)
1198 if self._refs:
1199 ret += ' refs: %s' % ', '.join(self._refs)
K Jonathan Harkerf95e7232015-04-29 13:33:16 -07001200 if self.ignore_deletes:
1201 ret += ' ignore_deletes: %s' % self.ignore_deletes
James E. Blairc053d022014-01-22 14:57:33 -08001202 if self.event_approvals:
1203 ret += ' event_approvals: %s' % ', '.join(
1204 ['%s:%s' % a for a in self.event_approvals.items()])
James E. Blair5bf78a32015-07-30 18:08:24 +00001205 if self.required_approvals:
1206 ret += ' required_approvals: %s' % ', '.join(
1207 ['%s' % a for a in self._required_approvals])
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001208 if self.reject_approvals:
1209 ret += ' reject_approvals: %s' % ', '.join(
1210 ['%s' % a for a in self._reject_approvals])
James E. Blair1fbfceb2014-06-23 14:42:53 -07001211 if self._comments:
1212 ret += ' comments: %s' % ', '.join(self._comments)
1213 if self._emails:
1214 ret += ' emails: %s' % ', '.join(self._emails)
1215 if self._usernames:
1216 ret += ' username_filters: %s' % ', '.join(self._usernames)
James E. Blair63bb0ef2013-07-29 17:14:51 -07001217 if self.timespecs:
1218 ret += ' timespecs: %s' % ', '.join(self.timespecs)
James E. Blairee743612012-05-29 14:49:32 -07001219 ret += '>'
1220
1221 return ret
1222
James E. Blairc053d022014-01-22 14:57:33 -08001223 def matches(self, event, change):
James E. Blairee743612012-05-29 14:49:32 -07001224 # event types are ORed
1225 matches_type = False
1226 for etype in self.types:
1227 if etype.match(event.type):
1228 matches_type = True
1229 if self.types and not matches_type:
1230 return False
1231
James E. Blairc494d542014-08-06 09:23:52 -07001232 # pipelines are ORed
1233 matches_pipeline = False
1234 for epipe in self.pipelines:
1235 if epipe.match(event.pipeline_name):
1236 matches_pipeline = True
1237 if self.pipelines and not matches_pipeline:
1238 return False
1239
James E. Blairee743612012-05-29 14:49:32 -07001240 # branches are ORed
1241 matches_branch = False
1242 for branch in self.branches:
1243 if branch.match(event.branch):
1244 matches_branch = True
1245 if self.branches and not matches_branch:
1246 return False
1247
1248 # refs are ORed
1249 matches_ref = False
Yolanda Robla16698872014-08-25 11:59:27 +02001250 if event.ref is not None:
1251 for ref in self.refs:
1252 if ref.match(event.ref):
1253 matches_ref = True
James E. Blairee743612012-05-29 14:49:32 -07001254 if self.refs and not matches_ref:
1255 return False
K Jonathan Harkerf95e7232015-04-29 13:33:16 -07001256 if self.ignore_deletes and event.newrev == EMPTY_GIT_REF:
1257 # If the updated ref has an empty git sha (all 0s),
1258 # then the ref is being deleted
1259 return False
James E. Blairee743612012-05-29 14:49:32 -07001260
James E. Blair1fbfceb2014-06-23 14:42:53 -07001261 # comments are ORed
1262 matches_comment_re = False
1263 for comment_re in self.comments:
Clark Boylanb9bcb402012-06-29 17:44:05 -07001264 if (event.comment is not None and
James E. Blair1fbfceb2014-06-23 14:42:53 -07001265 comment_re.search(event.comment)):
1266 matches_comment_re = True
1267 if self.comments and not matches_comment_re:
Clark Boylanb9bcb402012-06-29 17:44:05 -07001268 return False
1269
Antoine Mussob4e809e2012-12-06 16:58:06 +01001270 # We better have an account provided by Gerrit to do
1271 # email filtering.
1272 if event.account is not None:
James E. Blaircf429f32012-12-20 14:28:24 -08001273 account_email = event.account.get('email')
James E. Blair1fbfceb2014-06-23 14:42:53 -07001274 # emails are ORed
1275 matches_email_re = False
1276 for email_re in self.emails:
Antoine Mussob4e809e2012-12-06 16:58:06 +01001277 if (account_email is not None and
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001278 email_re.search(account_email)):
James E. Blair1fbfceb2014-06-23 14:42:53 -07001279 matches_email_re = True
1280 if self.emails and not matches_email_re:
Antoine Mussob4e809e2012-12-06 16:58:06 +01001281 return False
1282
James E. Blair1fbfceb2014-06-23 14:42:53 -07001283 # usernames are ORed
Joshua Heskethb8a817e2013-12-27 11:21:38 +11001284 account_username = event.account.get('username')
James E. Blair1fbfceb2014-06-23 14:42:53 -07001285 matches_username_re = False
1286 for username_re in self.usernames:
Joshua Heskethb8a817e2013-12-27 11:21:38 +11001287 if (account_username is not None and
James E. Blair1fbfceb2014-06-23 14:42:53 -07001288 username_re.search(account_username)):
1289 matches_username_re = True
1290 if self.usernames and not matches_username_re:
Joshua Heskethb8a817e2013-12-27 11:21:38 +11001291 return False
1292
James E. Blairee743612012-05-29 14:49:32 -07001293 # approvals are ANDed
James E. Blairc053d022014-01-22 14:57:33 -08001294 for category, value in self.event_approvals.items():
James E. Blairee743612012-05-29 14:49:32 -07001295 matches_approval = False
1296 for eapproval in event.approvals:
1297 if (normalizeCategory(eapproval['description']) == category and
1298 int(eapproval['value']) == int(value)):
1299 matches_approval = True
James E. Blair1e8dd892012-05-30 09:15:05 -07001300 if not matches_approval:
1301 return False
James E. Blair63bb0ef2013-07-29 17:14:51 -07001302
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001303 # required approvals are ANDed (reject approvals are ORed)
1304 if not self.matchesApprovals(change):
James E. Blair9c17dbf2014-06-23 14:21:58 -07001305 return False
James E. Blairc053d022014-01-22 14:57:33 -08001306
James E. Blair63bb0ef2013-07-29 17:14:51 -07001307 # timespecs are ORed
1308 matches_timespec = False
1309 for timespec in self.timespecs:
1310 if (event.timespec == timespec):
1311 matches_timespec = True
1312 if self.timespecs and not matches_timespec:
1313 return False
1314
James E. Blairee743612012-05-29 14:49:32 -07001315 return True
James E. Blaireff88162013-07-01 12:44:14 -04001316
1317
James E. Blair9c17dbf2014-06-23 14:21:58 -07001318class ChangeishFilter(BaseFilter):
Clark Boylana9702ad2014-05-08 17:17:24 -07001319 def __init__(self, open=None, current_patchset=None,
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001320 statuses=[], required_approvals=[],
1321 reject_approvals=[]):
James E. Blair9c17dbf2014-06-23 14:21:58 -07001322 super(ChangeishFilter, self).__init__(
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001323 required_approvals=required_approvals,
1324 reject_approvals=reject_approvals)
James E. Blair11041d22014-05-02 14:49:53 -07001325 self.open = open
Clark Boylana9702ad2014-05-08 17:17:24 -07001326 self.current_patchset = current_patchset
James E. Blair11041d22014-05-02 14:49:53 -07001327 self.statuses = statuses
James E. Blair11041d22014-05-02 14:49:53 -07001328
1329 def __repr__(self):
1330 ret = '<ChangeishFilter'
1331
1332 if self.open is not None:
1333 ret += ' open: %s' % self.open
Clark Boylana9702ad2014-05-08 17:17:24 -07001334 if self.current_patchset is not None:
1335 ret += ' current-patchset: %s' % self.current_patchset
James E. Blair11041d22014-05-02 14:49:53 -07001336 if self.statuses:
1337 ret += ' statuses: %s' % ', '.join(self.statuses)
James E. Blair5bf78a32015-07-30 18:08:24 +00001338 if self.required_approvals:
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001339 ret += (' required_approvals: %s' %
1340 str(self.required_approvals))
1341 if self.reject_approvals:
1342 ret += (' reject_approvals: %s' %
1343 str(self.reject_approvals))
James E. Blair11041d22014-05-02 14:49:53 -07001344 ret += '>'
1345
1346 return ret
1347
1348 def matches(self, change):
1349 if self.open is not None:
1350 if self.open != change.open:
1351 return False
1352
Clark Boylana9702ad2014-05-08 17:17:24 -07001353 if self.current_patchset is not None:
1354 if self.current_patchset != change.is_current_patchset:
1355 return False
1356
James E. Blair11041d22014-05-02 14:49:53 -07001357 if self.statuses:
1358 if change.status not in self.statuses:
1359 return False
1360
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001361 # required approvals are ANDed (reject approvals are ORed)
1362 if not self.matchesApprovals(change):
James E. Blair9c17dbf2014-06-23 14:21:58 -07001363 return False
James E. Blair11041d22014-05-02 14:49:53 -07001364
1365 return True
1366
1367
James E. Blaireff88162013-07-01 12:44:14 -04001368class Layout(object):
1369 def __init__(self):
1370 self.projects = {}
James E. Blair5a9918a2013-08-27 10:06:27 -07001371 self.pipelines = OrderedDict()
James E. Blaireff88162013-07-01 12:44:14 -04001372 self.jobs = {}
James E. Blairc28d1b02013-07-19 11:37:06 -07001373 self.metajobs = []
James E. Blaireff88162013-07-01 12:44:14 -04001374
1375 def getJob(self, name):
1376 if name in self.jobs:
1377 return self.jobs[name]
1378 job = Job(name)
Maru Newby79427a42015-02-17 17:54:45 +00001379 if job.is_metajob:
James E. Blaireff88162013-07-01 12:44:14 -04001380 regex = re.compile(name)
James E. Blairc28d1b02013-07-19 11:37:06 -07001381 self.metajobs.append((regex, job))
James E. Blaireff88162013-07-01 12:44:14 -04001382 else:
1383 # Apply attributes from matching meta-jobs
James E. Blairc28d1b02013-07-19 11:37:06 -07001384 for regex, metajob in self.metajobs:
James E. Blaireff88162013-07-01 12:44:14 -04001385 if regex.match(name):
1386 job.copy(metajob)
1387 self.jobs[name] = job
1388 return job
James E. Blairce8a2132016-05-19 15:21:52 -07001389
1390
1391class JobTimeData(object):
1392 format = 'B10H10H10B'
1393 version = 0
1394
1395 def __init__(self, path):
1396 self.path = path
1397 self.success_times = [0 for x in range(10)]
1398 self.failure_times = [0 for x in range(10)]
1399 self.results = [0 for x in range(10)]
1400
1401 def load(self):
1402 if not os.path.exists(self.path):
1403 return
1404 with open(self.path) as f:
1405 data = struct.unpack(self.format, f.read())
1406 version = data[0]
1407 if version != self.version:
1408 raise Exception("Unkown data version")
1409 self.success_times = list(data[1:11])
1410 self.failure_times = list(data[11:21])
1411 self.results = list(data[21:32])
1412
1413 def save(self):
1414 tmpfile = self.path + '.tmp'
1415 data = [self.version]
1416 data.extend(self.success_times)
1417 data.extend(self.failure_times)
1418 data.extend(self.results)
1419 data = struct.pack(self.format, *data)
1420 with open(tmpfile, 'w') as f:
1421 f.write(data)
1422 os.rename(tmpfile, self.path)
1423
1424 def add(self, elapsed, result):
1425 elapsed = int(elapsed)
1426 if result == 'SUCCESS':
1427 self.success_times.append(elapsed)
1428 self.success_times.pop(0)
1429 result = 0
1430 else:
1431 self.failure_times.append(elapsed)
1432 self.failure_times.pop(0)
1433 result = 1
1434 self.results.append(result)
1435 self.results.pop(0)
1436
1437 def getEstimatedTime(self):
1438 times = [x for x in self.success_times if x]
1439 if times:
1440 return float(sum(times)) / len(times)
1441 return 0.0
1442
1443
1444class TimeDataBase(object):
1445 def __init__(self, root):
1446 self.root = root
1447 self.jobs = {}
1448
1449 def _getTD(self, name):
1450 td = self.jobs.get(name)
1451 if not td:
1452 td = JobTimeData(os.path.join(self.root, name))
1453 self.jobs[name] = td
1454 td.load()
1455 return td
1456
1457 def getEstimatedTime(self, name):
1458 return self._getTD(name).getEstimatedTime()
1459
1460 def update(self, name, elapsed, result):
1461 td = self._getTD(name)
1462 td.add(elapsed, result)
1463 td.save()