blob: 15fdfef24d5eb9edb5830d135b031e893adbe810 [file] [log] [blame] [edit]
#!/usr/bin/env python3
# Copyright (C) 2014-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 argparse
import sys
import os
import subprocess
from webkitpy.static_analysis.results import get_project_issue_count_as_string
INDEX_HTML = 'index.html'
INDEX_TEMPLATE = """
<html>
<head>
<title>{title}</title>
</head>
<body>
<div><h1>{heading}</h1></div>
<div><b>Projects with issues:</b></div>
<div><ul>{project_list}</ul></div>
</body>
</html>
"""
PROJECT_TEMPLATE = '<li><a href="{project_file_url}">{project_name}</a> ({project_issue_count})</li>'
CATEGORY_TEMPLATE = '<div>{category_name}<ul>{checker_links}</ul></div>'
STATIC_ANALYZER = 'StaticAnalyzer'
STATIC_ANALYZER_REGRESSIONS = 'StaticAnalyzerRegressions'
STATIC_ANALYZER_UNEXPECTED = 'StaticAnalyzerUnexpectedRegressions'
def parse_command_line(args):
parser = argparse.ArgumentParser(description='Take a directory of static analyzer results and output an archive')
parser.add_argument('--output-root', dest='output_root', help='Root of static analysis output', default='./')
parser.add_argument('--destination', dest='destination', help='Where to output zip archive')
parser.add_argument('--id-string', dest='id_string', help='Identifier for what was built')
parser.add_argument('--count', '-c', dest='count', action='store_true', default=False,
help='Print total issue count.')
return parser.parse_args(args)
def generate_unexpected_results_links(output_root, static_analysis_dir, project_name):
results_links = ''
for category, category_formatted in {'UnexpectedPasses': 'Unexpected Passes', 'UnexpectedFailures': 'Unexpected Failures'}.items():
command = 'find {} -name {}\\* -print'.format(os.path.abspath(os.path.join(static_analysis_dir, project_name)), category)
try:
result_files = subprocess.check_output(command, shell=True, text=True)
except subprocess.CalledProcessError as e:
sys.stderr.write(f'{e.output}')
sys.stderr.write(f'Could not find results for {project_name}\n')
return -1
unexpected_results_files = result_files.splitlines()
checker_links = ''
for file_path in unexpected_results_files:
file_name = file_path.split('/')[-1].removeprefix(category).removesuffix('.txt')
relative_file_path = os.path.relpath(file_path, output_root)
with open(os.path.abspath(os.path.join(output_root, file_path))) as f:
count = len(f.read().splitlines())
if count:
checker_links += PROJECT_TEMPLATE.format(project_file_url=relative_file_path, project_name=file_name, project_issue_count=count)
if checker_links:
results_links += CATEGORY_TEMPLATE.format(category_name=category_formatted, checker_links=checker_links)
return results_links
def generate_results_page(project_dirs, id_string, output_root, static_analysis_dir, result_type=None):
project_list = ''
for project_name in sorted(project_dirs):
static_analyzer_index_path = os.path.join(static_analysis_dir, project_name, INDEX_HTML)
project_count = get_project_issue_count_as_string(static_analyzer_index_path)
if int(project_count) != 0:
project_list = project_list + PROJECT_TEMPLATE.format(
project_file_url=os.path.relpath(static_analyzer_index_path, output_root),
project_issue_count=project_count,
project_name=project_name)
if result_type == 'unexpected':
project_list += generate_unexpected_results_links(output_root, static_analysis_dir, project_name)
if result_type:
heading = f'{result_type.capitalize()} results for {id_string}'
else:
heading = f'Results for {id_string}'
return INDEX_TEMPLATE.format(
heading=heading,
project_list=project_list,
title='Static Analysis Results'
)
def create_results_file(options, output_root, static_analysis_dir, result_type=None):
project_dirs = list(filter(lambda x: x[0] != '.' and os.path.isdir(os.path.join(static_analysis_dir, x)),
os.listdir(static_analysis_dir)))
if options.id_string:
results_page = generate_results_page(project_dirs, options.id_string, output_root, static_analysis_dir, result_type)
if result_type:
f = open(output_root + f'/{result_type}-results.html', 'w')
else:
f = open(output_root + '/results.html', 'w')
f.write(results_page)
f.close()
def get_total_issue_count(project_dirs, static_analysis_dir):
total_issue_count = 0
for project_name in sorted(project_dirs):
static_analyzer_index_path = os.path.join(static_analysis_dir, project_name, INDEX_HTML)
try:
issue_count = int(get_project_issue_count_as_string(static_analyzer_index_path))
total_issue_count = total_issue_count + issue_count
except ValueError:
pass
return total_issue_count
def main(options):
output_root = options.output_root
static_analysis_dir = output_root
if os.path.isdir(os.path.join(static_analysis_dir, STATIC_ANALYZER)):
static_analysis_dir = os.path.join(static_analysis_dir, STATIC_ANALYZER)
create_results_file(options, output_root, static_analysis_dir)
if os.path.isdir(os.path.join(output_root, STATIC_ANALYZER_REGRESSIONS)):
static_analysis_new_dir = os.path.join(output_root, STATIC_ANALYZER_REGRESSIONS)
create_results_file(options, output_root, static_analysis_new_dir, result_type='new')
if os.path.isdir(os.path.join(output_root, STATIC_ANALYZER_UNEXPECTED)):
static_analysis_new_dir = os.path.join(output_root, STATIC_ANALYZER_UNEXPECTED)
create_results_file(options, output_root, static_analysis_new_dir, result_type='unexpected')
if options.destination:
if os.path.isfile(options.destination):
subprocess.check_call(['/bin/rm', options.destination])
subprocess.check_call(['/usr/bin/zip', '-9', '-r', options.destination, output_root, '-x', '*.d'])
if options.count:
project_dirs = list(filter(lambda x: x[0] != '.' and os.path.isdir(os.path.join(static_analysis_dir, x)),
os.listdir(static_analysis_dir)))
total_issue_count = get_total_issue_count(project_dirs, static_analysis_dir)
print('Total issue count: {}'.format(total_issue_count))
return 0
if __name__ == '__main__':
options = parse_command_line(sys.argv[1:])
try:
result = main(options)
exit(result)
except KeyboardInterrupt:
exit('Interrupted.')