xref: /aosp_15_r20/external/clang/utils/analyzer/SATestBuild.py (revision 67e74705e28f6214e480b399dd47ea732279e315)
1*67e74705SXin Li#!/usr/bin/env python
2*67e74705SXin Li
3*67e74705SXin Li"""
4*67e74705SXin LiStatic Analyzer qualification infrastructure.
5*67e74705SXin Li
6*67e74705SXin LiThe goal is to test the analyzer against different projects, check for failures,
7*67e74705SXin Licompare results, and measure performance.
8*67e74705SXin Li
9*67e74705SXin LiRepository Directory will contain sources of the projects as well as the
10*67e74705SXin Liinformation on how to build them and the expected output.
11*67e74705SXin LiRepository Directory structure:
12*67e74705SXin Li   - ProjectMap file
13*67e74705SXin Li   - Historical Performance Data
14*67e74705SXin Li   - Project Dir1
15*67e74705SXin Li     - ReferenceOutput
16*67e74705SXin Li   - Project Dir2
17*67e74705SXin Li     - ReferenceOutput
18*67e74705SXin Li   ..
19*67e74705SXin LiNote that the build tree must be inside the project dir.
20*67e74705SXin Li
21*67e74705SXin LiTo test the build of the analyzer one would:
22*67e74705SXin Li   - Copy over a copy of the Repository Directory. (TODO: Prefer to ensure that
23*67e74705SXin Li     the build directory does not pollute the repository to min network traffic).
24*67e74705SXin Li   - Build all projects, until error. Produce logs to report errors.
25*67e74705SXin Li   - Compare results.
26*67e74705SXin Li
27*67e74705SXin LiThe files which should be kept around for failure investigations:
28*67e74705SXin Li   RepositoryCopy/Project DirI/ScanBuildResults
29*67e74705SXin Li   RepositoryCopy/Project DirI/run_static_analyzer.log
30*67e74705SXin Li
31*67e74705SXin LiAssumptions (TODO: shouldn't need to assume these.):
32*67e74705SXin Li   The script is being run from the Repository Directory.
33*67e74705SXin Li   The compiler for scan-build and scan-build are in the PATH.
34*67e74705SXin Li   export PATH=/Users/zaks/workspace/c2llvm/build/Release+Asserts/bin:$PATH
35*67e74705SXin Li
36*67e74705SXin LiFor more logging, set the  env variables:
37*67e74705SXin Li   zaks:TI zaks$ export CCC_ANALYZER_LOG=1
38*67e74705SXin Li   zaks:TI zaks$ export CCC_ANALYZER_VERBOSE=1
39*67e74705SXin Li
40*67e74705SXin LiThe list of checkers tested are hardcoded in the Checkers variable.
41*67e74705SXin LiFor testing additional checkers, use the SA_ADDITIONAL_CHECKERS environment
42*67e74705SXin Livariable. It should contain a comma separated list.
43*67e74705SXin Li"""
44*67e74705SXin Liimport CmpRuns
45*67e74705SXin Li
46*67e74705SXin Liimport os
47*67e74705SXin Liimport csv
48*67e74705SXin Liimport sys
49*67e74705SXin Liimport glob
50*67e74705SXin Liimport math
51*67e74705SXin Liimport shutil
52*67e74705SXin Liimport time
53*67e74705SXin Liimport plistlib
54*67e74705SXin Liimport argparse
55*67e74705SXin Lifrom subprocess import check_call, check_output, CalledProcessError
56*67e74705SXin Li
57*67e74705SXin Li#------------------------------------------------------------------------------
58*67e74705SXin Li# Helper functions.
59*67e74705SXin Li#------------------------------------------------------------------------------
60*67e74705SXin Li
61*67e74705SXin Lidef detectCPUs():
62*67e74705SXin Li    """
63*67e74705SXin Li    Detects the number of CPUs on a system. Cribbed from pp.
64*67e74705SXin Li    """
65*67e74705SXin Li    # Linux, Unix and MacOS:
66*67e74705SXin Li    if hasattr(os, "sysconf"):
67*67e74705SXin Li        if os.sysconf_names.has_key("SC_NPROCESSORS_ONLN"):
68*67e74705SXin Li            # Linux & Unix:
69*67e74705SXin Li            ncpus = os.sysconf("SC_NPROCESSORS_ONLN")
70*67e74705SXin Li            if isinstance(ncpus, int) and ncpus > 0:
71*67e74705SXin Li                return ncpus
72*67e74705SXin Li        else: # OSX:
73*67e74705SXin Li            return int(capture(['sysctl', '-n', 'hw.ncpu']))
74*67e74705SXin Li    # Windows:
75*67e74705SXin Li    if os.environ.has_key("NUMBER_OF_PROCESSORS"):
76*67e74705SXin Li        ncpus = int(os.environ["NUMBER_OF_PROCESSORS"])
77*67e74705SXin Li        if ncpus > 0:
78*67e74705SXin Li            return ncpus
79*67e74705SXin Li    return 1 # Default
80*67e74705SXin Li
81*67e74705SXin Lidef which(command, paths = None):
82*67e74705SXin Li   """which(command, [paths]) - Look up the given command in the paths string
83*67e74705SXin Li   (or the PATH environment variable, if unspecified)."""
84*67e74705SXin Li
85*67e74705SXin Li   if paths is None:
86*67e74705SXin Li       paths = os.environ.get('PATH','')
87*67e74705SXin Li
88*67e74705SXin Li   # Check for absolute match first.
89*67e74705SXin Li   if os.path.exists(command):
90*67e74705SXin Li       return command
91*67e74705SXin Li
92*67e74705SXin Li   # Would be nice if Python had a lib function for this.
93*67e74705SXin Li   if not paths:
94*67e74705SXin Li       paths = os.defpath
95*67e74705SXin Li
96*67e74705SXin Li   # Get suffixes to search.
97*67e74705SXin Li   # On Cygwin, 'PATHEXT' may exist but it should not be used.
98*67e74705SXin Li   if os.pathsep == ';':
99*67e74705SXin Li       pathext = os.environ.get('PATHEXT', '').split(';')
100*67e74705SXin Li   else:
101*67e74705SXin Li       pathext = ['']
102*67e74705SXin Li
103*67e74705SXin Li   # Search the paths...
104*67e74705SXin Li   for path in paths.split(os.pathsep):
105*67e74705SXin Li       for ext in pathext:
106*67e74705SXin Li           p = os.path.join(path, command + ext)
107*67e74705SXin Li           if os.path.exists(p):
108*67e74705SXin Li               return p
109*67e74705SXin Li
110*67e74705SXin Li   return None
111*67e74705SXin Li
112*67e74705SXin Li# Make sure we flush the output after every print statement.
113*67e74705SXin Liclass flushfile(object):
114*67e74705SXin Li    def __init__(self, f):
115*67e74705SXin Li        self.f = f
116*67e74705SXin Li    def write(self, x):
117*67e74705SXin Li        self.f.write(x)
118*67e74705SXin Li        self.f.flush()
119*67e74705SXin Li
120*67e74705SXin Lisys.stdout = flushfile(sys.stdout)
121*67e74705SXin Li
122*67e74705SXin Lidef getProjectMapPath():
123*67e74705SXin Li    ProjectMapPath = os.path.join(os.path.abspath(os.curdir),
124*67e74705SXin Li                                  ProjectMapFile)
125*67e74705SXin Li    if not os.path.exists(ProjectMapPath):
126*67e74705SXin Li        print "Error: Cannot find the Project Map file " + ProjectMapPath +\
127*67e74705SXin Li                "\nRunning script for the wrong directory?"
128*67e74705SXin Li        sys.exit(-1)
129*67e74705SXin Li    return ProjectMapPath
130*67e74705SXin Li
131*67e74705SXin Lidef getProjectDir(ID):
132*67e74705SXin Li    return os.path.join(os.path.abspath(os.curdir), ID)
133*67e74705SXin Li
134*67e74705SXin Lidef getSBOutputDirName(IsReferenceBuild) :
135*67e74705SXin Li    if IsReferenceBuild == True :
136*67e74705SXin Li        return SBOutputDirReferencePrefix + SBOutputDirName
137*67e74705SXin Li    else :
138*67e74705SXin Li        return SBOutputDirName
139*67e74705SXin Li
140*67e74705SXin Li#------------------------------------------------------------------------------
141*67e74705SXin Li# Configuration setup.
142*67e74705SXin Li#------------------------------------------------------------------------------
143*67e74705SXin Li
144*67e74705SXin Li# Find Clang for static analysis.
145*67e74705SXin LiClang = which("clang", os.environ['PATH'])
146*67e74705SXin Liif not Clang:
147*67e74705SXin Li    print "Error: cannot find 'clang' in PATH"
148*67e74705SXin Li    sys.exit(-1)
149*67e74705SXin Li
150*67e74705SXin Li# Number of jobs.
151*67e74705SXin LiJobs = int(math.ceil(detectCPUs() * 0.75))
152*67e74705SXin Li
153*67e74705SXin Li# Project map stores info about all the "registered" projects.
154*67e74705SXin LiProjectMapFile = "projectMap.csv"
155*67e74705SXin Li
156*67e74705SXin Li# Names of the project specific scripts.
157*67e74705SXin Li# The script that downloads the project.
158*67e74705SXin LiDownloadScript = "download_project.sh"
159*67e74705SXin Li# The script that needs to be executed before the build can start.
160*67e74705SXin LiCleanupScript = "cleanup_run_static_analyzer.sh"
161*67e74705SXin Li# This is a file containing commands for scan-build.
162*67e74705SXin LiBuildScript = "run_static_analyzer.cmd"
163*67e74705SXin Li
164*67e74705SXin Li# The log file name.
165*67e74705SXin LiLogFolderName = "Logs"
166*67e74705SXin LiBuildLogName = "run_static_analyzer.log"
167*67e74705SXin Li# Summary file - contains the summary of the failures. Ex: This info can be be
168*67e74705SXin Li# displayed when buildbot detects a build failure.
169*67e74705SXin LiNumOfFailuresInSummary = 10
170*67e74705SXin LiFailuresSummaryFileName = "failures.txt"
171*67e74705SXin Li# Summary of the result diffs.
172*67e74705SXin LiDiffsSummaryFileName = "diffs.txt"
173*67e74705SXin Li
174*67e74705SXin Li# The scan-build result directory.
175*67e74705SXin LiSBOutputDirName = "ScanBuildResults"
176*67e74705SXin LiSBOutputDirReferencePrefix = "Ref"
177*67e74705SXin Li
178*67e74705SXin Li# The name of the directory storing the cached project source. If this directory
179*67e74705SXin Li# does not exist, the download script will be executed. That script should
180*67e74705SXin Li# create the "CachedSource" directory and download the project source into it.
181*67e74705SXin LiCachedSourceDirName = "CachedSource"
182*67e74705SXin Li
183*67e74705SXin Li# The name of the directory containing the source code that will be analyzed.
184*67e74705SXin Li# Each time a project is analyzed, a fresh copy of its CachedSource directory
185*67e74705SXin Li# will be copied to the PatchedSource directory and then the local patches
186*67e74705SXin Li# in PatchfileName will be applied (if PatchfileName exists).
187*67e74705SXin LiPatchedSourceDirName = "PatchedSource"
188*67e74705SXin Li
189*67e74705SXin Li# The name of the patchfile specifying any changes that should be applied
190*67e74705SXin Li# to the CachedSource before analyzing.
191*67e74705SXin LiPatchfileName = "changes_for_analyzer.patch"
192*67e74705SXin Li
193*67e74705SXin Li# The list of checkers used during analyzes.
194*67e74705SXin Li# Currently, consists of all the non-experimental checkers, plus a few alpha
195*67e74705SXin Li# checkers we don't want to regress on.
196*67e74705SXin LiCheckers="alpha.unix.SimpleStream,alpha.security.taint,cplusplus.NewDeleteLeaks,core,cplusplus,deadcode,security,unix,osx"
197*67e74705SXin Li
198*67e74705SXin LiVerbose = 1
199*67e74705SXin Li
200*67e74705SXin Li#------------------------------------------------------------------------------
201*67e74705SXin Li# Test harness logic.
202*67e74705SXin Li#------------------------------------------------------------------------------
203*67e74705SXin Li
204*67e74705SXin Li# Run pre-processing script if any.
205*67e74705SXin Lidef runCleanupScript(Dir, PBuildLogFile):
206*67e74705SXin Li    Cwd = os.path.join(Dir, PatchedSourceDirName)
207*67e74705SXin Li    ScriptPath = os.path.join(Dir, CleanupScript)
208*67e74705SXin Li    runScript(ScriptPath, PBuildLogFile, Cwd)
209*67e74705SXin Li
210*67e74705SXin Li# Run the script to download the project, if it exists.
211*67e74705SXin Lidef runDownloadScript(Dir, PBuildLogFile):
212*67e74705SXin Li    ScriptPath = os.path.join(Dir, DownloadScript)
213*67e74705SXin Li    runScript(ScriptPath, PBuildLogFile, Dir)
214*67e74705SXin Li
215*67e74705SXin Li# Run the provided script if it exists.
216*67e74705SXin Lidef runScript(ScriptPath, PBuildLogFile, Cwd):
217*67e74705SXin Li    if os.path.exists(ScriptPath):
218*67e74705SXin Li        try:
219*67e74705SXin Li            if Verbose == 1:
220*67e74705SXin Li                print "  Executing: %s" % (ScriptPath,)
221*67e74705SXin Li            check_call("chmod +x '%s'" % ScriptPath, cwd = Cwd,
222*67e74705SXin Li                                              stderr=PBuildLogFile,
223*67e74705SXin Li                                              stdout=PBuildLogFile,
224*67e74705SXin Li                                              shell=True)
225*67e74705SXin Li            check_call("'%s'" % ScriptPath, cwd = Cwd, stderr=PBuildLogFile,
226*67e74705SXin Li                                              stdout=PBuildLogFile,
227*67e74705SXin Li                                              shell=True)
228*67e74705SXin Li        except:
229*67e74705SXin Li            print "Error: Running %s failed. See %s for details." % (ScriptPath,
230*67e74705SXin Li                PBuildLogFile.name)
231*67e74705SXin Li            sys.exit(-1)
232*67e74705SXin Li
233*67e74705SXin Li# Download the project and apply the local patchfile if it exists.
234*67e74705SXin Lidef downloadAndPatch(Dir, PBuildLogFile):
235*67e74705SXin Li    CachedSourceDirPath = os.path.join(Dir, CachedSourceDirName)
236*67e74705SXin Li
237*67e74705SXin Li    # If the we don't already have the cached source, run the project's
238*67e74705SXin Li    # download script to download it.
239*67e74705SXin Li    if not os.path.exists(CachedSourceDirPath):
240*67e74705SXin Li      runDownloadScript(Dir, PBuildLogFile)
241*67e74705SXin Li      if not os.path.exists(CachedSourceDirPath):
242*67e74705SXin Li        print "Error: '%s' not found after download." % (CachedSourceDirPath)
243*67e74705SXin Li        exit(-1)
244*67e74705SXin Li
245*67e74705SXin Li    PatchedSourceDirPath = os.path.join(Dir, PatchedSourceDirName)
246*67e74705SXin Li
247*67e74705SXin Li    # Remove potentially stale patched source.
248*67e74705SXin Li    if os.path.exists(PatchedSourceDirPath):
249*67e74705SXin Li        shutil.rmtree(PatchedSourceDirPath)
250*67e74705SXin Li
251*67e74705SXin Li    # Copy the cached source and apply any patches to the copy.
252*67e74705SXin Li    shutil.copytree(CachedSourceDirPath, PatchedSourceDirPath, symlinks=True)
253*67e74705SXin Li    applyPatch(Dir, PBuildLogFile)
254*67e74705SXin Li
255*67e74705SXin Lidef applyPatch(Dir, PBuildLogFile):
256*67e74705SXin Li    PatchfilePath = os.path.join(Dir, PatchfileName)
257*67e74705SXin Li    PatchedSourceDirPath = os.path.join(Dir, PatchedSourceDirName)
258*67e74705SXin Li    if not os.path.exists(PatchfilePath):
259*67e74705SXin Li        print "  No local patches."
260*67e74705SXin Li        return
261*67e74705SXin Li
262*67e74705SXin Li    print "  Applying patch."
263*67e74705SXin Li    try:
264*67e74705SXin Li        check_call("patch -p1 < '%s'" % (PatchfilePath),
265*67e74705SXin Li                    cwd = PatchedSourceDirPath,
266*67e74705SXin Li                    stderr=PBuildLogFile,
267*67e74705SXin Li                    stdout=PBuildLogFile,
268*67e74705SXin Li                    shell=True)
269*67e74705SXin Li    except:
270*67e74705SXin Li        print "Error: Patch failed. See %s for details." % (PBuildLogFile.name)
271*67e74705SXin Li        sys.exit(-1)
272*67e74705SXin Li
273*67e74705SXin Li# Build the project with scan-build by reading in the commands and
274*67e74705SXin Li# prefixing them with the scan-build options.
275*67e74705SXin Lidef runScanBuild(Dir, SBOutputDir, PBuildLogFile):
276*67e74705SXin Li    BuildScriptPath = os.path.join(Dir, BuildScript)
277*67e74705SXin Li    if not os.path.exists(BuildScriptPath):
278*67e74705SXin Li        print "Error: build script is not defined: %s" % BuildScriptPath
279*67e74705SXin Li        sys.exit(-1)
280*67e74705SXin Li
281*67e74705SXin Li    AllCheckers = Checkers
282*67e74705SXin Li    if os.environ.has_key('SA_ADDITIONAL_CHECKERS'):
283*67e74705SXin Li        AllCheckers = AllCheckers + ',' + os.environ['SA_ADDITIONAL_CHECKERS']
284*67e74705SXin Li
285*67e74705SXin Li    # Run scan-build from within the patched source directory.
286*67e74705SXin Li    SBCwd = os.path.join(Dir, PatchedSourceDirName)
287*67e74705SXin Li
288*67e74705SXin Li    SBOptions = "--use-analyzer '%s' " %  Clang
289*67e74705SXin Li    SBOptions += "-plist-html -o '%s' " % SBOutputDir
290*67e74705SXin Li    SBOptions += "-enable-checker " + AllCheckers + " "
291*67e74705SXin Li    SBOptions += "--keep-empty "
292*67e74705SXin Li    # Always use ccc-analyze to ensure that we can locate the failures
293*67e74705SXin Li    # directory.
294*67e74705SXin Li    SBOptions += "--override-compiler "
295*67e74705SXin Li    try:
296*67e74705SXin Li        SBCommandFile = open(BuildScriptPath, "r")
297*67e74705SXin Li        SBPrefix = "scan-build " + SBOptions + " "
298*67e74705SXin Li        for Command in SBCommandFile:
299*67e74705SXin Li            Command = Command.strip()
300*67e74705SXin Li            if len(Command) == 0:
301*67e74705SXin Li                continue;
302*67e74705SXin Li            # If using 'make', auto imply a -jX argument
303*67e74705SXin Li            # to speed up analysis.  xcodebuild will
304*67e74705SXin Li            # automatically use the maximum number of cores.
305*67e74705SXin Li            if (Command.startswith("make ") or Command == "make") and \
306*67e74705SXin Li                "-j" not in Command:
307*67e74705SXin Li                Command += " -j%d" % Jobs
308*67e74705SXin Li            SBCommand = SBPrefix + Command
309*67e74705SXin Li            if Verbose == 1:
310*67e74705SXin Li                print "  Executing: %s" % (SBCommand,)
311*67e74705SXin Li            check_call(SBCommand, cwd = SBCwd, stderr=PBuildLogFile,
312*67e74705SXin Li                                               stdout=PBuildLogFile,
313*67e74705SXin Li                                               shell=True)
314*67e74705SXin Li    except:
315*67e74705SXin Li        print "Error: scan-build failed. See ",PBuildLogFile.name,\
316*67e74705SXin Li              " for details."
317*67e74705SXin Li        raise
318*67e74705SXin Li
319*67e74705SXin Lidef hasNoExtension(FileName):
320*67e74705SXin Li    (Root, Ext) = os.path.splitext(FileName)
321*67e74705SXin Li    if ((Ext == "")) :
322*67e74705SXin Li        return True
323*67e74705SXin Li    return False
324*67e74705SXin Li
325*67e74705SXin Lidef isValidSingleInputFile(FileName):
326*67e74705SXin Li    (Root, Ext) = os.path.splitext(FileName)
327*67e74705SXin Li    if ((Ext == ".i") | (Ext == ".ii") |
328*67e74705SXin Li        (Ext == ".c") | (Ext == ".cpp") |
329*67e74705SXin Li        (Ext == ".m") | (Ext == "")) :
330*67e74705SXin Li        return True
331*67e74705SXin Li    return False
332*67e74705SXin Li
333*67e74705SXin Li# Get the path to the SDK for the given SDK name. Returns None if
334*67e74705SXin Li# the path cannot be determined.
335*67e74705SXin Lidef getSDKPath(SDKName):
336*67e74705SXin Li    if which("xcrun") is None:
337*67e74705SXin Li        return None
338*67e74705SXin Li
339*67e74705SXin Li    Cmd = "xcrun --sdk " + SDKName + " --show-sdk-path"
340*67e74705SXin Li    return check_output(Cmd, shell=True).rstrip()
341*67e74705SXin Li
342*67e74705SXin Li# Run analysis on a set of preprocessed files.
343*67e74705SXin Lidef runAnalyzePreprocessed(Dir, SBOutputDir, Mode):
344*67e74705SXin Li    if os.path.exists(os.path.join(Dir, BuildScript)):
345*67e74705SXin Li        print "Error: The preprocessed files project should not contain %s" % \
346*67e74705SXin Li               BuildScript
347*67e74705SXin Li        raise Exception()
348*67e74705SXin Li
349*67e74705SXin Li    CmdPrefix = Clang + " -cc1 "
350*67e74705SXin Li
351*67e74705SXin Li    # For now, we assume the preprocessed files should be analyzed
352*67e74705SXin Li    # with the OS X SDK.
353*67e74705SXin Li    SDKPath = getSDKPath("macosx")
354*67e74705SXin Li    if SDKPath is not None:
355*67e74705SXin Li      CmdPrefix += "-isysroot " + SDKPath + " "
356*67e74705SXin Li
357*67e74705SXin Li    CmdPrefix += "-analyze -analyzer-output=plist -w "
358*67e74705SXin Li    CmdPrefix += "-analyzer-checker=" + Checkers +" -fcxx-exceptions -fblocks "
359*67e74705SXin Li
360*67e74705SXin Li    if (Mode == 2) :
361*67e74705SXin Li        CmdPrefix += "-std=c++11 "
362*67e74705SXin Li
363*67e74705SXin Li    PlistPath = os.path.join(Dir, SBOutputDir, "date")
364*67e74705SXin Li    FailPath = os.path.join(PlistPath, "failures");
365*67e74705SXin Li    os.makedirs(FailPath);
366*67e74705SXin Li
367*67e74705SXin Li    for FullFileName in glob.glob(Dir + "/*"):
368*67e74705SXin Li        FileName = os.path.basename(FullFileName)
369*67e74705SXin Li        Failed = False
370*67e74705SXin Li
371*67e74705SXin Li        # Only run the analyzes on supported files.
372*67e74705SXin Li        if (hasNoExtension(FileName)):
373*67e74705SXin Li            continue
374*67e74705SXin Li        if (isValidSingleInputFile(FileName) == False):
375*67e74705SXin Li            print "Error: Invalid single input file %s." % (FullFileName,)
376*67e74705SXin Li            raise Exception()
377*67e74705SXin Li
378*67e74705SXin Li        # Build and call the analyzer command.
379*67e74705SXin Li        OutputOption = "-o '%s.plist' " % os.path.join(PlistPath, FileName)
380*67e74705SXin Li        Command = CmdPrefix + OutputOption + ("'%s'" % FileName)
381*67e74705SXin Li        LogFile = open(os.path.join(FailPath, FileName + ".stderr.txt"), "w+b")
382*67e74705SXin Li        try:
383*67e74705SXin Li            if Verbose == 1:
384*67e74705SXin Li                print "  Executing: %s" % (Command,)
385*67e74705SXin Li            check_call(Command, cwd = Dir, stderr=LogFile,
386*67e74705SXin Li                                           stdout=LogFile,
387*67e74705SXin Li                                           shell=True)
388*67e74705SXin Li        except CalledProcessError, e:
389*67e74705SXin Li            print "Error: Analyzes of %s failed. See %s for details." \
390*67e74705SXin Li                  "Error code %d." % \
391*67e74705SXin Li                   (FullFileName, LogFile.name, e.returncode)
392*67e74705SXin Li            Failed = True
393*67e74705SXin Li        finally:
394*67e74705SXin Li            LogFile.close()
395*67e74705SXin Li
396*67e74705SXin Li        # If command did not fail, erase the log file.
397*67e74705SXin Li        if Failed == False:
398*67e74705SXin Li            os.remove(LogFile.name);
399*67e74705SXin Li
400*67e74705SXin Lidef getBuildLogPath(SBOutputDir):
401*67e74705SXin Li  return os.path.join(SBOutputDir, LogFolderName, BuildLogName)
402*67e74705SXin Li
403*67e74705SXin Lidef removeLogFile(SBOutputDir):
404*67e74705SXin Li  BuildLogPath = getBuildLogPath(SBOutputDir)
405*67e74705SXin Li  # Clean up the log file.
406*67e74705SXin Li  if (os.path.exists(BuildLogPath)) :
407*67e74705SXin Li      RmCommand = "rm '%s'" % BuildLogPath
408*67e74705SXin Li      if Verbose == 1:
409*67e74705SXin Li          print "  Executing: %s" % (RmCommand,)
410*67e74705SXin Li      check_call(RmCommand, shell=True)
411*67e74705SXin Li
412*67e74705SXin Lidef buildProject(Dir, SBOutputDir, ProjectBuildMode, IsReferenceBuild):
413*67e74705SXin Li    TBegin = time.time()
414*67e74705SXin Li
415*67e74705SXin Li    BuildLogPath = getBuildLogPath(SBOutputDir)
416*67e74705SXin Li    print "Log file: %s" % (BuildLogPath,)
417*67e74705SXin Li    print "Output directory: %s" %(SBOutputDir, )
418*67e74705SXin Li
419*67e74705SXin Li    removeLogFile(SBOutputDir)
420*67e74705SXin Li
421*67e74705SXin Li    # Clean up scan build results.
422*67e74705SXin Li    if (os.path.exists(SBOutputDir)) :
423*67e74705SXin Li        RmCommand = "rm -r '%s'" % SBOutputDir
424*67e74705SXin Li        if Verbose == 1:
425*67e74705SXin Li            print "  Executing: %s" % (RmCommand,)
426*67e74705SXin Li            check_call(RmCommand, shell=True)
427*67e74705SXin Li    assert(not os.path.exists(SBOutputDir))
428*67e74705SXin Li    os.makedirs(os.path.join(SBOutputDir, LogFolderName))
429*67e74705SXin Li
430*67e74705SXin Li    # Open the log file.
431*67e74705SXin Li    PBuildLogFile = open(BuildLogPath, "wb+")
432*67e74705SXin Li
433*67e74705SXin Li    # Build and analyze the project.
434*67e74705SXin Li    try:
435*67e74705SXin Li        if (ProjectBuildMode == 1):
436*67e74705SXin Li            downloadAndPatch(Dir, PBuildLogFile)
437*67e74705SXin Li            runCleanupScript(Dir, PBuildLogFile)
438*67e74705SXin Li            runScanBuild(Dir, SBOutputDir, PBuildLogFile)
439*67e74705SXin Li        else:
440*67e74705SXin Li            runAnalyzePreprocessed(Dir, SBOutputDir, ProjectBuildMode)
441*67e74705SXin Li
442*67e74705SXin Li        if IsReferenceBuild :
443*67e74705SXin Li            runCleanupScript(Dir, PBuildLogFile)
444*67e74705SXin Li
445*67e74705SXin Li            # Make the absolute paths relative in the reference results.
446*67e74705SXin Li            for (DirPath, Dirnames, Filenames) in os.walk(SBOutputDir):
447*67e74705SXin Li                for F in Filenames:
448*67e74705SXin Li                    if (not F.endswith('plist')):
449*67e74705SXin Li                        continue
450*67e74705SXin Li                    Plist = os.path.join(DirPath, F)
451*67e74705SXin Li                    Data = plistlib.readPlist(Plist)
452*67e74705SXin Li                    PathPrefix = Dir
453*67e74705SXin Li                    if (ProjectBuildMode == 1):
454*67e74705SXin Li                        PathPrefix = os.path.join(Dir, PatchedSourceDirName)
455*67e74705SXin Li                    Paths = [SourceFile[len(PathPrefix)+1:]\
456*67e74705SXin Li                              if SourceFile.startswith(PathPrefix)\
457*67e74705SXin Li                              else SourceFile for SourceFile in Data['files']]
458*67e74705SXin Li                    Data['files'] = Paths
459*67e74705SXin Li                    plistlib.writePlist(Data, Plist)
460*67e74705SXin Li
461*67e74705SXin Li    finally:
462*67e74705SXin Li        PBuildLogFile.close()
463*67e74705SXin Li
464*67e74705SXin Li    print "Build complete (time: %.2f). See the log for more details: %s" % \
465*67e74705SXin Li           ((time.time()-TBegin), BuildLogPath)
466*67e74705SXin Li
467*67e74705SXin Li# A plist file is created for each call to the analyzer(each source file).
468*67e74705SXin Li# We are only interested on the once that have bug reports, so delete the rest.
469*67e74705SXin Lidef CleanUpEmptyPlists(SBOutputDir):
470*67e74705SXin Li    for F in glob.glob(SBOutputDir + "/*/*.plist"):
471*67e74705SXin Li        P = os.path.join(SBOutputDir, F)
472*67e74705SXin Li
473*67e74705SXin Li        Data = plistlib.readPlist(P)
474*67e74705SXin Li        # Delete empty reports.
475*67e74705SXin Li        if not Data['files']:
476*67e74705SXin Li            os.remove(P)
477*67e74705SXin Li            continue
478*67e74705SXin Li
479*67e74705SXin Li# Given the scan-build output directory, checks if the build failed
480*67e74705SXin Li# (by searching for the failures directories). If there are failures, it
481*67e74705SXin Li# creates a summary file in the output directory.
482*67e74705SXin Lidef checkBuild(SBOutputDir):
483*67e74705SXin Li    # Check if there are failures.
484*67e74705SXin Li    Failures = glob.glob(SBOutputDir + "/*/failures/*.stderr.txt")
485*67e74705SXin Li    TotalFailed = len(Failures);
486*67e74705SXin Li    if TotalFailed == 0:
487*67e74705SXin Li        CleanUpEmptyPlists(SBOutputDir)
488*67e74705SXin Li        Plists = glob.glob(SBOutputDir + "/*/*.plist")
489*67e74705SXin Li        print "Number of bug reports (non-empty plist files) produced: %d" %\
490*67e74705SXin Li           len(Plists)
491*67e74705SXin Li        return;
492*67e74705SXin Li
493*67e74705SXin Li    # Create summary file to display when the build fails.
494*67e74705SXin Li    SummaryPath = os.path.join(SBOutputDir, LogFolderName, FailuresSummaryFileName)
495*67e74705SXin Li    if (Verbose > 0):
496*67e74705SXin Li        print "  Creating the failures summary file %s" % (SummaryPath,)
497*67e74705SXin Li
498*67e74705SXin Li    SummaryLog = open(SummaryPath, "w+")
499*67e74705SXin Li    try:
500*67e74705SXin Li        SummaryLog.write("Total of %d failures discovered.\n" % (TotalFailed,))
501*67e74705SXin Li        if TotalFailed > NumOfFailuresInSummary:
502*67e74705SXin Li            SummaryLog.write("See the first %d below.\n"
503*67e74705SXin Li                                                   % (NumOfFailuresInSummary,))
504*67e74705SXin Li        # TODO: Add a line "See the results folder for more."
505*67e74705SXin Li
506*67e74705SXin Li        FailuresCopied = NumOfFailuresInSummary
507*67e74705SXin Li        Idx = 0
508*67e74705SXin Li        for FailLogPathI in Failures:
509*67e74705SXin Li            if Idx >= NumOfFailuresInSummary:
510*67e74705SXin Li                break;
511*67e74705SXin Li            Idx += 1
512*67e74705SXin Li            SummaryLog.write("\n-- Error #%d -----------\n" % (Idx,));
513*67e74705SXin Li            FailLogI = open(FailLogPathI, "r");
514*67e74705SXin Li            try:
515*67e74705SXin Li                shutil.copyfileobj(FailLogI, SummaryLog);
516*67e74705SXin Li            finally:
517*67e74705SXin Li                FailLogI.close()
518*67e74705SXin Li    finally:
519*67e74705SXin Li        SummaryLog.close()
520*67e74705SXin Li
521*67e74705SXin Li    print "Error: analysis failed. See ", SummaryPath
522*67e74705SXin Li    sys.exit(-1)
523*67e74705SXin Li
524*67e74705SXin Li# Auxiliary object to discard stdout.
525*67e74705SXin Liclass Discarder(object):
526*67e74705SXin Li    def write(self, text):
527*67e74705SXin Li        pass # do nothing
528*67e74705SXin Li
529*67e74705SXin Li# Compare the warnings produced by scan-build.
530*67e74705SXin Li# Strictness defines the success criteria for the test:
531*67e74705SXin Li#   0 - success if there are no crashes or analyzer failure.
532*67e74705SXin Li#   1 - success if there are no difference in the number of reported bugs.
533*67e74705SXin Li#   2 - success if all the bug reports are identical.
534*67e74705SXin Lidef runCmpResults(Dir, Strictness = 0):
535*67e74705SXin Li    TBegin = time.time()
536*67e74705SXin Li
537*67e74705SXin Li    RefDir = os.path.join(Dir, SBOutputDirReferencePrefix + SBOutputDirName)
538*67e74705SXin Li    NewDir = os.path.join(Dir, SBOutputDirName)
539*67e74705SXin Li
540*67e74705SXin Li    # We have to go one level down the directory tree.
541*67e74705SXin Li    RefList = glob.glob(RefDir + "/*")
542*67e74705SXin Li    NewList = glob.glob(NewDir + "/*")
543*67e74705SXin Li
544*67e74705SXin Li    # Log folders are also located in the results dir, so ignore them.
545*67e74705SXin Li    RefLogDir = os.path.join(RefDir, LogFolderName)
546*67e74705SXin Li    if RefLogDir in RefList:
547*67e74705SXin Li        RefList.remove(RefLogDir)
548*67e74705SXin Li    NewList.remove(os.path.join(NewDir, LogFolderName))
549*67e74705SXin Li
550*67e74705SXin Li    if len(RefList) == 0 or len(NewList) == 0:
551*67e74705SXin Li        return False
552*67e74705SXin Li    assert(len(RefList) == len(NewList))
553*67e74705SXin Li
554*67e74705SXin Li    # There might be more then one folder underneath - one per each scan-build
555*67e74705SXin Li    # command (Ex: one for configure and one for make).
556*67e74705SXin Li    if (len(RefList) > 1):
557*67e74705SXin Li        # Assume that the corresponding folders have the same names.
558*67e74705SXin Li        RefList.sort()
559*67e74705SXin Li        NewList.sort()
560*67e74705SXin Li
561*67e74705SXin Li    # Iterate and find the differences.
562*67e74705SXin Li    NumDiffs = 0
563*67e74705SXin Li    PairList = zip(RefList, NewList)
564*67e74705SXin Li    for P in PairList:
565*67e74705SXin Li        RefDir = P[0]
566*67e74705SXin Li        NewDir = P[1]
567*67e74705SXin Li
568*67e74705SXin Li        assert(RefDir != NewDir)
569*67e74705SXin Li        if Verbose == 1:
570*67e74705SXin Li            print "  Comparing Results: %s %s" % (RefDir, NewDir)
571*67e74705SXin Li
572*67e74705SXin Li        DiffsPath = os.path.join(NewDir, DiffsSummaryFileName)
573*67e74705SXin Li        PatchedSourceDirPath = os.path.join(Dir, PatchedSourceDirName)
574*67e74705SXin Li        Opts = CmpRuns.CmpOptions(DiffsPath, "", PatchedSourceDirPath)
575*67e74705SXin Li        # Discard everything coming out of stdout (CmpRun produces a lot of them).
576*67e74705SXin Li        OLD_STDOUT = sys.stdout
577*67e74705SXin Li        sys.stdout = Discarder()
578*67e74705SXin Li        # Scan the results, delete empty plist files.
579*67e74705SXin Li        NumDiffs, ReportsInRef, ReportsInNew = \
580*67e74705SXin Li            CmpRuns.dumpScanBuildResultsDiff(RefDir, NewDir, Opts, False)
581*67e74705SXin Li        sys.stdout = OLD_STDOUT
582*67e74705SXin Li        if (NumDiffs > 0) :
583*67e74705SXin Li            print "Warning: %r differences in diagnostics. See %s" % \
584*67e74705SXin Li                  (NumDiffs, DiffsPath,)
585*67e74705SXin Li        if Strictness >= 2 and NumDiffs > 0:
586*67e74705SXin Li            print "Error: Diffs found in strict mode (2)."
587*67e74705SXin Li            sys.exit(-1)
588*67e74705SXin Li        elif Strictness >= 1 and ReportsInRef != ReportsInNew:
589*67e74705SXin Li            print "Error: The number of results are different in strict mode (1)."
590*67e74705SXin Li            sys.exit(-1)
591*67e74705SXin Li
592*67e74705SXin Li    print "Diagnostic comparison complete (time: %.2f)." % (time.time()-TBegin)
593*67e74705SXin Li    return (NumDiffs > 0)
594*67e74705SXin Li
595*67e74705SXin Lidef cleanupReferenceResults(SBOutputDir):
596*67e74705SXin Li    # Delete html, css, and js files from reference results. These can
597*67e74705SXin Li    # include multiple copies of the benchmark source and so get very large.
598*67e74705SXin Li    Extensions = ["html", "css", "js"]
599*67e74705SXin Li    for E in Extensions:
600*67e74705SXin Li        for F in glob.glob("%s/*/*.%s" % (SBOutputDir, E)):
601*67e74705SXin Li            P = os.path.join(SBOutputDir, F)
602*67e74705SXin Li            RmCommand = "rm '%s'" % P
603*67e74705SXin Li            check_call(RmCommand, shell=True)
604*67e74705SXin Li
605*67e74705SXin Li    # Remove the log file. It leaks absolute path names.
606*67e74705SXin Li    removeLogFile(SBOutputDir)
607*67e74705SXin Li
608*67e74705SXin Lidef updateSVN(Mode, ProjectsMap):
609*67e74705SXin Li    try:
610*67e74705SXin Li        ProjectsMap.seek(0)
611*67e74705SXin Li        for I in csv.reader(ProjectsMap):
612*67e74705SXin Li            ProjName = I[0]
613*67e74705SXin Li            Path = os.path.join(ProjName, getSBOutputDirName(True))
614*67e74705SXin Li
615*67e74705SXin Li            if Mode == "delete":
616*67e74705SXin Li                Command = "svn delete '%s'" % (Path,)
617*67e74705SXin Li            else:
618*67e74705SXin Li                Command = "svn add '%s'" % (Path,)
619*67e74705SXin Li
620*67e74705SXin Li            if Verbose == 1:
621*67e74705SXin Li                print "  Executing: %s" % (Command,)
622*67e74705SXin Li            check_call(Command, shell=True)
623*67e74705SXin Li
624*67e74705SXin Li        if Mode == "delete":
625*67e74705SXin Li            CommitCommand = "svn commit -m \"[analyzer tests] Remove " \
626*67e74705SXin Li                            "reference results.\""
627*67e74705SXin Li        else:
628*67e74705SXin Li            CommitCommand = "svn commit -m \"[analyzer tests] Add new " \
629*67e74705SXin Li                            "reference results.\""
630*67e74705SXin Li        if Verbose == 1:
631*67e74705SXin Li            print "  Executing: %s" % (CommitCommand,)
632*67e74705SXin Li        check_call(CommitCommand, shell=True)
633*67e74705SXin Li    except:
634*67e74705SXin Li        print "Error: SVN update failed."
635*67e74705SXin Li        sys.exit(-1)
636*67e74705SXin Li
637*67e74705SXin Lidef testProject(ID, ProjectBuildMode, IsReferenceBuild=False, Dir=None, Strictness = 0):
638*67e74705SXin Li    print " \n\n--- Building project %s" % (ID,)
639*67e74705SXin Li
640*67e74705SXin Li    TBegin = time.time()
641*67e74705SXin Li
642*67e74705SXin Li    if Dir is None :
643*67e74705SXin Li        Dir = getProjectDir(ID)
644*67e74705SXin Li    if Verbose == 1:
645*67e74705SXin Li        print "  Build directory: %s." % (Dir,)
646*67e74705SXin Li
647*67e74705SXin Li    # Set the build results directory.
648*67e74705SXin Li    RelOutputDir = getSBOutputDirName(IsReferenceBuild)
649*67e74705SXin Li    SBOutputDir = os.path.join(Dir, RelOutputDir)
650*67e74705SXin Li
651*67e74705SXin Li    buildProject(Dir, SBOutputDir, ProjectBuildMode, IsReferenceBuild)
652*67e74705SXin Li
653*67e74705SXin Li    checkBuild(SBOutputDir)
654*67e74705SXin Li
655*67e74705SXin Li    if IsReferenceBuild == False:
656*67e74705SXin Li        runCmpResults(Dir, Strictness)
657*67e74705SXin Li    else:
658*67e74705SXin Li        cleanupReferenceResults(SBOutputDir)
659*67e74705SXin Li
660*67e74705SXin Li    print "Completed tests for project %s (time: %.2f)." % \
661*67e74705SXin Li          (ID, (time.time()-TBegin))
662*67e74705SXin Li
663*67e74705SXin Lidef testAll(IsReferenceBuild = False, UpdateSVN = False, Strictness = 0):
664*67e74705SXin Li    PMapFile = open(getProjectMapPath(), "rb")
665*67e74705SXin Li    try:
666*67e74705SXin Li        # Validate the input.
667*67e74705SXin Li        for I in csv.reader(PMapFile):
668*67e74705SXin Li            if (len(I) != 2) :
669*67e74705SXin Li                print "Error: Rows in the ProjectMapFile should have 3 entries."
670*67e74705SXin Li                raise Exception()
671*67e74705SXin Li            if (not ((I[1] == "0") | (I[1] == "1") | (I[1] == "2"))):
672*67e74705SXin Li                print "Error: Second entry in the ProjectMapFile should be 0" \
673*67e74705SXin Li                      " (single file), 1 (project), or 2(single file c++11)."
674*67e74705SXin Li                raise Exception()
675*67e74705SXin Li
676*67e74705SXin Li        # When we are regenerating the reference results, we might need to
677*67e74705SXin Li        # update svn. Remove reference results from SVN.
678*67e74705SXin Li        if UpdateSVN == True:
679*67e74705SXin Li            assert(IsReferenceBuild == True);
680*67e74705SXin Li            updateSVN("delete",  PMapFile);
681*67e74705SXin Li
682*67e74705SXin Li        # Test the projects.
683*67e74705SXin Li        PMapFile.seek(0)
684*67e74705SXin Li        for I in csv.reader(PMapFile):
685*67e74705SXin Li            testProject(I[0], int(I[1]), IsReferenceBuild, None, Strictness)
686*67e74705SXin Li
687*67e74705SXin Li        # Add reference results to SVN.
688*67e74705SXin Li        if UpdateSVN == True:
689*67e74705SXin Li            updateSVN("add",  PMapFile);
690*67e74705SXin Li
691*67e74705SXin Li    except:
692*67e74705SXin Li        print "Error occurred. Premature termination."
693*67e74705SXin Li        raise
694*67e74705SXin Li    finally:
695*67e74705SXin Li        PMapFile.close()
696*67e74705SXin Li
697*67e74705SXin Liif __name__ == '__main__':
698*67e74705SXin Li    # Parse command line arguments.
699*67e74705SXin Li    Parser = argparse.ArgumentParser(description='Test the Clang Static Analyzer.')
700*67e74705SXin Li    Parser.add_argument('--strictness', dest='strictness', type=int, default=0,
701*67e74705SXin Li                       help='0 to fail on runtime errors, 1 to fail when the number\
702*67e74705SXin Li                             of found bugs are different from the reference, 2 to \
703*67e74705SXin Li                             fail on any difference from the reference. Default is 0.')
704*67e74705SXin Li    Parser.add_argument('-r', dest='regenerate', action='store_true', default=False,
705*67e74705SXin Li                        help='Regenerate reference output.')
706*67e74705SXin Li    Parser.add_argument('-rs', dest='update_reference', action='store_true',
707*67e74705SXin Li                        default=False, help='Regenerate reference output and update svn.')
708*67e74705SXin Li    Args = Parser.parse_args()
709*67e74705SXin Li
710*67e74705SXin Li    IsReference = False
711*67e74705SXin Li    UpdateSVN = False
712*67e74705SXin Li    Strictness = Args.strictness
713*67e74705SXin Li    if Args.regenerate:
714*67e74705SXin Li        IsReference = True
715*67e74705SXin Li    elif Args.update_reference:
716*67e74705SXin Li        IsReference = True
717*67e74705SXin Li        UpdateSVN = True
718*67e74705SXin Li
719*67e74705SXin Li    testAll(IsReference, UpdateSVN, Strictness)
720