#!/usr/bin/env python import os.path import subprocess import sys import argparse import time from packaging.version import Version parser = argparse.ArgumentParser() parser.add_argument('dir', help='directory with versioned folders') parser.add_argument('infile', help='the file to analyze') parser.add_argument('repo', nargs='?', default=None, help='the git repository (for sorting commit hashes)') parser.add_argument('--compare', action='store_true', help='compare output and only show when changed') parser.add_argument('--verbose', action='store_true', help='verbose output for debugging') parser.add_argument('--debug', action='store_true', help='passed through to binary if supported') parser.add_argument('--debug-warnings', action='store_true', help='passed through to binary if supported') parser.add_argument('--check-library', action='store_true', help='passed through to binary if supported') parser.add_argument('--timeout', type=int, default=2, help='the amount of seconds to wait for the analysis to finish') parser.add_argument('--compact', action='store_true', help='only print versions with changes with --compare') parser.add_argument('--no-quiet', action='store_true', default=False, help='do not specify -q') parser.add_argument('--perf', action='store_true', default=False, help='output duration of execution in seconds (CSV format)') parser.add_argument('--start', default=None, help='specify the start version/commit') package_group = parser.add_mutually_exclusive_group() package_group.add_argument('--no-stderr', action='store_true', default=False, help='do not display stdout') package_group.add_argument('--no-stdout', action='store_true', default=False, help='do not display stderr') args = parser.parse_args() def sort_commit_hashes(commits): git_cmd = 'git rev-list --abbrev-commit --topo-order --no-walk=sorted --reverse ' + ' '.join(commits) p = subprocess.Popen(git_cmd.split(), stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=git_repo, universal_newlines=True) stdout, stderr = p.communicate() if p.returncode != 0: print('error: sorting commit hashes failed') print(stderr) sys.exit(1) return stdout.splitlines() verbose = args.verbose do_compare = args.compare if args.compact: if not do_compare: print('error: --compact requires --compare') sys.exit(1) if args.perf: if args.compact: print('error: --compact has no effect with --perf') if args.no_stdout: print('error: --no-stdout has no effect with --perf') if args.no_stderr: print('error: --no-stderr has no effect with --perf') directory = args.dir input_file = args.infile git_repo = args.repo use_hashes = None versions = [] for filename in os.listdir(directory): f = os.path.join(directory, filename) if not os.path.isdir(f): continue versions.append(filename) if not len(versions): print("error: no versions found in '{}'".format(directory)) sys.exit(1) if verbose: print("found {} versions in '{}'".format(len(versions), directory)) try: Version(versions[0]) use_hashes = False versions.sort(key=Version) except: if verbose: print("'{}' not a version - assuming commit hashes".format(versions[0])) if not git_repo: print('error: git repository argument required for commit hash sorting') sys.exit(1) if verbose: print("using git repository '{}' to sort commit hashes".format(git_repo)) use_hashes = True # if you use the folder from the bisect script that contains the repo as a folder - so remove it from the list if versions.count('cppcheck'): versions.remove('cppcheck') # this is the commit hash for the 2.9 release tag. it does not exist in the main branch so the version for it cannot be determined if versions.count('aca3f6fef'): versions.remove('aca3f6fef') len_in = len(versions) versions = sort_commit_hashes(versions) if len(versions) != len_in: print('error: unexpected amount of versions after commit hash sorting') sys.exit(1) if verbose: print("analyzing '{}'".format(input_file)) last_ec = None last_out = None if args.perf: print('version,time') start_entry = args.start for entry in versions: if start_entry: if start_entry != entry: continue start_entry = None exe_path = os.path.join(directory, entry) exe = os.path.join(exe_path, 'cppcheck') if not use_hashes: version = entry else: # get version string version_cmd = exe + ' ' + '--version' version = subprocess.Popen(version_cmd.split(), stdout=subprocess.PIPE, universal_newlines=True).stdout.read().strip() # sanitize version version = version.replace('Cppcheck ', '').replace(' dev', '') cmd = [exe] if do_compare and not args.no_quiet: cmd.append('-q') if args.debug and Version(version) >= Version('1.45'): cmd.append('--debug') if args.debug_warnings and Version(version) >= Version('1.45'): cmd.append('--debug-warnings') if args.check_library and Version(version) >= Version('1.61'): cmd.append('--check-library') if Version(version) >= Version('1.39'): cmd.append('--enable=all') if Version(version) >= Version('1.40'): cmd.append('--inline-suppr') if Version(version) >= Version('1.48'): cmd.append('--suppress=missingInclude') cmd.append('--suppress=missingIncludeSystem') cmd.append('--suppress=unmatchedSuppression') cmd.append('--suppress=unusedFunction') if Version(version) >= Version('1.49'): cmd.append('--inconclusive') if Version(version) >= Version('1.69'): cmd.append('--platform=native') if Version(version) >= Version('1.52') and Version(version) < Version('2.0'): # extend Cppcheck 1.x format with error ID if Version(version) < Version('1.61'): # TODO: re-add inconclusive cmd.append('--template=[{file}:{line}]: ({severity}) {message} [{id}]') else: # TODO: re-add inconclusive: {callstack}: ({severity}{inconclusive:, inconclusive}) {message cmd.append('--template={callstack}: ({severity}) {message} [{id}]') # TODO: how to pass additional options? if args.perf: cmd.append('--error-exitcode=0') cmd.append(input_file) if verbose: print("running '{}'". format(' '.join(cmd))) if args.perf: start = time.time_ns() p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=exe_path, universal_newlines=True) try: comm = p.communicate(timeout=args.timeout) if args.perf: end = time.time_ns() out = '' if not args.no_stdout: out += comm[0] if not args.no_stdout and not args.no_stderr: out += '\n' if not args.no_stderr: out += comm[1] except subprocess.TimeoutExpired: out = "timeout" p.kill() comm = p.communicate() ec = p.returncode if not do_compare: if not use_hashes: ver_str = version else: ver_str = '{} ({})'.format(entry, version) if args.perf: if out == "timeout": data_str = "0.0" # TODO: how to handle these properly? elif not ec == 0: continue # skip errors else: data_str = '{}'.format((end - start) / 1000.0 / 1000.0 / 1000.0) print('"{}",{}'.format(ver_str, data_str)) continue print(ver_str) print(ec) print(out) continue # filter out some false positives # [*]: (information) Unmatched suppression: missingInclude # [*]: (information) Unmatched suppression: missingIncludeSystem # [*]: (information) Unmatched suppression: unmatchedSuppression # [*]: (information) Unmatched suppression: unusedFunction if not use_hashes and (Version(version) >= Version('1.48') or Version(version) <= Version('1.49')): lines = out.splitlines() out = "" for line in lines: if line.startswith('[*]: (information) Unmatched suppression:'): continue out += line + '\n' out = out.strip() if last_ec is None: # first run - only print version if not use_hashes: print(version) else: print('{} ({})'.format(entry, version)) last_ec = ec last_out = out continue do_print = False if last_ec != ec: if verbose: print("{}: exitcode changed".format(version)) do_print = True if last_out != out: if verbose: print("{}: output changed".format(version)) do_print = True if do_print: print(last_ec) print(last_out) # do not print intermediate versions with --compact if not args.compact or do_print: if not use_hashes: print(version) else: print('{} ({})'.format(entry, version)) last_ec = ec last_out = out if do_compare: print(last_ec) print(last_out) if verbose: print('done')