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

Side by Side Diff: third_party/WebKit/Tools/Scripts/webkitpy/tool/commands/rebaseline.py

Issue 2347643002: Refactor the way that SCM changes are tracked and aggregated in rebaseline.py. (Closed)
Patch Set: Refactor the way that SCM changes are tracked and aggregated. Created 4 years, 3 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
OLDNEW
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
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
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
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
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
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)
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698