blob: 93a3a72676804fb0a0f20d1428db3925cd3d642c [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
Anna Zaks8d4a5152011-11-08 22:41:25 +000073# The list of checkers used during analyzes.
74# Currently, consists of all the non experimental checkers.
Anna Zaks9747b532012-02-20 21:10:40 +000075Checkers="experimental.security.taint,core,deadcode,cplusplus,security,unix,osx,cocoa"
Anna Zaks8d4a5152011-11-08 22:41:25 +000076
Anna Zaks1b417162011-10-06 23:26:27 +000077Verbose = 1
78
Anna Zaksd3e29ef2012-01-10 18:10:25 +000079# Make sure we flush the output after every print statement.
80class flushfile(object):
81 def __init__(self, f):
82 self.f = f
83 def write(self, x):
84 self.f.write(x)
85 self.f.flush()
86
87sys.stdout = flushfile(sys.stdout)
88
Anna Zaks1b417162011-10-06 23:26:27 +000089def getProjectMapPath():
90 ProjectMapPath = os.path.join(os.path.abspath(os.curdir),
91 ProjectMapFile)
92 if not os.path.exists(ProjectMapPath):
93 print "Error: Cannot find the Project Map file " + ProjectMapPath +\
94 "\nRunning script for the wrong directory?"
95 sys.exit(-1)
96 return ProjectMapPath
97
98def getProjectDir(ID):
99 return os.path.join(os.path.abspath(os.curdir), ID)
100
Jordan Rose6d7e3722012-06-01 16:24:38 +0000101def getSBOutputDirName(IsReferenceBuild) :
Anna Zaks45518b12011-11-05 05:20:48 +0000102 if IsReferenceBuild == True :
103 return SBOutputDirReferencePrefix + SBOutputDirName
104 else :
105 return SBOutputDirName
106
Anna Zaks1b417162011-10-06 23:26:27 +0000107# Run pre-processing script if any.
Anna Zaks5fa3f132011-11-02 20:46:50 +0000108def runCleanupScript(Dir, PBuildLogFile):
109 ScriptPath = os.path.join(Dir, CleanupScript)
Anna Zaks1b417162011-10-06 23:26:27 +0000110 if os.path.exists(ScriptPath):
111 try:
112 if Verbose == 1:
113 print " Executing: %s" % (ScriptPath,)
114 check_call("chmod +x %s" % ScriptPath, cwd = Dir,
115 stderr=PBuildLogFile,
116 stdout=PBuildLogFile,
117 shell=True)
118 check_call(ScriptPath, cwd = Dir, stderr=PBuildLogFile,
119 stdout=PBuildLogFile,
120 shell=True)
121 except:
122 print "Error: The pre-processing step failed. See ", \
123 PBuildLogFile.name, " for details."
124 sys.exit(-1)
125
126# Build the project with scan-build by reading in the commands and
127# prefixing them with the scan-build options.
128def runScanBuild(Dir, SBOutputDir, PBuildLogFile):
129 BuildScriptPath = os.path.join(Dir, BuildScript)
130 if not os.path.exists(BuildScriptPath):
131 print "Error: build script is not defined: %s" % BuildScriptPath
132 sys.exit(-1)
Anna Zaks2c3038e2012-01-24 21:57:35 +0000133 SBOptions = "-plist-html -o " + SBOutputDir + " "
Anna Zaks8d4a5152011-11-08 22:41:25 +0000134 SBOptions += "-enable-checker " + Checkers + " "
Anna Zaks1b417162011-10-06 23:26:27 +0000135 try:
136 SBCommandFile = open(BuildScriptPath, "r")
137 SBPrefix = "scan-build " + SBOptions + " "
138 for Command in SBCommandFile:
139 SBCommand = SBPrefix + Command
140 if Verbose == 1:
141 print " Executing: %s" % (SBCommand,)
142 check_call(SBCommand, cwd = Dir, stderr=PBuildLogFile,
143 stdout=PBuildLogFile,
144 shell=True)
145 except:
146 print "Error: scan-build failed. See ",PBuildLogFile.name,\
147 " for details."
Anna Zaks45518b12011-11-05 05:20:48 +0000148 raise
Anna Zaks1b417162011-10-06 23:26:27 +0000149
Anna Zaks45518b12011-11-05 05:20:48 +0000150def hasNoExtension(FileName):
151 (Root, Ext) = os.path.splitext(FileName)
152 if ((Ext == "")) :
153 return True
154 return False
155
156def isValidSingleInputFile(FileName):
157 (Root, Ext) = os.path.splitext(FileName)
158 if ((Ext == ".i") | (Ext == ".ii") |
159 (Ext == ".c") | (Ext == ".cpp") |
160 (Ext == ".m") | (Ext == "")) :
161 return True
162 return False
163
164# Run analysis on a set of preprocessed files.
165def runAnalyzePreprocessed(Dir, SBOutputDir):
166 if os.path.exists(os.path.join(Dir, BuildScript)):
167 print "Error: The preprocessed files project should not contain %s" % \
168 BuildScript
169 raise Exception()
170
171 CmdPrefix = "clang -cc1 -analyze -analyzer-output=plist -w "
Anna Zaks8c345c02012-01-21 01:11:35 +0000172 CmdPrefix += "-analyzer-checker=" + Checkers +" -fcxx-exceptions -fblocks "
Anna Zaks45518b12011-11-05 05:20:48 +0000173
174 PlistPath = os.path.join(Dir, SBOutputDir, "date")
175 FailPath = os.path.join(PlistPath, "failures");
176 os.makedirs(FailPath);
177
178 for FullFileName in glob.glob(Dir + "/*"):
179 FileName = os.path.basename(FullFileName)
180 Failed = False
181
182 # Only run the analyzes on supported files.
183 if (hasNoExtension(FileName)):
184 continue
185 if (isValidSingleInputFile(FileName) == False):
186 print "Error: Invalid single input file %s." % (FullFileName,)
187 raise Exception()
188
189 # Build and call the analyzer command.
190 OutputOption = "-o " + os.path.join(PlistPath, FileName) + ".plist "
191 Command = CmdPrefix + OutputOption + os.path.join(Dir, FileName)
192 LogFile = open(os.path.join(FailPath, FileName + ".stderr.txt"), "w+b")
193 try:
194 if Verbose == 1:
195 print " Executing: %s" % (Command,)
196 check_call(Command, cwd = Dir, stderr=LogFile,
197 stdout=LogFile,
198 shell=True)
199 except CalledProcessError, e:
200 print "Error: Analyzes of %s failed. See %s for details." \
201 "Error code %d." % \
202 (FullFileName, LogFile.name, e.returncode)
203 Failed = True
204 finally:
205 LogFile.close()
206
207 # If command did not fail, erase the log file.
208 if Failed == False:
209 os.remove(LogFile.name);
210
Jordan Rose6d7e3722012-06-01 16:24:38 +0000211def buildProject(Dir, SBOutputDir, IsScanBuild, IsReferenceBuild):
Anna Zaks1b417162011-10-06 23:26:27 +0000212 TBegin = time.time()
213
Anna Zaks45518b12011-11-05 05:20:48 +0000214 BuildLogPath = os.path.join(SBOutputDir, LogFolderName, BuildLogName)
Anna Zaks1b417162011-10-06 23:26:27 +0000215 print "Log file: %s" % (BuildLogPath,)
Anna Zaks45518b12011-11-05 05:20:48 +0000216 print "Output directory: %s" %(SBOutputDir, )
217
Anna Zaks1b417162011-10-06 23:26:27 +0000218 # Clean up the log file.
219 if (os.path.exists(BuildLogPath)) :
220 RmCommand = "rm " + BuildLogPath
221 if Verbose == 1:
Anna Zaks5fa3f132011-11-02 20:46:50 +0000222 print " Executing: %s" % (RmCommand,)
Anna Zaks1b417162011-10-06 23:26:27 +0000223 check_call(RmCommand, shell=True)
Anna Zaks45518b12011-11-05 05:20:48 +0000224
225 # Clean up scan build results.
226 if (os.path.exists(SBOutputDir)) :
227 RmCommand = "rm -r " + SBOutputDir
228 if Verbose == 1:
229 print " Executing: %s" % (RmCommand,)
230 check_call(RmCommand, shell=True)
231 assert(not os.path.exists(SBOutputDir))
232 os.makedirs(os.path.join(SBOutputDir, LogFolderName))
Anna Zaks1b417162011-10-06 23:26:27 +0000233
234 # Open the log file.
235 PBuildLogFile = open(BuildLogPath, "wb+")
Anna Zaks1b417162011-10-06 23:26:27 +0000236
Anna Zaks45518b12011-11-05 05:20:48 +0000237 # Build and analyze the project.
238 try:
Anna Zaks5fa3f132011-11-02 20:46:50 +0000239 runCleanupScript(Dir, PBuildLogFile)
Anna Zaks5fa3f132011-11-02 20:46:50 +0000240
Anna Zaks45518b12011-11-05 05:20:48 +0000241 if IsScanBuild:
242 runScanBuild(Dir, SBOutputDir, PBuildLogFile)
243 else:
244 runAnalyzePreprocessed(Dir, SBOutputDir)
245
246 if IsReferenceBuild :
Anna Zaks5fa3f132011-11-02 20:46:50 +0000247 runCleanupScript(Dir, PBuildLogFile)
248
Anna Zaks1b417162011-10-06 23:26:27 +0000249 finally:
250 PBuildLogFile.close()
251
252 print "Build complete (time: %.2f). See the log for more details: %s" % \
253 ((time.time()-TBegin), BuildLogPath)
254
255# A plist file is created for each call to the analyzer(each source file).
256# We are only interested on the once that have bug reports, so delete the rest.
257def CleanUpEmptyPlists(SBOutputDir):
258 for F in glob.glob(SBOutputDir + "/*/*.plist"):
259 P = os.path.join(SBOutputDir, F)
260
261 Data = plistlib.readPlist(P)
262 # Delete empty reports.
263 if not Data['files']:
264 os.remove(P)
265 continue
266
267# Given the scan-build output directory, checks if the build failed
268# (by searching for the failures directories). If there are failures, it
269# creates a summary file in the output directory.
270def checkBuild(SBOutputDir):
271 # Check if there are failures.
272 Failures = glob.glob(SBOutputDir + "/*/failures/*.stderr.txt")
273 TotalFailed = len(Failures);
274 if TotalFailed == 0:
275 CleanUpEmptyPlists(SBOutputDir)
276 Plists = glob.glob(SBOutputDir + "/*/*.plist")
277 print "Number of bug reports (non empty plist files) produced: %d" %\
278 len(Plists)
279 return;
280
281 # Create summary file to display when the build fails.
Anna Zaks45518b12011-11-05 05:20:48 +0000282 SummaryPath = os.path.join(SBOutputDir, LogFolderName, FailuresSummaryFileName)
Anna Zaks1b417162011-10-06 23:26:27 +0000283 if (Verbose > 0):
Anna Zaks45518b12011-11-05 05:20:48 +0000284 print " Creating the failures summary file %s" % (SummaryPath,)
Anna Zaks1b417162011-10-06 23:26:27 +0000285
286 SummaryLog = open(SummaryPath, "w+")
287 try:
288 SummaryLog.write("Total of %d failures discovered.\n" % (TotalFailed,))
289 if TotalFailed > NumOfFailuresInSummary:
290 SummaryLog.write("See the first %d below.\n"
291 % (NumOfFailuresInSummary,))
292 # TODO: Add a line "See the results folder for more."
293
294 FailuresCopied = NumOfFailuresInSummary
295 Idx = 0
Jordan Rose04bc0142012-06-01 16:24:43 +0000296 for FailLogPathI in Failures:
Anna Zaks1b417162011-10-06 23:26:27 +0000297 if Idx >= NumOfFailuresInSummary:
298 break;
299 Idx += 1
300 SummaryLog.write("\n-- Error #%d -----------\n" % (Idx,));
301 FailLogI = open(FailLogPathI, "r");
302 try:
303 shutil.copyfileobj(FailLogI, SummaryLog);
304 finally:
305 FailLogI.close()
306 finally:
307 SummaryLog.close()
308
Anna Zaksf063a3b2012-01-04 23:53:50 +0000309 print "Error: analysis failed. See ", SummaryPath
Anna Zaks1b417162011-10-06 23:26:27 +0000310 sys.exit(-1)
311
312# Auxiliary object to discard stdout.
313class Discarder(object):
314 def write(self, text):
315 pass # do nothing
316
317# Compare the warnings produced by scan-build.
318def runCmpResults(Dir):
319 TBegin = time.time()
320
321 RefDir = os.path.join(Dir, SBOutputDirReferencePrefix + SBOutputDirName)
322 NewDir = os.path.join(Dir, SBOutputDirName)
323
324 # We have to go one level down the directory tree.
325 RefList = glob.glob(RefDir + "/*")
326 NewList = glob.glob(NewDir + "/*")
Anna Zaks45518b12011-11-05 05:20:48 +0000327
328 # Log folders are also located in the results dir, so ignore them.
329 RefList.remove(os.path.join(RefDir, LogFolderName))
330 NewList.remove(os.path.join(NewDir, LogFolderName))
331
Anna Zaks1b417162011-10-06 23:26:27 +0000332 if len(RefList) == 0 or len(NewList) == 0:
333 return False
334 assert(len(RefList) == len(NewList))
335
336 # There might be more then one folder underneath - one per each scan-build
337 # command (Ex: one for configure and one for make).
338 if (len(RefList) > 1):
339 # Assume that the corresponding folders have the same names.
340 RefList.sort()
341 NewList.sort()
342
343 # Iterate and find the differences.
Anna Zaksa7a25642011-11-08 19:56:31 +0000344 NumDiffs = 0
Anna Zaks1b417162011-10-06 23:26:27 +0000345 PairList = zip(RefList, NewList)
346 for P in PairList:
347 RefDir = P[0]
348 NewDir = P[1]
349
350 assert(RefDir != NewDir)
351 if Verbose == 1:
352 print " Comparing Results: %s %s" % (RefDir, NewDir)
353
354 DiffsPath = os.path.join(NewDir, DiffsSummaryFileName)
355 Opts = CmpRuns.CmpOptions(DiffsPath)
356 # Discard everything coming out of stdout (CmpRun produces a lot of them).
357 OLD_STDOUT = sys.stdout
358 sys.stdout = Discarder()
359 # Scan the results, delete empty plist files.
Anna Zaks7acc4072012-07-16 20:21:42 +0000360 NumDiffs = CmpRuns.dumpScanBuildResultsDiff(RefDir, NewDir, Opts, False)
Anna Zaks1b417162011-10-06 23:26:27 +0000361 sys.stdout = OLD_STDOUT
Anna Zaksa7a25642011-11-08 19:56:31 +0000362 if (NumDiffs > 0) :
363 print "Warning: %r differences in diagnostics. See %s" % \
364 (NumDiffs, DiffsPath,)
Anna Zaks1b417162011-10-06 23:26:27 +0000365
366 print "Diagnostic comparison complete (time: %.2f)." % (time.time()-TBegin)
Anna Zaksa7a25642011-11-08 19:56:31 +0000367 return (NumDiffs > 0)
Anna Zaks45518b12011-11-05 05:20:48 +0000368
Anna Zaks09e9cf02012-02-03 06:35:23 +0000369def updateSVN(Mode, ProjectsMap):
370 try:
371 ProjectsMap.seek(0)
372 for I in csv.reader(ProjectsMap):
373 ProjName = I[0]
Jordan Rose6d7e3722012-06-01 16:24:38 +0000374 Path = os.path.join(ProjName, getSBOutputDirName(True))
Anna Zaks09e9cf02012-02-03 06:35:23 +0000375
376 if Mode == "delete":
377 Command = "svn delete %s" % (Path,)
378 else:
379 Command = "svn add %s" % (Path,)
Anna Zaks1b417162011-10-06 23:26:27 +0000380
Anna Zaks09e9cf02012-02-03 06:35:23 +0000381 if Verbose == 1:
382 print " Executing: %s" % (Command,)
Jordan Rose04bc0142012-06-01 16:24:43 +0000383 check_call(Command, shell=True)
Anna Zaks09e9cf02012-02-03 06:35:23 +0000384
385 if Mode == "delete":
386 CommitCommand = "svn commit -m \"[analyzer tests] Remove " \
387 "reference results.\""
388 else:
389 CommitCommand = "svn commit -m \"[analyzer tests] Add new " \
390 "reference results.\""
391 if Verbose == 1:
392 print " Executing: %s" % (CommitCommand,)
Jordan Rose04bc0142012-06-01 16:24:43 +0000393 check_call(CommitCommand, shell=True)
Anna Zaks09e9cf02012-02-03 06:35:23 +0000394 except:
395 print "Error: SVN update failed."
396 sys.exit(-1)
397
Jordan Rose6d7e3722012-06-01 16:24:38 +0000398def testProject(ID, IsScanBuild, IsReferenceBuild=False, Dir=None):
Anna Zaks45518b12011-11-05 05:20:48 +0000399 print " \n\n--- Building project %s" % (ID,)
400
Anna Zaks1b417162011-10-06 23:26:27 +0000401 TBegin = time.time()
402
403 if Dir is None :
404 Dir = getProjectDir(ID)
405 if Verbose == 1:
406 print " Build directory: %s." % (Dir,)
407
408 # Set the build results directory.
Jordan Rose6d7e3722012-06-01 16:24:38 +0000409 RelOutputDir = getSBOutputDirName(IsReferenceBuild)
Anna Zaks09e9cf02012-02-03 06:35:23 +0000410 SBOutputDir = os.path.join(Dir, RelOutputDir)
411
Jordan Rose6d7e3722012-06-01 16:24:38 +0000412 buildProject(Dir, SBOutputDir, IsScanBuild, IsReferenceBuild)
Anna Zaks1b417162011-10-06 23:26:27 +0000413
414 checkBuild(SBOutputDir)
415
416 if IsReferenceBuild == False:
417 runCmpResults(Dir)
418
419 print "Completed tests for project %s (time: %.2f)." % \
420 (ID, (time.time()-TBegin))
421
Jordan Rose6d7e3722012-06-01 16:24:38 +0000422def testAll(IsReferenceBuild = False, UpdateSVN = False):
Anna Zaks1b417162011-10-06 23:26:27 +0000423 PMapFile = open(getProjectMapPath(), "rb")
Anna Zaks09e9cf02012-02-03 06:35:23 +0000424 try:
425 # Validate the input.
426 for I in csv.reader(PMapFile):
Anna Zaks45518b12011-11-05 05:20:48 +0000427 if (len(I) != 2) :
428 print "Error: Rows in the ProjectMapFile should have 3 entries."
429 raise Exception()
430 if (not ((I[1] == "1") | (I[1] == "0"))):
431 print "Error: Second entry in the ProjectMapFile should be 0 or 1."
432 raise Exception()
Anna Zaks09e9cf02012-02-03 06:35:23 +0000433
434 # When we are regenerating the reference results, we might need to
435 # update svn. Remove reference results from SVN.
436 if UpdateSVN == True:
Jordan Rose6d7e3722012-06-01 16:24:38 +0000437 assert(IsReferenceBuild == True);
Anna Zaks09e9cf02012-02-03 06:35:23 +0000438 updateSVN("delete", PMapFile);
439
440 # Test the projects.
441 PMapFile.seek(0)
442 for I in csv.reader(PMapFile):
Jordan Rose6d7e3722012-06-01 16:24:38 +0000443 testProject(I[0], int(I[1]), IsReferenceBuild)
Anna Zaks09e9cf02012-02-03 06:35:23 +0000444
445 # Add reference results to SVN.
446 if UpdateSVN == True:
447 updateSVN("add", PMapFile);
448
Anna Zaks45518b12011-11-05 05:20:48 +0000449 except:
450 print "Error occurred. Premature termination."
451 raise
Anna Zaks1b417162011-10-06 23:26:27 +0000452 finally:
453 PMapFile.close()
454
455if __name__ == '__main__':
Anna Zaks09e9cf02012-02-03 06:35:23 +0000456 IsReference = False
457 UpdateSVN = False
458 if len(sys.argv) >= 2:
459 if sys.argv[1] == "-r":
460 IsReference = True
461 elif sys.argv[1] == "-rs":
462 IsReference = True
463 UpdateSVN = True
464 else:
465 print >> sys.stderr, 'Usage: ', sys.argv[0],\
466 '[-r|-rs]' \
467 'Use -r to regenerate reference output' \
468 'Use -rs to regenerate reference output and update svn'
469
470 testAll(IsReference, UpdateSVN)