1
2
3 from zope.interface import implements
4 from twisted.python import log, components
5 import urllib
6
7 import time, locale
8 import operator
9
10 from buildbot import interfaces, util
11 from buildbot.status import builder
12
13 from buildbot.status.web.base import Box, HtmlResource, IBox, ICurrentBox, \
14 ITopBox, build_get_class, path_to_build, path_to_step, path_to_root, \
15 map_branches
16
17
19
20 if old:
21 if new < old:
22 return new
23 return old
24 return new
25
27
28 if old:
29 if new > old:
30 return new
31 return old
32 return new
33
34
36
37 implements(ICurrentBox)
38
54
56
57 state, builds = self.original.getState()
58
59
60
61
62
63
64 upcoming = []
65 builderName = self.original.getName()
66 for s in status.getSchedulers():
67 if builderName in s.listBuilderNames():
68 upcoming.extend(s.getPendingBuildTimes())
69 if state == "idle" and upcoming:
70 state = "waiting"
71
72 if state == "building":
73 text = ["building"]
74 if builds:
75 for b in builds:
76 eta = b.getETA()
77 text.extend(self.formatETA("ETA in", eta))
78 elif state == "offline":
79 text = ["offline"]
80 elif state == "idle":
81 text = ["idle"]
82 elif state == "waiting":
83 text = ["waiting"]
84 else:
85
86 text = [state]
87
88
89
90
91
92
93
94 pbs = self.original.getPendingBuilds()
95 if pbs:
96 text.append("%d pending" % len(pbs))
97 for t in upcoming:
98 eta = t - util.now()
99 text.extend(self.formatETA("next in", eta))
100 return Box(text, class_="Activity " + state)
101
102 components.registerAdapter(CurrentBox, builder.BuilderStatus, ICurrentBox)
103
104
106
107
108 implements(IBox)
109
126 components.registerAdapter(BuildTopBox, builder.BuilderStatus, ITopBox)
127
129
130 implements(IBox)
131
145 components.registerAdapter(BuildBox, builder.BuildStatus, IBox)
146
148 implements(IBox)
149
151 urlbase = path_to_step(req, self.original)
152 text = self.original.getText()
153 if text is None:
154 log.msg("getText() gave None", urlbase)
155 text = []
156 text = text[:]
157 logs = self.original.getLogs()
158
159 cxt = dict(text=text, logs=[], urls=[])
160
161 for num in range(len(logs)):
162 name = logs[num].getName()
163 if logs[num].hasContents():
164 url = urlbase + "/logs/%s" % urllib.quote(name)
165 else:
166 url = None
167 cxt['logs'].append(dict(name=name, url=url))
168
169 for name, target in self.original.getURLs().items():
170 cxt['urls'].append(dict(link=target,name=name))
171
172 template = req.site.buildbot_service.templates.get_template("box_macros.html")
173 text = template.module.step_box(**cxt)
174
175 class_ = "BuildStep " + build_get_class(self.original)
176 return Box(text, class_=class_)
177 components.registerAdapter(StepBox, builder.BuildStepStatus, IBox)
178
179
181 implements(IBox)
182
184 text = self.original.getText()
185 class_ = "Event"
186 return Box(text, class_=class_)
187 components.registerAdapter(EventBox, builder.Event, IBox)
188
189
201
203 implements(IBox)
204
206
207 b = Box([])
208 b.spacer = True
209 return b
210 components.registerAdapter(SpacerBox, Spacer, IBox)
211
212 -def insertGaps(g, showEvents, lastEventTime, idleGap=2):
213 debug = False
214
215 e = g.next()
216 starts, finishes = e.getTimes()
217 if debug: log.msg("E0", starts, finishes)
218 if finishes == 0:
219 finishes = starts
220 if debug: log.msg("E1 finishes=%s, gap=%s, lET=%s" % \
221 (finishes, idleGap, lastEventTime))
222 if finishes is not None and finishes + idleGap < lastEventTime:
223 if debug: log.msg(" spacer0")
224 yield Spacer(finishes, lastEventTime)
225
226 followingEventStarts = starts
227 if debug: log.msg(" fES0", starts)
228 yield e
229
230 while 1:
231 e = g.next()
232 if not showEvents and isinstance(e, builder.Event):
233 continue
234 starts, finishes = e.getTimes()
235 if debug: log.msg("E2", starts, finishes)
236 if finishes == 0:
237 finishes = starts
238 if finishes is not None and finishes + idleGap < followingEventStarts:
239
240
241
242 if debug:
243 log.msg(" finishes=%s, gap=%s, fES=%s" % \
244 (finishes, idleGap, followingEventStarts))
245 yield Spacer(finishes, followingEventStarts)
246 yield e
247 followingEventStarts = starts
248 if debug: log.msg(" fES1", starts)
249
250
252 title = "Waterfall Help"
253
257
258 - def content(self, request, cxt):
259 status = self.getStatus(request)
260
261 cxt['show_events_checked'] = request.args.get("show_events", ["false"])[0].lower() == "true"
262 cxt['branches'] = [b for b in request.args.get("branch", []) if b]
263 cxt['failures_only'] = request.args.get("failures_only", ["false"])[0].lower() == "true"
264 cxt['committers'] = [c for c in request.args.get("committer", []) if c]
265
266
267
268 show_builders = request.args.get("show", [])
269 show_builders.extend(request.args.get("builder", []))
270 cxt['show_builders'] = show_builders
271 cxt['all_builders'] = status.getBuilderNames(categories=self.categories)
272
273
274
275 times = [("none", "None"),
276 ("60", "60 seconds"),
277 ("300", "5 minutes"),
278 ("600", "10 minutes"),
279 ]
280 current_reload_time = request.args.get("reload", ["none"])
281 if current_reload_time:
282 current_reload_time = current_reload_time[0]
283 if current_reload_time not in [t[0] for t in times]:
284 times.insert(0, (current_reload_time, current_reload_time) )
285
286 cxt['times'] = times
287 cxt['current_reload_time'] = current_reload_time
288
289 template = request.site.buildbot_service.templates.get_template("waterfallhelp.html")
290 return template.render(**cxt)
291
292
294 """This builds the main status page, with the waterfall display, and
295 all child pages."""
296
297 - def __init__(self, categories=None, num_events=200, num_events_max=None):
303
311
315
317 if "reload" in request.args:
318 try:
319 reload_time = int(request.args["reload"][0])
320 return max(reload_time, 15)
321 except ValueError:
322 pass
323 return None
324
352
353 - def content(self, request, ctx):
354 status = self.getStatus(request)
355 ctx['refresh'] = self.get_reload_time(request)
356
357
358
359
360 allBuilderNames = status.getBuilderNames(categories=self.categories)
361 builders = [status.getBuilder(name) for name in allBuilderNames]
362
363
364
365
366
367 showBuilders = request.args.get("show", [])
368 showBuilders.extend(request.args.get("builder", []))
369 if showBuilders:
370 builders = [b for b in builders if b.name in showBuilders]
371
372
373
374
375 showCategories = request.args.get("category", [])
376 if showCategories:
377 builders = [b for b in builders if b.category in showCategories]
378
379
380
381
382 failuresOnly = request.args.get("failures_only", ["false"])[0]
383 if failuresOnly.lower() == "true":
384 builders = [b for b in builders if not self.isSuccess(b)]
385
386 (changeNames, builderNames, timestamps, eventGrid, sourceEvents) = \
387 self.buildGrid(request, builders)
388
389
390 locale_enc = locale.getdefaultlocale()[1]
391 if locale_enc is not None:
392 locale_tz = unicode(time.tzname[time.localtime()[-1]], locale_enc)
393 else:
394 locale_tz = unicode(time.tzname[time.localtime()[-1]])
395 ctx['tz'] = locale_tz
396 ctx['changes_url'] = request.childLink("../changes")
397
398 bn = ctx['builders'] = []
399
400 for name in builderNames:
401 builder = status.getBuilder(name)
402 top_box = ITopBox(builder).getBox(request)
403 current_box = ICurrentBox(builder).getBox(status)
404 bn.append({'name': name,
405 'url': request.childLink("../builders/%s" % urllib.quote(name, safe='')),
406 'top': top_box.text,
407 'top_class': top_box.class_,
408 'status': current_box.text,
409 'status_class': current_box.class_,
410 })
411
412 ctx.update(self.phase2(request, changeNames + builderNames, timestamps, eventGrid,
413 sourceEvents))
414
415 def with_args(req, remove_args=[], new_args=[], new_path=None):
416
417 newargs = req.args.copy()
418 for argname in remove_args:
419 newargs[argname] = []
420 if "branch" in newargs:
421 newargs["branch"] = [b for b in newargs["branch"] if b]
422 for k,v in new_args:
423 if k in newargs:
424 newargs[k].append(v)
425 else:
426 newargs[k] = [v]
427 newquery = "&".join(["%s=%s" % (urllib.quote(k), urllib.quote(v))
428 for k in newargs
429 for v in newargs[k]
430 ])
431 if new_path:
432 new_url = new_path
433 elif req.prepath:
434 new_url = req.prepath[-1]
435 else:
436 new_url = ''
437 if newquery:
438 new_url += "?" + newquery
439 return new_url
440
441 if timestamps:
442 bottom = timestamps[-1]
443 ctx['nextpage'] = with_args(request, ["last_time"],
444 [("last_time", str(int(bottom)))])
445
446
447 helpurl = path_to_root(request) + "waterfall/help"
448 ctx['help_url'] = with_args(request, new_path=helpurl)
449
450 if self.get_reload_time(request) is not None:
451 ctx['no_reload_page'] = with_args(request, remove_args=["reload"])
452
453 template = request.site.buildbot_service.templates.get_template("waterfall.html")
454 data = template.render(**ctx)
455 return data
456
458 debug = False
459
460
461 showEvents = False
462 if request.args.get("show_events", ["false"])[0].lower() == "true":
463 showEvents = True
464 filterCategories = request.args.get('category', [])
465 filterBranches = [b for b in request.args.get("branch", []) if b]
466 filterBranches = map_branches(filterBranches)
467 filterCommitters = [c for c in request.args.get("committer", []) if c]
468 maxTime = int(request.args.get("last_time", [util.now()])[0])
469 if "show_time" in request.args:
470 minTime = maxTime - int(request.args["show_time"][0])
471 elif "first_time" in request.args:
472 minTime = int(request.args["first_time"][0])
473 elif filterBranches or filterCommitters:
474 minTime = util.now() - 24 * 60 * 60
475 else:
476 minTime = 0
477 spanLength = 10
478 req_events=int(request.args.get("num_events", [self.num_events])[0])
479 if self.num_events_max and req_events > self.num_events_max:
480 maxPageLen = self.num_events_max
481 else:
482 maxPageLen = req_events
483
484
485
486
487
488 commit_source = self.getChangeManager(request)
489
490 lastEventTime = util.now()
491 sources = [commit_source] + builders
492 changeNames = ["changes"]
493 builderNames = map(lambda builder: builder.getName(), builders)
494 sourceNames = changeNames + builderNames
495 sourceEvents = []
496 sourceGenerators = []
497
498 def get_event_from(g):
499 try:
500 while True:
501 e = g.next()
502
503
504
505
506
507 if not showEvents and isinstance(e, builder.Event):
508 continue
509 break
510 event = interfaces.IStatusEvent(e)
511 if debug:
512 log.msg("gen %s gave1 %s" % (g, event.getText()))
513 except StopIteration:
514 event = None
515 return event
516
517 for s in sources:
518 gen = insertGaps(s.eventGenerator(filterBranches,
519 filterCategories,
520 filterCommitters,
521 minTime),
522 showEvents,
523 lastEventTime)
524 sourceGenerators.append(gen)
525
526 sourceEvents.append(get_event_from(gen))
527 eventGrid = []
528 timestamps = []
529
530 lastEventTime = 0
531 for e in sourceEvents:
532 if e and e.getTimes()[0] > lastEventTime:
533 lastEventTime = e.getTimes()[0]
534 if lastEventTime == 0:
535 lastEventTime = util.now()
536
537 spanStart = lastEventTime - spanLength
538 debugGather = 0
539
540 while 1:
541 if debugGather: log.msg("checking (%s,]" % spanStart)
542
543
544
545
546
547
548
549 spanEvents = []
550 firstTimestamp = None
551 lastTimestamp = None
552
553 for c in range(len(sourceGenerators)):
554 events = []
555 event = sourceEvents[c]
556 while event and spanStart < event.getTimes()[0]:
557
558
559 if not IBox(event, None):
560 log.msg("BAD EVENT", event, event.getText())
561 assert 0
562 if debug:
563 log.msg("pushing", event.getText(), event)
564 events.append(event)
565 starts, finishes = event.getTimes()
566 firstTimestamp = earlier(firstTimestamp, starts)
567 event = get_event_from(sourceGenerators[c])
568 if debug:
569 log.msg("finished span")
570
571 if event:
572
573 lastTimestamp = later(lastTimestamp,
574 event.getTimes()[0])
575 if debugGather:
576 log.msg(" got %s from %s" % (events, sourceNames[c]))
577 sourceEvents[c] = event
578 spanEvents.append(events)
579
580
581
582
583 if firstTimestamp is not None and firstTimestamp <= maxTime:
584 eventGrid.append(spanEvents)
585 timestamps.append(firstTimestamp)
586
587 if lastTimestamp:
588 spanStart = lastTimestamp - spanLength
589 else:
590
591 break
592 if minTime is not None and lastTimestamp < minTime:
593 break
594
595 if len(timestamps) > maxPageLen:
596 break
597
598
599
600
601
602 if debugGather: log.msg("finished loop")
603 assert(len(timestamps) == len(eventGrid))
604 return (changeNames, builderNames, timestamps, eventGrid, sourceEvents)
605
606 - def phase2(self, request, sourceNames, timestamps, eventGrid,
607 sourceEvents):
608
609 if not timestamps:
610 return dict(grid=[], gridlen=0)
611
612
613 grid = []
614 for i in range(1+len(sourceNames)):
615 grid.append([])
616
617
618
619 lastDate = time.strftime("%d %b %Y",
620 time.localtime(util.now()))
621 for r in range(0, len(timestamps)):
622 chunkstrip = eventGrid[r]
623
624
625 assert(len(chunkstrip) == len(sourceNames))
626 maxRows = reduce(lambda x,y: max(x,y),
627 map(lambda x: len(x), chunkstrip))
628 for i in range(maxRows):
629 if i != maxRows-1:
630 grid[0].append(None)
631 else:
632
633 stuff = []
634
635
636 todayday = time.strftime("%a",
637 time.localtime(timestamps[r]))
638 today = time.strftime("%d %b %Y",
639 time.localtime(timestamps[r]))
640 if today != lastDate:
641 stuff.append(todayday)
642 stuff.append(today)
643 lastDate = today
644 stuff.append(
645 time.strftime("%H:%M:%S",
646 time.localtime(timestamps[r])))
647 grid[0].append(Box(text=stuff, class_="Time",
648 valign="bottom", align="center"))
649
650
651
652 for c in range(0, len(chunkstrip)):
653 block = chunkstrip[c]
654 assert(block != None)
655 for i in range(maxRows - len(block)):
656
657 grid[c+1].append(None)
658 for i in range(len(block)):
659
660 b = IBox(block[i]).getBox(request)
661 b.parms['valign'] = "top"
662 b.parms['align'] = "center"
663 grid[c+1].append(b)
664
665
666 gridlen = len(grid[0])
667 for i in range(len(grid)):
668 strip = grid[i]
669 assert(len(strip) == gridlen)
670 if strip[-1] == None:
671 if sourceEvents[i-1]:
672 filler = IBox(sourceEvents[i-1]).getBox(request)
673 else:
674
675 filler = Box(text=["?"], align="center")
676 strip[-1] = filler
677 strip[-1].parms['rowspan'] = 1
678
679
680
681 noBubble = request.args.get("nobubble",['0'])
682 noBubble = int(noBubble[0])
683 if not noBubble:
684 for col in range(len(grid)):
685 strip = grid[col]
686 if col == 1:
687 for i in range(2, len(strip)+1):
688
689 if strip[-i] == None:
690 next = strip[-i+1]
691 assert(next)
692 if next:
693
694 if next.spacer:
695
696 strip[-i] = next
697 strip[-i].parms['rowspan'] += 1
698 strip[-i+1] = None
699 else:
700
701
702
703 strip[-i] = Box([], rowspan=1,
704 comment="commit bubble")
705 strip[-i].spacer = True
706 else:
707
708
709
710 pass
711 else:
712 for i in range(2, len(strip)+1):
713
714 if strip[-i] == None:
715
716 assert(strip[-i+1] != None)
717 strip[-i] = strip[-i+1]
718 strip[-i].parms['rowspan'] += 1
719 strip[-i+1] = None
720 else:
721 strip[-i].parms['rowspan'] = 1
722
723
724 for i in range(gridlen):
725 for strip in grid:
726 if strip[i]:
727 strip[i] = strip[i].td()
728
729 return dict(grid=grid, gridlen=gridlen, no_bubble=noBubble, time=lastDate)
730