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