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