1b4969144aaf6aad0c71770226c684790cb04427
[WebKit-https.git] / Tools / Scripts / webkitpy / results / upload.py
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     def __init__(self, suite=None, configuration=None, commits=[], timestamp=None, details=None, run_stats=None, results={}):
99         self.suite = suite
100         self.configuration = configuration
101         self.commits = commits
102         self.timestamp = timestamp
103         self.details = details
104         self.run_stats = run_stats
105         self.results = results
106
107     @staticmethod
108     def create_configuration(
109             platform=None,
110             is_simulator=False,
111             version=None,
112             architecture=None,
113             version_name=None,
114             model=None,
115             style=None,   # Debug/Production/Release
116             flavor=None,  # Dumping ground suite-wide configuration changes (ie, GuardMalloc)
117             sdk=None,
118         ):
119
120         # This deviates slightly from the rest of webkitpy, but it allows this file to be entirely portable.
121         config = dict(
122             platform=platform or (host_platform.system() if host_platform.system() != 'Darwin' else 'mac').lower(),
123             is_simulator=is_simulator,
124             version=version or (host_platform.release() if host_platform.system() != 'Darwin' else host_platform.mac_ver()[0]),
125             architecture=architecture or host_platform.machine(),
126         )
127         optional_data = dict(version_name=version_name, model=model, style=style, flavor=flavor, sdk=sdk)
128         config.update({key: value for key, value in optional_data.iteritems() if value is not None})
129         return config
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