forked from OSchip/llvm-project
				
			
		
			
				
	
	
		
			805 lines
		
	
	
		
			27 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
			
		
		
	
	
			805 lines
		
	
	
		
			27 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
| #!/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
 | |
| 
 | |
| The list of checkers tested are hardcoded in the Checkers variable.
 | |
| For testing additional checkers, use the SA_ADDITIONAL_CHECKERS environment
 | |
| variable. It should contain a comma separated list.
 | |
| """
 | |
| import CmpRuns
 | |
| import SATestUtils
 | |
| 
 | |
| from subprocess import CalledProcessError, check_call
 | |
| import argparse
 | |
| import csv
 | |
| import glob
 | |
| import logging
 | |
| import math
 | |
| import multiprocessing
 | |
| import os
 | |
| import plistlib
 | |
| import shutil
 | |
| import sys
 | |
| import threading
 | |
| import time
 | |
| try:
 | |
|     import queue
 | |
| except ImportError:
 | |
|     import Queue as queue
 | |
| 
 | |
| ###############################################################################
 | |
| # Helper functions.
 | |
| ###############################################################################
 | |
| 
 | |
| Local = threading.local()
 | |
| Local.stdout = sys.stdout
 | |
| Local.stderr = sys.stderr
 | |
| logging.basicConfig(
 | |
|     level=logging.DEBUG,
 | |
|     format='%(asctime)s:%(levelname)s:%(name)s: %(message)s')
 | |
| 
 | |
| class StreamToLogger(object):
 | |
|     def __init__(self, logger, log_level=logging.INFO):
 | |
|         self.logger = logger
 | |
|         self.log_level = log_level
 | |
| 
 | |
|     def write(self, buf):
 | |
|         # Rstrip in order not to write an extra newline.
 | |
|         self.logger.log(self.log_level, buf.rstrip())
 | |
| 
 | |
|     def flush(self):
 | |
|         pass
 | |
| 
 | |
|     def fileno(self):
 | |
|         return 0
 | |
| 
 | |
| 
 | |
| def getProjectMapPath():
 | |
|     ProjectMapPath = os.path.join(os.path.abspath(os.curdir),
 | |
|                                   ProjectMapFile)
 | |
|     if not os.path.exists(ProjectMapPath):
 | |
|         Local.stdout.write("Error: Cannot find the Project Map file " +
 | |
|                            ProjectMapPath +
 | |
|                            "\nRunning script for the wrong directory?\n")
 | |
|         sys.exit(1)
 | |
|     return ProjectMapPath
 | |
| 
 | |
| 
 | |
| def getProjectDir(ID):
 | |
|     return os.path.join(os.path.abspath(os.curdir), ID)
 | |
| 
 | |
| 
 | |
| def getSBOutputDirName(IsReferenceBuild):
 | |
|     if IsReferenceBuild:
 | |
|         return SBOutputDirReferencePrefix + SBOutputDirName
 | |
|     else:
 | |
|         return SBOutputDirName
 | |
| 
 | |
| ###############################################################################
 | |
| # Configuration setup.
 | |
| ###############################################################################
 | |
| 
 | |
| 
 | |
| # Find Clang for static analysis.
 | |
| if 'CC' in os.environ:
 | |
|     Clang = os.environ['CC']
 | |
| else:
 | |
|     Clang = SATestUtils.which("clang", os.environ['PATH'])
 | |
| if not Clang:
 | |
|     print("Error: cannot find 'clang' in PATH")
 | |
|     sys.exit(1)
 | |
| 
 | |
| # Number of jobs.
 | |
| MaxJobs = int(math.ceil(multiprocessing.cpu_count() * 0.75))
 | |
| 
 | |
| # Project map stores info about all the "registered" projects.
 | |
| ProjectMapFile = "projectMap.csv"
 | |
| 
 | |
| # Names of the project specific scripts.
 | |
| # The script that downloads the project.
 | |
| DownloadScript = "download_project.sh"
 | |
| # 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"
 | |
| 
 | |
| # A comment in a build script which disables wrapping.
 | |
| NoPrefixCmd = "#NOPREFIX"
 | |
| 
 | |
| # 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"
 | |
| 
 | |
| # The scan-build result directory.
 | |
| SBOutputDirName = "ScanBuildResults"
 | |
| SBOutputDirReferencePrefix = "Ref"
 | |
| 
 | |
| # The name of the directory storing the cached project source. If this
 | |
| # directory does not exist, the download script will be executed.
 | |
| # That script should create the "CachedSource" directory and download the
 | |
| # project source into it.
 | |
| CachedSourceDirName = "CachedSource"
 | |
| 
 | |
| # The name of the directory containing the source code that will be analyzed.
 | |
| # Each time a project is analyzed, a fresh copy of its CachedSource directory
 | |
| # will be copied to the PatchedSource directory and then the local patches
 | |
| # in PatchfileName will be applied (if PatchfileName exists).
 | |
| PatchedSourceDirName = "PatchedSource"
 | |
| 
 | |
| # The name of the patchfile specifying any changes that should be applied
 | |
| # to the CachedSource before analyzing.
 | |
| PatchfileName = "changes_for_analyzer.patch"
 | |
| 
 | |
| # 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 = ",".join([
 | |
|     "alpha.unix.SimpleStream",
 | |
|     "alpha.security.taint",
 | |
|     "cplusplus.NewDeleteLeaks",
 | |
|     "core",
 | |
|     "cplusplus",
 | |
|     "deadcode",
 | |
|     "security",
 | |
|     "unix",
 | |
|     "osx",
 | |
|     "nullability"
 | |
| ])
 | |
| 
 | |
| Verbose = 0
 | |
| 
 | |
| ###############################################################################
 | |
| # Test harness logic.
 | |
| ###############################################################################
 | |
| 
 | |
| 
 | |
| def runCleanupScript(Dir, PBuildLogFile):
 | |
|     """
 | |
