blob: dcceceb9a1e5dacca52007db56c8138dc30bb865 [file] [log] [blame]
# Copyright (C) 2018-2021 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 logging
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
_log = logging.getLogger(__name__)
class StatusBubble(View):
# These queue names are from shortname in https://trac.webkit.org/browser/webkit/trunk/Tools/CISupport/ews-build/config.json
# FIXME: Auto-generate this list https://bugs.webkit.org/show_bug.cgi?id=195640
# Note: This list is sorted in the order of which bubbles appear in bugzilla.
ALL_QUEUES = ['style', 'ios', 'ios-sim', 'mac', 'mac-debug', 'mac-AS-debug', 'tv', 'tv-sim', 'watch', 'watch-sim', 'gtk', 'wpe', 'wincairo', 'win',
'ios-wk2', 'mac-wk1', 'mac-wk2', 'mac-wk2-stress', 'mac-debug-wk1', 'mac-AS-debug-wk2', 'api-ios', 'api-mac', 'api-gtk',
'bindings', 'jsc', 'jsc-armv7', 'jsc-armv7-tests', 'jsc-mips', 'jsc-mips-tests', 'jsc-i386', '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-wk2-stress': 'mac',
'mac-debug-wk1': 'mac-debug',
'mac-AS-debug-wk2': 'mac-AS-debug',
'api-gtk': 'gtk',
'jsc-mips-tests': 'jsc-mips',
'jsc-armv7-tests': 'jsc-armv7',
}
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$',
'^Crash collection has quiesced$', '^Triggered crash log submission$',
'^Cleaned and updated working directory$', '^Checked out required revision$', '^Updated working directory$',
'^Validated patch$', '^Killed old processes$', '^Configured build$', '^OS:.*Xcode:', '(skipped)',
'^Printed configuration$', '^Patch contains relevant changes$', '^Deleted .git/index.lock$',
'^triggered.*$', '^Found modified ChangeLogs$', '^Created local git commit$', '^Set build summary$',
'^Validated commiter$', '^Validated commiter and reviewer$', '^Validated ChangeLog and Reviewer$',
'^Removed flags on bugzilla patch$', '^Checked patch status on other queues$', '^Identifier:.*$',
'^Updated branch information$', '^worker .* ready$']
DAYS_TO_CHECK_QUEUE_POSITION = 0.5
DAYS_TO_HIDE_BUBBLE = 7
BUILDER_ICON = u'\U0001f6e0'
TESTER_ICON = u'\U0001f9ea'
BUILD_RETRY_MSG = 'retrying build'
UNKNOWN_QUEUE_POSITION = '?'
def _build_bubble(self, patch, queue, hide_icons=False):
bubble = {
'name': queue,
}
is_tester_queue = self._is_tester_queue(queue)
is_builder_queue = self._is_builder_queue(queue)
if hide_icons == False:
if is_tester_queue:
bubble['name'] = StatusBubble.TESTER_ICON + ' ' + bubble['name']
if is_builder_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]
builds = builds[:10] # Limit number of builds to display in status-bubble hover over message
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))
if not queue_position:
return None
if queue_position != StatusBubble.UNKNOWN_QUEUE_POSITION:
bubble['queue_position'] = queue_position
if self._get_parent_queue(queue):
queue = self._get_parent_queue(queue)
queue_full_name = Buildbot.queue_name_by_shortname_mapping.get(queue)
if queue_full_name:
bubble['url'] = 'https://{}/#/builders/{}'.format(config.BUILDBOT_SERVER_HOST, queue_full_name)
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.retried:
bubble['state'] = 'started'
bubble['details_message'] = 'Waiting for available bot to retry the build.'
bubble['url'] = None
queue_full_name = Buildbot.queue_name_by_shortname_mapping.get(queue)
if queue_full_name:
bubble['url'] = 'https://{}/#/builders/{}'.format(config.BUILDBOT_SERVER_HOST, queue_full_name)
elif build.result == Buildbot.SUCCESS:
if is_parent_build:
if patch.created < (timezone.now() - datetime.timedelta(days=StatusBubble.DAYS_TO_HIDE_BUBBLE)):
# 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'] = 'Waiting to run tests.'
queue_full_name = Buildbot.queue_name_by_shortname_mapping.get(queue)
if queue_full_name:
bubble['url'] = 'https://{}/#/builders/{}'.format(config.BUILDBOT_SERVER_HOST, queue_full_name)
builder_full_name = queue_full_name.replace('-', ' ')
else:
bubble['state'] = 'pass'
if is_builder_queue and is_tester_queue:
bubble['details_message'] = 'Built successfully and passed tests'
elif is_builder_queue:
bubble['details_message'] = 'Built successfully'
elif is_tester_queue:
if queue == 'style':
bubble['details_message'] = 'Passed style check'
else:
bubble['details_message'] = 'Passed tests'
else:
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_failure_message(build)
if StatusBubble.BUILD_RETRY_MSG in bubble['details_message']:
bubble['state'] = 'provisional-fail'
elif build.result == Buildbot.SKIPPED:
bubble['state'] = 'skipped'
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.'
if len(builds) > 1:
bubble['details_message'] += '\nSome messages were logged while the patch was still eligible:'
bubble['details_message'] += self._steps_messages_from_multiple_builds(builds)
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'] = 'cancelled'
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_failure_message(self, build):
for step in build.step_set.all().order_by('-uid'):
if step.result == Buildbot.SUCCESS and StatusBubble.BUILD_RETRY_MSG in step.state_string:
return step.state_string
if step.result == Buildbot.FAILURE:
return step.state_string
return ''
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.number, 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 _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_QUEUE_POSITION)
hide_from_timestamp = timezone.now() - datetime.timedelta(days=StatusBubble.DAYS_TO_HIDE_BUBBLE)
if patch.created < hide_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
if patch.created < from_timestamp:
# This means patch has been waiting on given queue for long time, but not long enough to hide the status-bubble.
# Instead of calculating exact queue position (which might be slow), we display a fixed high queue position.
return StatusBubble.UNKNOWN_QUEUE_POSITION
sent = 'sent_to_commit_queue' if queue == 'commit' else 'sent_to_buildbot'
previously_sent_patches = set(Patch.objects
.filter(created__gte=from_timestamp)
.filter(**{sent: True})
.filter(obsolete=False)
.filter(created__lt=patch.created))
if parent_queue:
recent_builds_parent_queue = Build.objects \
.filter(created__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(created__gte=from_timestamp) \
.filter(builder_display_name=queue)
processed_patches = set([build.patch for build in recent_builds])
_log.debug('Patch: {}, queue: {}, previous patches: {}'.format(patch.patch_id, queue, previously_sent_patches - processed_patches))
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
show_retry = False
bubbles = []
if not patch:
return (None, show_submit_to_ews, failed_to_apply, show_retry)
if patch.sent_to_buildbot:
for queue in StatusBubble.ALL_QUEUES:
bubble = self._build_bubble(patch, queue, hide_icons)
if bubble:
show_submit_to_ews = False
bubbles.append(bubble)
if bubble['state'] in ('fail', 'error'):
show_retry = True
if patch.sent_to_commit_queue:
if not patch.sent_to_buildbot:
hide_icons = True
cq_bubble = self._build_bubble(patch, 'commit', hide_icons)
if cq_bubble:
bubbles.insert(0, cq_bubble)
return (bubbles, show_submit_to_ews, failed_to_apply, show_retry)
@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, show_retry = 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,
'show_retry_button': show_retry,
}
return render(request, 'statusbubble.html', template_values)