2010-08-10 Victor Wang <victorw@chromium.org>
[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
175         return True
176
177     @classmethod
178     def _merge_one_build(cls, aggregated_json, incremental_json,
179                          incremental_index):
180         """Merge one build of incremental json into aggregated json results.
181
182         Args:
183             aggregated_json: aggregated json object.
184             incremental_json: incremental json object.
185             incremental_index: index of the incremental json results to merge.
186         """
187
188         for key in incremental_json.keys():
189             # Merge json results except "tests" properties (results, times etc).
190             # "tests" properties will be handled separately.
191             if key == JSON_RESULTS_TESTS:
192                 continue
193
194             if key in aggregated_json:
195                 aggregated_json[key].insert(
196                     0, incremental_json[key][incremental_index])
197                 aggregated_json[key] = \
198                     aggregated_json[key][:JSON_RESULTS_MAX_BUILDS]
199             else:
200                 aggregated_json[key] = incremental_json[key]
201
202     @classmethod
203     def _merge_tests(cls, aggregated_json, incremental_json):
204         """Merge "tests" properties:results, times.
205
206         Args:
207             aggregated_json: aggregated json object.
208             incremental_json: incremental json object.
209         """
210
211         all_tests = (set(aggregated_json.iterkeys()) |
212                      set(incremental_json.iterkeys()))
213         for test_name in all_tests:
214             if test_name in aggregated_json:
215                 aggregated_test = aggregated_json[test_name]
216                 if test_name in incremental_json:
217                     incremental_test = incremental_json[test_name]
218                     results = incremental_test[JSON_RESULTS_RESULTS]
219                     times = incremental_test[JSON_RESULTS_TIMES]
220                 else:
221                     results = [[1, "P"]]
222                     times = [[1, "0"]]
223
224                 cls._insert_item_run_length_encoded(
225                     results, aggregated_test[JSON_RESULTS_RESULTS])
226                 cls._insert_item_run_length_encoded(
227                     times, aggregated_test[JSON_RESULTS_TIMES])
228             else:
229                 aggregated_json[test_name] = incremental_json[test_name]
230
231     @classmethod
232     def _insert_item_run_length_encoded(cls, incremental_item, aggregated_item):
233         """Inserts the incremental run-length encoded results into the aggregated
234            run-length encoded results.
235
236         Args:
237             incremental_item: incremental run-length encoded results.
238             aggregated_item: aggregated run-length encoded results.
239         """
240
241         for item in incremental_item:
242             if len(aggregated_item) and item[1] == aggregated_item[0][1]:
243                 aggregated_item[0][0] = min(
244                     aggregated_item[0][0] + item[0], JSON_RESULTS_MAX_BUILDS)
245             else:
246                 # The test item values need to be summed from continuous runs.
247                 # If there is an older item (not most recent one) whose value is
248                 # same as the one to insert, then we should remove the old item
249                 # from aggregated list.
250                 for i in reversed(range(1, len(aggregated_item))):
251                     if item[1] == aggregated_item[i][1]:
252                         aggregated_item.pop(i)
253
254                 aggregated_item.insert(0, item)
255
256     @classmethod
257     def _check_json(cls, builder, json):
258         """Check whether the given json is valid.
259
260         Args:
261             builder: builder name this json is for.
262             json: json object to check.
263
264         Returns:
265             True if the json is valid or
266             False otherwise.
267         """
268
269         version = json[JSON_RESULTS_VERSION_KEY]
270         if version > JSON_RESULTS_VERSION:
271             logging.error("Results JSON version '%s' is not supported.",
272                 version)
273             return False
274
275         if not builder in json:
276             logging.error("Builder '%s' is not in json results.", builder)
277             return False
278
279         results_for_builder = json[builder]
280         if not JSON_RESULTS_BUILD_NUMBERS in results_for_builder:
281             logging.error("Missing build number in json results.")
282             return False
283
284         return True
285
286     @classmethod
287     def merge(cls, builder, aggregated, incremental, sort_keys=False):
288         """Merge incremental json file data with aggregated json file data.
289
290         Args:
291             builder: builder name.
292             aggregated: aggregated json file data.
293             incremental: incremental json file data.
294             sort_key: whether or not to sort key when dumping json results.
295
296         Returns:
297             Merged json file data if merge succeeds or
298             None on failure.
299         """
300
301         if not incremental:
302             logging.warning("Nothing to merge.")
303             return None
304
305         logging.info("Loading incremental json...")
306         incremental_json = cls._load_json(incremental)
307         if not incremental_json:
308             return None
309
310         logging.info("Checking incremental json...")
311         if not cls._check_json(builder, incremental_json):
312             return None
313
314         logging.info("Loading existing aggregated json...")
315         aggregated_json = cls._load_json(aggregated)
316         if not aggregated_json:
317             return incremental
318
319         logging.info("Checking existing aggregated json...")
320         if not cls._check_json(builder, aggregated_json):
321             return incremental
322
323         logging.info("Merging json results...")
324         try:
325             if not cls._merge_json(
326                 aggregated_json[builder],
327                 incremental_json[builder]):
328                 return None
329         except Exception, err:
330             logging.error("Failed to merge json results: %s", str(err))
331             return None
332
333         aggregated_json[JSON_RESULTS_VERSION_KEY] = JSON_RESULTS_VERSION
334
335         return cls._generate_file_data(aggregated_json, sort_keys)
336
337     @classmethod
338     def update(cls, builder, test_type, incremental):
339         """Update datastore json file data by merging it with incremental json
340            file.
341
342         Args:
343             builder: builder name.
344             test_type: type of test results.
345             incremental: incremental json file data to merge.
346
347         Returns:
348             TestFile object if update succeeds or
349             None on failure.
350         """
351
352         files = TestFile.get_files(builder, test_type, JSON_RESULTS_FILE)
353         if files:
354             file = files[0]
355             new_results = cls.merge(builder, file.data, incremental)
356         else:
357             # Use the incremental data if there is no aggregated file to merge.
358             file = TestFile()
359             file.builder = builder
360             file.name = JSON_RESULTS_FILE
361             new_results = incremental
362             logging.info("No existing json results, incremental json is saved.")
363
364         if not new_results:
365             return None
366
367         if not file.save(new_results):
368             return None
369
370         return file