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