77c281c1f8bce9af03f9f642c0ac83dee95edf24
[WebKit-https.git] / Websites / webkit-perf.appspot.com / report_handler.py
1 #!/usr/bin/env python
2 # Copyright (C) 2012 Google Inc. All rights reserved.
3 #
4 # Redistribution and use in source and binary forms, with or without
5 # modification, are permitted provided that the following conditions are
6 # met:
7 #
8 #     * Redistributions of source code must retain the above copyright
9 # notice, this list of conditions and the following disclaimer.
10 #     * Redistributions in binary form must reproduce the above
11 # copyright notice, this list of conditions and the following disclaimer
12 # in the documentation and/or other materials provided with the
13 # distribution.
14 #     * Neither the name of Google Inc. nor the names of its
15 # contributors may be used to endorse or promote products derived from
16 # this software without specific prior written permission.
17 #
18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
30 import webapp2
31 from google.appengine.ext import db
32
33 import json
34 import re
35 import time
36 from datetime import datetime
37
38 from controller import schedule_runs_update
39 from controller import schedule_dashboard_update
40 from controller import schedule_manifest_update
41 from models import Builder
42 from models import Branch
43 from models import Build
44 from models import NumericIdHolder
45 from models import Platform
46 from models import ReportLog
47 from models import Test
48 from models import TestResult
49 from models import create_in_transaction_with_numeric_id_holder
50
51
52 class ReportHandler(webapp2.RequestHandler):
53     def post(self):
54         self.response.headers['Content-Type'] = 'text/plain; charset=utf-8'
55
56         headers = "\n".join([key + ': ' + value for key, value in self.request.headers.items()])
57
58         # Do as best as we can to remove the password
59         request_body_without_password = re.sub(r'"password"\s*:\s*".+?",', '', self.request.body)
60         log = ReportLog(timestamp=datetime.now(), headers=headers, payload=request_body_without_password)
61         log.put()
62
63         try:
64             self._body = json.loads(self.request.body)
65         except ValueError:
66             return self._output('Failed to parse the payload as a json. Report key: %d' % log.key().id())
67
68         builder = self._model_by_key_name_in_body_or_error(Builder, 'builder-name')
69         branch = self._model_by_key_name_in_body_or_error(Branch, 'branch')
70         platform = self._model_by_key_name_in_body_or_error(Platform, 'platform')
71         build_number = self._integer_in_body('build-number')
72         timestamp = self._timestamp_in_body()
73         revision = self._integer_in_body('webkit-revision')
74         chromium_revision = self._integer_in_body('webkit-revision') if 'chromium-revision' in self._body else None
75
76         failed = False
77         if builder and not (self.bypass_authentication() or builder.authenticate(self._body.get('password', ''))):
78             self._output('Authentication failed')
79             failed = True
80
81         if not self._results_are_valid():
82             self._output("The payload doesn't contain results or results are malformed")
83             failed = True
84
85         if not (builder and branch and platform and build_number and revision and timestamp) or failed:
86             return
87
88         build = self._create_build_if_possible(builder, build_number, branch, platform, timestamp, revision, chromium_revision)
89         if not build:
90             return
91
92         def _float_or_none(dictionary, key):
93             value = dictionary.get(key)
94             if value:
95                 return float(value)
96             return None
97
98         for test_name, result in self._body['results'].iteritems():
99             test = self._add_test_if_needed(test_name, branch, platform)
100             schedule_runs_update(test.id, branch.id, platform.id)
101             if isinstance(result, dict):
102                 TestResult(name=test_name, build=build, value=float(result['avg']), valueMedian=_float_or_none(result, 'median'),
103                     valueStdev=_float_or_none(result, 'stdev'), valueMin=_float_or_none(result, 'min'), valueMax=_float_or_none(result, 'max')).put()
104             else:
105                 TestResult(name=test_name, build=build, value=float(result)).put()
106
107         log = ReportLog.get(log.key())
108         log.delete()
109
110         # We need to update dashboard and manifest because they are affected by the existance of test results
111         schedule_dashboard_update()
112         schedule_manifest_update()
113
114         return self._output('OK')
115
116     def _model_by_key_name_in_body_or_error(self, model, keyName):
117         key = self._body.get(keyName, '')
118         instance = key and model.get_by_key_name(key)
119         if not instance:
120             self._output('There are no %s named "%s"' % (model.__name__.lower(), key))
121         return instance
122
123     def _integer_in_body(self, key):
124         value = self._body.get(key, '')
125         try:
126             return int(value)
127         except:
128             return self._output('Invalid %s: "%s"' % (key.replace('-', ' '), value))
129
130     def _timestamp_in_body(self):
131         value = self._body.get('timestamp', '')
132         try:
133             return datetime.fromtimestamp(int(value))
134         except:
135             return self._output('Failed to parse the timestamp: %s' % value)
136
137     def _output(self, message):
138         self.response.out.write(message + '\n')
139
140     def bypass_authentication(self):
141         return False
142
143     def _results_are_valid(self):
144
145         def _is_float_convertible(value):
146             try:
147                 float(value)
148                 return True
149             except TypeError:
150                 return False
151
152         if 'results' not in self._body or not isinstance(self._body['results'], dict):
153             return False
154
155         for testResult in self._body['results'].values():
156             if isinstance(testResult, dict):
157                 for value in testResult.values():
158                     if not _is_float_convertible(value):
159                         return False
160                 if 'avg' not in testResult:
161                     return False
162                 continue
163             if not _is_float_convertible(testResult):
164                 return False
165
166         return True
167
168     def _create_build_if_possible(self, builder, build_number, branch, platform, timestamp, revision, chromium_revision):
169         key_name = builder.name + ':' + str(int(time.mktime(timestamp.timetuple())))
170
171         def execute():
172             build = Build.get_by_key_name(key_name)
173             if build:
174                 return self._output('The build at %s already exists for %s' % (str(timestamp), builder.name))
175
176             return Build(branch=branch, platform=platform, builder=builder, buildNumber=build_number,
177                 timestamp=timestamp, revision=revision, chromiumRevision=chromium_revision, key_name=key_name).put()
178         return db.run_in_transaction(execute)
179
180     def _add_test_if_needed(self, test_name, branch, platform):
181
182         def execute(id):
183             test = Test.get_by_key_name(test_name)
184             returnValue = None
185             if not test:
186                 test = Test(id=id, name=test_name, key_name=test_name)
187                 returnValue = test
188             if branch.key() not in test.branches:
189                 test.branches.append(branch.key())
190             if platform.key() not in test.platforms:
191                 test.platforms.append(platform.key())
192             test.put()
193             return returnValue
194         return create_in_transaction_with_numeric_id_holder(execute) or Test.get_by_key_name(test_name)
195
196
197 class AdminReportHandler(ReportHandler):
198     def bypass_authentication(self):
199         return True