| # 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) |