1 # Copyright 2014 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
11 # TODO(dpranke): This code is largely cloned from, and redundant with,
12 # src/mojo/tools/run_mojo_python_tests.py, and also duplicates logic
13 # in test-webkitpy and run-webkit-tests. We should consolidate the
14 # python TestResult parsing/converting/uploading code as much as possible.
17 def AddOptions(parser):
18 parser.add_option('--metadata', action='append', default=[],
19 help=('optional key=value metadata that will be stored '
20 'in the results files (can be used for revision '
22 parser.add_option('--write-full-results-to', metavar='FILENAME',
24 help='The path to write the list of full results to.')
25 parser.add_option('--builder-name',
26 help='The name of the builder as shown on the waterfall.')
27 parser.add_option('--master-name',
28 help='The name of the buildbot master.')
29 parser.add_option("--test-results-server", default="",
30 help=('If specified, upload full_results.json file to '
32 parser.add_option('--test-type',
33 help=('Name of test type / step on the waterfall '
34 '(e.g., "telemetry_unittests").'))
37 def ValidateArgs(parser, args):
38 for val in args.metadata:
40 parser.error('Error: malformed metadata "%s"' % val)
42 if (args.test_results_server and
43 (not args.builder_name or not args.master_name or not args.test_type)):
44 parser.error('Error: --builder-name, --master-name, and --test-type '
45 'must be specified along with --test-result-server.')
48 def WriteFullResultsIfNecessary(args, full_results):
49 if not args.write_full_results_to:
52 with open(args.write_full_results_to, 'w') as fp:
53 json.dump(full_results, fp, indent=2)
57 def UploadFullResultsIfNecessary(args, _full_results):
58 if not args.test_results_server:
61 # TODO(dpranke) crbug.com/403663 disable this temporarily.
64 #url = 'http://%s/testfile/upload' % args.test_results_server
65 #attrs = [('builder', args.builder_name),
66 # ('master', args.master_name),
67 # ('testtype', args.test_type)]
68 #content_type, data = _EncodeMultiPartFormData(attrs, full_results)
69 #return _UploadData(url, data, content_type)
75 def FullResults(args, suite, results):
76 """Convert the unittest results to the Chromium JSON test result format.
78 This matches run-webkit-tests (the layout tests) and the flakiness dashboard.
82 full_results['interrupted'] = False
83 full_results['path_delimiter'] = TEST_SEPARATOR
84 full_results['version'] = 3
85 full_results['seconds_since_epoch'] = time.time()
86 full_results['builder_name'] = args.builder_name or ''
87 for md in args.metadata:
88 key, val = md.split('=', 1)
89 full_results[key] = val
91 # TODO(dpranke): Handle skipped tests as well.
93 all_test_names = AllTestNames(suite)
94 num_failures = NumFailuresAfterRetries(results)
95 full_results['num_failures_by_type'] = {
97 'PASS': len(all_test_names) - num_failures,
100 sets_of_passing_test_names = map(PassingTestNames, results)
101 sets_of_failing_test_names = map(FailedTestNames, results)
103 full_results['tests'] = {}
105 for test_name in all_test_names:
108 'actual': ActualResultsForTest(test_name, sets_of_failing_test_names,
109 sets_of_passing_test_names)
111 if value['actual'].endswith('FAIL'):
112 value['is_unexpected'] = True
113 _AddPathToTrie(full_results['tests'], test_name, value)
118 def ActualResultsForTest(test_name, sets_of_failing_test_names,
119 sets_of_passing_test_names):
121 for retry_num in range(len(sets_of_failing_test_names)):
122 if test_name in sets_of_failing_test_names[retry_num]:
123 actuals.append('FAIL')
124 elif test_name in sets_of_passing_test_names[retry_num]:
125 assert ((retry_num == 0) or
126 (test_name in sets_of_failing_test_names[retry_num - 1])), (
127 'We should not have run a test that did not fail '
128 'on the previous run.')
129 actuals.append('PASS')
131 assert actuals, 'We did not find any result data for %s.' % test_name
132 return ' '.join(actuals)
135 def ExitCodeFromFullResults(full_results):
136 return 1 if full_results['num_failures_by_type']['FAIL'] else 0
139 def AllTestNames(suite):
141 # _tests is protected pylint: disable=W0212
142 for test in suite._tests:
143 if isinstance(test, unittest.suite.TestSuite):
144 test_names.extend(AllTestNames(test))
146 test_names.append(test.id())
150 def NumFailuresAfterRetries(results):
151 return len(FailedTestNames(results[-1]))
154 def FailedTestNames(result):
155 return set(test.id() for test, _ in result.failures + result.errors)
158 def PassingTestNames(result):
159 return set(test.id() for test in result.successes)
162 def _AddPathToTrie(trie, path, value):
163 if TEST_SEPARATOR not in path:
166 directory, rest = path.split(TEST_SEPARATOR, 1)
167 if directory not in trie:
169 _AddPathToTrie(trie[directory], rest, value)
172 def _EncodeMultiPartFormData(attrs, full_results):
173 # Cloned from webkitpy/common/net/file_uploader.py
174 BOUNDARY = '-M-A-G-I-C---B-O-U-N-D-A-R-Y-'
178 for key, value in attrs:
179 lines.append('--' + BOUNDARY)
180 lines.append('Content-Disposition: form-data; name="%s"' % key)
184 lines.append('--' + BOUNDARY)
185 lines.append('Content-Disposition: form-data; name="file"; '
186 'filename="full_results.json"')
187 lines.append('Content-Type: application/json')
189 lines.append(json.dumps(full_results))
191 lines.append('--' + BOUNDARY + '--')
193 body = CRLF.join(lines)
194 content_type = 'multipart/form-data; boundary=%s' % BOUNDARY
195 return content_type, body
198 def _UploadData(url, data, content_type):
199 request = urllib2.Request(url, data, {'Content-Type': content_type})
201 response = urllib2.urlopen(request)
202 if response.code == 200:
204 return True, ('Uploading the JSON results failed with %d: "%s"' %
205 (response.code, response.read()))
206 except Exception as e:
207 return True, 'Uploading the JSON results raised "%s"\n' % str(e)