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) | |
wkorman
2016/09/15 21:05:06
Maybe the builder list should be a set too, though
qyearsley
2016/09/15 22:20:30
That makes sense; I should do this in the follow-u
| |
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 Args: | |
412 command_results: A list of 3-tuples (retcode, stdout, stderr). | |
413 | |
414 Returns: | |
415 A ChangeSet object. | |
Dirk Pranke
2016/09/15 21:03:02
I'm not generally a fan of the comments that docum
qyearsley
2016/09/15 22:20:30
I'm slightly hesitant to start adopting that forma
| |
416 """ | |
417 change_set = ChangeSet() | |
339 for output in [result[1].split('\n') for result in command_results]: | 418 for output in [result[1].split('\n') for result in command_results]: |
340 file_added = False | 419 file_added = False |
341 for line in output: | 420 for line in output: |
342 try: | 421 try: |
343 if line: | 422 if line: |
344 parsed_line = json.loads(line) | 423 parsed_line = json.loads(line) |
345 if 'add' in parsed_line: | 424 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 | 425 file_added = True |
357 except ValueError: | 426 except ValueError: |
358 _log.debug('"%s" is not a JSON object, ignoring', line) | 427 _log.debug('"%s" is not a JSON object, ignoring', line) |
359 | 428 |
360 if not file_added: | 429 if not file_added: |
361 _log.debug('Could not add file based off output "%s"', output) | 430 _log.debug('Could not add file based off output "%s"', output) |
362 | 431 |
363 return list(files_to_add), list(files_to_delete), lines_to_remove | 432 return change_set |
364 | 433 |
365 def _optimize_baselines(self, test_prefix_list, verbose=False): | 434 def _optimize_baselines(self, test_prefix_list, verbose=False): |
366 optimize_commands = [] | 435 optimize_commands = [] |
367 for test in test_prefix_list: | 436 for test in test_prefix_list: |
368 all_suffixes = set() | 437 all_suffixes = set() |
369 builders_to_fetch_from = self._builders_to_fetch_from(self._builder_ names(test_prefix_list[test])) | 438 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]): | 439 for build in sorted(test_prefix_list[test]): |
371 if build.builder_name not in builders_to_fetch_from: | 440 if build.builder_name not in builders_to_fetch_from: |
372 break | 441 break |
373 all_suffixes.update(self._suffixes_for_actual_failures(test, bui ld, test_prefix_list[test][build])) | 442 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): | 497 if fs.exists(smoke_test_filename) and test not in fs.read_text_file( smoke_test_filename): |
429 return True | 498 return True |
430 | 499 |
431 return (SKIP in full_expectations.get_expectations(test) and | 500 return (SKIP in full_expectations.get_expectations(test) and |
432 SKIP not in generic_expectations.get_expectations(test)) | 501 SKIP not in generic_expectations.get_expectations(test)) |
433 | 502 |
434 def _run_in_parallel(self, commands, update_scm=True): | 503 def _run_in_parallel(self, commands, update_scm=True): |
435 if not commands: | 504 if not commands: |
436 return {} | 505 return {} |
437 | 506 |
507 # TODO(qyearsley): Refactor this block. | |
438 command_results = self._tool.executive.run_in_parallel(commands) | 508 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') | 509 log_output = '\n'.join(result[2] for result in command_results).replace( '\n\n', '\n') |
440 for line in log_output.split('\n'): | 510 for line in log_output.split('\n'): |
441 if line: | 511 if line: |
442 _log.error(line) | 512 _log.error(line) |
443 | 513 |
444 files_to_add, files_to_delete, lines_to_remove = self._serial_commands(c ommand_results) | 514 change_set = self._serial_commands(command_results) |
445 # TODO(qyearsley): Consider removing this if possible, | 515 |
446 # or making it work and produce reasonable results for rebaseline-cl. | 516 # TODO(qyearsley): Instead of updating the SCM state here, aggregate cha nges |
517 # and update once in _rebaseline. See http://crbug.com/639410. | |
447 if update_scm: | 518 if update_scm: |
448 if files_to_delete: | 519 if change_set.files_to_delete: |
449 self._tool.scm().delete_list(files_to_delete) | 520 self._tool.scm().delete_list(change_set.files_to_delete) |
450 if files_to_add: | 521 if change_set.files_to_add: |
451 self._tool.scm().add_list(files_to_add) | 522 self._tool.scm().add_list(change_set.files_to_add) |
452 return lines_to_remove | 523 |
524 return change_set.lines_to_remove | |
453 | 525 |
454 def _rebaseline(self, options, test_prefix_list, update_scm=True): | 526 def _rebaseline(self, options, test_prefix_list, update_scm=True): |
455 """Downloads new baselines in parallel, then updates expectations files | 527 """Downloads new baselines in parallel, then updates expectations files |
456 and optimizes baselines. | 528 and optimizes baselines. |
457 | 529 |
458 Args: | 530 Args: |
459 options: An object with the options passed to the current command. | 531 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 | 532 test_prefix_list: A map of test names to Build objects to file suffi xes |
461 for new baselines. For example: | 533 for new baselines. For example: |
462 { | 534 { |
(...skipping 163 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
626 for test in args: | 698 for test in args: |
627 if test not in test_prefix_list: | 699 if test not in test_prefix_list: |
628 test_prefix_list[test] = {} | 700 test_prefix_list[test] = {} |
629 build = Build(builder) | 701 build = Build(builder) |
630 test_prefix_list[test][build] = suffixes_to_update | 702 test_prefix_list[test][build] = suffixes_to_update |
631 | 703 |
632 if options.verbose: | 704 if options.verbose: |
633 _log.debug("rebaseline-json: " + str(test_prefix_list)) | 705 _log.debug("rebaseline-json: " + str(test_prefix_list)) |
634 | 706 |
635 self._rebaseline(options, test_prefix_list) | 707 self._rebaseline(options, test_prefix_list) |
OLD | NEW |