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