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