webkit-perf.appspot.com should accept test results without medians
[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.api import memcache
32 from google.appengine.ext import db
33
34 import json
35 import re
36 import time
37 from datetime import datetime
38
39 from models import Builder
40 from models import Branch
41 from models import Build
42 from models import NumericIdHolder
43 from models import Platform
44 from models import ReportLog
45 from models import Test
46 from models import TestResult
47 from models import create_in_transaction_with_numeric_id_holder
48
49
50 class ReportHandler(webapp2.RequestHandler):
51     def post(self):
52         self.response.headers['Content-Type'] = 'text/plain; charset=utf-8'
53
54         headers = "\n".join([key + ': ' + value for key, value in self.request.headers.items()])
55
56         # Do as best as we can to remove the password
57         request_body_without_password = re.sub(r'"password"\s*:\s*".+?",', '', self.request.body)
58         log = ReportLog(timestamp=datetime.now(), headers=headers, payload=request_body_without_password)
59         log.put()
60
61         try:
62             self._body = json.loads(self.request.body)
63         except ValueError:
64             return self._output('Failed to parse the payload as a json. Report key: %d' % log.key().id())
65
66         builder = self._model_by_key_name_in_body_or_error(Builder, 'builder-name')
67         branch = self._model_by_key_name_in_body_or_error(Branch, 'branch')
68         platform = self._model_by_key_name_in_body_or_error(Platform, 'platform')
69         build_number = self._integer_in_body('build-number')
70         revision = self._integer_in_body('revision')
71         timestamp = self._timestamp_in_body()
72
73         failed = False
74         if builder and not (self.bypass_authentication() or builder.authenticate(self._body.get('password', ''))):
75             self._output('Authentication failed')
76             failed = True
77
78         if not self._results_are_valid():
79             self._output("The payload doesn't contain results or results are malformed")
80             failed = True
81
82         if not (builder and branch and platform and build_number and revision and timestamp) or failed:
83             return
84
85         build = self._create_build_if_possible(builder, build_number, branch, platform, revision, timestamp)
86         if not build:
87             return
88
89         def _float_or_none(dictionary, key):
90             value = dictionary.get(key)
91             if value:
92                 return float(value)
93             return None
94
95         for test_name, result in self._body['results'].iteritems():
96             test = self._add_test_if_needed(test_name, branch, platform)
97             memcache.delete(Test.cache_key(test.id, branch.id, platform.id))
98             if isinstance(result, dict):
99                 TestResult(name=test_name, build=build, value=float(result['avg']), valueMedian=_float_or_none(result, 'median'),
100                     valueStdev=_float_or_none(result, 'stdev'), valueMin=_float_or_none(result, 'min'), valueMax=_float_or_none(result, 'max')).put()
101             else:
102                 TestResult(name=test_name, build=build, value=float(result)).put()
103
104         log = ReportLog.get(log.key())
105         log.delete()
106
107         # We need to update dashboard and manifest because they are affected by the existance of test results
108         memcache.delete('dashboard')
109         memcache.delete('manifest')
110
111         return self._output('OK')
112
113     def _model_by_key_name_in_body_or_error(self, model, keyName):
114         key = self._body.get(keyName, '')
115         instance = key and model.get_by_key_name(key)
116         if not instance:
117             self._output('There are no %s named "%s"' % (model.__name__.lower(), key))
118         return instance
119
120     def _integer_in_body(self, key):
121         value = self._body.get(key, '')
122         try:
123             return int(value)
124         except:
125             return self._output('Invalid %s: "%s"' % (key.replace('-', ' '), value))
126
127     def _timestamp_in_body(self):
128         value = self._body.get('timestamp', '')
129         try:
130             return datetime.fromtimestamp(int(value))
131         except:
132             return self._output('Failed to parse the timestamp: %s' % value)
133
134     def _output(self, message):
135         self.response.out.write(message + '\n')
136
137     def bypass_authentication(self):
138         return False
139
140     def _results_are_valid(self):
141
142         def _is_float_convertible(value):
143             try:
144                 float(value)
145                 return True
146             except TypeError:
147                 return False
148
149         if 'results' not in self._body or not isinstance(self._body['results'], dict):
150             return False
151
152         for testResult in self._body['results'].values():
153             if isinstance(testResult, dict):
154                 for value in testResult.values():
155                     if not _is_float_convertible(value):
156                         return False
157                 if 'avg' not in testResult:
158                     return False
159                 continue
160             if not _is_float_convertible(testResult):
161                 return False
162
163         return True
164
165     def _create_build_if_possible(self, builder, build_number, branch, platform, revision, timestamp):
166         key_name = builder.name + ':' + str(int(time.mktime(timestamp.timetuple())))
167
168         def execute():
169             build = Build.get_by_key_name(key_name)
170             if build:
171                 return self._output('The build at %s already exists for %s' % (str(timestamp), builder.name))
172
173             return Build(branch=branch, platform=platform, builder=builder, buildNumber=build_number,
174                 timestamp=timestamp, revision=revision, key_name=key_name).put()
175         return db.run_in_transaction(execute)
176
177     def _add_test_if_needed(self, test_name, branch, platform):
178
179         def execute(id):
180             test = Test.get_by_key_name(test_name)
181             returnValue = None
182             if not test:
183                 test = Test(id=id, name=test_name, key_name=test_name)
184                 returnValue = test
185             if branch.key() not in test.branches:
186                 test.branches.append(branch.key())
187             if platform.key() not in test.platforms:
188                 test.platforms.append(platform.key())
189             test.put()
190             return returnValue
191         return create_in_transaction_with_numeric_id_holder(execute) or Test.get_by_key_name(test_name)
192
193
194 class AdminReportHandler(ReportHandler):
195     def bypass_authentication(self):
196         return True