| #!/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.') |