1*e038c9c4Sjoerg#!/usr/bin/env python 2*e038c9c4Sjoerg 3*e038c9c4Sjoergimport argparse 4*e038c9c4Sjoergimport sys 5*e038c9c4Sjoergimport os 6*e038c9c4Sjoerg 7*e038c9c4Sjoergfrom subprocess import call 8*e038c9c4Sjoerg 9*e038c9c4SjoergSCRIPTS_DIR = os.path.dirname(os.path.realpath(__file__)) 10*e038c9c4SjoergPROJECTS_DIR = os.path.join(SCRIPTS_DIR, "projects") 11*e038c9c4SjoergDEFAULT_LLVM_DIR = os.path.realpath(os.path.join(SCRIPTS_DIR, 12*e038c9c4Sjoerg os.path.pardir, 13*e038c9c4Sjoerg os.path.pardir, 14*e038c9c4Sjoerg os.path.pardir)) 15*e038c9c4Sjoerg 16*e038c9c4Sjoerg 17*e038c9c4Sjoergdef add(parser, args): 18*e038c9c4Sjoerg import SATestAdd 19*e038c9c4Sjoerg from ProjectMap import ProjectInfo 20*e038c9c4Sjoerg 21*e038c9c4Sjoerg if args.source == "git" and (args.origin == "" or args.commit == ""): 22*e038c9c4Sjoerg parser.error( 23*e038c9c4Sjoerg "Please provide both --origin and --commit if source is 'git'") 24*e038c9c4Sjoerg 25*e038c9c4Sjoerg if args.source != "git" and (args.origin != "" or args.commit != ""): 26*e038c9c4Sjoerg parser.error("Options --origin and --commit don't make sense when " 27*e038c9c4Sjoerg "source is not 'git'") 28*e038c9c4Sjoerg 29*e038c9c4Sjoerg project = ProjectInfo(args.name[0], args.mode, args.source, args.origin, 30*e038c9c4Sjoerg args.commit) 31*e038c9c4Sjoerg 32*e038c9c4Sjoerg SATestAdd.add_new_project(project) 33*e038c9c4Sjoerg 34*e038c9c4Sjoerg 35*e038c9c4Sjoergdef build(parser, args): 36*e038c9c4Sjoerg import SATestBuild 37*e038c9c4Sjoerg 38*e038c9c4Sjoerg SATestBuild.VERBOSE = args.verbose 39*e038c9c4Sjoerg 40*e038c9c4Sjoerg projects = get_projects(parser, args) 41*e038c9c4Sjoerg tester = SATestBuild.RegressionTester(args.jobs, 42*e038c9c4Sjoerg projects, 43*e038c9c4Sjoerg args.override_compiler, 44*e038c9c4Sjoerg args.extra_analyzer_config, 45*e038c9c4Sjoerg args.regenerate, 46*e038c9c4Sjoerg args.strictness) 47*e038c9c4Sjoerg tests_passed = tester.test_all() 48*e038c9c4Sjoerg 49*e038c9c4Sjoerg if not tests_passed: 50*e038c9c4Sjoerg sys.stderr.write("ERROR: Tests failed.\n") 51*e038c9c4Sjoerg sys.exit(42) 52*e038c9c4Sjoerg 53*e038c9c4Sjoerg 54*e038c9c4Sjoergdef compare(parser, args): 55*e038c9c4Sjoerg import CmpRuns 56*e038c9c4Sjoerg 57*e038c9c4Sjoerg choices = [CmpRuns.HistogramType.RELATIVE.value, 58*e038c9c4Sjoerg CmpRuns.HistogramType.LOG_RELATIVE.value, 59*e038c9c4Sjoerg CmpRuns.HistogramType.ABSOLUTE.value] 60*e038c9c4Sjoerg 61*e038c9c4Sjoerg if args.histogram is not None and args.histogram not in choices: 62*e038c9c4Sjoerg parser.error("Incorrect histogram type, available choices are {}" 63*e038c9c4Sjoerg .format(choices)) 64*e038c9c4Sjoerg 65*e038c9c4Sjoerg dir_old = CmpRuns.ResultsDirectory(args.old[0], args.root_old) 66*e038c9c4Sjoerg dir_new = CmpRuns.ResultsDirectory(args.new[0], args.root_new) 67*e038c9c4Sjoerg 68*e038c9c4Sjoerg CmpRuns.dump_scan_build_results_diff(dir_old, dir_new, 69*e038c9c4Sjoerg show_stats=args.show_stats, 70*e038c9c4Sjoerg stats_only=args.stats_only, 71*e038c9c4Sjoerg histogram=args.histogram, 72*e038c9c4Sjoerg verbose_log=args.verbose_log) 73*e038c9c4Sjoerg 74*e038c9c4Sjoerg 75*e038c9c4Sjoergdef update(parser, args): 76*e038c9c4Sjoerg import SATestUpdateDiffs 77*e038c9c4Sjoerg from ProjectMap import ProjectMap 78*e038c9c4Sjoerg 79*e038c9c4Sjoerg project_map = ProjectMap() 80*e038c9c4Sjoerg for project in project_map.projects: 81*e038c9c4Sjoerg SATestUpdateDiffs.update_reference_results(project, args.git) 82*e038c9c4Sjoerg 83*e038c9c4Sjoerg 84*e038c9c4Sjoergdef benchmark(parser, args): 85*e038c9c4Sjoerg from SATestBenchmark import Benchmark 86*e038c9c4Sjoerg 87*e038c9c4Sjoerg projects = get_projects(parser, args) 88*e038c9c4Sjoerg benchmark = Benchmark(projects, args.iterations, args.output) 89*e038c9c4Sjoerg benchmark.run() 90*e038c9c4Sjoerg 91*e038c9c4Sjoerg 92*e038c9c4Sjoergdef benchmark_compare(parser, args): 93*e038c9c4Sjoerg import SATestBenchmark 94*e038c9c4Sjoerg SATestBenchmark.compare(args.old, args.new, args.output) 95*e038c9c4Sjoerg 96*e038c9c4Sjoerg 97*e038c9c4Sjoergdef get_projects(parser, args): 98*e038c9c4Sjoerg from ProjectMap import ProjectMap, Size 99*e038c9c4Sjoerg 100*e038c9c4Sjoerg project_map = ProjectMap() 101*e038c9c4Sjoerg projects = project_map.projects 102*e038c9c4Sjoerg 103*e038c9c4Sjoerg def filter_projects(projects, predicate, force=False): 104*e038c9c4Sjoerg return [project.with_fields(enabled=(force or project.enabled) and 105*e038c9c4Sjoerg predicate(project)) 106*e038c9c4Sjoerg for project in projects] 107*e038c9c4Sjoerg 108*e038c9c4Sjoerg if args.projects: 109*e038c9c4Sjoerg projects_arg = args.projects.split(",") 110*e038c9c4Sjoerg available_projects = [project.name 111*e038c9c4Sjoerg for project in projects] 112*e038c9c4Sjoerg 113*e038c9c4Sjoerg # validate that given projects are present in the project map file 114*e038c9c4Sjoerg for manual_project in projects_arg: 115*e038c9c4Sjoerg if manual_project not in available_projects: 116*e038c9c4Sjoerg parser.error("Project '{project}' is not found in " 117*e038c9c4Sjoerg "the project map file. Available projects are " 118*e038c9c4Sjoerg "{all}.".format(project=manual_project, 119*e038c9c4Sjoerg all=available_projects)) 120*e038c9c4Sjoerg 121*e038c9c4Sjoerg projects = filter_projects(projects, lambda project: 122*e038c9c4Sjoerg project.name in projects_arg, 123*e038c9c4Sjoerg force=True) 124*e038c9c4Sjoerg 125*e038c9c4Sjoerg try: 126*e038c9c4Sjoerg max_size = Size.from_str(args.max_size) 127*e038c9c4Sjoerg except ValueError as e: 128*e038c9c4Sjoerg parser.error("{}".format(e)) 129*e038c9c4Sjoerg 130*e038c9c4Sjoerg projects = filter_projects(projects, lambda project: 131*e038c9c4Sjoerg project.size <= max_size) 132*e038c9c4Sjoerg 133*e038c9c4Sjoerg return projects 134*e038c9c4Sjoerg 135*e038c9c4Sjoerg 136*e038c9c4Sjoergdef docker(parser, args): 137*e038c9c4Sjoerg if len(args.rest) > 0: 138*e038c9c4Sjoerg if args.rest[0] != "--": 139*e038c9c4Sjoerg parser.error("REST arguments should start with '--'") 140*e038c9c4Sjoerg args.rest = args.rest[1:] 141*e038c9c4Sjoerg 142*e038c9c4Sjoerg if args.build_image: 143*e038c9c4Sjoerg docker_build_image() 144*e038c9c4Sjoerg elif args.shell: 145*e038c9c4Sjoerg docker_shell(args) 146*e038c9c4Sjoerg else: 147*e038c9c4Sjoerg sys.exit(docker_run(args, ' '.join(args.rest))) 148*e038c9c4Sjoerg 149*e038c9c4Sjoerg 150*e038c9c4Sjoergdef docker_build_image(): 151*e038c9c4Sjoerg sys.exit(call("docker build --tag satest-image {}".format(SCRIPTS_DIR), 152*e038c9c4Sjoerg shell=True)) 153*e038c9c4Sjoerg 154*e038c9c4Sjoerg 155*e038c9c4Sjoergdef docker_shell(args): 156*e038c9c4Sjoerg try: 157*e038c9c4Sjoerg # First we need to start the docker container in a waiting mode, 158*e038c9c4Sjoerg # so it doesn't do anything, but most importantly keeps working 159*e038c9c4Sjoerg # while the shell session is in progress. 160*e038c9c4Sjoerg docker_run(args, "--wait", "--detach") 161*e038c9c4Sjoerg # Since the docker container is running, we can actually connect to it 162*e038c9c4Sjoerg call("docker exec -it satest bash", shell=True) 163*e038c9c4Sjoerg 164*e038c9c4Sjoerg except KeyboardInterrupt: 165*e038c9c4Sjoerg pass 166*e038c9c4Sjoerg 167*e038c9c4Sjoerg finally: 168*e038c9c4Sjoerg docker_cleanup() 169*e038c9c4Sjoerg 170*e038c9c4Sjoerg 171*e038c9c4Sjoergdef docker_run(args, command, docker_args=""): 172*e038c9c4Sjoerg try: 173*e038c9c4Sjoerg return call("docker run --rm --name satest " 174*e038c9c4Sjoerg "-v {llvm}:/llvm-project " 175*e038c9c4Sjoerg "-v {build}:/build " 176*e038c9c4Sjoerg "-v {clang}:/analyzer " 177*e038c9c4Sjoerg "-v {scripts}:/scripts " 178*e038c9c4Sjoerg "-v {projects}:/projects " 179*e038c9c4Sjoerg "{docker_args} " 180*e038c9c4Sjoerg "satest-image:latest {command}" 181*e038c9c4Sjoerg .format(llvm=args.llvm_project_dir, 182*e038c9c4Sjoerg build=args.build_dir, 183*e038c9c4Sjoerg clang=args.clang_dir, 184*e038c9c4Sjoerg scripts=SCRIPTS_DIR, 185*e038c9c4Sjoerg projects=PROJECTS_DIR, 186*e038c9c4Sjoerg docker_args=docker_args, 187*e038c9c4Sjoerg command=command), 188*e038c9c4Sjoerg shell=True) 189*e038c9c4Sjoerg 190*e038c9c4Sjoerg except KeyboardInterrupt: 191*e038c9c4Sjoerg docker_cleanup() 192*e038c9c4Sjoerg 193*e038c9c4Sjoerg 194*e038c9c4Sjoergdef docker_cleanup(): 195*e038c9c4Sjoerg print("Please wait for docker to clean up") 196*e038c9c4Sjoerg call("docker stop satest", shell=True) 197*e038c9c4Sjoerg 198*e038c9c4Sjoerg 199*e038c9c4Sjoergdef main(): 200*e038c9c4Sjoerg parser = argparse.ArgumentParser() 201*e038c9c4Sjoerg subparsers = parser.add_subparsers() 202*e038c9c4Sjoerg 203*e038c9c4Sjoerg # add subcommand 204*e038c9c4Sjoerg add_parser = subparsers.add_parser( 205*e038c9c4Sjoerg "add", 206*e038c9c4Sjoerg help="Add a new project for the analyzer testing.") 207*e038c9c4Sjoerg # TODO: Add an option not to build. 208*e038c9c4Sjoerg # TODO: Set the path to the Repository directory. 209*e038c9c4Sjoerg add_parser.add_argument("name", nargs=1, help="Name of the new project") 210*e038c9c4Sjoerg add_parser.add_argument("--mode", action="store", default=1, type=int, 211*e038c9c4Sjoerg choices=[0, 1, 2], 212*e038c9c4Sjoerg help="Build mode: 0 for single file project, " 213*e038c9c4Sjoerg "1 for scan_build, " 214*e038c9c4Sjoerg "2 for single file c++11 project") 215*e038c9c4Sjoerg add_parser.add_argument("--source", action="store", default="script", 216*e038c9c4Sjoerg choices=["script", "git", "zip"], 217*e038c9c4Sjoerg help="Source type of the new project: " 218*e038c9c4Sjoerg "'git' for getting from git " 219*e038c9c4Sjoerg "(please provide --origin and --commit), " 220*e038c9c4Sjoerg "'zip' for unpacking source from a zip file, " 221*e038c9c4Sjoerg "'script' for downloading source by running " 222*e038c9c4Sjoerg "a custom script") 223*e038c9c4Sjoerg add_parser.add_argument("--origin", action="store", default="", 224*e038c9c4Sjoerg help="Origin link for a git repository") 225*e038c9c4Sjoerg add_parser.add_argument("--commit", action="store", default="", 226*e038c9c4Sjoerg help="Git hash for a commit to checkout") 227*e038c9c4Sjoerg add_parser.set_defaults(func=add) 228*e038c9c4Sjoerg 229*e038c9c4Sjoerg # build subcommand 230*e038c9c4Sjoerg build_parser = subparsers.add_parser( 231*e038c9c4Sjoerg "build", 232*e038c9c4Sjoerg help="Build projects from the project map and compare results with " 233*e038c9c4Sjoerg "the reference.") 234*e038c9c4Sjoerg build_parser.add_argument("--strictness", dest="strictness", 235*e038c9c4Sjoerg type=int, default=0, 236*e038c9c4Sjoerg help="0 to fail on runtime errors, 1 to fail " 237*e038c9c4Sjoerg "when the number of found bugs are different " 238*e038c9c4Sjoerg "from the reference, 2 to fail on any " 239*e038c9c4Sjoerg "difference from the reference. Default is 0.") 240*e038c9c4Sjoerg build_parser.add_argument("-r", dest="regenerate", action="store_true", 241*e038c9c4Sjoerg default=False, 242*e038c9c4Sjoerg help="Regenerate reference output.") 243*e038c9c4Sjoerg build_parser.add_argument("--override-compiler", action="store_true", 244*e038c9c4Sjoerg default=False, help="Call scan-build with " 245*e038c9c4Sjoerg "--override-compiler option.") 246*e038c9c4Sjoerg build_parser.add_argument("-j", "--jobs", dest="jobs", 247*e038c9c4Sjoerg type=int, default=0, 248*e038c9c4Sjoerg help="Number of projects to test concurrently") 249*e038c9c4Sjoerg build_parser.add_argument("--extra-analyzer-config", 250*e038c9c4Sjoerg dest="extra_analyzer_config", type=str, 251*e038c9c4Sjoerg default="", 252*e038c9c4Sjoerg help="Arguments passed to to -analyzer-config") 253*e038c9c4Sjoerg build_parser.add_argument("--projects", action="store", default="", 254*e038c9c4Sjoerg help="Comma-separated list of projects to test") 255*e038c9c4Sjoerg build_parser.add_argument("--max-size", action="store", default=None, 256*e038c9c4Sjoerg help="Maximum size for the projects to test") 257*e038c9c4Sjoerg build_parser.add_argument("-v", "--verbose", action="count", default=0) 258*e038c9c4Sjoerg build_parser.set_defaults(func=build) 259*e038c9c4Sjoerg 260*e038c9c4Sjoerg # compare subcommand 261*e038c9c4Sjoerg cmp_parser = subparsers.add_parser( 262*e038c9c4Sjoerg "compare", 263*e038c9c4Sjoerg help="Comparing two static analyzer runs in terms of " 264*e038c9c4Sjoerg "reported warnings and execution time statistics.") 265*e038c9c4Sjoerg cmp_parser.add_argument("--root-old", dest="root_old", 266*e038c9c4Sjoerg help="Prefix to ignore on source files for " 267*e038c9c4Sjoerg "OLD directory", 268*e038c9c4Sjoerg action="store", type=str, default="") 269*e038c9c4Sjoerg cmp_parser.add_argument("--root-new", dest="root_new", 270*e038c9c4Sjoerg help="Prefix to ignore on source files for " 271*e038c9c4Sjoerg "NEW directory", 272*e038c9c4Sjoerg action="store", type=str, default="") 273*e038c9c4Sjoerg cmp_parser.add_argument("--verbose-log", dest="verbose_log", 274*e038c9c4Sjoerg help="Write additional information to LOG " 275*e038c9c4Sjoerg "[default=None]", 276*e038c9c4Sjoerg action="store", type=str, default=None, 277*e038c9c4Sjoerg metavar="LOG") 278*e038c9c4Sjoerg cmp_parser.add_argument("--stats-only", action="store_true", 279*e038c9c4Sjoerg dest="stats_only", default=False, 280*e038c9c4Sjoerg help="Only show statistics on reports") 281*e038c9c4Sjoerg cmp_parser.add_argument("--show-stats", action="store_true", 282*e038c9c4Sjoerg dest="show_stats", default=False, 283*e038c9c4Sjoerg help="Show change in statistics") 284*e038c9c4Sjoerg cmp_parser.add_argument("--histogram", action="store", default=None, 285*e038c9c4Sjoerg help="Show histogram of paths differences. " 286*e038c9c4Sjoerg "Requires matplotlib") 287*e038c9c4Sjoerg cmp_parser.add_argument("old", nargs=1, help="Directory with old results") 288*e038c9c4Sjoerg cmp_parser.add_argument("new", nargs=1, help="Directory with new results") 289*e038c9c4Sjoerg cmp_parser.set_defaults(func=compare) 290*e038c9c4Sjoerg 291*e038c9c4Sjoerg # update subcommand 292*e038c9c4Sjoerg upd_parser = subparsers.add_parser( 293*e038c9c4Sjoerg "update", 294*e038c9c4Sjoerg help="Update static analyzer reference results based on the previous " 295*e038c9c4Sjoerg "run of SATest build. Assumes that SATest build was just run.") 296*e038c9c4Sjoerg upd_parser.add_argument("--git", action="store_true", 297*e038c9c4Sjoerg help="Stage updated results using git.") 298*e038c9c4Sjoerg upd_parser.set_defaults(func=update) 299*e038c9c4Sjoerg 300*e038c9c4Sjoerg # docker subcommand 301*e038c9c4Sjoerg dock_parser = subparsers.add_parser( 302*e038c9c4Sjoerg "docker", 303*e038c9c4Sjoerg help="Run regression system in the docker.") 304*e038c9c4Sjoerg 305*e038c9c4Sjoerg dock_parser.add_argument("--build-image", action="store_true", 306*e038c9c4Sjoerg help="Build docker image for running tests.") 307*e038c9c4Sjoerg dock_parser.add_argument("--shell", action="store_true", 308*e038c9c4Sjoerg help="Start a shell on docker.") 309*e038c9c4Sjoerg dock_parser.add_argument("--llvm-project-dir", action="store", 310*e038c9c4Sjoerg default=DEFAULT_LLVM_DIR, 311*e038c9c4Sjoerg help="Path to LLVM source code. Defaults " 312*e038c9c4Sjoerg "to the repo where this script is located. ") 313*e038c9c4Sjoerg dock_parser.add_argument("--build-dir", action="store", default="", 314*e038c9c4Sjoerg help="Path to a directory where docker should " 315*e038c9c4Sjoerg "build LLVM code.") 316*e038c9c4Sjoerg dock_parser.add_argument("--clang-dir", action="store", default="", 317*e038c9c4Sjoerg help="Path to find/install LLVM installation.") 318*e038c9c4Sjoerg dock_parser.add_argument("rest", nargs=argparse.REMAINDER, default=[], 319*e038c9c4Sjoerg help="Additionall args that will be forwarded " 320*e038c9c4Sjoerg "to the docker's entrypoint.") 321*e038c9c4Sjoerg dock_parser.set_defaults(func=docker) 322*e038c9c4Sjoerg 323*e038c9c4Sjoerg # benchmark subcommand 324*e038c9c4Sjoerg bench_parser = subparsers.add_parser( 325*e038c9c4Sjoerg "benchmark", 326*e038c9c4Sjoerg help="Run benchmarks by building a set of projects multiple times.") 327*e038c9c4Sjoerg 328*e038c9c4Sjoerg bench_parser.add_argument("-i", "--iterations", action="store", 329*e038c9c4Sjoerg type=int, default=20, 330*e038c9c4Sjoerg help="Number of iterations for building each " 331*e038c9c4Sjoerg "project.") 332*e038c9c4Sjoerg bench_parser.add_argument("-o", "--output", action="store", 333*e038c9c4Sjoerg default="benchmark.csv", 334*e038c9c4Sjoerg help="Output csv file for the benchmark results") 335*e038c9c4Sjoerg bench_parser.add_argument("--projects", action="store", default="", 336*e038c9c4Sjoerg help="Comma-separated list of projects to test") 337*e038c9c4Sjoerg bench_parser.add_argument("--max-size", action="store", default=None, 338*e038c9c4Sjoerg help="Maximum size for the projects to test") 339*e038c9c4Sjoerg bench_parser.set_defaults(func=benchmark) 340*e038c9c4Sjoerg 341*e038c9c4Sjoerg bench_subparsers = bench_parser.add_subparsers() 342*e038c9c4Sjoerg bench_compare_parser = bench_subparsers.add_parser( 343*e038c9c4Sjoerg "compare", 344*e038c9c4Sjoerg help="Compare benchmark runs.") 345*e038c9c4Sjoerg bench_compare_parser.add_argument("--old", action="store", required=True, 346*e038c9c4Sjoerg help="Benchmark reference results to " 347*e038c9c4Sjoerg "compare agains.") 348*e038c9c4Sjoerg bench_compare_parser.add_argument("--new", action="store", required=True, 349*e038c9c4Sjoerg help="New benchmark results to check.") 350*e038c9c4Sjoerg bench_compare_parser.add_argument("-o", "--output", 351*e038c9c4Sjoerg action="store", required=True, 352*e038c9c4Sjoerg help="Output file for plots.") 353*e038c9c4Sjoerg bench_compare_parser.set_defaults(func=benchmark_compare) 354*e038c9c4Sjoerg 355*e038c9c4Sjoerg args = parser.parse_args() 356*e038c9c4Sjoerg args.func(parser, args) 357*e038c9c4Sjoerg 358*e038c9c4Sjoerg 359*e038c9c4Sjoergif __name__ == "__main__": 360*e038c9c4Sjoerg main() 361