|     Run pre-processing script if any.
 | |
|     """
 | |
|     Cwd = os.path.join(Dir, PatchedSourceDirName)
 | |
|     ScriptPath = os.path.join(Dir, CleanupScript)
 | |
|     SATestUtils.runScript(ScriptPath, PBuildLogFile, Cwd,
 | |
|                           Stdout=Local.stdout, Stderr=Local.stderr)
 | |
| 
 | |
| 
 | |
| def runDownloadScript(Dir, PBuildLogFile):
 | |
|     """
 | |
|     Run the script to download the project, if it exists.
 | |
|     """
 | |
|     ScriptPath = os.path.join(Dir, DownloadScript)
 | |
|     SATestUtils.runScript(ScriptPath, PBuildLogFile, Dir,
 | |
|                           Stdout=Local.stdout, Stderr=Local.stderr)
 | |
| 
 | |
| 
 | |
| def downloadAndPatch(Dir, PBuildLogFile):
 | |
|     """
 | |
|     Download the project and apply the local patchfile if it exists.
 | |
|     """
 | |
|     CachedSourceDirPath = os.path.join(Dir, CachedSourceDirName)
 | |
| 
 | |
|     # If the we don't already have the cached source, run the project's
 | |
|     # download script to download it.
 | |
|     if not os.path.exists(CachedSourceDirPath):
 | |
|         runDownloadScript(Dir, PBuildLogFile)
 | |
|         if not os.path.exists(CachedSourceDirPath):
 | |
|             Local.stderr.write("Error: '%s' not found after download.\n" % (
 | |
|                                CachedSourceDirPath))
 | |
|             exit(1)
 | |
| 
 | |
|     PatchedSourceDirPath = os.path.join(Dir, PatchedSourceDirName)
 | |
| 
 | |
|     # Remove potentially stale patched source.
 | |
|     if os.path.exists(PatchedSourceDirPath):
 | |
|         shutil.rmtree(PatchedSourceDirPath)
 | |
| 
 | |
|     # Copy the cached source and apply any patches to the copy.
 | |
|     shutil.copytree(CachedSourceDirPath, PatchedSourceDirPath, symlinks=True)
 | |
|     applyPatch(Dir, PBuildLogFile)
 | |
| 
 | |
| 
 | |
| def applyPatch(Dir, PBuildLogFile):
 | |
|     PatchfilePath = os.path.join(Dir, PatchfileName)
 | |
|     PatchedSourceDirPath = os.path.join(Dir, PatchedSourceDirName)
 | |
|     if not os.path.exists(PatchfilePath):
 | |
|         Local.stdout.write("  No local patches.\n")
 | |
|         return
 | |
| 
 | |
|     Local.stdout.write("  Applying patch.\n")
 | |
|     try:
 | |
|         check_call("patch -p1 < '%s'" % (PatchfilePath),
 | |
|                    cwd=PatchedSourceDirPath,
 | |
|                    stderr=PBuildLogFile,
 | |
|                    stdout=PBuildLogFile,
 | |
|                    shell=True)
 | |
|     except:
 | |
|         Local.stderr.write("Error: Patch failed. See %s for details.\n" % (
 | |
|             PBuildLogFile.name))
 | |
|         sys.exit(1)
 | |
| 
 | |
| 
 | |
| def generateAnalyzerConfig(Args):
 | |
|     Out = "serialize-stats=true,stable-report-filename=true"
 | |
|     if Args.extra_analyzer_config:
 | |
|         Out += "," + Args.extra_analyzer_config
 | |
|     return Out
 | |
| 
 | |
| 
 | |
| def runScanBuild(Args, Dir, SBOutputDir, PBuildLogFile):
 | |
|     """
 | |
