blob: f0eb0b3de4a066dabb27ae7459ff40e1e4b8049b [file] [log] [blame]
Anna Zaks1b417162011-10-06 23:26:27 +00001#!/usr/bin/env python
2
3"""
4Static Analyzer qualification infrastructure.
5
6The goal is to test the analyzer against different projects, check for failures,
7compare results, and measure performance.
8
9Repository Directory will contain sources of the projects as well as the
10information on how to build them and the expected output.
11Repository Directory structure:
12 - ProjectMap file
13 - Historical Performance Data
14 - Project Dir1
15 - ReferenceOutput
16 - Project Dir2
17 - ReferenceOutput
18 ..
19
20To test the build of the analyzer one would:
21 - Copy over a copy of the Repository Directory. (TODO: Prefer to ensure that
22 the build directory does not pollute the repository to min network traffic).
23 - Build all projects, until error. Produce logs to report errors.
24 - Compare results.
25
26The files which should be kept around for failure investigations:
27 RepositoryCopy/Project DirI/ScanBuildResults
28 RepositoryCopy/Project DirI/run_static_analyzer.log
29
30Assumptions (TODO: shouldn't need to assume these.):
31 The script is being run from the Repository Directory.
Anna Zaks5fa3f132011-11-02 20:46:50 +000032 The compiler for scan-build and scan-build are in the PATH.
Anna Zaks1b417162011-10-06 23:26:27 +000033 export PATH=/Users/zaks/workspace/c2llvm/build/Release+Asserts/bin:$PATH
34
35For more logging, set the env variables:
36 zaks:TI zaks$ export CCC_ANALYZER_LOG=1
37 zaks:TI zaks$ export CCC_ANALYZER_VERBOSE=1
38"""
39import CmpRuns
40
41import os
42import csv
43import sys
44import glob
45import shutil
46import time
47import plistlib
Anna Zaks45518b12011-11-05 05:20:48 +000048from subprocess import check_call, CalledProcessError
Anna Zaks1b417162011-10-06 23:26:27 +000049
50# Project map stores info about all the "registered" projects.
51ProjectMapFile = "projectMap.csv"
52
53# Names of the project specific scripts.
54# The script that needs to be executed before the build can start.
Anna Zaks5fa3f132011-11-02 20:46:50 +000055CleanupScript = "cleanup_run_static_analyzer.sh"
Anna Zaks1b417162011-10-06 23:26:27 +000056# This is a file containing commands for scan-build.
57BuildScript = "run_static_analyzer.cmd"
58
59# The log file name.
Anna Zaks45518b12011-11-05 05:20:48 +000060LogFolderName = "Logs"
Anna Zaks1b417162011-10-06 23:26:27 +000061BuildLogName = "run_static_analyzer.log"
62# Summary file - contains the summary of the failures. Ex: This info can be be
63# displayed when buildbot detects a build failure.
64NumOfFailuresInSummary = 10
65FailuresSummaryFileName = "failures.txt"
66# Summary of the result diffs.
67DiffsSummaryFileName = "diffs.txt"
68
69# The scan-build result directory.
70SBOutputDirName = "ScanBuildResults"
71SBOutputDirReferencePrefix = "Ref"
72
73Verbose = 1
74
Anna Zaks45518b12011-11-05 05:20:48 +000075IsReferenceBuild = False
76
Anna Zaks1b417162011-10-06 23:26:27 +000077def getProjectMapPath():
78 ProjectMapPath = os.path.join(os.path.abspath(os.curdir),
79 ProjectMapFile)
80 if not os.path.exists(ProjectMapPath):
81 print "Error: Cannot find the Project Map file " + ProjectMapPath +\
82 "\nRunning script for the wrong directory?"
83 sys.exit(-1)
84 return ProjectMapPath
85
86def getProjectDir(ID):
87 return os.path.join(os.path.abspath(os.curdir), ID)
88
Anna Zaks45518b12011-11-05 05:20:48 +000089def getSBOutputDirName() :
90 if IsReferenceBuild == True :
91 return SBOutputDirReferencePrefix + SBOutputDirName
92 else :
93 return SBOutputDirName
94
Anna Zaks1b417162011-10-06 23:26:27 +000095# Run pre-processing script if any.
Anna Zaks5fa3f132011-11-02 20:46:50 +000096def runCleanupScript(Dir, PBuildLogFile):
97 ScriptPath = os.path.join(Dir, CleanupScript)
Anna Zaks1b417162011-10-06 23:26:27 +000098 if os.path.exists(ScriptPath):
99 try:
100 if Verbose == 1:
101 print " Executing: %s" % (ScriptPath,)
102 check_call("chmod +x %s" % ScriptPath, cwd = Dir,
103 stderr=PBuildLogFile,
104 stdout=PBuildLogFile,
105 shell=True)
106 check_call(ScriptPath, cwd = Dir, stderr=PBuildLogFile,
107 stdout=PBuildLogFile,
108 shell=True)
109 except:
110 print "Error: The pre-processing step failed. See ", \
111 PBuildLogFile.name, " for details."
112 sys.exit(-1)
113
114# Build the project with scan-build by reading in the commands and
115# prefixing them with the scan-build options.
116def runScanBuild(Dir, SBOutputDir, PBuildLogFile):
117 BuildScriptPath = os.path.join(Dir, BuildScript)
118 if not os.path.exists(BuildScriptPath):
119 print "Error: build script is not defined: %s" % BuildScriptPath
120 sys.exit(-1)
121 SBOptions = "-plist -o " + SBOutputDir + " "
122 SBOptions += "-enable-checker core,deadcode.DeadStores"
123 try:
124 SBCommandFile = open(BuildScriptPath, "r")
125 SBPrefix = "scan-build " + SBOptions + " "
126 for Command in SBCommandFile:
127 SBCommand = SBPrefix + Command
128 if Verbose == 1:
129 print " Executing: %s" % (SBCommand,)
130 check_call(SBCommand, cwd = Dir, stderr=PBuildLogFile,
131 stdout=PBuildLogFile,
132 shell=True)
133 except:
134 print "Error: scan-build failed. See ",PBuildLogFile.name,\
135 " for details."
Anna Zaks45518b12011-11-05 05:20:48 +0000136 raise
Anna Zaks1b417162011-10-06 23:26:27 +0000137
Anna Zaks45518b12011-11-05 05:20:48 +0000138def hasNoExtension(FileName):
139 (Root, Ext) = os.path.splitext(FileName)
140 if ((Ext == "")) :
141 return True
142 return False
143
144def isValidSingleInputFile(FileName):
145 (Root, Ext) = os.path.splitext(FileName)
146 if ((Ext == ".i") | (Ext == ".ii") |
147 (Ext == ".c") | (Ext == ".cpp") |
148 (Ext == ".m") | (Ext == "")) :
149 return True
150 return False
151
152# Run analysis on a set of preprocessed files.
153def runAnalyzePreprocessed(Dir, SBOutputDir):
154 if os.path.exists(os.path.join(Dir, BuildScript)):
155 print "Error: The preprocessed files project should not contain %s" % \
156 BuildScript
157 raise Exception()
158
159 CmdPrefix = "clang -cc1 -analyze -analyzer-output=plist -w "
160 CmdPrefix += "-analyzer-checker=core "
161
162 PlistPath = os.path.join(Dir, SBOutputDir, "date")
163 FailPath = os.path.join(PlistPath, "failures");
164 os.makedirs(FailPath);
165
166 for FullFileName in glob.glob(Dir + "/*"):
167 FileName = os.path.basename(FullFileName)
168 Failed = False
169
170 # Only run the analyzes on supported files.
171 if (hasNoExtension(FileName)):
172 continue
173 if (isValidSingleInputFile(FileName) == False):
174 print "Error: Invalid single input file %s." % (FullFileName,)
175 raise Exception()
176
177 # Build and call the analyzer command.
178 OutputOption = "-o " + os.path.join(PlistPath, FileName) + ".plist "
179 Command = CmdPrefix + OutputOption + os.path.join(Dir, FileName)
180 LogFile = open(os.path.join(FailPath, FileName + ".stderr.txt"), "w+b")
181 try:
182 if Verbose == 1:
183 print " Executing: %s" % (Command,)
184 check_call(Command, cwd = Dir, stderr=LogFile,
185 stdout=LogFile,
186 shell=True)
187 except CalledProcessError, e:
188 print "Error: Analyzes of %s failed. See %s for details." \
189 "Error code %d." % \
190 (FullFileName, LogFile.name, e.returncode)
191 Failed = True
192 finally:
193 LogFile.close()
194
195 # If command did not fail, erase the log file.
196 if Failed == False:
197 os.remove(LogFile.name);
198
199def buildProject(Dir, SBOutputDir, IsScanBuild):
Anna Zaks1b417162011-10-06 23:26:27 +0000200 TBegin = time.time()
201
Anna Zaks45518b12011-11-05 05:20:48 +0000202 BuildLogPath = os.path.join(SBOutputDir, LogFolderName, BuildLogName)
Anna Zaks1b417162011-10-06 23:26:27 +0000203 print "Log file: %s" % (BuildLogPath,)
Anna Zaks45518b12011-11-05 05:20:48 +0000204 print "Output directory: %s" %(SBOutputDir, )
205
Anna Zaks1b417162011-10-06 23:26:27 +0000206 # Clean up the log file.
207 if (os.path.exists(BuildLogPath)) :
208 RmCommand = "rm " + BuildLogPath
209 if Verbose == 1:
Anna Zaks5fa3f132011-11-02 20:46:50 +0000210 print " Executing: %s" % (RmCommand,)
Anna Zaks1b417162011-10-06 23:26:27 +0000211 check_call(RmCommand, shell=True)
Anna Zaks45518b12011-11-05 05:20:48 +0000212
213 # Clean up scan build results.
214 if (os.path.exists(SBOutputDir)) :
215 RmCommand = "rm -r " + SBOutputDir
216 if Verbose == 1:
217 print " Executing: %s" % (RmCommand,)
218 check_call(RmCommand, shell=True)
219 assert(not os.path.exists(SBOutputDir))
220 os.makedirs(os.path.join(SBOutputDir, LogFolderName))
Anna Zaks1b417162011-10-06 23:26:27 +0000221
222 # Open the log file.
223 PBuildLogFile = open(BuildLogPath, "wb+")
Anna Zaks1b417162011-10-06 23:26:27 +0000224
Anna Zaks45518b12011-11-05 05:20:48 +0000225 # Build and analyze the project.
226 try:
Anna Zaks5fa3f132011-11-02 20:46:50 +0000227 runCleanupScript(Dir, PBuildLogFile)
Anna Zaks5fa3f132011-11-02 20:46:50 +0000228
Anna Zaks45518b12011-11-05 05:20:48 +0000229 if IsScanBuild:
230 runScanBuild(Dir, SBOutputDir, PBuildLogFile)
231 else:
232 runAnalyzePreprocessed(Dir, SBOutputDir)
233
234 if IsReferenceBuild :
Anna Zaks5fa3f132011-11-02 20:46:50 +0000235 runCleanupScript(Dir, PBuildLogFile)
236
Anna Zaks1b417162011-10-06 23:26:27 +0000237 finally:
238 PBuildLogFile.close()
239
240 print "Build complete (time: %.2f). See the log for more details: %s" % \
241 ((time.time()-TBegin), BuildLogPath)
242
243# A plist file is created for each call to the analyzer(each source file).
244# We are only interested on the once that have bug reports, so delete the rest.
245def CleanUpEmptyPlists(SBOutputDir):
246 for F in glob.glob(SBOutputDir + "/*/*.plist"):
247 P = os.path.join(SBOutputDir, F)
248
249 Data = plistlib.readPlist(P)
250 # Delete empty reports.
251 if not Data['files']:
252 os.remove(P)
253 continue
254
255# Given the scan-build output directory, checks if the build failed
256# (by searching for the failures directories). If there are failures, it
257# creates a summary file in the output directory.
258def checkBuild(SBOutputDir):
259 # Check if there are failures.
260 Failures = glob.glob(SBOutputDir + "/*/failures/*.stderr.txt")
261 TotalFailed = len(Failures);
262 if TotalFailed == 0:
263 CleanUpEmptyPlists(SBOutputDir)
264 Plists = glob.glob(SBOutputDir + "/*/*.plist")
265 print "Number of bug reports (non empty plist files) produced: %d" %\
266 len(Plists)
267 return;
268
269 # Create summary file to display when the build fails.
Anna Zaks45518b12011-11-05 05:20:48 +0000270 SummaryPath = os.path.join(SBOutputDir, LogFolderName, FailuresSummaryFileName)
Anna Zaks1b417162011-10-06 23:26:27 +0000271 if (Verbose > 0):
Anna Zaks45518b12011-11-05 05:20:48 +0000272 print " Creating the failures summary file %s" % (SummaryPath,)
Anna Zaks1b417162011-10-06 23:26:27 +0000273
274 SummaryLog = open(SummaryPath, "w+")
275 try:
276 SummaryLog.write("Total of %d failures discovered.\n" % (TotalFailed,))
277 if TotalFailed > NumOfFailuresInSummary:
278 SummaryLog.write("See the first %d below.\n"
279 % (NumOfFailuresInSummary,))
280 # TODO: Add a line "See the results folder for more."
281
282 FailuresCopied = NumOfFailuresInSummary
283 Idx = 0
284 for FailLogPathI in glob.glob(SBOutputDir + "/*/failures/*.stderr.txt"):
285 if Idx >= NumOfFailuresInSummary:
286 break;
287 Idx += 1
288 SummaryLog.write("\n-- Error #%d -----------\n" % (Idx,));
289 FailLogI = open(FailLogPathI, "r");
290 try:
291 shutil.copyfileobj(FailLogI, SummaryLog);
292 finally:
293 FailLogI.close()
294 finally:
295 SummaryLog.close()
296
Anna Zaks45518b12011-11-05 05:20:48 +0000297 print "Error: analysis failed. See ", \
Anna Zaks1b417162011-10-06 23:26:27 +0000298 os.path.join(SBOutputDir, FailuresSummaryFileName)
299 sys.exit(-1)
300
301# Auxiliary object to discard stdout.
302class Discarder(object):
303 def write(self, text):
304 pass # do nothing
305
306# Compare the warnings produced by scan-build.
307def runCmpResults(Dir):
308 TBegin = time.time()
309
310 RefDir = os.path.join(Dir, SBOutputDirReferencePrefix + SBOutputDirName)
311 NewDir = os.path.join(Dir, SBOutputDirName)
312
313 # We have to go one level down the directory tree.
314 RefList = glob.glob(RefDir + "/*")
315 NewList = glob.glob(NewDir + "/*")
Anna Zaks45518b12011-11-05 05:20:48 +0000316
317 # Log folders are also located in the results dir, so ignore them.
318 RefList.remove(os.path.join(RefDir, LogFolderName))
319 NewList.remove(os.path.join(NewDir, LogFolderName))
320
Anna Zaks1b417162011-10-06 23:26:27 +0000321 if len(RefList) == 0 or len(NewList) == 0:
322 return False
323 assert(len(RefList) == len(NewList))
324
325 # There might be more then one folder underneath - one per each scan-build
326 # command (Ex: one for configure and one for make).
327 if (len(RefList) > 1):
328 # Assume that the corresponding folders have the same names.
329 RefList.sort()
330 NewList.sort()
331
332 # Iterate and find the differences.
Anna Zaksa7a25642011-11-08 19:56:31 +0000333 NumDiffs = 0
Anna Zaks1b417162011-10-06 23:26:27 +0000334 PairList = zip(RefList, NewList)
335 for P in PairList:
336 RefDir = P[0]
337 NewDir = P[1]
338
339 assert(RefDir != NewDir)
340 if Verbose == 1:
341 print " Comparing Results: %s %s" % (RefDir, NewDir)
342
343 DiffsPath = os.path.join(NewDir, DiffsSummaryFileName)
344 Opts = CmpRuns.CmpOptions(DiffsPath)
345 # Discard everything coming out of stdout (CmpRun produces a lot of them).
346 OLD_STDOUT = sys.stdout
347 sys.stdout = Discarder()
348 # Scan the results, delete empty plist files.
Anna Zaksa7a25642011-11-08 19:56:31 +0000349 NumDiffs = CmpRuns.cmpScanBuildResults(RefDir, NewDir, Opts, False)
Anna Zaks1b417162011-10-06 23:26:27 +0000350 sys.stdout = OLD_STDOUT
Anna Zaksa7a25642011-11-08 19:56:31 +0000351 if (NumDiffs > 0) :
352 print "Warning: %r differences in diagnostics. See %s" % \
353 (NumDiffs, DiffsPath,)
Anna Zaks1b417162011-10-06 23:26:27 +0000354
355 print "Diagnostic comparison complete (time: %.2f)." % (time.time()-TBegin)
Anna Zaksa7a25642011-11-08 19:56:31 +0000356 return (NumDiffs > 0)
Anna Zaks45518b12011-11-05 05:20:48 +0000357
358def testProject(ID, InIsReferenceBuild, IsScanBuild , Dir=None):
359 global IsReferenceBuild
360 IsReferenceBuild = InIsReferenceBuild
Anna Zaks1b417162011-10-06 23:26:27 +0000361
Anna Zaks45518b12011-11-05 05:20:48 +0000362 print " \n\n--- Building project %s" % (ID,)
363
Anna Zaks1b417162011-10-06 23:26:27 +0000364 TBegin = time.time()
365
366 if Dir is None :
367 Dir = getProjectDir(ID)
368 if Verbose == 1:
369 print " Build directory: %s." % (Dir,)
370
371 # Set the build results directory.
Anna Zaks45518b12011-11-05 05:20:48 +0000372 SBOutputDir = os.path.join(Dir, getSBOutputDirName())
373
374 buildProject(Dir, SBOutputDir, IsScanBuild)
Anna Zaks1b417162011-10-06 23:26:27 +0000375
376 checkBuild(SBOutputDir)
377
378 if IsReferenceBuild == False:
379 runCmpResults(Dir)
380
381 print "Completed tests for project %s (time: %.2f)." % \
382 (ID, (time.time()-TBegin))
383
Anna Zaks45518b12011-11-05 05:20:48 +0000384def testAll(InIsReferenceBuild = False):
385
Anna Zaks1b417162011-10-06 23:26:27 +0000386 PMapFile = open(getProjectMapPath(), "rb")
387 try:
388 PMapReader = csv.reader(PMapFile)
389 for I in PMapReader:
Anna Zaks45518b12011-11-05 05:20:48 +0000390 if (len(I) != 2) :
391 print "Error: Rows in the ProjectMapFile should have 3 entries."
392 raise Exception()
393 if (not ((I[1] == "1") | (I[1] == "0"))):
394 print "Error: Second entry in the ProjectMapFile should be 0 or 1."
395 raise Exception()
396 testProject(I[0], InIsReferenceBuild, int(I[1]))
Anna Zaksbc05f572011-11-08 00:27:56 +0000397 sys.stdout.flush()
Anna Zaks45518b12011-11-05 05:20:48 +0000398 except:
399 print "Error occurred. Premature termination."
400 raise
Anna Zaks1b417162011-10-06 23:26:27 +0000401 finally:
402 PMapFile.close()
403
404if __name__ == '__main__':
405 testAll()