- Sort failures alphabetically - Add way to suppress error when necessary - Add way to run test on a subset of files for testing - Fix case where number of files is less than number of workers
145 lines
4.4 KiB
Python
145 lines
4.4 KiB
Python
# Copyright (C) 2025 The Android Open Source Project
|
|
#
|
|
# 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.
|
|
|
|
import argparse
|
|
import os
|
|
import glob
|
|
import yaml
|
|
import hashlib
|
|
import concurrent.futures
|
|
import re
|
|
|
|
from utils import execute, ArgParseImpl
|
|
|
|
def get_line(file_name, offset):
|
|
with open(f'{file_name}', 'rb') as file:
|
|
bytes = file.read()[0:offset]
|
|
f_str = bytes.decode('utf-8')
|
|
return len(f_str.split('\n'))
|
|
return -1
|
|
|
|
def get_func_name(msg):
|
|
pattern = r"\'(.+)\'"
|
|
res = re.findall(pattern, msg)
|
|
if len(res) > 0:
|
|
return res[0].replace("'", '')
|
|
return msg
|
|
|
|
def run_tidy(files):
|
|
if len(files) == 0:
|
|
return []
|
|
files_str = ' '.join(files)
|
|
hid = hashlib.md5(files_str.encode('utf-8')).hexdigest()
|
|
_, _ = execute(f'clang-tidy --export-fixes=/tmp/{hid}.yaml --quiet --checks=-*,bugprone-exception-escape {files_str}')
|
|
results = []
|
|
with open(f'/tmp/{hid}.yaml', 'r') as f:
|
|
data = yaml.safe_load(f)
|
|
for d in data['Diagnostics']:
|
|
if d['DiagnosticName'] != 'bugprone-exception-escape':
|
|
continue
|
|
msg = d['DiagnosticMessage']
|
|
fpath = msg['FilePath']
|
|
offset = msg['FileOffset']
|
|
line_num = get_line(fpath, offset)
|
|
results.append((msg['FilePath'].replace(f'{os.getcwd()}/', ''), line_num, get_func_name(msg['Message'])))
|
|
return results
|
|
|
|
DEFAULT_SRC_GLOBS=[
|
|
'filament/**/*.mm',
|
|
'filament/**/*.cpp',
|
|
'filament/**/*.h',
|
|
]
|
|
|
|
def exception_escape_test(src_globs=DEFAULT_SRC_GLOBS):
|
|
files = []
|
|
if not isinstance(src_globs, list):
|
|
src_globs = [src_globs]
|
|
for i in src_globs:
|
|
files += glob.glob(i, recursive=True)
|
|
|
|
num_workers = min(len(files), 5) # Number of threads to spawn
|
|
part_len = len(files) // num_workers
|
|
workloads = []
|
|
for i in range(num_workers):
|
|
next = min(len(files), part_len)
|
|
workloads.append(files[0:next])
|
|
files = files[next:]
|
|
|
|
all_results = []
|
|
with concurrent.futures.ThreadPoolExecutor(max_workers=num_workers) as executor:
|
|
future_to_worker_id = {executor.submit(run_tidy, workloads[i]): i for i in range(num_workers)}
|
|
|
|
for future in concurrent.futures.as_completed(future_to_worker_id):
|
|
worker_id = future_to_worker_id[future]
|
|
try:
|
|
all_results.extend(future.result())
|
|
except Exception as exc:
|
|
print(f"Main: Worker {worker_id} generated an exception: {exc}")
|
|
test_name = 'code-correctness::exception-escape'
|
|
failure_str_lines = []
|
|
if len(all_results) > 0:
|
|
all_results.sort(key=lambda x: x[0])
|
|
failure_str_lines.append(f'Number of failures: {len(all_results)}')
|
|
for fname, line_num, msg in all_results:
|
|
failure_str_lines.append(f'{fname}({line_num}): {msg}()')
|
|
return (len(all_results) == 0, failure_str_lines)
|
|
|
|
TESTS = [
|
|
(
|
|
exception_escape_test,
|
|
# Test name
|
|
'exception-escape',
|
|
|
|
# Test description
|
|
'An exception may be thrown in a function which should not throw exceptions. '
|
|
'Consider adding \'NOLINT(bugprone-exception-escape)\' for valid suppression this check.',
|
|
|
|
# Maps a command line argument to a function argument
|
|
[('exception_escape_globs', 'src_globs')],
|
|
)
|
|
]
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser()
|
|
|
|
for f, name, desc, args in TESTS:
|
|
for cmd_arg_name, _ in args:
|
|
parser.add_argument(f'--{cmd_arg_name}', required=False)
|
|
|
|
args = parser.parse_args()
|
|
|
|
has_failures = False
|
|
for test_func, test_name, test_desc, arguments in TESTS:
|
|
func_args = {}
|
|
for cmdline_arg, func_arg in arguments:
|
|
arg_val = getattr(args, cmdline_arg, None)
|
|
if arg_val is not None:
|
|
func_args[func_arg] = arg_val.split(',') if ',' in arg_val else arg_val
|
|
|
|
result, res_strs = test_func(**func_args)
|
|
|
|
ss = ' ' * 4
|
|
if result:
|
|
print(f'[{test_name}] PASSED')
|
|
else:
|
|
has_failures = True
|
|
print(f'[{test_name}] FAILED')
|
|
print(f'{ss}Description: \'{test_desc}\'')
|
|
for s in res_strs:
|
|
print(f'{ss}{s}')
|
|
if has_failures:
|
|
# TODO: Enable this when we've fixed all the exception-escape errors
|
|
#exit(1)
|
|
pass
|