|     Build the project with scan-build by reading in the commands and
 | |
|     prefixing them with the scan-build options.
 | |
|     """
 | |
|     BuildScriptPath = os.path.join(Dir, BuildScript)
 | |
|     if not os.path.exists(BuildScriptPath):
 | |
|         Local.stderr.write(
 | |
|             "Error: build script is not defined: %s\n" % BuildScriptPath)
 | |
|         sys.exit(1)
 | |
| 
 | |
|     AllCheckers = Checkers
 | |
|     if 'SA_ADDITIONAL_CHECKERS' in os.environ:
 | |
|         AllCheckers = AllCheckers + ',' + os.environ['SA_ADDITIONAL_CHECKERS']
 | |
| 
 | |
|     # Run scan-build from within the patched source directory.
 | |
|     SBCwd = os.path.join(Dir, PatchedSourceDirName)
 | |
| 
 | |
|     SBOptions = "--use-analyzer '%s' " % Clang
 | |
|     SBOptions += "-plist-html -o '%s' " % SBOutputDir
 | |
|     SBOptions += "-enable-checker " + AllCheckers + " "
 | |
|     SBOptions += "--keep-empty "
 | |
|     SBOptions += "-analyzer-config '%s' " % generateAnalyzerConfig(Args)
 | |
| 
 | |
|     # Always use ccc-analyze to ensure that we can locate the failures
 | |
|     # directory.
 | |
|     SBOptions += "--override-compiler "
 | |
|     ExtraEnv = {}
 | |
|     try:
 | |
|         SBCommandFile = open(BuildScriptPath, "r")
 | |
|         SBPrefix = "scan-build " + SBOptions + " "
 | |
|         for Command in SBCommandFile:
 | |
|             Command = Command.strip()
 | |
|             if len(Command) == 0:
 | |
|                 continue
 | |
| 
 | |
|             # Custom analyzer invocation specified by project.
 | |
|             # Communicate required information using environment variables
 | |
|             # instead.
 | |
|             if Command == NoPrefixCmd:
 | |
|                 SBPrefix = ""
 | |
|                 ExtraEnv['OUTPUT'] = SBOutputDir
 | |
|                 ExtraEnv['CC'] = Clang
 | |
|                 ExtraEnv['ANALYZER_CONFIG'] = generateAnalyzerConfig(Args)
 | |
|                 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" % MaxJobs
 | |
|             SBCommand = SBPrefix + Command
 | |
| 
 | |
|             if Verbose == 1:
 | |
|                 Local.stdout.write("  Executing: %s\n" % (SBCommand,))
 | |
|             check_call(SBCommand, cwd=SBCwd,
 | |
|                        stderr=PBuildLogFile,
 | |
|                        stdout=PBuildLogFile,
 | |
|                        env=dict(os.environ, **ExtraEnv),
 | |
|                        shell=True)
 | |
|     except CalledProcessError:
 | |
|         Local.stderr.write("Error: scan-build failed. Its output was: \n")
 | |
|         PBuildLogFile.seek(0)
 | |
|         shutil.copyfileobj(PBuildLogFile, Local.stderr)
 | |
|         sys.exit(1)
 | |
| 
 | |
| 
 | |
| def runAnalyzePreprocessed(Args, Dir, SBOutputDir, Mode):
 | |
|     """
 | |
