| OLD | NEW |
| (Empty) |
| 1 # Copyright (c) 2013 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 | |
| 6 """Miscellaneous utilities needed by the Skia buildbot master.""" | |
| 7 | |
| 8 | |
| 9 import difflib | |
| 10 import httplib2 | |
| 11 import json | |
| 12 import os | |
| 13 import re | |
| 14 | |
| 15 # requires Google APIs client library for Python; see | |
| 16 # https://code.google.com/p/google-api-python-client/wiki/Installation | |
| 17 from apiclient.discovery import build | |
| 18 from buildbot.scheduler import Dependent | |
| 19 from buildbot.scheduler import Scheduler | |
| 20 from buildbot.schedulers import timed | |
| 21 from buildbot.schedulers.filter import ChangeFilter | |
| 22 from config_private import TRY_SVN_BASEURL | |
| 23 from master import try_job_svn | |
| 24 from master import try_job_rietveld | |
| 25 from master.builders_pools import BuildersPools | |
| 26 from oauth2client.client import SignedJwtAssertionCredentials | |
| 27 | |
| 28 import builder_name_schema | |
| 29 import config_private | |
| 30 import os | |
| 31 import skia_vars | |
| 32 import subprocess | |
| 33 | |
| 34 | |
| 35 GATEKEEPER_NAME = 'GateKeeper' | |
| 36 | |
| 37 TRY_SCHEDULER_SVN = 'skia_try_svn' | |
| 38 TRY_SCHEDULER_RIETVELD = 'skia_try_rietveld' | |
| 39 TRY_SCHEDULERS = [TRY_SCHEDULER_SVN, TRY_SCHEDULER_RIETVELD] | |
| 40 TRY_SCHEDULERS_STR = '|'.join(TRY_SCHEDULERS) | |
| 41 | |
| 42 | |
| 43 def GetListFromEnvVar(name, splitstring=','): | |
| 44 """ Returns contents of an environment variable, as a list. | |
| 45 | |
| 46 If the environment variable is unset or set to empty-string, this returns | |
| 47 an empty list. | |
| 48 | |
| 49 name: string; name of the environment variable to read | |
| 50 splitstring: string with which to split the env var into list items | |
| 51 """ | |
| 52 unsplit = os.environ.get(name, None) | |
| 53 if unsplit: | |
| 54 return unsplit.split(',') | |
| 55 else: | |
| 56 return [] | |
| 57 | |
| 58 | |
| 59 def StringDiff(expected, actual): | |
| 60 """ Returns the diff between two multiline strings, as a multiline string.""" | |
| 61 return ''.join(difflib.unified_diff(expected.splitlines(1), | |
| 62 actual.splitlines(1))) | |
| 63 | |
| 64 | |
| 65 def ToString(obj): | |
| 66 """ Returns a string representation of the given object. This differs from the | |
| 67 built-in string function in that it does not give memory locations. | |
| 68 | |
| 69 obj: the object to print. | |
| 70 """ | |
| 71 def sanitize(obj): | |
| 72 if isinstance(obj, list) or isinstance(obj, tuple): | |
| 73 return [sanitize(sub_obj) for sub_obj in obj] | |
| 74 elif isinstance(obj, dict): | |
| 75 rv = {} | |
| 76 for k, v in obj.iteritems(): | |
| 77 rv[k] = sanitize(v) | |
| 78 return rv | |
| 79 elif isinstance(obj, str) or obj is None: | |
| 80 return obj | |
| 81 else: | |
| 82 return '<Object>' | |
| 83 return json.dumps(sanitize(obj), indent=4, sort_keys=True) | |
| 84 | |
| 85 | |
| 86 def FixGitSvnEmail(addr): | |
| 87 """ Git-svn tacks a git-svn-id onto email addresses. This function removes it. | |
| 88 | |
| 89 For example, "skia.buildbots@gmail.com@2bbb7eff-a529-9590-31e7-b0007b416f81" | |
| 90 becomes, "skia.buildbots@gmail.com". Addresses containing a single '@' will be | |
| 91 unchanged. | |
| 92 """ | |
| 93 return '@'.join(addr.split('@')[:2]) | |
| 94 | |
| 95 | |
| 96 class SkiaChangeFilter(ChangeFilter): | |
| 97 """Skia specific subclass of ChangeFilter.""" | |
| 98 | |
| 99 def __init__(self, builders, **kwargs): | |
| 100 self._builders = builders | |
| 101 ChangeFilter.__init__(self, **kwargs) | |
| 102 | |
| 103 def filter_change(self, change): | |
| 104 """Overrides ChangeFilter.filter_change to pass builders to filter_fn. | |
| 105 | |
| 106 The code has been copied from | |
| 107 http://buildbot.net/buildbot/docs/0.8.3/reference/buildbot.schedulers.filter
-pysrc.html#ChangeFilter | |
| 108 with one change: We pass a sequence of builders to the filter function. | |
| 109 """ | |
| 110 if self.filter_fn is not None and not self.filter_fn(change, | |
| 111 self._builders): | |
| 112 return False | |
| 113 for (filt_list, filt_re, filt_fn, chg_attr) in self.checks: | |
| 114 chg_val = getattr(change, chg_attr, '') | |
| 115 if filt_list is not None and chg_val not in filt_list: | |
| 116 return False | |
| 117 if filt_re is not None and ( | |
| 118 chg_val is None or not filt_re.match(chg_val)): | |
| 119 return False | |
| 120 if filt_fn is not None and not filt_fn(chg_val): | |
| 121 return False | |
| 122 return True | |
| 123 | |
| 124 | |
| 125 def _AssertValidString(var, varName='[unknown]'): | |
| 126 """Raises an exception if a var is not a valid string. | |
| 127 | |
| 128 A string is considered valid if it is not None, is not the empty string and is | |
| 129 not just whitespace. | |
| 130 | |
| 131 Args: | |
| 132 var: the variable to validate | |
| 133 varName: name of the variable, for error reporting | |
| 134 """ | |
| 135 if not isinstance(var, str): | |
| 136 raise Exception('variable "%s" is not a string' % varName) | |
| 137 if not var: | |
| 138 raise Exception('variable "%s" is empty' % varName) | |
| 139 if var.isspace(): | |
| 140 raise Exception('variable "%s" is whitespace' % varName) | |
| 141 | |
| 142 | |
| 143 def _AssertValidStringList(var, varName='[unknown]'): | |
| 144 """Raises an exception if var is not a list of valid strings. | |
| 145 | |
| 146 A list is considered valid if it is either empty or if it contains at | |
| 147 least one item and each item it contains is also a valid string. | |
| 148 | |
| 149 Args: | |
| 150 var: the variable to validate | |
| 151 varName: name of the variable, for error reporting | |
| 152 """ | |
| 153 if not isinstance(var, list): | |
| 154 raise Exception('variable "%s" is not a list' % varName) | |
| 155 for index, item in zip(range(len(var)), var): | |
| 156 _AssertValidString(item, '%s[%d]' % (varName, index)) | |
| 157 | |
| 158 | |
| 159 def FileBug(summary, description, owner=None, ccs=None, labels=None): | |
| 160 """Files a bug to the Skia issue tracker. | |
| 161 | |
| 162 Args: | |
| 163 summary: a single-line string to use as the issue summary | |
| 164 description: a multiline string to use as the issue description | |
| 165 owner: email address of the issue owner (as a string), or None if unknown | |
| 166 ccs: email addresses (list of strings) to CC on the bug | |
| 167 labels: labels (list of strings) to apply to the bug | |
| 168 | |
| 169 Returns: | |
| 170 A representation of the issue tracker issue that was filed or raises an | |
| 171 exception if there was a problem. | |
| 172 """ | |
| 173 project_id = 'skia' # This is the project name: skia | |
| 174 key_file = 'key.p12' # Key file from the API console, renamed to key.p12 | |
| 175 service_acct = ('352371350305-b3u8jq5sotdh964othi9ntg9d0pelu77' | |
| 176 '@developer.gserviceaccount.com') # Created with the key | |
| 177 result = {} | |
| 178 if not ccs: | |
| 179 ccs = [] | |
| 180 if not labels: | |
| 181 labels = [] | |
| 182 | |
| 183 if owner is not None: # owner can be None | |
| 184 _AssertValidString(owner, 'owner') | |
| 185 _AssertValidString(summary, 'summary') | |
| 186 _AssertValidString(description, 'description') | |
| 187 _AssertValidStringList(ccs, 'ccs') | |
| 188 _AssertValidStringList(labels, 'labels') | |
| 189 | |
| 190 f = file(key_file, 'rb') | |
| 191 key = f.read() | |
| 192 f.close() | |
| 193 | |
| 194 # Create an httplib2.Http object to handle the HTTP requests and authorize | |
| 195 # it with the credentials. | |
| 196 credentials = SignedJwtAssertionCredentials( | |
| 197 service_acct, | |
| 198 key, | |
| 199 scope='https://www.googleapis.com/auth/projecthosting') | |
| 200 http = httplib2.Http() | |
| 201 http = credentials.authorize(http) | |
| 202 | |
| 203 service = build("projecthosting", "v2", http=http) | |
| 204 | |
| 205 # Insert a new issue into the project. | |
| 206 body = { | |
| 207 'summary': summary, | |
| 208 'description': description | |
| 209 } | |
| 210 | |
| 211 insertparams = { | |
| 212 'projectId': project_id, | |
| 213 'sendEmail': 'true' | |
| 214 } | |
| 215 | |
| 216 if owner is not None: | |
| 217 owner_value = { | |
| 218 'name': owner | |
| 219 } | |
| 220 body['owner'] = owner_value | |
| 221 | |
| 222 cc_values = [] | |
| 223 for cc in ccs: | |
| 224 cc_values.append({'name': cc}) | |
| 225 body['cc'] = cc_values | |
| 226 | |
| 227 body['labels'] = labels | |
| 228 | |
| 229 insertparams['body'] = body | |
| 230 | |
| 231 request = service.issues().insert(**insertparams) | |
| 232 result = request.execute() | |
| 233 | |
| 234 return result | |
| 235 | |
| 236 | |
| 237 # Skip buildbot runs of a CL if its commit log message contains the following | |
| 238 # substring. | |
| 239 SKIP_BUILDBOT_SUBSTRING = '(SkipBuildbotRuns)' | |
| 240 | |
| 241 # If the below regex is found in a CL's commit log message, only run the | |
| 242 # builders specified therein. | |
| 243 RUN_BUILDERS_REGEX = '\(RunBuilders:(.+)\)' | |
| 244 RUN_BUILDERS_RE_COMPILED = re.compile(RUN_BUILDERS_REGEX) | |
| 245 | |
| 246 | |
| 247 def CapWordsToUnderscores(string): | |
| 248 """ Converts a string containing capitalized words to one in which all | |
| 249 characters are lowercase and words are separated by underscores. | |
| 250 | |
| 251 Example: | |
| 252 'Nexus10' becomes 'nexus_10' | |
| 253 | |
| 254 string: string; string to manipulate. | |
| 255 """ | |
| 256 name_parts = [] | |
| 257 for part in re.split('(\d+)', string): | |
| 258 if re.match('(\d+)', part): | |
| 259 name_parts.append(part) | |
| 260 else: | |
| 261 name_parts.extend(re.findall('[A-Z][a-z]*', part)) | |
| 262 return '_'.join([part.lower() for part in name_parts]) | |
| 263 | |
| 264 | |
| 265 def UnderscoresToCapWords(string): | |
| 266 """ Converts a string lowercase words separated by underscores to one in which | |
| 267 words are capitalized and not separated by underscores. | |
| 268 | |
| 269 Example: | |
| 270 'nexus_10' becomes 'Nexus10' | |
| 271 | |
| 272 string: string; string to manipulate. | |
| 273 """ | |
| 274 name_parts = string.split('_') | |
| 275 return ''.join([part.title() for part in name_parts]) | |
| 276 | |
| 277 | |
| 278 # Since we can't modify the existing Helper class, we subclass it here, | |
| 279 # overriding the necessary parts to get things working as we want. | |
| 280 class SkiaHelper(object): | |
| 281 def __init__(self, defaults): | |
| 282 self._defaults = defaults | |
| 283 self._builders = [] | |
| 284 self._factories = {} | |
| 285 self._schedulers = {} | |
| 286 | |
| 287 def Builder(self, name, factory, gatekeeper=None, scheduler=None, | |
| 288 builddir=None, auto_reboot=False, notify_on_missing=False): | |
| 289 # Override the category with the first two parts of the builder name. | |
| 290 name_parts = name.split(builder_name_schema.BUILDER_NAME_SEP) | |
| 291 category = name_parts[0] | |
| 292 subcategory = name_parts[1] if len(name_parts) > 1 else 'default' | |
| 293 full_category = '|'.join((category, subcategory)) | |
| 294 self._builders.append({'name': name, | |
| 295 'factory': factory, | |
| 296 'gatekeeper': gatekeeper, | |
| 297 'schedulers': scheduler.split('|'), | |
| 298 'builddir': builddir, | |
| 299 'category': full_category, | |
| 300 'auto_reboot': auto_reboot, | |
| 301 'notify_on_missing': notify_on_missing}) | |
| 302 | |
| 303 def PeriodicScheduler(self, name, minute=0, hour='*', dayOfMonth='*', | |
| 304 month='*', dayOfWeek='*'): | |
| 305 """Helper method for the Periodic scheduler.""" | |
| 306 if name in self._schedulers: | |
| 307 raise ValueError('Scheduler %s already exists' % name) | |
| 308 self._schedulers[name] = {'type': 'PeriodicScheduler', | |
| 309 'builders': [], | |
| 310 'minute': minute, | |
| 311 'hour': hour, | |
| 312 'dayOfMonth': dayOfMonth, | |
| 313 'month': month, | |
| 314 'dayOfWeek': dayOfWeek} | |
| 315 | |
| 316 def Dependent(self, name, parent): | |
| 317 if name in self._schedulers: | |
| 318 raise ValueError('Scheduler %s already exists' % name) | |
| 319 self._schedulers[name] = {'type': 'Dependent', | |
| 320 'parent': parent, | |
| 321 'builders': []} | |
| 322 | |
| 323 def Factory(self, name, factory): | |
| 324 if name in self._factories: | |
| 325 raise ValueError('Factory %s already exists' % name) | |
| 326 self._factories[name] = factory | |
| 327 | |
| 328 def Scheduler(self, name, treeStableTimer=60, categories=None): | |
| 329 if name in self._schedulers: | |
| 330 raise ValueError('Scheduler %s already exists' % name) | |
| 331 self._schedulers[name] = {'type': 'Scheduler', | |
| 332 'treeStableTimer': treeStableTimer, | |
| 333 'builders': [], | |
| 334 'categories': categories} | |
| 335 | |
| 336 def TryJobSubversion(self, name): | |
| 337 """ Adds a Subversion-based try scheduler. """ | |
| 338 if name in self._schedulers: | |
| 339 raise ValueError('Scheduler %s already exists' % name) | |
| 340 self._schedulers[name] = {'type': 'TryJobSubversion', 'builders': []} | |
| 341 | |
| 342 def TryJobRietveld(self, name): | |
| 343 """ Adds a Rietveld-based try scheduler. """ | |
| 344 if name in self._schedulers: | |
| 345 raise ValueError('Scheduler %s already exists' % name) | |
| 346 self._schedulers[name] = {'type': 'TryJobRietveld', 'builders': []} | |
| 347 | |
| 348 def Update(self, c): | |
| 349 for builder in self._builders: | |
| 350 # Update the schedulers with the builder. | |
| 351 schedulers = builder['schedulers'] | |
| 352 if schedulers: | |
| 353 for scheduler in schedulers: | |
| 354 self._schedulers[scheduler]['builders'].append(builder['name']) | |
| 355 | |
| 356 # Construct the category. | |
| 357 categories = [] | |
| 358 if builder.get('category', None): | |
| 359 categories.append(builder['category']) | |
| 360 if builder.get('gatekeeper', None): | |
| 361 categories.extend(builder['gatekeeper'].split('|')) | |
| 362 category = '|'.join(categories) | |
| 363 | |
| 364 # Append the builder to the list. | |
| 365 new_builder = {'name': builder['name'], | |
| 366 'factory': self._factories[builder['factory']], | |
| 367 'category': category, | |
| 368 'auto_reboot': builder['auto_reboot']} | |
| 369 if builder['builddir']: | |
| 370 new_builder['builddir'] = builder['builddir'] | |
| 371 c['builders'].append(new_builder) | |
| 372 | |
| 373 c['builders'].sort(key=lambda builder: builder['name']) | |
| 374 | |
| 375 # Process the main schedulers. | |
| 376 for s_name in self._schedulers: | |
| 377 scheduler = self._schedulers[s_name] | |
| 378 if scheduler['type'] == 'Scheduler': | |
| 379 def filter_fn(change, builders): | |
| 380 """Filters out if change.comments contains certain keywords. | |
| 381 | |
| 382 The change is filtered out if the commit message contains: | |
| 383 * SKIP_BUILDBOT_SUBSTRING or | |
| 384 * RUN_BUILDERS_REGEX when the scheduler does not contain any of the | |
| 385 specified builders | |
| 386 | |
| 387 Args: | |
| 388 change: An instance of changes.Change. | |
| 389 builders: Sequence of strings. The builders that are run by this | |
| 390 scheduler. | |
| 391 | |
| 392 Returns: | |
| 393 If the change should be filtered out (i.e. not run by the buildbot | |
| 394 code) then False is returned else True is returned. | |
| 395 """ | |
| 396 if SKIP_BUILDBOT_SUBSTRING in change.comments: | |
| 397 return False | |
| 398 match_obj = RUN_BUILDERS_RE_COMPILED.search(change.comments) | |
| 399 if builders and match_obj: | |
| 400 for builder_to_run in match_obj.group(1).split(','): | |
| 401 if builder_to_run.strip() in builders: | |
| 402 break | |
| 403 else: | |
| 404 return False | |
| 405 return True | |
| 406 | |
| 407 skia_change_filter = SkiaChangeFilter( | |
| 408 builders=scheduler['builders'], | |
| 409 branch=skia_vars.GetGlobalVariable('master_branch_name'), | |
| 410 filter_fn=filter_fn) | |
| 411 | |
| 412 instance = Scheduler(name=s_name, | |
| 413 treeStableTimer=scheduler['treeStableTimer'], | |
| 414 builderNames=scheduler['builders'], | |
| 415 change_filter=skia_change_filter) | |
| 416 c['schedulers'].append(instance) | |
| 417 self._schedulers[s_name]['instance'] = instance | |
| 418 | |
| 419 # Process the periodic schedulers. | |
| 420 for s_name in self._schedulers: | |
| 421 scheduler = self._schedulers[s_name] | |
| 422 if scheduler['type'] == 'PeriodicScheduler': | |
| 423 instance = timed.Nightly( | |
| 424 name=s_name, | |
| 425 branch=skia_vars.GetGlobalVariable('master_branch_name'), | |
| 426 builderNames=scheduler['builders'], | |
| 427 minute=scheduler['minute'], | |
| 428 hour=scheduler['hour'], | |
| 429 dayOfMonth=scheduler['dayOfMonth'], | |
| 430 month=scheduler['month'], | |
| 431 dayOfWeek=scheduler['dayOfWeek']) | |
| 432 c['schedulers'].append(instance) | |
| 433 self._schedulers[s_name]['instance'] = instance | |
| 434 | |
| 435 # Process the Rietveld-based try schedulers. | |
| 436 for s_name in self._schedulers: | |
| 437 scheduler = self._schedulers[s_name] | |
| 438 if scheduler['type'] == 'TryJobRietveld': | |
| 439 pools = BuildersPools(s_name) | |
| 440 pools[s_name].extend(scheduler['builders']) | |
| 441 instance = try_job_rietveld.TryJobRietveld( | |
| 442 name=s_name, | |
| 443 pools=pools, | |
| 444 last_good_urls={'skia': None}, | |
| 445 code_review_sites={'skia': config_private.CODE_REVIEW_SITE}, | |
| 446 project='skia') | |
| 447 c['schedulers'].append(instance) | |
| 448 self._schedulers[s_name]['instance'] = instance | |
| 449 | |
| 450 # Process the svn-based try schedulers. | |
| 451 for s_name in self._schedulers: | |
| 452 scheduler = self._schedulers[s_name] | |
| 453 if scheduler['type'] == 'TryJobSubversion': | |
| 454 pools = BuildersPools(s_name) | |
| 455 pools[s_name].extend(scheduler['builders']) | |
| 456 instance = try_job_svn.TryJobSubversion( | |
| 457 name=s_name, | |
| 458 svn_url=TRY_SVN_BASEURL, | |
| 459 last_good_urls={'skia': None}, | |
| 460 code_review_sites={'skia': config_private.CODE_REVIEW_SITE}, | |
| 461 pools=pools) | |
| 462 c['schedulers'].append(instance) | |
| 463 self._schedulers[s_name]['instance'] = instance | |
| 464 | |
| 465 # Process the dependent schedulers. | |
| 466 for s_name in self._schedulers: | |
| 467 scheduler = self._schedulers[s_name] | |
| 468 if scheduler['type'] == 'Dependent': | |
| 469 instance = Dependent( | |
| 470 s_name, | |
| 471 self._schedulers[scheduler['parent']]['instance'], | |
| 472 scheduler['builders']) | |
| 473 c['schedulers'].append(instance) | |
| 474 self._schedulers[s_name]['instance'] = instance | |
| 475 | |
| 476 | |
| 477 def CanMergeBuildRequests(req1, req2): | |
| 478 """ Determine whether or not two BuildRequests can be merged. Note that the | |
| 479 call to buildbot.sourcestamp.SourceStamp.canBeMergedWith() is conspicuously | |
| 480 missing. This is because that method verifies that: | |
| 481 1. req1.source.repository == req2.source.repository | |
| 482 2. req1.source.project == req2.source.project | |
| 483 3. req1.source.branch == req2.source.branch | |
| 484 4. req1.patch == None and req2.patch = None | |
| 485 5. (req1.source.changes and req2.source.changes) or \ | |
| 486 (not req1.source.changes and not req2.source.changes and \ | |
| 487 req1.source.revision == req2.source.revision) | |
| 488 | |
| 489 Of the above, we want 1, 2, 3, and 5. | |
| 490 Instead of 4, we want to make sure that neither request is a Trybot request. | |
| 491 """ | |
| 492 # Verify that the repositories are the same (#1 above). | |
| 493 if req1.source.repository != req2.source.repository: | |
| 494 return False | |
| 495 | |
| 496 # Verify that the projects are the same (#2 above). | |
| 497 if req1.source.project != req2.source.project: | |
| 498 return False | |
| 499 | |
| 500 # Verify that the branches are the same (#3 above). | |
| 501 if req1.source.branch != req2.source.branch: | |
| 502 return False | |
| 503 | |
| 504 # If either is a try request, don't merge (#4 above). | |
| 505 if (builder_name_schema.IsTrybot(req1.buildername) or | |
| 506 builder_name_schema.IsTrybot(req2.buildername)): | |
| 507 return False | |
| 508 | |
| 509 # Verify that either: both requests are associated with changes OR neither | |
| 510 # request is associated with a change but the revisions match (#5 above). | |
| 511 if req1.source.changes and not req2.source.changes: | |
| 512 return False | |
| 513 if not req1.source.changes and req2.source.changes: | |
| 514 return False | |
| 515 if not (req1.source.changes and req2.source.changes): | |
| 516 if req1.source.revision != req2.source.revision: | |
| 517 return False | |
| 518 | |
| 519 return True | |
| 520 | |
| 521 | |
| 522 def get_current_revision(): | |
| 523 """Obtain the checked-out buildbot code revision.""" | |
| 524 checkout_dir = os.path.join(os.path.dirname(__file__), os.pardir, os.pardir) | |
| 525 if os.path.isdir(os.path.join(checkout_dir, '.git')): | |
| 526 return subprocess.check_output(['git', 'rev-parse', 'HEAD']).strip() | |
| 527 elif os.path.isdir(os.path.join(checkout_dir, '.svn')): | |
| 528 return subprocess.check_output(['svnversion', '.']).strip() | |
| 529 raise Exception('Unable to determine version control system.') | |
| OLD | NEW |