39e1fa72bab33ddaa5373b1a74e182b8ea670d18
[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         for test_name, result in self._body['results'].iteritems():
90             test = self._add_test_if_needed(test_name, branch, platform)
91             memcache.delete(Test.cache_key(test.id, branch.id, platform.id))
92             if isinstance(result, dict):
93                 TestResult(name=test_name, build=build, value=float(result.get('avg', 0)), valueMedian=float(result.get('median', 0)),
94                     valueStdev=float(result.get('stdev', 0)), valueMin=float(result.get('min', 0)), valueMax=float(result.get('max', 0))).put()
95             else:
96                 TestResult(name=test_name, build=build, value=float(result)).put()
97
98         log = ReportLog.get(log.key())
99         log.delete()
100
101         # We need to update dashboard and manifest because they are affected by the existance of test results
102         memcache.delete('dashboard')
103         memcache.delete('manifest')
104
105         return self._output('OK')
106
107     def _model_by_key_name_in_body_or_error(self, model, keyName):
108         key = self._body.get(keyName, '')
109         instance = key and model.get_by_key_name(key)
110         if not instance:
111             self._output('There are no %s named "%s"' % (model.__name__.lower(), key))
112         return instance
113
114     def _integer_in_body(self, key):
115         value = self._body.get(key, '')
116         try:
117             return int(value)
118         except:
119             return self._output('Invalid %s: "%s"' % (key.replace('-', ' '), value))
120
121     def _timestamp_in_body(self):
122         value = self._body.get('timestamp', '')
123         try:
124             return datetime.fromtimestamp(int(value))
125         except:
126             return self._output('Failed to parse the timestamp: %s' % value)
127
128     def _output(self, message):
129         self.response.out.write(message + '\n')
130
131     def bypass_authentication(self):
132         return False
133
134     def _results_are_valid(self):
135
136         def _is_float_convertible(value):
137             try:
138                 float(value)
139                 return True
140             except TypeError:
141                 return False
142
143         if 'results' not in self._body or not isinstance(self._body['results'], dict):
144             return False
145
146         for testResult in self._body['results'].values():
147             if isinstance(testResult, dict):
148                 for value in testResult.values():
149                     if not _is_float_convertible(value):
150                         return False
151                 if 'avg' not in testResult:
152                     return False
153                 continue
154             if not _is_float_convertible(testResult):
155                 return False
156
157         return True
158
159     def _create_build_if_possible(self, builder, build_number, branch, platform, revision, timestamp):
160         key_name = builder.name + ':' + str(int(time.mktime(timestamp.timetuple())))
161
162         def execute():
163             build = Build.get_by_key_name(key_name)
164             if build:
165                 return self._output('The build at %s already exists for %s' % (str(timestamp), builder.name))
166
167             return Build(branch=branch, platform=platform, builder=builder, buildNumber=build_number,
168                 timestamp=timestamp, revision=revision, key_name=key_name).put()
169         return db.run_in_transaction(execute)
170
171     def _add_test_if_needed(self, test_name, branch, platform):
172
173         def execute(id):
174             test = Test.get_by_key_name(test_name)
175             returnValue = None
176             if not test:
177                 test = Test(id=id, name=test_name, key_name=test_name)
178                 returnValue = test
179             if branch.key() not in test.branches:
180                 test.branches.append(branch.key())
181             if platform.key() not in test.platforms:
182                 test.platforms.append(platform.key())
183             test.put()
184             return returnValue
185         return create_in_transaction_with_numeric_id_holder(execute) or Test.get_by_key_name(test_name)
186
187
188 class AdminReportHandler(ReportHandler):
189     def bypass_authentication(self):
190         return True