|     Run analysis on a set of preprocessed files.
 | |
|     """
 | |
|     if os.path.exists(os.path.join(Dir, BuildScript)):
 | |
|         Local.stderr.write(
 | |
|             "Error: The preprocessed files project should not contain %s\n" % (
 | |
|                 BuildScript))
 | |
|         raise Exception()
 | |
| 
 | |
|     CmdPrefix = Clang + " --analyze "
 | |
| 
 | |
|     CmdPrefix += "--analyzer-output plist "
 | |
|     CmdPrefix += " -Xclang -analyzer-checker=" + Checkers
 | |
|     CmdPrefix += " -fcxx-exceptions -fblocks "
 | |
|     CmdPrefix += " -Xclang -analyzer-config -Xclang %s "\
 | |
|         % generateAnalyzerConfig(Args)
 | |
| 
 | |
|     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 SATestUtils.hasNoExtension(FileName):
 | |
|             continue
 | |
|         if not SATestUtils.isValidSingleInputFile(FileName):
 | |
|             Local.stderr.write(
 | |
|                 "Error: Invalid single input file %s.\n" % (FullFileName,))
 | |
|             raise Exception()
 | |
| 
 | |
|         # Build and call the analyzer command.
 | |
|         OutputOption = "-o '%s.plist' " % os.path.join(PlistPath, FileName)
 | |
|         Command = CmdPrefix + OutputOption + ("'%s'" % FileName)
 | |
|         LogFile = open(os.path.join(FailPath, FileName + ".stderr.txt"), "w+b")
 | |
|         try:
 | |
|             if Verbose == 1:
 | |
|                 Local.stdout.write("  Executing: %s\n" % (Command,))
 | |
|             check_call(Command, cwd=Dir, stderr=LogFile,
 | |
|                        stdout=LogFile,
 | |
|                        shell=True)
 | |
|         except CalledProcessError as e:
 | |
|             Local.stderr.write("Error: Analyzes of %s failed. "
 | |
|                                "See %s for details."
 | |
|                                "Error code %d.\n" % (
 | |
|                                    FullFileName, LogFile.name, e.returncode))
 | |
|             Failed = True
 | |
|         finally:
 | |
|             LogFile.close()
 | |
| 
 | |
|         # If command did not fail, erase the log file.
 | |
|         if not Failed:
 | |
|             os.remove(LogFile.name)
 | |
| 
 | |
| 
 | |
| def getBuildLogPath(SBOutputDir):
 | |
|     return os.path.join(SBOutputDir, LogFolderName, BuildLogName)
 | |
| 
 | |
| 
 | |
| def removeLogFile(SBOutputDir):
 | |
|     BuildLogPath = getBuildLogPath(SBOutputDir)
 | |
|     # Clean up the log file.
 | |
|     if (os.path.exists(BuildLogPath)):
 | |
|         RmCommand = "rm '%s'" % BuildLogPath
 | |
|         if Verbose == 1:
 | |
|             Local.stdout.write("  Executing: %s\n" % (RmCommand,))
 | |
|         check_call(RmCommand, shell=True)
 | |
| 
 | |
| 
 | |
| def buildProject(Args, Dir, SBOutputDir, ProjectBuildMode, IsReferenceBuild):
 | |
|     TBegin = time.time()
 | |
| 
 | |
|     BuildLogPath = getBuildLogPath(SBOutputDir)
 | |
|     Local.stdout.write("Log file: %s\n" % (BuildLogPath,))
 | |
|     Local.stdout.write("Output directory: %s\n" % (SBOutputDir, ))
 | |
| 
 | |
|     removeLogFile(SBOutputDir)
 | |
| 
 | |
|     # Clean up scan build results.
 | |
|     if (os.path.exists(SBOutputDir)):
 | |
|         RmCommand = "rm -r '%s'" % SBOutputDir
 | |
|         if Verbose == 1:
 | |
|             Local.stdout.write("  Executing: %s\n" % (RmCommand,))
 | |
|             check_call(RmCommand, shell=True, stdout=Local.stdout,
 | |
|                        stderr=Local.stderr)
 | |
|     assert(not os.path.exists(SBOutputDir))
 | |
|     os.makedirs(os.path.join(SBOutputDir, LogFolderName))
 | |
| 
 | |
|     # Build and analyze the project.
 | |
|     with open(BuildLogPath, "wb+") as PBuildLogFile:
 | |
|         if (ProjectBuildMode == 1):
 | |
|             downloadAndPatch(Dir, PBuildLogFile)
 | |
|             runCleanupScript(Dir, PBuildLogFile)
 | |
|             runScanBuild(Args, Dir, SBOutputDir, PBuildLogFile)
 | |
|         else:
 | |
|             runAnalyzePreprocessed(Args, Dir, SBOutputDir, ProjectBuildMode)
 | |
| 
 | |
|         if IsReferenceBuild:
 | |
|             runCleanupScript(Dir, PBuildLogFile)
 | |
|             normalizeReferenceResults(Dir, SBOutputDir, ProjectBuildMode)
 | |
| 
 | |
|     Local.stdout.write("Build complete (time: %.2f). "
 | |
|                        "See the log for more details: %s\n" % (
 | |
|                            (time.time() - TBegin), BuildLogPath))
 | |
| 
 | |
| 
 | |
| def normalizeReferenceResults(Dir, SBOutputDir, ProjectBuildMode):
 | |
|     """
 | |
