blob: 98efc9f96d1e2f00a4c2c319b16b3b7c2b2014cd [file] [log] [blame] [edit]
#!/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()