blob: 5a630d9363e17867bc86c7e5e2eb4a5f30c1ab67 [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
15import re
James E. Blairff986a12012-05-30 14:56:51 -070016import time
James E. Blairee743612012-05-29 14:49:32 -070017
James E. Blair1e8dd892012-05-30 09:15:05 -070018
James E. Blairee743612012-05-29 14:49:32 -070019class ChangeQueue(object):
20 def __init__(self, queue_name):
21 self.name = ''
22 self.queue_name = queue_name
23 self.projects = []
24 self._jobs = set()
25 self.queue = []
26
27 def __str__(self):
28 return '<ChangeQueue %s: %s>' % (self.queue_name, self.name)
29
30 def getJobs(self):
31 return self._jobs
32
33 def addProject(self, project):
34 if project not in self.projects:
35 self.projects.append(project)
36 names = [x.name for x in self.projects]
37 names.sort()
38 self.name = ', '.join(names)
39 self._jobs |= set(project.getJobs(self.queue_name))
40
41 def enqueueChange(self, change):
42 if self.queue:
43 self.queue[-1].change_behind = change
44 change.change_ahead = self.queue[-1]
45 self.queue.append(change)
46 change.queue = self
47
48 def dequeueChange(self, change):
49 if change in self.queue:
50 self.queue.remove(change)
51
52 def mergeChangeQueue(self, other):
53 for project in other.projects:
54 self.addProject(project)
55
James E. Blair1e8dd892012-05-30 09:15:05 -070056
James E. Blairee743612012-05-29 14:49:32 -070057class Job(object):
58 def __init__(self, name):
59 self.name = name
60 self.failure_message = None
61 self.success_message = None
62 self.event_filters = []
63
64 def __str__(self):
65 return self.name
66
67 def __repr__(self):
68 return '<Job %s>' % (self.name)
69
James E. Blair1e8dd892012-05-30 09:15:05 -070070
James E. Blairee743612012-05-29 14:49:32 -070071class Build(object):
72 def __init__(self, job, uuid):
73 self.job = job
74 self.uuid = uuid
75 self.status = None
76 self.url = None
77 self.number = None
James E. Blairff986a12012-05-30 14:56:51 -070078 self.result = None
79 self.launch_time = time.time()
James E. Blairee743612012-05-29 14:49:32 -070080
81 def __repr__(self):
82 return '<Build %s of %s>' % (self.uuid, self.job.name)
83
James E. Blair1e8dd892012-05-30 09:15:05 -070084
James E. Blairee743612012-05-29 14:49:32 -070085class JobTree(object):
86 """ A JobTree represents an instance of one Job, and holds JobTrees
87 whose jobs should be run if that Job succeeds. A root node of a
88 JobTree will have no associated Job. """
89
90 def __init__(self, job):
91 self.job = job
92 self.job_trees = []
93
94 def addJob(self, job):
95 if job not in [x.job for x in self.job_trees]:
96 t = JobTree(job)
97 self.job_trees.append(t)
98 return t
99
100 def getJobs(self):
101 jobs = []
102 for x in self.job_trees:
103 jobs.append(x.job)
104 jobs.extend(x.getJobs())
105 return jobs
106
107 def getJobTreeForJob(self, job):
108 if self.job == job:
109 return self
110 for tree in self.job_trees:
111 ret = tree.getJobTreeForJob(job)
112 if ret:
113 return ret
114 return None
115
James E. Blair1e8dd892012-05-30 09:15:05 -0700116
James E. Blairee743612012-05-29 14:49:32 -0700117class Project(object):
118 def __init__(self, name):
119 self.name = name
120 self.job_trees = {} # Queue -> JobTree
121
122 def __str__(self):
123 return self.name
124
125 def __repr__(self):
126 return '<Project %s>' % (self.name)
127
128 def addQueue(self, name):
129 self.job_trees[name] = JobTree(None)
130 return self.job_trees[name]
131
132 def hasQueue(self, name):
James E. Blair1e8dd892012-05-30 09:15:05 -0700133 if name in self.job_trees:
James E. Blairee743612012-05-29 14:49:32 -0700134 return True
135 return False
136
137 def getJobTreeForQueue(self, name):
138 return self.job_trees.get(name, None)
139
140 def getJobs(self, queue_name):
141 tree = self.getJobTreeForQueue(queue_name)
142 if not tree:
143 return []
144 return tree.getJobs()
145
James E. Blair1e8dd892012-05-30 09:15:05 -0700146
James E. Blairee743612012-05-29 14:49:32 -0700147class Change(object):
James E. Blair32663402012-06-01 10:04:18 -0700148 def __init__(self, queue_name, project, event):
James E. Blairee743612012-05-29 14:49:32 -0700149 self.queue_name = queue_name
150 self.project = project
James E. Blair32663402012-06-01 10:04:18 -0700151 self.branch = None
152 self.number = None
153 self.patchset = None
154 self.refspec = None
155 self.ref = None
156 self.oldrev = None
157 self.newrev = None
158
159 if event.change_number:
160 self.branch = event.branch
161 self.number = event.change_number
162 self.patchset = event.patch_number
163 self.refspec = event.refspec
164 if event.ref:
165 self.ref = event.ref
166 self.oldrev = event.oldrev
167 self.newrev = event.newrev
168
James E. Blairee743612012-05-29 14:49:32 -0700169 self.jobs = {}
170 self.job_urls = {}
171 self.change_ahead = None
172 self.change_behind = None
173 self.running_builds = []
174
James E. Blair32663402012-06-01 10:04:18 -0700175 def _id(self):
176 if self.number:
177 return '%s,%s' % (self.number, self.patchset)
178 return self.newrev
179
James E. Blairee743612012-05-29 14:49:32 -0700180 def __str__(self):
James E. Blair32663402012-06-01 10:04:18 -0700181 return '<Change 0x%x %s>' % (id(self), self._id())
James E. Blairee743612012-05-29 14:49:32 -0700182
183 def formatStatus(self, indent=0):
James E. Blair1e8dd892012-05-30 09:15:05 -0700184 indent_str = ' ' * indent
James E. Blairee743612012-05-29 14:49:32 -0700185 ret = ''
James E. Blair32663402012-06-01 10:04:18 -0700186 ret += '%sProject %s change %s\n' % (indent_str,
187 self.project.name,
188 self._id())
James E. Blairee743612012-05-29 14:49:32 -0700189 for job in self.project.getJobs(self.queue_name):
190 result = self.jobs.get(job.name)
191 ret += '%s %s: %s\n' % (indent_str, job.name, result)
192 if self.change_ahead:
193 ret += '%sWaiting on:\n' % (indent_str)
James E. Blair1e8dd892012-05-30 09:15:05 -0700194 ret += self.change_ahead.formatStatus(indent + 2)
James E. Blairee743612012-05-29 14:49:32 -0700195 return ret
196
197 def formatReport(self):
198 ret = ''
199 if self.didAllJobsSucceed():
200 ret += 'Build successful\n\n'
201 else:
202 ret += 'Build failed\n\n'
James E. Blair1e8dd892012-05-30 09:15:05 -0700203
James E. Blairee743612012-05-29 14:49:32 -0700204 for job in self.project.getJobs(self.queue_name):
205 result = self.jobs.get(job.name)
206 url = self.job_urls.get(job.name, job.name)
207 ret += '- %s : %s\n' % (url, result)
208 return ret
209
210 def resetAllBuilds(self):
211 self.jobs = {}
212 self.job_urls = {}
213 self.running_builds = []
214
215 def addBuild(self, build):
216 self.running_builds.append(build)
217
218 def setResult(self, build):
219 self.running_builds.remove(build)
220 self.jobs[build.job.name] = build.result
James E. Blairff986a12012-05-30 14:56:51 -0700221 if build.url:
222 self.job_urls[build.job.name] = build.url
James E. Blairee743612012-05-29 14:49:32 -0700223 if build.result != 'SUCCESS':
224 # Get a JobTree from a Job so we can find only its dependent jobs
225 root = self.project.getJobTreeForQueue(self.queue_name)
226 tree = root.getJobTreeForJob(build.job)
227 for job in tree.getJobs():
228 self.jobs[job.name] = 'SKIPPED'
229
230 def _findJobsToRun(self, job_trees):
231 torun = []
232 for tree in job_trees:
233 job = tree.job
234 if job:
235 result = self.jobs.get(job.name, None)
236 else:
237 # This is a null job tree, run all of its jobs
238 result = 'SUCCESS'
239 if not result:
240 if job not in [b.job for b in self.running_builds]:
241 torun.append(job)
242 elif result == 'SUCCESS':
243 torun.extend(self._findJobsToRun(tree.job_trees))
244 return torun
245
246 def findJobsToRun(self):
247 tree = self.project.getJobTreeForQueue(self.queue_name)
248 return self._findJobsToRun(tree.job_trees)
249
250 def areAllJobsComplete(self):
251 tree = self.project.getJobTreeForQueue(self.queue_name)
252 for job in tree.getJobs():
James E. Blair1e8dd892012-05-30 09:15:05 -0700253 if not job.name in self.jobs:
James E. Blairee743612012-05-29 14:49:32 -0700254 return False
255 return True
256
257 def didAllJobsSucceed(self):
258 for result in self.jobs.values():
259 if result != 'SUCCESS':
260 return False
261 return True
262
263 def delete(self):
264 if self.change_behind:
265 self.change_behind.change_ahead = None
266
James E. Blair1e8dd892012-05-30 09:15:05 -0700267
James E. Blairee743612012-05-29 14:49:32 -0700268class TriggerEvent(object):
269 def __init__(self):
270 self.data = None
James E. Blair32663402012-06-01 10:04:18 -0700271 # common
James E. Blairee743612012-05-29 14:49:32 -0700272 self.type = None
273 self.project_name = None
James E. Blair32663402012-06-01 10:04:18 -0700274 # patchset-created, comment-added, etc.
James E. Blairee743612012-05-29 14:49:32 -0700275 self.change_number = None
276 self.patch_number = None
James E. Blaira03262c2012-05-30 09:41:16 -0700277 self.refspec = None
James E. Blairee743612012-05-29 14:49:32 -0700278 self.approvals = []
279 self.branch = None
James E. Blair32663402012-06-01 10:04:18 -0700280 # ref-updated
James E. Blairee743612012-05-29 14:49:32 -0700281 self.ref = None
James E. Blair32663402012-06-01 10:04:18 -0700282 self.oldrev = None
283 self.newrew = None
James E. Blairee743612012-05-29 14:49:32 -0700284
285 def __str__(self):
286 ret = '<TriggerEvent %s %s' % (self.type, self.project_name)
James E. Blair1e8dd892012-05-30 09:15:05 -0700287
James E. Blairee743612012-05-29 14:49:32 -0700288 if self.branch:
289 ret += " %s" % self.branch
290 if self.change_number:
291 ret += " %s,%s" % (self.change_number, self.patch_number)
292 if self.approvals:
James E. Blair1e8dd892012-05-30 09:15:05 -0700293 ret += ' ' + ', '.join(
294 ['%s:%s' % (a['type'], a['value']) for a in self.approvals])
James E. Blairee743612012-05-29 14:49:32 -0700295 ret += '>'
296
297 return ret
298
James E. Blair1e8dd892012-05-30 09:15:05 -0700299
James E. Blairee743612012-05-29 14:49:32 -0700300class EventFilter(object):
301 def __init__(self, types=[], branches=[], refs=[], approvals=[]):
302 self._types = types
303 self._branches = branches
304 self._refs = refs
305 self.types = [re.compile(x) for x in types]
306 self.branches = [re.compile(x) for x in branches]
307 self.refs = [re.compile(x) for x in refs]
308 self.approvals = approvals
309
310 def __str__(self):
311 ret = '<EventFilter'
James E. Blair1e8dd892012-05-30 09:15:05 -0700312
James E. Blairee743612012-05-29 14:49:32 -0700313 if self._types:
314 ret += ' types: %s' % ', '.join(self._types)
315 if self._branches:
316 ret += ' branches: %s' % ', '.join(self._branches)
317 if self._refs:
318 ret += ' refs: %s' % ', '.join(self._refs)
319 if self.approvals:
James E. Blair1e8dd892012-05-30 09:15:05 -0700320 ret += ' approvals: %s' % ', '.join(
321 ['%s:%s' % a for a in self.approvals.items()])
James E. Blairee743612012-05-29 14:49:32 -0700322 ret += '>'
323
324 return ret
325
326 def matches(self, event):
327 def normalizeCategory(name):
328 name = name.lower()
329 return re.sub(' ', '-', name)
330
331 # event types are ORed
332 matches_type = False
333 for etype in self.types:
334 if etype.match(event.type):
335 matches_type = True
336 if self.types and not matches_type:
337 return False
338
339 # branches are ORed
340 matches_branch = False
341 for branch in self.branches:
342 if branch.match(event.branch):
343 matches_branch = True
344 if self.branches and not matches_branch:
345 return False
346
347 # refs are ORed
348 matches_ref = False
349 for ref in self.refs:
350 if ref.match(event.ref):
351 matches_ref = True
352 if self.refs and not matches_ref:
353 return False
354
355 # approvals are ANDed
356 for category, value in self.approvals.items():
357 matches_approval = False
358 for eapproval in event.approvals:
359 if (normalizeCategory(eapproval['description']) == category and
360 int(eapproval['value']) == int(value)):
361 matches_approval = True
James E. Blair1e8dd892012-05-30 09:15:05 -0700362 if not matches_approval:
363 return False
James E. Blairee743612012-05-29 14:49:32 -0700364 return True