OLD | NEW |
1 # Copyright (c) 2010 Google Inc. All rights reserved. | 1 # Copyright (c) 2010 Google Inc. All rights reserved. |
2 # | 2 # |
3 # Redistribution and use in source and binary forms, with or without | 3 # Redistribution and use in source and binary forms, with or without |
4 # modification, are permitted provided that the following conditions are | 4 # modification, are permitted provided that the following conditions are |
5 # met: | 5 # met: |
6 # | 6 # |
7 # * Redistributions of source code must retain the above copyright | 7 # * Redistributions of source code must retain the above copyright |
8 # notice, this list of conditions and the following disclaimer. | 8 # notice, this list of conditions and the following disclaimer. |
9 # * Redistributions in binary form must reproduce the above | 9 # * Redistributions in binary form must reproduce the above |
10 # copyright notice, this list of conditions and the following disclaimer | 10 # copyright notice, this list of conditions and the following disclaimer |
(...skipping 45 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
56 platform_options = factory.platform_options(use_globs=True) | 56 platform_options = factory.platform_options(use_globs=True) |
57 | 57 |
58 results_directory_option = optparse.make_option("--results-directory", help=
"Local results directory to use.") | 58 results_directory_option = optparse.make_option("--results-directory", help=
"Local results directory to use.") |
59 | 59 |
60 suffixes_option = optparse.make_option("--suffixes", default=','.join(BASELI
NE_SUFFIX_LIST), action="store", | 60 suffixes_option = optparse.make_option("--suffixes", default=','.join(BASELI
NE_SUFFIX_LIST), action="store", |
61 help="Comma-separated-list of file ty
pes to rebaseline.") | 61 help="Comma-separated-list of file ty
pes to rebaseline.") |
62 | 62 |
63 def __init__(self, options=None): | 63 def __init__(self, options=None): |
64 super(AbstractRebaseliningCommand, self).__init__(options=options) | 64 super(AbstractRebaseliningCommand, self).__init__(options=options) |
65 self._baseline_suffix_list = BASELINE_SUFFIX_LIST | 65 self._baseline_suffix_list = BASELINE_SUFFIX_LIST |
66 self._scm_changes = {'add': [], 'delete': [], 'remove-lines': []} | 66 self._scm_changes = ChangeSet() |
67 self._tool = None | 67 self._tool = None |
68 | 68 |
69 def _add_to_scm_later(self, path): | 69 def _print_scm_changes(self): |
70 self._scm_changes['add'].append(path) | 70 print(json.dumps(self._scm_changes.to_dict())) |
71 | 71 |
72 def _delete_from_scm_later(self, path): | |
73 self._scm_changes['delete'].append(path) | |
74 | 72 |
75 def _print_scm_changes(self): | 73 class ChangeSet(object): |
76 print(json.dumps(self._scm_changes)) | 74 """A record of added and deleted files and TestExpectation lines to remove. |
| 75 |
| 76 This class groups two kinds of changes together: |
| 77 (1) files that have been added or removed. |
| 78 (2) lines to remove from TestExpectations. |
| 79 |
| 80 The reason for keeping track of these changes together is that after rebasel
ining |
| 81 many tests in parallel by subprocesses, we can update the git staged files l
ist |
| 82 and the TestExpectations all at once at the end. |
| 83 |
| 84 Each subprocess can communicate some set of changes by outputting JSON lines
, |
| 85 and another process can aggregate the changes. |
| 86 |
| 87 TODO(qyearsley): Refactor this so that: |
| 88 - lines to remove are only tracked in one format. |
| 89 - files to add and delete are always disjoint sets. |
| 90 """ |
| 91 def __init__(self, files_to_add=None, files_to_delete=None, lines_to_remove=
None): |
| 92 self.files_to_add = files_to_add or [] |
| 93 self.files_to_delete = files_to_delete or [] |
| 94 self.lines_to_remove = lines_to_remove or {} |
| 95 |
| 96 def add_file(self, path): |
| 97 self.files_to_add.append(path) |
| 98 |
| 99 def delete_file(self, path): |
| 100 self.files_to_delete.append(path) |
| 101 |
| 102 def remove_line(self, test, builder): |
| 103 if test not in self.lines_to_remove: |
| 104 self.lines_to_remove[test] = [] |
| 105 self.lines_to_remove[test].append(builder) |
| 106 |
| 107 def to_dict(self): |
| 108 remove_lines = [] |
| 109 for test in self.lines_to_remove: |
| 110 for builder in self.lines_to_remove[test]: |
| 111 remove_lines.append({'test': test, 'builder': builder}) |
| 112 return { |
| 113 'add': list(self.files_to_add), |
| 114 'delete': list(self.files_to_delete), |
| 115 'remove-lines': remove_lines, |
| 116 } |
| 117 |
| 118 @staticmethod |
| 119 def from_dict(change_dict): |
| 120 files_to_add = set() |
| 121 files_to_delete = set() |
| 122 lines_to_remove = {} |
| 123 if 'add' in change_dict: |
| 124 files_to_add.update(change_dict['add']) |
| 125 if 'delete' in change_dict: |
| 126 files_to_delete.update(change_dict['delete']) |
| 127 if 'remove-lines' in change_dict: |
| 128 for line_to_remove in change_dict['remove-lines']: |
| 129 test = line_to_remove['test'] |
| 130 builder = line_to_remove['builder'] |
| 131 if test not in lines_to_remove: |
| 132 lines_to_remove[test] = [] |
| 133 lines_to_remove[test].append(builder) |
| 134 return ChangeSet( |
| 135 files_to_add=list(files_to_add), |
| 136 files_to_delete=list(files_to_delete), |
| 137 lines_to_remove=lines_to_remove) |
| 138 |
| 139 def update(self, other): |
| 140 assert isinstance(other, ChangeSet) |
| 141 assert type(other.lines_to_remove) is dict |
| 142 self.files_to_add.extend(other.files_to_add) |
| 143 self.files_to_delete.extend(other.files_to_delete) |
| 144 for test in other.lines_to_remove: |
| 145 if test not in self.lines_to_remove: |
| 146 self.lines_to_remove[test] = [] |
| 147 self.lines_to_remove[test].extend(other.lines_to_remove[test]) |
77 | 148 |
78 | 149 |
79 class BaseInternalRebaselineCommand(AbstractRebaseliningCommand): | 150 class BaseInternalRebaselineCommand(AbstractRebaseliningCommand): |
80 """Base class for rebaseline-related commands that are intended to be used b
y other commands.""" | 151 """Base class for rebaseline-related commands that are intended to be used b
y other commands.""" |
81 # Not overriding execute() - pylint: disable=abstract-method | 152 # Not overriding execute() - pylint: disable=abstract-method |
82 | 153 |
83 def __init__(self): | 154 def __init__(self): |
84 super(BaseInternalRebaselineCommand, self).__init__(options=[ | 155 super(BaseInternalRebaselineCommand, self).__init__(options=[ |
85 self.results_directory_option, | 156 self.results_directory_option, |
86 self.suffixes_option, | 157 self.suffixes_option, |
(...skipping 76 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
163 new_baselines.append(new_baseline) | 234 new_baselines.append(new_baseline) |
164 | 235 |
165 for i in range(len(old_baselines)): | 236 for i in range(len(old_baselines)): |
166 old_baseline = old_baselines[i] | 237 old_baseline = old_baselines[i] |
167 new_baseline = new_baselines[i] | 238 new_baseline = new_baselines[i] |
168 | 239 |
169 _log.debug("Copying baseline from %s to %s.", old_baseline, new_base
line) | 240 _log.debug("Copying baseline from %s to %s.", old_baseline, new_base
line) |
170 self._tool.filesystem.maybe_make_directory(self._tool.filesystem.dir
name(new_baseline)) | 241 self._tool.filesystem.maybe_make_directory(self._tool.filesystem.dir
name(new_baseline)) |
171 self._tool.filesystem.copyfile(old_baseline, new_baseline) | 242 self._tool.filesystem.copyfile(old_baseline, new_baseline) |
172 if not self._tool.scm().exists(new_baseline): | 243 if not self._tool.scm().exists(new_baseline): |
173 self._add_to_scm_later(new_baseline) | 244 self._scm_changes.add_file(new_baseline) |
174 | 245 |
175 def execute(self, options, args, tool): | 246 def execute(self, options, args, tool): |
176 self._tool = tool | 247 self._tool = tool |
177 for suffix in options.suffixes.split(','): | 248 for suffix in options.suffixes.split(','): |
178 self._copy_existing_baseline(options.builder, options.test, suffix) | 249 self._copy_existing_baseline(options.builder, options.test, suffix) |
179 self._print_scm_changes() | 250 self._print_scm_changes() |
180 | 251 |
181 | 252 |
182 class RebaselineTest(BaseInternalRebaselineCommand): | 253 class RebaselineTest(BaseInternalRebaselineCommand): |
183 name = "rebaseline-test-internal" | 254 name = "rebaseline-test-internal" |
184 help_text = "Rebaseline a single test from a buildbot. Only intended for use
by other webkit-patch commands." | 255 help_text = "Rebaseline a single test from a buildbot. Only intended for use
by other webkit-patch commands." |
185 | 256 |
186 def _save_baseline(self, data, target_baseline): | 257 def _save_baseline(self, data, target_baseline): |
187 if not data: | 258 if not data: |
188 _log.debug("No baseline data to save.") | 259 _log.debug("No baseline data to save.") |
189 return | 260 return |
190 | 261 |
191 filesystem = self._tool.filesystem | 262 filesystem = self._tool.filesystem |
192 filesystem.maybe_make_directory(filesystem.dirname(target_baseline)) | 263 filesystem.maybe_make_directory(filesystem.dirname(target_baseline)) |
193 filesystem.write_binary_file(target_baseline, data) | 264 filesystem.write_binary_file(target_baseline, data) |
194 if not self._tool.scm().exists(target_baseline): | 265 if not self._tool.scm().exists(target_baseline): |
195 self._add_to_scm_later(target_baseline) | 266 self._scm_changes.add_file(target_baseline) |
196 | 267 |
197 def _rebaseline_test(self, builder_name, test_name, suffix, results_url): | 268 def _rebaseline_test(self, builder_name, test_name, suffix, results_url): |
198 baseline_directory = self._baseline_directory(builder_name) | 269 baseline_directory = self._baseline_directory(builder_name) |
199 | 270 |
200 source_baseline = "%s/%s" % (results_url, self._file_name_for_actual_res
ult(test_name, suffix)) | 271 source_baseline = "%s/%s" % (results_url, self._file_name_for_actual_res
ult(test_name, suffix)) |
201 target_baseline = self._tool.filesystem.join(baseline_directory, self._f
ile_name_for_expected_result(test_name, suffix)) | 272 target_baseline = self._tool.filesystem.join(baseline_directory, self._f
ile_name_for_expected_result(test_name, suffix)) |
202 | 273 |
203 _log.debug("Retrieving source %s for target %s.", source_baseline, targe
t_baseline) | 274 _log.debug("Retrieving source %s for target %s.", source_baseline, targe
t_baseline) |
204 self._save_baseline(self._tool.web.get_binary(source_baseline, convert_4
04_to_None=True), | 275 self._save_baseline(self._tool.web.get_binary(source_baseline, convert_4
04_to_None=True), |
205 target_baseline) | 276 target_baseline) |
206 | 277 |
207 def _rebaseline_test_and_update_expectations(self, options): | 278 def _rebaseline_test_and_update_expectations(self, options): |
208 self._baseline_suffix_list = options.suffixes.split(',') | 279 self._baseline_suffix_list = options.suffixes.split(',') |
209 | 280 |
210 port = self._tool.port_factory.get_from_builder_name(options.builder) | 281 port = self._tool.port_factory.get_from_builder_name(options.builder) |
211 if port.reference_files(options.test): | 282 if port.reference_files(options.test): |
212 if 'png' in self._baseline_suffix_list: | 283 if 'png' in self._baseline_suffix_list: |
213 _log.warning("Cannot rebaseline image result for reftest: %s", o
ptions.test) | 284 _log.warning("Cannot rebaseline image result for reftest: %s", o
ptions.test) |
214 return | 285 return |
215 assert self._baseline_suffix_list == ['txt'] | 286 assert self._baseline_suffix_list == ['txt'] |
216 | 287 |
217 if options.results_directory: | 288 if options.results_directory: |
218 results_url = 'file://' + options.results_directory | 289 results_url = 'file://' + options.results_directory |
219 else: | 290 else: |
220 results_url = self._tool.buildbot.results_url(options.builder, build
_number=options.build_number) | 291 results_url = self._tool.buildbot.results_url(options.builder, build
_number=options.build_number) |
221 | 292 |
222 for suffix in self._baseline_suffix_list: | 293 for suffix in self._baseline_suffix_list: |
223 self._rebaseline_test(options.builder, options.test, suffix, results
_url) | 294 self._rebaseline_test(options.builder, options.test, suffix, results
_url) |
224 self._scm_changes['remove-lines'].append({'builder': options.builder, 't
est': options.test}) | 295 self._scm_changes.remove_line(test=options.test, builder=options.builder
) |
225 | 296 |
226 def execute(self, options, args, tool): | 297 def execute(self, options, args, tool): |
227 self._tool = tool | 298 self._tool = tool |
228 self._rebaseline_test_and_update_expectations(options) | 299 self._rebaseline_test_and_update_expectations(options) |
229 self._print_scm_changes() | 300 self._print_scm_changes() |
230 | 301 |
231 | 302 |
232 class AbstractParallelRebaselineCommand(AbstractRebaseliningCommand): | 303 class AbstractParallelRebaselineCommand(AbstractRebaseliningCommand): |
233 """Base class for rebaseline commands that do some tasks in parallel.""" | 304 """Base class for rebaseline commands that do some tasks in parallel.""" |
234 # Not overriding execute() - pylint: disable=abstract-method | 305 # Not overriding execute() - pylint: disable=abstract-method |
(...skipping 91 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
326 if options.verbose: | 397 if options.verbose: |
327 cmd_line.append('--verbose') | 398 cmd_line.append('--verbose') |
328 copy_baseline_commands.append( | 399 copy_baseline_commands.append( |
329 tuple([[self._tool.executable, path_to_webkit_patch, 'co
py-existing-baselines-internal'] + cmd_line, cwd])) | 400 tuple([[self._tool.executable, path_to_webkit_patch, 'co
py-existing-baselines-internal'] + cmd_line, cwd])) |
330 rebaseline_commands.append( | 401 rebaseline_commands.append( |
331 tuple([[self._tool.executable, path_to_webkit_patch, 're
baseline-test-internal'] + cmd_line, cwd])) | 402 tuple([[self._tool.executable, path_to_webkit_patch, 're
baseline-test-internal'] + cmd_line, cwd])) |
332 return copy_baseline_commands, rebaseline_commands, lines_to_remove | 403 return copy_baseline_commands, rebaseline_commands, lines_to_remove |
333 | 404 |
334 @staticmethod | 405 @staticmethod |
335 def _serial_commands(command_results): | 406 def _serial_commands(command_results): |
336 files_to_add = set() | 407 """Parses the JSON lines from command output to extract SCM changes. |
337 files_to_delete = set() | 408 |
338 lines_to_remove = {} | 409 TODO(qyearsley): Refactor and rename this function. |
| 410 """ |
| 411 change_set = ChangeSet() |
339 for output in [result[1].split('\n') for result in command_results]: | 412 for output in [result[1].split('\n') for result in command_results]: |
340 file_added = False | 413 file_added = False |
341 for line in output: | 414 for line in output: |
342 try: | 415 try: |
343 if line: | 416 if line: |
344 parsed_line = json.loads(line) | 417 parsed_line = json.loads(line) |
345 if 'add' in parsed_line: | 418 change_set.update(ChangeSet.from_dict(parsed_line)) |
346 files_to_add.update(parsed_line['add']) | |
347 if 'delete' in parsed_line: | |
348 files_to_delete.update(parsed_line['delete']) | |
349 if 'remove-lines' in parsed_line: | |
350 for line_to_remove in parsed_line['remove-lines']: | |
351 test = line_to_remove['test'] | |
352 builder = line_to_remove['builder'] | |
353 if test not in lines_to_remove: | |
354 lines_to_remove[test] = [] | |
355 lines_to_remove[test].append(builder) | |
356 file_added = True | 419 file_added = True |
357 except ValueError: | 420 except ValueError: |
358 _log.debug('"%s" is not a JSON object, ignoring', line) | 421 _log.debug('"%s" is not a JSON object, ignoring', line) |
359 | 422 |
360 if not file_added: | 423 if not file_added: |
361 _log.debug('Could not add file based off output "%s"', output) | 424 _log.debug('Could not add file based off output "%s"', output) |
362 | 425 |
363 return list(files_to_add), list(files_to_delete), lines_to_remove | 426 return change_set |
364 | 427 |
365 def _optimize_baselines(self, test_prefix_list, verbose=False): | 428 def _optimize_baselines(self, test_prefix_list, verbose=False): |
366 optimize_commands = [] | 429 optimize_commands = [] |
367 for test in test_prefix_list: | 430 for test in test_prefix_list: |
368 all_suffixes = set() | 431 all_suffixes = set() |
369 builders_to_fetch_from = self._builders_to_fetch_from(self._builder_
names(test_prefix_list[test])) | 432 builders_to_fetch_from = self._builders_to_fetch_from(self._builder_
names(test_prefix_list[test])) |
370 for build in sorted(test_prefix_list[test]): | 433 for build in sorted(test_prefix_list[test]): |
371 if build.builder_name not in builders_to_fetch_from: | 434 if build.builder_name not in builders_to_fetch_from: |
372 break | 435 break |
373 all_suffixes.update(self._suffixes_for_actual_failures(test, bui
ld, test_prefix_list[test][build])) | 436 all_suffixes.update(self._suffixes_for_actual_failures(test, bui
ld, test_prefix_list[test][build])) |
(...skipping 54 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
428 if fs.exists(smoke_test_filename) and test not in fs.read_text_file(
smoke_test_filename): | 491 if fs.exists(smoke_test_filename) and test not in fs.read_text_file(
smoke_test_filename): |
429 return True | 492 return True |
430 | 493 |
431 return (SKIP in full_expectations.get_expectations(test) and | 494 return (SKIP in full_expectations.get_expectations(test) and |
432 SKIP not in generic_expectations.get_expectations(test)) | 495 SKIP not in generic_expectations.get_expectations(test)) |
433 | 496 |
434 def _run_in_parallel(self, commands, update_scm=True): | 497 def _run_in_parallel(self, commands, update_scm=True): |
435 if not commands: | 498 if not commands: |
436 return {} | 499 return {} |
437 | 500 |
| 501 # TODO(qyearsley): Refactor this block. |
438 command_results = self._tool.executive.run_in_parallel(commands) | 502 command_results = self._tool.executive.run_in_parallel(commands) |
439 log_output = '\n'.join(result[2] for result in command_results).replace(
'\n\n', '\n') | 503 log_output = '\n'.join(result[2] for result in command_results).replace(
'\n\n', '\n') |
440 for line in log_output.split('\n'): | 504 for line in log_output.split('\n'): |
441 if line: | 505 if line: |
442 _log.error(line) | 506 _log.error(line) |
443 | 507 |
444 files_to_add, files_to_delete, lines_to_remove = self._serial_commands(c
ommand_results) | 508 change_set = self._serial_commands(command_results) |
445 # TODO(qyearsley): Consider removing this if possible, | 509 |
446 # or making it work and produce reasonable results for rebaseline-cl. | 510 # TODO(qyearsley): Instead of updating the SCM state here, aggregate cha
nges |
| 511 # and update once in _rebaseline. See http://crbug.com/639410. |
447 if update_scm: | 512 if update_scm: |
448 if files_to_delete: | 513 if change_set.files_to_delete: |
449 self._tool.scm().delete_list(files_to_delete) | 514 self._tool.scm().delete_list(change_set.files_to_delete) |
450 if files_to_add: | 515 if change_set.files_to_add: |
451 self._tool.scm().add_list(files_to_add) | 516 self._tool.scm().add_list(change_set.files_to_add) |
452 return lines_to_remove | 517 |
| 518 return change_set.lines_to_remove |
453 | 519 |
454 def _rebaseline(self, options, test_prefix_list, update_scm=True): | 520 def _rebaseline(self, options, test_prefix_list, update_scm=True): |
455 """Downloads new baselines in parallel, then updates expectations files | 521 """Downloads new baselines in parallel, then updates expectations files |
456 and optimizes baselines. | 522 and optimizes baselines. |
457 | 523 |
458 Args: | 524 Args: |
459 options: An object with the options passed to the current command. | 525 options: An object with the options passed to the current command. |
460 test_prefix_list: A map of test names to Build objects to file suffi
xes | 526 test_prefix_list: A map of test names to Build objects to file suffi
xes |
461 for new baselines. For example: | 527 for new baselines. For example: |
462 { | 528 { |
(...skipping 163 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
626 for test in args: | 692 for test in args: |
627 if test not in test_prefix_list: | 693 if test not in test_prefix_list: |
628 test_prefix_list[test] = {} | 694 test_prefix_list[test] = {} |
629 build = Build(builder) | 695 build = Build(builder) |
630 test_prefix_list[test][build] = suffixes_to_update | 696 test_prefix_list[test][build] = suffixes_to_update |
631 | 697 |
632 if options.verbose: | 698 if options.verbose: |
633 _log.debug("rebaseline-json: " + str(test_prefix_list)) | 699 _log.debug("rebaseline-json: " + str(test_prefix_list)) |
634 | 700 |
635 self._rebaseline(options, test_prefix_list) | 701 self._rebaseline(options, test_prefix_list) |
OLD | NEW |