| OLD | NEW |
| (Empty) | |
| 1 # Copyright 2016 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 """A command to download new baselines for NeedsRebaseline tests. |
| 6 |
| 7 This command checks the list of tests with NeedsRebaseline expectations, |
| 8 and downloads the latest baselines for those tests from the results archived |
| 9 by the continuous builders. |
| 10 """ |
| 11 |
| 12 import logging |
| 13 import optparse |
| 14 import re |
| 15 import sys |
| 16 import time |
| 17 import traceback |
| 18 import urllib2 |
| 19 |
| 20 from webkitpy.common.net.buildbot import Build |
| 21 from webkitpy.layout_tests.models.test_expectations import TestExpectations, BAS
ELINE_SUFFIX_LIST |
| 22 from webkitpy.tool.commands.rebaseline import AbstractParallelRebaselineCommand |
| 23 |
| 24 |
| 25 _log = logging.getLogger(__name__) |
| 26 |
| 27 |
| 28 class AutoRebaseline(AbstractParallelRebaselineCommand): |
| 29 name = "auto-rebaseline" |
| 30 help_text = "Rebaselines any NeedsRebaseline lines in TestExpectations that
have cycled through all the bots." |
| 31 AUTO_REBASELINE_BRANCH_NAME = "auto-rebaseline-temporary-branch" |
| 32 AUTO_REBASELINE_ALT_BRANCH_NAME = "auto-rebaseline-alt-temporary-branch" |
| 33 |
| 34 # Rietveld uploader stinks. Limit the number of rebaselines in a given patch
to keep upload from failing. |
| 35 # FIXME: http://crbug.com/263676 Obviously we should fix the uploader here. |
| 36 MAX_LINES_TO_REBASELINE = 200 |
| 37 |
| 38 SECONDS_BEFORE_GIVING_UP = 300 |
| 39 |
| 40 def __init__(self): |
| 41 super(AutoRebaseline, self).__init__(options=[ |
| 42 # FIXME: Remove this option. |
| 43 self.no_optimize_option, |
| 44 # FIXME: Remove this option. |
| 45 self.results_directory_option, |
| 46 optparse.make_option("--auth-refresh-token-json", help="Rietveld aut
h refresh JSON token."), |
| 47 optparse.make_option("--dry-run", action='store_true', default=False
, |
| 48 help='Run without creating a temporary branch,
committing locally, or uploading/landing ' |
| 49 'changes to the remote repository.') |
| 50 ]) |
| 51 self._blame_regex = re.compile(r''' |
| 52 ^(\S*) # Commit hash |
| 53 [^(]* \( # Whitespace and open parenthesis |
| 54 < # Email address is surrounded by <> |
| 55 ( |
| 56 [^@]+ # Username preceding @ |
| 57 @ |
| 58 [^@>]+ # Domain terminated by @ or >, some lines have an ad
ditional @ fragment after the email. |
| 59 ) |
| 60 .*?([^ ]*) # Test file name |
| 61 \ \[ # Single space followed by opening [ for expectation
specifier |
| 62 [^[]*$ # Prevents matching previous [ for version specifier
s instead of expectation specifiers |
| 63 ''', re.VERBOSE) |
| 64 |
| 65 def bot_revision_data(self, scm): |
| 66 revisions = [] |
| 67 for result in self.build_data().values(): |
| 68 if result.run_was_interrupted(): |
| 69 _log.error("Can't rebaseline because the latest run on %s exited
early.", result.builder_name()) |
| 70 return [] |
| 71 revisions.append({ |
| 72 "builder": result.builder_name(), |
| 73 "revision": result.chromium_revision(scm), |
| 74 }) |
| 75 return revisions |
| 76 |
| 77 @staticmethod |
| 78 def _strip_comments(line): |
| 79 comment_index = line.find("#") |
| 80 if comment_index == -1: |
| 81 comment_index = len(line) |
| 82 return re.sub(r"\s+", " ", line[:comment_index].strip()) |
| 83 |
| 84 def tests_to_rebaseline(self, tool, min_revision, print_revisions): |
| 85 port = tool.port_factory.get() |
| 86 expectations_file_path = port.path_to_generic_test_expectations_file() |
| 87 |
| 88 tests = set() |
| 89 revision = None |
| 90 commit = None |
| 91 author = None |
| 92 bugs = set() |
| 93 has_any_needs_rebaseline_lines = False |
| 94 |
| 95 for line in tool.scm().blame(expectations_file_path).split("\n"): |
| 96 line = self._strip_comments(line) |
| 97 if "NeedsRebaseline" not in line: |
| 98 continue |
| 99 |
| 100 has_any_needs_rebaseline_lines = True |
| 101 |
| 102 parsed_line = self._blame_regex.match(line) |
| 103 if not parsed_line: |
| 104 # Deal gracefully with inability to parse blame info for a line
in TestExpectations. |
| 105 # Parsing could fail if for example during local debugging the d
eveloper modifies |
| 106 # TestExpectations and does not commit. |
| 107 _log.info("Couldn't find blame info for expectations line, skipp
ing [line=%s].", line) |
| 108 continue |
| 109 |
| 110 commit_hash = parsed_line.group(1) |
| 111 commit_position = tool.scm().commit_position_from_git_commit(commit_
hash) |
| 112 |
| 113 test = parsed_line.group(3) |
| 114 if print_revisions: |
| 115 _log.info("%s is waiting for r%s", test, commit_position) |
| 116 |
| 117 if not commit_position or commit_position > min_revision: |
| 118 continue |
| 119 |
| 120 if revision and commit_position != revision: |
| 121 continue |
| 122 |
| 123 if not revision: |
| 124 revision = commit_position |
| 125 commit = commit_hash |
| 126 author = parsed_line.group(2) |
| 127 |
| 128 bugs.update(re.findall(r"crbug\.com\/(\d+)", line)) |
| 129 tests.add(test) |
| 130 |
| 131 if len(tests) >= self.MAX_LINES_TO_REBASELINE: |
| 132 _log.info("Too many tests to rebaseline in one patch. Doing the
first %d.", self.MAX_LINES_TO_REBASELINE) |
| 133 break |
| 134 |
| 135 return tests, revision, commit, author, bugs, has_any_needs_rebaseline_l
ines |
| 136 |
| 137 @staticmethod |
| 138 def link_to_patch(commit): |
| 139 return "https://chromium.googlesource.com/chromium/src/+/" + commit |
| 140 |
| 141 def commit_message(self, author, revision, commit, bugs): |
| 142 bug_string = "" |
| 143 if bugs: |
| 144 bug_string = "BUG=%s\n" % ",".join(bugs) |
| 145 |
| 146 return """Auto-rebaseline for r%s |
| 147 |
| 148 %s |
| 149 |
| 150 %sTBR=%s |
| 151 """ % (revision, self.link_to_patch(commit), bug_string, author) |
| 152 |
| 153 def get_test_prefix_list(self, tests): |
| 154 test_prefix_list = {} |
| 155 lines_to_remove = {} |
| 156 |
| 157 for builder_name in self._release_builders(): |
| 158 port_name = self._tool.builders.port_name_for_builder_name(builder_n
ame) |
| 159 port = self._tool.port_factory.get(port_name) |
| 160 expectations = TestExpectations(port, include_overrides=True) |
| 161 for test in expectations.get_needs_rebaseline_failures(): |
| 162 if test not in tests: |
| 163 continue |
| 164 |
| 165 if test not in test_prefix_list: |
| 166 lines_to_remove[test] = [] |
| 167 test_prefix_list[test] = {} |
| 168 lines_to_remove[test].append(builder_name) |
| 169 test_prefix_list[test][Build(builder_name)] = BASELINE_SUFFIX_LI
ST |
| 170 |
| 171 return test_prefix_list, lines_to_remove |
| 172 |
| 173 def _run_git_cl_command(self, options, command): |
| 174 subprocess_command = ['git', 'cl'] + command |
| 175 if options.verbose: |
| 176 subprocess_command.append('--verbose') |
| 177 if options.auth_refresh_token_json: |
| 178 subprocess_command.append('--auth-refresh-token-json') |
| 179 subprocess_command.append(options.auth_refresh_token_json) |
| 180 |
| 181 process = self._tool.executive.popen(subprocess_command, stdout=self._to
ol.executive.PIPE, |
| 182 stderr=self._tool.executive.STDOUT) |
| 183 last_output_time = time.time() |
| 184 |
| 185 # git cl sometimes completely hangs. Bail if we haven't gotten any outpu
t to stdout/stderr in a while. |
| 186 while process.poll() is None and time.time() < last_output_time + self.S
ECONDS_BEFORE_GIVING_UP: |
| 187 # FIXME: This doesn't make any sense. readline blocks, so all this c
ode to |
| 188 # try and bail is useless. Instead, we should do the readline calls
on a |
| 189 # subthread. Then the rest of this code would make sense. |
| 190 out = process.stdout.readline().rstrip('\n') |
| 191 if out: |
| 192 last_output_time = time.time() |
| 193 _log.info(out) |
| 194 |
| 195 if process.poll() is None: |
| 196 _log.error('Command hung: %s', subprocess_command) |
| 197 return False |
| 198 return True |
| 199 |
| 200 # FIXME: Move this somewhere more general. |
| 201 @staticmethod |
| 202 def tree_status(): |
| 203 blink_tree_status_url = "http://chromium-status.appspot.com/status" |
| 204 status = urllib2.urlopen(blink_tree_status_url).read().lower() |
| 205 if 'closed' in status or status == "0": |
| 206 return 'closed' |
| 207 elif 'open' in status or status == "1": |
| 208 return 'open' |
| 209 return 'unknown' |
| 210 |
| 211 def execute(self, options, args, tool): |
| 212 if tool.scm().executable_name == "svn": |
| 213 _log.error("Auto rebaseline only works with a git checkout.") |
| 214 return |
| 215 |
| 216 if not options.dry_run and tool.scm().has_working_directory_changes(): |
| 217 _log.error("Cannot proceed with working directory changes. Clean wor
king directory first.") |
| 218 return |
| 219 |
| 220 revision_data = self.bot_revision_data(tool.scm()) |
| 221 if not revision_data: |
| 222 return |
| 223 |
| 224 min_revision = int(min([item["revision"] for item in revision_data])) |
| 225 tests, revision, commit, author, bugs, _ = self.tests_to_rebaseline( |
| 226 tool, min_revision, print_revisions=options.verbose) |
| 227 |
| 228 if options.verbose: |
| 229 _log.info("Min revision across all bots is %s.", min_revision) |
| 230 for item in revision_data: |
| 231 _log.info("%s: r%s", item["builder"], item["revision"]) |
| 232 |
| 233 if not tests: |
| 234 _log.debug('No tests to rebaseline.') |
| 235 return |
| 236 |
| 237 if self.tree_status() == 'closed': |
| 238 _log.info('Cannot proceed. Tree is closed.') |
| 239 return |
| 240 |
| 241 _log.info('Rebaselining %s for r%s by %s.', list(tests), revision, autho
r) |
| 242 |
| 243 test_prefix_list, _ = self.get_test_prefix_list(tests) |
| 244 |
| 245 did_switch_branches = False |
| 246 did_finish = False |
| 247 old_branch_name_or_ref = '' |
| 248 rebaseline_branch_name = self.AUTO_REBASELINE_BRANCH_NAME |
| 249 try: |
| 250 # Save the current branch name and check out a clean branch for the
patch. |
| 251 old_branch_name_or_ref = tool.scm().current_branch_or_ref() |
| 252 if old_branch_name_or_ref == self.AUTO_REBASELINE_BRANCH_NAME: |
| 253 rebaseline_branch_name = self.AUTO_REBASELINE_ALT_BRANCH_NAME |
| 254 if not options.dry_run: |
| 255 tool.scm().delete_branch(rebaseline_branch_name) |
| 256 tool.scm().create_clean_branch(rebaseline_branch_name) |
| 257 did_switch_branches = True |
| 258 |
| 259 if test_prefix_list: |
| 260 self._rebaseline(options, test_prefix_list) |
| 261 |
| 262 if options.dry_run: |
| 263 return |
| 264 |
| 265 tool.scm().commit_locally_with_message( |
| 266 self.commit_message(author, revision, commit, bugs)) |
| 267 |
| 268 # FIXME: It would be nice if we could dcommit the patch without uplo
ading, but still |
| 269 # go through all the precommit hooks. For rebaselines with lots of f
iles, uploading |
| 270 # takes a long time and sometimes fails, but we don't want to commit
if, e.g. the |
| 271 # tree is closed. |
| 272 did_finish = self._run_git_cl_command(options, ['upload', '-f']) |
| 273 |
| 274 if did_finish: |
| 275 # Uploading can take a very long time. Do another pull to make s
ure TestExpectations is up to date, |
| 276 # so the dcommit can go through. |
| 277 # FIXME: Log the pull and dcommit stdout/stderr to the log-serve
r. |
| 278 tool.executive.run_command(['git', 'pull']) |
| 279 |
| 280 self._run_git_cl_command(options, ['land', '-f', '-v']) |
| 281 except OSError: |
| 282 traceback.print_exc(file=sys.stderr) |
| 283 finally: |
| 284 if did_switch_branches: |
| 285 if did_finish: |
| 286 # Close the issue if dcommit failed. |
| 287 issue_already_closed = tool.executive.run_command( |
| 288 ['git', 'config', 'branch.%s.rietveldissue' % rebaseline
_branch_name], |
| 289 return_exit_code=True) |
| 290 if not issue_already_closed: |
| 291 self._run_git_cl_command(options, ['set_close']) |
| 292 |
| 293 tool.scm().ensure_cleanly_tracking_remote_master() |
| 294 if old_branch_name_or_ref: |
| 295 tool.scm().checkout_branch(old_branch_name_or_ref) |
| 296 tool.scm().delete_branch(rebaseline_branch_name) |
| OLD | NEW |