d86fbcdecac5a55cc27dba5b2363dc56f7547c9b
[WebKit-https.git] / WebKitTools / TestResultServer / model / jsonresults.py
1 # Copyright (C) 2010 Google Inc. All rights reserved.
2 #
3 # Redistribution and use in source and binary forms, with or without
4 # modification, are permitted provided that the following conditions are
5 # met:
6 #
7 #     * Redistributions of source code must retain the above copyright
8 # notice, this list of conditions and the following disclaimer.
9 #     * Redistributions in binary form must reproduce the above
10 # copyright notice, this list of conditions and the following disclaimer
11 # in the documentation and/or other materials provided with the
12 # distribution.
13 #     * Neither the name of Google Inc. nor the names of its
14 # contributors may be used to endorse or promote products derived from
15 # this software without specific prior written permission.
16 #
17 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
29 from datetime import datetime
30 from django.utils import simplejson
31 import logging
32
33 from model.testfile import TestFile
34
35 JSON_RESULTS_FILE = "results.json"
36 JSON_RESULTS_PREFIX = "ADD_RESULTS("
37 JSON_RESULTS_SUFFIX = ");"
38 JSON_RESULTS_VERSION_KEY = "version"
39 JSON_RESULTS_BUILD_NUMBERS = "buildNumbers"
40 JSON_RESULTS_TESTS = "tests"
41 JSON_RESULTS_RESULTS = "results"
42 JSON_RESULTS_TIMES = "times"
43 JSON_RESULTS_VERSION = 3
44 JSON_RESULTS_MAX_BUILDS = 750
45
46
47 class JsonResults(object):
48     @classmethod
49     def _strip_prefix_suffix(cls, data):
50         """Strip out prefix and suffix of json results string.
51
52         Args:
53             data: json file content.
54
55         Returns:
56             json string without prefix and suffix.
57         """
58
59         assert(data.startswith(JSON_RESULTS_PREFIX))
60         assert(data.endswith(JSON_RESULTS_SUFFIX))
61
62         return data[len(JSON_RESULTS_PREFIX):
63                     len(data) - len(JSON_RESULTS_SUFFIX)]
64
65     @classmethod
66     def _generate_file_data(cls, json, sort_keys=False):
67         """Given json string, generate file content data by adding
68            prefix and suffix.
69
70         Args:
71             json: json string without prefix and suffix.
72
73         Returns:
74             json file data.
75         """
76
77         data = simplejson.dumps(json, separators=(',', ':'),
78             sort_keys=sort_keys)
79         return JSON_RESULTS_PREFIX + data + JSON_RESULTS_SUFFIX
80
81     @classmethod
82     def _load_json(cls, file_data):
83         """Load json file to a python object.
84
85         Args:
86             file_data: json file content.
87
88         Returns:
89             json object or
90             None on failure.
91         """
92
93         json_results_str = cls._strip_prefix_suffix(file_data)
94         if not json_results_str:
95             logging.warning("No json results data.")
96             return None
97
98         try:
99             return simplejson.loads(json_results_str)
100         except Exception, err:
101             logging.debug(json_results_str)
102             logging.error("Failed to load json results: %s", str(err))
103             return None
104
105     @classmethod
106     def _merge_json(cls, aggregated_json, incremental_json):
107         """Merge incremental json into aggregated json results.
108
109         Args:
110             aggregated_json: aggregated json object.
111             incremental_json: incremental json object.
112
113         Returns:
114             True if merge succeeds or
115             False on failure.
116         """
117
118         # Merge non tests property data.
119         # Tests properties are merged in _merge_tests.
120         if not cls._merge_non_test_data(aggregated_json, incremental_json):
121             return False
122
123         # Merge tests results and times
124         incremental_tests = incremental_json[JSON_RESULTS_TESTS]
125         if incremental_tests:
126             aggregated_tests = aggregated_json[JSON_RESULTS_TESTS]
127             cls._merge_tests(aggregated_tests, incremental_tests)
128
129         return True
130
131     @classmethod
132     def _merge_non_test_data(cls, aggregated_json, incremental_json):
133         """Merge incremental non tests property data into aggregated json results.
134
135         Args:
136             aggregated_json: aggregated json object.
137             incremental_json: incremental json object.
138
139         Returns:
140             True if merge succeeds or
141             False on failure.
142         """
143
144         incremental_builds = incremental_json[JSON_RESULTS_BUILD_NUMBERS]
145         aggregated_builds = aggregated_json[JSON_RESULTS_BUILD_NUMBERS]
146         aggregated_build_number = int(aggregated_builds[0])
147         # Loop through all incremental builds, start from the oldest run.
148         for index in reversed(range(len(incremental_builds))):
149             build_number = int(incremental_builds[index])
150             logging.debug("Merging build %s, incremental json index: %d.",
151                 build_number, index)
152
153             # Return if not all build numbers in the incremental json results
154             # are newer than the most recent build in the aggregated results.
155             # FIXME: make this case work.
156             if build_number < aggregated_build_number:
157                 logging.warning(("Build %d in incremental json is older than "
158                     "the most recent build in aggregated results: %d"),
159                     build_number, aggregated_build_number)
160                 return False
161
162             # Return if the build number is duplicated.
163             # FIXME: skip the duplicated build and merge rest of the results.
164             #        Need to be careful on skiping the corresponding value in
165             #        _merge_tests because the property data for each test could
166             #        be accumulated.
167             if build_number == aggregated_build_number:
168                 logging.warning("Duplicate build %d in incremental json",
169                     build_number)
170                 return False
171
172             # Merge this build into aggreagated results.
173             cls._merge_one_build(aggregated_json, incremental_json, index)
174             logging.debug("Merged build %s, merged json: %s.",
175                 build_number, aggregated_json)
176
177         return True
178
179     @classmethod
180     def _merge_one_build(cls, aggregated_json, incremental_json,
181                          incremental_index):
182         """Merge one build of incremental json into aggregated json results.
183
184         Args:
185             aggregated_json: aggregated json object.
186             incremental_json: incremental json object.
187             incremental_index: index of the incremental json results to merge.
188         """
189
190         for key in incremental_json.keys():
191             # Merge json results except "tests" properties (results, times etc).
192             # "tests" properties will be handled separately.
193             if key == JSON_RESULTS_TESTS:
194                 continue
195
196             if key in aggregated_json:
197                 aggregated_json[key].insert(
198                     0, incremental_json[key][incremental_index])
199                 aggregated_json[key] = \
200                     aggregated_json[key][:JSON_RESULTS_MAX_BUILDS]
201             else:
202                 aggregated_json[key] = incremental_json[key]
203
204     @classmethod
205     def _merge_tests(cls, aggregated_json, incremental_json):
206         """Merge "tests" properties:results, times.
207
208         Args:
209             aggregated_json: aggregated json object.
210             incremental_json: incremental json object.
211         """
212
213         for test_name in incremental_json:
214             incremental_test = incremental_json[test_name]
215             if test_name in aggregated_json:
216                 aggregated_test = aggregated_json[test_name]
217                 cls._insert_item_run_length_encoded(
218                     incremental_test[JSON_RESULTS_RESULTS],
219                     aggregated_test[JSON_RESULTS_RESULTS])
220                 cls._insert_item_run_length_encoded(
221                     incremental_test[JSON_RESULTS_TIMES],
222                     aggregated_test[JSON_RESULTS_TIMES])
223             else:
224                 aggregated_json[test_name] = incremental_test
225
226     @classmethod
227     def _insert_item_run_length_encoded(cls, incremental_item, aggregated_item):
228         """Inserts the incremental run-length encoded results into the aggregated
229            run-length encoded results.
230
231         Args:
232             incremental_item: incremental run-length encoded results.
233             aggregated_item: aggregated run-length encoded results.
234         """
235
236         for item in incremental_item:
237             if len(aggregated_item) and item[1] == aggregated_item[0][1]:
238                 aggregated_item[0][0] = min(
239                     aggregated_item[0][0] + item[0], JSON_RESULTS_MAX_BUILDS)
240             else:
241                 # The test item values need to be summed from continuous runs.
242                 # If there is an older item (not most recent one) whose value is
243                 # same as the one to insert, then we should remove the old item
244                 # from aggregated list.
245                 for i in reversed(range(1, len(aggregated_item))):
246                     if item[1] == aggregated_item[i][1]:
247                         aggregated_item.pop(i)
248
249                 aggregated_item.insert(0, item)
250
251     @classmethod
252     def _check_json(cls, builder, json):
253         """Check whether the given json is valid.
254
255         Args:
256             builder: builder name this json is for.
257             json: json object to check.
258
259         Returns:
260             True if the json is valid or
261             False otherwise.
262         """
263
264         version = json[JSON_RESULTS_VERSION_KEY]
265         if version > JSON_RESULTS_VERSION:
266             logging.error("Results JSON version '%s' is not supported.",
267                 version)
268             return False
269
270         if not builder in json:
271             logging.error("Builder '%s' is not in json results.", builder)
272             return False
273
274         results_for_builder = json[builder]
275         if not JSON_RESULTS_BUILD_NUMBERS in results_for_builder:
276             logging.error("Missing build number in json results.")
277             return False
278
279         return True
280
281     @classmethod
282     def merge(cls, builder, aggregated, incremental, sort_keys=False):
283         """Merge incremental json file data with aggregated json file data.
284
285         Args:
286             builder: builder name.
287             aggregated: aggregated json file data.
288             incremental: incremental json file data.
289             sort_key: whether or not to sort key when dumping json results.
290
291         Returns:
292             Merged json file data if merge succeeds or
293             None on failure.
294         """
295
296         if not incremental:
297             logging.warning("Nothing to merge.")
298             return None
299
300         logging.info("Loading incremental json...")
301         incremental_json = cls._load_json(incremental)
302         if not incremental_json:
303             return None
304
305         logging.info("Checking incremental json...")
306         if not cls._check_json(builder, incremental_json):
307             return None
308
309         logging.info("Loading existing aggregated json...")
310         aggregated_json = cls._load_json(aggregated)
311         if not aggregated_json:
312             return incremental
313
314         logging.info("Checking existing aggregated json...")
315         if not cls._check_json(builder, aggregated_json):
316             return incremental
317
318         logging.info("Merging json results...")
319         try:
320             if not cls._merge_json(
321                 aggregated_json[builder],
322                 incremental_json[builder]):
323                 return None
324         except Exception, err:
325             logging.error("Failed to merge json results: %s", str(err))
326             return None
327
328         aggregated_json[JSON_RESULTS_VERSION_KEY] = JSON_RESULTS_VERSION
329
330         return cls._generate_file_data(aggregated_json, sort_keys)
331
332     @classmethod
333     def update(cls, builder, test_type, incremental):
334         """Update datastore json file data by merging it with incremental json
335            file.
336
337         Args:
338             builder: builder name.
339             test_type: type of test results.
340             incremental: incremental json file data to merge.
341
342         Returns:
343             TestFile object if update succeeds or
344             None on failure.
345         """
346
347         files = TestFile.get_files(builder, test_type, JSON_RESULTS_FILE)
348         if files:
349             file = files[0]
350             new_results = cls.merge(builder, file.data, incremental)
351         else:
352             # Use the incremental data if there is no aggregated file to merge.
353             file = TestFile()
354             file.builder = builder
355             file.name = JSON_RESULTS_FILE
356             new_results = incremental
357             logging.info("No existing json results, incremental json is saved.")
358
359         if not new_results:
360             return None
361
362         if not file.save(new_results):
363             return None
364
365         return file