Edit

kc3-lang/angle/PRESUBMIT.py

Branch :

  • Show log

    Commit

  • Author : Yuly Novikov
    Date : 2023-02-03 22:44:31
    Hash : 0b3cca88
    Message : Bypass commit message presubmit checks on manual rolls Bug: angleproject:4683 Change-Id: I7e6b3655acfc7adde5bb0da31133e8eca9904207 Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/4221696 Reviewed-by: Roman Lavrov <romanl@google.com> Auto-Submit: Yuly Novikov <ynovikov@chromium.org> Commit-Queue: Roman Lavrov <romanl@google.com> Commit-Queue: Yuly Novikov <ynovikov@chromium.org>

  • PRESUBMIT.py
  • # Copyright 2019 The ANGLE Project Authors. All rights reserved.
    # Use of this source code is governed by a BSD-style license that can be
    # found in the LICENSE file.
    """Top-level presubmit script for code generation.
    
    See http://dev.chromium.org/developers/how-tos/depottools/presubmit-scripts
    for more details on the presubmit API built into depot_tools.
    """
    
    import itertools
    import os
    import re
    import shutil
    import subprocess
    import sys
    import tempfile
    import textwrap
    import pathlib
    
    # This line is 'magic' in that git-cl looks for it to decide whether to
    # use Python3 instead of Python2 when running the code in this file.
    USE_PYTHON3 = True
    
    # Fragment of a regular expression that matches C/C++ and Objective-C++ implementation files and headers.
    _IMPLEMENTATION_AND_HEADER_EXTENSIONS = r'\.(c|cc|cpp|cxx|mm|h|hpp|hxx)$'
    
    # Fragment of a regular expression that matches C++ and Objective-C++ header files.
    _HEADER_EXTENSIONS = r'\.(h|hpp|hxx)$'
    
    _PRIMARY_EXPORT_TARGETS = [
        '//:libEGL',
        '//:libGLESv1_CM',
        '//:libGLESv2',
        '//:translator',
    ]
    
    
    def _SplitIntoMultipleCommits(description_text):
        paragraph_split_pattern = r"(?m)(^\s*$\n)"
        multiple_paragraphs = re.split(paragraph_split_pattern, description_text)
        multiple_commits = [""]
        change_id_pattern = re.compile(r"(?m)^Change-Id: [a-zA-Z0-9]*$")
        for paragraph in multiple_paragraphs:
            multiple_commits[-1] += paragraph
            if change_id_pattern.search(paragraph):
                multiple_commits.append("")
        if multiple_commits[-1] == "":
            multiple_commits.pop()
        return multiple_commits
    
    
    def _CheckCommitMessageFormatting(input_api, output_api):
    
        def _IsLineBlank(line):
            return line.isspace() or line == ""
    
        def _PopBlankLines(lines, reverse=False):
            if reverse:
                while len(lines) > 0 and _IsLineBlank(lines[-1]):
                    lines.pop()
            else:
                while len(lines) > 0 and _IsLineBlank(lines[0]):
                    lines.pop(0)
    
        def _IsTagLine(line):
            return ":" in line
    
        def _CheckTabInCommit(lines):
            return all([line.find("\t") == -1 for line in lines])
    
        allowlist_strings = ['Revert "', 'Roll ', 'Manual roll ', 'Reland ', 'Re-land ']
        summary_linelength_warning_lower_limit = 65
        summary_linelength_warning_upper_limit = 70
        description_linelength_limit = 72
    
        git_output = input_api.change.DescriptionText()
    
        multiple_commits = _SplitIntoMultipleCommits(git_output)
        errors = []
    
        for k in range(len(multiple_commits)):
            commit_msg_lines = multiple_commits[k].splitlines()
            commit_number = len(multiple_commits) - k
            commit_tag = "Commit " + str(commit_number) + ":"
            commit_msg_line_numbers = {}
            for i in range(len(commit_msg_lines)):
                commit_msg_line_numbers[commit_msg_lines[i]] = i + 1
            _PopBlankLines(commit_msg_lines, True)
            _PopBlankLines(commit_msg_lines, False)
            allowlisted = False
            if len(commit_msg_lines) > 0:
                for allowlist_string in allowlist_strings:
                    if commit_msg_lines[0].startswith(allowlist_string):
                        allowlisted = True
                        break
            if allowlisted:
                continue
    
            if not _CheckTabInCommit(commit_msg_lines):
                errors.append(
                    output_api.PresubmitError(commit_tag + "Tabs are not allowed in commit message."))
    
            # the tags paragraph is at the end of the message
            # the break between the tags paragraph is the first line without ":"
            # this is sufficient because if a line is blank, it will not have ":"
            last_paragraph_line_count = 0
            while len(commit_msg_lines) > 0 and _IsTagLine(commit_msg_lines[-1]):
                last_paragraph_line_count += 1
                commit_msg_lines.pop()
            if last_paragraph_line_count == 0:
                errors.append(
                    output_api.PresubmitError(
                        commit_tag +
                        "Please ensure that there are tags (e.g., Bug:, Test:) in your description."))
            if len(commit_msg_lines) > 0:
                if not _IsLineBlank(commit_msg_lines[-1]):
                    output_api.PresubmitError(commit_tag +
                                              "Please ensure that there exists 1 blank line " +
                                              "between tags and description body.")
                else:
                    # pop the blank line between tag paragraph and description body
                    commit_msg_lines.pop()
                    if len(commit_msg_lines) > 0 and _IsLineBlank(commit_msg_lines[-1]):
                        errors.append(
                            output_api.PresubmitError(
                                commit_tag + 'Please ensure that there exists only 1 blank line '
                                'between tags and description body.'))
                        # pop all the remaining blank lines between tag and description body
                        _PopBlankLines(commit_msg_lines, True)
            if len(commit_msg_lines) == 0:
                errors.append(
                    output_api.PresubmitError(commit_tag +
                                              'Please ensure that your description summary'
                                              ' and description body are not blank.'))
                continue
    
            if summary_linelength_warning_lower_limit <= len(commit_msg_lines[0]) \
            <= summary_linelength_warning_upper_limit:
                errors.append(
                    output_api.PresubmitPromptWarning(
                        commit_tag + "Your description summary should be on one line of " +
                        str(summary_linelength_warning_lower_limit - 1) + " or less characters."))
            elif len(commit_msg_lines[0]) > summary_linelength_warning_upper_limit:
                errors.append(
                    output_api.PresubmitError(
                        commit_tag + "Please ensure that your description summary is on one line of " +
                        str(summary_linelength_warning_lower_limit - 1) + " or less characters."))
            commit_msg_lines.pop(0)  # get rid of description summary
            if len(commit_msg_lines) == 0:
                continue
            if not _IsLineBlank(commit_msg_lines[0]):
                errors.append(
                    output_api.PresubmitError(commit_tag +
                                              'Please ensure the summary is only 1 line and '
                                              'there is 1 blank line between the summary '
                                              'and description body.'))
            else:
                commit_msg_lines.pop(0)  # pop first blank line
                if len(commit_msg_lines) == 0:
                    continue
                if _IsLineBlank(commit_msg_lines[0]):
                    errors.append(
                        output_api.PresubmitError(commit_tag +
                                                  'Please ensure that there exists only 1 blank line '
                                                  'between description summary and description body.'))
                    # pop all the remaining blank lines between
                    # description summary and description body
                    _PopBlankLines(commit_msg_lines)
    
            # loop through description body
            while len(commit_msg_lines) > 0:
                line = commit_msg_lines.pop(0)
                # lines starting with 4 spaces or lines without space(urls)
                # are exempt from length check
                if line.startswith("    ") or " " not in line:
                    continue
                if len(line) > description_linelength_limit:
                    errors.append(
                        output_api.PresubmitError(
                            commit_tag + 'Line ' + str(commit_msg_line_numbers[line]) +
                            ' is too long.\n' + '"' + line + '"\n' + 'Please wrap it to ' +
                            str(description_linelength_limit) + ' characters. ' +
                            "Lines without spaces or lines starting with 4 spaces are exempt."))
                    break
        return errors
    
    
    def _CheckChangeHasBugField(input_api, output_api):
        """Requires that the changelist have a Bug: field from a known project."""
        bugs = input_api.change.BugsFromDescription()
        if not bugs:
            return [
                output_api.PresubmitError('Please ensure that your description contains:\n'
                                          '"Bug: angleproject:[bug number]"\n'
                                          'directly above the Change-Id tag.')
            ]
    
        # The bug must be in the form of "project:number".  None is also accepted, which is used by
        # rollers as well as in very minor changes.
        if len(bugs) == 1 and bugs[0] == 'None':
            return []
    
        projects = [
            'angleproject:', 'chromium:', 'dawn:', 'fuchsia:', 'skia:', 'swiftshader:', 'tint:', 'b/'
        ]
        bug_regex = re.compile(r"([a-z]+[:/])(\d+)")
        errors = []
        extra_help = None
    
        for bug in bugs:
            if bug == 'None':
                errors.append(
                    output_api.PresubmitError('Invalid bug tag "None" in presence of other bug tags.'))
                continue
    
            match = re.match(bug_regex, bug)
            if match == None or bug != match.group(0) or match.group(1) not in projects:
                errors.append(output_api.PresubmitError('Incorrect bug tag "' + bug + '".'))
                if not extra_help:
                    extra_help = output_api.PresubmitError('Acceptable format is:\n\n'
                                                           '    Bug: project:bugnumber\n\n'
                                                           'Acceptable projects are:\n\n    ' +
                                                           '\n    '.join(projects))
    
        if extra_help:
            errors.append(extra_help)
    
        return errors
    
    
    def _CheckCodeGeneration(input_api, output_api):
    
        class Msg(output_api.PresubmitError):
            """Specialized error message"""
    
            def __init__(self, message):
                super(output_api.PresubmitError, self).__init__(
                    message,
                    long_text='Please ensure your ANGLE repositiory is synced to tip-of-tree\n'
                    'and all ANGLE DEPS are fully up-to-date by running gclient sync.\n'
                    '\n'
                    'If that fails, run scripts/run_code_generation.py to refresh generated hashes.\n'
                    '\n'
                    'If you are building ANGLE inside Chromium you must bootstrap ANGLE\n'
                    'before gclient sync. See the DevSetup documentation for more details.\n')
    
        code_gen_path = input_api.os_path.join(input_api.PresubmitLocalPath(),
                                               'scripts/run_code_generation.py')
        cmd_name = 'run_code_generation'
        cmd = [input_api.python3_executable, code_gen_path, '--verify-no-dirty']
        test_cmd = input_api.Command(name=cmd_name, cmd=cmd, kwargs={}, message=Msg)
        if input_api.verbose:
            print('Running ' + cmd_name)
        return input_api.RunTests([test_cmd])
    
    
    # Taken directly from Chromium's PRESUBMIT.py
    def _CheckNewHeaderWithoutGnChange(input_api, output_api):
        """Checks that newly added header files have corresponding GN changes.
      Note that this is only a heuristic. To be precise, run script:
      build/check_gn_headers.py.
      """
    
        def headers(f):
            return input_api.FilterSourceFile(f, files_to_check=(r'.+%s' % _HEADER_EXTENSIONS,))
    
        new_headers = []
        for f in input_api.AffectedSourceFiles(headers):
            if f.Action() != 'A':
                continue
            new_headers.append(f.LocalPath())
    
        def gn_files(f):
            return input_api.FilterSourceFile(f, files_to_check=(r'.+\.gn',))
    
        all_gn_changed_contents = ''
        for f in input_api.AffectedSourceFiles(gn_files):
            for _, line in f.ChangedContents():
                all_gn_changed_contents += line
    
        problems = []
        for header in new_headers:
            basename = input_api.os_path.basename(header)
            if basename not in all_gn_changed_contents:
                problems.append(header)
    
        if problems:
            return [
                output_api.PresubmitPromptWarning(
                    'Missing GN changes for new header files',
                    items=sorted(problems),
                    long_text='Please double check whether newly added header files need '
                    'corresponding changes in gn or gni files.\nThis checking is only a '
                    'heuristic. Run build/check_gn_headers.py to be precise.\n'
                    'Read https://crbug.com/661774 for more info.')
            ]
        return []
    
    
    def _CheckExportValidity(input_api, output_api):
        outdir = tempfile.mkdtemp()
        # shell=True is necessary on Windows, as otherwise subprocess fails to find
        # either 'gn' or 'vpython3' even if they are findable via PATH.
        use_shell = input_api.is_windows
        try:
            try:
                subprocess.check_output(['gn', 'gen', outdir], shell=use_shell)
            except subprocess.CalledProcessError as e:
                return [
                    output_api.PresubmitError(
                        'Unable to run gn gen for export_targets.py: %s' % e.output)
                ]
            export_target_script = os.path.join(input_api.PresubmitLocalPath(), 'scripts',
                                                'export_targets.py')
            try:
                subprocess.check_output(
                    ['vpython3', export_target_script, outdir] + _PRIMARY_EXPORT_TARGETS,
                    stderr=subprocess.STDOUT,
                    shell=use_shell)
            except subprocess.CalledProcessError as e:
                if input_api.is_committing:
                    return [output_api.PresubmitError('export_targets.py failed: %s' % e.output)]
                return [
                    output_api.PresubmitPromptWarning(
                        'export_targets.py failed, this may just be due to your local checkout: %s' %
                        e.output)
                ]
            return []
        finally:
            shutil.rmtree(outdir)
    
    
    def _CheckTabsInSourceFiles(input_api, output_api):
        """Forbids tab characters in source files due to a WebKit repo requirement."""
    
        def implementation_and_headers_including_third_party(f):
            # Check third_party files too, because WebKit's checks don't make exceptions.
            return input_api.FilterSourceFile(
                f,
                files_to_check=(r'.+%s' % _IMPLEMENTATION_AND_HEADER_EXTENSIONS,),
                files_to_skip=[f for f in input_api.DEFAULT_FILES_TO_SKIP if not "third_party" in f])
    
        files_with_tabs = []
        for f in input_api.AffectedSourceFiles(implementation_and_headers_including_third_party):
            for (num, line) in f.ChangedContents():
                if '\t' in line:
                    files_with_tabs.append(f)
                    break
    
        if files_with_tabs:
            return [
                output_api.PresubmitError(
                    'Tab characters in source files.',
                    items=sorted(files_with_tabs),
                    long_text=
                    'Tab characters are forbidden in ANGLE source files because WebKit\'s Subversion\n'
                    'repository does not allow tab characters in source files.\n'
                    'Please remove tab characters from these files.')
            ]
        return []
    
    
    # https://stackoverflow.com/a/196392
    def is_ascii(s):
        return all(ord(c) < 128 for c in s)
    
    
    def _CheckNonAsciiInSourceFiles(input_api, output_api):
        """Forbids non-ascii characters in source files."""
    
        def implementation_and_headers(f):
            return input_api.FilterSourceFile(
                f, files_to_check=(r'.+%s' % _IMPLEMENTATION_AND_HEADER_EXTENSIONS,))
    
        files_with_non_ascii = []
        for f in input_api.AffectedSourceFiles(implementation_and_headers):
            for (num, line) in f.ChangedContents():
                if not is_ascii(line):
                    files_with_non_ascii.append("%s: %s" % (f, line))
                    break
    
        if files_with_non_ascii:
            return [
                output_api.PresubmitError(
                    'Non-ASCII characters in source files.',
                    items=sorted(files_with_non_ascii),
                    long_text='Non-ASCII characters are forbidden in ANGLE source files.\n'
                    'Please remove non-ASCII characters from these files.')
            ]
        return []
    
    
    def _CheckCommentBeforeTestInTestFiles(input_api, output_api):
        """Require a comment before TEST_P() and other tests."""
    
        def test_files(f):
            return input_api.FilterSourceFile(
                f, files_to_check=(r'^src/tests/.+\.cpp$', r'^src/.+_unittest\.cpp$'))
    
        tests_with_no_comment = []
        for f in input_api.AffectedSourceFiles(test_files):
            diff = f.GenerateScmDiff()
            last_line_was_comment = False
            for line in diff.splitlines():
                # Skip removed lines
                if line.startswith('-'):
                    continue
    
                new_line_is_comment = line.startswith(' //') or line.startswith('+//')
                new_line_is_test_declaration = (
                    line.startswith('+TEST_P(') or line.startswith('+TEST(') or
                    line.startswith('+TYPED_TEST('))
    
                if new_line_is_test_declaration and not last_line_was_comment:
                    tests_with_no_comment.append(line[1:])
    
                last_line_was_comment = new_line_is_comment
    
        if tests_with_no_comment:
            return [
                output_api.PresubmitError(
                    'Tests without comment.',
                    items=sorted(tests_with_no_comment),
                    long_text='ANGLE requires a comment describing what a test does.')
            ]
        return []
    
    
    def _CheckShaderVersionInShaderLangHeader(input_api, output_api):
        """Requires an update to ANGLE_SH_VERSION when ShaderLang.h or ShaderVars.h change."""
    
        def headers(f):
            return input_api.FilterSourceFile(
                f,
                files_to_check=(r'^include/GLSLANG/ShaderLang.h$', r'^include/GLSLANG/ShaderVars.h$'))
    
        headers_changed = input_api.AffectedSourceFiles(headers)
        if len(headers_changed) == 0:
            return []
    
        # Skip this check for reverts and rolls.  Unlike
        # _CheckCommitMessageFormatting, relands are still checked because the
        # original change might have incremented the version correctly, but the
        # rebase over a new version could accidentally remove that (because another
        # change in the meantime identically incremented it).
        git_output = input_api.change.DescriptionText()
        multiple_commits = _SplitIntoMultipleCommits(git_output)
        for commit in multiple_commits:
            if commit.startswith('Revert') or commit.startswith('Roll'):
                return []
    
        diffs = '\n'.join(f.GenerateScmDiff() for f in headers_changed)
        versions = dict(re.findall(r'^([-+])#define ANGLE_SH_VERSION\s+(\d+)', diffs, re.M))
    
        if len(versions) != 2 or int(versions['+']) <= int(versions['-']):
            return [
                output_api.PresubmitError(
                    'ANGLE_SH_VERSION should be incremented when ShaderLang.h or ShaderVars.h change.',
                )
            ]
        return []
    
    
    def _CheckGClientExists(input_api, output_api, search_limit=None):
        presubmit_path = pathlib.Path(input_api.PresubmitLocalPath())
    
        for current_path in itertools.chain([presubmit_path], presubmit_path.parents):
            gclient_path = current_path.joinpath('.gclient')
            if gclient_path.exists() and gclient_path.is_file():
                return []
            # search_limit parameter is used in unit tests to prevent searching all the way to root
            # directory for reproducibility.
            elif search_limit != None and current_path == search_limit:
                break
    
        return [
            output_api.PresubmitError(
                'Missing .gclient file.',
                long_text=textwrap.fill(
                    width=100,
                    text='The top level directory of the repository must contain a .gclient file.'
                    ' You can follow the steps outlined in the link below to get set up for ANGLE'
                    ' development:') +
                '\n\nhttps://chromium.googlesource.com/angle/angle/+/refs/heads/main/doc/DevSetup.md')
        ]
    
    
    def CheckChangeOnUpload(input_api, output_api):
        results = []
        results.extend(_CheckTabsInSourceFiles(input_api, output_api))
        results.extend(_CheckNonAsciiInSourceFiles(input_api, output_api))
        results.extend(_CheckCommentBeforeTestInTestFiles(input_api, output_api))
        results.extend(_CheckShaderVersionInShaderLangHeader(input_api, output_api))
        results.extend(_CheckCodeGeneration(input_api, output_api))
        results.extend(_CheckChangeHasBugField(input_api, output_api))
        results.extend(input_api.canned_checks.CheckChangeHasDescription(input_api, output_api))
        results.extend(_CheckNewHeaderWithoutGnChange(input_api, output_api))
        results.extend(_CheckExportValidity(input_api, output_api))
        results.extend(
            input_api.canned_checks.CheckPatchFormatted(
                input_api, output_api, result_factory=output_api.PresubmitError))
        results.extend(_CheckCommitMessageFormatting(input_api, output_api))
        results.extend(_CheckGClientExists(input_api, output_api))
    
        return results
    
    
    def CheckChangeOnCommit(input_api, output_api):
        return CheckChangeOnUpload(input_api, output_api)