| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596 |
- #!/usr/bin/env python
- """
- Static Analyzer qualification infrastructure.
- The goal is to test the analyzer against different projects, check for failures,
- compare results, and measure performance.
- Repository Directory will contain sources of the projects as well as the
- information on how to build them and the expected output.
- Repository Directory structure:
- - ProjectMap file
- - Historical Performance Data
- - Project Dir1
- - ReferenceOutput
- - Project Dir2
- - ReferenceOutput
- ..
- Note that the build tree must be inside the project dir.
- To test the build of the analyzer one would:
- - Copy over a copy of the Repository Directory. (TODO: Prefer to ensure that
- the build directory does not pollute the repository to min network traffic).
- - Build all projects, until error. Produce logs to report errors.
- - Compare results.
- The files which should be kept around for failure investigations:
- RepositoryCopy/Project DirI/ScanBuildResults
- RepositoryCopy/Project DirI/run_static_analyzer.log
-
- Assumptions (TODO: shouldn't need to assume these.):
- The script is being run from the Repository Directory.
- The compiler for scan-build and scan-build are in the PATH.
- export PATH=/Users/zaks/workspace/c2llvm/build/Release+Asserts/bin:$PATH
- For more logging, set the env variables:
- zaks:TI zaks$ export CCC_ANALYZER_LOG=1
- zaks:TI zaks$ export CCC_ANALYZER_VERBOSE=1
- """
- import CmpRuns
- import os
- import csv
- import sys
- import glob
- import math
- import shutil
- import time
- import plistlib
- import argparse
- from subprocess import check_call, CalledProcessError
- #------------------------------------------------------------------------------
- # Helper functions.
- #------------------------------------------------------------------------------
- def detectCPUs():
- """
- Detects the number of CPUs on a system. Cribbed from pp.
- """
- # Linux, Unix and MacOS:
- if hasattr(os, "sysconf"):
- if os.sysconf_names.has_key("SC_NPROCESSORS_ONLN"):
- # Linux & Unix:
- ncpus = os.sysconf("SC_NPROCESSORS_ONLN")
- if isinstance(ncpus, int) and ncpus > 0:
- return ncpus
- else: # OSX:
- return int(capture(['sysctl', '-n', 'hw.ncpu']))
- # Windows:
- if os.environ.has_key("NUMBER_OF_PROCESSORS"):
- ncpus = int(os.environ["NUMBER_OF_PROCESSORS"])
- if ncpus > 0:
- return ncpus
- return 1 # Default
- def which(command, paths = None):
- """which(command, [paths]) - Look up the given command in the paths string
- (or the PATH environment variable, if unspecified)."""
- if paths is None:
- paths = os.environ.get('PATH','')
- # Check for absolute match first.
- if os.path.exists(command):
- return command
- # Would be nice if Python had a lib function for this.
- if not paths:
- paths = os.defpath
- # Get suffixes to search.
- # On Cygwin, 'PATHEXT' may exist but it should not be used.
- if os.pathsep == ';':
- pathext = os.environ.get('PATHEXT', '').split(';')
- else:
- pathext = ['']
- # Search the paths...
- for path in paths.split(os.pathsep):
- for ext in pathext:
- p = os.path.join(path, command + ext)
- if os.path.exists(p):
- return p
- return None
- # Make sure we flush the output after every print statement.
- class flushfile(object):
- def __init__(self, f):
- self.f = f
- def write(self, x):
- self.f.write(x)
- self.f.flush()
- sys.stdout = flushfile(sys.stdout)
- def getProjectMapPath():
- ProjectMapPath = os.path.join(os.path.abspath(os.curdir),
- ProjectMapFile)
- if not os.path.exists(ProjectMapPath):
- print "Error: Cannot find the Project Map file " + ProjectMapPath +\
- "\nRunning script for the wrong directory?"
- sys.exit(-1)
- return ProjectMapPath
- def getProjectDir(ID):
- return os.path.join(os.path.abspath(os.curdir), ID)
- def getSBOutputDirName(IsReferenceBuild) :
- if IsReferenceBuild == True :
- return SBOutputDirReferencePrefix + SBOutputDirName
- else :
- return SBOutputDirName
- #------------------------------------------------------------------------------
- # Configuration setup.
- #------------------------------------------------------------------------------
- # Find Clang for static analysis.
- Clang = which("clang", os.environ['PATH'])
- if not Clang:
- print "Error: cannot find 'clang' in PATH"
- sys.exit(-1)
- # Number of jobs.
- Jobs = int(math.ceil(detectCPUs() * 0.75))
- # Project map stores info about all the "registered" projects.
- ProjectMapFile = "projectMap.csv"
- # Names of the project specific scripts.
- # The script that needs to be executed before the build can start.
- CleanupScript = "cleanup_run_static_analyzer.sh"
- # This is a file containing commands for scan-build.
- BuildScript = "run_static_analyzer.cmd"
- # The log file name.
- LogFolderName = "Logs"
- BuildLogName = "run_static_analyzer.log"
- # Summary file - contains the summary of the failures. Ex: This info can be be
- # displayed when buildbot detects a build failure.
- NumOfFailuresInSummary = 10
- FailuresSummaryFileName = "failures.txt"
- # Summary of the result diffs.
- DiffsSummaryFileName = "diffs.txt"
- # The scan-build result directory.
- SBOutputDirName = "ScanBuildResults"
- SBOutputDirReferencePrefix = "Ref"
- # The list of checkers used during analyzes.
- # Currently, consists of all the non-experimental checkers, plus a few alpha
- # checkers we don't want to regress on.
- Checkers="alpha.unix.SimpleStream,alpha.security.taint,cplusplus.NewDeleteLeaks,core,cplusplus,deadcode,security,unix,osx"
- Verbose = 1
- #------------------------------------------------------------------------------
- # Test harness logic.
- #------------------------------------------------------------------------------
- # Run pre-processing script if any.
- def runCleanupScript(Dir, PBuildLogFile):
- ScriptPath = os.path.join(Dir, CleanupScript)
- if os.path.exists(ScriptPath):
- try:
- if Verbose == 1:
- print " Executing: %s" % (ScriptPath,)
- check_call("chmod +x %s" % ScriptPath, cwd = Dir,
- stderr=PBuildLogFile,
- stdout=PBuildLogFile,
- shell=True)
- check_call(ScriptPath, cwd = Dir, stderr=PBuildLogFile,
- stdout=PBuildLogFile,
- shell=True)
- except:
- print "Error: The pre-processing step failed. See ", \
- PBuildLogFile.name, " for details."
- sys.exit(-1)
- # Build the project with scan-build by reading in the commands and
- # prefixing them with the scan-build options.
- def runScanBuild(Dir, SBOutputDir, PBuildLogFile):
- BuildScriptPath = os.path.join(Dir, BuildScript)
- if not os.path.exists(BuildScriptPath):
- print "Error: build script is not defined: %s" % BuildScriptPath
- sys.exit(-1)
- SBOptions = "--use-analyzer " + Clang + " "
- SBOptions += "-plist-html -o " + SBOutputDir + " "
- SBOptions += "-enable-checker " + Checkers + " "
- SBOptions += "--keep-empty "
- # Always use ccc-analyze to ensure that we can locate the failures
- # directory.
- SBOptions += "--override-compiler "
- try:
- SBCommandFile = open(BuildScriptPath, "r")
- SBPrefix = "scan-build " + SBOptions + " "
- for Command in SBCommandFile:
- Command = Command.strip()
- if len(Command) == 0:
- continue;
- # If using 'make', auto imply a -jX argument
- # to speed up analysis. xcodebuild will
- # automatically use the maximum number of cores.
- if (Command.startswith("make ") or Command == "make") and \
- "-j" not in Command:
- Command += " -j%d" % Jobs
- SBCommand = SBPrefix + Command
- if Verbose == 1:
- print " Executing: %s" % (SBCommand,)
- check_call(SBCommand, cwd = Dir, stderr=PBuildLogFile,
- stdout=PBuildLogFile,
- shell=True)
- except:
- print "Error: scan-build failed. See ",PBuildLogFile.name,\
- " for details."
- raise
- def hasNoExtension(FileName):
- (Root, Ext) = os.path.splitext(FileName)
- if ((Ext == "")) :
- return True
- return False
- def isValidSingleInputFile(FileName):
- (Root, Ext) = os.path.splitext(FileName)
- if ((Ext == ".i") | (Ext == ".ii") |
- (Ext == ".c") | (Ext == ".cpp") |
- (Ext == ".m") | (Ext == "")) :
- return True
- return False
-
- # Run analysis on a set of preprocessed files.
- def runAnalyzePreprocessed(Dir, SBOutputDir, Mode):
- if os.path.exists(os.path.join(Dir, BuildScript)):
- print "Error: The preprocessed files project should not contain %s" % \
- BuildScript
- raise Exception()
- CmdPrefix = Clang + " -cc1 -analyze -analyzer-output=plist -w "
- CmdPrefix += "-analyzer-checker=" + Checkers +" -fcxx-exceptions -fblocks "
-
- if (Mode == 2) :
- CmdPrefix += "-std=c++11 "
-
- PlistPath = os.path.join(Dir, SBOutputDir, "date")
- FailPath = os.path.join(PlistPath, "failures");
- os.makedirs(FailPath);
-
- for FullFileName in glob.glob(Dir + "/*"):
- FileName = os.path.basename(FullFileName)
- Failed = False
-
- # Only run the analyzes on supported files.
- if (hasNoExtension(FileName)):
- continue
- if (isValidSingleInputFile(FileName) == False):
- print "Error: Invalid single input file %s." % (FullFileName,)
- raise Exception()
-
- # Build and call the analyzer command.
- OutputOption = "-o " + os.path.join(PlistPath, FileName) + ".plist "
- Command = CmdPrefix + OutputOption + FileName
- LogFile = open(os.path.join(FailPath, FileName + ".stderr.txt"), "w+b")
- try:
- if Verbose == 1:
- print " Executing: %s" % (Command,)
- check_call(Command, cwd = Dir, stderr=LogFile,
- stdout=LogFile,
- shell=True)
- except CalledProcessError, e:
- print "Error: Analyzes of %s failed. See %s for details." \
- "Error code %d." % \
- (FullFileName, LogFile.name, e.returncode)
- Failed = True
- finally:
- LogFile.close()
-
- # If command did not fail, erase the log file.
- if Failed == False:
- os.remove(LogFile.name);
- def buildProject(Dir, SBOutputDir, ProjectBuildMode, IsReferenceBuild):
- TBegin = time.time()
- BuildLogPath = os.path.join(SBOutputDir, LogFolderName, BuildLogName)
- print "Log file: %s" % (BuildLogPath,)
- print "Output directory: %s" %(SBOutputDir, )
-
- # Clean up the log file.
- if (os.path.exists(BuildLogPath)) :
- RmCommand = "rm " + BuildLogPath
- if Verbose == 1:
- print " Executing: %s" % (RmCommand,)
- check_call(RmCommand, shell=True)
-
- # Clean up scan build results.
- if (os.path.exists(SBOutputDir)) :
- RmCommand = "rm -r " + SBOutputDir
- if Verbose == 1:
- print " Executing: %s" % (RmCommand,)
- check_call(RmCommand, shell=True)
- assert(not os.path.exists(SBOutputDir))
- os.makedirs(os.path.join(SBOutputDir, LogFolderName))
-
- # Open the log file.
- PBuildLogFile = open(BuildLogPath, "wb+")
-
- # Build and analyze the project.
- try:
- runCleanupScript(Dir, PBuildLogFile)
-
- if (ProjectBuildMode == 1):
- runScanBuild(Dir, SBOutputDir, PBuildLogFile)
- else:
- runAnalyzePreprocessed(Dir, SBOutputDir, ProjectBuildMode)
-
- if IsReferenceBuild :
- runCleanupScript(Dir, PBuildLogFile)
- # Make the absolute paths relative in the reference results.
- for (DirPath, Dirnames, Filenames) in os.walk(SBOutputDir):
- for F in Filenames:
- if (not F.endswith('plist')):
- continue
- Plist = os.path.join(DirPath, F)
- Data = plistlib.readPlist(Plist)
- Paths = [SourceFile[len(Dir)+1:] if SourceFile.startswith(Dir)\
- else SourceFile for SourceFile in Data['files']]
- Data['files'] = Paths
- plistlib.writePlist(Data, Plist)
-
- finally:
- PBuildLogFile.close()
-
- print "Build complete (time: %.2f). See the log for more details: %s" % \
- ((time.time()-TBegin), BuildLogPath)
-
- # A plist file is created for each call to the analyzer(each source file).
- # We are only interested on the once that have bug reports, so delete the rest.
- def CleanUpEmptyPlists(SBOutputDir):
- for F in glob.glob(SBOutputDir + "/*/*.plist"):
- P = os.path.join(SBOutputDir, F)
-
- Data = plistlib.readPlist(P)
- # Delete empty reports.
- if not Data['files']:
- os.remove(P)
- continue
- # Given the scan-build output directory, checks if the build failed
- # (by searching for the failures directories). If there are failures, it
- # creates a summary file in the output directory.
- def checkBuild(SBOutputDir):
- # Check if there are failures.
- Failures = glob.glob(SBOutputDir + "/*/failures/*.stderr.txt")
- TotalFailed = len(Failures);
- if TotalFailed == 0:
- CleanUpEmptyPlists(SBOutputDir)
- Plists = glob.glob(SBOutputDir + "/*/*.plist")
- print "Number of bug reports (non-empty plist files) produced: %d" %\
- len(Plists)
- return;
-
- # Create summary file to display when the build fails.
- SummaryPath = os.path.join(SBOutputDir, LogFolderName, FailuresSummaryFileName)
- if (Verbose > 0):
- print " Creating the failures summary file %s" % (SummaryPath,)
-
- SummaryLog = open(SummaryPath, "w+")
- try:
- SummaryLog.write("Total of %d failures discovered.\n" % (TotalFailed,))
- if TotalFailed > NumOfFailuresInSummary:
- SummaryLog.write("See the first %d below.\n"
- % (NumOfFailuresInSummary,))
- # TODO: Add a line "See the results folder for more."
-
- FailuresCopied = NumOfFailuresInSummary
- Idx = 0
- for FailLogPathI in Failures:
- if Idx >= NumOfFailuresInSummary:
- break;
- Idx += 1
- SummaryLog.write("\n-- Error #%d -----------\n" % (Idx,));
- FailLogI = open(FailLogPathI, "r");
- try:
- shutil.copyfileobj(FailLogI, SummaryLog);
- finally:
- FailLogI.close()
- finally:
- SummaryLog.close()
-
- print "Error: analysis failed. See ", SummaryPath
- sys.exit(-1)
- # Auxiliary object to discard stdout.
- class Discarder(object):
- def write(self, text):
- pass # do nothing
- # Compare the warnings produced by scan-build.
- # Strictness defines the success criteria for the test:
- # 0 - success if there are no crashes or analyzer failure.
- # 1 - success if there are no difference in the number of reported bugs.
- # 2 - success if all the bug reports are identical.
- def runCmpResults(Dir, Strictness = 0):
- TBegin = time.time()
- RefDir = os.path.join(Dir, SBOutputDirReferencePrefix + SBOutputDirName)
- NewDir = os.path.join(Dir, SBOutputDirName)
-
- # We have to go one level down the directory tree.
- RefList = glob.glob(RefDir + "/*")
- NewList = glob.glob(NewDir + "/*")
-
- # Log folders are also located in the results dir, so ignore them.
- RefLogDir = os.path.join(RefDir, LogFolderName)
- if RefLogDir in RefList:
- RefList.remove(RefLogDir)
- NewList.remove(os.path.join(NewDir, LogFolderName))
-
- if len(RefList) == 0 or len(NewList) == 0:
- return False
- assert(len(RefList) == len(NewList))
- # There might be more then one folder underneath - one per each scan-build
- # command (Ex: one for configure and one for make).
- if (len(RefList) > 1):
- # Assume that the corresponding folders have the same names.
- RefList.sort()
- NewList.sort()
-
- # Iterate and find the differences.
- NumDiffs = 0
- PairList = zip(RefList, NewList)
- for P in PairList:
- RefDir = P[0]
- NewDir = P[1]
-
- assert(RefDir != NewDir)
- if Verbose == 1:
- print " Comparing Results: %s %s" % (RefDir, NewDir)
-
- DiffsPath = os.path.join(NewDir, DiffsSummaryFileName)
- Opts = CmpRuns.CmpOptions(DiffsPath, "", Dir)
- # Discard everything coming out of stdout (CmpRun produces a lot of them).
- OLD_STDOUT = sys.stdout
- sys.stdout = Discarder()
- # Scan the results, delete empty plist files.
- NumDiffs, ReportsInRef, ReportsInNew = \
- CmpRuns.dumpScanBuildResultsDiff(RefDir, NewDir, Opts, False)
- sys.stdout = OLD_STDOUT
- if (NumDiffs > 0) :
- print "Warning: %r differences in diagnostics. See %s" % \
- (NumDiffs, DiffsPath,)
- if Strictness >= 2 and NumDiffs > 0:
- print "Error: Diffs found in strict mode (2)."
- sys.exit(-1)
- elif Strictness >= 1 and ReportsInRef != ReportsInNew:
- print "Error: The number of results are different in strict mode (1)."
- sys.exit(-1)
-
- print "Diagnostic comparison complete (time: %.2f)." % (time.time()-TBegin)
- return (NumDiffs > 0)
-
- def updateSVN(Mode, ProjectsMap):
- try:
- ProjectsMap.seek(0)
- for I in csv.reader(ProjectsMap):
- ProjName = I[0]
- Path = os.path.join(ProjName, getSBOutputDirName(True))
-
- if Mode == "delete":
- Command = "svn delete %s" % (Path,)
- else:
- Command = "svn add %s" % (Path,)
- if Verbose == 1:
- print " Executing: %s" % (Command,)
- check_call(Command, shell=True)
-
- if Mode == "delete":
- CommitCommand = "svn commit -m \"[analyzer tests] Remove " \
- "reference results.\""
- else:
- CommitCommand = "svn commit -m \"[analyzer tests] Add new " \
- "reference results.\""
- if Verbose == 1:
- print " Executing: %s" % (CommitCommand,)
- check_call(CommitCommand, shell=True)
- except:
- print "Error: SVN update failed."
- sys.exit(-1)
-
- def testProject(ID, ProjectBuildMode, IsReferenceBuild=False, Dir=None, Strictness = 0):
- print " \n\n--- Building project %s" % (ID,)
- TBegin = time.time()
- if Dir is None :
- Dir = getProjectDir(ID)
- if Verbose == 1:
- print " Build directory: %s." % (Dir,)
-
- # Set the build results directory.
- RelOutputDir = getSBOutputDirName(IsReferenceBuild)
- SBOutputDir = os.path.join(Dir, RelOutputDir)
-
- buildProject(Dir, SBOutputDir, ProjectBuildMode, IsReferenceBuild)
- checkBuild(SBOutputDir)
-
- if IsReferenceBuild == False:
- runCmpResults(Dir, Strictness)
-
- print "Completed tests for project %s (time: %.2f)." % \
- (ID, (time.time()-TBegin))
-
- def testAll(IsReferenceBuild = False, UpdateSVN = False, Strictness = 0):
- PMapFile = open(getProjectMapPath(), "rb")
- try:
- # Validate the input.
- for I in csv.reader(PMapFile):
- if (len(I) != 2) :
- print "Error: Rows in the ProjectMapFile should have 3 entries."
- raise Exception()
- if (not ((I[1] == "0") | (I[1] == "1") | (I[1] == "2"))):
- print "Error: Second entry in the ProjectMapFile should be 0" \
- " (single file), 1 (project), or 2(single file c++11)."
- raise Exception()
- # When we are regenerating the reference results, we might need to
- # update svn. Remove reference results from SVN.
- if UpdateSVN == True:
- assert(IsReferenceBuild == True);
- updateSVN("delete", PMapFile);
-
- # Test the projects.
- PMapFile.seek(0)
- for I in csv.reader(PMapFile):
- testProject(I[0], int(I[1]), IsReferenceBuild, None, Strictness)
- # Add reference results to SVN.
- if UpdateSVN == True:
- updateSVN("add", PMapFile);
- except:
- print "Error occurred. Premature termination."
- raise
- finally:
- PMapFile.close()
-
- if __name__ == '__main__':
- # Parse command line arguments.
- Parser = argparse.ArgumentParser(description='Test the Clang Static Analyzer.')
- 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.')
- Parser.add_argument('-r', dest='regenerate', action='store_true', default=False,
- help='Regenerate reference output.')
- Parser.add_argument('-rs', dest='update_reference', action='store_true',
- default=False, help='Regenerate reference output and update svn.')
- Args = Parser.parse_args()
- IsReference = False
- UpdateSVN = False
- Strictness = Args.strictness
- if Args.regenerate:
- IsReference = True
- elif Args.update_reference:
- IsReference = True
- UpdateSVN = True
- testAll(IsReference, UpdateSVN, Strictness)
|