blob: c5cc24ea7dce17ea9955f4717f5cdcc620fc6963 [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 buildbot.plugins import steps, util
from buildbot.process import buildstep, logobserver, properties
from buildbot.process.results import Results, SUCCESS, FAILURE, CANCELLED, WARNINGS, SKIPPED, EXCEPTION, RETRY
from buildbot.steps import master, shell, transfer, trigger
from buildbot.steps.source import git
from buildbot.steps.worker import CompositeStepMixin
from datetime import date
from twisted.internet import defer
from layout_test_failures import LayoutTestFailures
from send_email import send_email_to_patch_author, send_email_to_bot_watchers, send_email_to_github_admin
import json
import os
import re
import requests
import socket
import sys
import time
if sys.version_info < (3, 5):
print('ERROR: Please use Python 3. This code is not compatible with Python 2.')
sys.exit(1)
BUG_SERVER_URL = 'https://bugs.webkit.org/'
COMMITS_INFO_URL = 'https://commits.webkit.org/'
S3URL = 'https://s3-us-west-2.amazonaws.com/'
S3_RESULTS_URL = 'https://ews-build.s3-us-west-2.amazonaws.com/'
CURRENT_HOSTNAME = socket.gethostname().strip()
EWS_BUILD_HOSTNAME = 'ews-build.webkit.org'
EWS_URL = 'https://ews.webkit.org/'
RESULTS_DB_URL = 'https://results.webkit.org/'
WithProperties = properties.WithProperties
Interpolate = properties.Interpolate
class ConfigureBuild(buildstep.BuildStep):
name = 'configure-build'
description = ['configuring build']
descriptionDone = ['Configured build']
def __init__(self, platform, configuration, architectures, buildOnly, triggers, remotes, additionalArguments, triggered_by=None):
super(ConfigureBuild, self).__init__()
self.platform = platform
if platform != 'jsc-only':
self.platform = platform.split('-', 1)[0]
self.fullPlatform = platform
self.configuration = configuration
self.architecture = ' '.join(architectures) if architectures else None
self.buildOnly = buildOnly
self.triggers = triggers
self.triggered_by = triggered_by
self.remotes = remotes
self.additionalArguments = additionalArguments
def start(self):
if self.platform and self.platform != '*':
self.setProperty('platform', self.platform, 'config.json')
if self.fullPlatform and self.fullPlatform != '*':
self.setProperty('fullPlatform', self.fullPlatform, 'ConfigureBuild')
if self.configuration:
self.setProperty('configuration', self.configuration, 'config.json')
if self.architecture:
self.setProperty('architecture', self.architecture, 'config.json')
if self.buildOnly:
self.setProperty('buildOnly', self.buildOnly, 'config.json')
if self.triggers and not self.getProperty('triggers'):
self.setProperty('triggers', self.triggers, 'config.json')
if self.triggered_by:
self.setProperty('triggered_by', self.triggered_by, 'config.json')
if self.remotes:
self.setProperty('remotes', self.remotes, 'config.json')
if self.additionalArguments:
self.setProperty('additionalArguments', self.additionalArguments, 'config.json')
self.add_patch_id_url()
self.finished(SUCCESS)
return defer.succeed(None)
def add_patch_id_url(self):
patch_id = self.getProperty('patch_id', '')
if patch_id:
self.addURL('Patch {}'.format(patch_id), Bugzilla.patch_url(patch_id))
class CheckOutSource(git.Git):
name = 'clean-and-update-working-directory'
CHECKOUT_DELAY_AND_MAX_RETRIES_PAIR = (0, 2)
haltOnFailure = False
def __init__(self, repourl='https://github.com/WebKit/WebKit.git', **kwargs):
super(CheckOutSource, self).__init__(repourl=repourl,
retry=self.CHECKOUT_DELAY_AND_MAX_RETRIES_PAIR,
timeout=2 * 60 * 60,
alwaysUseLatest=True,
logEnviron=False,
method='clean',
progress=True,
**kwargs)
def getResultSummary(self):
if self.results == FAILURE:
self.build.addStepsAfterCurrentStep([CleanUpGitIndexLock()])
if self.results != SUCCESS:
return {'step': 'Failed to updated working directory'}
else:
return {'step': 'Cleaned and updated working directory'}
class CleanUpGitIndexLock(shell.ShellCommand):
name = 'clean-git-index-lock'
command = ['rm', '-f', '.git/index.lock']
descriptionDone = ['Deleted .git/index.lock']
def __init__(self, **kwargs):
super(CleanUpGitIndexLock, self).__init__(timeout=2 * 60, logEnviron=False, **kwargs)
def start(self):
platform = self.getProperty('platform', '*')
if platform == 'wincairo':
self.command = ['del', r'.git\index.lock']
self.send_email_for_git_issue()
return shell.ShellCommand.start(self)
def evaluateCommand(self, cmd):
self.build.buildFinished(['Git issue, retrying build'], RETRY)
return super(CleanUpGitIndexLock, self).evaluateCommand(cmd)
def send_email_for_git_issue(self):
try:
builder_name = self.getProperty('buildername', '')
worker_name = self.getProperty('workername', '')
build_url = '{}#/builders/{}/builds/{}'.format(self.master.config.buildbotURL, self.build._builderid, self.build.number)
email_subject = 'Git issue on {}'.format(worker_name)
email_text = 'Git issue on {}\n\nBuild: {}\n\nBuilder: {}'.format(worker_name, build_url, builder_name)
send_email_to_bot_watchers(email_subject, email_text, builder_name, worker_name)
except Exception as e:
print('Error in sending email for git issue: {}'.format(e))
class CheckOutSpecificRevision(shell.ShellCommand):
name = 'checkout-specific-revision'
descriptionDone = ['Checked out required revision']
flunkOnFailure = False
haltOnFailure = False
def __init__(self, **kwargs):
super(CheckOutSpecificRevision, self).__init__(logEnviron=False, **kwargs)
def doStepIf(self, step):
return self.getProperty('ews_revision', False)
def hideStepIf(self, results, step):
return not self.doStepIf(step)
def start(self):
self.setCommand(['git', 'checkout', self.getProperty('ews_revision')])
return shell.ShellCommand.start(self)
class GitResetHard(shell.ShellCommand):
name = 'git-reset-hard'
descriptionDone = ['Performed git reset --hard']
def __init__(self, **kwargs):
super(GitResetHard, self).__init__(logEnviron=False, **kwargs)
def start(self):
self.setCommand(['git', 'reset', 'HEAD~10', '--hard'])
return shell.ShellCommand.start(self)
class FetchBranches(shell.ShellCommand):
name = 'fetch-branch-references'
descriptionDone = ['Updated branch information']
command = ['git', 'fetch']
flunkOnFailure = False
haltOnFailure = False
def __init__(self, **kwargs):
super(FetchBranches, self).__init__(timeout=5 * 60, logEnviron=False, **kwargs)
def hideStepIf(self, results, step):
return results == SUCCESS
class ShowIdentifier(shell.ShellCommand):
name = 'show-identifier'
identifier_re = '^Identifier: (.*)$'
flunkOnFailure = False
haltOnFailure = False
def __init__(self, **kwargs):
shell.ShellCommand.__init__(self, timeout=5 * 60, logEnviron=False, **kwargs)
def start(self):
self.log_observer = logobserver.BufferLogObserver()
self.addLogObserver('stdio', self.log_observer)
revision = self.getProperty('ews_revision', self.getProperty('got_revision'))
if not revision:
revision = 'HEAD'
self.setCommand(['python3', 'Tools/Scripts/git-webkit', 'find', revision])
return shell.ShellCommand.start(self)
def evaluateCommand(self, cmd):
rc = shell.ShellCommand.evaluateCommand(self, cmd)
if rc != SUCCESS:
return rc
log_text = self.log_observer.getStdout()
match = re.search(self.identifier_re, log_text, re.MULTILINE)
if match:
identifier = match.group(1)
if identifier:
identifier = identifier.replace('master', 'main')
self.setProperty('identifier', identifier)
ews_revision = self.getProperty('ews_revision')
if ews_revision:
step = self.getLastBuildStepByName(CheckOutSpecificRevision.name)
else:
step = self.getLastBuildStepByName(CheckOutSource.name)
if not step:
step = self
step.addURL('Updated to {}'.format(identifier), self.url_for_identifier(identifier))
self.descriptionDone = 'Identifier: {}'.format(identifier)
else:
self.descriptionDone = 'Failed to find identifier'
return rc
def getLastBuildStepByName(self, name):
for step in reversed(self.build.executedSteps):
if name in step.name:
return step
return None
def url_for_identifier(self, identifier):
return '{}{}'.format(COMMITS_INFO_URL, identifier)
def getResultSummary(self):
if self.results != SUCCESS:
return {'step': 'Failed to find identifier'}
return shell.ShellCommand.getResultSummary(self)
def hideStepIf(self, results, step):
return results == SUCCESS
class CleanWorkingDirectory(shell.ShellCommand):
name = 'clean-working-directory'
description = ['clean-working-directory running']
descriptionDone = ['Cleaned working directory']
flunkOnFailure = True
haltOnFailure = True
command = ['python3', 'Tools/Scripts/clean-webkit']
def __init__(self, **kwargs):
super(CleanWorkingDirectory, self).__init__(logEnviron=False, **kwargs)
def start(self):
platform = self.getProperty('platform')
if platform in ('gtk', 'wpe'):
self.setCommand(self.command + ['--keep-jhbuild-directory'])
return shell.ShellCommand.start(self)
class UpdateWorkingDirectory(shell.ShellCommand):
name = 'update-working-directory'
description = ['update-workring-directory running']
flunkOnFailure = True
haltOnFailure = True
command = ['perl', 'Tools/Scripts/update-webkit']
def __init__(self, **kwargs):
super(UpdateWorkingDirectory, self).__init__(logEnviron=False, **kwargs)
def getResultSummary(self):
if self.results != SUCCESS:
return {'step': 'Failed to updated working directory'}
else:
return {'step': 'Updated working directory'}
def evaluateCommand(self, cmd):
rc = shell.ShellCommand.evaluateCommand(self, cmd)
if rc == FAILURE:
self.build.buildFinished(['Git issue, retrying build'], RETRY)
return rc
class ApplyPatch(shell.ShellCommand, CompositeStepMixin):
name = 'apply-patch'
description = ['applying-patch']
descriptionDone = ['Applied patch']
haltOnFailure = False
command = ['perl', 'Tools/Scripts/svn-apply', '--force', '.buildbot-diff']
def __init__(self, **kwargs):
super(ApplyPatch, self).__init__(timeout=10 * 60, logEnviron=False, **kwargs)
def _get_patch(self):
sourcestamp = self.build.getSourceStamp(self.getProperty('codebase', ''))
if not sourcestamp or not sourcestamp.patch:
return None
return sourcestamp.patch[1]
def start(self):
patch = self._get_patch()
if not patch:
# Forced build, don't have patch_id raw data on the request, need to fech it.
patch_id = self.getProperty('patch_id', '')
self.command = ['/bin/sh', '-c', 'curl -L "https://bugs.webkit.org/attachment.cgi?id={}" -o .buildbot-diff && {}'.format(patch_id, ' '.join(self.command))]
shell.ShellCommand.start(self)
return None
patch_reviewer_name = self.getProperty('patch_reviewer_full_name', '')
if patch_reviewer_name:
self.command.extend(['--reviewer', patch_reviewer_name])
d = self.downloadFileContentToWorker('.buildbot-diff', patch)
d.addCallback(lambda res: shell.ShellCommand.start(self))
def hideStepIf(self, results, step):
return results == SUCCESS and self.getProperty('sensitive', False)
def getResultSummary(self):
if self.results != SUCCESS:
return {'step': 'svn-apply failed to apply patch to trunk'}
return super(ApplyPatch, self).getResultSummary()
def evaluateCommand(self, cmd):
rc = shell.ShellCommand.evaluateCommand(self, cmd)
patch_id = self.getProperty('patch_id', '')
if rc == FAILURE:
message = 'Tools/Scripts/svn-apply failed to apply patch {} to trunk'.format(patch_id)
if self.getProperty('buildername', '').lower() == 'commit-queue':
comment_text = '{}.\nPlease resolve the conflicts and upload a new patch.'.format(message.replace('patch', 'attachment'))
self.setProperty('bugzilla_comment_text', comment_text)
self.setProperty('build_finish_summary', message)
self.build.addStepsAfterCurrentStep([CommentOnBug(), SetCommitQueueMinusFlagOnPatch()])
else:
self.build.buildFinished([message], FAILURE)
return rc
class AnalyzePatch(buildstep.BuildStep):
flunkOnFailure = True
haltOnFailure = True
def _get_patch(self):
sourcestamp = self.build.getSourceStamp(self.getProperty('codebase', ''))
if not sourcestamp or not sourcestamp.patch:
return None
return sourcestamp.patch[1]
@defer.inlineCallbacks
def _addToLog(self, logName, message):
try:
log = self.getLog(logName)
except KeyError:
log = yield self.addLog(logName)
log.addStdout(message)
def getResultSummary(self):
if self.results in [FAILURE, SKIPPED]:
return {'step': 'Patch doesn\'t have relevant changes'}
if self.results == SUCCESS:
return {'step': 'Patch contains relevant changes'}
return buildstep.BuildStep.getResultSummary(self)
class CheckPatchRelevance(AnalyzePatch):
name = 'check-patch-relevance'
description = ['check-patch-relevance running']
descriptionDone = ['Patch contains relevant changes']
MAX_LINE_SIZE = 250
bindings_path_regexes = [
re.compile(rb'Source/WebCore', re.IGNORECASE),
re.compile(rb'Tools', re.IGNORECASE),
]
services_path_regexes = [
re.compile(rb'Tools/CISupport/build-webkit-org', re.IGNORECASE),
re.compile(rb'Tools/CISupport/ews-build', re.IGNORECASE),
re.compile(rb'Tools/CISupport/Shared', re.IGNORECASE),
re.compile(rb'Tools/Scripts/libraries/resultsdbpy', re.IGNORECASE),
re.compile(rb'Tools/Scripts/libraries/webkitcorepy', re.IGNORECASE),
re.compile(rb'Tools/Scripts/libraries/webkitscmpy', re.IGNORECASE),
]
jsc_path_regexes = [
re.compile(rb'.*jsc.*', re.IGNORECASE),
re.compile(rb'.*javascriptcore.*', re.IGNORECASE),
re.compile(rb'JSTests/', re.IGNORECASE),
re.compile(rb'Source/WTF/', re.IGNORECASE),
re.compile(rb'Source/bmalloc/', re.IGNORECASE),
re.compile(rb'.*Makefile.*', re.IGNORECASE),
re.compile(rb'Tools/Scripts/build-webkit', re.IGNORECASE),
re.compile(rb'Tools/Scripts/webkitdirs.pm', re.IGNORECASE),
]
wk1_path_regexes = [
re.compile(rb'Source/WebKitLegacy', re.IGNORECASE),
re.compile(rb'Source/WebCore', re.IGNORECASE),
re.compile(rb'Source/WebInspectorUI', re.IGNORECASE),
re.compile(rb'Source/WebDriver', re.IGNORECASE),
re.compile(rb'Source/WTF', re.IGNORECASE),
re.compile(rb'Source/bmalloc', re.IGNORECASE),
re.compile(rb'Source/JavaScriptCore', re.IGNORECASE),
re.compile(rb'Source/ThirdParty', re.IGNORECASE),
re.compile(rb'LayoutTests', re.IGNORECASE),
re.compile(rb'Tools', re.IGNORECASE),
]
big_sur_builder_path_regexes = [
re.compile(rb'Source/', re.IGNORECASE),
re.compile(rb'Tools/', re.IGNORECASE),
]
webkitpy_path_regexes = [
re.compile(rb'Tools/Scripts/webkitpy', re.IGNORECASE),
re.compile(rb'Tools/Scripts/libraries', re.IGNORECASE),
re.compile(rb'Tools/Scripts/commit-log-editor', re.IGNORECASE),
re.compile(rb'Source/WebKit/Scripts', re.IGNORECASE),
]
group_to_paths_mapping = {
'bindings': bindings_path_regexes,
'bigsur-release-build': big_sur_builder_path_regexes,
'services-ews': services_path_regexes,
'jsc': jsc_path_regexes,
'webkitpy': webkitpy_path_regexes,
'wk1-tests': wk1_path_regexes,
'windows': wk1_path_regexes,
}
def _patch_is_relevant(self, patch, builderName, timeout=30):
group = [group for group in self.group_to_paths_mapping.keys() if group.lower() in builderName.lower()]
if not group:
# This builder doesn't have paths defined, all patches are relevant.
return True
relevant_path_regexes = self.group_to_paths_mapping.get(group[0], [])
start = time.time()
for change in patch.splitlines():
for regex in relevant_path_regexes:
if type(change) == str:
change = change.encode(encoding='utf-8', errors='replace')
if regex.search(change[:self.MAX_LINE_SIZE]):
return True
if time.time() > start + timeout:
return False
return False
def start(self):
patch = self._get_patch()
if not patch:
# This build doesn't have a patch, it might be a force build.
self.finished(SUCCESS)
return None
if self._patch_is_relevant(patch, self.getProperty('buildername', '')):
self._addToLog('stdio', 'This patch contains relevant changes.')
self.finished(SUCCESS)
return None
self._addToLog('stdio', 'This patch does not have relevant changes.')
self.finished(FAILURE)
self.build.results = SKIPPED
self.build.buildFinished(['Patch {} doesn\'t have relevant changes'.format(self.getProperty('patch_id', ''))], SKIPPED)
return None
class FindModifiedLayoutTests(AnalyzePatch):
name = 'find-modified-layout-tests'
RE_LAYOUT_TEST = br'^(\+\+\+).*(LayoutTests.*\.html)'
DIRECTORIES_TO_IGNORE = ['reference', 'reftest', 'resources', 'support', 'script-tests', 'tools']
SUFFIXES_TO_IGNORE = ['-expected', '-expected-mismatch', '-ref', '-notref']
def __init__(self, skipBuildIfNoResult=True):
self.skipBuildIfNoResult = skipBuildIfNoResult
buildstep.BuildStep.__init__(self)
def find_test_names_from_patch(self, patch):
tests = []
for line in patch.splitlines():
match = re.search(self.RE_LAYOUT_TEST, line, re.IGNORECASE)
if match:
if any((suffix + '.html').encode('utf-8') in line for suffix in self.SUFFIXES_TO_IGNORE):
continue
test_name = match.group(2).decode('utf-8')
if any(directory in test_name.split('/') for directory in self.DIRECTORIES_TO_IGNORE):
continue
tests.append(test_name)
return list(set(tests))
def start(self):
patch = self._get_patch()
if not patch:
self.finished(SUCCESS)
return None
tests = self.find_test_names_from_patch(patch)
if tests:
self._addToLog('stdio', 'This patch modifies following tests: {}'.format(tests))
self.setProperty('modified_tests', tests)
self.finished(SUCCESS)
return None
self._addToLog('stdio', 'This patch does not modify any layout tests')
self.finished(SKIPPED)
if self.skipBuildIfNoResult:
self.build.results = SKIPPED
self.build.buildFinished(['Patch {} doesn\'t have relevant changes'.format(self.getProperty('patch_id', ''))], SKIPPED)
return None
class Bugzilla(object):
@classmethod
def bug_url(cls, bug_id):
if not bug_id:
return ''
return '{}show_bug.cgi?id={}'.format(BUG_SERVER_URL, bug_id)
@classmethod
def patch_url(cls, patch_id):
if not patch_id:
return ''
return '{}attachment.cgi?id={}&action=prettypatch'.format(BUG_SERVER_URL, patch_id)
class BugzillaMixin(object):
addURLs = False
bug_open_statuses = ['UNCONFIRMED', 'NEW', 'ASSIGNED', 'REOPENED']
bug_closed_statuses = ['RESOLVED', 'VERIFIED', 'CLOSED']
fast_cq_preambles = ('revert of r', 'fast-cq', '[fast-cq]')
@defer.inlineCallbacks
def _addToLog(self, logName, message):
try:
log = self.getLog(logName)
except KeyError:
log = yield self.addLog(logName)
log.addStdout(message)
def fetch_data_from_url_with_authentication(self, url):
response = None
try:
response = requests.get(url, timeout=60, params={'Bugzilla_api_key': self.get_bugzilla_api_key()})
if response.status_code != 200:
self._addToLog('stdio', 'Accessed {url} with unexpected status code {status_code}.\n'.format(url=url, status_code=response.status_code))
return None
except Exception as e:
# Catching all exceptions here to safeguard api key.
self._addToLog('stdio', 'Failed to access {url}.\n'.format(url=url))
return None
return response
def fetch_data_from_url(self, url):
response = None
try:
response = requests.get(url, timeout=60)
except Exception as e:
if response:
self._addToLog('stdio', 'Failed to access {url} with status code {status_code}.\n'.format(url=url, status_code=response.status_code))
else:
self._addToLog('stdio', 'Failed to access {url} with exception: {exception}\n'.format(url=url, exception=e))
return None
if response.status_code != 200:
self._addToLog('stdio', 'Accessed {url} with unexpected status code {status_code}.\n'.format(url=url, status_code=response.status_code))
return None
return response
def get_patch_json(self, patch_id):
patch_url = '{}rest/bug/attachment/{}'.format(BUG_SERVER_URL, patch_id)
patch = self.fetch_data_from_url_with_authentication(patch_url)
if not patch:
return None
try:
patch_json = patch.json().get('attachments')
except Exception as e:
print('Failed to fetch patch json from {}, error: {}'.format(patch_url, e))
return None
if not patch_json or len(patch_json) == 0:
return None
return patch_json.get(str(patch_id))
def get_bug_json(self, bug_id):
bug_url = '{}rest/bug/{}'.format(BUG_SERVER_URL, bug_id)
bug = self.fetch_data_from_url_with_authentication(bug_url)
if not bug:
return None
try:
bugs_json = bug.json().get('bugs')
except Exception as e:
print('Failed to fetch bug json from {}, error: {}'.format(bug_url, e))
return None
if not bugs_json or len(bugs_json) == 0:
return None
return bugs_json[0]
def get_bug_id_from_patch(self, patch_id):
patch_json = self.get_patch_json(patch_id)
if not patch_json:
self._addToLog('stdio', 'Unable to fetch patch {}.\n'.format(patch_id))
return -1
return patch_json.get('bug_id')
def _is_patch_obsolete(self, patch_id):
patch_json = self.get_patch_json(patch_id)
if not patch_json:
self._addToLog('stdio', 'Unable to fetch patch {}.\n'.format(patch_id))
return -1
if str(patch_json.get('id')) != self.getProperty('patch_id', ''):
self._addToLog('stdio', 'Fetched patch id {} does not match with requested patch id {}. Unable to validate.\n'.format(patch_json.get('id'), self.getProperty('patch_id', '')))
return -1
patch_author = patch_json.get('creator')
self.setProperty('patch_author', patch_author)
patch_title = patch_json.get('summary')
if patch_title.lower().startswith(self.fast_cq_preambles):
self.setProperty('fast_commit_queue', True)
if self.addURLs:
self.addURL('Patch by: {}'.format(patch_author), '')
return patch_json.get('is_obsolete')
def _is_patch_review_denied(self, patch_id):
patch_json = self.get_patch_json(patch_id)
if not patch_json:
self._addToLog('stdio', 'Unable to fetch patch {}.\n'.format(patch_id))
return -1
for flag in patch_json.get('flags', []):
if flag.get('name') == 'review' and flag.get('status') == '-':
return 1
return 0
def _is_patch_cq_plus(self, patch_id):
patch_json = self.get_patch_json(patch_id)
if not patch_json:
self._addToLog('stdio', 'Unable to fetch patch {}.\n'.format(patch_id))
return -1
for flag in patch_json.get('flags', []):
if flag.get('name') == 'commit-queue' and flag.get('status') == '+':
self.setProperty('patch_committer', flag.get('setter', ''))
return 1
return 0
def _does_patch_have_acceptable_review_flag(self, patch_id):
patch_json = self.get_patch_json(patch_id)
if not patch_json:
self._addToLog('stdio', 'Unable to fetch patch {}.\n'.format(patch_id))
return -1
for flag in patch_json.get('flags', []):
if flag.get('name') == 'review':
review_status = flag.get('status')
if review_status == '+':
patch_reviewer = flag.get('setter', '')
self.setProperty('patch_reviewer', patch_reviewer)
if self.addURLs:
self.addURL('Reviewed by: {}'.format(patch_reviewer), '')
return 1
if review_status in ['-', '?']:
self._addToLog('stdio', 'Patch {} is marked r{}.\n'.format(patch_id, review_status))
return 0
return 1 # Patch without review flag is acceptable, since the ChangeLog might have 'Reviewed by' in it.
def _is_bug_closed(self, bug_id):
if not bug_id:
self._addToLog('stdio', 'Skipping bug status validation since bug id is None.\n')
return -1
bug_json = self.get_bug_json(bug_id)
if not bug_json or not bug_json.get('status'):
self._addToLog('stdio', 'Unable to fetch bug {}.\n'.format(bug_id))
return -1
bug_title = bug_json.get('summary')
self.setProperty('bug_title', bug_title)
sensitive = bug_json.get('product') == 'Security'
if sensitive:
self.setProperty('sensitive', True)
bug_title = ''
if self.addURLs:
self.addURL('Bug {} {}'.format(bug_id, bug_title), Bugzilla.bug_url(bug_id))
if bug_json.get('status') in self.bug_closed_statuses:
return 1
return 0
def should_send_email(self, patch_id):
patch_json = self.get_patch_json(patch_id)
if not patch_json:
self._addToLog('stdio', 'Unable to fetch patch {}'.format(patch_id))
return True
obsolete = patch_json.get('is_obsolete')
if obsolete == 1:
self._addToLog('stdio', 'Skipping email since patch {} is obsolete'.format(patch_id))
return False
review_denied = False
for flag in patch_json.get('flags', []):
if flag.get('name') == 'review' and flag.get('status') == '-':
review_denied = True
if review_denied:
self._addToLog('stdio', 'Skipping email since patch {} is marked r-'.format(patch_id))
return False
return True
def get_bugzilla_api_key(self):
try:
passwords = json.load(open('passwords.json'))
return passwords.get('BUGZILLA_API_KEY', '')
except Exception as e:
print('Error in reading Bugzilla api key')
return ''
def remove_flags_on_patch(self, patch_id):
patch_url = '{}rest/bug/attachment/{}'.format(BUG_SERVER_URL, patch_id)
flags = [{'name': 'review', 'status': 'X'}, {'name': 'commit-queue', 'status': 'X'}]
try:
response = requests.put(patch_url, json={'flags': flags, 'Bugzilla_api_key': self.get_bugzilla_api_key()})
if response.status_code not in [200, 201]:
self._addToLog('stdio', 'Unable to remove flags on patch {}. Unexpected response code from bugzilla: {}'.format(patch_id, response.status_code))
return FAILURE
except Exception as e:
self._addToLog('stdio', 'Error in removing flags on Patch {}'.format(patch_id))
return FAILURE
return SUCCESS
def set_cq_minus_flag_on_patch(self, patch_id):
patch_url = '{}rest/bug/attachment/{}'.format(BUG_SERVER_URL, patch_id)
flags = [{'name': 'commit-queue', 'status': '-'}]
try:
response = requests.put(patch_url, json={'flags': flags, 'Bugzilla_api_key': self.get_bugzilla_api_key()})
if response.status_code not in [200, 201]:
self._addToLog('stdio', 'Unable to set cq- flag on patch {}. Unexpected response code from bugzilla: {}'.format(patch_id, response.status_code))
return FAILURE
except Exception as e:
self._addToLog('stdio', 'Error in setting cq- flag on patch {}'.format(patch_id))
return FAILURE
return SUCCESS
def close_bug(self, bug_id):
bug_url = '{}rest/bug/{}'.format(BUG_SERVER_URL, bug_id)
try:
response = requests.put(bug_url, json={'status': 'RESOLVED', 'resolution': 'FIXED', 'Bugzilla_api_key': self.get_bugzilla_api_key()})
if response.status_code not in [200, 201]:
self._addToLog('stdio', 'Unable to close bug {}. Unexpected response code from bugzilla: {}'.format(bug_id, response.status_code))
return FAILURE
except Exception as e:
self._addToLog('stdio', 'Error in closing bug {}'.format(bug_id))
return FAILURE
return SUCCESS
def comment_on_bug(self, bug_id, comment_text):
bug_comment_url = '{}rest/bug/{}/comment'.format(BUG_SERVER_URL, bug_id)
if not comment_text:
return FAILURE
try:
response = requests.post(bug_comment_url, data={'comment': comment_text, 'Bugzilla_api_key': self.get_bugzilla_api_key()})
if response.status_code not in [200, 201]:
self._addToLog('stdio', 'Unable to comment on bug {}. Unexpected response code from bugzilla: {}'.format(bug_id, response.status_code))
return FAILURE
except Exception as e:
self._addToLog('stdio', 'Error in commenting on bug {}'.format(bug_id))
return FAILURE
return SUCCESS
def create_bug(self, bug_title, bug_description, component='Tools / Tests', cc_list=None):
bug_url = '{}rest/bug'.format(BUG_SERVER_URL)
if not (bug_title and bug_description):
return FAILURE
try:
response = requests.post(bug_url, data={'product': 'WebKit',
'component': component,
'version': 'WebKit Nightly Build',
'summary': bug_title,
'description': bug_description,
'cc': cc_list,
'Bugzilla_api_key': self.get_bugzilla_api_key()})
if response.status_code not in [200, 201]:
self._addToLog('stdio', 'Unable to file bug. Unexpected response code from bugzilla: {}'.format(response.status_code))
return FAILURE
except Exception as e:
self._addToLog('stdio', 'Error in creating bug: {}'.format(bug_title))
return FAILURE
self._addToLog('stdio', 'Filed bug: {}'.format(bug_title))
return SUCCESS
class ValidatePatch(buildstep.BuildStep, BugzillaMixin):
name = 'validate-patch'
description = ['validate-patch running']
descriptionDone = ['Validated patch']
flunkOnFailure = True
haltOnFailure = True
def __init__(self, verifyObsolete=True, verifyBugClosed=True, verifyReviewDenied=True, addURLs=True, verifycqplus=False):
self.verifyObsolete = verifyObsolete
self.verifyBugClosed = verifyBugClosed
self.verifyReviewDenied = verifyReviewDenied
self.verifycqplus = verifycqplus
self.addURLs = addURLs
buildstep.BuildStep.__init__(self)
def getResultSummary(self):
if self.results == FAILURE:
return {'step': self.descriptionDone}
return super(ValidatePatch, self).getResultSummary()
def doStepIf(self, step):
return not self.getProperty('skip_validation', False)
def skip_build(self, reason):
self._addToLog('stdio', reason)
self.finished(FAILURE)
self.build.results = SKIPPED
self.descriptionDone = reason
self.build.buildFinished([reason], SKIPPED)
def start(self):
patch_id = self.getProperty('patch_id', '')
if not patch_id:
self._addToLog('stdio', 'No patch_id found. Unable to proceed without patch_id.\n')
self.descriptionDone = 'No patch id found'
self.finished(FAILURE)
return None
bug_id = self.getProperty('bug_id', '') or self.get_bug_id_from_patch(patch_id)
bug_closed = self._is_bug_closed(bug_id) if self.verifyBugClosed else 0
if bug_closed == 1:
self.skip_build('Bug {} is already closed'.format(bug_id))
return None
obsolete = self._is_patch_obsolete(patch_id) if self.verifyObsolete else 0
if obsolete == 1:
self.skip_build('Patch {} is obsolete'.format(patch_id))
return None
review_denied = self._is_patch_review_denied(patch_id) if self.verifyReviewDenied else 0
if review_denied == 1:
self.skip_build('Patch {} is marked r-'.format(patch_id))
return None
cq_plus = self._is_patch_cq_plus(patch_id) if self.verifycqplus else 1
if cq_plus != 1:
self.skip_build('Patch {} is not marked cq+.'.format(patch_id))
return None
acceptable_review_flag = self._does_patch_have_acceptable_review_flag(patch_id) if self.verifycqplus else 1
if acceptable_review_flag != 1:
self.skip_build('Patch {} does not have acceptable review flag.'.format(patch_id))
return None
if obsolete == -1 or review_denied == -1 or bug_closed == -1:
self.finished(WARNINGS)
return None
if self.verifyBugClosed:
self._addToLog('stdio', 'Bug is open.\n')
if self.verifyObsolete:
self._addToLog('stdio', 'Patch is not obsolete.\n')
if self.verifyReviewDenied:
self._addToLog('stdio', 'Patch is not marked r-.\n')
if self.verifycqplus:
self._addToLog('stdio', 'Patch is marked cq+.\n')
self._addToLog('stdio', 'Patch have acceptable review flag.\n')
self.finished(SUCCESS)
return None
class ValidateCommiterAndReviewer(buildstep.BuildStep):
name = 'validate-commiter-and-reviewer'
descriptionDone = ['Validated commiter and reviewer']
url = 'https://raw.githubusercontent.com/WebKit/WebKit/main/metadata/contributors.json'
contributors = {}
def load_contributors_from_disk(self):
cwd = os.path.abspath(os.path.dirname(__file__))
repo_root = os.path.dirname(os.path.dirname(os.path.dirname(cwd)))
contributors_path = os.path.join(repo_root, 'metadata/contributors.json')
try:
with open(contributors_path, 'rb') as contributors_json:
return json.load(contributors_json)
except Exception as e:
self._addToLog('stdio', 'Failed to load {}\n'.format(contributors_path))
return {}
def load_contributors_from_github(self):
try:
response = requests.get(self.url, timeout=60)
if response.status_code != 200:
self._addToLog('stdio', 'Failed to access {} with status code: {}\n'.format(self.url, response.status_code))
return {}
return response.json()
except Exception as e:
self._addToLog('stdio', 'Failed to access {url}\n'.format(url=self.url))
return {}
def load_contributors(self):
contributors_json = self.load_contributors_from_github()
if not contributors_json:
contributors_json = self.load_contributors_from_disk()
contributors = {}
for value in contributors_json:
name = value.get('name')
emails = value.get('emails')
if name and emails:
bugzilla_email = emails[0].lower() # We're requiring that the first email is the primary bugzilla email
contributors[bugzilla_email] = {'name': name, 'status': value.get('status')}
return contributors
@defer.inlineCallbacks
def _addToLog(self, logName, message):
try:
log = self.getLog(logName)
except KeyError:
log = yield self.addLog(logName)
log.addStdout(message)
def getResultSummary(self):
if self.results == FAILURE:
return {'step': self.descriptionDone}
return buildstep.BuildStep.getResultSummary(self)
def fail_build(self, email, status):
reason = '{} does not have {} permissions'.format(email, status)
comment = '{} does not have {} permissions according to {}.'.format(email, status, self.url)
comment += '\n\nRejecting attachment {} from commit queue.'.format(self.getProperty('patch_id', ''))
self.setProperty('bugzilla_comment_text', comment)
self._addToLog('stdio', reason)
self.setProperty('build_finish_summary', reason)
self.build.addStepsAfterCurrentStep([CommentOnBug(), SetCommitQueueMinusFlagOnPatch()])
self.finished(FAILURE)
self.descriptionDone = reason
def is_reviewer(self, email):
contributor = self.contributors.get(email)
return contributor and contributor['status'] == 'reviewer'
def is_committer(self, email):
contributor = self.contributors.get(email)
return contributor and contributor['status'] in ['reviewer', 'committer']
def full_name_from_email(self, email):
contributor = self.contributors.get(email)
if not contributor:
return ''
return contributor.get('name')
def start(self):
self.contributors = self.load_contributors()
if not self.contributors:
self.finished(FAILURE)
self.descriptionDone = 'Failed to get contributors information'
self.build.buildFinished(['Failed to get contributors information'], FAILURE)
return None
patch_committer = self.getProperty('patch_committer', '').lower()
if not self.is_committer(patch_committer):
self.fail_build(patch_committer, 'committer')
return None
self._addToLog('stdio', '{} is a valid commiter.\n'.format(patch_committer))
patch_reviewer = self.getProperty('patch_reviewer', '').lower()
if not patch_reviewer:
# Patch does not have r+ flag. This is acceptable, since the ChangeLog might have 'Reviewed by' in it.
self.descriptionDone = 'Validated committer'
self.finished(SUCCESS)
return None
self.setProperty('patch_reviewer_full_name', self.full_name_from_email(patch_reviewer))
if not self.is_reviewer(patch_reviewer):
self.fail_build(patch_reviewer, 'reviewer')
return None
self._addToLog('stdio', '{} is a valid reviewer.\n'.format(patch_reviewer))
self.finished(SUCCESS)
return None
class ValidateChangeLogAndReviewer(shell.ShellCommand):
name = 'validate-changelog-and-reviewer'
descriptionDone = ['Validated ChangeLog and Reviewer']
command = ['python3', 'Tools/Scripts/webkit-patch', 'validate-changelog', '--check-oops', '--non-interactive']
haltOnFailure = False
flunkOnFailure = True
def __init__(self, **kwargs):
shell.ShellCommand.__init__(self, timeout=3 * 60, logEnviron=False, **kwargs)
def start(self):
self.log_observer = logobserver.BufferLogObserver(wantStderr=True)
self.addLogObserver('stdio', self.log_observer)
return shell.ShellCommand.start(self)
def getResultSummary(self):
if self.results != SUCCESS:
return {'step': 'ChangeLog validation failed'}
return shell.ShellCommand.getResultSummary(self)
def evaluateCommand(self, cmd):
rc = shell.ShellCommand.evaluateCommand(self, cmd)
if rc == FAILURE:
log_text = self.log_observer.getStdout() + self.log_observer.getStderr()
self.setProperty('bugzilla_comment_text', log_text)
self.setProperty('build_finish_summary', 'ChangeLog validation failed')
self.build.addStepsAfterCurrentStep([CommentOnBug(), SetCommitQueueMinusFlagOnPatch()])
return rc
class SetCommitQueueMinusFlagOnPatch(buildstep.BuildStep, BugzillaMixin):
name = 'set-cq-minus-flag-on-patch'
def start(self):
patch_id = self.getProperty('patch_id', '')
build_finish_summary = self.getProperty('build_finish_summary', None)
rc = SKIPPED
if CURRENT_HOSTNAME == EWS_BUILD_HOSTNAME:
rc = self.set_cq_minus_flag_on_patch(patch_id)
self.finished(rc)
if build_finish_summary:
self.build.buildFinished([build_finish_summary], FAILURE)
return None
def getResultSummary(self):
if self.results == SUCCESS:
return {'step': 'Set cq- flag on patch'}
elif self.results == SKIPPED:
return buildstep.BuildStep.getResultSummary(self)
return {'step': 'Failed to set cq- flag on patch'}
class RemoveFlagsOnPatch(buildstep.BuildStep, BugzillaMixin):
name = 'remove-flags-from-patch'
flunkOnFailure = False
haltOnFailure = False
def start(self):
patch_id = self.getProperty('patch_id', '')
if not patch_id:
self._addToLog('stdio', 'patch_id build property not found.\n')
self.descriptionDone = 'No patch id found'
self.finished(FAILURE)
return None
rc = self.remove_flags_on_patch(patch_id)
self.finished(rc)
return None
def getResultSummary(self):
if self.results == SUCCESS:
return {'step': 'Removed flags on bugzilla patch'}
return {'step': 'Failed to remove flags on bugzilla patch'}
class CloseBug(buildstep.BuildStep, BugzillaMixin):
name = 'close-bugzilla-bug'
flunkOnFailure = False
haltOnFailure = False
def start(self):
self.bug_id = self.getProperty('bug_id', '')
if not self.bug_id:
self._addToLog('stdio', 'bug_id build property not found.\n')
self.descriptionDone = 'No bug id found'
self.finished(FAILURE)
return None
rc = self.close_bug(self.bug_id)
self.finished(rc)
return None
def getResultSummary(self):
if self.results == SUCCESS:
return {'step': 'Closed bug {}'.format(self.bug_id)}
return {'step': 'Failed to close bug {}'.format(self.bug_id)}
class CommentOnBug(buildstep.BuildStep, BugzillaMixin):
name = 'comment-on-bugzilla-bug'
flunkOnFailure = False
haltOnFailure = False
def start(self):
self.bug_id = self.getProperty('bug_id', '')
self.comment_text = self.getProperty('bugzilla_comment_text', '')
if not self.comment_text:
self._addToLog('stdio', 'bugzilla_comment_text build property not found.\n')
self.descriptionDone = 'No bugzilla comment found'
self.finished(WARNINGS)
return None
rc = self.comment_on_bug(self.bug_id, self.comment_text)
self.finished(rc)
return None
def getResultSummary(self):
if self.results == SUCCESS:
return {'step': 'Added comment on bug {}'.format(self.bug_id)}
elif self.results == SKIPPED:
return buildstep.BuildStep.getResultSummary(self)
return {'step': 'Failed to add comment on bug {}'.format(self.bug_id)}
def doStepIf(self, step):
return CURRENT_HOSTNAME == EWS_BUILD_HOSTNAME
class UnApplyPatchIfRequired(CleanWorkingDirectory):
name = 'unapply-patch'
descriptionDone = ['Unapplied patch']
def doStepIf(self, step):
return self.getProperty('patchFailedToBuild') or self.getProperty('patchFailedTests')
def hideStepIf(self, results, step):
return not self.doStepIf(step)
class Trigger(trigger.Trigger):
def __init__(self, schedulerNames, include_revision=True, triggers=None, **kwargs):
self.include_revision = include_revision
self.triggers = triggers
set_properties = self.propertiesToPassToTriggers() or {}
super(Trigger, self).__init__(schedulerNames=schedulerNames, set_properties=set_properties, **kwargs)
def propertiesToPassToTriggers(self):
properties_to_pass = {
'patch_id': properties.Property('patch_id'),
'bug_id': properties.Property('bug_id'),
'configuration': properties.Property('configuration'),
'platform': properties.Property('platform'),
'fullPlatform': properties.Property('fullPlatform'),
'architecture': properties.Property('architecture'),
'owner': properties.Property('owner'),
}
if self.include_revision:
properties_to_pass['ews_revision'] = properties.Property('got_revision')
if self.triggers:
properties_to_pass['triggers'] = self.triggers
return properties_to_pass
class TestWithFailureCount(shell.Test):
failedTestsFormatString = '%d test%s failed'
failedTestCount = 0
def start(self):
self.log_observer = logobserver.BufferLogObserver(wantStderr=True)
self.addLogObserver('stdio', self.log_observer)
return shell.Test.start(self)
def countFailures(self, cmd):
raise NotImplementedError
def commandComplete(self, cmd):
shell.Test.commandComplete(self, cmd)
self.failedTestCount = self.countFailures(cmd)
self.failedTestPluralSuffix = '' if self.failedTestCount == 1 else 's'
def evaluateCommand(self, cmd):
if self.failedTestCount:
return FAILURE
if cmd.rc != 0:
return FAILURE
return SUCCESS
def getResultSummary(self):
status = self.name
if self.results != SUCCESS:
if self.failedTestCount:
status = self.failedTestsFormatString % (self.failedTestCount, self.failedTestPluralSuffix)
else:
status += ' ({})'.format(Results[self.results])
return {'step': status}
class CheckStyle(TestWithFailureCount):
name = 'check-webkit-style'
description = ['check-webkit-style running']
descriptionDone = ['check-webkit-style']
flunkOnFailure = True
failedTestsFormatString = '%d style error%s'
command = ['python3', 'Tools/Scripts/check-webkit-style']
def __init__(self, **kwargs):
super(CheckStyle, self).__init__(logEnviron=False, **kwargs)
def countFailures(self, cmd):
log_text = self.log_observer.getStdout() + self.log_observer.getStderr()
match = re.search(r'Total errors found: (?P<errors>\d+) in (?P<files>\d+) files', log_text)
if not match:
return 0
return int(match.group('errors'))
class RunBindingsTests(shell.ShellCommand):
name = 'bindings-tests'
description = ['bindings-tests running']
descriptionDone = ['bindings-tests']
flunkOnFailure = True
jsonFileName = 'bindings_test_results.json'
logfiles = {'json': jsonFileName}
command = ['python3', 'Tools/Scripts/run-bindings-tests', '--json-output={0}'.format(jsonFileName)]
def __init__(self, **kwargs):
super(RunBindingsTests, self).__init__(timeout=5 * 60, logEnviron=False, **kwargs)
def start(self):
self.log_observer = logobserver.BufferLogObserver()
self.addLogObserver('json', self.log_observer)
return shell.ShellCommand.start(self)
def getResultSummary(self):
if self.results == SUCCESS:
message = 'Passed bindings tests'
self.build.buildFinished([message], SUCCESS)
return {'step': message}
logLines = self.log_observer.getStdout()
json_text = ''.join([line for line in logLines.splitlines()])
try:
webkitpy_results = json.loads(json_text)
except Exception as ex:
self._addToLog('stderr', 'ERROR: unable to parse data, exception: {}'.format(ex))
return super(RunBindingsTests, self).getResultSummary()
failures = webkitpy_results.get('failures')
if not failures:
return super(RunBindingsTests, self).getResultSummary()
pluralSuffix = 's' if len(failures) > 1 else ''
failures_string = ', '.join([failure.replace('(JS) ', '') for failure in failures])
message = 'Found {} Binding test failure{}: {}'.format(len(failures), pluralSuffix, failures_string)
self.build.buildFinished([message], FAILURE)
return {'step': message}
@defer.inlineCallbacks
def _addToLog(self, logName, message):
try:
log = self.getLog(logName)
except KeyError:
log = yield self.addLog(logName)
log.addStdout(message)
class RunWebKitPerlTests(shell.ShellCommand):
name = 'webkitperl-tests'
description = ['webkitperl-tests running']
descriptionDone = ['webkitperl-tests']
flunkOnFailure = False
haltOnFailure = False
command = ['perl', 'Tools/Scripts/test-webkitperl']
def __init__(self, **kwargs):
super(RunWebKitPerlTests, self).__init__(timeout=2 * 60, logEnviron=False, **kwargs)
def getResultSummary(self):
if self.results == SUCCESS:
message = 'Passed webkitperl tests'
self.build.buildFinished([message], SUCCESS)
return {'step': message}
return {'step': 'Failed webkitperl tests'}
def evaluateCommand(self, cmd):
rc = shell.ShellCommand.evaluateCommand(self, cmd)
if rc == FAILURE:
self.build.addStepsAfterCurrentStep([KillOldProcesses(), ReRunWebKitPerlTests()])
return rc
class ReRunWebKitPerlTests(RunWebKitPerlTests):
name = 're-run-webkitperl-tests'
flunkOnFailure = True
haltOnFailure = True
def evaluateCommand(self, cmd):
return shell.ShellCommand.evaluateCommand(self, cmd)
class RunBuildWebKitOrgUnitTests(shell.ShellCommand):
name = 'build-webkit-org-unit-tests'
description = ['build-webkit-unit-tests running']
command = ['python3', 'runUnittests.py', 'build-webkit-org']
def __init__(self, **kwargs):
super(RunBuildWebKitOrgUnitTests, self).__init__(workdir='build/Tools/CISupport', timeout=2 * 60, logEnviron=False, **kwargs)
def start(self):
return shell.ShellCommand.start(self)
def getResultSummary(self):
if self.results == SUCCESS:
return {'step': 'Passed build.webkit.org unit tests'}
return {'step': 'Failed build.webkit.org unit tests'}
class RunEWSUnitTests(shell.ShellCommand):
name = 'ews-unit-tests'
description = ['ews-unit-tests running']
command = ['python3', 'runUnittests.py', 'ews-build']
def __init__(self, **kwargs):
super(RunEWSUnitTests, self).__init__(workdir='build/Tools/CISupport', timeout=2 * 60, logEnviron=False, **kwargs)
def getResultSummary(self):
if self.results == SUCCESS:
return {'step': 'Passed EWS unit tests'}
return {'step': 'Failed EWS unit tests'}
class RunBuildbotCheckConfig(shell.ShellCommand):
name = 'buildbot-check-config'
description = ['buildbot-checkconfig running']
command = ['buildbot', 'checkconfig']
directory = 'build/Tools/CISupport/ews-build'
timeout = 2 * 60
def __init__(self, **kwargs):
super(RunBuildbotCheckConfig, self).__init__(workdir=self.directory, timeout=self.timeout, logEnviron=False, **kwargs)
def start(self):
self.workerEnvironment['LC_CTYPE'] = 'en_US.UTF-8'
return shell.ShellCommand.start(self)
def getResultSummary(self):
if self.results == SUCCESS:
return {'step': 'Passed buildbot checkconfig'}
return {'step': 'Failed buildbot checkconfig'}
class RunBuildbotCheckConfigForEWS(RunBuildbotCheckConfig):
name = 'buildbot-check-config-for-ews'
directory = 'build/Tools/CISupport/ews-build'
class RunBuildbotCheckConfigForBuildWebKit(RunBuildbotCheckConfig):
name = 'buildbot-check-config-for-build-webkit'
directory = 'build/Tools/CISupport/build-webkit-org'
class RunResultsdbpyTests(shell.ShellCommand):
name = 'resultsdbpy-unit-tests'
description = ['resultsdbpy-unit-tests running']
command = [
'python3',
'Tools/Scripts/libraries/resultsdbpy/resultsdbpy/run-tests',
'--verbose',
'--no-selenium',
'--fast-tests',
]
def __init__(self, **kwargs):
super(RunResultsdbpyTests, self).__init__(timeout=2 * 60, logEnviron=False, **kwargs)
def getResultSummary(self):
if self.results == SUCCESS:
return {'step': 'Passed resultsdbpy unit tests'}
return {'step': 'Failed resultsdbpy unit tests'}
class WebKitPyTest(shell.ShellCommand):
language = 'python'
descriptionDone = ['webkitpy-tests']
flunkOnFailure = True
NUM_FAILURES_TO_DISPLAY = 10
def __init__(self, **kwargs):
super(WebKitPyTest, self).__init__(timeout=2 * 60, logEnviron=False, **kwargs)
def start(self):
self.log_observer = logobserver.BufferLogObserver()
self.addLogObserver('json', self.log_observer)
return shell.ShellCommand.start(self)
def setBuildSummary(self, build_summary):
previous_build_summary = self.getProperty('build_summary', '')
if not previous_build_summary:
self.setProperty('build_summary', build_summary)
return
if build_summary in previous_build_summary:
# Ensure that we do not append same build summary multiple times in case
# this method is called multiple times.
return
new_build_summary = previous_build_summary + ', ' + build_summary
self.setProperty('build_summary', new_build_summary)
def getResultSummary(self):
if self.results == SUCCESS:
message = 'Passed webkitpy {} tests'.format(self.language)
self.setBuildSummary(message)
return {'step': message}
logLines = self.log_observer.getStdout()
json_text = ''.join([line for line in logLines.splitlines()])
try:
webkitpy_results = json.loads(json_text)
except Exception as ex:
self._addToLog('stderr', 'ERROR: unable to parse data, exception: {}'.format(ex))
return super(WebKitPyTest, self).getResultSummary()
failures = webkitpy_results.get('failures', []) + webkitpy_results.get('errors', [])
if not failures:
return super(WebKitPyTest, self).getResultSummary()
pluralSuffix = 's' if len(failures) > 1 else ''
failures_string = ', '.join([failure.get('name') for failure in failures[:self.NUM_FAILURES_TO_DISPLAY]])
message = 'Found {} webkitpy {} test failure{}: {}'.format(len(failures), self.language, pluralSuffix, failures_string)
if len(failures) > self.NUM_FAILURES_TO_DISPLAY:
message += ' ...'
self.setBuildSummary(message)
return {'step': message}
@defer.inlineCallbacks
def _addToLog(self, logName, message):
try:
log = self.getLog(logName)
except KeyError:
log = yield self.addLog(logName)
log.addStdout(message)
class RunWebKitPyPython2Tests(WebKitPyTest):
language = 'python2'
name = 'webkitpy-tests-{}'.format(language)
description = ['webkitpy-tests running ({})'.format(language)]
jsonFileName = 'webkitpy_test_{}_results.json'.format(language)
logfiles = {'json': jsonFileName}
command = ['python', 'Tools/Scripts/test-webkitpy', '--verbose', '--json-output={0}'.format(jsonFileName)]
class RunWebKitPyPython3Tests(WebKitPyTest):
language = 'python3'
name = 'webkitpy-tests-{}'.format(language)
description = ['webkitpy-tests running ({})'.format(language)]
jsonFileName = 'webkitpy_test_{}_results.json'.format(language)
logfiles = {'json': jsonFileName}
command = ['python3', 'Tools/Scripts/test-webkitpy', '--verbose', '--json-output={0}'.format(jsonFileName)]
class InstallGtkDependencies(shell.ShellCommand):
name = 'jhbuild'
description = ['updating gtk dependencies']
descriptionDone = ['Updated gtk dependencies']
command = ['perl', 'Tools/Scripts/update-webkitgtk-libs', WithProperties('--%(configuration)s')]
haltOnFailure = True
def __init__(self, **kwargs):
super(InstallGtkDependencies, self).__init__(logEnviron=False, **kwargs)
class InstallWpeDependencies(shell.ShellCommand):
name = 'jhbuild'
description = ['updating wpe dependencies']
descriptionDone = ['Updated wpe dependencies']
command = ['perl', 'Tools/Scripts/update-webkitwpe-libs', WithProperties('--%(configuration)s')]
haltOnFailure = True
def __init__(self, **kwargs):
super(InstallWpeDependencies, self).__init__(logEnviron=False, **kwargs)
def appendCustomBuildFlags(step, platform, fullPlatform):
# FIXME: Make a common 'supported platforms' list.
if platform not in ('gtk', 'wincairo', 'ios', 'jsc-only', 'wpe', 'playstation', 'tvos', 'watchos'):
return
if 'simulator' in fullPlatform:
platform = platform + '-simulator'
elif platform in ['ios', 'tvos', 'watchos']:
platform = platform + '-device'
step.setCommand(step.command + ['--' + platform])
class BuildLogLineObserver(logobserver.LogLineObserver, object):
def __init__(self, errorReceived, searchString='rror:', includeRelatedLines=True):
self.errorReceived = errorReceived
self.searchString = searchString
self.includeRelatedLines = includeRelatedLines
self.error_context_buffer = []
self.whitespace_re = re.compile(r'^[\s]*$')
super(BuildLogLineObserver, self).__init__()
def outLineReceived(self, line):
if not self.errorReceived:
return
if not self.includeRelatedLines:
if self.searchString in line:
self.errorReceived(line)
return
is_whitespace = self.whitespace_re.search(line) is not None
if is_whitespace:
self.error_context_buffer = []
else:
self.error_context_buffer.append(line)
if self.searchString in line:
for log in self.error_context_buffer[-50:]:
self.errorReceived(log)
self.error_context_buffer = []
class CompileWebKit(shell.Compile):
name = 'compile-webkit'
description = ['compiling']
descriptionDone = ['Compiled WebKit']
env = {'MFLAGS': ''}
warningPattern = '.*arning: .*'
haltOnFailure = False
command = ['perl', 'Tools/Scripts/build-webkit', WithProperties('--%(configuration)s')]
def __init__(self, skipUpload=False, **kwargs):
self.skipUpload = skipUpload
super(CompileWebKit, self).__init__(logEnviron=False, **kwargs)
def doStepIf(self, step):
return not (self.getProperty('fast_commit_queue') and self.getProperty('buildername', '').lower() == 'commit-queue')
def start(self):
platform = self.getProperty('platform')
buildOnly = self.getProperty('buildOnly')
architecture = self.getProperty('architecture')
additionalArguments = self.getProperty('additionalArguments')
if platform in ['win', 'wincairo']:
self.addLogObserver('stdio', BuildLogLineObserver(self.errorReceived, searchString='error ', includeRelatedLines=False))
else:
self.addLogObserver('stdio', BuildLogLineObserver(self.errorReceived))
if additionalArguments:
self.setCommand(self.command + additionalArguments)
if platform in ('mac', 'ios', 'tvos', 'watchos') and architecture:
self.setCommand(self.command + ['ARCHS=' + architecture])
if platform in ['ios', 'tvos', 'watchos']:
self.setCommand(self.command + ['ONLY_ACTIVE_ARCH=NO'])
if platform in ('mac', 'ios', 'tvos', 'watchos') and buildOnly:
# For build-only bots, the expectation is that tests will be run on separate machines,
# so we need to package debug info as dSYMs. Only generating line tables makes
# this much faster than full debug info, and crash logs still have line numbers.
self.setCommand(self.command + ['DEBUG_INFORMATION_FORMAT=dwarf-with-dsym'])
self.setCommand(self.command + ['CLANG_DEBUG_INFORMATION_LEVEL=line-tables-only'])
if platform == 'gtk':
prefix = os.path.join("/app", "webkit", "WebKitBuild", self.getProperty("configuration"), "install")
self.setCommand(self.command + [f'--prefix={prefix}'])
appendCustomBuildFlags(self, platform, self.getProperty('fullPlatform'))
return shell.Compile.start(self)
@defer.inlineCallbacks
def _addToLog(self, logName, message):
try:
log = self.getLog(logName)
except KeyError:
log = yield self.addLog(logName)
log.addStdout(message)
def errorReceived(self, error):
self._addToLog('errors', error + '\n')
def evaluateCommand(self, cmd):
if cmd.didFail():
self.setProperty('patchFailedToBuild', True)
steps_to_add = [UnApplyPatchIfRequired(), ValidatePatch(verifyBugClosed=False, addURLs=False)]
platform = self.getProperty('platform')
if platform == 'wpe':
steps_to_add.append(InstallWpeDependencies())
elif platform == 'gtk':
steps_to_add.append(InstallGtkDependencies())
if self.getProperty('group') == 'jsc':
steps_to_add.append(CompileJSCWithoutPatch())
else:
steps_to_add.append(CompileWebKitWithoutPatch())
steps_to_add.append(AnalyzeCompileWebKitResults())
# Using a single addStepsAfterCurrentStep because of https://github.com/buildbot/buildbot/issues/4874
self.build.addStepsAfterCurrentStep(steps_to_add)
else:
triggers = self.getProperty('triggers', None)
if triggers or not self.skipUpload:
steps_to_add = [ArchiveBuiltProduct(), UploadBuiltProduct(), TransferToS3()]
if triggers:
steps_to_add.append(Trigger(schedulerNames=triggers))
self.build.addStepsAfterCurrentStep(steps_to_add)
return super(CompileWebKit, self).evaluateCommand(cmd)
def getResultSummary(self):
if self.results == FAILURE:
return {'step': 'Failed to compile WebKit'}
if self.results == SKIPPED:
if self.getProperty('fast_commit_queue'):
return {'step': 'Skipped compiling WebKit in fast-cq mode'}
return {'step': 'Skipped compiling WebKit'}
return shell.Compile.getResultSummary(self)
class CompileWebKitWithoutPatch(CompileWebKit):
name = 'compile-webkit-without-patch'
haltOnFailure = False
def __init__(self, retry_build_on_failure=False, **kwargs):
self.retry_build_on_failure = retry_build_on_failure
super(CompileWebKitWithoutPatch, self).__init__(**kwargs)
def doStepIf(self, step):
return self.getProperty('patchFailedToBuild') or self.getProperty('patchFailedTests')
def hideStepIf(self, results, step):
return not self.doStepIf(step)
def evaluateCommand(self, cmd):
rc = shell.Compile.evaluateCommand(self, cmd)
if rc == FAILURE and self.retry_build_on_failure:
message = 'Unable to build WebKit without patch, retrying build'
self.descriptionDone = message
self.send_email_for_unexpected_build_failure()
self.build.buildFinished([message], RETRY)
return rc
def send_email_for_unexpected_build_failure(self):
try:
builder_name = self.getProperty('buildername', '')
worker_name = self.getProperty('workername', '')
build_url = '{}#/builders/{}/builds/{}'.format(self.master.config.buildbotURL, self.build._builderid, self.build.number)
email_subject = '{} might be in bad state, unable to build WebKit'.format(worker_name)
email_text = '{} might be in bad state. It is unable to build WebKit.'.format(worker_name)
email_text += ' Same patch was built successfuly on builder queue previously.\n\nBuild: {}\n\nBuilder: {}'.format(build_url, builder_name)
reference = 'build-failure-{}'.format(worker_name)
send_email_to_bot_watchers(email_subject, email_text, builder_name, reference)
except Exception as e:
print('Error in sending email for unexpected build failure: {}'.format(e))
class AnalyzeCompileWebKitResults(buildstep.BuildStep, BugzillaMixin):
name = 'analyze-compile-webkit-results'
description = ['analyze-compile-webkit-results']
descriptionDone = ['analyze-compile-webkit-results']
def start(self):
self.error_logs = {}
self.compile_webkit_step = CompileWebKit.name
if self.getProperty('group') == 'jsc':
self.compile_webkit_step = CompileJSC.name
d = self.getResults(self.compile_webkit_step)
d.addCallback(lambda res: self.analyzeResults())
return defer.succeed(None)
def analyzeResults(self):
compile_without_patch_step = CompileWebKitWithoutPatch.name
if self.getProperty('group') == 'jsc':
compile_without_patch_step = CompileJSCWithoutPatch.name
compile_without_patch_result = self.getStepResult(compile_without_patch_step)
if compile_without_patch_result == FAILURE:
message = 'Unable to build WebKit without patch, retrying build'
self.descriptionDone = message
self.send_email_for_preexisting_build_failure()
self.finished(FAILURE)
self.build.buildFinished([message], RETRY)
return defer.succeed(None)
self.build.results = FAILURE
patch_id = self.getProperty('patch_id', '')
message = 'Patch {} does not build'.format(patch_id)
self.send_email_for_new_build_failure()
self.descriptionDone = message
self.finished(FAILURE)
self.setProperty('build_finish_summary', message)
if self.getProperty('buildername', '').lower() == 'commit-queue':
self.setProperty('bugzilla_comment_text', message)
self.build.addStepsAfterCurrentStep([CommentOnBug(), SetCommitQueueMinusFlagOnPatch()])
else:
self.build.addStepsAfterCurrentStep([SetCommitQueueMinusFlagOnPatch()])
@defer.inlineCallbacks
def getResults(self, name):
step = self.getBuildStepByName(name)
if not step:
defer.returnValue(None)
logs = yield self.master.db.logs.getLogs(step.stepid)
log = next((log for log in logs if log['name'] == 'errors'), None)
if not log:
defer.returnValue(None)
lastline = int(max(0, log['num_lines'] - 1))
logLines = yield self.master.db.logs.getLogLines(log['id'], 0, lastline)
if log['type'] == 's':
logLines = '\n'.join([line[1:] for line in logLines.splitlines()])
self.error_logs[name] = logLines
def getStepResult(self, step_name):
for step in self.build.executedSteps:
if step.name == step_name:
return step.results
def getBuildStepByName(self, step_name):
for step in self.build.executedSteps:
if step.name == step_name:
return step
return None
def filter_logs_containing_error(self, logs, searchString='rror:', max_num_lines=10):
if not logs:
return None
filtered_logs = []
for line in logs.splitlines():
if searchString in line:
filtered_logs.append(line)
return '\n'.join(filtered_logs[-max_num_lines:])
def send_email_for_new_build_failure(self):
try:
patch_id = self.getProperty('patch_id', '')
if not self.should_send_email(patch_id):
return
builder_name = self.getProperty('buildername', '')
bug_id = self.getProperty('bug_id', '')
bug_title = self.getProperty('bug_title', '')
worker_name = self.getProperty('workername', '')
patch_author = self.getProperty('patch_author', '')
platform = self.getProperty('platform', '')
build_url = '{}#/builders/{}/builds/{}'.format(self.master.config.buildbotURL, self.build._builderid, self.build.number)
logs = self.error_logs.get(self.compile_webkit_step)
if platform in ['win', 'wincairo']:
logs = self.filter_logs_containing_error(logs, searchString='error ')
else:
logs = self.filter_logs_containing_error(logs)
email_subject = 'Build failure for Patch {}: {}'.format(patch_id, bug_title)
email_text = 'EWS has detected build failure on {}'.format(builder_name)
email_text += ' while testing <a href="{}">Patch {}</a>'.format(Bugzilla.patch_url(patch_id), patch_id)
email_text += ' for <a href="{}">Bug {}</a>.'.format(Bugzilla.bug_url(bug_id), bug_id)
email_text += '\n\nFull details are available at: {}\n\nPatch author: {}'.format(build_url, patch_author)
if logs:
logs = logs.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
email_text += '\n\nError lines:\n\n<code>{}</code>'.format(logs)
email_text += '\n\nTo unsubscrible from these notifications or to provide any feedback please email aakash_jain@apple.com'
self._addToLog('stdio', 'Sending email notification to {}'.format(patch_author))
send_email_to_patch_author(patch_author, email_subject, email_text, patch_id)
except Exception as e:
print('Error in sending email for new build failure: {}'.format(e))
def send_email_for_preexisting_build_failure(self):
try:
builder_name = self.getProperty('buildername', '')
worker_name = self.getProperty('workername', '')
platform = self.getProperty('platform', '')
build_url = '{}#/builders/{}/builds/{}'.format(self.master.config.buildbotURL, self.build._builderid, self.build.number)
logs = self.error_logs.get(self.compile_webkit_step)
if platform in ['win', 'wincairo']:
logs = self.filter_logs_containing_error(logs, searchString='error ')
else:
logs = self.filter_logs_containing_error(logs)
email_subject = 'Build failure on trunk on {}'.format(builder_name)
email_text = 'Failed to build WebKit without patch in {}\n\nBuilder: {}\n\nWorker: {}'.format(build_url, builder_name, worker_name)
if logs:
logs = logs.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
email_text += '\n\nError lines:\n\n<code>{}</code>'.format(logs)
reference = 'preexisting-build-failure-{}-{}'.format(builder_name, date.today().strftime("%Y-%d-%m"))
send_email_to_bot_watchers(email_subject, email_text, builder_name, reference)
except Exception as e:
print('Error in sending email for build failure: {}'.format(e))
class CompileJSC(CompileWebKit):
name = 'compile-jsc'
descriptionDone = ['Compiled JSC']
command = ['perl', 'Tools/Scripts/build-jsc', WithProperties('--%(configuration)s')]
def start(self):
self.setProperty('group', 'jsc')
return CompileWebKit.start(self)
def getResultSummary(self):
if self.results == FAILURE:
return {'step': 'Failed to compile JSC'}
return shell.Compile.getResultSummary(self)
class CompileJSCWithoutPatch(CompileJSC):
name = 'compile-jsc-without-patch'
def evaluateCommand(self, cmd):
return shell.Compile.evaluateCommand(self, cmd)
class RunJavaScriptCoreTests(shell.Test):
name = 'jscore-test'
description = ['jscore-tests running']
descriptionDone = ['jscore-tests']
flunkOnFailure = True
jsonFileName = 'jsc_results.json'
logfiles = {'json': jsonFileName}
command = ['perl', 'Tools/Scripts/run-javascriptcore-tests', '--no-build', '--no-fail-fast', '--json-output={0}'.format(jsonFileName), WithProperties('--%(configuration)s')]
prefix = 'jsc_'
NUM_FAILURES_TO_DISPLAY_IN_STATUS = 5
def __init__(self, **kwargs):
shell.Test.__init__(self, logEnviron=False, sigtermTime=10, **kwargs)
self.binaryFailures = []
self.stressTestFailures = []
def start(self):
self.log_observer_json = logobserver.BufferLogObserver()
self.addLogObserver('json', self.log_observer_json)
# add remotes configuration file path to the command line if needed
remotesfile = self.getProperty('remotes', False)
if remotesfile:
self.command.append('--remote-config-file={0}'.format(remotesfile))
platform = self.getProperty('platform')
if platform == 'jsc-only' and remotesfile:
# FIXME: the bundle copied to the remote should include the testair, testb3, testapi, etc.. binaries
self.command.extend(['--no-testmasm', '--no-testair', '--no-testb3', '--no-testdfg', '--no-testapi'])
# Linux bots have currently problems with JSC tests that try to use large amounts of memory.
# Check: https://bugs.webkit.org/show_bug.cgi?id=175140
if platform in ('gtk', 'wpe', 'jsc-only'):
self.command.extend(['--memory-limited', '--verbose'])
appendCustomBuildFlags(self, self.getProperty('platform'), self.getProperty('fullPlatform'))
return shell.Test.start(self)
def evaluateCommand(self, cmd):
rc = shell.Test.evaluateCommand(self, cmd)
if rc == SUCCESS or rc == WARNINGS:
message = 'Passed JSC tests'
self.descriptionDone = message
self.build.results = SUCCESS
self.build.buildFinished([message], SUCCESS)
else:
self.build.addStepsAfterCurrentStep([ValidatePatch(verifyBugClosed=False, addURLs=False), KillOldProcesses(), ReRunJavaScriptCoreTests()])
return rc
def commandComplete(self, cmd):
shell.Test.commandComplete(self, cmd)
logLines = self.log_observer_json.getStdout()
json_text = ''.join([line for line in logLines.splitlines()])
try:
jsc_results = json.loads(json_text)
except Exception as ex:
self._addToLog('stderr', 'ERROR: unable to parse data, exception: {}'.format(ex))
return
if jsc_results.get('allMasmTestsPassed') is False:
self.binaryFailures.append('testmasm')
if jsc_results.get('allAirTestsPassed') is False:
self.binaryFailures.append('testair')
if jsc_results.get('allB3TestsPassed') is False:
self.binaryFailures.append('testb3')
if jsc_results.get('allDFGTestsPassed') is False:
self.binaryFailures.append('testdfg')
if jsc_results.get('allApiTestsPassed') is False:
self.binaryFailures.append('testapi')
self.stressTestFailures = jsc_results.get('stressTestFailures')
if self.stressTestFailures:
self.setProperty(self.prefix + 'stress_test_failures', self.stressTestFailures)
if self.binaryFailures:
self.setProperty(self.prefix + 'binary_failures', self.binaryFailures)
def getResultSummary(self):
if self.results != SUCCESS and (self.stressTestFailures or self.binaryFailures):
status = ''
if self.stressTestFailures:
num_failures = len(self.stressTestFailures)
pluralSuffix = 's' if num_failures > 1 else ''
failures_to_display = self.stressTestFailures[:self.NUM_FAILURES_TO_DISPLAY_IN_STATUS]
status = 'Found {} jsc stress test failure{}: '.format(num_failures, pluralSuffix) + ', '.join(failures_to_display)
if num_failures > self.NUM_FAILURES_TO_DISPLAY_IN_STATUS:
status += ' ...'
if self.binaryFailures:
if status:
status += ', '
pluralSuffix = 's' if len(self.binaryFailures) > 1 else ''
status += 'JSC test binary failure{}: {}'.format(pluralSuffix, ', '.join(self.binaryFailures))
return {'step': status}
return shell.Test.getResultSummary(self)
@defer.inlineCallbacks
def _addToLog(self, logName, message):
try:
log = self.getLog(logName)
except KeyError:
log = yield self.addLog(logName)
log.addStdout(message)
class ReRunJavaScriptCoreTests(RunJavaScriptCoreTests):
name = 'jscore-test-rerun'
prefix = 'jsc_rerun_'
def evaluateCommand(self, cmd):
rc = shell.Test.evaluateCommand(self, cmd)
first_run_failures = set(self.getProperty('jsc_stress_test_failures', []) + self.getProperty('jsc_binary_failures', []))
second_run_failures = set(self.getProperty('jsc_rerun_stress_test_failures', []) + self.getProperty('jsc_rerun_binary_failures', []))
flaky_failures = first_run_failures.union(second_run_failures) - first_run_failures.intersection(second_run_failures)
flaky_failures_string = ', '.join(sorted(flaky_failures))
if rc == SUCCESS or rc == WARNINGS:
pluralSuffix = 's' if len(flaky_failures) > 1 else ''
message = 'Found flaky test{}: {}'.format(pluralSuffix, flaky_failures_string)
self.descriptionDone = message
self.build.results = SUCCESS
self.build.buildFinished([message], SUCCESS)
else:
self.setProperty('patchFailedTests', True)
self.build.addStepsAfterCurrentStep([UnApplyPatchIfRequired(),
ValidatePatch(verifyBugClosed=False, addURLs=False),
CompileJSCWithoutPatch(),
ValidatePatch(verifyBugClosed=False, addURLs=False),
KillOldProcesses(),
RunJSCTestsWithoutPatch(),
AnalyzeJSCTestsResults()])
return rc
class RunJSCTestsWithoutPatch(RunJavaScriptCoreTests):
name = 'jscore-test-without-patch'
prefix = 'jsc_clean_tree_'
def evaluateCommand(self, cmd):
rc = shell.Test.evaluateCommand(self, cmd)
self.setProperty('clean_tree_run_status', rc)
return rc
class AnalyzeJSCTestsResults(buildstep.BuildStep):
name = 'analyze-jsc-tests-results'
description = ['analyze-jsc-test-results']
descriptionDone = ['analyze-jsc-tests-results']
NUM_FAILURES_TO_DISPLAY = 10
def start(self):
first_run_stress_failures = set(self.getProperty('jsc_stress_test_failures', []))
first_run_binary_failures = set(self.getProperty('jsc_binary_failures', []))
second_run_stress_failures = set(self.getProperty('jsc_rerun_stress_test_failures', []))
second_run_binary_failures = set(self.getProperty('jsc_rerun_binary_failures', []))
clean_tree_stress_failures = set(self.getProperty('jsc_clean_tree_stress_test_failures', []))
clean_tree_binary_failures = set(self.getProperty('jsc_clean_tree_binary_failures', []))
clean_tree_failures = list(clean_tree_binary_failures) + list(clean_tree_stress_failures)
clean_tree_failures_string = ', '.join(clean_tree_failures[:self.NUM_FAILURES_TO_DISPLAY])
stress_failures_with_patch = first_run_stress_failures.intersection(second_run_stress_failures)
binary_failures_with_patch = first_run_binary_failures.intersection(second_run_binary_failures)
flaky_stress_failures = first_run_stress_failures.union(second_run_stress_failures) - first_run_stress_failures.intersection(second_run_stress_failures)
flaky_binary_failures = first_run_binary_failures.union(second_run_binary_failures) - first_run_binary_failures.intersection(second_run_binary_failures)
flaky_failures = sorted(list(flaky_binary_failures) + list(flaky_stress_failures))[:self.NUM_FAILURES_TO_DISPLAY]
flaky_failures_string = ', '.join(flaky_failures)
new_stress_failures = stress_failures_with_patch - clean_tree_stress_failures
new_binary_failures = binary_failures_with_patch - clean_tree_binary_failures
self.new_stress_failures_to_display = ', '.join(sorted(list(new_stress_failures))[:self.NUM_FAILURES_TO_DISPLAY])
self.new_binary_failures_to_display = ', '.join(sorted(list(new_binary_failures))[:self.NUM_FAILURES_TO_DISPLAY])
self._addToLog('stderr', '\nFailures in first run: {}'.format((list(first_run_binary_failures) + list(first_run_stress_failures))[:self.NUM_FAILURES_TO_DISPLAY]))
self._addToLog('stderr', '\nFailures in second run: {}'.format((list(second_run_binary_failures) + list(second_run_stress_failures))[:self.NUM_FAILURES_TO_DISPLAY]))
self._addToLog('stderr', '\nFlaky Tests: {}'.format(flaky_failures_string))
self._addToLog('stderr', '\nFailures on clean tree: {}'.format(clean_tree_failures_string))
if (not first_run_stress_failures) and (not first_run_binary_failures) and (not second_run_stress_failures) and (not second_run_binary_failures):
# If we've made it here, then jsc-tests and re-run-jsc-tests failed, which means
# there should have been some test failures. Otherwise there is some unexpected issue.
clean_tree_run_status = self.getProperty('clean_tree_run_status', FAILURE)
if clean_tree_run_status == SUCCESS:
return self.report_failure(set(), set())
# TODO: email EWS admins
return self.retry_build('Unexpected infrastructure issue, retrying build')
if new_stress_failures or new_binary_failures:
self._addToLog('stderr', '\nNew binary failures: {}.\nNew stress test failures: {}\n'.format(self.new_binary_failures_to_display, self.new_stress_failures_to_display))
return self.report_failure(new_binary_failures, new_stress_failures)
else:
self._addToLog('stderr', '\nNo new failures\n')
self.finished(SUCCESS)
self.build.results = SUCCESS
self.descriptionDone = 'Passed JSC tests'
pluralSuffix = 's' if len(clean_tree_failures) > 1 else ''
message = ''
if clean_tree_failures:
message = 'Found {} pre-existing JSC test failure{}: {}'.format(len(clean_tree_failures), pluralSuffix, clean_tree_failures_string)
for clean_tree_failure in clean_tree_failures[:self.NUM_FAILURES_TO_DISPLAY]:
self.send_email_for_pre_existing_failure(clean_tree_failure)
if len(clean_tree_failures) > self.NUM_FAILURES_TO_DISPLAY:
message += ' ...'
if flaky_failures:
message += ' Found flaky tests: {}'.format(flaky_failures_string)
for flaky_failure in flaky_failures:
self.send_email_for_flaky_failure(flaky_failure)
self.build.buildFinished([message], SUCCESS)
return defer.succeed(None)
def retry_build(self, message):
self.descriptionDone = message
self.finished(RETRY)
self.build.buildFinished([message], RETRY)
return defer.succeed(None)
def report_failure(self, new_binary_failures, new_stress_failures):
message = ''
if (not new_binary_failures) and (not new_stress_failures):
message = 'Found unexpected failure with patch'
if new_binary_failures:
pluralSuffix = 's' if len(new_binary_failures) > 1 else ''
message = 'Found {} new JSC binary failure{}: {}'.format(len(new_binary_failures), pluralSuffix, self.new_binary_failures_to_display)
if new_stress_failures:
if message:
message += ', '
pluralSuffix = 's' if len(new_stress_failures) > 1 else ''
message += 'Found {} new JSC stress test failure{}: {}'.format(len(new_stress_failures), pluralSuffix, self.new_stress_failures_to_display)
if len(new_stress_failures) > self.NUM_FAILURES_TO_DISPLAY:
message += ' ...'
self.finished(FAILURE)
self.build.results = FAILURE
self.descriptionDone = message
self.build.buildFinished([message], FAILURE)
return defer.succeed(None)
@defer.inlineCallbacks
def _addToLog(self, logName, message):
try:
log = self.getLog(logName)
except KeyError:
log = yield self.addLog(logName)
log.addStdout(message)
def send_email_for_flaky_failure(self, test_name):
try:
builder_name = self.getProperty('buildername', '')
worker_name = self.getProperty('workername', '')
build_url = '{}#/builders/{}/builds/{}'.format(self.master.config.buildbotURL, self.build._builderid, self.build.number)
history_url = '{}?suite=javascriptcore-tests&test={}'.format(RESULTS_DB_URL, test_name)
email_subject = 'Flaky test: {}'.format(test_name)
email_text = 'Flaky test: {}\n\nBuild: {}\n\nBuilder: {}\n\nWorker: {}\n\nHistory: {}'.format(test_name, build_url, builder_name, worker_name, history_url)
send_email_to_bot_watchers(email_subject, email_text, builder_name, 'flaky-{}'.format(test_name))
except Exception as e:
print('Error in sending email for flaky failure: {}'.format(e))
def send_email_for_pre_existing_failure(self, test_name):
try:
builder_name = self.getProperty('buildername', '')
worker_name = self.getProperty('workername', '')
build_url = '{}#/builders/{}/builds/{}'.format(self.master.config.buildbotURL, self.build._builderid, self.build.number)
history_url = '{}?suite=javascriptcore-tests&test={}'.format(RESULTS_DB_URL, test_name)
email_subject = 'Pre-existing test failure: {}'.format(test_name)
email_text = 'Test {} failed on clean tree run in {}.\n\nBuilder: {}\n\nWorker: {}\n\nHistory: {}'.format(test_name, build_url, builder_name, worker_name, history_url)
send_email_to_bot_watchers(email_subject, email_text, builder_name, 'preexisting-{}'.format(test_name))
except Exception as e:
print('Error in sending email for pre-existing failure: {}'.format(e))
class InstallBuiltProduct(shell.ShellCommand):
name = 'install-built-product'
description = ['Installing Built Product']
descriptionDone = ['Installed Built Product']
command = ["python3", "Tools/Scripts/install-built-product",
WithProperties("--platform=%(fullPlatform)s"), WithProperties("--%(configuration)s")]
class CleanBuild(shell.Compile):
name = 'delete-WebKitBuild-directory'
description = ['deleting WebKitBuild directory']
descriptionDone = ['Deleted WebKitBuild directory']
command = ['python3', 'Tools/CISupport/clean-build', WithProperties('--platform=%(fullPlatform)s'), WithProperties('--%(configuration)s')]
class KillOldProcesses(shell.Compile):
name = 'kill-old-processes'
description = ['killing old processes']
descriptionDone = ['Killed old processes']
command = ['python3', 'Tools/CISupport/kill-old-processes', 'buildbot']
def __init__(self, **kwargs):
super(KillOldProcesses, self).__init__(timeout=2 * 60, logEnviron=False, **kwargs)
def evaluateCommand(self, cmd):
rc = shell.Compile.evaluateCommand(self, cmd)
if rc in [FAILURE, EXCEPTION]:
self.build.buildFinished(['Failed to kill old processes, retrying build'], RETRY)
return rc
def getResultSummary(self):
if self.results in [FAILURE, EXCEPTION]:
return {'step': 'Failed to kill old processes'}
return shell.Compile.getResultSummary(self)
class TriggerCrashLogSubmission(shell.Compile):
name = 'trigger-crash-log-submission'
description = ['triggering crash log submission']
descriptionDone = ['Triggered crash log submission']
command = ['python3', 'Tools/CISupport/trigger-crash-log-submission']
def __init__(self, **kwargs):
super(TriggerCrashLogSubmission, self).__init__(timeout=60, logEnviron=False, **kwargs)
def getResultSummary(self):
if self.results in [FAILURE, EXCEPTION]:
return {'step': 'Failed to trigger crash log submission'}
return shell.Compile.getResultSummary(self)
class WaitForCrashCollection(shell.Compile):
name = 'wait-for-crash-collection'
description = ['waiting-for-crash-collection-to-quiesce']
descriptionDone = ['Crash collection has quiesced']
command = ['python3', 'Tools/CISupport/wait-for-crash-collection', '--timeout', str(5 * 60)]
def __init__(self, **kwargs):
super(WaitForCrashCollection, self).__init__(timeout=6 * 60, logEnviron=False, **kwargs)
def getResultSummary(self):
if self.results in [FAILURE, EXCEPTION]:
return {'step': 'Crash log collection process still running'}
return shell.Compile.getResultSummary(self)
class RunWebKitTests(shell.Test):
name = 'layout-tests'
description = ['layout-tests running']
descriptionDone = ['layout-tests']
resultDirectory = 'layout-test-results'
jsonFileName = 'layout-test-results/full_results.json'
logfiles = {'json': jsonFileName}
test_failures_log_name = 'test-failures'
ENABLE_GUARD_MALLOC = False
EXIT_AFTER_FAILURES = '30'
command = ['python', 'Tools/Scripts/run-webkit-tests',
'--no-build',
'--no-show-results',
'--no-new-test-results',
'--clobber-old-results',
WithProperties('--%(configuration)s')]
def __init__(self, **kwargs):
shell.Test.__init__(self, logEnviron=False, **kwargs)
self.incorrectLayoutLines = []
def _get_patch(self):
sourcestamp = self.build.getSourceStamp(self.getProperty('codebase', ''))
if not sourcestamp or not sourcestamp.patch:
return None
return sourcestamp.patch[1]
def doStepIf(self, step):
return not ((self.getProperty('buildername', '').lower() == 'commit-queue') and
(self.getProperty('fast_commit_queue') or self.getProperty('passed_mac_wk2')))
def setLayoutTestCommand(self):
platform = self.getProperty('platform')
appendCustomBuildFlags(self, platform, self.getProperty('fullPlatform'))
additionalArguments = self.getProperty('additionalArguments')
if self.getProperty('use-dump-render-tree', False):
self.setCommand(self.command + ['--dump-render-tree'])
self.setCommand(self.command + ['--results-directory', self.resultDirectory])
self.setCommand(self.command + ['--debug-rwt-logging'])
patch_author = self.getProperty('patch_author')
if patch_author in ['webkit-wpt-import-bot@igalia.com']:
self.setCommand(self.command + ['imported/w3c/web-platform-tests'])
else:
self.setCommand(self.command + ['--exit-after-n-failures', self.EXIT_AFTER_FAILURES, '--skip-failing-tests'])
if additionalArguments:
self.setCommand(self.command + additionalArguments)
if self.ENABLE_GUARD_MALLOC:
self.setCommand(self.command + ['--guard-malloc'])
if self.name == 'run-layout-tests-without-patch':
# In order to speed up testing, on the step that retries running the layout tests without patch
# only run the subset of tests that failed on the previous steps.
# But only do that if the previous steps didn't exceed the test failure limit and the patch doesn't
# modify the TestExpectations files (there are corner cases where we can't guarantee the correctnes
# of this optimization if the patch modifies the TestExpectations files, for example, if the patch
# removes skipped tests but those tests still fail).
first_results_did_exceed_test_failure_limit = self.getProperty('first_results_exceed_failure_limit', False)
second_results_did_exceed_test_failure_limit = self.getProperty('second_results_exceed_failure_limit', False)
if not first_results_did_exceed_test_failure_limit and not second_results_did_exceed_test_failure_limit:
patch_modifies_expectation_files = False
patch = self._get_patch()
if patch:
for line in patch.splitlines():
line = line.strip()
# patch is stored by buildbot as bytes: https://github.com/buildbot/buildbot/issues/5812#issuecomment-790175979
if (b'LayoutTests/' in line and b'TestExpectations' in line) and (line.startswith(b'---') or line.startswith(b'+++')):
patch_modifies_expectation_files = True
break
if not patch_modifies_expectation_files:
first_results_failing_tests = set(self.getProperty('first_run_failures', set()))
second_results_failing_tests = set(self.getProperty('second_run_failures', set()))
list_retry_tests = sorted(first_results_failing_tests.union(second_results_failing_tests))
self.setCommand(self.command + list_retry_tests)
def start(self):
self.log_observer = logobserver.BufferLogObserver(wantStderr=True)
self.addLogObserver('stdio', self.log_observer)
self.log_observer_json = logobserver.BufferLogObserver()
self.addLogObserver('json', self.log_observer_json)
self.setLayoutTestCommand()
return shell.Test.start(self)
# FIXME: This will break if run-webkit-tests changes its default log formatter.
nrwt_log_message_regexp = re.compile(r'\d{2}:\d{2}:\d{2}(\.\d+)?\s+\d+\s+(?P<message>.*)')
def _strip_python_logging_prefix(self, line):
match_object = self.nrwt_log_message_regexp.match(line)
if match_object:
return match_object.group('message')
return line
@defer.inlineCallbacks
def _addToLog(self, logName, message):
try:
log = self.getLog(logName)
except KeyError:
log = yield self.addLog(logName)
log.addStdout(message)
def _parseRunWebKitTestsOutput(self, logText):
incorrectLayoutLines = []
expressions = [
('flakes', re.compile(r'Unexpected flakiness.+\((\d+)\)')),
('new passes', re.compile(r'Expected to .+, but passed:\s+\((\d+)\)')),
('missing results', re.compile(r'Regressions: Unexpected missing results\s+\((\d+)\)')),
('failures', re.compile(r'Regressions: Unexpected.+\((\d+)\)')),
]
testFailures = {}
for line in logText.splitlines():
if line.find('Exiting early') >= 0 or line.find('leaks found') >= 0:
incorrectLayoutLines.append(self._strip_python_logging_prefix(line))
continue
for name, expression in expressions:
match = expression.search(line)
if match:
testFailures[name] = testFailures.get(name, 0) + int(match.group(1))
break
# FIXME: Parse file names and put them in results
for name in testFailures:
incorrectLayoutLines.append(str(testFailures[name]) + ' ' + name)
self.incorrectLayoutLines = incorrectLayoutLines
def commandComplete(self, cmd):
shell.Test.commandComplete(self, cmd)
logText = self.log_observer.getStdout() + self.log_observer.getStderr()
logTextJson = self.log_observer_json.getStdout()
first_results = LayoutTestFailures.results_from_string(logTextJson)
if first_results:
self.setProperty('first_results_exceed_failure_limit', first_results.did_exceed_test_failure_limit)
self.setProperty('first_run_failures', sorted(first_results.failing_tests))
if first_results.failing_tests:
self._addToLog(self.test_failures_log_name, '\n'.join(first_results.failing_tests))
self._parseRunWebKitTestsOutput(logText)
def evaluateResult(self, cmd):
result = SUCCESS
if self.incorrectLayoutLines:
if len(self.incorrectLayoutLines) == 1:
line = self.incorrectLayoutLines[0]
if line.find('were new') >= 0 or line.find('was new') >= 0 or line.find(' leak') >= 0:
return WARNINGS
for line in self.incorrectLayoutLines:
if line.find('flakes') >= 0 or line.find('new passes') >= 0:
result = WARNINGS
elif line.find('missing results') >= 0:
return FAILURE
else:
return FAILURE
if cmd.rc != 0:
return FAILURE
return result
def evaluateCommand(self, cmd):
rc = self.evaluateResult(cmd)
if rc == SUCCESS or rc == WARNINGS:
message = 'Passed layout tests'
self.descriptionDone = message
self.build.results = SUCCESS
self.setProperty('build_summary', message)
else:
self.build.addStepsAfterCurrentStep([
ArchiveTestResults(),
UploadTestResults(),
ExtractTestResults(),
ValidatePatch(verifyBugClosed=False, addURLs=False),
KillOldProcesses(),
ReRunWebKitTests(),
])
return rc
def getResultSummary(self):
status = self.name
if self.results != SUCCESS and self.incorrectLayoutLines:
status = ' '.join(self.incorrectLayoutLines)
return {'step': status}
if self.results == SKIPPED:
if self.getProperty('fast_commit_queue'):
return {'step': 'Skipped layout-tests in fast-cq mode'}
return {'step': 'Skipped layout-tests'}
return super(RunWebKitTests, self).getResultSummary()
class RunWebKitTestsInStressMode(RunWebKitTests):
name = 'run-layout-tests-in-stress-mode'
suffix = 'stress-mode'
EXIT_AFTER_FAILURES = '10'
def __init__(self, num_iterations=100):
self.num_iterations = num_iterations
super(RunWebKitTestsInStressMode, self).__init__()
def setLayoutTestCommand(self):
RunWebKitTests.setLayoutTestCommand(self)
self.setCommand(self.command + ['--iterations', self.num_iterations])
modified_tests = self.getProperty('modified_tests')
if modified_tests:
self.setCommand(self.command + modified_tests)
def evaluateCommand(self, cmd):
rc = self.evaluateResult(cmd)
if rc == SUCCESS or rc == WARNINGS:
message = 'Passed layout tests'
self.descriptionDone = message
self.build.results = SUCCESS
self.setProperty('build_summary', message)
else:
self.setProperty('build_summary', 'Found test failures')
self.build.addStepsAfterCurrentStep([
ArchiveTestResults(),
UploadTestResults(identifier=self.suffix),
ExtractTestResults(identifier=self.suffix),
])
return rc
def doStepIf(self, step):
return self.getProperty('modified_tests', False)
class RunWebKitTestsInStressGuardmallocMode(RunWebKitTestsInStressMode):
name = 'run-layout-tests-in-guard-malloc-stress-mode'
suffix = 'guard-malloc'
ENABLE_GUARD_MALLOC = True
class ReRunWebKitTests(RunWebKitTests):
name = 're-run-layout-tests'
NUM_FAILURES_TO_DISPLAY = 10
def evaluateCommand(self, cmd):
rc = self.evaluateResult(cmd)
first_results_did_exceed_test_failure_limit = self.getProperty('first_results_exceed_failure_limit')
first_results_failing_tests = set(self.getProperty('first_run_failures', []))
second_results_did_exceed_test_failure_limit = self.getProperty('second_results_exceed_failure_limit')
second_results_failing_tests = set(self.getProperty('second_run_failures', []))
tests_that_consistently_failed = first_results_failing_tests.intersection(second_results_failing_tests)
flaky_failures = first_results_failing_tests.union(second_results_failing_tests) - first_results_failing_tests.intersection(second_results_failing_tests)
flaky_failures = sorted(list(flaky_failures))[:self.NUM_FAILURES_TO_DISPLAY]
flaky_failures_string = ', '.join(flaky_failures)
if rc == SUCCESS or rc == WARNINGS:
message = 'Passed layout tests'
self.descriptionDone = message
self.build.results = SUCCESS
if (not first_results_did_exceed_test_failure_limit) and flaky_failures:
pluralSuffix = 's' if len(flaky_failures) > 1 else ''
message = 'Found flaky test{}: {}'.format(pluralSuffix, flaky_failures_string)
for flaky_failure in flaky_failures:
self.send_email_for_flaky_failure(flaky_failure)
self.setProperty('build_summary', message)
else:
self.setProperty('patchFailedTests', True)
self.build.addStepsAfterCurrentStep([ArchiveTestResults(),
UploadTestResults(identifier='rerun'),
ExtractTestResults(identifier='rerun'),
UnApplyPatchIfRequired(),
ValidatePatch(verifyBugClosed=False, addURLs=False),
CompileWebKitWithoutPatch(retry_build_on_failure=True),
ValidatePatch(verifyBugClosed=False, addURLs=False),
KillOldProcesses(),
RunWebKitTestsWithoutPatch()])
return rc
def commandComplete(self, cmd):
shell.Test.commandComplete(self, cmd)
logText = self.log_observer.getStdout() + self.log_observer.getStderr()
logTextJson = self.log_observer_json.getStdout()
second_results = LayoutTestFailures.results_from_string(logTextJson)
if second_results:
self.setProperty('second_results_exceed_failure_limit', second_results.did_exceed_test_failure_limit)
self.setProperty('second_run_failures', sorted(second_results.failing_tests))
if second_results.failing_tests:
self._addToLog(self.test_failures_log_name, '\n'.join(second_results.failing_tests))
self._parseRunWebKitTestsOutput(logText)
def send_email_for_flaky_failure(self, test_name):
try:
builder_name = self.getProperty('buildername', '')
worker_name = self.getProperty('workername', '')
build_url = '{}#/builders/{}/builds/{}'.format(self.master.config.buildbotURL, self.build._builderid, self.build.number)
history_url = '{}?suite=layout-tests&test={}'.format(RESULTS_DB_URL, test_name)
email_subject = 'Flaky test: {}'.format(test_name)
email_text = 'Test {} flaked in {}\n\nBuilder: {}'.format(test_name, build_url, builder_name)
email_text = 'Flaky test: {}\n\nBuild: {}\n\nBuilder: {}\n\nWorker: {}\n\nHistory: {}'.format(test_name, build_url, builder_name, worker_name, history_url)
send_email_to_bot_watchers(email_subject, email_text, builder_name, 'flaky-{}'.format(test_name))
except Exception as e:
# Catching all exceptions here to ensure that failure to send email doesn't impact the build
print('Error in sending email for flaky failures: {}'.format(e))
class RunWebKitTestsWithoutPatch(RunWebKitTests):
name = 'run-layout-tests-without-patch'
def evaluateCommand(self, cmd):
rc = shell.Test.evaluateCommand(self, cmd)
self.build.addStepsAfterCurrentStep([ArchiveTestResults(), UploadTestResults(identifier='clean-tree'), ExtractTestResults(identifier='clean-tree'), AnalyzeLayoutTestsResults()])
self.setProperty('clean_tree_run_status', rc)
return rc
def commandComplete(self, cmd):
shell.Test.commandComplete(self, cmd)
logText = self.log_observer.getStdout() + self.log_observer.getStderr()
logTextJson = self.log_observer_json.getStdout()
clean_tree_results = LayoutTestFailures.results_from_string(logTextJson)
if clean_tree_results:
self.setProperty('clean_tree_results_exceed_failure_limit', clean_tree_results.did_exceed_test_failure_limit)
self.setProperty('clean_tree_run_failures', clean_tree_results.failing_tests)
if clean_tree_results.failing_tests:
self._addToLog(self.test_failures_log_name, '\n'.join(clean_tree_results.failing_tests))
self._parseRunWebKitTestsOutput(logText)
class AnalyzeLayoutTestsResults(buildstep.BuildStep, BugzillaMixin):
name = 'analyze-layout-tests-results'
description = ['analyze-layout-test-results']
descriptionDone = ['analyze-layout-tests-results']
NUM_FAILURES_TO_DISPLAY = 10
def report_failure(self, new_failures):
self.finished(FAILURE)
self.build.results = FAILURE
if not new_failures:
message = 'Found unexpected failure with patch'
else:
pluralSuffix = 's' if len(new_failures) > 1 else ''
new_failures_string = ', '.join(sorted(new_failures)[:self.NUM_FAILURES_TO_DISPLAY])
message = 'Found {} new test failure{}: {}'.format(len(new_failures), pluralSuffix, new_failures_string)
if len(new_failures) > self.NUM_FAILURES_TO_DISPLAY:
message += ' ...'
self.send_email_for_new_test_failures(new_failures)
self.descriptionDone = message
self.setProperty('build_finish_summary', message)
if self.getProperty('buildername', '').lower() == 'commit-queue':
self.setProperty('bugzilla_comment_text', message)
self.build.addStepsAfterCurrentStep([CommentOnBug(), SetCommitQueueMinusFlagOnPatch()])
else:
self.build.addStepsAfterCurrentStep([SetCommitQueueMinusFlagOnPatch()])
return defer.succeed(None)
def report_pre_existing_failures(self, clean_tree_failures, flaky_failures):
self.finished(SUCCESS)
self.build.results = SUCCESS
self.descriptionDone = 'Passed layout tests'
message = ''
if clean_tree_failures:
clean_tree_failures_string = ', '.join(sorted(clean_tree_failures)[:self.NUM_FAILURES_TO_DISPLAY])
pluralSuffix = 's' if len(clean_tree_failures) > 1 else ''
message = 'Found {} pre-existing test failure{}: {}'.format(len(clean_tree_failures), pluralSuffix, clean_tree_failures_string)
if len(clean_tree_failures) > self.NUM_FAILURES_TO_DISPLAY:
message += ' ...'
for clean_tree_failure in list(clean_tree_failures)[:self.NUM_FAILURES_TO_DISPLAY]:
self.send_email_for_pre_existing_failure(clean_tree_failure)
if flaky_failures:
flaky_failures_string = ', '.join(sorted(flaky_failures)[:self.NUM_FAILURES_TO_DISPLAY])
pluralSuffix = 's' if len(flaky_failures) > 1 else ''
message += ' Found flaky test{}: {}'.format(pluralSuffix, flaky_failures_string)
if len(flaky_failures) > self.NUM_FAILURES_TO_DISPLAY:
message += ' ...'
for flaky_failure in list(flaky_failures)[:self.NUM_FAILURES_TO_DISPLAY]:
self.send_email_for_flaky_failure(flaky_failure)
self.setProperty('build_summary', message)