| #!/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 normalize_platform(platform): | |
| if not platform: | |
| return None | |
| platform = platform.lower() | |
| if platform == "mac" or platform == "macos": | |
| return "macOS" | |
| elif platform == "ios": | |
| return "iOS" | |
| return None | |
| 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 update_expectations_from_file(unexpected_results, project, add=False, platform=None): | |
| 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: | |
| checker_type = None | |
| file_list = [] | |
| for line in unexpected_contents: | |
| if '=>' in line: | |
| if checker_type and file_list: | |
| print(checker_type, file_list) | |
| modify_expectations_for_checker(Checker.find_checker_by_name(checker_type), file_list, project, add, platform) | |
| checker_type = line.split()[-1] | |
| file_list = [] | |
| continue | |
| file_path = line.strip() | |
| if file_path: | |
| file_list.append(file_path) | |
| if checker_type and file_list: | |
| print(checker_type, file_list) | |
| modify_expectations_for_checker(Checker.find_checker_by_name(checker_type), file_list, project, add, platform) | |
| 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 normalize_platform(platform) not in [None, 'macOS', '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: | |
| if normalize_platform(platform) == 'macOS' and normalize_platform(platform) == 'iOS': | |
| expectation_platform = None | |
| elif normalize_platform(platform) == 'macOS' and normalize_platform(platform) == 'iOS': | |
| expectation_platform = None | |
| else: | |
| 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'] | |
| if normalize_platform(expectation_platform) == normalize_platform(platform): | |
| print(f'Path is already present in {expectation_relpath}: {path}') | |
| continue | |
| if normalize_platform(expectation_platform) not in [None, 'macOS', 'iOS']: | |
| print(f'Unexpected platform in {expectation_relpath}: {expectation_platform}') | |
| continue | |
| expectation_map[path]['platform'] = None | |
| else: | |
| expectation_map[path] = {'platform': normalize_platform(platform), 'comments': []} | |
| 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 = normalize_platform(expectation_map[path]['platform']) | |
| if expectation_platform not in [None, normalize_platform(platform)]: | |
| print(f'Expectation not found in {expectation_relpath}: {path} for {normalized_platform(platform)}') | |
| continue # Expectation is for another platform. We're done | |
| if expectation_platform == None: | |
| # None means it currently applies to both. Keep the expectation for the other platform. | |
| expectation_map[path]['platform'] = 'macOS' if normalize_platform(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, args.platform) | |
| else: | |
| update_expectations(vars(args), args.project, args.add, args.platform) | |
| if __name__ == '__main__': | |
| main() |