blob: 9428875b75725ef323485dfef192872a980aaf64 [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
James E. Blair9f9667e2012-06-12 17:51:08 -070027 def __repr__(self):
James E. Blairee743612012-05-29 14:49:32 -070028 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. Blairb0954652012-06-01 11:32:01 -070070 def copy(self, other):
71 self.failure_message = other.failure_message
72 self.success_message = other.failure_message
73 self.event_filters = other.event_filters[:]
74
James E. Blair1e8dd892012-05-30 09:15:05 -070075
James E. Blairee743612012-05-29 14:49:32 -070076class Build(object):
77 def __init__(self, job, uuid):
78 self.job = job
79 self.uuid = uuid
James E. Blair11700c32012-07-05 17:50:05 -070080 self.base_url = None
James E. Blairee743612012-05-29 14:49:32 -070081 self.url = None
82 self.number = None
James E. Blairff986a12012-05-30 14:56:51 -070083 self.result = None
James E. Blair7e530ad2012-07-03 16:12:28 -070084 self.build_set = None
James E. Blairff986a12012-05-30 14:56:51 -070085 self.launch_time = time.time()
James E. Blairee743612012-05-29 14:49:32 -070086
87 def __repr__(self):
88 return '<Build %s of %s>' % (self.uuid, self.job.name)
89
James E. Blair11700c32012-07-05 17:50:05 -070090 def formatDescription(self):
91 concurrent_changes = ''
92 concurrent_builds = ''
93 other_builds = ''
94
95 for change in self.build_set.other_changes:
96 concurrent_changes += '<li><a href="{change.url}">\
97 {change.number},{change.patchset}</a></li>'.format(
98 change=change)
99
100 change = self.build_set.change
101
102 for build in self.build_set.getBuilds():
103 if build.base_url:
104 concurrent_builds += """\
105<li>
106 <a href="{build.base_url}">
107 {build.job.name} #{build.number}</a>: {build.result}
108</li>
109""".format(build=build)
110 else:
111 concurrent_builds += """\
112<li>
113 {build.job.name}: {build.result}
114</li>""".format(build=build)
115
116 if self.build_set.previous_build_set:
117 build = self.build_set.previous_build_set.getBuild(self.job.name)
118 if build:
119 other_builds += """\
120<li>
121 Preceded by: <a href="{build.base_url}">
122 {build.job.name} #{build.number}</a>
123</li>
124""".format(build=build)
125
126 if self.build_set.next_build_set:
127 build = self.build_set.next_build_set.getBuild(self.job.name)
128 if build:
129 other_builds += """\
130<li>
131 Succeeded by: <a href="{build.base_url}">
132 {build.job.name} #{build.number}</a>
133</li>
134""".format(build=build)
135
136 result = self.build_set.result
137
138 if change.number:
139 ret = """\
140<p>
141 Triggered by change:
142 <a href="{change.url}">{change.number},{change.patchset}</a><br/>
143 Branch: <b>{change.branch}</b><br/>
144 Pipeline: <b>{change.queue_name}</b>
145</p>"""
146 else:
147 ret = """\
148<p>
149 Triggered by reference:
150 {change.ref}</a><br/>
151 Old revision: <b>{change.oldrev}</b><br/>
152 New revision: <b>{change.newrev}</b><br/>
153 Pipeline: <b>{change.queue_name}</b>
154</p>"""
155
156 if concurrent_changes:
157 ret += """\
158<p>
159 Other changes tested concurrently with this change:
160 <ul>{concurrent_changes}</ul>
161</p>
162"""
163 if concurrent_builds:
164 ret += """\
165<p>
166 All builds for this change set:
167 <ul>{concurrent_builds}</ul>
168</p>
169"""
170
171 if other_builds:
172 ret += """\
173<p>
174 Other build sets for this change:
175 <ul>{other_builds}</ul>
176</p>
177"""
178 if result:
179 ret += """\
180<p>
181 Reported result: <b>{result}</b>
182</p>
183"""
184
185 ret = ret.format(**locals())
186 return ret
187
James E. Blair1e8dd892012-05-30 09:15:05 -0700188
James E. Blairee743612012-05-29 14:49:32 -0700189class JobTree(object):
190 """ A JobTree represents an instance of one Job, and holds JobTrees
191 whose jobs should be run if that Job succeeds. A root node of a
192 JobTree will have no associated Job. """
193
194 def __init__(self, job):
195 self.job = job
196 self.job_trees = []
197
198 def addJob(self, job):
199 if job not in [x.job for x in self.job_trees]:
200 t = JobTree(job)
201 self.job_trees.append(t)
202 return t
203
204 def getJobs(self):
205 jobs = []
206 for x in self.job_trees:
207 jobs.append(x.job)
208 jobs.extend(x.getJobs())
209 return jobs
210
211 def getJobTreeForJob(self, job):
212 if self.job == job:
213 return self
214 for tree in self.job_trees:
215 ret = tree.getJobTreeForJob(job)
216 if ret:
217 return ret
218 return None
219
James E. Blair1e8dd892012-05-30 09:15:05 -0700220
James E. Blairee743612012-05-29 14:49:32 -0700221class Project(object):
222 def __init__(self, name):
223 self.name = name
224 self.job_trees = {} # Queue -> JobTree
225
226 def __str__(self):
227 return self.name
228
229 def __repr__(self):
230 return '<Project %s>' % (self.name)
231
232 def addQueue(self, name):
233 self.job_trees[name] = JobTree(None)
234 return self.job_trees[name]
235
236 def hasQueue(self, name):
James E. Blair1e8dd892012-05-30 09:15:05 -0700237 if name in self.job_trees:
James E. Blairee743612012-05-29 14:49:32 -0700238 return True
239 return False
240
241 def getJobTreeForQueue(self, name):
242 return self.job_trees.get(name, None)
243
244 def getJobs(self, queue_name):
245 tree = self.getJobTreeForQueue(queue_name)
246 if not tree:
247 return []
248 return tree.getJobs()
249
James E. Blair1e8dd892012-05-30 09:15:05 -0700250
James E. Blair7e530ad2012-07-03 16:12:28 -0700251class BuildSet(object):
James E. Blair11700c32012-07-05 17:50:05 -0700252 def __init__(self, change):
253 self.change = change
254 self.other_changes = []
James E. Blair7e530ad2012-07-03 16:12:28 -0700255 self.builds = {}
James E. Blair11700c32012-07-05 17:50:05 -0700256 self.result = None
257 self.next_build_set = None
258 self.previous_build_set = None
James E. Blair7e530ad2012-07-03 16:12:28 -0700259
260 def addBuild(self, build):
261 self.builds[build.job.name] = build
262 build.build_set = self
263
James E. Blair11700c32012-07-05 17:50:05 -0700264 # The change isn't enqueued until after it's created
265 # so we don't know what the other changes ahead will be
266 # until jobs start.
267 if not self.other_changes:
268 next_change = self.change.change_ahead
269 while next_change:
270 self.other_changes.append(next_change)
271 next_change = next_change.change_ahead
272
James E. Blair7e530ad2012-07-03 16:12:28 -0700273 def getBuild(self, job_name):
274 return self.builds.get(job_name)
275
James E. Blair11700c32012-07-05 17:50:05 -0700276 def getBuilds(self):
277 keys = self.builds.keys()
278 keys.sort()
279 return [self.builds.get(x) for x in keys]
280
James E. Blair7e530ad2012-07-03 16:12:28 -0700281
James E. Blairee743612012-05-29 14:49:32 -0700282class Change(object):
James E. Blair32663402012-06-01 10:04:18 -0700283 def __init__(self, queue_name, project, event):
James E. Blairee743612012-05-29 14:49:32 -0700284 self.queue_name = queue_name
285 self.project = project
James E. Blair32663402012-06-01 10:04:18 -0700286 self.branch = None
287 self.number = None
Clark Boylanfc56df32012-06-28 15:25:57 -0700288 self.url = None
James E. Blair32663402012-06-01 10:04:18 -0700289 self.patchset = None
290 self.refspec = None
291 self.ref = None
292 self.oldrev = None
293 self.newrev = None
294
295 if event.change_number:
296 self.branch = event.branch
297 self.number = event.change_number
Clark Boylanfc56df32012-06-28 15:25:57 -0700298 self.url = event.change_url
James E. Blair32663402012-06-01 10:04:18 -0700299 self.patchset = event.patch_number
300 self.refspec = event.refspec
301 if event.ref:
302 self.ref = event.ref
303 self.oldrev = event.oldrev
304 self.newrev = event.newrev
305
James E. Blair7e530ad2012-07-03 16:12:28 -0700306 self.build_sets = []
James E. Blairee743612012-05-29 14:49:32 -0700307 self.change_ahead = None
308 self.change_behind = None
James E. Blair11700c32012-07-05 17:50:05 -0700309 self.current_build_set = BuildSet(self)
310 self.build_sets.append(self.current_build_set)
James E. Blairee743612012-05-29 14:49:32 -0700311
James E. Blair32663402012-06-01 10:04:18 -0700312 def _id(self):
313 if self.number:
314 return '%s,%s' % (self.number, self.patchset)
315 return self.newrev
316
James E. Blair9f9667e2012-06-12 17:51:08 -0700317 def __repr__(self):
James E. Blair32663402012-06-01 10:04:18 -0700318 return '<Change 0x%x %s>' % (id(self), self._id())
James E. Blairee743612012-05-29 14:49:32 -0700319
Clark Boylan69878832012-06-21 16:25:39 -0700320 def formatStatus(self, indent=0, html=False):
James E. Blair1e8dd892012-05-30 09:15:05 -0700321 indent_str = ' ' * indent
James E. Blairee743612012-05-29 14:49:32 -0700322 ret = ''
Clark Boylanfc56df32012-06-28 15:25:57 -0700323 if html and self.url is not None:
324 ret += '%sProject %s change <a href="%s">%s</a>\n' % (indent_str,
325 self.project.name,
326 self.url,
327 self._id())
328 else:
329 ret += '%sProject %s change %s\n' % (indent_str,
330 self.project.name,
331 self._id())
James E. Blairee743612012-05-29 14:49:32 -0700332 for job in self.project.getJobs(self.queue_name):
James E. Blair7e530ad2012-07-03 16:12:28 -0700333 build = self.current_build_set.getBuild(job.name)
334 if build:
335 result = build.result
336 else:
337 result = None
Clark Boylan69878832012-06-21 16:25:39 -0700338 job_name = job.name
339 if html:
Clark Boylan69878832012-06-21 16:25:39 -0700340 if build:
341 url = build.url
342 else:
James E. Blair7e530ad2012-07-03 16:12:28 -0700343 url = None
Clark Boylan69878832012-06-21 16:25:39 -0700344 if url is not None:
345 job_name = '<a href="%s">%s</a>' % (url, job_name)
346 ret += '%s %s: %s' % (indent_str, job_name, result)
347 ret += '\n'
James E. Blairee743612012-05-29 14:49:32 -0700348 if self.change_ahead:
349 ret += '%sWaiting on:\n' % (indent_str)
Clark Boylan69878832012-06-21 16:25:39 -0700350 ret += self.change_ahead.formatStatus(indent + 2, html)
James E. Blairee743612012-05-29 14:49:32 -0700351 return ret
352
353 def formatReport(self):
354 ret = ''
355 if self.didAllJobsSucceed():
356 ret += 'Build successful\n\n'
357 else:
358 ret += 'Build failed\n\n'
James E. Blair1e8dd892012-05-30 09:15:05 -0700359
James E. Blairee743612012-05-29 14:49:32 -0700360 for job in self.project.getJobs(self.queue_name):
James E. Blair7e530ad2012-07-03 16:12:28 -0700361 build = self.current_build_set.getBuild(job.name)
362 result = build.result
363 url = build.url
364 if not url:
365 url = job.name
James E. Blairee743612012-05-29 14:49:32 -0700366 ret += '- %s : %s\n' % (url, result)
367 return ret
368
James E. Blair11700c32012-07-05 17:50:05 -0700369 def setReportedResult(self, result):
370 self.current_build_set.result = result
371
James E. Blairee743612012-05-29 14:49:32 -0700372 def resetAllBuilds(self):
James E. Blair11700c32012-07-05 17:50:05 -0700373 old = self.current_build_set
374 self.current_build_set.result = 'CANCELED'
375 self.current_build_set = BuildSet(self)
376 old.next_build_set = self.current_build_set
377 self.current_build_set.previous_build_set = old
James E. Blair7e530ad2012-07-03 16:12:28 -0700378 self.build_sets.append(self.current_build_set)
James E. Blairee743612012-05-29 14:49:32 -0700379
380 def addBuild(self, build):
James E. Blair7e530ad2012-07-03 16:12:28 -0700381 self.current_build_set.addBuild(build)
James E. Blairee743612012-05-29 14:49:32 -0700382
383 def setResult(self, build):
James E. Blairee743612012-05-29 14:49:32 -0700384 if build.result != 'SUCCESS':
385 # Get a JobTree from a Job so we can find only its dependent jobs
386 root = self.project.getJobTreeForQueue(self.queue_name)
387 tree = root.getJobTreeForJob(build.job)
388 for job in tree.getJobs():
James E. Blair7e530ad2012-07-03 16:12:28 -0700389 fakebuild = Build(job, None)
390 fakebuild.result = 'SKIPPED'
391 self.addBuild(fakebuild)
James E. Blairee743612012-05-29 14:49:32 -0700392
393 def _findJobsToRun(self, job_trees):
394 torun = []
395 for tree in job_trees:
396 job = tree.job
James E. Blair7e530ad2012-07-03 16:12:28 -0700397 result = None
James E. Blairee743612012-05-29 14:49:32 -0700398 if job:
James E. Blair7e530ad2012-07-03 16:12:28 -0700399 build = self.current_build_set.getBuild(job.name)
400 if build:
401 result = build.result
402 else:
403 # There is no build for the root of this job tree,
404 # so we should run it.
James E. Blairee743612012-05-29 14:49:32 -0700405 torun.append(job)
James E. Blair7e530ad2012-07-03 16:12:28 -0700406 # If there is no job, this is a null job tree, and we should
407 # run all of its jobs.
408 if result == 'SUCCESS' or not job:
James E. Blairee743612012-05-29 14:49:32 -0700409 torun.extend(self._findJobsToRun(tree.job_trees))
410 return torun
411
412 def findJobsToRun(self):
413 tree = self.project.getJobTreeForQueue(self.queue_name)
James E. Blair9d43ac12012-06-04 14:15:42 -0700414 if not tree:
415 return []
James E. Blairee743612012-05-29 14:49:32 -0700416 return self._findJobsToRun(tree.job_trees)
417
418 def areAllJobsComplete(self):
419 tree = self.project.getJobTreeForQueue(self.queue_name)
420 for job in tree.getJobs():
James E. Blair7e530ad2012-07-03 16:12:28 -0700421 build = self.current_build_set.getBuild(job.name)
422 if not build or not build.result:
James E. Blairee743612012-05-29 14:49:32 -0700423 return False
424 return True
425
426 def didAllJobsSucceed(self):
James E. Blair7e530ad2012-07-03 16:12:28 -0700427 tree = self.project.getJobTreeForQueue(self.queue_name)
428 for job in tree.getJobs():
429 build = self.current_build_set.getBuild(job.name)
430 if not build:
431 return False
432 if build.result != 'SUCCESS':
James E. Blairee743612012-05-29 14:49:32 -0700433 return False
434 return True
435
436 def delete(self):
437 if self.change_behind:
438 self.change_behind.change_ahead = None
439
James E. Blair1e8dd892012-05-30 09:15:05 -0700440
James E. Blairee743612012-05-29 14:49:32 -0700441class TriggerEvent(object):
442 def __init__(self):
443 self.data = None
James E. Blair32663402012-06-01 10:04:18 -0700444 # common
James E. Blairee743612012-05-29 14:49:32 -0700445 self.type = None
446 self.project_name = None
James E. Blair32663402012-06-01 10:04:18 -0700447 # patchset-created, comment-added, etc.
James E. Blairee743612012-05-29 14:49:32 -0700448 self.change_number = None
Clark Boylanfc56df32012-06-28 15:25:57 -0700449 self.change_url = None
James E. Blairee743612012-05-29 14:49:32 -0700450 self.patch_number = None
James E. Blaira03262c2012-05-30 09:41:16 -0700451 self.refspec = None
James E. Blairee743612012-05-29 14:49:32 -0700452 self.approvals = []
453 self.branch = None
Clark Boylanb9bcb402012-06-29 17:44:05 -0700454 self.comment = None
James E. Blair32663402012-06-01 10:04:18 -0700455 # ref-updated
James E. Blairee743612012-05-29 14:49:32 -0700456 self.ref = None
James E. Blair32663402012-06-01 10:04:18 -0700457 self.oldrev = None
458 self.newrew = None
James E. Blairee743612012-05-29 14:49:32 -0700459
James E. Blair9f9667e2012-06-12 17:51:08 -0700460 def __repr__(self):
James E. Blairee743612012-05-29 14:49:32 -0700461 ret = '<TriggerEvent %s %s' % (self.type, self.project_name)
James E. Blair1e8dd892012-05-30 09:15:05 -0700462
James E. Blairee743612012-05-29 14:49:32 -0700463 if self.branch:
464 ret += " %s" % self.branch
465 if self.change_number:
466 ret += " %s,%s" % (self.change_number, self.patch_number)
467 if self.approvals:
James E. Blair1e8dd892012-05-30 09:15:05 -0700468 ret += ' ' + ', '.join(
469 ['%s:%s' % (a['type'], a['value']) for a in self.approvals])
James E. Blairee743612012-05-29 14:49:32 -0700470 ret += '>'
471
472 return ret
473
James E. Blair1e8dd892012-05-30 09:15:05 -0700474
James E. Blairee743612012-05-29 14:49:32 -0700475class EventFilter(object):
Clark Boylanb9bcb402012-06-29 17:44:05 -0700476 def __init__(self, types=[], branches=[], refs=[], approvals=[],
477 comment_filters=[]):
James E. Blairee743612012-05-29 14:49:32 -0700478 self._types = types
479 self._branches = branches
480 self._refs = refs
481 self.types = [re.compile(x) for x in types]
482 self.branches = [re.compile(x) for x in branches]
483 self.refs = [re.compile(x) for x in refs]
Clark Boylanb9bcb402012-06-29 17:44:05 -0700484 self.comment_filters = [re.compile(x) for x in comment_filters]
James E. Blairee743612012-05-29 14:49:32 -0700485 self.approvals = approvals
486
James E. Blair9f9667e2012-06-12 17:51:08 -0700487 def __repr__(self):
James E. Blairee743612012-05-29 14:49:32 -0700488 ret = '<EventFilter'
James E. Blair1e8dd892012-05-30 09:15:05 -0700489
James E. Blairee743612012-05-29 14:49:32 -0700490 if self._types:
491 ret += ' types: %s' % ', '.join(self._types)
492 if self._branches:
493 ret += ' branches: %s' % ', '.join(self._branches)
494 if self._refs:
495 ret += ' refs: %s' % ', '.join(self._refs)
496 if self.approvals:
James E. Blair1e8dd892012-05-30 09:15:05 -0700497 ret += ' approvals: %s' % ', '.join(
498 ['%s:%s' % a for a in self.approvals.items()])
James E. Blairee743612012-05-29 14:49:32 -0700499 ret += '>'
500
501 return ret
502
503 def matches(self, event):
504 def normalizeCategory(name):
505 name = name.lower()
506 return re.sub(' ', '-', name)
507
508 # event types are ORed
509 matches_type = False
510 for etype in self.types:
511 if etype.match(event.type):
512 matches_type = True
513 if self.types and not matches_type:
514 return False
515
516 # branches are ORed
517 matches_branch = False
518 for branch in self.branches:
519 if branch.match(event.branch):
520 matches_branch = True
521 if self.branches and not matches_branch:
522 return False
523
524 # refs are ORed
525 matches_ref = False
526 for ref in self.refs:
527 if ref.match(event.ref):
528 matches_ref = True
529 if self.refs and not matches_ref:
530 return False
531
Clark Boylanb9bcb402012-06-29 17:44:05 -0700532 # comment_filters are ORed
533 matches_comment_filter = False
534 for comment_filter in self.comment_filters:
535 if (event.comment is not None and
536 comment_filter.search(event.comment)):
537 matches_comment_filter = True
538 if self.comment_filters and not matches_comment_filter:
539 return False
540
James E. Blairee743612012-05-29 14:49:32 -0700541 # approvals are ANDed
542 for category, value in self.approvals.items():
543 matches_approval = False
544 for eapproval in event.approvals:
545 if (normalizeCategory(eapproval['description']) == category and
546 int(eapproval['value']) == int(value)):
547 matches_approval = True
James E. Blair1e8dd892012-05-30 09:15:05 -0700548 if not matches_approval:
549 return False
James E. Blairee743612012-05-29 14:49:32 -0700550 return True