| #!python |
| # Copyright 2012 Google Inc. All Rights Reserved. |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| # |
| # Presubmit script for Syzygy. |
| |
| import itertools |
| import json |
| import os |
| import re |
| import subprocess |
| import sys |
| |
| |
| # Determine the root of the source tree. We use getcwd() instead of __file__ |
| # because gcl loads this script as text and runs it using eval(). In this |
| # context the variable __file__ is undefined. However, gcl assures us that |
| # the current working directory will be the directory containing this file. |
| SYZYGY_ROOT_DIR = os.path.abspath(os.getcwd()) |
| |
| |
| # Bring in some presubmit tools. |
| sys.path.insert(0, os.path.join(SYZYGY_ROOT_DIR, 'py')) |
| import test_utils.presubmit as presubmit # pylint: disable=F0401 |
| |
| |
| # Bring in internal-only presubmit checks. These live in a parallel |
| # repository that is overlaid with the public version of syzygy. The |
| # internal presubmit check is expected to live in the 'internal' |
| # subdirectory off the syzygy root. |
| try: |
| internal_dir = os.path.join(SYZYGY_ROOT_DIR, 'internal') |
| if os.path.isdir(internal_dir): |
| sys.path.insert(0, internal_dir) |
| import internal_presubmit # pylint: disable=F0401 |
| except ImportError: |
| internal_presubmit = None |
| |
| |
| _UNITTEST_MESSAGE = """\ |
| Your %%s unittests must succeed before submitting! To clear this error, |
| run: %s""" % os.path.join(SYZYGY_ROOT_DIR, 'run_all_tests.bat') |
| |
| |
| # License header and copyright line taken from: |
| # http://go/ossreleasing#Apache_License |
| _LICENSE_HEADER = """\ |
| (#!python\n\ |
| )?.*? Copyright 20[0-9][0-9] Google Inc\. All Rights Reserved\.\n\ |
| .*?\n\ |
| .*? Licensed under the Apache License, Version 2\.0 \(the "License"\);\n\ |
| .*? you may not use this file except in compliance with the License\.\n\ |
| .*? You may obtain a copy of the License at\n\ |
| .*?\n\ |
| .*? http://www\.apache\.org/licenses/LICENSE-2\.0\n\ |
| .*? |
| .*? Unless required by applicable law or agreed to in writing, software\n\ |
| .*? distributed under the License is distributed on an "AS IS" BASIS,\n\ |
| .*? WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied\.\n\ |
| .*? See the License for the specific language governing permissions and\n\ |
| .*? limitations under the License\.\n\ |
| """ |
| |
| |
| # Regular expressions to recognize source and header files. |
| # These are lifted from presubmit_support.py in depot_tools and are |
| # formulated as a list of regex strings so that they can be passed to |
| # input_api.FilterSourceFile() as the white_list parameter. |
| _CC_SOURCES = (r'.+\.c$', r'.+\.cc$', r'.+\.cpp$', r'.+\.rc$') |
| _CC_HEADERS = (r'.+\.h$', r'.+\.inl$', r'.+\.hxx$', r'.+\.hpp$') |
| _CC_FILES = _CC_SOURCES + _CC_HEADERS |
| _CC_SOURCES_RE = re.compile('|'.join('(?:%s)' % x for x in _CC_SOURCES)) |
| |
| # Ignore the headers present in the binaries directory, as they're a copy of |
| # another header, making the header guard invalid. |
| _CC_FILES_BLACKLIST = [r'syzygy\\binaries\\.+\.h$'] |
| |
| # Regular expressions used to extract headers and recognize empty lines. |
| _INCLUDE_RE = re.compile(r'^\s*#\s*include\s+(?P<header>[<"][^<"]+[>"])' |
| r'\s*(?://.*(?P<nolint>NOLINT).*)?$') |
| _COMMENT_OR_BLANK_RE = re.compile(r'^\s*(?://.)?$') |
| |
| |
| def _IsSourceHeaderPair(source_path, header): |
| # Returns true if source and header are a matched pair. |
| # Source is the path on disk to the source file and header is the include |
| # reference to the header (i.e., "blah/foo.h" or <blah/foo.h> including the |
| # outer quotes or brackets. |
| if not _CC_SOURCES_RE.match(source_path): |
| return False |
| |
| source_root = os.path.splitext(source_path)[0] |
| if source_root.endswith('_unittest'): |
| source_root = source_root[0:-9] |
| include = os.path.normpath(source_root + '.h') |
| header = os.path.normpath(header[1:-1]) |
| |
| return include.endswith(header) |
| |
| |
| def _GetHeaderCompareKey(source_path, header): |
| if _IsSourceHeaderPair(source_path, header): |
| # We put the header that corresponds to this source file first. |
| group = 0 |
| elif header.startswith('<'): |
| # C++ system headers should come after C system headers. |
| group = 1 if header.endswith('.h>') else 2 |
| else: |
| group = 3 |
| dirname, basename = os.path.split(header[1:-1]) |
| return (group, dirname.lower(), basename.lower()) |
| |
| |
| def _GetHeaderCompareKeyFunc(source): |
| return lambda header : _GetHeaderCompareKey(source, header) |
| |
| |
| def _HeaderGroups(source_lines): |
| # Generates lists of headers in source, one per block of headers. |
| # Each generated value is a tuple (line, headers) denoting on which |
| # line of the source file an uninterrupted sequences of includes begins, |
| # and the list of included headers (paths including the quotes or angle |
| # brackets). |
| start_line, headers = None, [] |
| for line, num in itertools.izip(source_lines, itertools.count(1)): |
| match = _INCLUDE_RE.match(line) |
| if match: |
| # The win32 api has all sorts of implicit include order dependencies. |
| # Rather than encode exceptions for these, we require that they be |
| # excluded from the ordering by a // NOLINT comment. |
| if not match.group('nolint'): |
| headers.append(match.group('header')) |
| if start_line is None: |
| # We just started a new run of headers. |
| start_line = num |
| elif headers and not _COMMENT_OR_BLANK_RE.match(line): |
| # Any non-empty or non-comment line interrupts a sequence of includes. |
| assert start_line is not None |
| yield (start_line, headers) |
| start_line = None |
| headers = [] |
| |
| # Just in case we have some headers we haven't yielded yet, this is our |
| # last chance to do so. |
| if headers: |
| assert start_line is not None |
| yield (start_line, headers) |
| |
| |
| def CheckIncludeOrder(input_api, output_api): |
| """Checks that the C/C++ include order is correct.""" |
| errors = [] |
| is_cc_file = lambda x: input_api.FilterSourceFile(x, white_list=_CC_FILES) |
| for f in input_api.AffectedFiles(include_deletes=False, |
| file_filter=is_cc_file): |
| for line_num, group in _HeaderGroups(f.NewContents()): |
| sorted_group = sorted(group, key=_GetHeaderCompareKeyFunc(f.LocalPath())) |
| if group != sorted_group: |
| message = '%s, line %s: Out of order includes. ' \ |
| 'Expected:\n\t#include %s' % ( |
| f.LocalPath(), |
| line_num, |
| '\n\t#include '.join(sorted_group)) |
| errors.append(output_api.PresubmitPromptWarning(message)) |
| return errors |
| |
| |
| def CheckUnittestsRan(input_api, output_api, committing, configuration): |
| """Checks that the unittests success file is newer than any modified file""" |
| return presubmit.CheckTestSuccess(input_api, output_api, committing, |
| configuration, 'ALL', |
| message=_UNITTEST_MESSAGE % configuration) |
| |
| |
| def CheckEnforcedChanges(input_api, output_api, committing, enforced): |
| """Enforces changes based on the provided rules. |
| |
| |enforced| is a list of 2-tuples, where each entry is a list of file |
| names relative to the repository root. If all of the files in |
| the first list have been changed, then all of the files in the second |
| list must also be changed. |
| """ |
| |
| errors = [] |
| |
| changed = {} |
| for f in input_api.AffectedFiles(include_deletes=False): |
| changed[f.LocalPath()] = True |
| |
| for (a, b) in enforced: |
| all_changed = all(changed.get(f, False) for f in a) |
| if not all_changed: |
| continue |
| |
| for f in b: |
| if f not in changed: |
| errors.append(output_api.PresubmitPromptWarning( |
| '%s needs to be updated.' % f)) |
| |
| return errors |
| |
| |
| def CheckReleaseNotes(input_api, output_api, committing): |
| version = os.path.join('syzygy', 'SYZYGY_VERSION') |
| release_notes = os.path.join('syzygy', 'build', 'RELEASE-NOTES.TXT') |
| return CheckEnforcedChanges(input_api, output_api, committing, |
| [[[version], [release_notes]]]) |
| |
| |
| def CheckReadMe(input_api, output_api, committing): |
| binaries = os.path.join('syzygy', 'build', 'binaries.gypi') |
| readme = os.path.join('syzygy', 'build', 'README.TXT.template') |
| return CheckEnforcedChanges(input_api, output_api, committing, |
| [[[binaries], [readme]]]) |
| |
| |
| def CheckChange(input_api, output_api, committing): |
| if 'dcommit' in sys.argv: |
| return [output_api.PresubmitPromptWarning( |
| 'You must use "git cl land" and not "git cl dcommit".')] |
| |
| # The list of (canned) checks we perform on all files in all changes. |
| checks = [ |
| CheckIncludeOrder, |
| input_api.canned_checks.CheckChangeHasDescription, |
| input_api.canned_checks.CheckChangeHasNoCrAndHasOnlyOneEol, |
| input_api.canned_checks.CheckChangeHasNoTabs, |
| input_api.canned_checks.CheckChangeHasNoStrayWhitespace, |
| input_api.canned_checks.CheckDoNotSubmit, |
| input_api.canned_checks.CheckGenderNeutral, |
| ] |
| |
| results = [] |
| for check in checks: |
| results += check(input_api, output_api) |
| |
| results += input_api.canned_checks.CheckLongLines(input_api, output_api, 80) |
| |
| # We run lint only on C/C++ files so that we avoid getting notices about |
| # files being ignored. |
| is_cc_file = lambda x: input_api.FilterSourceFile(x, white_list=_CC_FILES, |
| black_list=_CC_FILES_BLACKLIST) |
| results += input_api.canned_checks.CheckChangeLintsClean( |
| input_api, output_api, source_file_filter=is_cc_file) |
| |
| # We check the license on the default recognized source file types, as well |
| # as GYP and Python files. |
| gyp_file_re = r'.+\.gypi?$' |
| py_file_re = r'.+\.py$' |
| white_list = input_api.DEFAULT_WHITE_LIST + (gyp_file_re, py_file_re) |
| sources = lambda x: input_api.FilterSourceFile(x, white_list=white_list) |
| results += input_api.canned_checks.CheckLicense(input_api, |
| output_api, |
| _LICENSE_HEADER, |
| source_file_filter=sources) |
| |
| results += CheckReleaseNotes(input_api, output_api, committing) |
| results += CheckReadMe(input_api, output_api, committing) |
| results += CheckUnittestsRan(input_api, output_api, committing, "Debug") |
| results += CheckUnittestsRan(input_api, output_api, committing, "Release") |
| |
| if internal_presubmit: |
| results += internal_presubmit.CheckChange(input_api, |
| output_api, |
| committing) |
| |
| return results |
| |
| |
| def CheckChangeOnUpload(input_api, output_api): |
| return CheckChange(input_api, output_api, False) |
| |
| |
| def CheckChangeOnCommit(input_api, output_api): |
| return CheckChange(input_api, output_api, True) |
| |
| # Unused argument - pylint: disable=W0613 |
| def GetPreferredTryMasters(project, change): # pragma: no cover |
| cq_config_path = os.path.join( |
| change.RepositoryRoot(), 'syzygy', 'infra', 'config', 'cq.cfg') |
| # commit_queue.py below is a script in depot_tools directory, which has a |
| # 'builders' command to retrieve a list of CQ builders from the CQ config. |
| masters = json.loads(subprocess.check_output( |
| ['commit_queue', 'builders', cq_config_path], shell=True)) |
| try_config = {} |
| for master in masters: |
| try_config.setdefault(master, {}) |
| for builder in masters[master]: |
| # Do not trigger presubmit builders, since they're likely to fail |
| # (e.g. OWNERS checks before finished code review), and we're |
| # running local presubmit anyway. |
| if 'presubmit' not in builder.lower(): |
| try_config[master][builder] = ['defaulttests'] |
| return try_config |