OLD | NEW |
1 # Copyright (c) 2013 The Chromium Authors. All rights reserved. | 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 | 2 # Use of this source code is governed by a BSD-style license that can be |
3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
4 | 4 |
5 | 5 |
6 """Top-level presubmit script for Skia. | 6 """Top-level presubmit script for Skia. |
7 | 7 |
8 See http://dev.chromium.org/developers/how-tos/depottools/presubmit-scripts | 8 See http://dev.chromium.org/developers/how-tos/depottools/presubmit-scripts |
9 for more details about the presubmit API built into gcl. | 9 for more details about the presubmit API built into gcl. |
10 """ | 10 """ |
11 | 11 |
| 12 import collections |
12 import csv | 13 import csv |
13 import fnmatch | 14 import fnmatch |
14 import os | 15 import os |
15 import re | 16 import re |
16 import subprocess | 17 import subprocess |
17 import sys | 18 import sys |
18 import traceback | 19 import traceback |
19 | 20 |
20 | 21 |
21 REVERT_CL_SUBJECT_PREFIX = 'Revert ' | 22 REVERT_CL_SUBJECT_PREFIX = 'Revert ' |
22 | 23 |
23 SKIA_TREE_STATUS_URL = 'http://skia-tree-status.appspot.com' | 24 SKIA_TREE_STATUS_URL = 'http://skia-tree-status.appspot.com' |
24 | 25 |
25 CQ_KEYWORDS_THAT_NEED_APPENDING = ('CQ_INCLUDE_TRYBOTS', 'CQ_EXTRA_TRYBOTS', | |
26 'CQ_EXCLUDE_TRYBOTS', 'CQ_TRYBOTS') | |
27 | |
28 # Please add the complete email address here (and not just 'xyz@' or 'xyz'). | 26 # Please add the complete email address here (and not just 'xyz@' or 'xyz'). |
29 PUBLIC_API_OWNERS = ( | 27 PUBLIC_API_OWNERS = ( |
30 'reed@chromium.org', | 28 'reed@chromium.org', |
31 'reed@google.com', | 29 'reed@google.com', |
32 'bsalomon@chromium.org', | 30 'bsalomon@chromium.org', |
33 'bsalomon@google.com', | 31 'bsalomon@google.com', |
34 'djsollen@chromium.org', | 32 'djsollen@chromium.org', |
35 'djsollen@google.com', | 33 'djsollen@google.com', |
36 ) | 34 ) |
37 | 35 |
38 AUTHORS_FILE_NAME = 'AUTHORS' | 36 AUTHORS_FILE_NAME = 'AUTHORS' |
39 | 37 |
40 DOCS_PREVIEW_URL = 'https://skia.org/?cl=' | 38 DOCS_PREVIEW_URL = 'https://skia.org/?cl=' |
41 | 39 |
| 40 # Path to CQ bots feature is described in skbug.com/4364 |
| 41 PATH_PREFIX_TO_EXTRA_TRYBOTS = { |
| 42 # pylint: disable=line-too-long |
| 43 'cmake/': 'client.skia.compile:Build-Mac10.9-Clang-x86_64-Release-CMake-Tryb
ot,Build-Ubuntu-GCC-x86_64-Release-CMake-Trybot', |
| 44 # pylint: disable=line-too-long |
| 45 'src/opts/': 'client.skia:Test-Ubuntu-GCC-GCE-CPU-AVX2-x86_64-Release-SKNX_N
O_SIMD-Trybot', |
| 46 |
| 47 # Below are examples to show what is possible with this feature. |
| 48 # 'src/svg/': 'master1:abc;master2:def', |
| 49 # 'src/svg/parser/': 'master3:ghi,jkl;master4:mno', |
| 50 # 'src/image/SkImage_Base.h': 'master5:pqr,stu;master1:abc1;master2:def', |
| 51 } |
| 52 |
42 | 53 |
43 def _CheckChangeHasEol(input_api, output_api, source_file_filter=None): | 54 def _CheckChangeHasEol(input_api, output_api, source_file_filter=None): |
44 """Checks that files end with atleast one \n (LF).""" | 55 """Checks that files end with atleast one \n (LF).""" |
45 eof_files = [] | 56 eof_files = [] |
46 for f in input_api.AffectedSourceFiles(source_file_filter): | 57 for f in input_api.AffectedSourceFiles(source_file_filter): |
47 contents = input_api.ReadFile(f, 'rb') | 58 contents = input_api.ReadFile(f, 'rb') |
48 # Check that the file ends in atleast one newline character. | 59 # Check that the file ends in atleast one newline character. |
49 if len(contents) > 1 and contents[-1:] != '\n': | 60 if len(contents) > 1 and contents[-1:] != '\n': |
50 eof_files.append(f.LocalPath()) | 61 eof_files.append(f.LocalPath()) |
51 | 62 |
(...skipping 280 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
332 | 343 |
333 This hook does the following: | 344 This hook does the following: |
334 * Adds a link to preview docs changes if there are any docs changes in the CL. | 345 * Adds a link to preview docs changes if there are any docs changes in the CL. |
335 * Adds 'NOTRY=true' if the CL contains only docs changes. | 346 * Adds 'NOTRY=true' if the CL contains only docs changes. |
336 * Adds 'NOTREECHECKS=true' for non master branch changes since they do not | 347 * Adds 'NOTREECHECKS=true' for non master branch changes since they do not |
337 need to be gated on the master branch's tree. | 348 need to be gated on the master branch's tree. |
338 * Adds 'NOTRY=true' for non master branch changes since trybots do not yet | 349 * Adds 'NOTRY=true' for non master branch changes since trybots do not yet |
339 work on them. | 350 work on them. |
340 * Adds 'NOPRESUBMIT=true' for non master branch changes since those don't | 351 * Adds 'NOPRESUBMIT=true' for non master branch changes since those don't |
341 run the presubmit checks. | 352 run the presubmit checks. |
| 353 * Adds extra trybots for the paths defined in PATH_TO_EXTRA_TRYBOTS. |
342 """ | 354 """ |
343 | 355 |
344 results = [] | 356 results = [] |
345 atleast_one_docs_change = False | 357 atleast_one_docs_change = False |
346 all_docs_changes = True | 358 all_docs_changes = True |
347 for affected_file in change.AffectedFiles(): | 359 for affected_file in change.AffectedFiles(): |
348 affected_file_path = affected_file.LocalPath() | 360 affected_file_path = affected_file.LocalPath() |
349 file_path, _ = os.path.splitext(affected_file_path) | 361 file_path, _ = os.path.splitext(affected_file_path) |
350 if 'site' == file_path.split(os.path.sep)[0]: | 362 if 'site' == file_path.split(os.path.sep)[0]: |
351 atleast_one_docs_change = True | 363 atleast_one_docs_change = True |
(...skipping 49 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
401 output_api.PresubmitNotifyResult( | 413 output_api.PresubmitNotifyResult( |
402 'Trybots do not yet work for non-master branches. ' | 414 'Trybots do not yet work for non-master branches. ' |
403 'Automatically added \'NOTRY=true\' to the CL\'s description')) | 415 'Automatically added \'NOTRY=true\' to the CL\'s description')) |
404 if not re.search( | 416 if not re.search( |
405 r'^NOPRESUBMIT=true$', new_description, re.M | re.I): | 417 r'^NOPRESUBMIT=true$', new_description, re.M | re.I): |
406 new_description += "\nNOPRESUBMIT=true" | 418 new_description += "\nNOPRESUBMIT=true" |
407 results.append( | 419 results.append( |
408 output_api.PresubmitNotifyResult( | 420 output_api.PresubmitNotifyResult( |
409 'Branch changes do not run the presubmit checks.')) | 421 'Branch changes do not run the presubmit checks.')) |
410 | 422 |
411 # Read and process the HASHTAGS file. | 423 # Automatically set CQ_EXTRA_TRYBOTS if any of the changed files here begin |
412 hashtags_fullpath = os.path.join(change._local_root, 'HASHTAGS') | 424 # with the paths of interest. |
413 with open(hashtags_fullpath, 'rb') as hashtags_csv: | 425 cq_master_to_trybots = collections.defaultdict(set) |
414 hashtags_reader = csv.reader(hashtags_csv, delimiter=',') | 426 for affected_file in change.AffectedFiles(): |
415 for row in hashtags_reader: | 427 affected_file_path = affected_file.LocalPath() |
416 if not row or row[0].startswith('#'): | 428 for path_prefix, extra_bots in PATH_PREFIX_TO_EXTRA_TRYBOTS.iteritems(): |
417 # Ignore empty lines and comments | 429 if affected_file_path.startswith(path_prefix): |
418 continue | 430 results.append( |
419 hashtag = row[0] | 431 output_api.PresubmitNotifyResult( |
420 # Search for the hashtag in the description. | 432 'Your CL modifies the path %s.\nAutomatically adding %s to ' |
421 if re.search('#%s' % hashtag, new_description, re.M | re.I): | 433 'the CL description.' % (affected_file_path, extra_bots))) |
422 for mapped_text in row[1:]: | 434 _MergeCQExtraTrybotsMaps( |
423 # Special case handling for CQ_KEYWORDS_THAT_NEED_APPENDING. | 435 cq_master_to_trybots, _GetCQExtraTrybotsMap(extra_bots)) |
424 appended_description = _HandleAppendingCQKeywords( | 436 if cq_master_to_trybots: |
425 hashtag, mapped_text, new_description, results, output_api) | 437 new_description = _AddCQExtraTrybotsToDesc( |
426 if appended_description: | 438 cq_master_to_trybots, new_description) |
427 new_description = appended_description | |
428 continue | |
429 | |
430 # Add the mapped text if it does not already exist in the | |
431 # CL's description. | |
432 if not re.search( | |
433 r'^%s$' % mapped_text, new_description, re.M | re.I): | |
434 new_description += '\n%s' % mapped_text | |
435 results.append( | |
436 output_api.PresubmitNotifyResult( | |
437 'Found \'#%s\', automatically added \'%s\' to the CL\'s ' | |
438 'description' % (hashtag, mapped_text))) | |
439 | 439 |
440 # If the description has changed update it. | 440 # If the description has changed update it. |
441 if new_description != original_description: | 441 if new_description != original_description: |
442 rietveld_obj.update_description(issue, new_description) | 442 rietveld_obj.update_description(issue, new_description) |
443 | 443 |
444 return results | 444 return results |
445 | 445 |
446 | 446 |
447 def _HandleAppendingCQKeywords(hashtag, keyword_and_value, description, | 447 def _AddCQExtraTrybotsToDesc(cq_master_to_trybots, description): |
448 results, output_api): | 448 """Adds the specified master and trybots to the CQ_EXTRA_TRYBOTS keyword. |
449 """Handles the CQ keywords that need appending if specified in hashtags.""" | 449 |
450 keyword = keyword_and_value.split('=')[0] | 450 If the keyword already exists in the description then it appends to it only |
451 if keyword in CQ_KEYWORDS_THAT_NEED_APPENDING: | 451 if the specified values do not already exist. |
452 # If the keyword is already in the description then append to it. | 452 If the keyword does not exist then it creates a new section in the |
453 match = re.search( | 453 description. |
454 r'^%s=(.*)$' % keyword, description, re.M | re.I) | 454 """ |
455 if match: | 455 match = re.search(r'^CQ_EXTRA_TRYBOTS=(.*)$', description, re.M | re.I) |
456 old_values = match.group(1).split(';') | 456 if match: |
457 new_value = keyword_and_value.split('=')[1] | 457 original_trybots_map = _GetCQExtraTrybotsMap(match.group(1)) |
458 if new_value in old_values: | 458 _MergeCQExtraTrybotsMaps(cq_master_to_trybots, original_trybots_map) |
459 # Do not need to do anything here. | 459 new_description = description.replace( |
460 return description | 460 match.group(0), _GetCQExtraTrybotsStr(cq_master_to_trybots)) |
461 # Update the description with the new values. | 461 else: |
462 new_description = description.replace( | 462 new_description = description + "\n%s" % ( |
463 match.group(0), "%s;%s" % (match.group(0), new_value)) | 463 _GetCQExtraTrybotsStr(cq_master_to_trybots)) |
464 results.append( | 464 return new_description |
465 output_api.PresubmitNotifyResult( | 465 |
466 'Found \'#%s\', automatically appended \'%s\' to %s in ' | 466 |
467 'the CL\'s description' % (hashtag, new_value, keyword))) | 467 def _MergeCQExtraTrybotsMaps(dest_map, map_to_be_consumed): |
468 return new_description | 468 """Merges two maps of masters to trybots into one.""" |
469 return None | 469 for master, trybots in map_to_be_consumed.iteritems(): |
| 470 dest_map[master].update(trybots) |
| 471 return dest_map |
| 472 |
| 473 |
| 474 def _GetCQExtraTrybotsMap(cq_extra_trybots_str): |
| 475 """Parses the CQ_EXTRA_TRYBOTS str and returns a map of masters to trybots.""" |
| 476 cq_master_to_trybots = collections.defaultdict(set) |
| 477 for section in cq_extra_trybots_str.split(';'): |
| 478 if section: |
| 479 master, bots = section.split(':') |
| 480 cq_master_to_trybots[master].update(bots.split(',')) |
| 481 return cq_master_to_trybots |
| 482 |
| 483 |
| 484 def _GetCQExtraTrybotsStr(cq_master_to_trybots): |
| 485 """Constructs the CQ_EXTRA_TRYBOTS str from a map of masters to trybots.""" |
| 486 sections = [] |
| 487 for master, trybots in cq_master_to_trybots.iteritems(): |
| 488 sections.append('%s:%s' % (master, ','.join(trybots))) |
| 489 return 'CQ_EXTRA_TRYBOTS=%s' % ';'.join(sections) |
470 | 490 |
471 | 491 |
472 def CheckChangeOnCommit(input_api, output_api): | 492 def CheckChangeOnCommit(input_api, output_api): |
473 """Presubmit checks for the change on commit. | 493 """Presubmit checks for the change on commit. |
474 | 494 |
475 The following are the presubmit checks: | 495 The following are the presubmit checks: |
476 * Check change has one and only one EOL. | 496 * Check change has one and only one EOL. |
477 * Ensures that the Skia tree is open in | 497 * Ensures that the Skia tree is open in |
478 http://skia-tree-status.appspot.com/. Shows a warning if it is in 'Caution' | 498 http://skia-tree-status.appspot.com/. Shows a warning if it is in 'Caution' |
479 state and an error if it is in 'Closed' state. | 499 state and an error if it is in 'Closed' state. |
480 """ | 500 """ |
481 results = [] | 501 results = [] |
482 results.extend(_CommonChecks(input_api, output_api)) | 502 results.extend(_CommonChecks(input_api, output_api)) |
483 results.extend( | 503 results.extend( |
484 _CheckTreeStatus(input_api, output_api, json_url=( | 504 _CheckTreeStatus(input_api, output_api, json_url=( |
485 SKIA_TREE_STATUS_URL + '/banner-status?format=json'))) | 505 SKIA_TREE_STATUS_URL + '/banner-status?format=json'))) |
486 results.extend(_CheckLGTMsForPublicAPI(input_api, output_api)) | 506 results.extend(_CheckLGTMsForPublicAPI(input_api, output_api)) |
487 results.extend(_CheckOwnerIsInAuthorsFile(input_api, output_api)) | 507 results.extend(_CheckOwnerIsInAuthorsFile(input_api, output_api)) |
488 return results | 508 return results |
OLD | NEW |