Move more logic from handler classes to model classes and add unit tests
[WebKit-https.git] / Websites / webkit-perf.appspot.com / models.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 hashlib
31 import json
32 import re
33
34 from datetime import datetime
35 from google.appengine.ext import db
36 from google.appengine.api import memcache
37 from time import mktime
38
39
40 class NumericIdHolder(db.Model):
41     owner = db.ReferenceProperty()
42     # Dummy class whose sole purpose is to generate key().id()
43
44
45 def create_in_transaction_with_numeric_id_holder(callback):
46     id_holder = NumericIdHolder()
47     id_holder.put()
48     id_holder = NumericIdHolder.get(id_holder.key())
49     owner = None
50     try:
51         owner = db.run_in_transaction(callback, id_holder.key().id())
52         if owner:
53             id_holder.owner = owner
54             id_holder.put()
55     finally:
56         if not owner:
57             id_holder.delete()
58     return owner
59
60
61 def delete_model_with_numeric_id_holder(model):
62     id_holder = NumericIdHolder.get_by_id(model.id)
63     model.delete()
64     id_holder.delete()
65
66
67 def model_from_numeric_id(id, expected_kind):
68     id_holder = NumericIdHolder.get_by_id(id)
69     return id_holder.owner if id_holder and id_holder.owner and isinstance(id_holder.owner, expected_kind) else None
70
71
72 def _create_if_possible(model, key, name):
73
74     def execute(id):
75         if model.get_by_key_name(key):
76             return None
77         branch = model(id=id, name=name, key_name=key)
78         branch.put()
79         return branch
80
81     return create_in_transaction_with_numeric_id_holder(execute)
82
83
84 class Branch(db.Model):
85     id = db.IntegerProperty(required=True)
86     name = db.StringProperty(required=True)
87
88     @staticmethod
89     def create_if_possible(key, name):
90         return _create_if_possible(Branch, key, name)
91
92
93 class Platform(db.Model):
94     id = db.IntegerProperty(required=True)
95     name = db.StringProperty(required=True)
96
97     @staticmethod
98     def create_if_possible(key, name):
99         return _create_if_possible(Platform, key, name)
100
101
102 class Builder(db.Model):
103     name = db.StringProperty(required=True)
104     password = db.StringProperty(required=True)
105
106     @staticmethod
107     def create(name, raw_password):
108         return Builder(name=name, password=Builder._hashed_password(raw_password), key_name=name).put()
109
110     def update_password(self, raw_password):
111         self.password = Builder._hashed_password(raw_password)
112         self.put()
113
114     def authenticate(self, raw_password):
115         return self.password == hashlib.sha256(raw_password).hexdigest()
116
117     @staticmethod
118     def _hashed_password(raw_password):
119         return hashlib.sha256(raw_password).hexdigest()
120
121
122 class Build(db.Model):
123     branch = db.ReferenceProperty(Branch, required=True, collection_name='build_branch')
124     platform = db.ReferenceProperty(Platform, required=True, collection_name='build_platform')
125     builder = db.ReferenceProperty(Builder, required=True, collection_name='builder_key')
126     buildNumber = db.IntegerProperty(required=True)
127     revision = db.IntegerProperty(required=True)
128     chromiumRevision = db.IntegerProperty()
129     timestamp = db.DateTimeProperty(required=True)
130
131     @staticmethod
132     def get_or_insert_from_log(log):
133         builder = log.builder()
134         key_name = builder.name + ':' + str(int(mktime(log.timestamp().timetuple())))
135
136         return Build.get_or_insert(key_name, branch=log.branch(), platform=log.platform(), builder=builder,
137             buildNumber=log.build_number(), timestamp=log.timestamp(),
138             revision=log.webkit_revision(), chromiumRevision=log.chromium_revision())
139
140
141 # Used to generate TestMap in the manifest efficiently
142 class Test(db.Model):
143     id = db.IntegerProperty(required=True)
144     name = db.StringProperty(required=True)
145     branches = db.ListProperty(db.Key)
146     platforms = db.ListProperty(db.Key)
147
148     @staticmethod
149     def cache_key(test_id, branch_id, platform_id):
150         return 'runs:%d,%d,%d' % (test_id, branch_id, platform_id)
151
152     @staticmethod
153     def update_or_insert(test_name, branch, platform):
154         existing_test = [None]
155
156         def execute(id):
157             test = Test.get_by_key_name(test_name)
158             if test:
159                 if branch.key() not in test.branches:
160                     test.branches.append(branch.key())
161                 if platform.key() not in test.platforms:
162                     test.platforms.append(platform.key())
163                 existing_test[0] = test
164                 return None
165
166             test = Test(id=id, name=test_name, key_name=test_name, branches=[branch.key()], platforms=[platform.key()])
167             test.put()
168             return test
169
170         return create_in_transaction_with_numeric_id_holder(execute) or existing_test[0]
171
172
173 class TestResult(db.Model):
174     name = db.StringProperty(required=True)
175     build = db.ReferenceProperty(Build, required=True)
176     value = db.FloatProperty(required=True)
177     valueMedian = db.FloatProperty()
178     valueStdev = db.FloatProperty()
179     valueMin = db.FloatProperty()
180     valueMax = db.FloatProperty()
181
182     @staticmethod
183     def key_name(build, test_name):
184         return build.key().name() + ':' + test_name
185
186     @classmethod
187     def get_or_insert_from_parsed_json(cls, test_name, build, result):
188         key_name = cls.key_name(build, test_name)
189
190         def _float_or_none(dictionary, key):
191             value = dictionary.get(key)
192             if value:
193                 return float(value)
194             return None
195
196         if not isinstance(result, dict):
197             return cls.get_or_insert(key_name, name=test_name, build=build, value=float(result))
198
199         return cls.get_or_insert(key_name, name=test_name, build=build, value=float(result['avg']),
200             valueMedian=_float_or_none(result, 'median'), valueStdev=_float_or_none(result, 'stdev'),
201             valueMin=_float_or_none(result, 'min'), valueMax=_float_or_none(result, 'max'))
202
203     @staticmethod
204     def generate_runs(branch, platform, test_name):
205         builds = Build.all()
206         builds.filter('branch =', branch)
207         builds.filter('platform =', platform)
208
209         for build in builds:
210             results = TestResult.all()
211             results.filter('name =', test_name)
212             results.filter('build =', build)
213             for result in results:
214                 yield build, result
215         raise StopIteration
216
217
218 class ReportLog(db.Model):
219     timestamp = db.DateTimeProperty(required=True)
220     headers = db.TextProperty()
221     payload = db.TextProperty()
222     commit = db.BooleanProperty()
223
224     def _parsed_payload(self):
225         if self.__dict__.get('_parsed') == None:
226             try:
227                 self._parsed = json.loads(self.payload)
228             except ValueError:
229                 self._parsed = False
230         return self._parsed
231
232     def get_value(self, keyName):
233         if not self._parsed_payload():
234             return None
235         return self._parsed.get(keyName)
236
237     def results(self):
238         return self.get_value('results')
239
240     def builder(self):
241         return self._model_by_key_name_in_payload(Builder, 'builder-name')
242
243     def branch(self):
244         return self._model_by_key_name_in_payload(Branch, 'branch')
245
246     def platform(self):
247         return self._model_by_key_name_in_payload(Platform, 'platform')
248
249     def build_number(self):
250         return self._integer_in_payload('build-number')
251
252     def webkit_revision(self):
253         return self._integer_in_payload('webkit-revision')
254
255     def chromium_revision(self):
256         return self._integer_in_payload('chromium-revision')
257
258     def _model_by_key_name_in_payload(self, model, keyName):
259         key = self.get_value(keyName)
260         if not key:
261             return None
262         return model.get_by_key_name(key)
263
264     def _integer_in_payload(self, keyName):
265         try:
266             return int(self.get_value(keyName))
267         except TypeError:
268             return None
269         except ValueError:
270             return None
271
272     # FIXME: We also have timestamp as a member variable.
273     def timestamp(self):
274         try:
275             return datetime.fromtimestamp(self._integer_in_payload('timestamp'))
276         except TypeError:
277             return None
278         except ValueError:
279             return None
280
281
282 class PersistentCache(db.Model):
283     value = db.TextProperty(required=True)
284
285     @staticmethod
286     def set_cache(name, value):
287         memcache.set(name, value)
288
289         def execute():
290             cache = PersistentCache.get_by_key_name(name)
291             if cache:
292                 cache.value = value
293                 cache.put()
294             else:
295                 PersistentCache(key_name=name, value=value).put()
296
297         db.run_in_transaction(execute)
298
299     @staticmethod
300     def get_cache(name):
301         value = memcache.get(name)
302         if value:
303             return value
304         cache = PersistentCache.get_by_key_name(name)
305         if not cache:
306             return None
307         memcache.set(name, cache.value)
308         return cache.value