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