| #!/usr/bin/env python3 |
| # Copyright (C) 2024 Apple Inc. All rights reserved. |
| # |
| # Redistribution and use in source and binary forms, with or without |
| # modification, are permitted provided that the following conditions |
| # are met: |
| # 1. Redistributions of source code must retain the above copyright |
| # notice, this list of conditions and the following disclaimer. |
| # 2. Redistributions in binary form must reproduce the above copyright |
| # notice, this list of conditions and the following disclaimer in the |
| # documentation and/or other materials provided with the distribution. |
| # |
| # THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS "AS IS" AND |
| # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
| # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| # DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR |
| # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL |
| # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR |
| # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
| # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, |
| # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| import os |
| import re |
| import argparse |
| |
| from webkitpy.safer_cpp.checkers import Checker |
| |
| EXPECTATION_LINE_RE = r'(\s*\[\s*(?P<platform>\w+)\s*\]\s*)?(?P<path>.+)' |
| |
| |
| def parser(): |
| parser = argparse.ArgumentParser(description='Automated tooling for updating safer CPP expectations', epilog='Example: update-safer-cpp-expectations -p WebKit --RefCntblBaseVirtualDtor platform/Scrollbar.h --UncountedCallArgsChecker platform/ScrollAnimator.h') |
| |
| checkers_group = parser.add_argument_group('checker arguments', 'List files to update for each checker') |
| for checker in Checker.enumerate(): |
| checker_name = checker.name() |
| checkers_group.add_argument(f'--{checker_name}', type=str, nargs='+') |
| |
| parser.add_argument( |
| '--project', '-p', |
| choices=Checker.projects(), |
| help='Specify which project expectations you want to update' |
| ) |
| parser.add_argument( |
| '--find-expectations', '-f', |
| dest='expected_file', |
| default=None, |
| help='Check if the given file has expected failures' |
| ) |
| parser.add_argument( |
| '--add-expected-failures', |
| dest='add', |
| action='store_true', |
| default=False, |
| help='OVERRIDE: Add expected failures to the expectations files in SaferCPPExpectations' |
| ) |
| parser.add_argument( |
| '--unexpected-results-file', '-r', |
| dest='unexpected_results_file', |
| default=None, |
| help='Path to unexpected results file' |
| ) |
| parser.add_argument( |
| '--platform', |
| default=None, |
| help='Mac or iOS' |
| ) |
| return parser.parse_args() |
| |
| |
| def modify_expectations_for_checker_from_file(checker, unexpected_contents, project, add=False): |
| path_to_expectations = checker.expectations_path(project) |
| with open(path_to_expectations) as expectations_file: |
| expectations = expectations_file.readlines() |
| prev_len = len(expectations) |
| for line in unexpected_contents: |
| if '=>' in line: |
| new_checker_type = line.split()[-1] |
| modify_expectations_for_checker_from_file(Checker.find_checker_by_name(new_checker_type), unexpected_contents, project, add) |
| elif line.strip(): |
| if not add: |
| try: |
| expectations.remove(line) |
| except ValueError: |
| print(f'Error: {line.strip()} is not in {os.path.relpath(path_to_expectations)}!') |
| elif line not in expectations: |
| expectations.append(line) |
| else: |
| print(f'Error: {line.strip()} is already in {os.path.relpath(path_to_expectations)}!\n') |
| with open(path_to_expectations, 'w') as expectations_file: |
| expectations_file.writelines(sorted(expectations)) |
| print(f'Updated expectations for {checker.name()}!') |
| if not add: |
| print(f'Removed {prev_len - len(expectations)} fixed files.\n') |
| else: |
| print(f'Added {len(expectations) - prev_len} files with issues.\n') |
| |
| |
| def update_expectations_from_file(unexpected_results, project, add=False): |
| filename = os.path.basename(unexpected_results) |
| if not project: |
| projects = [p for p in Checker.projects() if p in filename] |
| if projects: |
| project = projects[0] |
| else: |
| print(f'Could not find a project to update. Please include the project in the filename or pass in the --project argument.') |
| return |
| print(f"{'Adding' if add else 'Removing'} unexpected failures in {project}...\n") |
| with open(unexpected_results, 'r') as unexpected_contents: |
| for line in unexpected_contents: |
| if '=>' in line: |
| checker_type = line.split()[-1] |
| modify_expectations_for_checker_from_file(Checker.find_checker_by_name(checker_type), unexpected_contents, project, add) |
| print(f'Please add any changes to your commit using `git add` and `git commit --amend`.') |
| |
| |
| def modify_expectations_for_checker(checker, unexpected_contents, project, add, platform): |
| if platform: |
| platform = platform.lower() |
| if platform != None and platform != 'mac' and platform != 'ios': |
| print(f'Unknown platform: {platform}') |
| return |
| |
| path_to_expectations = checker.expectations_path(project) |
| expectation_relpath = os.path.relpath(path_to_expectations) |
| checker_name = checker.name() |
| expectation_map = {} |
| with open(path_to_expectations) as expectations_file: |
| expectation_line_re = re.compile(EXPECTATION_LINE_RE) |
| comments = [] |
| for line in expectations_file.read().splitlines(): |
| if line.startswith('//') or line.startswith('#'): |
| comments.append(line) |
| continue |
| match = expectation_line_re.match(line) |
| if not match: |
| print(f'Unexpected line in {expectation_relpath}: {line}') |
| continue |
| expectation_platform = match.group('platform') |
| path = match.group('path') |
| if path in expectation_map: |
| print(f'Conflicting or duplicate line in {expectation_relpath}: {path}') |
| continue |
| expectation_map[match.group('path')] = {'platform': expectation_platform, 'comments': comments} |
| comments = [] |
| |
| count = 0 |
| if add: |
| for path in unexpected_contents: |
| if path in expectation_map: |
| expectation_platform = expectation_map[path]['platform'] |
| lowercase_platform = expectation_platform.lower() if expectation_platform else None |
| if lowercase_platform == platform: |
| print(f'Path is already present in {expectation_relpath}: {path}') |
| continue |
| if lowercase_platform != 'mac' and lowercase_platform != 'ios': |
| print(f'Unexpected platform in {expectation_relpath}: {expectation_platform}') |
| continue |
| expectation_map[path]['platform'] = None |
| count += 1 |
| print(f'Added {count} files with issues.\n') |
| else: |
| for path in unexpected_contents: |
| if path not in expectation_map: |
| print(f'Path not found in {expectation_relpath}: {path}') |
| continue |
| if platform: |
| expectation_platform = expectation_map[path]['platform'] |
| lowercase_platform = expectation_platform.lower() if expectation_platform else None |
| if lowercase_platform != platform: |
| platform_name = 'Mac' if platform == 'mac' else 'iOS' |
| print(f'Expectation not found in {expectation_relpath}: {path} for {platform_name}') |
| continue # Expectation is for another platform. We're done |
| if expectation_platform == None: |
| expectation_map[path]['platform'] = 'Mac' if platform == 'ios' else 'iOS' |
| else: |
| del expectation_map[path] |
| else: |
| del expectation_map[path] |
| count += 1 |
| print(f'Removed {count} fixed files.\n') |
| |
| lines = [] |
| for path in sorted(expectation_map.keys()): |
| platform = expectation_map[path]['platform'] |
| for comment in expectation_map[path]['comments']: |
| lines.append(f'{comment}\n') |
| lines.append(f'[ {platform} ] {path}\n' if platform else f'{path}\n') |
| |
| with open(path_to_expectations, 'w') as expectations_file: |
| expectations_file.writelines(lines) |
| |
| print(f'Updated expectations for {checker_name}!') |
| |
| |
| def update_expectations(args, project, add, platform): |
| if not project: |
| print(f'Could not find a project to update. Please pass in the --project argument.') |
| return |
| print(f"{'Adding' if add else 'Removing'} unexpected failures in {project}...\n") |
| for checker in Checker.enumerate(): |
| files_per_checker = args[checker.name()] |
| if files_per_checker: |
| modify_expectations_for_checker(checker, files_per_checker, project, add, platform) |
| print(f'Please add any changes to your commit using `git add` and `git commit --amend`.') |
| |
| |
| ''' |
| This currently checks against the files in your local checkout at SaferCPPExpectations. |
| Ensure that it is up-to-date before using this script. |
| ''' |
| def is_expected_file(expected_file): |
| print('This checks against local expectations. Ensure your checkout is up-to-date.\n') |
| line = f'{expected_file}\n' |
| issues = False |
| for project in Checker.projects(): |
| for checker in Checker.enumerate(): |
| path_to_expectations = checker.expectations_path(project) |
| with open(path_to_expectations, 'r') as f: |
| expectations = f.read() |
| if line in expectations: |
| if not issues: |
| print(f'{expected_file} has the following issues:') |
| issues = True |
| print(f'- {project} {checker.name()}') |
| if not issues: |
| print(f'{expected_file} has no known issues!') |
| else: |
| print('Follow this link for the latest results: https://build.webkit.org/#/builders?tags=%2BSafer&tags=%2BCPP\n') |
| |
| |
| def main(): |
| args = parser() |
| if args.expected_file: |
| is_expected_file(args.expected_file) |
| return |
| if args.add: |
| print('WARNING: Adding expected failures. Please only add expected failures when you fix false negatives in our tools.') |
| user_input = input('Are you sure you want to proceed? [y/N]: ') or 'N' |
| if user_input[0].lower() != 'y': |
| return |
| if args.unexpected_results_file: |
| update_expectations_from_file(args.unexpected_results_file, args.project, args.add) |
| else: |
| update_expectations(vars(args), args.project, args.add, args.platform) |
| |
| |
| if __name__ == '__main__': |
| main() |