blob: 449c62462421d76f7419b504d80d09867555a4e0 [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>
<style>body {{ font-family: Helvetica, sans-serif; font-size:10pt }}</style>
</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 style="margin-top: 10px;">{category_name}<ul>{checker_links}</ul></div>'
STATIC_ANALYZER = 'StaticAnalyzer'
STATIC_ANALYZER_REGRESSIONS = 'StaticAnalyzerRegressions'
STATIC_ANALYZER_UNEXPECTED = 'StaticAnalyzerUnexpectedRegressions'
LINK_TEMPLATE = '<a href="{link}">{text}</a>'
TABLE_ITEM_TEMPLATE = '<tr><td>{checker_type}</td><td>{link}</td></tr>'
RESULTS_PAGE = """
<html>
<head>
<title>{title}</title>
<style>
body {{ font-family: Helvetica, sans-serif; font-size: 10pt }}
table {{ font-size:10pt }}
table {{ border-spacing: 0px; border: 1px solid black }}
th, table thead {{
background-color:#eee; color:#666666;
font-weight: bold; cursor: default;
text-align:center;
font-weight: bold; font-family: Verdana;
white-space:nowrap;
}}
.inner {{
margin-left: 50px;
}}
th, td {{ padding:5px; padding-left:8px; text-align:left }}
</style>
</head>
<body>
<div><h1>{heading}</h1></div>
<div>{body}</div>
</body>
</html>
"""
UNEXPECTED_TABLE_TEMPLATE = """
<table>
<thead>
<tr style="font-weight:bold">
<td>Checker</td>
<td>Files</td>
</tr>
</thead>
<tbody>
{table_body_contents}
</tbody>
</table>
"""
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_table(output_root, static_analysis_dir, project_name, category):
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()
unexpected_results_per_project = ''
table_rows = ''
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:
table_rows += TABLE_ITEM_TEMPLATE.format(checker_type=file_name, link=LINK_TEMPLATE.format(link=relative_file_path, text=count))
if table_rows:
unexpected_results_per_project += UNEXPECTED_TABLE_TEMPLATE.format(table_body_contents=table_rows)
return unexpected_results_per_project
def generate_unexpected_results_page(project_dirs, id_string, output_root, static_analysis_dir):
body = ''
for category, category_formatted in {'UnexpectedFailures': 'Unexpected Failures', 'UnexpectedPasses': 'Unexpected Passes'}.items():
body += f'<h2>{category_formatted}</h2>'
body += '<div class="inner">'
for project_name in sorted(project_dirs):
# For each project, link to all unexpected issues and create tables for failures and passes
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)
body += f'<h3>{project_name}</h3>'
if int(project_count) != 0 and category == 'UnexpectedFailures':
body += f"<b>{LINK_TEMPLATE.format(link=os.path.relpath(static_analyzer_index_path, output_root), text='View Issues')}</b></br></br>"
unexpected_results_per_project = generate_unexpected_table(output_root, static_analysis_dir, project_name, category)
body += unexpected_results_per_project or '<li>No unexpected results!</li>'
body += '</br></br></div>'
return RESULTS_PAGE.format(title=f'Static Analysis Results', heading=f'Unexpected Results for {id_string}', body=body)
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:
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:
if result_type == 'unexpected':
results_page = generate_unexpected_results_page(project_dirs, options.id_string, output_root, static_analysis_dir)
else:
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.')