blob: 124897a6f044be873adb74dd434973ee15013c0b [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.
75Checkers="core,deadcode,cplusplus,security,unix,osx,cocoa"
76
Anna Zaks1b417162011-10-06 23:26:27 +000077Verbose = 1
78
Anna Zaks45518b12011-11-05 05:20:48 +000079IsReferenceBuild = False
80
Anna Zaks1b417162011-10-06 23:26:27 +000081def getProjectMapPath():
82 ProjectMapPath = os.path.join(os.path.abspath(os.curdir),
83 ProjectMapFile)
84 if not os.path.exists(ProjectMapPath):
85 print "Error: Cannot find the Project Map file " + ProjectMapPath +\
86 "\nRunning script for the wrong directory?"
87 sys.exit(-1)
88 return ProjectMapPath
89
90def getProjectDir(ID):
91 return os.path.join(os.path.abspath(os.curdir), ID)
92
Anna Zaks45518b12011-11-05 05:20:48 +000093def getSBOutputDirName() :
94 if IsReferenceBuild == True :
95 return SBOutputDirReferencePrefix + SBOutputDirName
96 else :
97 return SBOutputDirName
98
Anna Zaks1b417162011-10-06 23:26:27 +000099# Run pre-processing script if any.
Anna Zaks5fa3f132011-11-02 20:46:50 +0000100def runCleanupScript(Dir, PBuildLogFile):
101 ScriptPath = os.path.join(Dir, CleanupScript)
Anna Zaks1b417162011-10-06 23:26:27 +0000102 if os.path.exists(ScriptPath):
103 try:
104 if Verbose == 1:
105 print " Executing: %s" % (ScriptPath,)
106 check_call("chmod +x %s" % ScriptPath, cwd = Dir,
107 stderr=PBuildLogFile,
108 stdout=PBuildLogFile,
109 shell=True)
110 check_call(ScriptPath, cwd = Dir, stderr=PBuildLogFile,
111 stdout=PBuildLogFile,
112 shell=True)
113 except:
114 print "Error: The pre-processing step failed. See ", \
115 PBuildLogFile.name, " for details."
116 sys.exit(-1)
117
118# Build the project with scan-build by reading in the commands and
119# prefixing them with the scan-build options.
120def runScanBuild(Dir, SBOutputDir, PBuildLogFile):
121 BuildScriptPath = os.path.join(Dir, BuildScript)
122 if not os.path.exists(BuildScriptPath):
123 print "Error: build script is not defined: %s" % BuildScriptPath
124 sys.exit(-1)
125 SBOptions = "-plist -o " + SBOutputDir + " "
Anna Zaks8d4a5152011-11-08 22:41:25 +0000126 SBOptions += "-enable-checker " + Checkers + " "
Anna Zaks1b417162011-10-06 23:26:27 +0000127 try:
128 SBCommandFile = open(BuildScriptPath, "r")
129 SBPrefix = "scan-build " + SBOptions + " "
130 for Command in SBCommandFile:
131 SBCommand = SBPrefix + Command
132 if Verbose == 1:
133 print " Executing: %s" % (SBCommand,)
134 check_call(SBCommand, cwd = Dir, stderr=PBuildLogFile,
135 stdout=PBuildLogFile,
136 shell=True)
137 except:
138 print "Error: scan-build failed. See ",PBuildLogFile.name,\
139 " for details."
Anna Zaks45518b12011-11-05 05:20:48 +0000140 raise
Anna Zaks1b417162011-10-06 23:26:27 +0000141
Anna Zaks45518b12011-11-05 05:20:48 +0000142def hasNoExtension(FileName):
143 (Root, Ext) = os.path.splitext(FileName)
144 if ((Ext == "")) :
145 return True
146 return False
147
148def isValidSingleInputFile(FileName):
149 (Root, Ext) = os.path.splitext(FileName)
150 if ((Ext == ".i") | (Ext == ".ii") |
151 (Ext == ".c") | (Ext == ".cpp") |
152 (Ext == ".m") | (Ext == "")) :
153 return True
154 return False
155
156# Run analysis on a set of preprocessed files.
157def runAnalyzePreprocessed(Dir, SBOutputDir):
158 if os.path.exists(os.path.join(Dir, BuildScript)):
159 print "Error: The preprocessed files project should not contain %s" % \
160 BuildScript
161 raise Exception()
162
163 CmdPrefix = "clang -cc1 -analyze -analyzer-output=plist -w "
Anna Zaks8d4a5152011-11-08 22:41:25 +0000164 CmdPrefix += "-analyzer-checker=" + Checkers +" -fcxx-exceptions -fblocks "
Anna Zaks45518b12011-11-05 05:20:48 +0000165
166 PlistPath = os.path.join(Dir, SBOutputDir, "date")
167 FailPath = os.path.join(PlistPath, "failures");
168 os.makedirs(FailPath);
169
170 for FullFileName in glob.glob(Dir + "/*"):
171 FileName = os.path.basename(FullFileName)
172 Failed = False
173
174 # Only run the analyzes on supported files.
175 if (hasNoExtension(FileName)):
176 continue
177 if (isValidSingleInputFile(FileName) == False):
178 print "Error: Invalid single input file %s." % (FullFileName,)
179 raise Exception()
180
181 # Build and call the analyzer command.
182 OutputOption = "-o " + os.path.join(PlistPath, FileName) + ".plist "
183 Command = CmdPrefix + OutputOption + os.path.join(Dir, FileName)
184 LogFile = open(os.path.join(FailPath, FileName + ".stderr.txt"), "w+b")
185 try:
186 if Verbose == 1:
187 print " Executing: %s" % (Command,)
188 check_call(Command, cwd = Dir, stderr=LogFile,
189 stdout=LogFile,
190 shell=True)
191 except CalledProcessError, e:
192 print "Error: Analyzes of %s failed. See %s for details." \
193 "Error code %d." % \
194 (FullFileName, LogFile.name, e.returncode)
195 Failed = True
196 finally:
197 LogFile.close()
198
199 # If command did not fail, erase the log file.
200 if Failed == False:
201 os.remove(LogFile.name);
202
203def buildProject(Dir, SBOutputDir, IsScanBuild):
Anna Zaks1b417162011-10-06 23:26:27 +0000204 TBegin = time.time()
205
Anna Zaks45518b12011-11-05 05:20:48 +0000206 BuildLogPath = os.path.join(SBOutputDir, LogFolderName, BuildLogName)
Anna Zaks1b417162011-10-06 23:26:27 +0000207 print "Log file: %s" % (BuildLogPath,)
Anna Zaks45518b12011-11-05 05:20:48 +0000208 print "Output directory: %s" %(SBOutputDir, )
209
Anna Zaks1b417162011-10-06 23:26:27 +0000210 # Clean up the log file.
211 if (os.path.exists(BuildLogPath)) :
212 RmCommand = "rm " + BuildLogPath
213 if Verbose == 1:
Anna Zaks5fa3f132011-11-02 20:46:50 +0000214 print " Executing: %s" % (RmCommand,)
Anna Zaks1b417162011-10-06 23:26:27 +0000215 check_call(RmCommand, shell=True)
Anna Zaks45518b12011-11-05 05:20:48 +0000216
217 # Clean up scan build results.
218 if (os.path.exists(SBOutputDir)) :
219 RmCommand = "rm -r " + SBOutputDir
220 if Verbose == 1:
221 print " Executing: %s" % (RmCommand,)
222 check_call(RmCommand, shell=True)
223 assert(not os.path.exists(SBOutputDir))
224 os.makedirs(os.path.join(SBOutputDir, LogFolderName))
Anna Zaks1b417162011-10-06 23:26:27 +0000225
226 # Open the log file.
227 PBuildLogFile = open(BuildLogPath, "wb+")
Anna Zaks1b417162011-10-06 23:26:27 +0000228
Anna Zaks45518b12011-11-05 05:20:48 +0000229 # Build and analyze the project.
230 try:
Anna Zaks5fa3f132011-11-02 20:46:50 +0000231 runCleanupScript(Dir, PBuildLogFile)
Anna Zaks5fa3f132011-11-02 20:46:50 +0000232
Anna Zaks45518b12011-11-05 05:20:48 +0000233 if IsScanBuild:
234 runScanBuild(Dir, SBOutputDir, PBuildLogFile)
235 else:
236 runAnalyzePreprocessed(Dir, SBOutputDir)
237
238 if IsReferenceBuild :
Anna Zaks5fa3f132011-11-02 20:46:50 +0000239 runCleanupScript(Dir, PBuildLogFile)
240
Anna Zaks1b417162011-10-06 23:26:27 +0000241 finally:
242 PBuildLogFile.close()
243
244 print "Build complete (time: %.2f). See the log for more details: %s" % \
245 ((time.time()-TBegin), BuildLogPath)
246
247# A plist file is created for each call to the analyzer(each source file).
248# We are only interested on the once that have bug reports, so delete the rest.
249def CleanUpEmptyPlists(SBOutputDir):
250 for F in glob.glob(SBOutputDir + "/*/*.plist"):
251 P = os.path.join(SBOutputDir, F)
252
253 Data = plistlib.readPlist(P)
254 # Delete empty reports.
255 if not Data['files']:
256 os.remove(P)
257 continue
258
259# Given the scan-build output directory, checks if the build failed
260# (by searching for the failures directories). If there are failures, it
261# creates a summary file in the output directory.
262def checkBuild(SBOutputDir):
263 # Check if there are failures.
264 Failures = glob.glob(SBOutputDir + "/*/failures/*.stderr.txt")
265 TotalFailed = len(Failures);
266 if TotalFailed == 0:
267 CleanUpEmptyPlists(SBOutputDir)
268 Plists = glob.glob(SBOutputDir + "/*/*.plist")
269 print "Number of bug reports (non empty plist files) produced: %d" %\
270 len(Plists)
271 return;
272
273 # Create summary file to display when the build fails.
Anna Zaks45518b12011-11-05 05:20:48 +0000274 SummaryPath = os.path.join(SBOutputDir, LogFolderName, FailuresSummaryFileName)
Anna Zaks1b417162011-10-06 23:26:27 +0000275 if (Verbose > 0):
Anna Zaks45518b12011-11-05 05:20:48 +0000276 print " Creating the failures summary file %s" % (SummaryPath,)
Anna Zaks1b417162011-10-06 23:26:27 +0000277
278 SummaryLog = open(SummaryPath, "w+")
279 try:
280 SummaryLog.write("Total of %d failures discovered.\n" % (TotalFailed,))
281 if TotalFailed > NumOfFailuresInSummary:
282 SummaryLog.write("See the first %d below.\n"
283 % (NumOfFailuresInSummary,))
284 # TODO: Add a line "See the results folder for more."
285
286 FailuresCopied = NumOfFailuresInSummary
287 Idx = 0
288 for FailLogPathI in glob.glob(SBOutputDir + "/*/failures/*.stderr.txt"):
289 if Idx >= NumOfFailuresInSummary:
290 break;
291 Idx += 1
292 SummaryLog.write("\n-- Error #%d -----------\n" % (Idx,));
293 FailLogI = open(FailLogPathI, "r");
294 try:
295 shutil.copyfileobj(FailLogI, SummaryLog);
296 finally:
297 FailLogI.close()
298 finally:
299 SummaryLog.close()
300
Anna Zaks45518b12011-11-05 05:20:48 +0000301 print "Error: analysis failed. See ", \
Anna Zaks1b417162011-10-06 23:26:27 +0000302 os.path.join(SBOutputDir, FailuresSummaryFileName)
303 sys.exit(-1)
304
305# Auxiliary object to discard stdout.
306class Discarder(object):
307 def write(self, text):
308 pass # do nothing
309
310# Compare the warnings produced by scan-build.
311def runCmpResults(Dir):
312 TBegin = time.time()
313
314 RefDir = os.path.join(Dir, SBOutputDirReferencePrefix + SBOutputDirName)
315 NewDir = os.path.join(Dir, SBOutputDirName)
316
317 # We have to go one level down the directory tree.
318 RefList = glob.glob(RefDir + "/*")
319 NewList = glob.glob(NewDir + "/*")
Anna Zaks45518b12011-11-05 05:20:48 +0000320
321 # Log folders are also located in the results dir, so ignore them.
322 RefList.remove(os.path.join(RefDir, LogFolderName))
323 NewList.remove(os.path.join(NewDir, LogFolderName))
324
Anna Zaks1b417162011-10-06 23:26:27 +0000325 if len(RefList) == 0 or len(NewList) == 0:
326 return False
327 assert(len(RefList) == len(NewList))
328
329 # There might be more then one folder underneath - one per each scan-build
330 # command (Ex: one for configure and one for make).
331 if (len(RefList) > 1):
332 # Assume that the corresponding folders have the same names.
333 RefList.sort()
334 NewList.sort()
335
336 # Iterate and find the differences.
Anna Zaksa7a25642011-11-08 19:56:31 +0000337 NumDiffs = 0
Anna Zaks1b417162011-10-06 23:26:27 +0000338 PairList = zip(RefList, NewList)
339 for P in PairList:
340 RefDir = P[0]
341 NewDir = P[1]
342
343 assert(RefDir != NewDir)
344 if Verbose == 1:
345 print " Comparing Results: %s %s" % (RefDir, NewDir)
346
347 DiffsPath = os.path.join(NewDir, DiffsSummaryFileName)
348 Opts = CmpRuns.CmpOptions(DiffsPath)
349 # Discard everything coming out of stdout (CmpRun produces a lot of them).
350 OLD_STDOUT = sys.stdout
351 sys.stdout = Discarder()
352 # Scan the results, delete empty plist files.
Anna Zaksa7a25642011-11-08 19:56:31 +0000353 NumDiffs = CmpRuns.cmpScanBuildResults(RefDir, NewDir, Opts, False)
Anna Zaks1b417162011-10-06 23:26:27 +0000354 sys.stdout = OLD_STDOUT
Anna Zaksa7a25642011-11-08 19:56:31 +0000355 if (NumDiffs > 0) :
356 print "Warning: %r differences in diagnostics. See %s" % \
357 (NumDiffs, DiffsPath,)
Anna Zaks1b417162011-10-06 23:26:27 +0000358
359 print "Diagnostic comparison complete (time: %.2f)." % (time.time()-TBegin)
Anna Zaksa7a25642011-11-08 19:56:31 +0000360 return (NumDiffs > 0)
Anna Zaks45518b12011-11-05 05:20:48 +0000361
362def testProject(ID, InIsReferenceBuild, IsScanBuild , Dir=None):
363 global IsReferenceBuild
364 IsReferenceBuild = InIsReferenceBuild
Anna Zaks1b417162011-10-06 23:26:27 +0000365
Anna Zaks45518b12011-11-05 05:20:48 +0000366 print " \n\n--- Building project %s" % (ID,)
367
Anna Zaks1b417162011-10-06 23:26:27 +0000368 TBegin = time.time()
369
370 if Dir is None :
371 Dir = getProjectDir(ID)
372 if Verbose == 1:
373 print " Build directory: %s." % (Dir,)
374
375 # Set the build results directory.
Anna Zaks45518b12011-11-05 05:20:48 +0000376 SBOutputDir = os.path.join(Dir, getSBOutputDirName())
377
378 buildProject(Dir, SBOutputDir, IsScanBuild)
Anna Zaks1b417162011-10-06 23:26:27 +0000379
380 checkBuild(SBOutputDir)
381
382 if IsReferenceBuild == False:
383 runCmpResults(Dir)
384
385 print "Completed tests for project %s (time: %.2f)." % \
386 (ID, (time.time()-TBegin))
387
Anna Zaks45518b12011-11-05 05:20:48 +0000388def testAll(InIsReferenceBuild = False):
389
Anna Zaks1b417162011-10-06 23:26:27 +0000390 PMapFile = open(getProjectMapPath(), "rb")
391 try:
392 PMapReader = csv.reader(PMapFile)
393 for I in PMapReader:
Anna Zaks45518b12011-11-05 05:20:48 +0000394 if (len(I) != 2) :
395 print "Error: Rows in the ProjectMapFile should have 3 entries."
396 raise Exception()
397 if (not ((I[1] == "1") | (I[1] == "0"))):
398 print "Error: Second entry in the ProjectMapFile should be 0 or 1."
399 raise Exception()
400 testProject(I[0], InIsReferenceBuild, int(I[1]))
Anna Zaksbc05f572011-11-08 00:27:56 +0000401 sys.stdout.flush()
Anna Zaks45518b12011-11-05 05:20:48 +0000402 except:
403 print "Error occurred. Premature termination."
404 raise
Anna Zaks1b417162011-10-06 23:26:27 +0000405 finally:
406 PMapFile.close()
407
408if __name__ == '__main__':
409 testAll()