|
Line 0
Tools/Scripts/webkitpy/results/upload.py_sec1
|
|
|
1 |
# Copyright (C) 2019 Apple Inc. All rights reserved. |
| 2 |
# |
| 3 |
# Redistribution and use in source and binary forms, with or without |
| 4 |
# modification, are permitted provided that the following conditions |
| 5 |
# are met: |
| 6 |
# 1. Redistributions of source code must retain the above copyright |
| 7 |
# notice, this list of conditions and the following disclaimer. |
| 8 |
# 2. Redistributions in binary form must reproduce the above copyright |
| 9 |
# notice, this list of conditions and the following disclaimer in the |
| 10 |
# documentation and/or other materials provided with the distribution. |
| 11 |
# |
| 12 |
# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND |
| 13 |
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
| 14 |
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| 15 |
# DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR |
| 16 |
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL |
| 17 |
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR |
| 18 |
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
| 19 |
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, |
| 20 |
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| 21 |
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| 22 |
|
| 23 |
import webkitpy.thirdparty.autoinstalled.requests |
| 24 |
|
| 25 |
import json |
| 26 |
import requests |
| 27 |
import sys |
| 28 |
|
| 29 |
import platform as host_platform |
| 30 |
|
| 31 |
|
| 32 |
class Upload(object): |
| 33 |
UPLOAD_ENDPOINT = '/api/upload' |
| 34 |
BUILDBOT_DETAILS = ['buildbot-master', 'builder-name', 'build-number', 'buildbot-worker'] |
| 35 |
VERSION = 0 |
| 36 |
|
| 37 |
class Expectations: |
| 38 |
# These are ordered by priority, meaning that a test which both crashes and has |
| 39 |
# a warning should be considered to have crashed. |
| 40 |
ORDER = [ |
| 41 |
'CRASH', |
| 42 |
'TIMEOUT', |
| 43 |
'IMAGE', # Image-diff |
| 44 |
'AUDIO', # Audio-diff |
| 45 |
'TEXT', # Text-diff |
| 46 |
'FAIL', |
| 47 |
'ERROR', |
| 48 |
'WARNING', |
| 49 |
'PASS', |
| 50 |
] |
| 51 |
CRASH, TIMEOUT, IMAGE, AUDIO, TEXT, FAIL, ERROR, WARNING, PASS = ORDER |
| 52 |
|
| 53 |
class Encoder(json.JSONEncoder): |
| 54 |
|
| 55 |
def default(self, obj): |
| 56 |
if not isinstance(obj, Upload): |
| 57 |
return super(Upload.Encoder, self).default(obj) |
| 58 |
|
| 59 |
if not obj.suite: |
| 60 |
raise ValueError('No suite specified to results upload') |
| 61 |
if not obj.commits: |
| 62 |
raise ValueError('No commits specified to results upload') |
| 63 |
|
| 64 |
details = obj.details or obj.create_details() |
| 65 |
buildbot_args = [details.get(arg, None) is None for arg in obj.BUILDBOT_DETAILS] |
| 66 |
if any(buildbot_args) and not all(buildbot_args): |
| 67 |
raise ValueError('All buildbot details must be defined for upload, details missing: {}'.format(', '.join( |
| 68 |
[obj.BUILDBOT_DETAILS[i] for i in xrange(len(obj.BUILDBOT_DETAILS)) if buildbot_args[i]], |
| 69 |
))) |
| 70 |
|
| 71 |
def unpack_test(current, path_to_test, data): |
| 72 |
if len(path_to_test) == 1: |
| 73 |
current[path_to_test[0]] = data |
| 74 |
return |
| 75 |
if not current.get(path_to_test[0]): |
| 76 |
current[path_to_test[0]] = {} |
| 77 |
unpack_test(current[path_to_test[0]], path_to_test[1:], data) |
| 78 |
|
| 79 |
results = {} |
| 80 |
for test, data in obj.results.iteritems(): |
| 81 |
unpack_test(results, test.split('/'), data) |
| 82 |
|
| 83 |
result = dict( |
| 84 |
version=obj.VERSION, |
| 85 |
suite=obj.suite, |
| 86 |
configuration=obj.configuration or obj.create_configuration(), |
| 87 |
commits=obj.commits, |
| 88 |
test_results=dict( |
| 89 |
details=details, |
| 90 |
run_stats=obj.run_stats or obj.create_run_stats(), |
| 91 |
results=results, |
| 92 |
), |
| 93 |
) |
| 94 |
if obj.timestamp: |
| 95 |
result['timestamp'] = obj.timestamp |
| 96 |
return result |
| 97 |
|
| 98 |
@staticmethod |
| 99 |
def create_configuration( |
| 100 |
platform=None, |
| 101 |
is_simulator=False, |
| 102 |
version=None, |
| 103 |
architecture=None, |
| 104 |
version_name=None, |
| 105 |
model=None, |
| 106 |
style=None, # Debug/Production/Release |
| 107 |
flavor=None, # Dumping ground suite-wide configuration changes (ie, GuardMalloc) |
| 108 |
sdk=None, |
| 109 |
): |
| 110 |
|
| 111 |
# This deviates slightly from the rest of webkitpy, but it allows this file to be entirely portable. |
| 112 |
config = dict( |
| 113 |
platform=platform or (host_platform.system() if host_platform.system() != 'Darwin' else 'mac').lower(), |
| 114 |
is_simulator=is_simulator, |
| 115 |
version=version or (host_platform.release() if host_platform.system() != 'Darwin' else host_platform.mac_ver()[0]), |
| 116 |
architecture=architecture or host_platform.machine(), |
| 117 |
) |
| 118 |
optional_data = dict(version_name=version_name, model=model, style=style, flavor=flavor, sdk=sdk) |
| 119 |
config.update({key: value for key, value in optional_data.iteritems() if value is not None}) |
| 120 |
return config |
| 121 |
|
| 122 |
def __init__(self, suite=None, configuration=None, commits=[], timestamp=None, details=None, run_stats=None, results={}): |
| 123 |
self.suite = suite |
| 124 |
self.configuration = configuration |
| 125 |
self.commits = commits |
| 126 |
self.timestamp = timestamp |
| 127 |
self.details = details |
| 128 |
self.run_stats = run_stats |
| 129 |
self.results = results |
| 130 |
|
| 131 |
@staticmethod |
| 132 |
def create_commit(repository_id, id, branch=None): |
| 133 |
commit = dict(repository_id=repository_id, id=id) |
| 134 |
if branch: |
| 135 |
commit['branch'] = branch |
| 136 |
return commit |
| 137 |
|
| 138 |
@staticmethod |
| 139 |
def create_details(link=None, options=None, **kwargs): |
| 140 |
result = dict(**kwargs) |
| 141 |
if link: |
| 142 |
result['link'] = link |
| 143 |
if not options: |
| 144 |
return result |
| 145 |
|
| 146 |
for element in Upload.BUILDBOT_DETAILS: |
| 147 |
value = getattr(options, element.replace('-', '_'), None) |
| 148 |
if value is not None: |
| 149 |
result[element] = value |
| 150 |
return result |
| 151 |
|
| 152 |
@staticmethod |
| 153 |
def create_run_stats(start_time=None, end_time=None, tests_skipped=None, **kwargs): |
| 154 |
stats = dict(**kwargs) |
| 155 |
optional_data = dict(start_time=start_time, end_time=end_time, tests_skipped=tests_skipped) |
| 156 |
stats.update({key: value for key, value in optional_data.iteritems() if value is not None}) |
| 157 |
return stats |
| 158 |
|
| 159 |
@staticmethod |
| 160 |
def create_test_result(expected=None, actual=None, log=None, **kwargs): |
| 161 |
result = dict(**kwargs) |
| 162 |
|
| 163 |
# Tests which don't declare expectations or results are assumed to have passed. |
| 164 |
optional_data = dict(expected=expected, actual=actual, log=log) |
| 165 |
result.update({key: value for key, value in optional_data.iteritems() if value is not None}) |
| 166 |
return result |
| 167 |
|
| 168 |
def upload(self, url, log_line_func=lambda val: sys.stdout.write(val + '\n')): |
| 169 |
try: |
| 170 |
response = requests.post(url + self.UPLOAD_ENDPOINT, data=json.dumps(self, cls=Upload.Encoder)) |
| 171 |
except requests.exceptions.ConnectionError: |
| 172 |
log_line_func(' ' * 4 + 'Failed to upload to {}, results server not online'.format(url)) |
| 173 |
return False |
| 174 |
except ValueError as e: |
| 175 |
log_line_func(' ' * 4 + 'Failed to encode upload data: {}'.format(e)) |
| 176 |
return False |
| 177 |
|
| 178 |
if response.status_code != 200: |
| 179 |
log_line_func(' ' * 4 + 'Error uploading to {}:'.format(url)) |
| 180 |
log_line_func(' ' * 8 + response.json()['description']) |
| 181 |
return False |
| 182 |
|
| 183 |
log_line_func(' ' * 4 + 'Uploaded results to {}'.format(url)) |
| 184 |
return True |