|     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)
 | |
|             PathPrefix = Dir
 | |
|             if (ProjectBuildMode == 1):
 | |
|                 PathPrefix = os.path.join(Dir, PatchedSourceDirName)
 | |
|             Paths = [SourceFile[len(PathPrefix) + 1:]
 | |
|                      if SourceFile.startswith(PathPrefix)
 | |
|                      else SourceFile for SourceFile in Data['files']]
 | |
|             Data['files'] = Paths
 | |
| 
 | |
|             # Remove transient fields which change from run to run.
 | |
|             for Diag in Data['diagnostics']:
 | |
|                 if 'HTMLDiagnostics_files' in Diag:
 | |
|                     Diag.pop('HTMLDiagnostics_files')
 | |
|             if 'clang_version' in Data:
 | |
|                 Data.pop('clang_version')
 | |
| 
 | |
|             plistlib.writePlist(Data, Plist)
 | |
| 
 | |
| 
 | |
| def CleanUpEmptyPlists(SBOutputDir):
 | |
|     """
 | |
|     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.
 | |
|     """
 | |
|     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
 | |
| 
 | |
| 
 | |
| def CleanUpEmptyFolders(SBOutputDir):
 | |
|     """
 | |
|     Remove empty folders from results, as git would not store them.
 | |
|     """
 | |
|     Subfolders = glob.glob(SBOutputDir + "/*")
 | |
|     for Folder in Subfolders:
 | |
|         if not os.listdir(Folder):
 | |
|             os.removedirs(Folder)
 | |
| 
 | |
| 
 | |
| def checkBuild(SBOutputDir):
 | |
|     """
 | |
|     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.
 | |
| 
 | |
|     """
 | |
|     # Check if there are failures.
 | |
|     Failures = glob.glob(SBOutputDir + "/*/failures/*.stderr.txt")
 | |
