llvm-project/clang/utils/analyzer/SATest.py

366 lines
15 KiB
Python
Executable File

#!/usr/bin/env python
import argparse
import sys
import os
from subprocess import call
SCRIPTS_DIR = os.path.dirname(os.path.realpath(__file__))
PROJECTS_DIR = os.path.join(SCRIPTS_DIR, "projects")
DEFAULT_LLVM_DIR = os.path.realpath(os.path.join(SCRIPTS_DIR,
os.path.pardir,
os.path.pardir,
os.path.pardir))
def add(parser, args):
import SATestAdd
from ProjectMap import ProjectInfo
if args.source == "git" and (args.origin == "" or args.commit == ""):
parser.error(
"Please provide both --origin and --commit if source is 'git'")
if args.source != "git" and (args.origin != "" or args.commit != ""):
parser.error("Options --origin and --commit don't make sense when "
"source is not 'git'")
project = ProjectInfo(args.name[0], args.mode, args.source, args.origin,
args.commit)
SATestAdd.add_new_project(project)
def build(parser, args):
import SATestBuild
SATestBuild.VERBOSE = args.verbose
projects = get_projects(parser, args)
tester = SATestBuild.RegressionTester(args.jobs,
projects,
args.override_compiler,
args.extra_analyzer_config,
args.extra_checkers,
args.regenerate,
args.strictness)
tests_passed = tester.test_all()
if not tests_passed:
sys.stderr.write("ERROR: Tests failed.\n")
sys.exit(42)
def compare(parser, args):
import CmpRuns
choices = [CmpRuns.HistogramType.RELATIVE.value,
CmpRuns.HistogramType.LOG_RELATIVE.value,
CmpRuns.HistogramType.ABSOLUTE.value]
if args.histogram is not None and args.histogram not in choices:
parser.error("Incorrect histogram type, available choices are {}"
.format(choices))
dir_old = CmpRuns.ResultsDirectory(args.old[0], args.root_old)
dir_new = CmpRuns.ResultsDirectory(args.new[0], args.root_new)
CmpRuns.dump_scan_build_results_diff(dir_old, dir_new,
show_stats=args.show_stats,
stats_only=args.stats_only,
histogram=args.histogram,
verbose_log=args.verbose_log)
def update(parser, args):
import SATestUpdateDiffs
from ProjectMap import ProjectMap
project_map = ProjectMap()
for project in project_map.projects:
SATestUpdateDiffs.update_reference_results(project, args.git)
def benchmark(parser, args):
from SATestBenchmark import Benchmark
projects = get_projects(parser, args)
benchmark = Benchmark(projects, args.iterations, args.output)
benchmark.run()
def benchmark_compare(parser, args):
import SATestBenchmark
SATestBenchmark.compare(args.old, args.new, args.output)
def get_projects(parser, args):
from ProjectMap import ProjectMap, Size
project_map = ProjectMap()
projects = project_map.projects
def filter_projects(projects, predicate, force=False):
return [project.with_fields(enabled=(force or project.enabled) and
predicate(project))
for project in projects]
if args.projects:
projects_arg = args.projects.split(",")
available_projects = [project.name
for project in projects]
# validate that given projects are present in the project map file
for manual_project in projects_arg:
if manual_project not in available_projects:
parser.error("Project '{project}' is not found in "
"the project map file. Available projects are "
"{all}.".format(project=manual_project,
all=available_projects))
projects = filter_projects(projects, lambda project:
project.name in projects_arg,
force=True)
try:
max_size = Size.from_str(args.max_size)
except ValueError as e:
parser.error("{}".format(e))
projects = filter_projects(projects, lambda project:
project.size <= max_size)
return projects
def docker(parser, args):
if len(args.rest) > 0:
if args.rest[0] != "--":
parser.error("REST arguments should start with '--'")
args.rest = args.rest[1:]
if args.build_image:
docker_build_image()
elif args.shell:
docker_shell(args)
else:
sys.exit(docker_run(args, ' '.join(args.rest)))
def docker_build_image():
sys.exit(call("docker build --tag satest-image {}".format(SCRIPTS_DIR),
shell=True))
def docker_shell(args):
try:
# First we need to start the docker container in a waiting mode,
# so it doesn't do anything, but most importantly keeps working
# while the shell session is in progress.
docker_run(args, "--wait", "--detach")
# Since the docker container is running, we can actually connect to it
call("docker exec -it satest bash", shell=True)
except KeyboardInterrupt:
pass
finally:
docker_cleanup()
def docker_run(args, command, docker_args=""):
try:
return call("docker run --rm --name satest "
"-v {llvm}:/llvm-project "
"-v {build}:/build "
"-v {clang}:/analyzer "
"-v {scripts}:/scripts "
"-v {projects}:/projects "
"{docker_args} "
"satest-image:latest {command}"
.format(llvm=args.llvm_project_dir,
build=args.build_dir,
clang=args.clang_dir,
scripts=SCRIPTS_DIR,
projects=PROJECTS_DIR,
docker_args=docker_args,
command=command),
shell=True)
except KeyboardInterrupt:
docker_cleanup()
def docker_cleanup():
print("Please wait for docker to clean up")
call("docker stop satest", shell=True)
def main():
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
# add subcommand
add_parser = subparsers.add_parser(
"add",
help="Add a new project for the analyzer testing.")
# TODO: Add an option not to build.
# TODO: Set the path to the Repository directory.
add_parser.add_argument("name", nargs=1, help="Name of the new project")
add_parser.add_argument("--mode", action="store", default=1, type=int,
choices=[0, 1, 2],
help="Build mode: 0 for single file project, "
"1 for scan_build, "
"2 for single file c++11 project")
add_parser.add_argument("--source", action="store", default="script",
choices=["script", "git", "zip"],
help="Source type of the new project: "
"'git' for getting from git "
"(please provide --origin and --commit), "
"'zip' for unpacking source from a zip file, "
"'script' for downloading source by running "
"a custom script")
add_parser.add_argument("--origin", action="store", default="",
help="Origin link for a git repository")
add_parser.add_argument("--commit", action="store", default="",
help="Git hash for a commit to checkout")
add_parser.set_defaults(func=add)
# build subcommand
build_parser = subparsers.add_parser(
"build",
help="Build projects from the project map and compare results with "
"the reference.")
build_parser.add_argument("--strictness", dest="strictness",
type=int, default=0,
help="0 to fail on runtime errors, 1 to fail "
"when the number of found bugs are different "
"from the reference, 2 to fail on any "
"difference from the reference. Default is 0.")
build_parser.add_argument("-r", dest="regenerate", action="store_true",
default=False,
help="Regenerate reference output.")
build_parser.add_argument("--override-compiler", action="store_true",
default=False, help="Call scan-build with "
"--override-compiler option.")
build_parser.add_argument("-j", "--jobs", dest="jobs",
type=int, default=0,
help="Number of projects to test concurrently")
build_parser.add_argument("--extra-analyzer-config",
dest="extra_analyzer_config", type=str,
default="",
help="Arguments passed to to -analyzer-config")
build_parser.add_argument("--extra-checkers",
dest="extra_checkers", type=str,
default="",
help="Extra checkers to enable")
build_parser.add_argument("--projects", action="store", default="",
help="Comma-separated list of projects to test")
build_parser.add_argument("--max-size", action="store", default=None,
help="Maximum size for the projects to test")
build_parser.add_argument("-v", "--verbose", action="count", default=0)
build_parser.set_defaults(func=build)
# compare subcommand
cmp_parser = subparsers.add_parser(
"compare",
help="Comparing two static analyzer runs in terms of "
"reported warnings and execution time statistics.")
cmp_parser.add_argument("--root-old", dest="root_old",
help="Prefix to ignore on source files for "
"OLD directory",
action="store", type=str, default="")
cmp_parser.add_argument("--root-new", dest="root_new",
help="Prefix to ignore on source files for "
"NEW directory",
action="store", type=str, default="")
cmp_parser.add_argument("--verbose-log", dest="verbose_log",
help="Write additional information to LOG "
"[default=None]",
action="store", type=str, default=None,
metavar="LOG")
cmp_parser.add_argument("--stats-only", action="store_true",
dest="stats_only", default=False,
help="Only show statistics on reports")
cmp_parser.add_argument("--show-stats", action="store_true",
dest="show_stats", default=False,
help="Show change in statistics")
cmp_parser.add_argument("--histogram", action="store", default=None,
help="Show histogram of paths differences. "
"Requires matplotlib")
cmp_parser.add_argument("old", nargs=1, help="Directory with old results")
cmp_parser.add_argument("new", nargs=1, help="Directory with new results")
cmp_parser.set_defaults(func=compare)
# update subcommand
upd_parser = subparsers.add_parser(
"update",
help="Update static analyzer reference results based on the previous "
"run of SATest build. Assumes that SATest build was just run.")
upd_parser.add_argument("--git", action="store_true",
help="Stage updated results using git.")
upd_parser.set_defaults(func=update)
# docker subcommand
dock_parser = subparsers.add_parser(
"docker",
help="Run regression system in the docker.")
dock_parser.add_argument("--build-image", action="store_true",
help="Build docker image for running tests.")
dock_parser.add_argument("--shell", action="store_true",
help="Start a shell on docker.")
dock_parser.add_argument("--llvm-project-dir", action="store",
default=DEFAULT_LLVM_DIR,
help="Path to LLVM source code. Defaults "
"to the repo where this script is located. ")
dock_parser.add_argument("--build-dir", action="store", default="",
help="Path to a directory where docker should "
"build LLVM code.")
dock_parser.add_argument("--clang-dir", action="store", default="",
help="Path to find/install LLVM installation.")
dock_parser.add_argument("rest", nargs=argparse.REMAINDER, default=[],
help="Additionall args that will be forwarded "
"to the docker's entrypoint.")
dock_parser.set_defaults(func=docker)
# benchmark subcommand
bench_parser = subparsers.add_parser(
"benchmark",
help="Run benchmarks by building a set of projects multiple times.")
bench_parser.add_argument("-i", "--iterations", action="store",
type=int, default=20,
help="Number of iterations for building each "
"project.")
bench_parser.add_argument("-o", "--output", action="store",
default="benchmark.csv",
help="Output csv file for the benchmark results")
bench_parser.add_argument("--projects", action="store", default="",
help="Comma-separated list of projects to test")
bench_parser.add_argument("--max-size", action="store", default=None,
help="Maximum size for the projects to test")
bench_parser.set_defaults(func=benchmark)
bench_subparsers = bench_parser.add_subparsers()
bench_compare_parser = bench_subparsers.add_parser(
"compare",
help="Compare benchmark runs.")
bench_compare_parser.add_argument("--old", action="store", required=True,
help="Benchmark reference results to "
"compare agains.")
bench_compare_parser.add_argument("--new", action="store", required=True,
help="New benchmark results to check.")
bench_compare_parser.add_argument("-o", "--output",
action="store", required=True,
help="Output file for plots.")
bench_compare_parser.set_defaults(func=benchmark_compare)
args = parser.parse_args()
args.func(parser, args)
if __name__ == "__main__":
main()