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