|     TotalFailed = len(Failures)
 | |
|     if TotalFailed == 0:
 | |
|         CleanUpEmptyPlists(SBOutputDir)
 | |
|         CleanUpEmptyFolders(SBOutputDir)
 | |
|         Plists = glob.glob(SBOutputDir + "/*/*.plist")
 | |
|         Local.stdout.write(
 | |
|             "Number of bug reports (non-empty plist files) produced: %d\n" %
 | |
|             len(Plists))
 | |
|         return
 | |
| 
 | |
|     Local.stderr.write("Error: analysis failed.\n")
 | |
|     Local.stderr.write("Total of %d failures discovered.\n" % TotalFailed)
 | |
|     if TotalFailed > NumOfFailuresInSummary:
 | |
|         Local.stderr.write(
 | |
|             "See the first %d below.\n" % NumOfFailuresInSummary)
 | |
|         # TODO: Add a line "See the results folder for more."
 | |
| 
 | |
|     Idx = 0
 | |
|     for FailLogPathI in Failures:
 | |
|         if Idx >= NumOfFailuresInSummary:
 | |
|             break
 | |
|         Idx += 1
 | |
|         Local.stderr.write("\n-- Error #%d -----------\n" % Idx)
 | |
|         with open(FailLogPathI, "r") as FailLogI:
 | |
|             shutil.copyfileobj(FailLogI, Local.stdout)
 | |
| 
 | |
|     sys.exit(1)
 | |
| 
 | |
| 
 | |
| def runCmpResults(Dir, Strictness=0):
 | |
|     """
 | |
|     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.
 | |
| 
 | |
|     :return success: Whether tests pass according to the Strictness
 | |
|     criteria.
 | |
|     """
 | |
|     TestsPassed = True
 | |
|     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) != len(NewList):
 | |
|         print("Mismatch in number of results folders: %s vs %s" % (
 | |
|             RefList, NewList))
 | |
|         sys.exit(1)
 | |
| 
 | |
|     # 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
 | |
|     for P in zip(RefList, NewList):
 | |
|         RefDir = P[0]
 | |
|         NewDir = P[1]
 | |
| 
 | |
|         assert(RefDir != NewDir)
 | |
|         if Verbose == 1:
 | |
|             Local.stdout.write("  Comparing Results: %s %s\n" % (
 | |
|                                RefDir, NewDir))
 | |
| 
 | |
|         PatchedSourceDirPath = os.path.join(Dir, PatchedSourceDirName)
 | |
|         Opts, Args = CmpRuns.generate_option_parser().parse_args(
 | |
|             ["--rootA", "", "--rootB", PatchedSourceDirPath])
 | |
|         # Scan the results, delete empty plist files.
 | |
|         NumDiffs, ReportsInRef, ReportsInNew = \
 | |
|             CmpRuns.dumpScanBuildResultsDiff(RefDir, NewDir, Opts,
 | |
|                                              deleteEmpty=False,
 | |
|                                              Stdout=Local.stdout)
 | |
|         if (NumDiffs > 0):
 | |
|             Local.stdout.write("Warning: %s differences in diagnostics.\n"
 | |
|                                % NumDiffs)
 | |
|         if Strictness >= 2 and NumDiffs > 0:
 | |
|             Local.stdout.write("Error: Diffs found in strict mode (2).\n")
 | |
|             TestsPassed = False
 | |
|         elif Strictness >= 1 and ReportsInRef != ReportsInNew:
 | |
|             Local.stdout.write("Error: The number of results are different " +
 | |
|                                " strict mode (1).\n")
 | |
|             TestsPassed = False
 | |
| 
 | |
|     Local.stdout.write("Diagnostic comparison complete (time: %.2f).\n" % (
 | |
|                        time.time() - TBegin))
 | |
|     return TestsPassed
 | |
| 
 | |
| 
 | |
| def cleanupReferenceResults(SBOutputDir):
 | |
|     """
 | |
|     Delete html, css, and js files from reference results. These can
 | |
|     include multiple copies of the benchmark source and so get very large.
 | |
|     """
 | |
