Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(407)

Side by Side Diff: webkit/tools/layout_tests/layout_package/json_results_generator.py

Issue 545145: Move the layout test scripts into a 'webkitpy' subdirectory in preparation... (Closed) Base URL: svn://chrome-svn/chrome/trunk/src/
Patch Set: try to de-confuse svn and the try bots Created 10 years, 11 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(Empty)
1 # Copyright (c) 2009 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.
4
5 import logging
6 import os
7 import subprocess
8 import sys
9 import time
10 import urllib2
11 import xml.dom.minidom
12
13 from layout_package import path_utils
14 from layout_package import test_expectations
15
16 sys.path.append(path_utils.PathFromBase('third_party'))
17 import simplejson
18
19
20 class JSONResultsGenerator(object):
21
22 MAX_NUMBER_OF_BUILD_RESULTS_TO_LOG = 750
23 # Min time (seconds) that will be added to the JSON.
24 MIN_TIME = 1
25 JSON_PREFIX = "ADD_RESULTS("
26 JSON_SUFFIX = ");"
27 PASS_RESULT = "P"
28 SKIP_RESULT = "X"
29 NO_DATA_RESULT = "N"
30 VERSION = 3
31 VERSION_KEY = "version"
32 RESULTS = "results"
33 TIMES = "times"
34 BUILD_NUMBERS = "buildNumbers"
35 WEBKIT_SVN = "webkitRevision"
36 CHROME_SVN = "chromeRevision"
37 TIME = "secondsSinceEpoch"
38 TESTS = "tests"
39
40 FIXABLE_COUNT = "fixableCount"
41 FIXABLE = "fixableCounts"
42 ALL_FIXABLE_COUNT = "allFixableCount"
43
44 # Note that we omit test_expectations.FAIL from this list because
45 # it should never show up (it's a legacy input expectation, never
46 # an output expectation).
47 FAILURE_TO_CHAR = {test_expectations.CRASH: "C",
48 test_expectations.TIMEOUT: "T",
49 test_expectations.IMAGE: "I",
50 test_expectations.TEXT: "F",
51 test_expectations.MISSING: "O",
52 test_expectations.IMAGE_PLUS_TEXT: "Z"}
53 FAILURE_CHARS = FAILURE_TO_CHAR.values()
54
55 RESULTS_FILENAME = "results.json"
56
57 def __init__(self, builder_name, build_name, build_number,
58 results_file_base_path, builder_base_url,
59 test_timings, failures, passed_tests, skipped_tests, all_tests):
60 """Modifies the results.json file. Grabs it off the archive directory
61 if it is not found locally.
62
63 Args
64 builder_name: the builder name (e.g. Webkit).
65 build_name: the build name (e.g. webkit-rel).
66 build_number: the build number.
67 results_file_base_path: Absolute path to the directory containing the
68 results json file.
69 builder_base_url: the URL where we have the archived test results.
70 test_timings: Map of test name to a test_run-time.
71 failures: Map of test name to a failure type (of test_expectations).
72 passed_tests: A set containing all the passed tests.
73 skipped_tests: A set containing all the skipped tests.
74 all_tests: List of all the tests that were run. This should not
75 include skipped tests.
76 """
77 self._builder_name = builder_name
78 self._build_name = build_name
79 self._build_number = build_number
80 self._builder_base_url = builder_base_url
81 self._results_file_path = os.path.join(results_file_base_path,
82 self.RESULTS_FILENAME)
83 self._test_timings = test_timings
84 self._failures = failures
85 self._passed_tests = passed_tests
86 self._skipped_tests = skipped_tests
87 self._all_tests = all_tests
88
89 self._GenerateJSONOutput()
90
91 def _GenerateJSONOutput(self):
92 """Generates the JSON output file."""
93 json = self._GetJSON()
94 if json:
95 results_file = open(self._results_file_path, "w")
96 results_file.write(json)
97 results_file.close()
98
99 def _GetSVNRevision(self, in_directory=None):
100 """Returns the svn revision for the given directory.
101
102 Args:
103 in_directory: The directory where svn is to be run.
104 """
105 output = subprocess.Popen(["svn", "info", "--xml"],
106 cwd=in_directory,
107 shell=(sys.platform == 'win32'),
108 stdout=subprocess.PIPE).communicate()[0]
109 try:
110 dom = xml.dom.minidom.parseString(output)
111 return dom.getElementsByTagName('entry')[0].getAttribute(
112 'revision')
113 except xml.parsers.expat.ExpatError:
114 return ""
115
116 def _GetArchivedJSONResults(self):
117 """Reads old results JSON file if it exists.
118 Returns (archived_results, error) tuple where error is None if results
119 were successfully read.
120 """
121 results_json = {}
122 old_results = None
123 error = None
124
125 if os.path.exists(self._results_file_path):
126 old_results_file = open(self._results_file_path, "r")
127 old_results = old_results_file.read()
128 elif self._builder_base_url:
129 # Check if we have the archived JSON file on the buildbot server.
130 results_file_url = (self._builder_base_url +
131 self._build_name + "/" + self.RESULTS_FILENAME)
132 logging.error("Local results.json file does not exist. Grabbing "
133 "it off the archive at " + results_file_url)
134
135 try:
136 results_file = urllib2.urlopen(results_file_url)
137 info = results_file.info()
138 old_results = results_file.read()
139 except urllib2.HTTPError, http_error:
140 # A non-4xx status code means the bot is hosed for some reason
141 # and we can't grab the results.json file off of it.
142 if (http_error.code < 400 and http_error.code >= 500):
143 error = http_error
144 except urllib2.URLError, url_error:
145 error = url_error
146
147 if old_results:
148 # Strip the prefix and suffix so we can get the actual JSON object.
149 old_results = old_results[len(self.JSON_PREFIX):
150 len(old_results) - len(self.JSON_SUFFIX)]
151
152 try:
153 results_json = simplejson.loads(old_results)
154 except:
155 logging.debug("results.json was not valid JSON. Clobbering.")
156 # The JSON file is not valid JSON. Just clobber the results.
157 results_json = {}
158 else:
159 logging.debug('Old JSON results do not exist. Starting fresh.')
160 results_json = {}
161
162 return results_json, error
163
164 def _GetJSON(self):
165 """Gets the results for the results.json file."""
166 results_json, error = self._GetArchivedJSONResults()
167 if error:
168 # If there was an error don't write a results.json
169 # file at all as it would lose all the information on the bot.
170 logging.error("Archive directory is inaccessible. Not modifying "
171 "or clobbering the results.json file: " + str(error))
172 return None
173
174 builder_name = self._builder_name
175 if results_json and builder_name not in results_json:
176 logging.debug("Builder name (%s) is not in the results.json file."
177 % builder_name)
178
179 self._ConvertJSONToCurrentVersion(results_json)
180
181 if builder_name not in results_json:
182 results_json[builder_name] = self._CreateResultsForBuilderJSON()
183
184 results_for_builder = results_json[builder_name]
185
186 self._InsertGenericMetadata(results_for_builder)
187
188 self._InsertFailureSummaries(results_for_builder)
189
190 # Update the all failing tests with result type and time.
191 tests = results_for_builder[self.TESTS]
192 all_failing_tests = set(self._failures.iterkeys())
193 all_failing_tests.update(tests.iterkeys())
194 for test in all_failing_tests:
195 self._InsertTestTimeAndResult(test, tests)
196
197 # Specify separators in order to get compact encoding.
198 results_str = simplejson.dumps(results_json, separators=(',', ':'))
199 return self.JSON_PREFIX + results_str + self.JSON_SUFFIX
200
201 def _InsertFailureSummaries(self, results_for_builder):
202 """Inserts aggregate pass/failure statistics into the JSON.
203 This method reads self._skipped_tests, self._passed_tests and
204 self._failures and inserts FIXABLE, FIXABLE_COUNT and ALL_FIXABLE_COUNT
205 entries.
206
207 Args:
208 results_for_builder: Dictionary containing the test results for a
209 single builder.
210 """
211 # Insert the number of tests that failed.
212 self._InsertItemIntoRawList(results_for_builder,
213 len(set(self._failures.keys()) | self._skipped_tests),
214 self.FIXABLE_COUNT)
215
216 # Create a pass/skip/failure summary dictionary.
217 entry = {}
218 entry[self.SKIP_RESULT] = len(self._skipped_tests)
219 entry[self.PASS_RESULT] = len(self._passed_tests)
220 get = entry.get
221 for failure_type in self._failures.values():
222 failure_char = self.FAILURE_TO_CHAR[failure_type]
223 entry[failure_char] = get(failure_char, 0) + 1
224
225 # Insert the pass/skip/failure summary dictionary.
226 self._InsertItemIntoRawList(results_for_builder, entry, self.FIXABLE)
227
228 # Insert the number of all the tests that are supposed to pass.
229 self._InsertItemIntoRawList(results_for_builder,
230 len(self._skipped_tests | self._all_tests),
231 self.ALL_FIXABLE_COUNT)
232
233 def _InsertItemIntoRawList(self, results_for_builder, item, key):
234 """Inserts the item into the list with the given key in the results for
235 this builder. Creates the list if no such list exists.
236
237 Args:
238 results_for_builder: Dictionary containing the test results for a
239 single builder.
240 item: Number or string to insert into the list.
241 key: Key in results_for_builder for the list to insert into.
242 """
243 if key in results_for_builder:
244 raw_list = results_for_builder[key]
245 else:
246 raw_list = []
247
248 raw_list.insert(0, item)
249 raw_list = raw_list[:self.MAX_NUMBER_OF_BUILD_RESULTS_TO_LOG]
250 results_for_builder[key] = raw_list
251
252 def _InsertItemRunLengthEncoded(self, item, encoded_results):
253 """Inserts the item into the run-length encoded results.
254
255 Args:
256 item: String or number to insert.
257 encoded_results: run-length encoded results. An array of arrays, e.g.
258 [[3,'A'],[1,'Q']] encodes AAAQ.
259 """
260 if len(encoded_results) and item == encoded_results[0][1]:
261 num_results = encoded_results[0][0]
262 if num_results <= self.MAX_NUMBER_OF_BUILD_RESULTS_TO_LOG:
263 encoded_results[0][0] = num_results + 1
264 else:
265 # Use a list instead of a class for the run-length encoding since
266 # we want the serialized form to be concise.
267 encoded_results.insert(0, [1, item])
268
269 def _InsertGenericMetadata(self, results_for_builder):
270 """ Inserts generic metadata (such as version number, current time etc)
271 into the JSON.
272
273 Args:
274 results_for_builder: Dictionary containing the test results for
275 a single builder.
276 """
277 self._InsertItemIntoRawList(results_for_builder,
278 self._build_number, self.BUILD_NUMBERS)
279
280 path_to_webkit = path_utils.PathFromBase('third_party', 'WebKit',
281 'WebCore')
282 self._InsertItemIntoRawList(results_for_builder,
283 self._GetSVNRevision(path_to_webkit),
284 self.WEBKIT_SVN)
285
286 path_to_chrome_base = path_utils.PathFromBase()
287 self._InsertItemIntoRawList(results_for_builder,
288 self._GetSVNRevision(path_to_chrome_base),
289 self.CHROME_SVN)
290
291 self._InsertItemIntoRawList(results_for_builder,
292 int(time.time()),
293 self.TIME)
294
295 def _InsertTestTimeAndResult(self, test_name, tests):
296 """ Insert a test item with its results to the given tests dictionary.
297
298 Args:
299 tests: Dictionary containing test result entries.
300 """
301
302 result = JSONResultsGenerator.PASS_RESULT
303 time = 0
304
305 if test_name not in self._all_tests:
306 result = JSONResultsGenerator.NO_DATA_RESULT
307
308 if test_name in self._failures:
309 result = self.FAILURE_TO_CHAR[self._failures[test_name]]
310
311 if test_name in self._test_timings:
312 # Floor for now to get time in seconds.
313 time = int(self._test_timings[test_name])
314
315 if test_name not in tests:
316 tests[test_name] = self._CreateResultsAndTimesJSON()
317
318 thisTest = tests[test_name]
319 self._InsertItemRunLengthEncoded(result, thisTest[self.RESULTS])
320 self._InsertItemRunLengthEncoded(time, thisTest[self.TIMES])
321 self._NormalizeResultsJSON(thisTest, test_name, tests)
322
323 def _ConvertJSONToCurrentVersion(self, results_json):
324 """If the JSON does not match the current version, converts it to the
325 current version and adds in the new version number.
326 """
327 if (self.VERSION_KEY in results_json and
328 results_json[self.VERSION_KEY] == self.VERSION):
329 return
330
331 results_json[self.VERSION_KEY] = self.VERSION
332
333 def _CreateResultsAndTimesJSON(self):
334 results_and_times = {}
335 results_and_times[self.RESULTS] = []
336 results_and_times[self.TIMES] = []
337 return results_and_times
338
339 def _CreateResultsForBuilderJSON(self):
340 results_for_builder = {}
341 results_for_builder[self.TESTS] = {}
342 return results_for_builder
343
344 def _RemoveItemsOverMaxNumberOfBuilds(self, encoded_list):
345 """Removes items from the run-length encoded list after the final
346 item that exceeds the max number of builds to track.
347
348 Args:
349 encoded_results: run-length encoded results. An array of arrays, e.g.
350 [[3,'A'],[1,'Q']] encodes AAAQ.
351 """
352 num_builds = 0
353 index = 0
354 for result in encoded_list:
355 num_builds = num_builds + result[0]
356 index = index + 1
357 if num_builds > self.MAX_NUMBER_OF_BUILD_RESULTS_TO_LOG:
358 return encoded_list[:index]
359 return encoded_list
360
361 def _NormalizeResultsJSON(self, test, test_name, tests):
362 """ Prune tests where all runs pass or tests that no longer exist and
363 truncate all results to maxNumberOfBuilds.
364
365 Args:
366 test: ResultsAndTimes object for this test.
367 test_name: Name of the test.
368 tests: The JSON object with all the test results for this builder.
369 """
370 test[self.RESULTS] = self._RemoveItemsOverMaxNumberOfBuilds(
371 test[self.RESULTS])
372 test[self.TIMES] = self._RemoveItemsOverMaxNumberOfBuilds(
373 test[self.TIMES])
374
375 is_all_pass = self._IsResultsAllOfType(test[self.RESULTS],
376 self.PASS_RESULT)
377 is_all_no_data = self._IsResultsAllOfType(test[self.RESULTS],
378 self.NO_DATA_RESULT)
379 max_time = max([time[1] for time in test[self.TIMES]])
380
381 # Remove all passes/no-data from the results to reduce noise and
382 # filesize. If a test passes every run, but takes > MIN_TIME to run,
383 # don't throw away the data.
384 if is_all_no_data or (is_all_pass and max_time <= self.MIN_TIME):
385 del tests[test_name]
386
387 def _IsResultsAllOfType(self, results, type):
388 """Returns whether all the results are of the given type
389 (e.g. all passes)."""
390 return len(results) == 1 and results[0][1] == type
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698