summaryrefslogtreecommitdiff
path: root/utils/analyzer/SATestBuild.py
diff options
context:
space:
mode:
Diffstat (limited to 'utils/analyzer/SATestBuild.py')
-rw-r--r--utils/analyzer/SATestBuild.py272
1 files changed, 204 insertions, 68 deletions
diff --git a/utils/analyzer/SATestBuild.py b/utils/analyzer/SATestBuild.py
index 60c8796e338fd..4da025aa53b3a 100644
--- a/utils/analyzer/SATestBuild.py
+++ b/utils/analyzer/SATestBuild.py
@@ -45,32 +45,55 @@ variable. It should contain a comma separated list.
import CmpRuns
import SATestUtils
-import os
+from subprocess import CalledProcessError, check_call
+import argparse
import csv
-import sys
import glob
+import logging
import math
+import multiprocessing
+import os
+import plistlib
import shutil
+import sys
+import threading
import time
-import plistlib
-import argparse
-from subprocess import check_call, CalledProcessError
-import multiprocessing
+import 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')
-sys.stdout = SATestUtils.flushfile(sys.stdout)
+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):
- print "Error: Cannot find the Project Map file " + ProjectMapPath +\
- "\nRunning script for the wrong directory?"
+ Local.stdout.write("Error: Cannot find the Project Map file "
+ + ProjectMapPath
+ + "\nRunning script for the wrong directory?\n")
sys.exit(1)
return ProjectMapPath
@@ -100,7 +123,7 @@ if not Clang:
sys.exit(1)
# Number of jobs.
-Jobs = int(math.ceil(multiprocessing.cpu_count() * 0.75))
+MaxJobs = int(math.ceil(multiprocessing.cpu_count() * 0.75))
# Project map stores info about all the "registered" projects.
ProjectMapFile = "projectMap.csv"
@@ -113,6 +136,9 @@ 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"
@@ -157,7 +183,7 @@ Checkers = ",".join([
"nullability"
])
-Verbose = 1
+Verbose = 0
#------------------------------------------------------------------------------
# Test harness logic.
@@ -170,7 +196,8 @@ def runCleanupScript(Dir, PBuildLogFile):
"""
Cwd = os.path.join(Dir, PatchedSourceDirName)
ScriptPath = os.path.join(Dir, CleanupScript)
- SATestUtils.runScript(ScriptPath, PBuildLogFile, Cwd)
+ SATestUtils.runScript(ScriptPath, PBuildLogFile, Cwd,
+ Stdout=Local.stdout, Stderr=Local.stderr)
def runDownloadScript(Dir, PBuildLogFile):
@@ -178,7 +205,8 @@ 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)
+ SATestUtils.runScript(ScriptPath, PBuildLogFile, Dir,
+ Stdout=Local.stdout, Stderr=Local.stderr)
def downloadAndPatch(Dir, PBuildLogFile):
@@ -192,8 +220,8 @@ def downloadAndPatch(Dir, PBuildLogFile):
if not os.path.exists(CachedSourceDirPath):
runDownloadScript(Dir, PBuildLogFile)
if not os.path.exists(CachedSourceDirPath):
- print "Error: '%s' not found after download." % (
- CachedSourceDirPath)
+ Local.stderr.write("Error: '%s' not found after download.\n" % (
+ CachedSourceDirPath))
exit(1)
PatchedSourceDirPath = os.path.join(Dir, PatchedSourceDirName)
@@ -211,10 +239,10 @@ def applyPatch(Dir, PBuildLogFile):
PatchfilePath = os.path.join(Dir, PatchfileName)
PatchedSourceDirPath = os.path.join(Dir, PatchedSourceDirName)
if not os.path.exists(PatchfilePath):
- print " No local patches."
+ Local.stdout.write(" No local patches.\n")
return
- print " Applying patch."
+ Local.stdout.write(" Applying patch.\n")
try:
check_call("patch -p1 < '%s'" % (PatchfilePath),
cwd=PatchedSourceDirPath,
@@ -222,7 +250,8 @@ def applyPatch(Dir, PBuildLogFile):
stdout=PBuildLogFile,
shell=True)
except:
- print "Error: Patch failed. See %s for details." % (PBuildLogFile.name)
+ Local.stderr.write("Error: Patch failed. See %s for details.\n" % (
+ PBuildLogFile.name))
sys.exit(1)
@@ -233,7 +262,8 @@ def runScanBuild(Dir, SBOutputDir, PBuildLogFile):
"""
BuildScriptPath = os.path.join(Dir, BuildScript)
if not os.path.exists(BuildScriptPath):
- print "Error: build script is not defined: %s" % BuildScriptPath
+ Local.stderr.write(
+ "Error: build script is not defined: %s\n" % BuildScriptPath)
sys.exit(1)
AllCheckers = Checkers
@@ -247,9 +277,18 @@ def runScanBuild(Dir, SBOutputDir, PBuildLogFile):
SBOptions += "-plist-html -o '%s' " % SBOutputDir
SBOptions += "-enable-checker " + AllCheckers + " "
SBOptions += "--keep-empty "
+ AnalyzerConfig = [
+ ("stable-report-filename", "true"),
+ ("serialize-stats", "true"),
+ ]
+
+ SBOptions += "-analyzer-config '%s' " % (
+ ",".join("%s=%s" % (key, value) for (key, value) in AnalyzerConfig))
+
# 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 + " "
@@ -257,23 +296,35 @@ def runScanBuild(Dir, SBOutputDir, PBuildLogFile):
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
+ continue
+
# If using 'make', auto imply a -jX argument
# to speed up analysis. xcodebuild will
# automatically use the maximum number of cores.
if (Command.startswith("make ") or Command == "make") and \
"-j" not in Command:
- Command += " -j%d" % Jobs
+ Command += " -j%d" % MaxJobs
SBCommand = SBPrefix + Command
+
if Verbose == 1:
- print " Executing: %s" % (SBCommand,)
+ 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:
- print "Error: scan-build failed. Its output was: "
+ Local.stderr.write("Error: scan-build failed. Its output was: \n")
PBuildLogFile.seek(0)
- shutil.copyfileobj(PBuildLogFile, sys.stdout)
+ shutil.copyfileobj(PBuildLogFile, Local.stderr)
sys.exit(1)
@@ -282,8 +333,9 @@ def runAnalyzePreprocessed(Dir, SBOutputDir, Mode):
Run analysis on a set of preprocessed files.
"""
if os.path.exists(os.path.join(Dir, BuildScript)):
- print "Error: The preprocessed files project should not contain %s" % \
- BuildScript
+ Local.stderr.write(
+ "Error: The preprocessed files project should not contain %s\n" % (
+ BuildScript))
raise Exception()
CmdPrefix = Clang + " -cc1 "
@@ -313,7 +365,8 @@ def runAnalyzePreprocessed(Dir, SBOutputDir, Mode):
if SATestUtils.hasNoExtension(FileName):
continue
if not SATestUtils.isValidSingleInputFile(FileName):
- print "Error: Invalid single input file %s." % (FullFileName,)
+ Local.stderr.write(
+ "Error: Invalid single input file %s.\n" % (FullFileName,))
raise Exception()
# Build and call the analyzer command.
@@ -322,14 +375,15 @@ def runAnalyzePreprocessed(Dir, SBOutputDir, Mode):
LogFile = open(os.path.join(FailPath, FileName + ".stderr.txt"), "w+b")
try:
if Verbose == 1:
- print " Executing: %s" % (Command,)
+ Local.stdout.write(" Executing: %s\n" % (Command,))
check_call(Command, cwd=Dir, stderr=LogFile,
stdout=LogFile,
shell=True)
except CalledProcessError, e:
- print "Error: Analyzes of %s failed. See %s for details." \
- "Error code %d." % (
- FullFileName, LogFile.name, e.returncode)
+ 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()
@@ -349,7 +403,7 @@ def removeLogFile(SBOutputDir):
if (os.path.exists(BuildLogPath)):
RmCommand = "rm '%s'" % BuildLogPath
if Verbose == 1:
- print " Executing: %s" % (RmCommand,)
+ Local.stdout.write(" Executing: %s\n" % (RmCommand,))
check_call(RmCommand, shell=True)
@@ -357,8 +411,8 @@ def buildProject(Dir, SBOutputDir, ProjectBuildMode, IsReferenceBuild):
TBegin = time.time()
BuildLogPath = getBuildLogPath(SBOutputDir)
- print "Log file: %s" % (BuildLogPath,)
- print "Output directory: %s" % (SBOutputDir, )
+ Local.stdout.write("Log file: %s\n" % (BuildLogPath,))
+ Local.stdout.write("Output directory: %s\n" % (SBOutputDir, ))
removeLogFile(SBOutputDir)
@@ -366,8 +420,9 @@ def buildProject(Dir, SBOutputDir, ProjectBuildMode, IsReferenceBuild):
if (os.path.exists(SBOutputDir)):
RmCommand = "rm -r '%s'" % SBOutputDir
if Verbose == 1:
- print " Executing: %s" % (RmCommand,)
- check_call(RmCommand, shell=True)
+ 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))
@@ -384,8 +439,9 @@ def buildProject(Dir, SBOutputDir, ProjectBuildMode, IsReferenceBuild):
runCleanupScript(Dir, PBuildLogFile)
normalizeReferenceResults(Dir, SBOutputDir, ProjectBuildMode)
- print "Build complete (time: %.2f). See the log for more details: %s" % \
- ((time.time() - TBegin), BuildLogPath)
+ Local.stdout.write("Build complete (time: %.2f). "
+ "See the log for more details: %s\n" % (
+ (time.time() - TBegin), BuildLogPath))
def normalizeReferenceResults(Dir, SBOutputDir, ProjectBuildMode):
@@ -456,14 +512,16 @@ def checkBuild(SBOutputDir):
CleanUpEmptyPlists(SBOutputDir)
CleanUpEmptyFolders(SBOutputDir)
Plists = glob.glob(SBOutputDir + "/*/*.plist")
- print "Number of bug reports (non-empty plist files) produced: %d" %\
- len(Plists)
+ Local.stdout.write(
+ "Number of bug reports (non-empty plist files) produced: %d\n" %
+ len(Plists))
return
- print "Error: analysis failed."
- print "Total of %d failures discovered." % TotalFailed
+ Local.stderr.write("Error: analysis failed.\n")
+ Local.stderr.write("Total of %d failures discovered.\n" % TotalFailed)
if TotalFailed > NumOfFailuresInSummary:
- print "See the first %d below.\n" % NumOfFailuresInSummary
+ Local.stderr.write(
+ "See the first %d below.\n" % NumOfFailuresInSummary)
# TODO: Add a line "See the results folder for more."
Idx = 0
@@ -471,9 +529,9 @@ def checkBuild(SBOutputDir):
if Idx >= NumOfFailuresInSummary:
break
Idx += 1
- print "\n-- Error #%d -----------\n" % Idx
+ Local.stderr.write("\n-- Error #%d -----------\n" % Idx)
with open(FailLogPathI, "r") as FailLogI:
- shutil.copyfileobj(FailLogI, sys.stdout)
+ shutil.copyfileobj(FailLogI, Local.stdout)
sys.exit(1)
@@ -526,25 +584,30 @@ def runCmpResults(Dir, Strictness=0):
assert(RefDir != NewDir)
if Verbose == 1:
- print " Comparing Results: %s %s" % (RefDir, NewDir)
+ Local.stdout.write(" Comparing Results: %s %s\n" % (
+ RefDir, NewDir))
PatchedSourceDirPath = os.path.join(Dir, PatchedSourceDirName)
- Opts = CmpRuns.CmpOptions(rootA="", rootB=PatchedSourceDirPath)
+ 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, False)
+ CmpRuns.dumpScanBuildResultsDiff(RefDir, NewDir, Opts,
+ deleteEmpty=False,
+ Stdout=Local.stdout)
if (NumDiffs > 0):
- print "Warning: %s differences in diagnostics." % NumDiffs
+ Local.stdout.write("Warning: %s differences in diagnostics.\n"
+ % NumDiffs)
if Strictness >= 2 and NumDiffs > 0:
- print "Error: Diffs found in strict mode (2)."
+ Local.stdout.write("Error: Diffs found in strict mode (2).\n")
TestsPassed = False
elif Strictness >= 1 and ReportsInRef != ReportsInNew:
- print "Error: The number of results are different in "\
- "strict mode (1)."
+ Local.stdout.write("Error: The number of results are different " +
+ " strict mode (1).\n")
TestsPassed = False
- print "Diagnostic comparison complete (time: %.2f)." % (
- time.time() - TBegin)
+ Local.stdout.write("Diagnostic comparison complete (time: %.2f).\n" % (
+ time.time() - TBegin))
return TestsPassed
@@ -564,19 +627,49 @@ def cleanupReferenceResults(SBOutputDir):
removeLogFile(SBOutputDir)
+class TestProjectThread(threading.Thread):
+ def __init__(self, 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.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(*ProjArgs):
+ self.ResultsDiffer.set()
+ self.TasksQueue.task_done()
+ except:
+ self.FailureFlag.set()
+ raise
+
+
def testProject(ID, ProjectBuildMode, IsReferenceBuild=False, Strictness=0):
"""
Test a given project.
:return TestsPassed: Whether tests have passed according
to the :param Strictness: criteria.
"""
- print " \n\n--- Building project %s" % (ID,)
+ Local.stdout.write(" \n\n--- Building project %s\n" % (ID,))
TBegin = time.time()
Dir = getProjectDir(ID)
if Verbose == 1:
- print " Build directory: %s." % (Dir,)
+ Local.stdout.write(" Build directory: %s.\n" % (Dir,))
# Set the build results directory.
RelOutputDir = getSBOutputDirName(IsReferenceBuild)
@@ -592,8 +685,8 @@ def testProject(ID, ProjectBuildMode, IsReferenceBuild=False, Strictness=0):
else:
TestsPassed = runCmpResults(Dir, Strictness)
- print "Completed tests for project %s (time: %.2f)." % \
- (ID, (time.time() - TBegin))
+ Local.stdout.write("Completed tests for project %s (time: %.2f).\n" % (
+ ID, (time.time() - TBegin)))
return TestsPassed
@@ -626,17 +719,62 @@ def validateProjectFile(PMapFile):
" (single file), 1 (project), or 2(single file c++11)."
raise Exception()
+def singleThreadedTestAll(ProjectsToTest):
+ """
+ Run all projects.
+ :return: whether tests have passed.
+ """
+ Success = True
+ for ProjArgs in ProjectsToTest:
+ Success &= testProject(*ProjArgs)
+ return Success
+
+def multiThreadedTestAll(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(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 = []
-def testAll(IsReferenceBuild=False, Strictness=0):
- TestsPassed = True
with projectFileHandler() as PMapFile:
validateProjectFile(PMapFile)
# Test the projects.
for (ProjName, ProjBuildMode) in iterateOverProjects(PMapFile):
- TestsPassed &= testProject(
- ProjName, int(ProjBuildMode), IsReferenceBuild, Strictness)
- return TestsPassed
+ ProjectsToTest.append((ProjName,
+ int(ProjBuildMode),
+ Args.regenerate,
+ Args.strictness))
+ if Args.jobs <= 1:
+ return singleThreadedTestAll(ProjectsToTest)
+ else:
+ return multiThreadedTestAll(ProjectsToTest, Args.jobs)
if __name__ == '__main__':
@@ -650,14 +788,12 @@ if __name__ == '__main__':
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')
Args = Parser.parse_args()
- IsReference = False
- Strictness = Args.strictness
- if Args.regenerate:
- IsReference = True
-
- TestsPassed = testAll(IsReference, Strictness)
+ TestsPassed = testAll(Args)
if not TestsPassed:
print "ERROR: Tests failed."
sys.exit(42)