Index: infra/scripts/legacy/scripts/slave/gtest/json_results_generator.py |
diff --git a/infra/scripts/legacy/scripts/slave/gtest/json_results_generator.py b/infra/scripts/legacy/scripts/slave/gtest/json_results_generator.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..cf207b0adeab0743299855491c3c4e26ca11ac49 |
--- /dev/null |
+++ b/infra/scripts/legacy/scripts/slave/gtest/json_results_generator.py |
@@ -0,0 +1,255 @@ |
+# Copyright (c) 2012 The Chromium Authors. All rights reserved. |
+# Use of this source code is governed by a BSD-style license that can be |
+# found in the LICENSE file. |
+ |
+"""A utility class to generate JSON results from given test results and upload |
+them to the specified results server. |
+ |
+""" |
+ |
+from __future__ import with_statement |
+ |
+import codecs |
+import logging |
+import os |
+import time |
+ |
+import simplejson |
+from slave.gtest.test_result import TestResult |
+from slave.gtest.test_results_uploader import TestResultsUploader |
+ |
+# A JSON results generator for generic tests. |
+ |
+JSON_PREFIX = 'ADD_RESULTS(' |
+JSON_SUFFIX = ');' |
+ |
+ |
+def test_did_pass(test_result): |
+ return not test_result.failed and test_result.modifier == TestResult.NONE |
+ |
+ |
+def add_path_to_trie(path, value, trie): |
+ """Inserts a single flat directory path and associated value into a directory |
+ trie structure.""" |
+ if not '/' in path: |
+ trie[path] = value |
+ return |
+ |
+ # we don't use slash |
+ # pylint: disable=W0612 |
+ directory, slash, rest = path.partition('/') |
+ if not directory in trie: |
+ trie[directory] = {} |
+ add_path_to_trie(rest, value, trie[directory]) |
+ |
+ |
+def generate_test_timings_trie(individual_test_timings): |
+ """Breaks a test name into chunks by directory and puts the test time as a |
+ value in the lowest part, e.g. |
+ foo/bar/baz.html: 1ms |
+ foo/bar/baz1.html: 3ms |
+ |
+ becomes |
+ foo: { |
+ bar: { |
+ baz.html: 1, |
+ baz1.html: 3 |
+ } |
+ } |
+ """ |
+ trie = {} |
+ # Only use the timing of the first try of each test. |
+ for test_results in individual_test_timings: |
+ test = test_results[0].test_name |
+ |
+ add_path_to_trie(test, int(1000 * test_results[-1].test_run_time), trie) |
+ |
+ return trie |
+ |
+ |
+class JSONResultsGenerator(object): |
+ """A JSON results generator for generic tests.""" |
+ |
+ FAIL_LABEL = 'FAIL' |
+ PASS_LABEL = 'PASS' |
+ FLAKY_LABEL = ' '.join([FAIL_LABEL, PASS_LABEL]) |
+ SKIP_LABEL = 'SKIP' |
+ |
+ ACTUAL = 'actual' |
+ BLINK_REVISION = 'blink_revision' |
+ BUILD_NUMBER = 'build_number' |
+ BUILDER_NAME = 'builder_name' |
+ CHROMIUM_REVISION = 'chromium_revision' |
+ EXPECTED = 'expected' |
+ FAILURE_SUMMARY = 'num_failures_by_type' |
+ SECONDS_SINCE_EPOCH = 'seconds_since_epoch' |
+ TEST_TIME = 'time' |
+ TESTS = 'tests' |
+ VERSION = 'version' |
+ VERSION_NUMBER = 3 |
+ |
+ RESULTS_FILENAME = 'results.json' |
+ TIMES_MS_FILENAME = 'times_ms.json' |
+ FULL_RESULTS_FILENAME = 'full_results.json' |
+ |
+ def __init__(self, builder_name, build_name, build_number, |
+ results_file_base_path, builder_base_url, |
+ test_results_map, svn_revisions=None, |
+ test_results_server=None, |
+ test_type='', |
+ master_name='', |
+ file_writer=None): |
+ """Modifies the results.json file. Grabs it off the archive directory |
+ if it is not found locally. |
+ |
+ Args |
+ builder_name: the builder name (e.g. Webkit). |
+ build_name: the build name (e.g. webkit-rel). |
+ build_number: the build number. |
+ results_file_base_path: Absolute path to the directory containing the |
+ results json file. |
+ builder_base_url: the URL where we have the archived test results. |
+ If this is None no archived results will be retrieved. |
+ test_results_map: A dictionary that maps test_name to a list of |
+ TestResult, one for each time the test was retried. |
+ svn_revisions: A (json_field_name, revision) pair for SVN |
+ repositories that tests rely on. The SVN revision will be |
+ included in the JSON with the given json_field_name. |
+ test_results_server: server that hosts test results json. |
+ test_type: test type string (e.g. 'layout-tests'). |
+ master_name: the name of the buildbot master. |
+ file_writer: if given the parameter is used to write JSON data to a file. |
+ The parameter must be the function that takes two arguments, 'file_path' |
+ and 'data' to be written into the file_path. |
+ """ |
+ self._builder_name = builder_name |
+ self._build_name = build_name |
+ self._build_number = build_number |
+ self._builder_base_url = builder_base_url |
+ self._results_directory = results_file_base_path |
+ |
+ self._test_results_map = test_results_map |
+ |
+ self._svn_revisions = svn_revisions |
+ if not self._svn_revisions: |
+ self._svn_revisions = {} |
+ |
+ self._test_results_server = test_results_server |
+ self._test_type = test_type |
+ self._master_name = master_name |
+ self._file_writer = file_writer |
+ |
+ def generate_json_output(self): |
+ json = self.get_full_results_json() |
+ if json: |
+ file_path = os.path.join(self._results_directory, |
+ self.FULL_RESULTS_FILENAME) |
+ self._write_json(json, file_path) |
+ |
+ def generate_times_ms_file(self): |
+ times = generate_test_timings_trie(self._test_results_map.values()) |
+ file_path = os.path.join(self._results_directory, self.TIMES_MS_FILENAME) |
+ self._write_json(times, file_path) |
+ |
+ def get_full_results_json(self): |
+ results = {self.VERSION: self.VERSION_NUMBER} |
+ |
+ # Metadata generic to all results. |
+ results[self.BUILDER_NAME] = self._builder_name |
+ results[self.BUILD_NUMBER] = self._build_number |
+ results[self.SECONDS_SINCE_EPOCH] = int(time.time()) |
+ for name, revision in self._svn_revisions: |
+ results[name + '_revision'] = revision |
+ |
+ tests = results.setdefault(self.TESTS, {}) |
+ for test_name in self._test_results_map.iterkeys(): |
+ tests[test_name] = self._make_test_data(test_name) |
+ |
+ self._insert_failure_map(results) |
+ |
+ return results |
+ |
+ def _insert_failure_map(self, results): |
+ # FAIL, PASS, NOTRUN |
+ summary = {self.PASS_LABEL: 0, self.FAIL_LABEL: 0, self.SKIP_LABEL: 0} |
+ for test_results in self._test_results_map.itervalues(): |
+ # Use the result of the first test for aggregate statistics. This may |
+ # count as failing a test that passed on retry, but it's a more useful |
+ # statistic and it's consistent with our other test harnesses. |
+ test_result = test_results[0] |
+ if test_did_pass(test_result): |
+ summary[self.PASS_LABEL] += 1 |
+ elif test_result.modifier == TestResult.DISABLED: |
+ summary[self.SKIP_LABEL] += 1 |
+ elif test_result.failed: |
+ summary[self.FAIL_LABEL] += 1 |
+ |
+ results[self.FAILURE_SUMMARY] = summary |
+ |
+ def _make_test_data(self, test_name): |
+ test_data = {} |
+ expected, actual = self._get_expected_and_actual_results(test_name) |
+ test_data[self.EXPECTED] = expected |
+ test_data[self.ACTUAL] = actual |
+ # Use the timing of the first try, it's a better representative since it |
+ # runs under more load than retries. |
+ run_time = int(self._test_results_map[test_name][0].test_run_time) |
+ test_data[self.TEST_TIME] = run_time |
+ |
+ return test_data |
+ |
+ def _get_expected_and_actual_results(self, test_name): |
+ test_results = self._test_results_map[test_name] |
+ # Use the modifier of the first try, they should all be the same. |
+ modifier = test_results[0].modifier |
+ |
+ if modifier == TestResult.DISABLED: |
+ return (self.SKIP_LABEL, self.SKIP_LABEL) |
+ |
+ actual_list = [] |
+ for test_result in test_results: |
+ label = self.FAIL_LABEL if test_result.failed else self.PASS_LABEL |
+ actual_list.append(label) |
+ actual = " ".join(actual_list) |
+ |
+ if modifier == TestResult.NONE: |
+ return (self.PASS_LABEL, actual) |
+ if modifier == TestResult.FLAKY: |
+ return (self.FLAKY_LABEL, actual) |
+ if modifier == TestResult.FAILS: |
+ return (self.FAIL_LABEL, actual) |
+ |
+ def upload_json_files(self, json_files): |
+ """Uploads the given json_files to the test_results_server (if the |
+ test_results_server is given).""" |
+ if not self._test_results_server: |
+ return |
+ |
+ if not self._master_name: |
+ logging.error('--test-results-server was set, but --master-name was not. ' |
+ 'Not uploading JSON files.') |
+ return |
+ |
+ print 'Uploading JSON files for builder: %s' % self._builder_name |
+ attrs = [('builder', self._builder_name), |
+ ('testtype', self._test_type), |
+ ('master', self._master_name)] |
+ |
+ files = [(f, os.path.join(self._results_directory, f)) for f in json_files] |
+ |
+ uploader = TestResultsUploader(self._test_results_server) |
+ # Set uploading timeout in case appengine server is having problem. |
+ # 120 seconds are more than enough to upload test results. |
+ uploader.upload(attrs, files, 120) |
+ |
+ print 'JSON files uploaded.' |
+ |
+ def _write_json(self, json_object, file_path): |
+ # Specify separators in order to get compact encoding. |
+ json_data = simplejson.dumps(json_object, separators=(',', ':')) |
+ json_string = json_data |
+ if self._file_writer: |
+ self._file_writer(file_path, json_string) |
+ else: |
+ with codecs.open(file_path, 'w', 'utf8') as f: |
+ f.write(json_string) |