blob: c0b7867c5494450980be1aa2f7b7257001931b0c [file] [log] [blame]
# Copyright (C) 2018-2022 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 requests.auth import HTTPBasicAuth
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, FROM_EMAIL
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)
custom_suffix = '-uat' if os.getenv('BUILDBOT_UAT') else ''
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/'.format(custom_suffix)
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
GITHUB_URL = 'https://github.com/'
GITHUB_PROJECTS = ['WebKit/WebKit', 'apple/WebKit', 'WebKit/WebKit-security']
HASH_LENGTH_TO_DISPLAY = 8
DEFAULT_BRANCH = 'main'
class BufferLogHeaderObserver(logobserver.BufferLogObserver):
def __init__(self, **kwargs):
self.headers = []
super().__init__(**kwargs)
def headerReceived(self, data):
self.headers.append(data)
def getHeaders(self):
return self._get(self.headers)
class GitHub(object):
_cache = {}
@classmethod
def repository_urls(cls):
return [GITHUB_URL + project for project in GITHUB_PROJECTS]
@classmethod
def user_for_queue(cls, queue):
if queue.lower() in ['commit-queue', 'merge-queue', 'unsafe-merge-queue']:
return 'merge-queue'
return None
@classmethod
def pr_url(cls, pr_number, repository_url=None):
if not repository_url:
repository_url = '{}{}'.format(GITHUB_URL, GITHUB_PROJECTS[0])
if repository_url not in GitHub.repository_urls():
return ''
if not pr_number or not isinstance(pr_number, int):
return ''
return '{}/pull/{}'.format(repository_url, pr_number)
@classmethod
def commit_url(cls, sha, repository_url=None):
if not repository_url:
repository_url = '{}{}'.format(GITHUB_URL, GITHUB_PROJECTS[0])
if repository_url not in GitHub.repository_urls():
return ''
if not sha:
return ''
return '{}/commit/{}'.format(repository_url, sha)
@classmethod
def api_url(cls, repository_url=None):
if not repository_url:
repository_url = '{}{}'.format(GITHUB_URL, GITHUB_PROJECTS[0])
if repository_url not in GitHub.repository_urls():
return ''
_, url_base = repository_url.split('://', 1)
host, path = url_base.split('/', 1)
return 'https://api.{}/repos/{}'.format(host, path)
@classmethod
def commit_status_url(cls, sha, repository_url=None):
api_url = cls.api_url(repository_url)
if not sha or not api_url:
return ''
return '{}/statuses/{}'.format(api_url, sha)
@classmethod
def credentials(cls, user=None):
prefix = f"GITHUB_COM_{user.upper().replace('-', '_')}_" if user else 'GITHUB_COM_'
if prefix in cls._cache:
return cls._cache[prefix]
try:
passwords = json.load(open('passwords.json'))
cls._cache[prefix] = passwords.get(f'{prefix}USERNAME', None), passwords.get(f'{prefix}ACCESS_TOKEN', None)
except Exception as e:
print('Error reading GitHub credentials')
cls._cache[prefix] = None, None
return cls._cache[prefix]
@classmethod
def email_for_owners(cls, owners):
if not owners:
return None, 'No owners defined, so email cannot be extracted'
contributors, errors = Contributors.load()
return contributors.get(owners[0].lower(), {}).get('email'), errors
class GitHubMixin(object):
addURLs = False
pr_open_states = ['open']
pr_closed_states = ['closed']
BLOCKED_LABEL = 'merging-blocked'
MERGE_QUEUE_LABEL = 'merge-queue'
UNSAFE_MERGE_QUEUE_LABEL = 'unsafe-merge-queue'
def fetch_data_from_url_with_authentication_github(self, url):
response = None
try:
username, access_token = GitHub.credentials(user=GitHub.user_for_queue(self.getProperty('buildername', '')))
auth = HTTPBasicAuth(username, access_token) if username and access_token else None
response = requests.get(
url, timeout=60, auth=auth,
headers=dict(Accept='application/vnd.github.v3+json'),
)
if response.status_code // 100 != 2:
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 access token.
self._addToLog('stdio', 'Failed to access {url}.\n'.format(url=url))
return None
return response
def get_pr_json(self, pr_number, repository_url=None, retry=0):
api_url = GitHub.api_url(repository_url)
if not api_url:
return None
pr_url = '{}/pulls/{}'.format(api_url, pr_number)
content = self.fetch_data_from_url_with_authentication_github(pr_url)
if not content:
return None
for attempt in range(retry + 1):
try:
pr_json = content.json()
if pr_json and len(pr_json):
return pr_json
except Exception as e:
self._addToLog('stdio', 'Failed to get pull request data from {}, error: {}'.format(pr_url, e))
self._addToLog('stdio', 'Unable to fetch pull request {}.\n'.format(pr_number))
if attempt > retry:
return None
wait_for = (attempt + 1) * 15
self._addToLog('stdio', 'Backing off for {} seconds before retrying.\n'.format(wait_for))
time.sleep(wait_for)
return None
def get_reviewers(self, pr_number, repository_url=None):
api_url = GitHub.api_url(repository_url)
if not api_url:
return []
reviews_url = f'{api_url}/pulls/{pr_number}/reviews'
content = self.fetch_data_from_url_with_authentication_github(reviews_url)
if not content:
return []
result = []
for review in (content.json() or []):
reviewer = review.get('user', {}).get('login')
if reviewer and review.get('state') == 'APPROVED':
result.append(reviewer)
return result
def _is_pr_closed(self, pr_json):
if not pr_json or not pr_json.get('state'):
self._addToLog('stdio', 'Cannot determine pull request status.\n')
return -1
if pr_json.get('state') in self.pr_closed_states:
return 1
return 0
def _is_hash_outdated(self, pr_json):
pr_sha = (pr_json or {}).get('head', {}).get('sha', '')
if not pr_sha:
self._addToLog('stdio', 'Cannot determine if hash is outdated or not.\n')
return -1
return 0 if pr_sha == self.getProperty('github.head.sha', '?') else 1
def _is_pr_blocked(self, pr_json):
for label in (pr_json or {}).get('labels', {}):
if label.get('name', '') == self.BLOCKED_LABEL:
return 1
return 0
def _is_pr_in_merge_queue(self, pr_json):
for label in (pr_json or {}).get('labels', {}):
if label.get('name', '') in (self.MERGE_QUEUE_LABEL, self.UNSAFE_MERGE_QUEUE_LABEL):
return 1
return 0
def _is_pr_draft(self, pr_json):
if pr_json.get('draft', False):
return 1
return 0
def should_send_email_for_pr(self, pr_number, repository_url=None):
pr_json = self.get_pr_json(pr_number, repository_url=repository_url)
if not pr_json:
return True
if 1 == self._is_hash_outdated(pr_json):
self._addToLog('stdio', 'Skipping email since hash {} on PR #{} is outdated\n'.format(
self.getProperty('github.head.sha', '?')[:HASH_LENGTH_TO_DISPLAY], pr_number,
))
return False
return True
def add_label(self, pr_number, label, repository_url=None):
api_url = GitHub.api_url(repository_url)
if not api_url:
return False
pr_label_url = '{}/issues/{}/labels'.format(api_url, pr_number)
try:
username, access_token = GitHub.credentials(user=GitHub.user_for_queue(self.getProperty('buildername', '')))
auth = HTTPBasicAuth(username, access_token) if username and access_token else None
response = requests.request(
'POST', pr_label_url, timeout=60, auth=auth,
headers=dict(Accept='application/vnd.github.v3+json'),
json=dict(labels=[label]),
)
if response.status_code // 100 != 2:
self._addToLog('stdio', f"Unable to add '{label}' label on PR {pr_number}. Unexpected response code from GitHub: {response.status_code}\n")
return False
except Exception as e:
self._addToLog('stdio', f"Error in adding '{label}' label on PR {pr_number}\n")
return False
return True
def remove_labels(self, pr_number, labels=None, repository_url=None):
labels = labels or []
if not labels:
return True
api_url = GitHub.api_url(repository_url)
if not api_url:
return False
pr_label_url = f'{api_url}/issues/{pr_number}/labels'
content = self.fetch_data_from_url_with_authentication_github(pr_label_url)
if not content:
self._addToLog('stdio', "Failed to fetch existing labels, cannot remove labels\n")
return True
existing_labels = [label.get('name') for label in (content.json() or [])]
new_labels = list(filter(lambda label: label not in labels, existing_labels))
if len(existing_labels) == len(new_labels):
return True
try:
username, access_token = GitHub.credentials(user=GitHub.user_for_queue(self.getProperty('buildername', '')))
auth = HTTPBasicAuth(username, access_token) if username and access_token else None
response = requests.request(
'PUT', pr_label_url, timeout=60, auth=auth,
headers=dict(Accept='application/vnd.github.v3+json'),
json=dict(labels=new_labels),
)
if response.status_code // 100 != 2:
for label in labels:
self._addToLog('stdio', f"Unable to remove '{label}' label on PR {pr_number}. Unexpected response code from GitHub: {response.status_code}\n")
return False
except Exception as e:
for label in labels:
self._addToLog('stdio', f"Error in removing '{label}' label on PR {pr_number}\n")
return False
return True
def comment_on_pr(self, pr_number, content, repository_url=None):
api_url = GitHub.api_url(repository_url)
if not api_url:
return False
comment_url = f'{api_url}/issues/{pr_number}/comments'
try:
username, access_token = GitHub.credentials(user=GitHub.user_for_queue(self.getProperty('buildername', '')))
auth = HTTPBasicAuth(username, access_token) if username and access_token else None
response = requests.request(
'POST', comment_url, timeout=60, auth=auth,
headers=dict(Accept='application/vnd.github.v3+json'),
json=dict(body=content),
)
if response.status_code // 100 != 2:
self._addToLog('stdio', f"Failed to post comment to PR {pr_number}. Unexpected response code from GitHub: {response.status_code}\n")
return False
except Exception as e:
self._addToLog('stdio', f"Error in posting comment to PR {pr_number}\n")
return False
return True
def update_pr(self, pr_number, title, description, base=None, head=None, repository_url=None):
api_url = GitHub.api_url(repository_url)
if not api_url:
return False
pr_info = dict(
title=title,
body=description,
)
if base:
pr_info['base'] = base
if head:
pr_info['head'] = head
update_url = f'{api_url}/pulls/{pr_number}'
try:
username, access_token = GitHub.credentials(user=GitHub.user_for_queue(self.getProperty('buildername', '')))
auth = HTTPBasicAuth(username, access_token) if username and access_token else None
response = requests.request(
'POST', update_url, timeout=60, auth=auth,
headers=dict(Accept='application/vnd.github.v3+json'),
json=pr_info,
)
if response.status_code // 100 != 2:
self._addToLog('stdio', f"Failed to update PR {pr_number}. Unexpected response code from GitHub: {response.status_code}\n")
return False
except Exception as e:
self._addToLog('stdio', f"Error in updating PR {pr_number}\n")
return False
return True
def close_pr(self, pr_number, repository_url=None):
api_url = GitHub.api_url(repository_url)
if not api_url:
return False
update_url = f'{api_url}/pulls/{pr_number}'
try:
username, access_token = GitHub.credentials(user=GitHub.user_for_queue(self.getProperty('buildername', '')))
auth = HTTPBasicAuth(username, access_token) if username and access_token else None
response = requests.request(
'POST', update_url, timeout=60, auth=auth,
headers=dict(Accept='application/vnd.github.v3+json'),
json=dict(state='closed'),
)
if response.status_code // 100 != 2:
self._addToLog('stdio', f"Failed to close PR {pr_number}. Unexpected response code from GitHub: {response.status_code}\n")
return False
except Exception as e:
self._addToLog('stdio', f"Error in closing PR {pr_number}\n")
return False
return True
class ShellMixin(object):
WINDOWS_SHELL_PLATFORMS = ['wincairo']
def has_windows_shell(self):
return self.getProperty('platform', '*') in self.WINDOWS_SHELL_PLATFORMS
def shell_command(self, command):
if self.has_windows_shell():
return ['sh', '-c', command]
return ['/bin/sh', '-c', command]
def shell_exit_0(self):
if self.has_windows_shell():
return 'exit 0'
return 'true'
class AddToLogMixin(object):
@defer.inlineCallbacks
def _addToLog(self, logName, message):
try:
log = self.getLog(logName)
except KeyError:
log = yield self.addLog(logName)
log.addStdout(message)
class Contributors(object):
url = 'https://raw.githubusercontent.com/WebKit/WebKit/main/metadata/contributors.json'
contributors = {}
last_update = 0
REFRESH = 4 * 60 * 60
@classmethod
def load_from_disk(cls):
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), None
except Exception as e:
return {}, 'Failed to load {}\n'.format(contributors_path)
@classmethod
def load_from_github(cls):
try:
response = requests.get(cls.url, timeout=60)
if response.status_code != 200:
return {}, 'Failed to access {} with status code: {}\n'.format(cls.url, response.status_code)
return response.json(), None
except Exception as e:
return {}, 'Failed to access {url}\n'.format(url=cls.url)
@classmethod
def load(cls, use_network=None):
errors = []
now = int(time.time())
if cls.last_update < now - cls.REFRESH:
cls.contributors = {}
if use_network:
cls.contributors = {}
contributors_json, error = cls.load_from_github()
if error:
errors.append(error)
else:
cls.last_update = now
elif use_network is False:
contributors_json, error = cls.load_from_disk()
if error:
errors.append(error)
elif cls.contributors:
return cls.contributors, errors
else:
contributors_json, error = cls.load_from_github()
if error:
errors.append(error)
else:
cls.last_update = now
if not contributors_json:
contributors_json, error = cls.load_from_disk()
if error:
errors.append(error)
for value in contributors_json:
name = value.get('name')
emails = value.get('emails')
github_username = value.get('github')
if name and emails:
bugzilla_email = emails[0].lower() # We're requiring that the first email is the primary bugzilla email
cls.contributors[bugzilla_email] = {'name': name, 'status': value.get('status')}
cls.contributors[name] = {'status': value.get('status')}
if github_username and name and emails:
cls.contributors[github_username.lower()] = dict(
name=name,
status=value.get('status'),
email=emails[0].lower(),
)
return cls.contributors, errors
class ConfigureBuild(buildstep.BuildStep, AddToLogMixin):
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.add_pr_details()
self.finished(SUCCESS)
return defer.succeed(None)
def add_patch_id_url(self):
patch_id = self.getProperty('patch_id', '')
if patch_id:
self.setProperty('change_id', patch_id, 'ConfigureBuild')
self.addURL('Patch {}'.format(patch_id), Bugzilla.patch_url(patch_id))
def add_pr_details(self):
pr_number = self.getProperty('github.number')
if not pr_number:
return
repository_url = self.getProperty('repository', '')
title = self.getProperty('github.title', '')
owners = self.getProperty('owners', [])
revision = self.getProperty('github.head.sha')
if self.getProperty('project') != 'WebKit/WebKit':
self.setProperty('sensitive', True)
self.setProperty('change_id', revision[:HASH_LENGTH_TO_DISPLAY], 'ConfigureBuild')
title = f': {title}' if title else ''
self.addURL(f'PR {pr_number}{title}', GitHub.pr_url(pr_number, repository_url))
if owners:
email, errors = GitHub.email_for_owners(owners)
for error in errors:
print(error)
self._addToLog('stdio', error)
if email:
display_name = '{} ({})'.format(email, owners[0])
else:
display_name = owners[0]
self.addURL('PR by: {}'.format(display_name), '{}{}'.format(GITHUB_URL, owners[0]))
if revision:
self.addURL('Hash: {}'.format(revision[:HASH_LENGTH_TO_DISPLAY]), GitHub.commit_url(revision, repository_url))
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'}
def run(self):
self.branch = self.getProperty('github.base.ref', self.branch)
return super(CheckOutSource, self).run()
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) and not self.getProperty('github.number', 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 = 'HEAD'
# Note that these properties are delibrately in priority order.
for property_ in ['ews_revision', 'got_revision']:
candidate = self.getProperty(property_)
if candidate:
revision = candidate
break
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', DEFAULT_BRANCH)
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-working-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, ShellMixin):
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 doStepIf(self, step):
return self.getProperty('patch_id', False)
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 = self.shell_command('curl -L "https://bugs.webkit.org/attachment.cgi?id={}" -o .buildbot-diff && {}'.format(patch_id, ' '.join(self.command)))
shell.ShellCommand.start(self)
return None
reviewers_names = self.getProperty('reviewers_full_names', [])
if reviewers_names:
self.command.extend(['--reviewer', reviewers_names[0]])
d = self.downloadFileContentToWorker('.buildbot-diff', patch)
d.addCallback(lambda res: shell.ShellCommand.start(self))
def hideStepIf(self, results, step):
return not self.doStepIf(step) or (results == SUCCESS and self.getProperty('sensitive', False))
def getResultSummary(self):
if self.results == SKIPPED:
return {'step': "Skipping applying patch since patch_id isn't provided"}
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('comment_text', comment_text)
self.setProperty('build_finish_summary', message)
self.build.addStepsAfterCurrentStep([LeaveComment(), SetCommitQueueMinusFlagOnPatch()])
else:
self.build.buildFinished([message], FAILURE)
return rc
class CommitPatch(steps.ShellSequence, CompositeStepMixin, ShellMixin):
name = 'commit-patch'
description = ['commit-patch']
descriptionDone = ['Created commit from patch']
haltOnFailure = True
env = dict(FILTER_BRANCH_SQUELCH_WARNING='1')
FILTER_BRANCH_PROGRAM = '''import re
import sys
lines = [l for l in sys.stdin]
for s in re.split(r' (Need the bug URL \(OOPS!\).)|(\S+:\/\/\S+)', lines[0].rstrip()):
if s and s != ' ':
print(s)
for l in lines[1:]:
sys.stdout.write(l)
'''
def __init__(self, **kwargs):
super(CommitPatch, self).__init__(timeout=10 * 60, logEnviron=False, **kwargs)
def doStepIf(self, step):
return self.getProperty('patch_id', False)
def hideStepIf(self, results, step):
return not self.doStepIf(step) or (results == SUCCESS and self.getProperty('sensitive', False))
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 run(self):
self.commands = []
patch = self._get_patch()
commands = []
if not patch:
commands += [['curl', '-L', 'https://bugs.webkit.org/attachment.cgi?id={}'.format(self.getProperty('patch_id', '')), '-o', '.buildbot-diff']]
commands += [
['git', 'am', '--keep-non-patch', '.buildbot-diff'],
['git', 'filter-branch', '-f', '--msg-filter', 'python3 -c "{}"'.format(self.FILTER_BRANCH_PROGRAM), 'HEAD...HEAD~1'],
]
for command in commands:
self.commands.append(util.ShellArg(command=command, logname='stdio', haltOnFailure=True))
_ = yield self.downloadFileContentToWorker('.buildbot-diff', patch)
res = yield super(CommitPatch, self).run()
return res
def getResultSummary(self):
if self.results != SUCCESS:
return {'step': 'git failed to apply patch to trunk'}
return super(CommitPatch, self).getResultSummary()
class CheckOutPullRequest(steps.ShellSequence, ShellMixin):
name = 'checkout-pull-request'
description = ['checking-out-pull-request']
descriptionDone = ['Checked out pull request']
haltOnFailure = True
def __init__(self, **kwargs):
super(CheckOutPullRequest, self).__init__(timeout=10 * 60, logEnviron=False, **kwargs)
def doStepIf(self, step):
return self.getProperty('github.number', False)
def hideStepIf(self, results, step):
return not self.doStepIf(step)
def run(self):
self.commands = []
remote = self.getProperty('github.head.repo.full_name', 'origin').split('/')[0]
project = self.getProperty('github.head.repo.full_name', self.getProperty('project'))
pr_branch = self.getProperty('github.head.ref', DEFAULT_BRANCH)
base_hash = self.getProperty('github.base.sha')
rebase_target_hash = self.getProperty('ews_revision') or self.getProperty('got_revision')
commands = [
['git', 'config', 'credential.helper', '!echo_credentials() { sleep 1; echo "username=${GIT_USER}"; echo "password=${GIT_PASSWORD}"; }; echo_credentials'],
self.shell_command('git remote add {} {}{}.git || {}'.format(remote, GITHUB_URL, project, self.shell_exit_0())),
['git', 'remote', 'set-url', remote, '{}{}.git'.format(GITHUB_URL, project)],
['git', 'fetch', remote],
['git', 'branch', '-f', pr_branch, 'remotes/{}/{}'.format(remote, pr_branch)],
['git', 'checkout', pr_branch],
]
if rebase_target_hash and base_hash and rebase_target_hash != base_hash:
commands += [
['git', 'config', 'merge.changelog.driver', 'perl Tools/Scripts/resolve-ChangeLogs --merge-driver -c %O %A %B'],
['git', 'rebase', '--onto', rebase_target_hash, base_hash, pr_branch],
]
for command in commands:
self.commands.append(util.ShellArg(command=command, logname='stdio', haltOnFailure=True))
username, access_token = GitHub.credentials(user=GitHub.user_for_queue(self.getProperty('buildername', '')))
self.env = dict(
GIT_COMMITTER_NAME='EWS',
GIT_COMMITTER_EMAIL=FROM_EMAIL,
GIT_USER=username,
GIT_PASSWORD=access_token,
)
return super(CheckOutPullRequest, self).run()
def getResultSummary(self):
if self.results == SKIPPED:
return {'step': 'No pull request to checkout'}
if self.results != SUCCESS:
return {'step': 'Failed to checkout and rebase branch from PR {}'.format(self.getProperty('github.number'))}
return super(CheckOutPullRequest, self).getResultSummary()
class AnalyzeChange(buildstep.BuildStep, AddToLogMixin):
flunkOnFailure = True
haltOnFailure = True
def _get_patch(self):
sourcestamp = self.build.getSourceStamp(self.getProperty('codebase', ''))
if sourcestamp and sourcestamp.changes:
return '\n'.join(sourcestamp.changes[0].files)
if sourcestamp and sourcestamp.patch:
return sourcestamp.patch[1]
return None
@property
def change_type(self):
if self.getProperty('github.number', False):
return 'Pull request'
return 'Patch'
def getResultSummary(self):
if self.results in [FAILURE, SKIPPED]:
return {'step': '{} doesn\'t have relevant changes'.format(self.change_type)}
if self.results == SUCCESS:
return {'step': '{} contains relevant changes'.format(self.change_type)}
return buildstep.BuildStep.getResultSummary(self)
class CheckChangeRelevance(AnalyzeChange):
name = 'check-change-relevance'
description = ['check-change-relevance running']
descriptionDone = ['Change 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'Source/cmake/', 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 {} contains relevant changes.'.format(self.change_type.lower()))
self.finished(SUCCESS)
return None
self._addToLog('stdio', 'This {} does not have relevant changes.'.format(self.change_type.lower()))
self.finished(FAILURE)
self.build.results = SKIPPED
self.build.buildFinished(['{} {} doesn\'t have relevant changes'.format(
self.change_type,
self.getProperty('patch_id', '') or self.getProperty('github.number', ''),
)], SKIPPED)
return None
class FindModifiedLayoutTests(AnalyzeChange):
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 change modifies following tests: {}'.format(tests))
self.setProperty('modified_tests', tests)
self.finished(SUCCESS)
return None
self._addToLog('stdio', 'This change does not modify any layout tests')
self.finished(SKIPPED)
if self.skipBuildIfNoResult:
self.build.results = SKIPPED
self.build.buildFinished(['{} {} doesn\'t have relevant changes'.format(
self.change_type,
self.getProperty('patch_id', '') or self.getProperty('github.number', ''),
)], 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(AddToLogMixin):
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]')
def fetch_data_from_url_with_authentication_bugzilla(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_bugzilla(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_bugzilla(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 == '+':
reviewer = flag.get('setter', '')
self.setProperty('reviewer', reviewer)
if self.addURLs:
self.addURL('Reviewed by: {}'.format(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')
sensitive = bug_json.get('product') == 'Security'
if sensitive:
self.setProperty('sensitive', True)
bug_title = ''
self.setProperty('bug_title', 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_for_patch(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 send_email_for_infrastructure_issue(self, infrastructure_issue_text):
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 = 'Infrastructure issue at {}'.format(builder_name)
email_text = 'The following infrastructure issue happened at:\n\n'
email_text += ' - Build : <a href="{}">{}</a>\n'.format(build_url, build_url)
email_text += ' - Builder : {}\n'.format(builder_name)
email_text += ' - Worker : {}\n'.format(worker_name)
email_text += ' - Issue: {}\n'.format(infrastructure_issue_text)
send_email_to_bot_watchers(email_subject, email_text, builder_name, 'infrastructure-{}'.format(builder_name))
except Exception as e:
print('Error in sending email for infrastructure issue: {}'.format(e))
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 ValidateChange(buildstep.BuildStep, BugzillaMixin, GitHubMixin):
name = 'validate-change'
description = ['validate-change running']
descriptionDone = ['Validated change']
flunkOnFailure = True
haltOnFailure = True
def __init__(
self,
verifyObsolete=True,
verifyBugClosed=True,
verifyReviewDenied=True,
addURLs=True,
verifycqplus=False,
verifyMergeQueue=False,
verifyNoDraftForMergeQueue=False,
):
self.verifyObsolete = verifyObsolete
self.verifyBugClosed = verifyBugClosed
self.verifyReviewDenied = verifyReviewDenied
self.verifycqplus = verifycqplus
self.verifyMergeQueue = verifyMergeQueue
self.verifyNoDraftForMergeQueue = verifyNoDraftForMergeQueue
self.addURLs = addURLs
buildstep.BuildStep.__init__(self)
def getResultSummary(self):
if self.results == FAILURE:
return {'step': self.descriptionDone}
return super(ValidateChange, 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 fail_build(self, reason):
self._addToLog('stdio', reason)
self.finished(FAILURE)
self.build.results = FAILURE
self.descriptionDone = reason
self.build.buildFinished([reason], FAILURE)
def start(self):
patch_id = self.getProperty('patch_id', '')
pr_number = self.getProperty('github.number', '')
if not patch_id and not pr_number:
self._addToLog('stdio', 'No patch_id or pr_number found. Unable to proceed without one of them.\n')
self.descriptionDone = 'No change found'
self.finished(FAILURE)
return None
if patch_id and pr_number:
self._addToLog('stdio', 'Both patch_id and pr_number found. Unable to proceed with both.\n')
self.descriptionDone = 'Error: both PR and patch number found'
self.finished(FAILURE)
return None
if patch_id and not self.validate_bugzilla(patch_id):
return None
if pr_number and not self.validate_github(pr_number):
return None
if self.verifyBugClosed and patch_id:
self._addToLog('stdio', 'Bug is open.\n')
if self.verifyObsolete:
self._addToLog('stdio', 'Change is not obsolete.\n')
if self.verifyReviewDenied and patch_id:
self._addToLog('stdio', 'Change has not been denied.\n')
if self.verifycqplus and patch_id:
self._addToLog('stdio', 'Change is in commit queue.\n')
self._addToLog('stdio', 'Change has been reviewed.\n')
if self.verifyNoDraftForMergeQueue and pr_number:
self._addToLog('stdio', 'Change is not a draft.\n')
if self.verifyMergeQueue and pr_number:
self._addToLog('stdio', 'Change is in merge queue.\n')
self.finished(SUCCESS)
return None
def validate_bugzilla(self, patch_id):
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 False
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 False
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 False
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 False
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 False
if obsolete == -1 or review_denied == -1 or bug_closed == -1:
self.finished(WARNINGS)
return False
return True
def validate_github(self, pr_number):
if not pr_number:
return False
repository_url = self.getProperty('repository', '')
pr_json = self.get_pr_json(pr_number, repository_url, retry=3)
pr_closed = self._is_pr_closed(pr_json) if self.verifyBugClosed else 0
if pr_closed == 1:
self.skip_build('Pull request {} is already closed'.format(pr_number))
return False
obsolete = self._is_hash_outdated(pr_json) if self.verifyObsolete else 0
if obsolete == 1:
self.skip_build('Hash {} on PR {} is outdated'.format(self.getProperty('github.head.sha', '?')[:HASH_LENGTH_TO_DISPLAY], pr_number))
return False
blocked = self._is_pr_blocked(pr_json) if self.verifyMergeQueue else 0
if blocked == 1:
self.skip_build("PR {} has been marked as '{}'".format(pr_number, self.BLOCKED_LABEL))
return False
if self.verifyMergeQueue:
if not pr_json:
self.send_email_for_github_failure()
self.skip_build("Infrastructure issue: unable to check PR status")
return False
merge_queue = self._is_pr_in_merge_queue(pr_json)
if merge_queue == 0:
self.skip_build("PR {} does not have a merge queue label".format(pr_number))
return False
draft = self._is_pr_draft(pr_json) if self.verifyNoDraftForMergeQueue else 0
if draft == 1:
self.fail_build("PR {} is a draft pull request".format(pr_number))
return False
if -1 in (obsolete, pr_closed, blocked, draft):
self.finished(WARNINGS)
return False
return True
def send_email_for_github_failure(self):
try:
pr_number = self.getProperty('github.number', '')
sha = self.getProperty('github.head.sha', '')[:HASH_LENGTH_TO_DISPLAY]
change_string = 'Hash {}'.format(sha)
change_author, errors = GitHub.email_for_owners(self.getProperty('owners', []))
for error in errors:
print(error)
self._addToLog('stdio', error)
if not change_author:
self._addToLog('stderr', 'Unable to determine email address for {} from metadata/contributors.json. Skipping sending email.'.format(self.getProperty('owners', [])))
return
builder_name = self.getProperty('buildername', '')
title = self.getProperty('github.title', '')
worker_name = self.getProperty('workername', '')
build_url = '{}#/builders/{}/builds/{}'.format(self.master.config.buildbotURL, self.build._builderid, self.build.number)
email_subject = f'Infrastructure failure on {builder_name} for PR #{pr_number}: {title}'
email_text = f'EWS has encountered infrastructure failure on {builder_name}'
repository = self.getProperty('repository')
email_text += ' while testing <a href="{}">{}</a>'.format(GitHub.commit_url(sha, repository), change_string)
email_text += ' for <a href="{}">PR #{}: {}</a>.'.format(GitHub.pr_url(pr_number, repository), pr_number, title)
email_text += '\n\nFull details are available at: {}\n\nChange author: {}'.format(build_url, change_author)
email_text += '\n\nPlease contact one of the WebKit administrators on Slack or email admin@webkit.org to fix the issue.'
self._addToLog('stdio', 'Sending email notification to {}.\nPlease contact an admin to fix the issue.\n'.format(change_author))
send_email_to_patch_author(change_author, email_subject, email_text, self.getProperty('github.head.sha', ''))
except Exception as e:
print('Error in sending email for github failure: {}'.format(e))
class ValidateCommitterAndReviewer(buildstep.BuildStep, GitHubMixin, AddToLogMixin):
name = 'validate-commiter-and-reviewer'
descriptionDone = ['Validated commiter and reviewer']
def __init__(self, *args, **kwargs):
super(ValidateCommitterAndReviewer, self).__init__(*args, **kwargs)
self.contributors = {}
def getResultSummary(self):
if self.results == FAILURE:
return {'step': self.descriptionDone}
return buildstep.BuildStep.getResultSummary(self)
def fail_build(self, email_or_username, status):
patch_id = self.getProperty('patch_id', '')
pr_number = self.getProperty('github.number', '')
reason = f'{email_or_username} does not have {status} permissions'
comment = f'{"@" if pr_number else ""}{email_or_username} does not have {status} permissions according to {Contributors.url}.'
if patch_id:
comment += f'\n\nRejecting attachment {patch_id} from commit queue.'
elif pr_number:
comment += f'\n\nIf you do have {status} permmissions, please ensure that your GitHub username is added to contributors.json.'
comment += f'\n\nRejecting {self.getProperty("github.head.sha", f"#{pr_number}")} from merge queue.'
self.setProperty('comment_text', comment)
self._addToLog('stdio', reason)
self.setProperty('build_finish_summary', reason)
self.build.addStepsAfterCurrentStep([LeaveComment(), BlockPullRequest(), SetCommitQueueMinusFlagOnPatch()])
self.finished(FAILURE)
self.descriptionDone = reason
def is_reviewer(self, email):
contributor = self.contributors.get(email.lower())
return contributor and contributor['status'] == 'reviewer'
def is_committer(self, email):
contributor = self.contributors.get(email.lower())
return contributor and contributor['status'] in ['reviewer', 'committer']
def full_name_from_email(self, email):
contributor = self.contributors.get(email.lower())
if not contributor:
return ''
return contributor.get('name')
def start(self):
self.contributors, errors = Contributors.load(use_network=True)
for error in errors:
print(error)
self._addToLog('stdio', error)
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
pr_number = self.getProperty('github.number', '')
if pr_number:
committer = (self.getProperty('owners', []) or [''])[0]
else:
committer = self.getProperty('patch_committer', '').lower()
if not self.is_committer(committer):
self.fail_build(committer, 'committer')
return None
self._addToLog('stdio', f'{committer} is a valid commiter.\n')
if pr_number:
reviewers = self.get_reviewers(pr_number, self.getProperty('repository', ''))
if any([self.is_reviewer(reviewer) for reviewer in reviewers]):
reviewers = list(filter(self.is_reviewer, reviewers))
else:
reviewer = self.getProperty('reviewer', '').lower()
reviewers = [reviewer] if reviewer else []
reviewers = set(reviewers)
if not reviewers:
# Change has not been reviewed in bug tracker. This is acceptable, since the ChangeLog might have 'Reviewed by' in it.
self.descriptionDone = 'Validated committer'
self.finished(SUCCESS)
return None
for reviewer in reviewers:
if not self.is_reviewer(reviewer):
self.fail_build(reviewer, 'reviewer')
return None
self._addToLog('stdio', f'{reviewer} is a valid reviewer.\n')
self.setProperty('reviewers_full_names', [self.full_name_from_email(reviewer) for reviewer in reviewers])
self.finished(SUCCESS)
return None
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'}
def doStepIf(self, step):
return self.getProperty('patch_id', False)
def hideStepIf(self, results, step):
return not self.doStepIf(step)
class BlockPullRequest(buildstep.BuildStep, GitHubMixin, AddToLogMixin):
name = 'block-pull-request'
def start(self):
pr_number = self.getProperty('github.number', '')
build_finish_summary = self.getProperty('build_finish_summary', None)
rc = SKIPPED
repository_url = self.getProperty('repository', '')
pr_json = self.get_pr_json(pr_number, repository_url)
if self._is_hash_outdated(pr_json) == 0 and CURRENT_HOSTNAME == EWS_BUILD_HOSTNAME:
repository_url = self.getProperty('repository', '')
rc = SUCCESS
if any((
not self.remove_labels(pr_number, [self.MERGE_QUEUE_LABEL, self.UNSAFE_MERGE_QUEUE_LABEL], repository_url=repository_url),
not self.add_label(pr_number, self.BLOCKED_LABEL, repository_url=repository_url),
)):
rc = FAILURE
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': f"Added '{self.BLOCKED_LABEL}' label to pull request"}
elif self.results == SKIPPED:
return buildstep.BuildStep.getResultSummary(self)
return {'step': f"Failed to add '{self.BLOCKED_LABEL}' label to pull request"}
def doStepIf(self, step):
return self.getProperty('github.number')
def hideStepIf(self, results, step):
return not self.doStepIf(step)
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 == SKIPPED:
return buildstep.BuildStep.getResultSummary(self)
if self.results == SUCCESS:
return {'step': 'Removed flags on bugzilla patch'}
return {'step': 'Failed to remove flags on bugzilla patch'}
def doStepIf(self, step):
return self.getProperty('patch_id')
def hideStepIf(self, results, step):
return not self.doStepIf(step)
class RemoveLabelsFromPullRequest(buildstep.BuildStep, GitHubMixin, AddToLogMixin):
name = 'remove-labels-from-pull-request'
flunkOnFailure = False
haltOnFailure = False
LABELS_TO_REMOVE = [
GitHubMixin.MERGE_QUEUE_LABEL,
GitHubMixin.UNSAFE_MERGE_QUEUE_LABEL,
GitHubMixin.BLOCKED_LABEL,
]
def start(self):
pr_number = self.getProperty('github.number', '')
repository_url = self.getProperty('repository', '')
rc = SUCCESS
if not self.remove_labels(pr_number, self.LABELS_TO_REMOVE, repository_url=repository_url):
rc = FAILURE
self.finished(rc)
return None
def getResultSummary(self):
if self.results == SUCCESS:
return {'step': f"Removed labels from pull request"}
elif self.results == FAILURE:
return {'step': f"Failed to remove labels from pull request"}
return buildstep.BuildStep.getResultSummary(self)
def doStepIf(self, step):
return self.getProperty('github.number') and CURRENT_HOSTNAME == EWS_BUILD_HOSTNAME
def hideStepIf(self, results, step):
return not self.doStepIf(step)
class CloseBug(buildstep.BuildStep, BugzillaMixin):
name = 'close-bugzilla-bug'
flunkOnFailure = False
haltOnFailure = False
bug_id = ''
def start(self):
self.bug_id = self.getProperty('bug_id', '')
rc = self.close_bug(self.bug_id)
self.finished(rc)
return None
def getResultSummary(self):
if self.results == SKIPPED:
return buildstep.BuildStep.getResultSummary(self)
if self.results == SUCCESS:
return {'step': 'Closed bug {}'.format(self.bug_id)}
return {'step': 'Failed to close bug {}'.format(self.bug_id)}
def doStepIf(self, step):
return self.getProperty('bug_id') and not self.getProperty('is_test_gardening')
def hideStepIf(self, results, step):
return not self.doStepIf(step)
class ClosePullRequest(buildstep.BuildStep, GitHubMixin, AddToLogMixin):
name = 'close-pull-request'
flunkOnFailure = False
haltOnFailure = False
def start(self):
self.pr_number = self.getProperty('github.number', '')
if self.close_pr(self.pr_number, self.getProperty('repository')):
self.finished(SUCCESS)
else:
self.finished(FAILURE)
return None
def getResultSummary(self):
if self.results == FAILURE:
return {'step': f'Failed to close PR {self.pr_number}'}
if self.results == SUCCESS:
return {'step': f'Closed PR {self.pr_number}'}
return buildstep.BuildStep.getResultSummary(self)
def doStepIf(self, step):
return self.getProperty('github.number')
def hideStepIf(self, results, step):
return not self.doStepIf(step)
class LeaveComment(buildstep.BuildStep, BugzillaMixin, GitHubMixin):
name = 'leave-comment'
flunkOnFailure = False
haltOnFailure = False
def start(self):
self.bug_id = self.getProperty('bug_id', '')
self.pr_number = self.getProperty('github.number', '')
self.comment_text = self.getProperty('comment_text', '')
if not self.comment_text:
self._addToLog('stdio', 'comment_text build property not found.\n')
self.descriptionDone = 'No comment found'
self.finished(WARNINGS)
return None
rc = SUCCESS
if self.pr_number and not self.comment_on_pr(self.pr_number, self.comment_text, self.getProperty('repository')):
rc = FAILURE
if self.bug_id and self.comment_on_bug(self.bug_id, self.comment_text) != SUCCESS:
rc = FAILURE
if not self.pr_number and not self.bug_id:
self._addToLog('stdio', 'No bug or pull request to comment to.\n')
self.descriptionDone = 'No bug or PR found'
self.finished(FAILURE)
return None
self.finished(rc)
return None
def getResultSummary(self):
if self.results == SUCCESS:
if self.pr_number and self.bug_id:
return {'step': f'Added comment on PR {self.pr_number} and added comment on bug {self.bug_id}'}
if self.pr_number:
return {'step': f'Added comment on PR {self.pr_number}'}
if self.bug_id:
return {'step': f'Added comment on bug {self.bug_id}'}
if self.results == FAILURE:
if self.pr_number and self.bug_id:
return {'step': f'Failed to add comment on PR {self.pr_number} and failed to add comment on bug {self.bug_id}'}
if self.pr_number:
return {'step': f'Failed to add comment on PR {self.pr_number}'}
if self.bug_id:
return {'step': f'Failed to add comment on bug {self.bug_id}'}
return {'step': 'Failed to add comment'}
return buildstep.BuildStep.getResultSummary(self)
def doStepIf(self, step):
return CURRENT_HOSTNAME == EWS_BUILD_HOSTNAME
class UnApplyPatch(CleanWorkingDirectory):
name = 'unapply-patch'
descriptionDone = ['Unapplied patch']
def doStepIf(self, step):
return self.getProperty('patch_id')
def hideStepIf(self, results, step):
return not self.doStepIf(step)
class RevertPullRequestChanges(steps.ShellSequence):
name = 'revert-pull-request-changes'
description = ['revert-pull-request-changes running']
descriptionDone = ['Reverted pull request changes']
flunkOnFailure = True
haltOnFailure = True
def __init__(self, **kwargs):
super(RevertPullRequestChanges, self).__init__(timeout=5 * 60, logEnviron=False, **kwargs)
def run(self):
self.commands = []
for command in [
['git', 'clean', '-f', '-d'],
['git', 'checkout', self.getProperty('ews_revision') or self.getProperty('got_revision')],
]:
self.commands.append(util.ShellArg(command=command, logname='stdio'))
return super(RevertPullRequestChanges, self).run()
def doStepIf(self, step):
return self.getProperty('github.number')
def hideStepIf(self, results, step):
return not self.doStepIf(step)
class Trigger(trigger.Trigger):
def __init__(self, schedulerNames, include_revision=True, triggers=None, patch=True, pull_request=False, **kwargs):
self.include_revision = include_revision
self.triggers = triggers
set_properties = self.propertiesToPassToTriggers(patch=patch, pull_request=pull_request) or {}
super(Trigger, self).__init__(schedulerNames=schedulerNames, set_properties=set_properties, **kwargs)
def propertiesToPassToTriggers(self, patch=True, pull_request=False):
property_names = [
'configuration',
'platform',
'fullPlatform',
'architecture',
]
if patch:
property_names += ['patch_id', 'bug_id', 'owner']
if pull_request:
property_names += [
'github.base.ref', 'github.base.sha', 'github.head.ref', 'github.head.sha',
'github.head.repo.full_name', 'github.number', 'github.title',
'repository', 'project', 'owners',
]
if self.triggers:
property_names.append('triggers')
properties_to_pass = {prop: properties.Property(prop) for prop in property_names}
properties_to_pass['retry_count'] = properties.Property('retry_count', default=0)
if self.include_revision:
properties_to_pass['ews_revision'] = properties.Property('got_revision')
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, AddToLogMixin):
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}
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, AddToLogMixin):
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}
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, AddToLogMixin):
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
# https://bugs.webkit.org/show_bug.cgi?id=239455: The timeout needs to be >20 min to work
# around log output delays on slower machines.
kwargs.setdefault('timeout', 60 * 30)
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)
def errorReceived(self, error):
self._addToLog('errors', error + '\n')
def evaluateCommand(self, cmd):
if cmd.didFail():
steps_to_add = [UnApplyPatch(), RevertPullRequestChanges(), ValidateChange(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(CompileJSCWithoutChange())
else:
steps_to_add.append(CompileWebKitWithoutChange())
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,
patch=bool(self.getProperty('patch_id')),
pull_request=bool(self.getProperty('github.number')),
))
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 CompileWebKitWithoutChange(CompileWebKit):
name = 'compile-webkit-without-change'
haltOnFailure = False
def __init__(self, retry_build_on_failure=False, **kwargs):
self.retry_build_on_failure = retry_build_on_failure
super(CompileWebKitWithoutChange, self).__init__(**kwargs)
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 change, 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, GitHubMixin):
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 = CompileWebKitWithoutChange.name
if self.getProperty('group') == 'jsc':
compile_without_patch_step = CompileJSCWithoutChange.name
compile_without_patch_result = self.getStepResult(compile_without_patch_step)
patch_id = self.getProperty('patch_id', '')
pr_number = self.getProperty('github.number')
if compile_without_patch_result == FAILURE:
message = 'Unable to build WebKit without {}, retrying build'.format('PR' if pr_number else 'patch')
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
sha = self.getProperty('github.head.sha')
if sha and pr_number:
message = 'Hash {} for PR {} does not build'.format(sha[:HASH_LENGTH_TO_DISPLAY], pr_number)
else:
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 patch_id:
if self.getProperty('buildername', '').lower() == 'commit-queue':
self.setProperty('comment_text', message)
self.build.addStepsAfterCurrentStep([LeaveComment(), SetCommitQueueMinusFlagOnPatch()])
else:
self.build.addStepsAfterCurrentStep([SetCommitQueueMinusFlagOnPatch()])
else:
self.build.addStepsAfterCurrentStep([BlockPullRequest()])
@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', '')
pr_number = self.getProperty('github.number', '')
sha = self.getProperty('github.head.sha', '')[:HASH_LENGTH_TO_DISPLAY]
if patch_id and not self.should_send_email_for_patch(patch_id):
return
if pr_number and not self.should_send_email_for_pr(pr_number, self.getProperty('repository')):
return
if not patch_id and not (pr_number and sha):
self._addToLog('stderr', 'Unrecognized change type')
return
change_string = None
change_author = None
if patch_id:
change_author = self.getProperty('patch_author', '')
change_string = 'Patch {}'.format(patch_id)
elif pr_number and sha:
change_string = 'Hash {}'.format(sha)
change_author, errors = GitHub.email_for_owners(self.getProperty('owners', []))
for error in errors:
print(error)
self._addToLog('stdio', error)
if not change_author:
self._addToLog('stderr', 'Unable to determine email address for {} from metadata/contributors.json. Skipping sending email.'.format(self.getProperty('owners', [])))
return
builder_name = self.getProperty('buildername', '')
bug_id = self.getProperty('bug_id', '') or pr_number
title = self.getProperty('bug_title', '') or self.getProperty('github.title', '')
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 for {}: {}'.format(change_string, title)
email_text = 'EWS has detected build failure on {}'.format(builder_name)
if patch_id:
email_text += ' while testing <a href="{}">{}</a>'.format(Bugzilla.patch_url(patch_id), change_string)
email_text += ' for <a href="{}">Bug {}</a>.'.format(Bugzilla.bug_url(bug_id), bug_id)
if sha:
repository = self.getProperty('repository')
email_text += ' while testing <a href="{}">{}</a>'.format(GitHub.commit_url(sha, repository), change_string)
email_text += ' for <a href="{}">PR #{}</a>.'.format(GitHub.pr_url(pr_number, repository), pr_number)
email_text += '\n\nFull details are available at: {}\n\nChange author: {}'.format(build_url, change_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 unsubscribe from these notifications or to provide any feedback please email aakash_jain@apple.com'
self._addToLog('stdio', 'Sending email notification to {}'.format(change_author))
send_email_to_patch_author(change_author, email_subject, email_text, patch_id or self.getProperty('github.head.sha', ''))
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 CompileJSCWithoutChange(CompileJSC):
name = 'compile-jsc-without-change'
def evaluateCommand(self, cmd):
return shell.Compile.evaluateCommand(self, cmd)
class RunJavaScriptCoreTests(shell.Test, AddToLogMixin):
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')]
# We rely on run-jsc-stress-tests to weed out any flaky tests
command_extra = ['--treat-failing-as-flaky=0.6,10,200']
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 = []
self.flaky = {}
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'))
self.command.extend(self.command_extra)
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([UnApplyPatch(),
RevertPullRequestChanges(),
ValidateChange(verifyBugClosed=False, addURLs=False),
CompileJSCWithoutChange(),
ValidateChange(verifyBugClosed=False, addURLs=False),
KillOldProcesses(),
RunJSCTestsWithoutChange(),
AnalyzeJSCTestsResults()])
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)
self.flaky = jsc_results.get('flakyAndPassed')
if self.flaky:
self.setProperty(self.prefix + 'flaky_and_passed', self.flaky)
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}
elif self.results == SUCCESS and self.flaky:
return {'step': "Passed JSC tests (%d flaky)" % len(self.flaky)}
return shell.Test.getResultSummary(self)
class RunJSCTestsWithoutChange(RunJavaScriptCoreTests):
name = 'jscore-test-without-change'
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, AddToLogMixin):
name = 'analyze-jsc-tests-results'
description = ['analyze-jsc-test-results']
descriptionDone = ['analyze-jsc-tests-results']
NUM_FAILURES_TO_DISPLAY = 10
def start(self):
stress_failures_with_change = set(self.getProperty('jsc_stress_test_failures', []))
binary_failures_with_change = set(self.getProperty('jsc_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])
flaky_stress_failures_with_change = set(self.getProperty('jsc_flaky_and_passed', {}).keys())
clean_tree_flaky_stress_failures = set(self.getProperty('jsc_clean_tree_flaky_and_passed', {}).keys())
flaky_stress_failures = sorted(list(flaky_stress_failures_with_change) + list(clean_tree_flaky_stress_failures))[:self.NUM_FAILURES_TO_DISPLAY]
flaky_failures_string = ', '.join(flaky_stress_failures)
new_stress_failures = stress_failures_with_change - clean_tree_stress_failures
new_binary_failures = binary_failures_with_change - 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 with change: {}'.format(list(binary_failures_with_change) + list(stress_failures_with_change))[:self.NUM_FAILURES_TO_DISPLAY])
self._addToLog('stderr', '\nFlaky Tests with change: {}'.format(', '.join(flaky_stress_failures_with_change)))
self._addToLog('stderr', '\nFailures on clean tree: {}'.format(clean_tree_failures_string))
self._addToLog('stderr', '\nFlaky Tests on clean tree: {}'.format(', '.join(clean_tree_flaky_stress_failures)))
if (not stress_failures_with_change) and (not binary_failures_with_change):
# 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_stress_failures:
message += ' Found flaky tests: {}'.format(flaky_failures_string)
for flaky_failure in flaky_stress_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 change'
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)
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, AddToLogMixin):
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 = ['python3', '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 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:
if self.EXIT_AFTER_FAILURES is not None:
self.setCommand(self.command + ['--exit-after-n-failures', '{}'.format(self.EXIT_AFTER_FAILURES)])
self.setCommand(self.command + ['--skip-failing-tests'])
if additionalArguments:
self.setCommand(self.command + additionalArguments)
if self.ENABLE_GUARD_MALLOC:
self.setCommand(self.command + ['--guard-malloc'])
def start(self, BufferLogObserverClass=logobserver.BufferLogObserver):
self.log_observer = BufferLogObserverClass(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
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))
self.setProperty('first_run_flakies', sorted(first_results.flaky_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(),
ValidateChange(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', []))
# FIXME: here it can be a good idea to also use the info from second_run_flakies and first_run_flakies
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.build.addStepsAfterCurrentStep([ArchiveTestResults(),
UploadTestResults(identifier='rerun'),
ExtractTestResults(identifier='rerun'),
UnApplyPatch(),
RevertPullRequestChanges(),
ValidateChange(verifyBugClosed=False, addURLs=False),
CompileWebKitWithoutChange(retry_build_on_failure=True),
ValidateChange(verifyBugClosed=False, addURLs=False),
KillOldProcesses(),
RunWebKitTestsWithoutChange()])
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))
self.setProperty('second_run_flakies', sorted(second_results.flaky_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 RunWebKitTestsWithoutChange(RunWebKitTests):
name = 'run-layout-tests-without-change'
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)
self.setProperty('clean_tree_run_flakies', sorted(clean_tree_results.flaky_tests))
if clean_tree_results.failing_tests:
self._addToLog(self.test_failures_log_name, '\n'.join(clean_tree_results.failing_tests))
self._parseRunWebKitTestsOutput(logText)
def setLayoutTestCommand(self):
super(RunWebKitTestsWithoutChange, self).setLayoutTestCommand()
# In order to speed up testing, on the step that retries running the layout tests without change
# 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
# Also pass '--skipped=always' to avoid running a test that is skipped on the clean tree and that
# the change removed from the TestExpectations file meanwhile it still fails with the change (so
# it is passed as an argument on the command-line)
# The flag '--skip-failing-tests' that is passed by default (in combination with '--skipped=always')
# avoids running tests marked as failing on the Expectation files even when those are passed as arguments.
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:
first_results_failing_tests = set(self.getProperty('first_run_failures', set()))
second_results_failing_tests = set(self.getProperty('second_run_failures', set()))
list_failed_tests_with_change = sorted(first_results_failing_tests.union(second_results_failing_tests))
if list_failed_tests_with_change:
self.setCommand(self.command + ['--skipped=always'] + list_failed_tests_with_change)
class AnalyzeLayoutTestsResults(buildstep.BuildStep, BugzillaMixin, GitHubMixin):
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, exceed_failure_limit=False):
self.finished(FAILURE)
self.build.results = FAILURE
if not new_failures:
message = 'Found unexpected failure with change'
else:
pluralSuffix = 's' if len(new_failures) > 1 else ''
if exceed_failure_limit:
message = 'Failure limit exceed. At least found'
else:
message = 'Found'
new_failures_string = ', '.join(sorted(new_failures)[:self.NUM_FAILURES_TO_DISPLAY])
message += ' {} 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, exceed_failure_limit)
self.descriptionDone = message
self.setProperty('build_finish_summary', message)
if self.getProperty('buildername', '').lower() == 'commit-queue':
self.setProperty('comment_text', message)
self.build.addStepsAfterCurrentStep([LeaveComment(), SetCommitQueueMinusFlagOnPatch()])
else:
self.build.addStepsAfterCurrentStep([SetCommitQueueMinusFlagOnPatch(), BlockPullRequest()])
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)
return defer.succeed(None)
def retry_build(self, message=''):
if not message:
message = 'Unable to confirm if test failures are introduced by change, retrying build'
self.descriptionDone = message
triggered_by = self.getProperty('triggered_by', None)
if triggered_by:
# Trigger parent build so that it can re-build ToT
schduler_for_current_queue = self.getProperty('scheduler')
self.build.addStepsAfterCurrentStep([Trigger(
schedulerNames=triggered_by,
include_revision=False,
triggers=[schduler_for_current_queue],
patch=bool(self.getProperty('patch_id')),
pull_request=bool(self.getProperty('github.number')),
)])
self.setProperty('build_summary', message)
self.finished(SUCCESS)
else:
self.finished(RETRY)
self.build.buildFinished([message], RETRY)
return defer.succeed(None)
def _results_failed_different_tests(self, first_results_failing_tests, second_results_failing_tests):
return first_results_failing_tests != second_results_failing_tests
def send_email_for_flaky_failure(self, test_name, step_str=None):
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 = 'Flaky test: {}\n\nBuild: {}\n\nBuilder: {}\n\nWorker: {}\n\nHistory: {}'.format(test_name, build_url, builder_name, worker_name, history_url)
if step_str:
email_text += '\nThis test was flaky on the steps: {}'.format(step_str)
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=layout-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))
def send_email_for_new_test_failures(self, test_names, exceed_failure_limit=False):
try:
patch_id = self.getProperty('patch_id', '')
pr_number = self.getProperty('github.number', '')
sha = self.getProperty('github.head.sha', '')[:HASH_LENGTH_TO_DISPLAY]
if patch_id and not self.should_send_email_for_patch(patch_id):
return
if pr_number and not self.should_send_email_for_pr(pr_number, self.getProperty('repository')):
return
if not patch_id and not (pr_number and sha):
self._addToLog('stderr', 'Unrecognized change type')
return
change_string = None
change_author = None
if patch_id:
change_author = self.getProperty('patch_author', '')
change_string = 'Patch {}'.format(patch_id)
elif pr_number and sha:
change_string = 'Hash {}'.format(sha)
change_author, errors = GitHub.email_for_owners(self.getProperty('owners', []))
for error in errors:
print(error)
self._addToLog('stdio', error)
if not change_author:
self._addToLog('stderr', 'Unable to determine email address for {} from metadata/contributors.json. Skipping sending email.'.format(self.getProperty('owners', [])))
return
builder_name = self.getProperty('buildername', '')
bug_id = self.getProperty('bug_id', '') or pr_number
title = self.getProperty('bug_title', '') or self.getProperty('github.title', '')
worker_name = self.getProperty('workername', '')
patch_author = self.getProperty('patch_author', '')
build_url = '{}#/builders/{}/builds/{}'.format(self.master.config.buildbotURL, self.build._builderid, self.build.number)
test_names_string = ''
for test_name in sorted(test_names):
history_url = '{}?suite=layout-tests&test={}'.format(RESULTS_DB_URL, test_name)
test_names_string += '\n- {} (<a href="{}">test history</a>)'.format(test_name, history_url)
pluralSuffix = 's' if len(test_names) > 1 else ''
email_subject = 'Layout test failure for {}: {}'.format(change_string, title)
email_text = 'EWS has detected layout test failure{} on {}'.format(pluralSuffix, builder_name)
if patch_id:
email_text += ' while testing <a href="{}">{}</a>'.format(Bugzilla.patch_url(patch_id), change_string)
email_text += ' for <a href="{}">Bug {}</a>.'.format(Bugzilla.bug_url(bug_id), bug_id)
else:
repository = self.getProperty('repository')
email_text += ' while testing <a href="{}">{}</a>'.format(GitHub.commit_url(sha, repository), change_string)
email_text += ' for <a href="{}">PR #{}</a>.'.format(GitHub.pr_url(pr_number, repository), pr_number)
email_text += '\n\nFull details are available at: {}\n\nChange author: {}'.format(build_url, change_author)
if exceed_failure_limit:
email_text += '\n\nAditionally the failure limit has been exceeded, so the test suite has been terminated early. It is likely that there would be more failures than the ones listed below.'
email_text += '\n\nLayout test failure{}:\n{}'.format(pluralSuffix, test_names_string)
email_text += '\n\nTo unsubscribe from these notifications or to provide any feedback please email aakash_jain@apple.com'
self._addToLog('stdio', 'Sending email notification to {}'.format(change_author))
send_email_to_patch_author(change_author, email_subject, email_text, patch_id or self.getProperty('github.head.sha', ''))
except Exception as e:
print('Error in sending email for new layout test failures: {}'.format(e))
def _report_flaky_tests(self, flaky_tests):
# TODO: implement this
pass
def start(self):
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', []))
clean_tree_results_did_exceed_test_failure_limit = self.getProperty('clean_tree_results_exceed_failure_limit')
clean_tree_results_failing_tests = set(self.getProperty('clean_tree_run_failures', []))
flaky_failures = first_results_failing_tests.union(second_results_failing_tests) - first_results_failing_tests.intersection(second_results_failing_tests)
if (not first_results_failing_tests) and (not second_results_failing_tests):
# If we've made it here, then layout-tests and re-run-layout-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())
self.send_email_for_infrastructure_issue('Both first and second layout-test runs with patch generated no list of results but exited with error, and the clean_tree without change retry also failed.')
return self.retry_build('Unexpected infrastructure issue, retrying build')
if first_results_did_exceed_test_failure_limit and second_results_did_exceed_test_failure_limit:
if (len(first_results_failing_tests) - len(clean_tree_results_failing_tests)) <= 5:
# If we've made it here, then many tests are failing with the patch applied, but
# if the clean tree is also failing many tests, even if it's not quite as many,
# then we can't be certain that the discrepancy isn't due to flakiness, and hence we must defer judgement.
return self.retry_build()
return self.report_failure(first_results_failing_tests)
if second_results_did_exceed_test_failure_limit:
if clean_tree_results_did_exceed_test_failure_limit:
return self.retry_build()
failures_introduced_by_patch = first_results_failing_tests - clean_tree_results_failing_tests
if failures_introduced_by_patch:
return self.report_failure(failures_introduced_by_patch)
return self.retry_build()
if first_results_did_exceed_test_failure_limit:
if clean_tree_results_did_exceed_test_failure_limit:
return self.retry_build()
failures_introduced_by_patch = second_results_failing_tests - clean_tree_results_failing_tests
if failures_introduced_by_patch:
return self.report_failure(failures_introduced_by_patch)
return self.retry_build()
# FIXME: Here it could be a good idea to also use the info of results.flaky_tests from the runs
if self._results_failed_different_tests(first_results_failing_tests, second_results_failing_tests):
tests_that_only_failed_first = first_results_failing_tests.difference(second_results_failing_tests)
self._report_flaky_tests(tests_that_only_failed_first)
tests_that_only_failed_second = second_results_failing_tests.difference(first_results_failing_tests)
self._report_flaky_tests(tests_that_only_failed_second)
tests_that_consistently_failed = first_results_failing_tests.intersection(second_results_failing_tests)
if tests_that_consistently_failed:
if clean_tree_results_did_exceed_test_failure_limit:
return self.retry_build()
failures_introduced_by_patch = tests_that_consistently_failed - clean_tree_results_failing_tests
if failures_introduced_by_patch:
return self.report_failure(failures_introduced_by_patch)
# At this point we know that at least one test flaked, but no consistent failures
# were introduced. This is a bit of a grey-zone. It's possible that the patch introduced some flakiness.
# We still mark the build as SUCCESS.
return self.report_pre_existing_failures(clean_tree_results_failing_tests, flaky_failures)
if clean_tree_results_did_exceed_test_failure_limit:
return self.retry_build()
failures_introduced_by_patch = first_results_failing_tests - clean_tree_results_failing_tests
if failures_introduced_by_patch:
return self.report_failure(failures_introduced_by_patch)
# At this point, we know that the first and second runs had the exact same failures,
# and that those failures are all present on the clean tree, so we can say with certainty
# that the patch is good.
return self.report_pre_existing_failures(clean_tree_results_failing_tests, flaky_failures)
class RunWebKit1Tests(RunWebKitTests):
def start(self):
self.setProperty('use-dump-render-tree', True)
return RunWebKitTests.start(self)
# This is a specialized class designed to cope with a tree that is not always green.
# It tries hard to avoid reporting any false positive, so it will only report new
# consistent failures (fail always with the patch and pass always without it).
class RunWebKitTestsRedTree(RunWebKitTests):
EXIT_AFTER_FAILURES = 500
def _did_command_timed_out(self, logHeadersText):
timed_out_line_start = 'command timed out: {} seconds elapsed running'.format(self.MAX_SECONDS_STEP_RUN)
timed_out_line_end = 'attempting to kill'
for line in logHeadersText.splitlines():
line = line.strip()
if line.startswith(timed_out_line_start) and line.endswith(timed_out_line_end):
return True
return False
def evaluateCommand(self, cmd):
first_results_failing_tests = set(self.getProperty('first_run_failures', []))
first_results_flaky_tests = set(self.getProperty('first_run_flakies', []))
rc = self.evaluateResult(cmd)
next_steps = [ArchiveTestResults(), UploadTestResults(), ExtractTestResults()]
if first_results_failing_tests:
next_steps.extend([ValidateChange(verifyBugClosed=False, addURLs=False), KillOldProcesses(), RunWebKitTestsRepeatFailuresRedTree()])
elif first_results_flaky_tests:
next_steps.append(AnalyzeLayoutTestsResultsRedTree())
elif rc == SUCCESS or rc == WARNINGS:
next_steps = None
message = 'Passed layout tests'
self.descriptionDone = message
self.build.results = SUCCESS
self.setProperty('build_summary', message)
else:
# We have a failure return code but not a list of failed or flaky tests, so we can't run the repeat steps.
# If we are on the last retry then run the whole layout tests without patch.
# If not, then go to analyze-layout-tests-results where we will retry everything hoping this was a random failure.
retry_count = int(self.getProperty('retry_count', 0))
if retry_count < AnalyzeLayoutTestsResultsRedTree.MAX_RETRY:
next_steps.append(AnalyzeLayoutTestsResultsRedTree())
else:
next_steps.extend([
UnApplyPatch(),
RevertPullRequestChanges(),
CompileWebKitWithoutChange(retry_build_on_failure=True),
ValidateChange(verifyBugClosed=False, addURLs=False),
RunWebKitTestsWithoutChangeRedTree(),
])
if next_steps:
self.build.addStepsAfterCurrentStep(next_steps)
return rc
class RunWebKitTestsRepeatFailuresRedTree(RunWebKitTestsRedTree):
name = 'layout-tests-repeat-failures'
NUM_REPEATS_PER_TEST = 10
EXIT_AFTER_FAILURES = None
MAX_SECONDS_STEP_RUN = 18000 # 5h
def __init__(self, **kwargs):
super().__init__(maxTime=self.MAX_SECONDS_STEP_RUN, **kwargs)
def setLayoutTestCommand(self):
super().setLayoutTestCommand()
first_results_failing_tests = set(self.getProperty('first_run_failures', []))
self.setCommand(self.command + ['--fully-parallel', '--repeat-each=%s' % self.NUM_REPEATS_PER_TEST] + sorted(first_results_failing_tests))
def evaluateCommand(self, cmd):
with_change_repeat_failures_results_nonflaky_failures = set(self.getProperty('with_change_repeat_failures_results_nonflaky_failures', []))
with_change_repeat_failures_results_flakies = set(self.getProperty('with_change_repeat_failures_results_flakies', []))
with_change_repeat_failures_timedout = self.getProperty('with_change_repeat_failures_timedout', False)
first_results_flaky_tests = set(self.getProperty('first_run_flakies', []))
rc = self.evaluateResult(cmd)
self.setProperty('with_change_repeat_failures_retcode', rc)
next_steps = [ArchiveTestResults(), UploadTestResults(identifier='repeat-failures'), ExtractTestResults(identifier='repeat-failures')]
if with_change_repeat_failures_results_nonflaky_failures or with_change_repeat_failures_timedout:
next_steps.extend([
ValidateChange(verifyBugClosed=False, addURLs=False),
KillOldProcesses(),
UnApplyPatch(),
RevertPullRequestChanges(),
CompileWebKitWithoutChange(retry_build_on_failure=True),
ValidateChange(verifyBugClosed=False, addURLs=False),
RunWebKitTestsRepeatFailuresWithoutChangeRedTree(),
])
else:
next_steps.append(AnalyzeLayoutTestsResultsRedTree())
if next_steps:
self.build.addStepsAfterCurrentStep(next_steps)
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()
with_change_repeat_failures_results = LayoutTestFailures.results_from_string(logTextJson)
if with_change_repeat_failures_results:
self.setProperty('with_change_repeat_failures_results_exceed_failure_limit', with_change_repeat_failures_results.did_exceed_test_failure_limit)
self.setProperty('with_change_repeat_failures_results_nonflaky_failures', sorted(with_change_repeat_failures_results.failing_tests))
self.setProperty('with_change_repeat_failures_results_flakies', sorted(with_change_repeat_failures_results.flaky_tests))
if with_change_repeat_failures_results.failing_tests:
self._addToLog(self.test_failures_log_name, '\n'.join(with_change_repeat_failures_results.failing_tests))
command_timedout = self._did_command_timed_out(self.log_observer.getHeaders())
self.setProperty('with_change_repeat_failures_timedout', command_timedout)
self._parseRunWebKitTestsOutput(logText)
def start(self):
# buildbot messages about timeout reached appear on the header stream of BufferLog
return super().start(BufferLogObserverClass=BufferLogHeaderObserver)
class RunWebKitTestsRepeatFailuresWithoutChangeRedTree(RunWebKitTestsRedTree):
name = 'layout-tests-repeat-failures-without-change'
NUM_REPEATS_PER_TEST = 10
EXIT_AFTER_FAILURES = None
MAX_SECONDS_STEP_RUN = 10800 # 3h
def __init__(self, **kwargs):
super().__init__(maxTime=self.MAX_SECONDS_STEP_RUN, **kwargs)
def setLayoutTestCommand(self):
super().setLayoutTestCommand()
with_change_nonflaky_failures = set(self.getProperty('with_change_repeat_failures_results_nonflaky_failures', []))
first_run_failures = set(self.getProperty('first_run_failures', []))
with_change_repeat_failures_timedout = self.getProperty('with_change_repeat_failures_timedout', False)
failures_to_repeat = first_run_failures if with_change_repeat_failures_timedout else with_change_nonflaky_failures
# Pass '--skipped=always' to ensure that any test passed via command line arguments
# is skipped anyways if is marked as such on the Expectation files or if is marked
# as failure (since we are passing also '--skip-failing-tests'). That way we ensure
# to report the case of a change removing an expectation that still fails with it.
self.setCommand(self.command + ['--fully-parallel', '--repeat-each=%s' % self.NUM_REPEATS_PER_TEST, '--skipped=always'] + sorted(failures_to_repeat))
def evaluateCommand(self, cmd):
rc = self.evaluateResult(cmd)
self.setProperty('without_change_repeat_failures_retcode', rc)
self.build.addStepsAfterCurrentStep([ArchiveTestResults(), UploadTestResults(identifier='repeat-failures-without-change'), ExtractTestResults(identifier='repeat-failures-without-change'), AnalyzeLayoutTestsResultsRedTree()])
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()
without_change_repeat_failures_results = LayoutTestFailures.results_from_string(logTextJson)
if without_change_repeat_failures_results:
self.setProperty('without_change_repeat_failures_results_exceed_failure_limit', without_change_repeat_failures_results.did_exceed_test_failure_limit)
self.setProperty('without_change_repeat_failures_results_nonflaky_failures', sorted(without_change_repeat_failures_results.failing_tests))
self.setProperty('without_change_repeat_failures_results_flakies', sorted(without_change_repeat_failures_results.flaky_tests))
if without_change_repeat_failures_results.failing_tests:
self._addToLog(self.test_failures_log_name, '\n'.join(without_change_repeat_failures_results.failing_tests))
command_timedout = self._did_command_timed_out(self.log_observer.getHeaders())
self.setProperty('without_change_repeat_failures_timedout', command_timedout)
self._parseRunWebKitTestsOutput(logText)
def start(self):
# buildbot messages about timeout reached appear on the header stream of BufferLog
return super().start(BufferLogObserverClass=BufferLogHeaderObserver)
class RunWebKitTestsWithoutChangeRedTree(RunWebKitTestsWithoutChange):
EXIT_AFTER_FAILURES = 500
def evaluateCommand(self, cmd):
rc = shell.Test.evaluateCommand(self, cmd)
self.build.addStepsAfterCurrentStep([ArchiveTestResults(), UploadTestResults(identifier='clean-tree'), ExtractTestResults(identifier='clean-tree'), AnalyzeLayoutTestsResultsRedTree()])
self.setProperty('clean_tree_run_status', rc)
return rc
class AnalyzeLayoutTestsResultsRedTree(AnalyzeLayoutTestsResults):
MAX_RETRY = 3
def report_success(self):
self.finished(SUCCESS)
self.build.results = SUCCESS
self.descriptionDone = 'Passed layout tests'
message = ''
self.setProperty('build_summary', message)
return defer.succeed(None)
def report_warning(self, message):
self.finished(WARNINGS)
self.build.results = WARNINGS
self.descriptionDone = message
self.setProperty('build_summary', message)
return defer.succeed(None)
def report_infrastructure_issue_and_maybe_retry_build(self, message):
retry_count = int(self.getProperty('retry_count', 0))
if retry_count >= self.MAX_RETRY:
message += '\nReached the maximum number of retries ({}). Unable to determine if change is bad or there is a pre-existent infrastructure issue.'.format(self.MAX_RETRY)
self.send_email_for_infrastructure_issue(message)
return self.report_warning(message)
message += "\nRetrying build [retry count is {} of {}]".format(retry_count, self.MAX_RETRY)
self.setProperty('retry_count', retry_count + 1)
self.send_email_for_infrastructure_issue(message)
return self.retry_build(message='Unexpected infrastructure issue: {}'.format(message))
def send_email_for_pre_existent_failures(self, test_names):
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)
number_failures = len(test_names)
pluralSuffix = 's' if number_failures > 1 else ''
email_subject = 'Info about {} pre-existent failure{} at {}'.format(number_failures, pluralSuffix, builder_name)
email_text = 'Info about pre-existent (non-flaky) test failure{} at EWS:\n'.format(pluralSuffix)
email_text += ' - Build : {}\n'.format(build_url)
email_text += ' - Builder : {}\n'.format(builder_name)
email_text += ' - Worker : {}\n'.format(worker_name)
for test_name in sorted(test_names):
history_url = '{}?suite=layout-tests&test={}'.format(RESULTS_DB_URL, test_name)
email_text += '\n- {} (<a href="{}">test history</a>)'.format(test_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 flaky failure: {}'.format(e))
def send_email_for_flaky_failures_and_steps(self, test_names_steps_dict):
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)
number_failures = len(test_names_steps_dict)
pluralSuffix = 's' if number_failures > 1 else ''
email_subject = 'Info about {} flaky failure{} at {}'.format(number_failures, pluralSuffix, builder_name)
email_text = 'Info about {} flaky test failure{} at EWS:\n'.format(number_failures, pluralSuffix)
email_text += ' - Build : {}\n'.format(build_url)
email_text += ' - Builder : {}\n'.format(builder_name)
email_text += ' - Worker : {}\n'.format(worker_name)
flaky_number = 0
for test_name in test_names_steps_dict:
flaky_number += 1
history_url = '{}?suite=layout-tests&test={}'.format(RESULTS_DB_URL, test_name)
number_steps_flaky = len(test_names_steps_dict[test_name])
pluralstepSuffix = 's' if number_steps_flaky > 1 else ''
step_names_str = '"{}"'.format('", "'.join(test_names_steps_dict[test_name]))
email_text += '\nFlaky #{}\n - Test name: {}\n - Flaky on step{}: {}\n - History: {}\n'.format(flaky_number, test_name, pluralstepSuffix, step_names_str, history_url)
send_email_to_bot_watchers(email_subject, email_text, builder_name, 'flaky-{}'.format(worker_name))
except Exception as e:
print('Error in sending email for flaky failure: {}'.format(e))
def start(self):
# Run with change, running the whole layout test suite
first_results_exceed_failure_limit = self.getProperty('first_results_exceed_failure_limit', False)
first_run_failures = set(self.getProperty('first_run_failures', []))
first_run_flakies = set(self.getProperty('first_run_flakies', []))
# Run with change, running first_run_failures 10 times each test
with_change_repeat_failures_results_exceed_failure_limit = self.getProperty('with_change_repeat_failures_results_exceed_failure_limit', False)
with_change_repeat_failures_results_nonflaky_failures = set(self.getProperty('with_change_repeat_failures_results_nonflaky_failures', []))
with_change_repeat_failures_results_flakies = set(self.getProperty('with_change_repeat_failures_results_flakies', []))
with_change_repeat_failures_timedout = self.getProperty('with_change_repeat_failures_timedout', False)
# Run without change, running with_change_repeat_failures_results_nonflaky_failures 10 times each test
without_change_repeat_failures_results_exceed_failure_limit = self.getProperty('without_change_repeat_failures_results_exceed_failure_limit', False)
without_change_repeat_failures_results_nonflaky_failures = set(self.getProperty('without_change_repeat_failures_results_nonflaky_failures', []))
without_change_repeat_failures_results_flakies = set(self.getProperty('without_change_repeat_failures_results_flakies', []))
without_change_repeat_failures_timedout = self.getProperty('without_change_repeat_failures_timedout', False)
# If we've made it here that means that the first_run failed (non-zero status) but we don't have a list of failures or flakies. That is not expected.
if (not first_run_failures) and (not first_run_flakies):
# If we are not on the last retry, then try to retry the whole testing with the hope it was a random infrastructure error.
retry_count = int(self.getProperty('retry_count', 0))
if retry_count < self.MAX_RETRY:
return self.report_infrastructure_issue_and_maybe_retry_build('The layout-test run with change generated no list of results and exited with error, retrying with the hope it was a random infrastructure error.')
# Otherwise (last retry) report and error or a warning, since we already gave it enough retries for the issue to not be caused by a random infrastructure error.
# The clean tree run that only happens when the first run gives an error code without generating a list of failures or flakies.
clean_tree_run_failures = set(self.getProperty('clean_tree_run_failures', []))
clean_tree_run_flakies = set(self.getProperty('clean_tree_run_flakies', []))
clean_tree_run_status = self.getProperty('clean_tree_run_status', FAILURE)
# If the clean-tree run generated some results then we assume this change broke the script run-webkit-tests or something like that.
if (clean_tree_run_status in [SUCCESS, WARNINGS]) or clean_tree_run_failures or clean_tree_run_flakies:
return self.report_failure(set(), first_results_exceed_failure_limit)
# This will end the testing as retry_count will be now self.MAX_RETRY and a warning will be reported.
return self.report_infrastructure_issue_and_maybe_retry_build('The layout-test run with change generated no list of results and exited with error, and the clean_tree without change run did the same thing.')
if with_change_repeat_failures_results_exceed_failure_limit or without_change_repeat_failures_results_exceed_failure_limit:
return self.report_infrastructure_issue_and_maybe_retry_build('One of the steps for retrying the failed tests has exited early, but this steps should run without "--exit-after-n-failures" switch, so they should not exit early.')
if without_change_repeat_failures_timedout:
return self.report_infrastructure_issue_and_maybe_retry_build('The step "layout-tests-repeat-failures-without-change" was interrumped because it reached the timeout.')
if with_change_repeat_failures_timedout:
# The change is causing the step 'layout-tests-repeat-failures-with-change' to timeout, likely the change is adding many failures or long timeouts needing lot of time to test the repeats.
# Report the tests that failed on the first run as we don't have the information of the ones that failed on 'layout-tests-repeat-failures-with-change' because it was interrupted due to the timeout.
# There is no point in repeating this run, it would happen the same on next runs and consume lot of time.
likely_new_non_flaky_failures = first_run_failures - without_change_repeat_failures_results_nonflaky_failures.union(without_change_repeat_failures_results_flakies)
self.send_email_for_infrastructure_issue('The step "layout-tests-repeat-failures-with-change" reached the timeout but the step "layout-tests-repeat-failures-without-change" ended. Not trying to repeat this. Reporting {} failures from the first run.'.format(len(likely_new_non_flaky_failures)))
return self.report_failure(likely_new_non_flaky_failures, first_results_exceed_failure_limit)
# The checks below need to be after the timeout ones (above) because when a timeout is trigerred no results will be generated for the step.
# The step with_change_repeat_failures generated no list of failures or flakies, which should only happen when the return code of the step is SUCESS or WARNINGS.
if not with_change_repeat_failures_results_nonflaky_failures and not with_change_repeat_failures_results_flakies:
with_change_repeat_failures_retcode = self.getProperty('with_change_repeat_failures_retcode', FAILURE)
if with_change_repeat_failures_retcode not in [SUCCESS, WARNINGS]:
return self.report_infrastructure_issue_and_maybe_retry_build('The step "layout-tests-repeat-failures" failed to generate any list of failures or flakies and returned an error code.')
# Check the same for the step without_change_repeat_failures
if not without_change_repeat_failures_results_nonflaky_failures and not without_change_repeat_failures_results_flakies:
without_change_repeat_failures_retcode = self.getProperty('without_change_repeat_failures_retcode', FAILURE)
if without_change_repeat_failures_retcode not in [SUCCESS, WARNINGS]:
return self.report_infrastructure_issue_and_maybe_retry_build('The step "layout-tests-repeat-failures-without-change" failed to generate any list of failures or flakies and returned an error code.')
# Warn EWS bot watchers about flakies so they can garden those. Include the step where the flaky was found in the e-mail to know if it was found with change or without it.
# Due to the way this class works most of the flakies are filtered on the step with change even when those were pre-existent issues (so this is also useful for bot watchers).
all_flaky_failures = first_run_flakies.union(with_change_repeat_failures_results_flakies).union(without_change_repeat_failures_results_flakies)
flaky_steps_dict = {}
for flaky_failure in all_flaky_failures:
step_names = []
if flaky_failure in without_change_repeat_failures_results_flakies:
step_names.append('layout-tests-repeat-failures-without-change')
if flaky_failure in with_change_repeat_failures_results_flakies:
step_names.append('layout-tests-repeat-failures (with change)')
if flaky_failure in first_run_flakies:
step_names.append('layout-tests (with change)')
flaky_steps_dict[flaky_failure] = step_names
self.send_email_for_flaky_failures_and_steps(flaky_steps_dict)
# Warn EWS bot watchers about pre-existent non-flaky failures (if any), but send only one e-mail with all the tests to avoid sending too much e-mails.
pre_existent_non_flaky_failures = without_change_repeat_failures_results_nonflaky_failures - all_flaky_failures
if pre_existent_non_flaky_failures:
self.send_email_for_pre_existent_failures(pre_existent_non_flaky_failures)
# Finally check if there are new consistent (non-flaky) failures caused by the change and warn the change author stetting the status for the build.
new_non_flaky_failures = with_change_repeat_failures_results_nonflaky_failures - without_change_repeat_failures_results_nonflaky_failures.union(without_change_repeat_failures_results_flakies)
if new_non_flaky_failures:
return self.report_failure(new_non_flaky_failures, first_results_exceed_failure_limit)
return self.report_success()
class ArchiveBuiltProduct(shell.ShellCommand):
command = ['python3', 'Tools/CISupport/built-product-archive',
WithProperties('--platform=%(fullPlatform)s'), WithProperties('--%(configuration)s'), 'archive']
name = 'archive-built-product'
description = ['archiving built product']
descriptionDone = ['Archived built product']
haltOnFailure = True
def __init__(self, **kwargs):
super(ArchiveBuiltProduct, self).__init__(logEnviron=False, **kwargs)
class UploadBuiltProduct(transfer.FileUpload):
name = 'upload-built-product'
workersrc = WithProperties('WebKitBuild/%(configuration)s.zip')
masterdest = WithProperties('public_html/archives/%(fullPlatform)s-%(architecture)s-%(configuration)s/%(change_id)s.zip')
descriptionDone = ['Uploaded built product']
haltOnFailure = True
def __init__(self, **kwargs):
kwargs['workersrc'] = self.workersrc
kwargs['masterdest'] = self.masterdest
kwargs['mode'] = 0o0644
kwargs['blocksize'] = 1024 * 256
transfer.FileUpload.__init__(self, **kwargs)
def getResultSummary(self):
if self.results != SUCCESS:
return {'step': 'Failed to upload built product'}
return super(UploadBuiltProduct, self).getResultSummary()
class TransferToS3(master.MasterShellCommand):
name = 'transfer-to-s3'
description = ['transferring to s3']
descriptionDone = ['Transferred archive to S3']
archive = WithProperties('public_html/archives/%(fullPlatform)s-%(architecture)s-%(configuration)s/%(change_id)s.zip')
identifier = WithProperties('%(fullPlatform)s-%(architecture)s-%(configuration)s')
change_id = WithProperties('%(change_id)s')
command = ['python3', '../Shared/transfer-archive-to-s3', '--change-id', change_id, '--identifier', identifier, '--archive', archive]
haltOnFailure = False
flunkOnFailure = False
def __init__(self, **kwargs):
kwargs['command'] = self.command
master.MasterShellCommand.__init__(self, logEnviron=False, **kwargs)
def start(self):
self.log_observer = logobserver.BufferLogObserver(wantStderr=True)
self.addLogObserver('stdio', self.log_observer)
return super(TransferToS3, self).start()
def finished(self, results):
log_text = self.log_observer.getStdout() + self.log_observer.getStderr()
match = re.search(r'S3 URL: (?P<url>[^\s]+)', log_text)
# Sample log: S3 URL: https://s3-us-west-2.amazonaws.com/ews-archives.webkit.org/ios-simulator-12-x86_64-release/123456.zip
if match:
self.addURL('uploaded archive', match.group('url'))
return super(TransferToS3, self).finished(results)
def doStepIf(self, step):
return CURRENT_HOSTNAME == EWS_BUILD_HOSTNAME
def hideStepIf(self, results, step):
return results == SUCCESS and self.getProperty('sensitive', False)
def getResultSummary(self):
if self.results == FAILURE:
return {'step': 'Failed to transfer archive to S3'}
return super(TransferToS3, self).getResultSummary()
class DownloadBuiltProduct(shell.ShellCommand):
command = ['python3', 'Tools/CISupport/download-built-product',
WithProperties('--%(configuration)s'),
WithProperties(S3URL + 'ews-archives.webkit.org/%(fullPlatform)s-%(architecture)s-%(configuration)s/%(change_id)s.zip')]
name = 'download-built-product'
description = ['downloading built product']
descriptionDone = ['Downloaded built product']
flunkOnFailure = False
def getResultSummary(self):
if self.results not in [SUCCESS, SKIPPED]:
return {'step': 'Failed to download built product from S3'}
return super(DownloadBuiltProduct, self).getResultSummary()
def __init__(self, **kwargs):
super(DownloadBuiltProduct, self).__init__(logEnviron=False, **kwargs)
def start(self):
# Only try to download from S3 on the official deployment <https://webkit.org/b/230006>
if CURRENT_HOSTNAME == EWS_BUILD_HOSTNAME:
return shell.ShellCommand.start(self)
self.build.addStepsAfterCurrentStep([DownloadBuiltProductFromMaster()])
self.finished(SKIPPED)
return defer.succeed(None)
def evaluateCommand(self, cmd):
rc = shell.ShellCommand.evaluateCommand(self, cmd)
if rc == FAILURE:
self.build.addStepsAfterCurrentStep([DownloadBuiltProductFromMaster()])
return rc
class DownloadBuiltProductFromMaster(transfer.FileDownload):
mastersrc = WithProperties('public_html/archives/%(fullPlatform)s-%(architecture)s-%(configuration)s/%(change_id)s.zip')
workerdest = WithProperties('WebKitBuild/%(configuration)s.zip')
name = 'download-built-product-from-master'
description = ['downloading built product from buildbot master']
descriptionDone = ['Downloaded built product']
haltOnFailure = True
flunkOnFailure = True
def __init__(self, **kwargs):
# Allow the unit test to override mastersrc
if 'mastersrc' not in kwargs:
kwargs['mastersrc'] = self.mastersrc
kwargs['workerdest'] = self.workerdest
kwargs['mode'] = 0o0644
kwargs['blocksize'] = 1024 * 256
transfer.FileDownload.__init__(self, **kwargs)
def getResultSummary(self):
if self.results != SUCCESS:
return {'step': 'Failed to download built product from build master'}
return super(DownloadBuiltProductFromMaster, self).getResultSummary()
class ExtractBuiltProduct(shell.ShellCommand):
command = ['python3', 'Tools/CISupport/built-product-archive',
WithProperties('--platform=%(fullPlatform)s'), WithProperties('--%(configuration)s'), 'extract']
name = 'extract-built-product'
description = ['extracting built product']
descriptionDone = ['Extracted built product']
haltOnFailure = True
flunkOnFailure = True
def __init__(self, **kwargs):
super(ExtractBuiltProduct, self).__init__(logEnviron=False, **kwargs)
class RunAPITests(TestWithFailureCount):
name = 'run-api-tests'
description = ['api tests running']
descriptionDone = ['api-tests']
jsonFileName = 'api_test_results.json'
logfiles = {'json': jsonFileName}
command = ['python3', 'Tools/Scripts/run-api-tests', '--no-build',
WithProperties('--%(configuration)s'), '--verbose', '--json-output={0}'.format(jsonFileName)]
failedTestsFormatString = '%d api test%s failed or timed out'
def __init__(self, **kwargs):
super(RunAPITests, self).__init__(logEnviron=False, **kwargs)
def start(self):
platform = self.getProperty('platform')
if platform == 'gtk':
command = ['python3', 'Tools/Scripts/run-gtk-tests',
'--{0}'.format(self.getProperty('configuration')),
'--json-output={0}'.format(self.jsonFileName)]
self.setCommand(command)
else:
appendCustomBuildFlags(self, platform, self.getProperty('fullPlatform'))
return TestWithFailureCount.start(self)
def countFailures(self, cmd):
log_text = self.log_observer.getStdout() + self.log_observer.getStderr()
match = re.search(r'Ran (?P<ran>\d+) tests of (?P<total>\d+) with (?P<passed>\d+) successful', log_text)
if not match:
return 0
return int(match.group('ran')) - int(match.group('passed'))
def evaluateCommand(self, cmd):
rc = super(RunAPITests, self).evaluateCommand(cmd)
if rc == SUCCESS:
message = 'Passed API tests'
self.descriptionDone = message
self.build.results = SUCCESS
self.build.buildFinished([message], SUCCESS)
else:
self.build.addStepsAfterCurrentStep([ValidateChange(verifyBugClosed=False, addURLs=False), KillOldProcesses(), ReRunAPITests()])
return rc
class ReRunAPITests(RunAPITests):
name = 're-run-api-tests'
def evaluateCommand(self, cmd):
rc = TestWithFailureCount.evaluateCommand(self, cmd)
if rc == SUCCESS:
message = 'Passed API tests'
self.descriptionDone = message
self.build.results = SUCCESS
self.build.buildFinished([message], SUCCESS)
else:
steps_to_add = [UnApplyPatch(), RevertPullRequestChanges(), ValidateChange(verifyBugClosed=False, addURLs=False)]
platform = self.getProperty('platform')
if platform == 'wpe':
steps_to_add.append(InstallWpeDependencies())
elif platform == 'gtk':
steps_to_add.append(InstallGtkDependencies())
steps_to_add.append(CompileWebKitWithoutChange(retry_build_on_failure=True))
steps_to_add.append(ValidateChange(verifyBugClosed=False, addURLs=False))
steps_to_add.append(KillOldProcesses())
steps_to_add.append(RunAPITestsWithoutChange())
steps_to_add.append(AnalyzeAPITestsResults())
# Using a single addStepsAfterCurrentStep because of https://github.com/buildbot/buildbot/issues/4874
self.build.addStepsAfterCurrentStep(steps_to_add)
return rc
class RunAPITestsWithoutChange(RunAPITests):
name = 'run-api-tests-without-change'
def evaluateCommand(self, cmd):
return TestWithFailureCount.evaluateCommand(self, cmd)
class AnalyzeAPITestsResults(buildstep.BuildStep, AddToLogMixin):
name = 'analyze-api-tests-results'
description = ['analyze-api-test-results']
descriptionDone = ['analyze-api-tests-results']
NUM_FAILURES_TO_DISPLAY = 10
def start(self):
self.results = {}
d = self.getTestsResults(RunAPITests.name)
d.addCallback(lambda res: self.getTestsResults(ReRunAPITests.name))
d.addCallback(lambda res: self.getTestsResults(RunAPITestsWithoutChange.name))
d.addCallback(lambda res: self.analyzeResults())
return defer.succeed(None)
def analyzeResults(self):
if not self.results or len(self.results) == 0:
self._addToLog('stderr', 'Unable to parse API test results: {}'.format(self.results))
self.finished(RETRY)
self.build.buildFinished(['Unable to parse API test results'], RETRY)
return -1
first_run_results = self.results.get(RunAPITests.name)
second_run_results = self.results.get(ReRunAPITests.name)
clean_tree_results = self.results.get(RunAPITestsWithoutChange.name)
if not (first_run_results and second_run_results):
self.finished(RETRY)
self.build.buildFinished(['Unable to parse API test results'], RETRY)
return -1
def getAPITestFailures(result):
if not result:
return set([])
# TODO: Analyze Time-out, Crash and Failure independently
return set([failure.get('name') for failure in result.get('Timedout', [])] +
[failure.get('name') for failure in result.get('Crashed', [])] +
[failure.get('name') for failure in result.get('Failed', [])])
first_run_failures = getAPITestFailures(first_run_results)
second_run_failures = getAPITestFailures(second_run_results)
clean_tree_failures = getAPITestFailures(clean_tree_results)
clean_tree_failures_to_display = list(clean_tree_failures)[:self.NUM_FAILURES_TO_DISPLAY]
clean_tree_failures_string = ', '.join(clean_tree_failures_to_display)
failures_with_patch = first_run_failures.intersection(second_run_failures)
flaky_failures = first_run_failures.union(second_run_failures) - first_run_failures.intersection(second_run_failures)
flaky_failures = list(flaky_failures)[:self.NUM_FAILURES_TO_DISPLAY]
flaky_failures_string = ', '.join(flaky_failures)
new_failures = failures_with_patch - clean_tree_failures
new_failures_to_display = list(new_failures)[:self.NUM_FAILURES_TO_DISPLAY]
new_failures_string = ', '.join(new_failures_to_display)
self._addToLog('stderr', '\nFailures in API Test first run: {}'.format(list(first_run_failures)[:self.NUM_FAILURES_TO_DISPLAY]))
self._addToLog('stderr', '\nFailures in API Test second run: {}'.format(list(second_run_failures)[:self.NUM_FAILURES_TO_DISPLAY]))
self._addToLog('stderr', '\nFlaky Tests: {}'.format(flaky_failures_string))
self._addToLog('stderr', '\nFailures in API Test on clean tree: {}'.format(clean_tree_failures_string))
if new_failures:
self._addToLog('stderr', '\nNew failures: {}\n'.format(new_failures_string))
self.finished(FAILURE)
self.build.results = FAILURE
pluralSuffix = 's' if len(new_failures) > 1 else ''
message = 'Found {} new API test failure{}: {}'.format(len(new_failures), pluralSuffix, new_failures_string)
if len(new_failures) > self.NUM_FAILURES_TO_DISPLAY:
message += ' ...'
self.descriptionDone = message
self.build.buildFinished([message], FAILURE)
else:
self._addToLog('stderr', '\nNo new failures\n')
self.finished(SUCCESS)
self.build.results = SUCCESS
self.descriptionDone = 'Passed API tests'
pluralSuffix = 's' if len(clean_tree_failures) > 1 else ''
message = ''
if clean_tree_failures:
message = 'Found {} pre-existing API test failure{}: {}'.format(len(clean_tree_failures), pluralSuffix, clean_tree_failures_string)
for clean_tree_failure in clean_tree_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)
def getBuildStepByName(self, name):
for step in self.build.executedSteps:
if step.name == name:
return step
return None
@defer.inlineCallbacks
def getTestsResults(self, name):
step = self.getBuildStepByName(name)
if not step:
self._addToLog('stderr', 'ERROR: step not found: {}'.format(step))
defer.returnValue(None)
logs = yield self.master.db.logs.getLogs(step.stepid)
log = next((log for log in logs if log['name'] == 'json'), None)
if not log:
self._addToLog('stderr', 'ERROR: log for step not found: {}'.format(step))
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 = ''.join([line[1:] for line in logLines.splitlines()])
try:
self.results[name] = json.loads(logLines)
except Exception as ex:
self._addToLog('stderr', 'ERROR: unable to parse data, exception: {}'.format(ex))
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=api-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=api-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 ArchiveTestResults(shell.ShellCommand):
command = ['python3', 'Tools/CISupport/test-result-archive',
Interpolate('--platform=%(prop:platform)s'), Interpolate('--%(prop:configuration)s'), 'archive']
name = 'archive-test-results'
description = ['archiving test results']
descriptionDone = ['Archived test results']
haltOnFailure = True
def __init__(self, **kwargs):
super(ArchiveTestResults, self).__init__(logEnviron=False, **kwargs)
class UploadTestResults(transfer.FileUpload):
name = 'upload-test-results'
descriptionDone = ['Uploaded test results']
workersrc = 'layout-test-results.zip'
haltOnFailure = True
def __init__(self, identifier='', **kwargs):
if identifier and not identifier.startswith('-'):
identifier = '-{}'.format(identifier)
kwargs['workersrc'] = self.workersrc
kwargs['masterdest'] = Interpolate('public_html/results/%(prop:buildername)s/%(prop:change_id)s-%(prop:buildnumber)s{}.zip'.format(identifier))
kwargs['mode'] = 0o0644
kwargs['blocksize'] = 1024 * 256
transfer.FileUpload.__init__(self, **kwargs)
class ExtractTestResults(master.MasterShellCommand):
name = 'extract-test-results'
descriptionDone = ['Extracted test results']
renderables = ['resultDirectory', 'zipFile']
haltOnFailure = False
flunkOnFailure = False
def __init__(self, identifier=''):
if identifier and not identifier.startswith('-'):
identifier = '-{}'.format(identifier)
self.zipFile = Interpolate('public_html/results/%(prop:buildername)s/%(prop:change_id)s-%(prop:buildnumber)s{}.zip'.format(identifier))
self.resultDirectory = Interpolate('public_html/results/%(prop:buildername)s/%(prop:change_id)s-%(prop:buildnumber)s{}'.format(identifier))
self.command = ['unzip', '-q', '-o', self.zipFile, '-d', self.resultDirectory]
master.MasterShellCommand.__init__(self, command=self.command, logEnviron=False)
def resultDirectoryURL(self):
path = self.resultDirectory.replace('public_html/results/', '') + '/'
return '{}{}'.format(S3_RESULTS_URL, path)
def resultsDownloadURL(self):
path = self.zipFile.replace('public_html/results/', '')
return '{}{}'.format(S3_RESULTS_URL, path)
def getLastBuildStepByName(self, name):
for step in reversed(self.build.executedSteps):
if name in step.name:
return step
return None
def addCustomURLs(self):
step = self.getLastBuildStepByName(RunWebKitTests.name)
if not step:
step = self
step.addURL('view layout test results', self.resultDirectoryURL() + 'results.html')
step.addURL('download layout test results', self.resultsDownloadURL())
def finished(self, result):
self.addCustomURLs()
return master.MasterShellCommand.finished(self, result)
class PrintConfiguration(steps.ShellSequence):
name = 'configuration'
description = ['configuration']
haltOnFailure = False
flunkOnFailure = False
warnOnFailure = False
logEnviron = False
command_list_generic = [['hostname']]
command_list_apple = [['df', '-hl'], ['date'], ['sw_vers'], ['system_profiler', 'SPSoftwareDataType', 'SPHardwareDataType'], ['xcodebuild', '-sdk', '-version']]
command_list_linux = [['df', '-hl'], ['date'], ['uname', '-a'], ['uptime']]
command_list_win = [['df', '-hl']]
def __init__(self, **kwargs):
super(PrintConfiguration, self).__init__(timeout=60, **kwargs)
self.commands = []
self.log_observer = logobserver.BufferLogObserver(wantStderr=True)
self.addLogObserver('stdio', self.log_observer)
def run(self):
command_list = list(self.command_list_generic)
platform = self.getProperty('platform', '*')
if platform != 'jsc-only':
platform = platform.split('-')[0]
if platform in ('mac', 'ios', 'tvos', 'watchos', '*'):
command_list.extend(self.command_list_apple)
elif platform in ('gtk', 'wpe', 'jsc-only'):
command_list.extend(self.command_list_linux)
elif platform in ('win'):
command_list.extend(self.command_list_win)
for command in command_list:
self.commands.append(util.ShellArg(command=command, logname='stdio'))
return super(PrintConfiguration, self).run()
def convert_build_to_os_name(self, build):
if not build:
return 'Unknown'
build_to_name_mapping = {
'12': 'Monterey',
'11': 'Big Sur',
'10.15': 'Catalina',
}
for key, value in build_to_name_mapping.items():
if build.startswith(key):
return value
return 'Unknown'
def getResultSummary(self):
if self.results != SUCCESS:
return {'step': 'Failed to print configuration'}
logText = self.log_observer.getStdout() + self.log_observer.getStderr()
configuration = 'Printed configuration'
match = re.search('ProductVersion:[ \t]*(.+?)\n', logText)
if match:
os_version = match.group(1).strip()
os_name = self.convert_build_to_os_name(os_version)
configuration = 'OS: {} ({})'.format(os_name, os_version)
xcode_re = sdk_re = 'Xcode[ \t]+?([0-9.]+?)\n'
match = re.search(xcode_re, logText)
if match:
xcode_version = match.group(1).strip()
configuration += ', Xcode: {}'.format(xcode_version)
return {'step': configuration}
# FIXME: We should be able to remove this step once abandoning patch workflows
class CleanGitRepo(steps.ShellSequence, ShellMixin):
name = 'clean-up-git-repo'
haltOnFailure = False
flunkOnFailure = False
logEnviron = False
def __init__(self, default_branch=DEFAULT_BRANCH, remote='origin', **kwargs):
super(CleanGitRepo, self).__init__(timeout=5 * 60, **kwargs)
self.default_branch = default_branch
self.git_remote = remote
def run(self):
branch = self.getProperty('basename', self.default_branch)
self.commands = []
for command in [
self.shell_command('git rebase --abort || {}'.format(self.shell_exit_0())),
self.shell_command('git am --abort || {}'.format(self.shell_exit_0())),
['git', 'clean', '-f', '-d'], # Remove any left-over layout test results, added files, etc.
['git', 'fetch', self.git_remote], # Avoid updating the working copy to a stale revision.
['git', 'checkout', '{}/{}'.format(self.git_remote, branch), '-f'], # Checkout branch from specific remote
['git', 'branch', '-D', '{}'.format(branch)], # Delete any local cache of the specified branch
['git', 'checkout', '{}/{}'.format(self.git_remote, branch), '-b', '{}'.format(branch)], # Checkout local instance of branch from remote
self.shell_command('git branch | grep -v {} | grep -v {} | xargs git branch -D || {}'.format(self.default_branch, branch, self.shell_exit_0())),
self.shell_command('git remote | grep -v {} | xargs -L 1 git remote rm || {}'.format(self.git_remote, self.shell_exit_0())),
]:
self.commands.append(util.ShellArg(command=command, logname='stdio'))
return super(CleanGitRepo, self).run()
def getResultSummary(self):
if self.results != SUCCESS:
return {'step': 'Encountered some issues during cleanup'}
return {'step': 'Cleaned up git repository'}
class ApplyWatchList(shell.ShellCommand):
name = 'apply-watch-list'
description = ['applying watchilist']
descriptionDone = ['Applied WatchList']
bug_id = WithProperties('%(bug_id)s')
command = ['python3', 'Tools/Scripts/webkit-patch', 'apply-watchlist-local', bug_id]
haltOnFailure = True
flunkOnFailure = True
def __init__(self, **kwargs):
shell.ShellCommand.__init__(self, timeout=2 * 60, logEnviron=False, **kwargs)
def getResultSummary(self):
if self.results != SUCCESS:
return {'step': 'Failed to apply watchlist'}
return super(ApplyWatchList, self).getResultSummary()
class SetBuildSummary(buildstep.BuildStep):
name = 'set-build-summary'
descriptionDone = ['Set build summary']
alwaysRun = True
haltOnFailure = False
flunkOnFailure = False
def doStepIf(self, step):
return self.getProperty('build_summary', False)
def hideStepIf(self, results, step):
return not self.doStepIf(step)
def start(self):
build_summary = self.getProperty('build_summary', 'build successful')
self.finished(SUCCESS)
self.build.buildFinished([build_summary], self.build.results)
return defer.succeed(None)
class PushCommitToWebKitRepo(shell.ShellCommand):
name = 'push-commit-to-webkit-repo'
descriptionDone = ['Pushed commit to WebKit repository']
haltOnFailure = False
MAX_RETRY = 2
HASH_RE = re.compile(r'\s+[0-9a-f]+\.\.+(?P<hash>[0-9a-f]+)\s+')
def __init__(self, **kwargs):
super(PushCommitToWebKitRepo, self).__init__(logEnviron=False, timeout=300, **kwargs)
def start(self, BufferLogObserverClass=logobserver.BufferLogObserver):
head_ref = self.getProperty('github.base.ref', 'main')
self.command = ['git', 'push', 'origin', f'HEAD:{head_ref}'] # FIXME: Support secret remotes
username, access_token = GitHub.credentials(user=GitHub.user_for_queue(self.getProperty('buildername', '')))
self.workerEnvironment['GIT_USER'] = username
self.workerEnvironment['GIT_PASSWORD'] = access_token
self.log_observer = logobserver.BufferLogObserver(wantStderr=True)
self.addLogObserver('stdio', self.log_observer)
return super(PushCommitToWebKitRepo, self).start()
def evaluateCommand(self, cmd):
rc = shell.ShellCommand.evaluateCommand(self, cmd)
if rc == SUCCESS:
log_text = self.log_observer.getStdout() + self.log_observer.getStderr()
landed_hash = self.hash_from_commit_text(log_text)
if landed_hash:
self.setProperty('landed_hash', landed_hash)
steps_to_add = [
DetermineLandedIdentifier(),
LeaveComment(),
RemoveFlagsOnPatch(), RemoveLabelsFromPullRequest(),
CloseBug(), ClosePullRequest(),
]
self.build.addStepsAfterCurrentStep(steps_to_add)
else:
retry_count = int(self.getProperty('retry_count', 0))
if retry_count < self.MAX_RETRY:
self.setProperty('retry_count', retry_count + 1)
if self.getProperty('github.number', ''):
self.build.addStepsAfterCurrentStep([
CleanGitRepo(),
CheckOutSource(),
ShowIdentifier(),
UpdateWorkingDirectory(),
CheckOutPullRequest(),
AddReviewerToCommitMessage(),
ValidateChange(verifyMergeQueue=True, verifyNoDraftForMergeQueue=True),
Canonicalize(),
PushCommitToWebKitRepo(),
])
else:
self.build.addStepsAfterCurrentStep([
CleanGitRepo(),
CheckOutSource(),
ShowIdentifier(),
UpdateWorkingDirectory(),
CommitPatch(),
AddReviewerToCommitMessage(),
ValidateChange(addURLs=False, verifycqplus=True),
Canonicalize(),
PushPullRequestBranch(),
UpdatePullRequest(),
PushCommitToWebKitRepo(),
])
return rc
if self.getProperty('github.number', ''):
self.setProperty('comment_text', 'merge-queue failed to commit PR to repository. To retry, remove any blocking labels and re-apply merge-queue label')
else:
patch_id = self.getProperty('patch_id', '')
self.setProperty('comment_text', f'commit-queue failed to commit attachment {patch_id} to WebKit repository. To retry, please set cq+ flag again.')
self.setProperty('build_finish_summary', 'Failed to commit to WebKit repository')
self.build.addStepsAfterCurrentStep([LeaveComment(), SetCommitQueueMinusFlagOnPatch(), BlockPullRequest()])
return rc
def getResultSummary(self):
if self.results != SUCCESS:
return {'step': 'Failed to push commit to Webkit repository'}
return shell.ShellCommand.getResultSummary(self)
def doStepIf(self, step):
return CURRENT_HOSTNAME == EWS_BUILD_HOSTNAME
def hash_from_commit_text(self, commit_text):
match = self.HASH_RE.search(commit_text)
if match:
return match.group('hash')
return None
class DetermineLandedIdentifier(shell.ShellCommand):
name = 'determine-landed-identifier'
descriptionDone = ['Determined landed identifier']
command = ['git', 'log', '-1', '--no-decorate']
CANONICAL_LINK_RE = re.compile(r'\ACanonical link: https://commits\.webkit\.org/(?P<identifier>\d+.?\d*@\S+)\Z')
haltOnFailure = False
def __init__(self, **kwargs):
self.identifier = None
super(DetermineLandedIdentifier, self).__init__(logEnviron=False, timeout=300, **kwargs)
def start(self, BufferLogObserverClass=logobserver.BufferLogObserver):
self.log_observer = BufferLogObserverClass(wantStderr=True)
self.addLogObserver('stdio', self.log_observer)
return super(DetermineLandedIdentifier, self).start()
def getResultSummary(self):
if self.results == SUCCESS:
return {'step': f'Identifier: {self.identifier}'}
if self.results == FAILURE:
return {'step': 'Failed to determine identifier'}
return super(DetermineLandedIdentifier, self).getResultSummary()
def evaluateCommand(self, cmd):
rc = super(DetermineLandedIdentifier, self).evaluateCommand(cmd)
loglines = self.log_observer.getStdout().splitlines()
for line in loglines[4:]:
match = self.CANONICAL_LINK_RE.match(line[4:])
if match:
self.identifier = match.group('identifier')
break
landed_hash = self.getProperty('landed_hash')
if not self.identifier:
time.sleep(60) # It takes time for commits.webkit.org to digest commits
self.identifier = self.identifier_for_hash(landed_hash)
if '@' not in self.identifier:
rc = FAILURE
self.setProperty('comment_text', self.comment_text_for_bug(landed_hash, self.identifier))
commit_summary = f'Committed {self.identifier or landed_hash}'
self.descriptionDone = commit_summary
self.setProperty('build_summary', commit_summary)
self.addURL(self.identifier, self.url_for_identifier(self.identifier))
return rc
def url_for_hash_details(self, hash):
return '{}{}/json'.format(COMMITS_INFO_URL, hash)
def url_for_identifier(self, identifier):
return '{}{}'.format(COMMITS_INFO_URL, identifier)
def identifier_for_hash(self, hash):
try:
response = requests.get(self.url_for_hash_details(hash), timeout=60)
if response and response.status_code == 200:
return response.json().get('identifier', '{}'.format(hash)).replace('@trunk', '@main')
else:
print('Non-200 status code received from {}: {}'.format(COMMITS_INFO_URL, response.status_code))
print(response.text)
except Exception as e:
print(e)
return hash
def comment_text_for_bug(self, hash=None, identifier=None):
identifier_str = identifier if identifier and '@' in identifier else '?'
comment = '{} {} ({}): <{}>'.format(
'Test gardening commit' if self.getProperty('is_test_gardening') else 'Committed',
identifier_str, hash, self.url_for_identifier(identifier),
)
patch_id = self.getProperty('patch_id', '')
if patch_id:
comment += f'\n\nAll reviewed patches have been landed. Closing bug and clearing flags on attachment {patch_id}.'
pr_number = self.getProperty('github.number', '')
if pr_number:
comment += f'\n\nReviewed commits have been landed. Closing PR #{pr_number} and removing active labels.'
return comment
def hideStepIf(self, results, step):
return self.getProperty('sensitive', False)
class CheckPatchStatusOnEWSQueues(buildstep.BuildStep, BugzillaMixin):
name = 'check-status-on-other-ewses'
descriptionDone = ['Checked patch status on other queues']
def get_patch_status(self, patch_id, queue):
url = '{}status/{}'.format(EWS_URL, patch_id)
try:
response = requests.get(url, timeout=60)
if response.status_code != 200:
self._addToLog('stdio', 'Failed to access {} with status code: {}\n'.format(url, response.status_code))
return -1
queue_data = response.json().get(queue)
if not queue_data:
return -1
return queue_data.get('state')
except Exception as e:
self._addToLog('stdio', 'Failed to access {}\n'.format(url))
return -1
def start(self):
patch_id = self.getProperty('patch_id', '')
patch_status_on_mac_wk2 = self.get_patch_status(patch_id, 'mac-wk2')
if patch_status_on_mac_wk2 == SUCCESS:
self.setProperty('passed_mac_wk2', True)
self.finished(SUCCESS)
return None
class ValidateSquashed(shell.ShellCommand):
name = 'validate-squashed'
haltOnFailure = True
def __init__(self, **kwargs):
super(ValidateSquashed, self).__init__(logEnviron=False, **kwargs)
def start(self, BufferLogObserverClass=logobserver.BufferLogObserver):
base_ref = self.getProperty('github.base.ref', f'origin/{DEFAULT_BRANCH}')
head_ref = self.getProperty('github.head.ref', 'HEAD')
self.command = ['git', 'log', '--oneline', head_ref, f'^{base_ref}', '--max-count=2']
self.log_observer = BufferLogObserverClass(wantStderr=True)
self.addLogObserver('stdio', self.log_observer)
return shell.ShellCommand.start(self)
def getResultSummary(self):
if self.results == FAILURE:
return {'step': 'Can only land squashed branches'}
if self.results == SUCCESS:
return {'step': 'Verified branch is squashed'}
return super(ValidateSquashed, self).getResultSummary()
def evaluateCommand(self, cmd):
rc = shell.ShellCommand.evaluateCommand(self, cmd)
if rc != SUCCESS:
return rc
log_text = self.log_observer.getStdout()
if len(log_text.splitlines()) == 1:
return SUCCESS
return FAILURE
class AddReviewerMixin(object):
NOBODY_SED = 's/NOBODY (OO*PP*S!*)/{}/g'
def gitCommitEnvironment(self):
owners = self.getProperty('owners', [])
if not owners:
return dict(
GIT_COMMITTER_NAME='EWS',
GIT_COMMITTER_EMAIL=FROM_EMAIL,
FILTER_BRANCH_SQUELCH_WARNING='1',
)
contributors, _ = Contributors.load()
return dict(
GIT_COMMITTER_NAME=contributors.get(owners[0].lower(), {}).get('name', 'EWS'),
GIT_COMMITTER_EMAIL=contributors.get(owners[0].lower(), {}).get('email', FROM_EMAIL),
FILTER_BRANCH_SQUELCH_WARNING='1',
)
def reviewers(self):
reviewers = self.getProperty('reviewers_full_names', [])
if len(reviewers) == 1:
return reviewers[0]
if reviewers:
return f'{", ".join(reviewers[:-1])} and {reviewers[-1]}'
return 'NOBODY (OOPS!)'
class AddReviewerToCommitMessage(shell.ShellCommand, AddReviewerMixin):
name = 'add-reviewer-to-commit-message'
haltOnFailure = True
def __init__(self, **kwargs):
super(AddReviewerToCommitMessage, self).__init__(logEnviron=False, timeout=60, **kwargs)
def start(self, BufferLogObserverClass=logobserver.BufferLogObserver):
base_ref = self.getProperty('github.base.ref', f'origin/{DEFAULT_BRANCH}')
head_ref = self.getProperty('github.head.ref', 'HEAD')
gmtoffset = int(time.localtime().tm_gmtoff * 100 / (60 * 60))
date = f'{int(time.time())} {gmtoffset}'
self.command = [
'git', 'filter-branch', '-f',
'--env-filter', f"GIT_AUTHOR_DATE='{date}';GIT_COMMITTER_DATE='{date}'",
'--msg-filter', f'sed "{self.NOBODY_SED.format(self.reviewers())}"',
f'{head_ref}...{base_ref}',
]
for key, value in self.gitCommitEnvironment().items():
self.workerEnvironment[key] = value
return super(AddReviewerToCommitMessage, self).start()
def getResultSummary(self):
if self.results == FAILURE:
return {'step': 'Failed to apply reviewers'}
if self.results == SUCCESS:
return {'step': f'Reviewed by {self.reviewers()}'}
return super(AddReviewerToCommitMessage, self).getResultSummary()
def doStepIf(self, step):
return self.getProperty('reviewers_full_names')
def hideStepIf(self, results, step):
return not self.doStepIf(step)
class ValidateCommitMessage(steps.ShellSequence, ShellMixin, AddToLogMixin):
name = 'validate-commit-message'
haltOnFailure = False
flunkOnFailure = True
OOPS_RE = 'OO*PP*S!'
REVIEWED_STRINGS = (
'Reviewed by',
'Rubber-stamped by',
'Rubber stamped by',
'Unreviewed',
)
RE_CHANGELOG = br'^(\+\+\+)\s+(.*ChangeLog.*)'
BY_RE = re.compile(r'.+\s+by\s+(.+)$')
SPLIT_RE = re.compile(r'(,\s*)|( and )')
def __init__(self, **kwargs):
super(ValidateCommitMessage, self).__init__(logEnviron=False, timeout=60, **kwargs)
self.contributors = {}
@classmethod
def extract_reviewers(cls, text):
reviewers = set()
stripped_text = ''
for line in text.splitlines():
match = cls.BY_RE.match(line)
if not match:
stripped_text += line + '\n'
continue
for person in cls.SPLIT_RE.split(match.group(1)):
if person and not cls.SPLIT_RE.match(person):
reviewers.add(person.rstrip('.'))
return reviewers, stripped_text
def is_reviewer(self, name):
contributor = self.contributors.get(name)
return contributor and contributor['status'] == 'reviewer'
def _files(self):
sourcestamp = self.build.getSourceStamp(self.getProperty('codebase', ''))
if sourcestamp and sourcestamp.changes:
return sourcestamp.changes[0].files
if sourcestamp and sourcestamp.patch:
files = []
for line in sourcestamp.patch[1].splitlines():
match = re.search(self.RE_CHANGELOG, line)
if match:
files.append(match.group(1))
return files
return []
@defer.inlineCallbacks
def run(self, BufferLogObserverClass=logobserver.BufferLogObserver):
base_ref = self.getProperty('github.base.ref', f'origin/{DEFAULT_BRANCH}')
head_ref = self.getProperty('github.head.ref', 'HEAD')
self.commands = []
commands = [
f"git log {head_ref} ^{base_ref} | grep -q '{self.OOPS_RE}' && echo 'Commit message contains (OOPS!)' || test $? -eq 1",
"git log {} ^{} | grep -q '\\({}\\)' || echo 'No reviewer information in commit message'".format(
head_ref, base_ref,
'\\|'.join(self.REVIEWED_STRINGS),
), "git log {} ^{} | grep '\\({}\\)' || true".format(
head_ref, base_ref,
'\\|'.join(self.REVIEWED_STRINGS[:-1]),
),
]
for command in commands:
self.commands.append(util.ShellArg(command=self.shell_command(command), logname='stdio', haltOnFailure=True))
self.log_observer = BufferLogObserverClass(wantStderr=True)
self.addLogObserver('stdio', self.log_observer)
rc = yield super(ValidateCommitMessage, self).run()
if rc == SKIPPED:
self.summary = 'Patches have no commit message'
return rc
self.contributors, errors = Contributors.load(use_network=True)
for error in errors:
print(error)
self._addToLog('stdio', error)
reviewers, log_text = self.extract_reviewers(self.log_observer.getStdout())
log_text = log_text.rstrip()
author = self.getProperty('author', '')
if any(['ChangeLog' in file for file in self._files()]):
self.summary = 'ChangeLog modified, WebKit only allows commit messages'
rc = FAILURE
elif log_text:
self.summary = log_text
rc = FAILURE
elif rc == SUCCESS:
if reviewers and not self.contributors:
self.summary = "Failed to load contributors.json, can't validate reviewers"
rc = FAILURE
elif reviewers and any([not self.is_reviewer(reviewer) for reviewer in reviewers]):
self.summary = "'{}' is not a reviewer"
for reviewer in reviewers:
if not self.is_reviewer(reviewer):
self.summary = self.summary.format(reviewer)
break
rc = FAILURE
elif reviewers and author and any([author.startswith(reviewer) for reviewer in reviewers]):
self.summary = f"'{author}' cannot review their own change"
rc = FAILURE
else:
self.summary = 'Validated commit message'
else:
self.summary = 'Error parsing commit message'
rc = FAILURE
if rc == FAILURE:
self.setProperty('comment_text', f"{self.summary}, blocking PR #{self.getProperty('github.number')}")
self.setProperty('build_finish_summary', 'Commit message validation failed')
self.build.addStepsAfterCurrentStep([LeaveComment(), SetCommitQueueMinusFlagOnPatch(), BlockPullRequest()])
return rc
def getResultSummary(self):
if self.results in (SUCCESS, FAILURE):
return {'step': self.summary}
return super(ValidateCommitMessage, self).getResultSummary()
class Canonicalize(steps.ShellSequence, ShellMixin):
name = 'canonicalize-commit'
description = ['canonicalize-commit']
descriptionDone = ['Canonicalize Commit']
haltOnFailure = True
env = dict(FILTER_BRANCH_SQUELCH_WARNING='1')
def __init__(self, rebase_enabled=True, **kwargs):
super(Canonicalize, self).__init__(logEnviron=False, timeout=300, **kwargs)
self.rebase_enabled = rebase_enabled
def run(self):
self.commands = []
base_ref = self.getProperty('github.base.ref', DEFAULT_BRANCH)
head_ref = self.getProperty('github.head.ref', None)
commands = [self.shell_command('rm .git/identifiers.json || {}'.format(self.shell_exit_0()))]
if self.rebase_enabled:
commands += [['git', 'pull', 'origin', base_ref, '--rebase']]
if head_ref:
commands += [['git', 'branch', '-f', base_ref, head_ref]]
commands += [['git', 'checkout', base_ref]]
commands.append(['python3', 'Tools/Scripts/git-webkit', 'canonicalize', '-n', '1' if self.rebase_enabled else '3'])
gmtoffset = int(time.localtime().tm_gmtoff * 100 / (60 * 60))
date = f'{int(time.time())} {gmtoffset}'
commands.append([
'git', 'filter-branch', '-f',
'--env-filter', f"GIT_AUTHOR_DATE='{date}';GIT_COMMITTER_DATE='{date}'",
f'HEAD...HEAD~1',
])
for command in commands:
self.commands.append(util.ShellArg(command=command, logname='stdio', haltOnFailure=True))
return super(Canonicalize, self).run()
def getResultSummary(self):
commit_pluralized = "commit" if self.rebase_enabled else "commits"
if self.results == SUCCESS:
return {'step': f'Canonicalized {commit_pluralized}'}
if self.results == FAILURE:
return {'step': f'Failed to canonicalize {commit_pluralized}'}
return super(Canonicalize, self).getResultSummary()
class PushPullRequestBranch(shell.ShellCommand):
name = 'push-pull-request-branch'
haltOnFailure = True
def __init__(self, **kwargs):
super(PushPullRequestBranch, self).__init__(logEnviron=False, timeout=300, **kwargs)
def start(self, BufferLogObserverClass=logobserver.BufferLogObserver):
remote = self.getProperty('github.head.repo.full_name').split('/')[0]
head_ref = self.getProperty('github.head.ref')
self.command = ['git', 'push', '-f', remote, f'HEAD:{head_ref}']
username, access_token = GitHub.credentials(user=GitHub.user_for_queue(self.getProperty('buildername', '')))
self.workerEnvironment['GIT_USER'] = username
self.workerEnvironment['GIT_PASSWORD'] = access_token
return super(PushPullRequestBranch, self).start()
def getResultSummary(self):
if self.results == SUCCESS:
return {'step': 'Pushed to pull request branch'}
if self.results == FAILURE:
return {'step': 'Failed to push to pull request branch'}
return super(PushPullRequestBranch, self).getResultSummary()
def doStepIf(self, step):
return CURRENT_HOSTNAME == EWS_BUILD_HOSTNAME and self.getProperty('github.number') and self.getProperty('github.head.ref') and self.getProperty('github.head.repo.full_name')
def hideStepIf(self, results, step):
return not self.doStepIf(step)
class UpdatePullRequest(shell.ShellCommand, GitHubMixin, AddToLogMixin):
name = 'update-pull-request'
haltOnFailure = False
command = ['git', 'log', '-1', '--no-decorate']
ESCAPE_TABLE = {
'"': '&quot;',
"'": '&apos;',
'>': '&gt;',
'<': '&lt;',
'&': '&amp;',
}
BUGS_RE = [
re.compile(r'\Awebkit.org/b/(?P<id>\d+)\Z'),
re.compile(r'\Ahttps?://webkit.org/b/(?P<id>\d+)\Z'),
re.compile(r'\Awebkit.org/b/(?P<id>\d+)\Z'),
re.compile(r'\Abugs.webkit.org/show_bug.cgi\?id=(?P<id>\d+)\Z'),
re.compile(r'\Ahttps?://bugs.webkit.org/show_bug.cgi\?id=(?P<id>\d+)\Z'),
]
@classmethod
def escape_html(cls, message):
message = ''.join(cls.ESCAPE_TABLE.get(c, c) for c in message)
return re.sub(r'(https?://[^\s<>,:;]+?)(?=[\s<>,:;]|(&gt))', r'<a href="\1">\1</a>', message)
def __init__(self, **kwargs):
super(UpdatePullRequest, self).__init__(logEnviron=False, timeout=300, **kwargs)
def start(self, BufferLogObserverClass=logobserver.BufferLogObserver):
self.log_observer = BufferLogObserverClass(wantStderr=True)
self.addLogObserver('stdio', self.log_observer)
return super(UpdatePullRequest, self).start()
def getResultSummary(self):
if self.results == SUCCESS:
return {'step': 'Updated pull request'}
if self.results == FAILURE:
return {'step': 'Failed to update pull request'}
return super(UpdatePullRequest, self).getResultSummary()
@classmethod
def bug_id_from_log(cls, lines):
for line in lines:
for word in line.split():
for candidate in cls.BUGS_RE:
match = candidate.match(word)
if match:
return match.group('id')
return None
@classmethod
def is_test_gardening(cls, lines):
for line in lines:
if line.lstrip().startswith('Unreviewed test gardening'):
return True
return False
def evaluateCommand(self, cmd):
rc = super(UpdatePullRequest, self).evaluateCommand(cmd)
loglines = self.log_observer.getStdout().splitlines()
title = loglines[4][4:].rstrip()
description = f'#### {loglines[0].split()[1]}\n<pre>\n'
for line in loglines[4:]:
description += self.escape_html(line[4:] + '\n')
description += '</pre>\n'
bug_id = self.bug_id_from_log(loglines)
if bug_id:
self.setProperty('bug_id', bug_id)
self.setProperty('is_test_gardening', self.is_test_gardening(loglines))
user = self.getProperty('github.head.user.login', '')
head = self.getProperty('github.head.ref', '')
if not self.update_pr(
self.getProperty('github.number'),
title=title,
description=description,
base=self.getProperty('github.base.ref', ''),
head=f"{user}:{head}" if user and head else None,
repository_url=self.getProperty('repository', ''),
):
return FAILURE
return rc
def doStepIf(self, step):
return CURRENT_HOSTNAME == EWS_BUILD_HOSTNAME and self.getProperty('github.number')
def hideStepIf(self, results, step):
return not self.doStepIf(step)