|     Extensions = ["html", "css", "js"]
 | |
|     for E in Extensions:
 | |
|         for F in glob.glob("%s/*/*.%s" % (SBOutputDir, E)):
 | |
|             P = os.path.join(SBOutputDir, F)
 | |
|             RmCommand = "rm '%s'" % P
 | |
|             check_call(RmCommand, shell=True)
 | |
| 
 | |
|     # Remove the log file. It leaks absolute path names.
 | |
|     removeLogFile(SBOutputDir)
 | |
| 
 | |
| 
 | |
| class TestProjectThread(threading.Thread):
 | |
|     def __init__(self, Args, TasksQueue, ResultsDiffer, FailureFlag):
 | |
|         """
 | |
|         :param ResultsDiffer: Used to signify that results differ from
 | |
|         the canonical ones.
 | |
|         :param FailureFlag: Used to signify a failure during the run.
 | |
|         """
 | |
|         self.Args = Args
 | |
|         self.TasksQueue = TasksQueue
 | |
|         self.ResultsDiffer = ResultsDiffer
 | |
|         self.FailureFlag = FailureFlag
 | |
|         super(TestProjectThread, self).__init__()
 | |
| 
 | |
|         # Needed to gracefully handle interrupts with Ctrl-C
 | |
|         self.daemon = True
 | |
| 
 | |
|     def run(self):
 | |
|         while not self.TasksQueue.empty():
 | |
|             try:
 | |
|                 ProjArgs = self.TasksQueue.get()
 | |
|                 Logger = logging.getLogger(ProjArgs[0])
 | |
|                 Local.stdout = StreamToLogger(Logger, logging.INFO)
 | |
|                 Local.stderr = StreamToLogger(Logger, logging.ERROR)
 | |
|                 if not testProject(Args, *ProjArgs):
 | |
|                     self.ResultsDiffer.set()
 | |
|                 self.TasksQueue.task_done()
 | |
|             except:
 | |
|                 self.FailureFlag.set()
 | |
|                 raise
 | |
| 
 | |
| 
 | |
| def testProject(Args, ID, ProjectBuildMode, IsReferenceBuild=False, Strictness=0):
 | |
|     """
 | |
|     Test a given project.
 | |
|     :return TestsPassed: Whether tests have passed according
 | |
|     to the :param Strictness: criteria.
 | |
|     """
 | |
|     Local.stdout.write(" \n\n--- Building project %s\n" % (ID,))
 | |
| 
 | |
|     TBegin = time.time()
 | |
| 
 | |
|     Dir = getProjectDir(ID)
 | |
|     if Verbose == 1:
 | |
|         Local.stdout.write("  Build directory: %s.\n" % (Dir,))
 | |
| 
 | |
|     # Set the build results directory.
 | |
|     RelOutputDir = getSBOutputDirName(IsReferenceBuild)
 | |
|     SBOutputDir = os.path.join(Dir, RelOutputDir)
 | |
| 
 | |
|     buildProject(Args, Dir, SBOutputDir, ProjectBuildMode, IsReferenceBuild)
 | |
| 
 | |
|     checkBuild(SBOutputDir)
 | |
| 
 | |
|     if IsReferenceBuild:
 | |
|         cleanupReferenceResults(SBOutputDir)
 | |
|         TestsPassed = True
 | |
|     else:
 | |
|         TestsPassed = runCmpResults(Dir, Strictness)
 | |
| 
 | |
|     Local.stdout.write("Completed tests for project %s (time: %.2f).\n" % (
 | |
|                        ID, (time.time() - TBegin)))
 | |
|     return TestsPassed
 | |
| 
 | |
| 
 | |
| def projectFileHandler():
 | |
|     return open(getProjectMapPath(), "rb")
 | |
| 
 | |
| 
 | |
| def iterateOverProjects(PMapFile):
 | |
|     """
 | |
|     Iterate over all projects defined in the project file handler `PMapFile`
 | |
|     from the start.
 | |
|     """
 | |
