1# Copyright 2014 Google Inc. All rights reserved. 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15from collections import OrderedDict 16 17import json 18 19 20class ResultType(object): 21 Pass = 'Pass' 22 Failure = 'Failure' 23 ImageOnlyFailure = 'ImageOnlyFailure' 24 Timeout = 'Timeout' 25 Crash = 'Crash' 26 Skip = 'Skip' 27 28 values = (Pass, Failure, ImageOnlyFailure, Timeout, Crash, Skip) 29 30 31class Result(object): 32 # too many instance attributes pylint: disable=R0902 33 # too many arguments pylint: disable=R0913 34 35 def __init__(self, name, actual, started, took, worker, 36 expected=None, unexpected=False, 37 flaky=False, code=0, out='', err='', pid=0): 38 self.name = name 39 self.actual = actual 40 self.started = started 41 self.took = took 42 self.worker = worker 43 self.expected = expected or [ResultType.Pass] 44 self.unexpected = unexpected 45 self.flaky = flaky 46 self.code = code 47 self.out = out 48 self.err = err 49 self.pid = pid 50 51 52class ResultSet(object): 53 54 def __init__(self): 55 self.results = [] 56 57 def add(self, result): 58 self.results.append(result) 59 60 61TEST_SEPARATOR = '.' 62 63 64def make_full_results(metadata, seconds_since_epoch, all_test_names, results): 65 """Convert the typ results to the Chromium JSON test result format. 66 67 See http://www.chromium.org/developers/the-json-test-results-format 68 """ 69 70 # We use OrderedDicts here so that the output is stable. 71 full_results = OrderedDict() 72 full_results['version'] = 3 73 full_results['interrupted'] = False 74 full_results['path_delimiter'] = TEST_SEPARATOR 75 full_results['seconds_since_epoch'] = seconds_since_epoch 76 77 for md in metadata: 78 key, val = md.split('=', 1) 79 full_results[key] = val 80 81 passing_tests = _passing_test_names(results) 82 failed_tests = failed_test_names(results) 83 skipped_tests = set(all_test_names) - passing_tests - failed_tests 84 85 full_results['num_failures_by_type'] = OrderedDict() 86 full_results['num_failures_by_type']['FAIL'] = len(failed_tests) 87 full_results['num_failures_by_type']['PASS'] = len(passing_tests) 88 full_results['num_failures_by_type']['SKIP'] = len(skipped_tests) 89 90 full_results['tests'] = OrderedDict() 91 92 for test_name in all_test_names: 93 value = OrderedDict() 94 if test_name in skipped_tests: 95 value['expected'] = 'SKIP' 96 value['actual'] = 'SKIP' 97 else: 98 value['expected'] = 'PASS' 99 value['actual'] = _actual_results_for_test(test_name, results) 100 if value['actual'].endswith('FAIL'): 101 value['is_unexpected'] = True 102 _add_path_to_trie(full_results['tests'], test_name, value) 103 104 return full_results 105 106 107def make_upload_request(test_results_server, builder, master, testtype, 108 full_results): 109 url = 'http://%s/testfile/upload' % test_results_server 110 attrs = [('builder', builder), 111 ('master', master), 112 ('testtype', testtype)] 113 content_type, data = _encode_multipart_form_data(attrs, full_results) 114 return url, content_type, data 115 116 117def exit_code_from_full_results(full_results): 118 return 1 if num_failures(full_results) else 0 119 120 121def num_failures(full_results): 122 return full_results['num_failures_by_type']['FAIL'] 123 124 125def failed_test_names(results): 126 names = set() 127 for r in results.results: 128 if r.actual == ResultType.Failure: 129 names.add(r.name) 130 elif r.actual == ResultType.Pass and r.name in names: 131 names.remove(r.name) 132 return names 133 134 135def _passing_test_names(results): 136 return set(r.name for r in results.results if r.actual == ResultType.Pass) 137 138 139def _actual_results_for_test(test_name, results): 140 actuals = [] 141 for r in results.results: 142 if r.name == test_name: 143 if r.actual == ResultType.Failure: 144 actuals.append('FAIL') 145 elif r.actual == ResultType.Pass: 146 actuals.append('PASS') 147 148 assert actuals, 'We did not find any result data for %s.' % test_name 149 return ' '.join(actuals) 150 151 152def _add_path_to_trie(trie, path, value): 153 if TEST_SEPARATOR not in path: 154 trie[path] = value 155 return 156 directory, rest = path.split(TEST_SEPARATOR, 1) 157 if directory not in trie: 158 trie[directory] = {} 159 _add_path_to_trie(trie[directory], rest, value) 160 161 162def _encode_multipart_form_data(attrs, test_results): 163 # Cloned from webkitpy/common/net/file_uploader.py 164 BOUNDARY = '-J-S-O-N-R-E-S-U-L-T-S---B-O-U-N-D-A-R-Y-' 165 CRLF = '\r\n' 166 lines = [] 167 168 for key, value in attrs: 169 lines.append('--' + BOUNDARY) 170 lines.append('Content-Disposition: form-data; name="%s"' % key) 171 lines.append('') 172 lines.append(value) 173 174 lines.append('--' + BOUNDARY) 175 lines.append('Content-Disposition: form-data; name="file"; ' 176 'filename="full_results.json"') 177 lines.append('Content-Type: application/json') 178 lines.append('') 179 lines.append(json.dumps(test_results)) 180 181 lines.append('--' + BOUNDARY + '--') 182 lines.append('') 183 body = CRLF.join(lines) 184 content_type = 'multipart/form-data; boundary=%s' % BOUNDARY 185 return content_type, body 186