Home | History | Annotate | Download | only in model
      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_FILE_SMALL = "results-small.json"
     37 JSON_RESULTS_PREFIX = "ADD_RESULTS("
     38 JSON_RESULTS_SUFFIX = ");"
     39 JSON_RESULTS_VERSION_KEY = "version"
     40 JSON_RESULTS_BUILD_NUMBERS = "buildNumbers"
     41 JSON_RESULTS_TESTS = "tests"
     42 JSON_RESULTS_RESULTS = "results"
     43 JSON_RESULTS_TIMES = "times"
     44 JSON_RESULTS_PASS = "P"
     45 JSON_RESULTS_NO_DATA = "N"
     46 JSON_RESULTS_MIN_TIME = 1
     47 JSON_RESULTS_VERSION = 3
     48 JSON_RESULTS_MAX_BUILDS = 750
     49 JSON_RESULTS_MAX_BUILDS_SMALL = 200
     50 
     51 
     52 class JsonResults(object):
     53     @classmethod
     54     def _strip_prefix_suffix(cls, data):
     55         """Strip out prefix and suffix of json results string.
     56 
     57         Args:
     58             data: json file content.
     59 
     60         Returns:
     61             json string without prefix and suffix.
     62         """
     63 
     64         assert(data.startswith(JSON_RESULTS_PREFIX))
     65         assert(data.endswith(JSON_RESULTS_SUFFIX))
     66 
     67         return data[len(JSON_RESULTS_PREFIX):
     68                     len(data) - len(JSON_RESULTS_SUFFIX)]
     69 
     70     @classmethod
     71     def _generate_file_data(cls, json, sort_keys=False):
     72         """Given json string, generate file content data by adding
     73            prefix and suffix.
     74 
     75         Args:
     76             json: json string without prefix and suffix.
     77 
     78         Returns:
     79             json file data.
     80         """
     81 
     82         data = simplejson.dumps(json, separators=(',', ':'),
     83             sort_keys=sort_keys)
     84         return JSON_RESULTS_PREFIX + data + JSON_RESULTS_SUFFIX
     85 
     86     @classmethod
     87     def _load_json(cls, file_data):
     88         """Load json file to a python object.
     89 
     90         Args:
     91             file_data: json file content.
     92 
     93         Returns:
     94             json object or
     95             None on failure.
     96         """
     97 
     98         json_results_str = cls._strip_prefix_suffix(file_data)
     99         if not json_results_str:
    100             logging.warning("No json results data.")
    101             return None
    102 
    103         try:
    104             return simplejson.loads(json_results_str)
    105         except Exception, err:
    106             logging.debug(json_results_str)
    107             logging.error("Failed to load json results: %s", str(err))
    108             return None
    109 
    110     @classmethod
    111     def _merge_json(cls, aggregated_json, incremental_json, num_runs):
    112         """Merge incremental json into aggregated json results.
    113 
    114         Args:
    115             aggregated_json: aggregated json object.
    116             incremental_json: incremental json object.
    117             num_runs: number of total runs to include.
    118 
    119         Returns:
    120             True if merge succeeds or
    121             False on failure.
    122         """
    123 
    124         # Merge non tests property data.
    125         # Tests properties are merged in _merge_tests.
    126         if not cls._merge_non_test_data(aggregated_json, incremental_json, num_runs):
    127             return False
    128 
    129         # Merge tests results and times
    130         incremental_tests = incremental_json[JSON_RESULTS_TESTS]
    131         if incremental_tests:
    132             aggregated_tests = aggregated_json[JSON_RESULTS_TESTS]
    133             cls._merge_tests(aggregated_tests, incremental_tests, num_runs)
    134 
    135         return True
    136 
    137     @classmethod
    138     def _merge_non_test_data(cls, aggregated_json, incremental_json, num_runs):
    139         """Merge incremental non tests property data into aggregated json results.
    140 
    141         Args:
    142             aggregated_json: aggregated json object.
    143             incremental_json: incremental json object.
    144             num_runs: number of total runs to include.
    145 
    146         Returns:
    147             True if merge succeeds or
    148             False on failure.
    149         """
    150 
    151         incremental_builds = incremental_json[JSON_RESULTS_BUILD_NUMBERS]
    152         aggregated_builds = aggregated_json[JSON_RESULTS_BUILD_NUMBERS]
    153         aggregated_build_number = int(aggregated_builds[0])
    154         # Loop through all incremental builds, start from the oldest run.
    155         for index in reversed(range(len(incremental_builds))):
    156             build_number = int(incremental_builds[index])
    157             logging.debug("Merging build %s, incremental json index: %d.",
    158                 build_number, index)
    159 
    160             # Return if not all build numbers in the incremental json results
    161             # are newer than the most recent build in the aggregated results.
    162             # FIXME: make this case work.
    163             if build_number < aggregated_build_number:
    164                 logging.warning(("Build %d in incremental json is older than "
    165                     "the most recent build in aggregated results: %d"),
    166                     build_number, aggregated_build_number)
    167                 return False
    168 
    169             # Return if the build number is duplicated.
    170             # FIXME: skip the duplicated build and merge rest of the results.
    171             #        Need to be careful on skiping the corresponding value in
    172             #        _merge_tests because the property data for each test could
    173             #        be accumulated.
    174             if build_number == aggregated_build_number:
    175                 logging.warning("Duplicate build %d in incremental json",
    176                     build_number)
    177                 return False
    178 
    179             # Merge this build into aggreagated results.
    180             cls._merge_one_build(aggregated_json, incremental_json, index, num_runs)
    181 
    182         return True
    183 
    184     @classmethod
    185     def _merge_one_build(cls, aggregated_json, incremental_json,
    186                          incremental_index, num_runs):
    187         """Merge one build of incremental json into aggregated json results.
    188 
    189         Args:
    190             aggregated_json: aggregated json object.
    191             incremental_json: incremental json object.
    192             incremental_index: index of the incremental json results to merge.
    193             num_runs: number of total runs to include.
    194         """
    195 
    196         for key in incremental_json.keys():
    197             # Merge json results except "tests" properties (results, times etc).
    198             # "tests" properties will be handled separately.
    199             if key == JSON_RESULTS_TESTS:
    200                 continue
    201 
    202             if key in aggregated_json:
    203                 aggregated_json[key].insert(
    204                     0, incremental_json[key][incremental_index])
    205                 aggregated_json[key] = \
    206                     aggregated_json[key][:num_runs]
    207             else:
    208                 aggregated_json[key] = incremental_json[key]
    209 
    210     @classmethod
    211     def _merge_tests(cls, aggregated_json, incremental_json, num_runs):
    212         """Merge "tests" properties:results, times.
    213 
    214         Args:
    215             aggregated_json: aggregated json object.
    216             incremental_json: incremental json object.
    217             num_runs: number of total runs to include.
    218         """
    219 
    220         all_tests = (set(aggregated_json.iterkeys()) |
    221                      set(incremental_json.iterkeys()))
    222         for test_name in all_tests:
    223             if test_name in aggregated_json:
    224                 aggregated_test = aggregated_json[test_name]
    225                 if test_name in incremental_json:
    226                     incremental_test = incremental_json[test_name]
    227                     results = incremental_test[JSON_RESULTS_RESULTS]
    228                     times = incremental_test[JSON_RESULTS_TIMES]
    229                 else:
    230                     results = [[1, JSON_RESULTS_NO_DATA]]
    231                     times = [[1, 0]]
    232 
    233                 cls._insert_item_run_length_encoded(
    234                     results, aggregated_test[JSON_RESULTS_RESULTS], num_runs)
    235                 cls._insert_item_run_length_encoded(
    236                     times, aggregated_test[JSON_RESULTS_TIMES], num_runs)
    237                 cls._normalize_results_json(test_name, aggregated_json, num_runs)
    238             else:
    239                 aggregated_json[test_name] = incremental_json[test_name]
    240 
    241     @classmethod
    242     def _insert_item_run_length_encoded(cls, incremental_item, aggregated_item, num_runs):
    243         """Inserts the incremental run-length encoded results into the aggregated
    244            run-length encoded results.
    245 
    246         Args:
    247             incremental_item: incremental run-length encoded results.
    248             aggregated_item: aggregated run-length encoded results.
    249             num_runs: number of total runs to include.
    250         """
    251 
    252         for item in incremental_item:
    253             if len(aggregated_item) and item[1] == aggregated_item[0][1]:
    254                 aggregated_item[0][0] = min(
    255                     aggregated_item[0][0] + item[0], num_runs)
    256             else:
    257                 aggregated_item.insert(0, item)
    258 
    259     @classmethod
    260     def _normalize_results_json(cls, test_name, aggregated_json, num_runs):
    261         """ Prune tests where all runs pass or tests that no longer exist and
    262         truncate all results to num_runs.
    263 
    264         Args:
    265           test_name: Name of the test.
    266           aggregated_json: The JSON object with all the test results for
    267                            this builder.
    268           num_runs: number of total runs to include.
    269         """
    270 
    271         aggregated_test = aggregated_json[test_name]
    272         aggregated_test[JSON_RESULTS_RESULTS] = \
    273             cls._remove_items_over_max_number_of_builds(
    274                 aggregated_test[JSON_RESULTS_RESULTS], num_runs)
    275         aggregated_test[JSON_RESULTS_TIMES] = \
    276             cls._remove_items_over_max_number_of_builds(
    277                 aggregated_test[JSON_RESULTS_TIMES], num_runs)
    278 
    279         is_all_pass = cls._is_results_all_of_type(
    280             aggregated_test[JSON_RESULTS_RESULTS], JSON_RESULTS_PASS)
    281         is_all_no_data = cls._is_results_all_of_type(
    282             aggregated_test[JSON_RESULTS_RESULTS], JSON_RESULTS_NO_DATA)
    283 
    284         max_time = max(
    285             [time[1] for time in aggregated_test[JSON_RESULTS_TIMES]])
    286         # Remove all passes/no-data from the results to reduce noise and
    287         # filesize. If a test passes every run, but
    288         # takes >= JSON_RESULTS_MIN_TIME to run, don't throw away the data.
    289         if (is_all_no_data or
    290            (is_all_pass and max_time < JSON_RESULTS_MIN_TIME)):
    291             del aggregated_json[test_name]
    292 
    293     @classmethod
    294     def _remove_items_over_max_number_of_builds(cls, encoded_list, num_runs):
    295         """Removes items from the run-length encoded list after the final
    296         item that exceeds the max number of builds to track.
    297 
    298         Args:
    299           encoded_results: run-length encoded results. An array of arrays, e.g.
    300               [[3,'A'],[1,'Q']] encodes AAAQ.
    301           num_runs: number of total runs to include.
    302         """
    303         num_builds = 0
    304         index = 0
    305         for result in encoded_list:
    306             num_builds = num_builds + result[0]
    307             index = index + 1
    308             if num_builds >= num_runs:
    309                 return encoded_list[:index]
    310 
    311         return encoded_list
    312 
    313     @classmethod
    314     def _is_results_all_of_type(cls, results, type):
    315         """Returns whether all the results are of the given type
    316         (e.g. all passes).
    317         """
    318 
    319         return len(results) == 1 and results[0][1] == type
    320 
    321     @classmethod
    322     def _check_json(cls, builder, json):
    323         """Check whether the given json is valid.
    324 
    325         Args:
    326             builder: builder name this json is for.
    327             json: json object to check.
    328 
    329         Returns:
    330             True if the json is valid or
    331             False otherwise.
    332         """
    333 
    334         version = json[JSON_RESULTS_VERSION_KEY]
    335         if version > JSON_RESULTS_VERSION:
    336             logging.error("Results JSON version '%s' is not supported.",
    337                 version)
    338             return False
    339 
    340         if not builder in json:
    341             logging.error("Builder '%s' is not in json results.", builder)
    342             return False
    343 
    344         results_for_builder = json[builder]
    345         if not JSON_RESULTS_BUILD_NUMBERS in results_for_builder:
    346             logging.error("Missing build number in json results.")
    347             return False
    348 
    349         return True
    350 
    351     @classmethod
    352     def merge(cls, builder, aggregated, incremental, num_runs, sort_keys=False):
    353         """Merge incremental json file data with aggregated json file data.
    354 
    355         Args:
    356             builder: builder name.
    357             aggregated: aggregated json file data.
    358             incremental: incremental json file data.
    359             sort_key: whether or not to sort key when dumping json results.
    360 
    361         Returns:
    362             Merged json file data if merge succeeds or
    363             None on failure.
    364         """
    365 
    366         if not incremental:
    367             logging.warning("Nothing to merge.")
    368             return None
    369 
    370         logging.info("Loading incremental json...")
    371         incremental_json = cls._load_json(incremental)
    372         if not incremental_json:
    373             return None
    374 
    375         logging.info("Checking incremental json...")
    376         if not cls._check_json(builder, incremental_json):
    377             return None
    378 
    379         logging.info("Loading existing aggregated json...")
    380         aggregated_json = cls._load_json(aggregated)
    381         if not aggregated_json:
    382             return incremental
    383 
    384         logging.info("Checking existing aggregated json...")
    385         if not cls._check_json(builder, aggregated_json):
    386             return incremental
    387 
    388         logging.info("Merging json results...")
    389         try:
    390             if not cls._merge_json(aggregated_json[builder], incremental_json[builder], num_runs):
    391                 return None
    392         except Exception, err:
    393             logging.error("Failed to merge json results: %s", str(err))
    394             return None
    395 
    396         aggregated_json[JSON_RESULTS_VERSION_KEY] = JSON_RESULTS_VERSION
    397 
    398         return cls._generate_file_data(aggregated_json, sort_keys)
    399 
    400     @classmethod
    401     def update(cls, master, builder, test_type, incremental):
    402         """Update datastore json file data by merging it with incremental json
    403            file. Writes the large file and a small file. The small file just stores
    404            fewer runs.
    405 
    406         Args:
    407             master: master name.
    408             builder: builder name.
    409             test_type: type of test results.
    410             incremental: incremental json file data to merge.
    411 
    412         Returns:
    413             Large TestFile object if update succeeds or
    414             None on failure.
    415         """
    416         small_file_updated = cls.update_file(master, builder, test_type, incremental, JSON_RESULTS_FILE_SMALL, JSON_RESULTS_MAX_BUILDS_SMALL)
    417         large_file_updated = cls.update_file(master, builder, test_type, incremental, JSON_RESULTS_FILE, JSON_RESULTS_MAX_BUILDS)
    418 
    419         return small_file_updated and large_file_updated
    420 
    421     @classmethod
    422     def update_file(cls, master, builder, test_type, incremental, filename, num_runs):
    423         files = TestFile.get_files(master, builder, test_type, filename)
    424         if files:
    425             file = files[0]
    426             new_results = cls.merge(builder, file.data, incremental, num_runs)
    427         else:
    428             # Use the incremental data if there is no aggregated file to merge.
    429             file = TestFile()            
    430             file.master = master
    431             file.builder = builder
    432             file.test_type = test_type
    433             file.name = filename
    434             new_results = incremental
    435             logging.info("No existing json results, incremental json is saved.")
    436 
    437         if not new_results or not file.save(new_results):
    438             logging.info(
    439                 "Update failed, master: %s, builder: %s, test_type: %s, name: %s." %
    440                 (master, builder, test_type, filename))
    441             return False
    442 
    443         return True
    444 
    445     @classmethod
    446     def get_test_list(cls, builder, json_file_data):
    447         """Get list of test names from aggregated json file data.
    448 
    449         Args:
    450             json_file_data: json file data that has all test-data and
    451                             non-test-data.
    452 
    453         Returns:
    454             json file with test name list only. The json format is the same
    455             as the one saved in datastore, but all non-test-data and test detail
    456             results are removed.
    457         """
    458 
    459         logging.debug("Loading test results json...")
    460         json = cls._load_json(json_file_data)
    461         if not json:
    462             return None
    463 
    464         logging.debug("Checking test results json...")
    465         if not cls._check_json(builder, json):
    466             return None
    467 
    468         test_list_json = {}
    469         tests = json[builder][JSON_RESULTS_TESTS]
    470         test_list_json[builder] = {
    471             "tests": dict.fromkeys(tests, {})}
    472 
    473         return cls._generate_file_data(test_list_json)
    474