|     PMapFile.seek(0)
 | |
|     for I in csv.reader(PMapFile):
 | |
|         if (SATestUtils.isCommentCSVLine(I)):
 | |
|             continue
 | |
|         yield I
 | |
| 
 | |
| 
 | |
| def validateProjectFile(PMapFile):
 | |
|     """
 | |
|     Validate project file.
 | |
|     """
 | |
|     for I in iterateOverProjects(PMapFile):
 | |
|         if len(I) != 2:
 | |
|             print("Error: Rows in the ProjectMapFile should have 2 entries.")
 | |
|             raise Exception()
 | |
|         if I[1] not in ('0', '1', '2'):
 | |
|             print("Error: Second entry in the ProjectMapFile should be 0" \
 | |
|                   " (single file), 1 (project), or 2(single file c++11).")
 | |
|             raise Exception()
 | |
| 
 | |
| def singleThreadedTestAll(Args, ProjectsToTest):
 | |
|     """
 | |
|     Run all projects.
 | |
|     :return: whether tests have passed.
 | |
|     """
 | |
|     Success = True
 | |
|     for ProjArgs in ProjectsToTest:
 | |
|         Success &= testProject(Args, *ProjArgs)
 | |
|     return Success
 | |
| 
 | |
| def multiThreadedTestAll(Args, ProjectsToTest, Jobs):
 | |
|     """
 | |
|     Run each project in a separate thread.
 | |
| 
 | |
|     This is OK despite GIL, as testing is blocked
 | |
|     on launching external processes.
 | |
| 
 | |
|     :return: whether tests have passed.
 | |
|     """
 | |
|     TasksQueue = queue.Queue()
 | |
| 
 | |
|     for ProjArgs in ProjectsToTest:
 | |
|         TasksQueue.put(ProjArgs)
 | |
| 
 | |
|     ResultsDiffer = threading.Event()
 | |
|     FailureFlag = threading.Event()
 | |
| 
 | |
|     for i in range(Jobs):
 | |
|         T = TestProjectThread(Args, TasksQueue, ResultsDiffer, FailureFlag)
 | |
|         T.start()
 | |
| 
 | |
|     # Required to handle Ctrl-C gracefully.
 | |
|     while TasksQueue.unfinished_tasks:
 | |
|         time.sleep(0.1)  # Seconds.
 | |
|         if FailureFlag.is_set():
 | |
|             Local.stderr.write("Test runner crashed\n")
 | |
|             sys.exit(1)
 | |
|     return not ResultsDiffer.is_set()
 | |
| 
 | |
| 
 | |
| def testAll(Args):
 | |
|     ProjectsToTest = []
 | |
| 
 | |
|     with projectFileHandler() as PMapFile:
 | |
|         validateProjectFile(PMapFile)
 | |
| 
 | |
|         # Test the projects.
 | |
|         for (ProjName, ProjBuildMode) in iterateOverProjects(PMapFile):
 | |
|             ProjectsToTest.append((ProjName,
 | |
|                                   int(ProjBuildMode),
 | |
|                                   Args.regenerate,
 | |
|                                   Args.strictness))
 | |
|     if Args.jobs <= 1:
 | |
|         return singleThreadedTestAll(Args, ProjectsToTest)
 | |
|     else:
 | |
|         return multiThreadedTestAll(Args, ProjectsToTest, Args.jobs)
 | |
| 
 | |
| 
 | |
| 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('-j', '--jobs', dest='jobs', type=int,
 | |
|                         default=0,
 | |
|                         help='Number of projects to test concurrently')
 | |
|     Parser.add_argument('--extra-analyzer-config', dest='extra_analyzer_config',
 | |
|                         type=str,
 | |
|                         default="",
 | |
|                         help="Arguments passed to to -analyzer-config")
 | |
|     Args = Parser.parse_args()
 | |
| 
 | |
|     TestsPassed = testAll(Args)
 | |
|     if not TestsPassed:
 | |
|         print("ERROR: Tests failed.")
 | |
|         sys.exit(42)
 |