blob: 16d234cafd2ff84249a7760239a0cd56bbe59824 [file] [log] [blame]
# Copyright (C) 2018-2019 Apple Inc. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
from __future__ import unicode_literals
import datetime
import re
from django.http import HttpResponse
from django.shortcuts import render
from django.utils import timezone
from django.views import View
from django.views.decorators.clickjacking import xframe_options_exempt
from ews.common.buildbot import Buildbot
from ews.models.build import Build
from ews.models.patch import Patch
import ews.config as config
class StatusBubble(View):
# These queue names are from shortname in https://trac.webkit.org/browser/webkit/trunk/Tools/BuildSlaveSupport/ews-build/config.json
# FIXME: Auto-generate this list https://bugs.webkit.org/show_bug.cgi?id=195640
ALL_QUEUES = ['style', 'ios', 'ios-sim', 'mac', 'mac-debug', 'gtk', 'wpe', 'wincairo',
'ios-wk2', 'mac-wk1', 'mac-wk2', 'mac-debug-wk1', 'api-ios', 'api-mac', 'bindings', 'jsc', 'webkitperl', 'webkitpy', 'win', 'services']
ENABLED_QUEUES = ['style', 'ios', 'ios-sim', 'mac', 'mac-debug', 'gtk', 'wpe', 'wincairo',
'ios-wk2', 'mac-wk1', 'mac-wk2', 'mac-debug-wk1', 'api-ios', 'api-mac', 'bindings', 'webkitperl', 'webkitpy', 'services']
# FIXME: Auto-generate the queue's trigger relationship
QUEUE_TRIGGERS = {
'api-ios': 'ios-sim',
'ios-wk2': 'ios-sim',
'api-mac': 'mac',
'mac-wk1': 'mac',
'mac-wk2': 'mac',
'mac-debug-wk1': 'mac-debug',
}
STEPS_TO_HIDE = ['^Archived built product$', '^Uploaded built product$', '^Transferred archive to S3$',
'^Archived test results$', '^Uploaded test results$', '^Extracted test results$',
'^Downloaded built product$', '^Extracted built product$',
'^Cleaned and updated working directory$', '^Checked out required revision$', '^Updated working directory$',
'^Validated patch$', '^Killed old processes$', '^Configured build$', '^OS:.*Xcode:', '(skipped)',
'^Printed configuration$', '^Checked patch relevance$']
DAYS_TO_CHECK = 3
BUILDER_ICON = u'\U0001f6e0'
TESTER_ICON = u'\U0001f52c'
def _build_bubble(self, patch, queue, hide_icons=False):
bubble = {
'name': queue,
}
if hide_icons == False:
if self._is_tester_queue(queue):
bubble['name'] = StatusBubble.TESTER_ICON + ' ' + bubble['name']
if self._is_builder_queue(queue):
bubble['name'] = StatusBubble.BUILDER_ICON + ' ' + bubble['name']
builds, is_parent_build = self.get_all_builds_for_queue(patch, queue, self._get_parent_queue(queue))
build = None
if builds:
build = builds[0]
if not self._should_show_bubble_for_build(build):
return None
if not build:
bubble['state'] = 'none'
queue_position = self._queue_position(patch, queue, self._get_parent_queue(queue))
bubble['queue_position'] = queue_position
if not queue_position:
return None
bubble['details_message'] = 'Waiting in queue, processing has not started yet.\n\nPosition in queue: {}'.format(queue_position)
return bubble
bubble['url'] = 'https://{}/#/builders/{}/builds/{}'.format(config.BUILDBOT_SERVER_HOST, build.builder_id, build.number)
builder_full_name = build.builder_name.replace('-', ' ')
if build.result is None: # In-progress build
if self._does_build_contains_any_failed_step(build):
bubble['state'] = 'provisional-fail'
else:
bubble['state'] = 'started'
bubble['details_message'] = 'Build is in-progress. Recent messages:' + self._steps_messages_from_multiple_builds(builds)
elif build.result == Buildbot.SUCCESS:
if is_parent_build:
if patch.modified < (timezone.now() - datetime.timedelta(days=StatusBubble.DAYS_TO_CHECK)):
# Do not display bubble for old patch for which no build has been reported on given queue.
# Most likely the patch would never be processed on this queue, since either the queue was
# added after the patch was submitted, or build request for that patch was cancelled.
return None
bubble['state'] = 'started'
bubble['details_message'] = 'Build is in-progress. Recent messages:' + self._steps_messages_from_multiple_builds(builds) + '\n\nWaiting to run tests.'
else:
bubble['state'] = 'pass'
bubble['details_message'] = 'Pass'
elif build.result == Buildbot.WARNINGS:
bubble['state'] = 'pass'
bubble['details_message'] = 'Warning' + self._steps_messages_from_multiple_builds(builds)
elif build.result == Buildbot.FAILURE:
bubble['state'] = 'fail'
bubble['details_message'] = self._most_recent_step_message(build)
elif build.result == Buildbot.SKIPPED:
bubble['state'] = 'none'
bubble['details_message'] = 'The patch is no longer eligible for processing.'
if re.search(r'Bug .* is already closed', build.state_string):
bubble['details_message'] += ' Bug was already closed when EWS attempted to process it.'
elif re.search(r'Patch .* is marked r-', build.state_string):
bubble['details_message'] += ' Patch was already marked r- when EWS attempted to process it.'
elif re.search(r'Patch .* is obsolete', build.state_string):
bubble['details_message'] += ' Patch was obsolete when EWS attempted to process it.'
elif build.result == Buildbot.EXCEPTION:
bubble['state'] = 'error'
bubble['details_message'] = 'An unexpected error occured. Recent messages:' + self._steps_messages_from_multiple_builds(builds)
elif build.result == Buildbot.RETRY:
bubble['state'] = 'provisional-fail'
bubble['details_message'] = 'Build is being retried. Recent messages:' + self._steps_messages_from_multiple_builds(builds)
elif build.result == Buildbot.CANCELLED:
bubble['state'] = 'fail'
bubble['details_message'] = 'Build was cancelled. Recent messages:' + self._steps_messages_from_multiple_builds(builds)
else:
bubble['state'] = 'error'
bubble['details_message'] = 'An unexpected error occured. Recent messages:' + self._steps_messages_from_multiple_builds(builds)
if 'details_message' in bubble:
bubble['details_message'] = builder_full_name + '\n\n' + bubble['details_message']
os_details = self.get_os_details(build)
timestamp = self.get_build_timestamp(build)
if os_details:
bubble['details_message'] += '\n\n' + os_details + '\n' + timestamp
else:
bubble['details_message'] += '\n\n' + timestamp
return bubble
def _is_tester_queue(self, queue):
icon = Buildbot.icons_for_queues_mapping.get(queue)
return icon in ['testOnly', 'buildAndTest']
def _is_builder_queue(self, queue):
icon = Buildbot.icons_for_queues_mapping.get(queue)
return icon in ['buildOnly', 'buildAndTest']
def _get_parent_queue(self, queue):
return StatusBubble.QUEUE_TRIGGERS.get(queue)
def get_os_details(self, build):
for step in build.step_set.all():
if step.state_string.startswith('OS:'):
return step.state_string
return ''
def get_build_timestamp(self, build):
if build.complete_at:
return self._iso_time(build.complete_at)
recent_build_step = build.step_set.last()
if recent_build_step:
return self._iso_time(recent_build_step.started_at)
return self._iso_time(build.started_at)
def _iso_time(self, time):
return '[[' + datetime.datetime.fromtimestamp(time).isoformat() + 'Z]]'
def _steps_messages(self, build):
return '\n'.join([step.state_string for step in build.step_set.all().order_by('uid') if self._should_display_step(step)])
def _steps_messages_from_multiple_builds(self, builds):
message = ''
for build in reversed(builds):
message += '\n\n' + self._steps_messages(build)
return message
def _should_display_step(self, step):
return not filter(lambda step_to_hide: re.search(step_to_hide, step.state_string), StatusBubble.STEPS_TO_HIDE)
def _does_build_contains_any_failed_step(self, build):
for step in build.step_set.all():
if step.result and step.result != Buildbot.SUCCESS and step.result != Buildbot.WARNINGS and step.result != Buildbot.SKIPPED:
return True
return False
def _most_recent_step_message(self, build):
recent_step = build.step_set.last()
if not recent_step:
return ''
return recent_step.state_string
def get_latest_build_for_queue(self, patch, queue, parent_queue=None):
builds, is_parent_build = self.get_all_builds_for_queue(patch, queue, parent_queue)
if not builds:
return (None, None)
return (builds[0], is_parent_build)
def get_all_builds_for_queue(self, patch, queue, parent_queue=None):
builds = self.get_builds_for_queue(patch, queue)
is_parent_build = False
if not builds and parent_queue:
builds = self.get_builds_for_queue(patch, parent_queue)
is_parent_build = True
if not builds:
return (None, None)
builds.sort(key=lambda build: build.started_at, reverse=True)
return (builds, is_parent_build)
def get_builds_for_queue(self, patch, queue):
return [build for build in patch.build_set.all() if build.builder_display_name == queue]
def find_failed_builds_for_patch(self, patch_id):
patch = Patch.get_patch(patch_id)
if not patch:
return []
failed_builds = []
for queue in StatusBubble.ALL_QUEUES:
build, _ = self.get_latest_build_for_queue(patch, queue)
if not build:
continue
if build.result in (Buildbot.FAILURE, Buildbot.EXCEPTION, Buildbot.CANCELLED):
failed_builds.append(build)
return failed_builds
def _should_show_bubble_for_build(self, build):
if build and build.result == Buildbot.SKIPPED and re.search(r'Patch .* doesn\'t have relevant changes', build.state_string):
return False
return True
def _should_show_bubble_for_queue(self, queue):
return queue in StatusBubble.ENABLED_QUEUES
def _queue_position(self, patch, queue, parent_queue=None):
# FIXME: Handle retried builds and cancelled build-requests as well.
from_timestamp = timezone.now() - datetime.timedelta(days=StatusBubble.DAYS_TO_CHECK)
if patch.modified < from_timestamp:
# Do not display bubble for old patch for which no build has been reported on given queue.
# Most likely the patch would never be processed on this queue, since either the queue was
# added after the patch was submitted, or build request for that patch was cancelled.
return None
previously_sent_patches = set(Patch.objects
.filter(modified__gte=from_timestamp)
.filter(sent_to_buildbot=True)
.filter(obsolete=False)
.filter(modified__lt=patch.modified))
if parent_queue:
recent_builds_parent_queue = Build.objects \
.filter(modified__gte=from_timestamp) \
.filter(builder_display_name=parent_queue)
processed_patches_parent_queue = set([build.patch for build in recent_builds_parent_queue])
return len(previously_sent_patches - processed_patches_parent_queue) + 1
recent_builds = Build.objects \
.filter(modified__gte=from_timestamp) \
.filter(builder_display_name=queue)
processed_patches = set([build.patch for build in recent_builds])
return len(previously_sent_patches - processed_patches) + 1
def _build_bubbles_for_patch(self, patch, hide_icons=False):
show_submit_to_ews = True
failed_to_apply = False # TODO: https://bugs.webkit.org/show_bug.cgi?id=194598
bubbles = []
if not (patch and patch.sent_to_buildbot):
return (None, show_submit_to_ews, failed_to_apply)
for queue in StatusBubble.ALL_QUEUES:
if not self._should_show_bubble_for_queue(queue):
continue
bubble = self._build_bubble(patch, queue, hide_icons)
if bubble:
show_submit_to_ews = False
bubbles.append(bubble)
return (bubbles, show_submit_to_ews, failed_to_apply)
@xframe_options_exempt
def get(self, request, patch_id):
hide_icons = request.GET.get('hide_icons', False)
patch_id = int(patch_id)
patch = Patch.get_patch(patch_id)
bubbles, show_submit_to_ews, show_failure_to_apply = self._build_bubbles_for_patch(patch, hide_icons)
template_values = {
'bubbles': bubbles,
'patch_id': patch_id,
'show_submit_to_ews': show_submit_to_ews,
'show_failure_to_apply': show_failure_to_apply,
}
return render(request, 'statusbubble.html', template_values)