Changeset 62989 in webkit


Ignore:
Timestamp:
Jul 9, 2010 1:38:28 PM (14 years ago)
Author:
kinuko@chromium.org
Message:

2010-07-08 Kinuko Yasuda <kinuko@chromium.org>

Reviewed by Ojan Vafai.

cleanup json_results_generator dependencies so that non-layout-tests can also use it safely
https://bugs.webkit.org/show_bug.cgi?id=38693

Introduced a new base class JSONResultsGeneratorBase that doesn't
have any dependency on layout_tests packages.
Turned JSONResultsGenerator into a wrapper class of the base class
so that the old code can work with it during the cleanup.

Added json_results_generator_unittest.py.

  • Scripts/webkitpy/layout_tests/layout_package/json_layout_results_generator.py:
  • Scripts/webkitpy/layout_tests/layout_package/json_results_generator.py:
  • Scripts/webkitpy/layout_tests/layout_package/json_results_generator_unittest.py: Added
Location:
trunk/WebKitTools
Files:
1 added
3 edited

Legend:

Unmodified
Added
Removed
  • trunk/WebKitTools/ChangeLog

    r62983 r62989  
     12010-07-08  Kinuko Yasuda  <kinuko@chromium.org>
     2
     3        Reviewed by Ojan Vafai.
     4
     5        cleanup json_results_generator dependencies so that non-layout-tests can also use it safely
     6        https://bugs.webkit.org/show_bug.cgi?id=38693
     7
     8        Introduced a new base class JSONResultsGeneratorBase that doesn't
     9        have any dependency on layout_tests packages.
     10        Turned JSONResultsGenerator into a wrapper class of the base class
     11        so that the old code can work with it during the cleanup.
     12
     13        Added json_results_generator_unittest.py.
     14
     15        * Scripts/webkitpy/layout_tests/layout_package/json_layout_results_generator.py:
     16        * Scripts/webkitpy/layout_tests/layout_package/json_results_generator.py:
     17        * Scripts/webkitpy/layout_tests/layout_package/json_results_generator_unittest.py: Added
     18
    1192010-07-09  Abhishek Arya  <inferno@chromium.org>
    220
  • trunk/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/json_layout_results_generator.py

    r60690 r62989  
    1 #!/usr/bin/env python
    21# Copyright (C) 2010 Google Inc. All rights reserved.
    32#
     
    3635import webkitpy.thirdparty.simplejson as simplejson
    3736
    38 class JSONLayoutResultsGenerator(json_results_generator.JSONResultsGenerator):
     37
     38class JSONLayoutResultsGenerator(json_results_generator.JSONResultsGeneratorBase):
    3939    """A JSON results generator for layout tests."""
    4040
     
    4444    WONTFIX = "wontfixCounts"
    4545    DEFERRED = "deferredCounts"
     46
     47    # Note that we omit test_expectations.FAIL from this list because
     48    # it should never show up (it's a legacy input expectation, never
     49    # an output expectation).
     50    FAILURE_TO_CHAR = {test_expectations.CRASH: "C",
     51                       test_expectations.TIMEOUT: "T",
     52                       test_expectations.IMAGE: "I",
     53                       test_expectations.TEXT: "F",
     54                       test_expectations.MISSING: "O",
     55                       test_expectations.IMAGE_PLUS_TEXT: "Z"}
    4656
    4757    def __init__(self, port, builder_name, build_name, build_number,
     
    5464          result_summary: ResultsSummary object storing the summary of the test
    5565              results.
    56           (see the comment of JSONResultsGenerator.__init__ for other Args)
    5766        """
     67        super(JSONLayoutResultsGenerator, self).__init__(
     68            builder_name, build_name, build_number, results_file_base_path,
     69            builder_base_url, {}, port.test_repository_paths())
     70
    5871        self._port = port
    59         self._builder_name = builder_name
    60         self._build_name = build_name
    61         self._build_number = build_number
    62         self._builder_base_url = builder_base_url
    63         self._results_file_path = os.path.join(results_file_base_path,
    64             self.RESULTS_FILENAME)
    6572        self._expectations = expectations
    66 
    67         # We don't use self._skipped_tests and self._passed_tests as we
    68         # override _InsertFailureSummaries.
    6973
    7074        # We want relative paths to LayoutTest root for JSON output.
     
    7882            (path_to_name(test_tuple.filename), test_tuple.test_run_time)
    7983            for test_tuple in test_timings)
    80         self._svn_repositories = port.test_repository_paths()
    81 
    82         self._generate_json_output()
     84
     85        self.generate_json_output()
    8386
    8487    def _get_path_relative_to_layout_test_root(self, test):
     
    101104        # Make sure all paths are unix-style.
    102105        return relativePath.replace('\\', '/')
     106
     107    # override
     108    def _get_test_timing(self, test_name):
     109        if test_name in self._test_timings:
     110            # Floor for now to get time in seconds.
     111            return int(self._test_timings[test_name])
     112        return 0
     113
     114    # override
     115    def _get_failed_test_names(self):
     116        return set(self._failures.keys())
     117
     118    # override
     119    def _get_result_type_char(self, test_name):
     120        if test_name not in self._all_tests:
     121            return self.NO_DATA_RESULT
     122
     123        if test_name in self._failures:
     124            return self.FAILURE_TO_CHAR[self._failures[test_name]]
     125
     126        return self.PASS_RESULT
    103127
    104128    # override
  • trunk/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/json_results_generator.py

    r60714 r62989  
    1 #!/usr/bin/env python
    21# Copyright (C) 2010 Google Inc. All rights reserved.
    32#
     
    3938import xml.dom.minidom
    4039
    41 from webkitpy.common.checkout import scm
    42 from webkitpy.common.system.executive import ScriptError
    43 from webkitpy.layout_tests.layout_package import test_expectations
    4440import webkitpy.thirdparty.simplejson as simplejson
    4541
    46 _log = logging.getLogger("webkitpy.layout_tests.layout_package."
    47                          "json_results_generator")
    48 
    49 
    50 class JSONResultsGenerator(object):
     42# A JSON results generator for generic tests.
     43# FIXME: move this code out of the layout_package directory.
     44
     45_log = logging.getLogger("webkitpy.layout_tests.layout_package.json_results_generator")
     46
     47
     48class TestResult(object):
     49    """A simple class that represents a single test result."""
     50    def __init__(self, name, failed=False, skipped=False, elapsed_time=0):
     51        self.name = name
     52        self.failed = failed
     53        self.skipped = skipped
     54        self.time = elapsed_time
     55
     56    def fixable(self):
     57        return self.failed or self.skipped
     58
     59
     60class JSONResultsGeneratorBase(object):
    5161    """A JSON results generator for generic tests."""
    5262
     
    5868    PASS_RESULT = "P"
    5969    SKIP_RESULT = "X"
     70    FAIL_RESULT = "F"
    6071    NO_DATA_RESULT = "N"
    6172    VERSION = 3
     
    7182    ALL_FIXABLE_COUNT = "allFixableCount"
    7283
    73     # Note that we omit test_expectations.FAIL from this list because
    74     # it should never show up (it's a legacy input expectation, never
    75     # an output expectation).
    76     FAILURE_TO_CHAR = {test_expectations.CRASH: "C",
    77                        test_expectations.TIMEOUT: "T",
    78                        test_expectations.IMAGE: "I",
    79                        test_expectations.TEXT: "F",
    80                        test_expectations.MISSING: "O",
    81                        test_expectations.IMAGE_PLUS_TEXT: "Z"}
    82     FAILURE_CHARS = FAILURE_TO_CHAR.values()
    83 
    8484    RESULTS_FILENAME = "results.json"
     85
     86    def __init__(self, builder_name, build_name, build_number,
     87        results_file_base_path, builder_base_url,
     88        test_results_map, svn_repositories=None):
     89        """Modifies the results.json file. Grabs it off the archive directory
     90        if it is not found locally.
     91
     92        Args
     93          builder_name: the builder name (e.g. Webkit).
     94          build_name: the build name (e.g. webkit-rel).
     95          build_number: the build number.
     96          results_file_base_path: Absolute path to the directory containing the
     97              results json file.
     98          builder_base_url: the URL where we have the archived test results.
     99              If this is None no archived results will be retrieved.
     100          test_results_map: A dictionary that maps test_name to TestResult.
     101          svn_repositories: A (json_field_name, svn_path) pair for SVN
     102              repositories that tests rely on.  The SVN revision will be
     103              included in the JSON with the given json_field_name.
     104        """
     105        self._builder_name = builder_name
     106        self._build_name = build_name
     107        self._build_number = build_number
     108        self._builder_base_url = builder_base_url
     109        self._results_file_path = os.path.join(results_file_base_path,
     110            self.RESULTS_FILENAME)
     111
     112        self._test_results_map = test_results_map
     113        self._test_results = test_results_map.values()
     114
     115        self._svn_repositories = svn_repositories
     116        if not self._svn_repositories:
     117            self._svn_repositories = {}
     118
     119        self._json = None
     120
     121    def generate_json_output(self):
     122        """Generates the JSON output file."""
     123        if not self._json:
     124            self._json = self.get_json()
     125        if self._json:
     126            # Specify separators in order to get compact encoding.
     127            json_data = simplejson.dumps(self._json, separators=(',', ':'))
     128            json_string = self.JSON_PREFIX + json_data + self.JSON_SUFFIX
     129
     130            results_file = codecs.open(self._results_file_path, "w", "utf-8")
     131            results_file.write(json_string)
     132            results_file.close()
     133
     134    def get_json(self):
     135        """Gets the results for the results.json file."""
     136        if self._json:
     137            return self._json
     138
     139        results_json, error = self._get_archived_json_results()
     140        if error:
     141            # If there was an error don't write a results.json
     142            # file at all as it would lose all the information on the bot.
     143            _log.error("Archive directory is inaccessible. Not modifying "
     144                       "or clobbering the results.json file: " + str(error))
     145            return None
     146
     147        builder_name = self._builder_name
     148        if results_json and builder_name not in results_json:
     149            _log.debug("Builder name (%s) is not in the results.json file."
     150                       % builder_name)
     151
     152        self._convert_json_to_current_version(results_json)
     153
     154        if builder_name not in results_json:
     155            results_json[builder_name] = (
     156                self._create_results_for_builder_json())
     157
     158        results_for_builder = results_json[builder_name]
     159
     160        self._insert_generic_metadata(results_for_builder)
     161
     162        self._insert_failure_summaries(results_for_builder)
     163
     164        # Update the all failing tests with result type and time.
     165        tests = results_for_builder[self.TESTS]
     166        all_failing_tests = self._get_failed_test_names()
     167        all_failing_tests.update(tests.iterkeys())
     168        for test in all_failing_tests:
     169            self._insert_test_time_and_result(test, tests)
     170
     171        self._json = results_json
     172        return self._json
     173
     174    def _get_test_timing(self, test_name):
     175        """Returns test timing data (elapsed time) in second
     176        for the given test_name."""
     177        if test_name in self._test_results_map:
     178            # Floor for now to get time in seconds.
     179            return int(self._test_results_map[test_name].time)
     180        return 0
     181
     182    def _get_failed_test_names(self):
     183        """Returns a set of failed test names."""
     184        return set([r.name for r in self._test_results if r.failed])
     185
     186    def _get_result_type_char(self, test_name):
     187        """Returns a single char (e.g. SKIP_RESULT, FAIL_RESULT,
     188        PASS_RESULT, NO_DATA_RESULT, etc) that indicates the test result
     189        for the given test_name.
     190        """
     191        if test_name not in self._test_results_map:
     192            return JSONResultsGenerator.NO_DATA_RESULT
     193
     194        test_result = self._test_results_map[test_name]
     195        if test_result.skipped:
     196            return JSONResultsGenerator.SKIP_RESULT
     197        if test_result.failed:
     198            return JSONResultsGenerator.FAIL_RESULT
     199
     200        return JSONResultsGenerator.PASS_RESULT
     201
     202    # FIXME: Callers should use scm.py instead.
     203    # FIXME: Identify and fix the run-time errors that were observed on Windows
     204    # chromium buildbot when we had updated this code to use scm.py once before.
     205    def _get_svn_revision(self, in_directory):
     206        """Returns the svn revision for the given directory.
     207
     208        Args:
     209          in_directory: The directory where svn is to be run.
     210        """
     211        if os.path.exists(os.path.join(in_directory, '.svn')):
     212            # Note: Not thread safe: http://bugs.python.org/issue2320
     213            output = subprocess.Popen(["svn", "info", "--xml"],
     214                                      cwd=in_directory,
     215                                      shell=(sys.platform == 'win32'),
     216                                      stdout=subprocess.PIPE).communicate()[0]
     217            try:
     218                dom = xml.dom.minidom.parseString(output)
     219                return dom.getElementsByTagName('entry')[0].getAttribute(
     220                    'revision')
     221            except xml.parsers.expat.ExpatError:
     222                return ""
     223        return ""
     224
     225    def _get_archived_json_results(self):
     226        """Reads old results JSON file if it exists.
     227        Returns (archived_results, error) tuple where error is None if results
     228        were successfully read.
     229        """
     230        results_json = {}
     231        old_results = None
     232        error = None
     233
     234        if os.path.exists(self._results_file_path):
     235            with codecs.open(self._results_file_path, "r", "utf-8") as file:
     236                old_results = file.read()
     237        elif self._builder_base_url:
     238            # Check if we have the archived JSON file on the buildbot server.
     239            results_file_url = (self._builder_base_url +
     240                self._build_name + "/" + self.RESULTS_FILENAME)
     241            _log.error("Local results.json file does not exist. Grabbing "
     242                       "it off the archive at " + results_file_url)
     243
     244            try:
     245                results_file = urllib2.urlopen(results_file_url)
     246                info = results_file.info()
     247                old_results = results_file.read()
     248            except urllib2.HTTPError, http_error:
     249                # A non-4xx status code means the bot is hosed for some reason
     250                # and we can't grab the results.json file off of it.
     251                if (http_error.code < 400 and http_error.code >= 500):
     252                    error = http_error
     253            except urllib2.URLError, url_error:
     254                error = url_error
     255
     256        if old_results:
     257            # Strip the prefix and suffix so we can get the actual JSON object.
     258            old_results = old_results[len(self.JSON_PREFIX):
     259                                      len(old_results) - len(self.JSON_SUFFIX)]
     260
     261            try:
     262                results_json = simplejson.loads(old_results)
     263            except:
     264                _log.debug("results.json was not valid JSON. Clobbering.")
     265                # The JSON file is not valid JSON. Just clobber the results.
     266                results_json = {}
     267        else:
     268            _log.debug('Old JSON results do not exist. Starting fresh.')
     269            results_json = {}
     270
     271        return results_json, error
     272
     273    def _insert_failure_summaries(self, results_for_builder):
     274        """Inserts aggregate pass/failure statistics into the JSON.
     275        This method reads self._test_results and generates
     276        FIXABLE, FIXABLE_COUNT and ALL_FIXABLE_COUNT entries.
     277
     278        Args:
     279          results_for_builder: Dictionary containing the test results for a
     280              single builder.
     281        """
     282        # Insert the number of tests that failed or skipped.
     283        fixable_count = len([r for r in self._test_results if r.fixable()])
     284        self._insert_item_into_raw_list(results_for_builder,
     285            fixable_count, self.FIXABLE_COUNT)
     286
     287        # Create a pass/skip/failure summary dictionary.
     288        entry = {}
     289        for test_name in self._test_results_map.iterkeys():
     290            result_char = self._get_result_type_char(test_name)
     291            entry[result_char] = entry.get(result_char, 0) + 1
     292
     293        # Insert the pass/skip/failure summary dictionary.
     294        self._insert_item_into_raw_list(results_for_builder, entry,
     295                                        self.FIXABLE)
     296
     297        # Insert the number of all the tests that are supposed to pass.
     298        all_test_count = len(self._test_results)
     299        self._insert_item_into_raw_list(results_for_builder,
     300            all_test_count, self.ALL_FIXABLE_COUNT)
     301
     302    def _insert_item_into_raw_list(self, results_for_builder, item, key):
     303        """Inserts the item into the list with the given key in the results for
     304        this builder. Creates the list if no such list exists.
     305
     306        Args:
     307          results_for_builder: Dictionary containing the test results for a
     308              single builder.
     309          item: Number or string to insert into the list.
     310          key: Key in results_for_builder for the list to insert into.
     311        """
     312        if key in results_for_builder:
     313            raw_list = results_for_builder[key]
     314        else:
     315            raw_list = []
     316
     317        raw_list.insert(0, item)
     318        raw_list = raw_list[:self.MAX_NUMBER_OF_BUILD_RESULTS_TO_LOG]
     319        results_for_builder[key] = raw_list
     320
     321    def _insert_item_run_length_encoded(self, item, encoded_results):
     322        """Inserts the item into the run-length encoded results.
     323
     324        Args:
     325          item: String or number to insert.
     326          encoded_results: run-length encoded results. An array of arrays, e.g.
     327              [[3,'A'],[1,'Q']] encodes AAAQ.
     328        """
     329        if len(encoded_results) and item == encoded_results[0][1]:
     330            num_results = encoded_results[0][0]
     331            if num_results <= self.MAX_NUMBER_OF_BUILD_RESULTS_TO_LOG:
     332                encoded_results[0][0] = num_results + 1
     333        else:
     334            # Use a list instead of a class for the run-length encoding since
     335            # we want the serialized form to be concise.
     336            encoded_results.insert(0, [1, item])
     337
     338    def _insert_generic_metadata(self, results_for_builder):
     339        """ Inserts generic metadata (such as version number, current time etc)
     340        into the JSON.
     341
     342        Args:
     343          results_for_builder: Dictionary containing the test results for
     344              a single builder.
     345        """
     346        self._insert_item_into_raw_list(results_for_builder,
     347            self._build_number, self.BUILD_NUMBERS)
     348
     349        # Include SVN revisions for the given repositories.
     350        for (name, path) in self._svn_repositories:
     351            self._insert_item_into_raw_list(results_for_builder,
     352                self._get_svn_revision(path),
     353                name + 'Revision')
     354
     355        self._insert_item_into_raw_list(results_for_builder,
     356            int(time.time()),
     357            self.TIME)
     358
     359    def _insert_test_time_and_result(self, test_name, tests):
     360        """ Insert a test item with its results to the given tests dictionary.
     361
     362        Args:
     363          tests: Dictionary containing test result entries.
     364        """
     365
     366        result = self._get_result_type_char(test_name)
     367        time = self._get_test_timing(test_name)
     368
     369        if test_name not in tests:
     370            tests[test_name] = self._create_results_and_times_json()
     371
     372        thisTest = tests[test_name]
     373        self._insert_item_run_length_encoded(result, thisTest[self.RESULTS])
     374        self._insert_item_run_length_encoded(time, thisTest[self.TIMES])
     375        self._normalize_results_json(thisTest, test_name, tests)
     376
     377    def _convert_json_to_current_version(self, results_json):
     378        """If the JSON does not match the current version, converts it to the
     379        current version and adds in the new version number.
     380        """
     381        if (self.VERSION_KEY in results_json and
     382            results_json[self.VERSION_KEY] == self.VERSION):
     383            return
     384
     385        results_json[self.VERSION_KEY] = self.VERSION
     386
     387    def _create_results_and_times_json(self):
     388        results_and_times = {}
     389        results_and_times[self.RESULTS] = []
     390        results_and_times[self.TIMES] = []
     391        return results_and_times
     392
     393    def _create_results_for_builder_json(self):
     394        results_for_builder = {}
     395        results_for_builder[self.TESTS] = {}
     396        return results_for_builder
     397
     398    def _remove_items_over_max_number_of_builds(self, encoded_list):
     399        """Removes items from the run-length encoded list after the final
     400        item that exceeds the max number of builds to track.
     401
     402        Args:
     403          encoded_results: run-length encoded results. An array of arrays, e.g.
     404              [[3,'A'],[1,'Q']] encodes AAAQ.
     405        """
     406        num_builds = 0
     407        index = 0
     408        for result in encoded_list:
     409            num_builds = num_builds + result[0]
     410            index = index + 1
     411            if num_builds > self.MAX_NUMBER_OF_BUILD_RESULTS_TO_LOG:
     412                return encoded_list[:index]
     413        return encoded_list
     414
     415    def _normalize_results_json(self, test, test_name, tests):
     416        """ Prune tests where all runs pass or tests that no longer exist and
     417        truncate all results to maxNumberOfBuilds.
     418
     419        Args:
     420          test: ResultsAndTimes object for this test.
     421          test_name: Name of the test.
     422          tests: The JSON object with all the test results for this builder.
     423        """
     424        test[self.RESULTS] = self._remove_items_over_max_number_of_builds(
     425            test[self.RESULTS])
     426        test[self.TIMES] = self._remove_items_over_max_number_of_builds(
     427            test[self.TIMES])
     428
     429        is_all_pass = self._is_results_all_of_type(test[self.RESULTS],
     430                                                   self.PASS_RESULT)
     431        is_all_no_data = self._is_results_all_of_type(test[self.RESULTS],
     432            self.NO_DATA_RESULT)
     433        max_time = max([time[1] for time in test[self.TIMES]])
     434
     435        # Remove all passes/no-data from the results to reduce noise and
     436        # filesize. If a test passes every run, but takes > MIN_TIME to run,
     437        # don't throw away the data.
     438        if is_all_no_data or (is_all_pass and max_time <= self.MIN_TIME):
     439            del tests[test_name]
     440
     441    def _is_results_all_of_type(self, results, type):
     442        """Returns whether all the results are of the given type
     443        (e.g. all passes)."""
     444        return len(results) == 1 and results[0][1] == type
     445
     446
     447# A wrapper class for JSONResultsGeneratorBase.
     448# Note: There's a script outside the WebKit codebase calling this script.
     449# FIXME: Please keep the interface until the other script is cleaned up.
     450# (http://src.chromium.org/viewvc/chrome/trunk/src/webkit/tools/layout_tests/webkitpy/layout_tests/test_output_xml_to_json.py?view=markup)
     451class JSONResultsGenerator(JSONResultsGeneratorBase):
     452    # The flag is for backward compatibility.
     453    output_json_in_init = True
    85454
    86455    def __init__(self, port, builder_name, build_name, build_number,
    87456        results_file_base_path, builder_base_url,
    88457        test_timings, failures, passed_tests, skipped_tests, all_tests):
    89         """Modifies the results.json file. Grabs it off the archive directory
    90         if it is not found locally.
     458        """Generates a JSON results file.
    91459
    92460        Args
     
    104472              include skipped tests.
    105473        """
    106         self._builder_name = builder_name
    107         self._build_name = build_name
    108         self._build_number = build_number
    109         self._builder_base_url = builder_base_url
    110         self._results_file_path = os.path.join(results_file_base_path,
    111             self.RESULTS_FILENAME)
    112         self._test_timings = test_timings
    113         self._failures = failures
    114         self._passed_tests = passed_tests
    115         self._skipped_tests = skipped_tests
    116         self._all_tests = all_tests
    117         self._svn_repositories = port.test_repository_paths()
    118 
    119         self._generate_json_output()
    120 
    121     def _generate_json_output(self):
    122         """Generates the JSON output file."""
    123         json = self._get_json()
    124         if json:
    125             results_file = codecs.open(self._results_file_path, "w", "utf-8")
    126             results_file.write(json)
    127             results_file.close()
    128 
    129     # FIXME: Callers should use scm.py instead.
    130     def _get_svn_revision(self, in_directory):
    131         """Returns the svn revision for the given directory.
    132 
    133         Args:
    134           in_directory: The directory where svn is to be run.
    135         """
    136 
    137         if os.path.exists(os.path.join(in_directory, '.svn')):
    138             # Note: Not thread safe: http://bugs.python.org/issue2320
    139             output = subprocess.Popen(["svn", "info", "--xml"],
    140                                       cwd=in_directory,
    141                                       shell=(sys.platform == 'win32'),
    142                                       stdout=subprocess.PIPE).communicate()[0]
    143             try:
    144                 dom = xml.dom.minidom.parseString(output)
    145                 return dom.getElementsByTagName('entry')[0].getAttribute(
    146                     'revision')
    147             except xml.parsers.expat.ExpatError:
    148                 return ""
    149         return ""
    150 
    151     def _get_archived_json_results(self):
    152         """Reads old results JSON file if it exists.
    153         Returns (archived_results, error) tuple where error is None if results
    154         were successfully read.
    155         """
    156         results_json = {}
    157         old_results = None
    158         error = None
    159 
    160         if os.path.exists(self._results_file_path):
    161             with codecs.open(self._results_file_path, "r", "utf-8") as file:
    162                 old_results = file.read()
    163         elif self._builder_base_url:
    164             # Check if we have the archived JSON file on the buildbot server.
    165             results_file_url = (self._builder_base_url +
    166                 self._build_name + "/" + self.RESULTS_FILENAME)
    167             _log.error("Local results.json file does not exist. Grabbing "
    168                        "it off the archive at " + results_file_url)
    169 
    170             try:
    171                 results_file = urllib2.urlopen(results_file_url)
    172                 info = results_file.info()
    173                 old_results = results_file.read()
    174             except urllib2.HTTPError, http_error:
    175                 # A non-4xx status code means the bot is hosed for some reason
    176                 # and we can't grab the results.json file off of it.
    177                 if (http_error.code < 400 and http_error.code >= 500):
    178                     error = http_error
    179             except urllib2.URLError, url_error:
    180                 error = url_error
    181 
    182         if old_results:
    183             # Strip the prefix and suffix so we can get the actual JSON object.
    184             old_results = old_results[len(self.JSON_PREFIX):
    185                                       len(old_results) - len(self.JSON_SUFFIX)]
    186 
    187             try:
    188                 results_json = simplejson.loads(old_results)
    189             except:
    190                 _log.debug("results.json was not valid JSON. Clobbering.")
    191                 # The JSON file is not valid JSON. Just clobber the results.
    192                 results_json = {}
    193         else:
    194             _log.debug('Old JSON results do not exist. Starting fresh.')
    195             results_json = {}
    196 
    197         return results_json, error
    198 
    199     def _get_json(self):
    200         """Gets the results for the results.json file."""
    201         results_json, error = self._get_archived_json_results()
    202         if error:
    203             # If there was an error don't write a results.json
    204             # file at all as it would lose all the information on the bot.
    205             _log.error("Archive directory is inaccessible. Not modifying "
    206                        "or clobbering the results.json file: " + str(error))
    207             return None
    208 
    209         builder_name = self._builder_name
    210         if results_json and builder_name not in results_json:
    211             _log.debug("Builder name (%s) is not in the results.json file."
    212                        % builder_name)
    213 
    214         self._convert_json_to_current_version(results_json)
    215 
    216         if builder_name not in results_json:
    217             results_json[builder_name] = (
    218                 self._create_results_for_builder_json())
    219 
    220         results_for_builder = results_json[builder_name]
    221 
    222         self._insert_generic_metadata(results_for_builder)
    223 
    224         self._insert_failure_summaries(results_for_builder)
    225 
    226         # Update the all failing tests with result type and time.
    227         tests = results_for_builder[self.TESTS]
    228         all_failing_tests = set(self._failures.iterkeys())
    229         all_failing_tests.update(tests.iterkeys())
    230         for test in all_failing_tests:
    231             self._insert_test_time_and_result(test, tests)
    232 
    233         # Specify separators in order to get compact encoding.
    234         results_str = simplejson.dumps(results_json, separators=(',', ':'))
    235         return self.JSON_PREFIX + results_str + self.JSON_SUFFIX
    236 
    237     def _insert_failure_summaries(self, results_for_builder):
    238         """Inserts aggregate pass/failure statistics into the JSON.
    239         This method reads self._skipped_tests, self._passed_tests and
    240         self._failures and inserts FIXABLE, FIXABLE_COUNT and ALL_FIXABLE_COUNT
    241         entries.
    242 
    243         Args:
    244           results_for_builder: Dictionary containing the test results for a
    245               single builder.
    246         """
    247         # Insert the number of tests that failed.
    248         self._insert_item_into_raw_list(results_for_builder,
    249             len(set(self._failures.keys()) | self._skipped_tests),
    250             self.FIXABLE_COUNT)
    251 
    252         # Create a pass/skip/failure summary dictionary.
    253         entry = {}
    254         entry[self.SKIP_RESULT] = len(self._skipped_tests)
    255         entry[self.PASS_RESULT] = len(self._passed_tests)
    256         get = entry.get
    257         for failure_type in self._failures.values():
    258             failure_char = self.FAILURE_TO_CHAR[failure_type]
    259             entry[failure_char] = get(failure_char, 0) + 1
    260 
    261         # Insert the pass/skip/failure summary dictionary.
    262         self._insert_item_into_raw_list(results_for_builder, entry,
    263                                         self.FIXABLE)
    264 
    265         # Insert the number of all the tests that are supposed to pass.
    266         self._insert_item_into_raw_list(results_for_builder,
    267             len(self._skipped_tests | self._all_tests),
    268             self.ALL_FIXABLE_COUNT)
    269 
    270     def _insert_item_into_raw_list(self, results_for_builder, item, key):
    271         """Inserts the item into the list with the given key in the results for
    272         this builder. Creates the list if no such list exists.
    273 
    274         Args:
    275           results_for_builder: Dictionary containing the test results for a
    276               single builder.
    277           item: Number or string to insert into the list.
    278           key: Key in results_for_builder for the list to insert into.
    279         """
    280         if key in results_for_builder:
    281             raw_list = results_for_builder[key]
    282         else:
    283             raw_list = []
    284 
    285         raw_list.insert(0, item)
    286         raw_list = raw_list[:self.MAX_NUMBER_OF_BUILD_RESULTS_TO_LOG]
    287         results_for_builder[key] = raw_list
    288 
    289     def _insert_item_run_length_encoded(self, item, encoded_results):
    290         """Inserts the item into the run-length encoded results.
    291 
    292         Args:
    293           item: String or number to insert.
    294           encoded_results: run-length encoded results. An array of arrays, e.g.
    295               [[3,'A'],[1,'Q']] encodes AAAQ.
    296         """
    297         if len(encoded_results) and item == encoded_results[0][1]:
    298             num_results = encoded_results[0][0]
    299             if num_results <= self.MAX_NUMBER_OF_BUILD_RESULTS_TO_LOG:
    300                 encoded_results[0][0] = num_results + 1
    301         else:
    302             # Use a list instead of a class for the run-length encoding since
    303             # we want the serialized form to be concise.
    304             encoded_results.insert(0, [1, item])
    305 
    306     def _insert_generic_metadata(self, results_for_builder):
    307         """ Inserts generic metadata (such as version number, current time etc)
    308         into the JSON.
    309 
    310         Args:
    311           results_for_builder: Dictionary containing the test results for
    312               a single builder.
    313         """
    314         self._insert_item_into_raw_list(results_for_builder,
    315             self._build_number, self.BUILD_NUMBERS)
    316 
    317         # Include SVN revisions for the given repositories.
    318         for (name, path) in self._svn_repositories:
    319             self._insert_item_into_raw_list(results_for_builder,
    320                 self._get_svn_revision(path),
    321                 name + 'Revision')
    322 
    323         self._insert_item_into_raw_list(results_for_builder,
    324             int(time.time()),
    325             self.TIME)
    326 
    327     def _insert_test_time_and_result(self, test_name, tests):
    328         """ Insert a test item with its results to the given tests dictionary.
    329 
    330         Args:
    331           tests: Dictionary containing test result entries.
    332         """
    333 
    334         result = JSONResultsGenerator.PASS_RESULT
    335         time = 0
    336 
    337         if test_name not in self._all_tests:
    338             result = JSONResultsGenerator.NO_DATA_RESULT
    339 
    340         if test_name in self._failures:
    341             result = self.FAILURE_TO_CHAR[self._failures[test_name]]
    342 
    343         if test_name in self._test_timings:
    344             # Floor for now to get time in seconds.
    345             time = int(self._test_timings[test_name])
    346 
    347         if test_name not in tests:
    348             tests[test_name] = self._create_results_and_times_json()
    349 
    350         thisTest = tests[test_name]
    351         self._insert_item_run_length_encoded(result, thisTest[self.RESULTS])
    352         self._insert_item_run_length_encoded(time, thisTest[self.TIMES])
    353         self._normalize_results_json(thisTest, test_name, tests)
    354 
    355     def _convert_json_to_current_version(self, results_json):
    356         """If the JSON does not match the current version, converts it to the
    357         current version and adds in the new version number.
    358         """
    359         if (self.VERSION_KEY in results_json and
    360             results_json[self.VERSION_KEY] == self.VERSION):
    361             return
    362 
    363         results_json[self.VERSION_KEY] = self.VERSION
    364 
    365     def _create_results_and_times_json(self):
    366         results_and_times = {}
    367         results_and_times[self.RESULTS] = []
    368         results_and_times[self.TIMES] = []
    369         return results_and_times
    370 
    371     def _create_results_for_builder_json(self):
    372         results_for_builder = {}
    373         results_for_builder[self.TESTS] = {}
    374         return results_for_builder
    375 
    376     def _remove_items_over_max_number_of_builds(self, encoded_list):
    377         """Removes items from the run-length encoded list after the final
    378         item that exceeds the max number of builds to track.
    379 
    380         Args:
    381           encoded_results: run-length encoded results. An array of arrays, e.g.
    382               [[3,'A'],[1,'Q']] encodes AAAQ.
    383         """
    384         num_builds = 0
    385         index = 0
    386         for result in encoded_list:
    387             num_builds = num_builds + result[0]
    388             index = index + 1
    389             if num_builds > self.MAX_NUMBER_OF_BUILD_RESULTS_TO_LOG:
    390                 return encoded_list[:index]
    391         return encoded_list
    392 
    393     def _normalize_results_json(self, test, test_name, tests):
    394         """ Prune tests where all runs pass or tests that no longer exist and
    395         truncate all results to maxNumberOfBuilds.
    396 
    397         Args:
    398           test: ResultsAndTimes object for this test.
    399           test_name: Name of the test.
    400           tests: The JSON object with all the test results for this builder.
    401         """
    402         test[self.RESULTS] = self._remove_items_over_max_number_of_builds(
    403             test[self.RESULTS])
    404         test[self.TIMES] = self._remove_items_over_max_number_of_builds(
    405             test[self.TIMES])
    406 
    407         is_all_pass = self._is_results_all_of_type(test[self.RESULTS],
    408                                                    self.PASS_RESULT)
    409         is_all_no_data = self._is_results_all_of_type(test[self.RESULTS],
    410             self.NO_DATA_RESULT)
    411         max_time = max([time[1] for time in test[self.TIMES]])
    412 
    413         # Remove all passes/no-data from the results to reduce noise and
    414         # filesize. If a test passes every run, but takes > MIN_TIME to run,
    415         # don't throw away the data.
    416         if is_all_no_data or (is_all_pass and max_time <= self.MIN_TIME):
    417             del tests[test_name]
    418 
    419     def _is_results_all_of_type(self, results, type):
    420         """Returns whether all the results are of the given type
    421         (e.g. all passes)."""
    422         return len(results) == 1 and results[0][1] == type
     474
     475        # Create a map of (name, TestResult).
     476        test_results_map = dict()
     477        get = test_results_map.get
     478        for (test, time) in test_timings.iteritems():
     479            test_results_map[test] = TestResult(test, elapsed_time=time)
     480        for test in failures.iterkeys():
     481            test_results_map[test] = test_result = get(test, TestResult(test))
     482            test_result.failed = True
     483        for test in skipped_tests:
     484            test_results_map[test] = test_result = get(test, TestResult(test))
     485            test_result.skipped = True
     486        for test in passed_tests:
     487            test_results_map[test] = test_result = get(test, TestResult(test))
     488            test_result.failed = False
     489            test_result.skipped = False
     490        for test in all_tests:
     491            if test not in test_results_map:
     492                test_results_map[test] = TestResult(test)
     493
     494        super(JSONResultsGenerator, self).__init__(
     495            builder_name, build_name, build_number,
     496            results_file_base_path, builder_base_url, test_results_map,
     497            svn_repositories=port.test_repository_paths())
     498
     499        if self.__class__.output_json_in_init:
     500            self.generate_json_output()
Note: See TracChangeset for help on using the changeset viewer.