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