blob: 71946c75c7b49c303dfa229dbee806c1ded1e411 [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.
Ted Kremenekaade5892012-08-28 18:45:22 +000075Checkers="alpha.security.taint,core,deadcode,security,unix,osx"
Anna Zaks8d4a5152011-11-08 22:41:25 +000076
Anna Zaks1b417162011-10-06 23:26:27 +000077Verbose = 1
78
Ted Kremenek5abd3d22012-08-28 20:20:52 +000079def which(command, paths = None):
80 """which(command, [paths]) - Look up the given command in the paths string
81 (or the PATH environment variable, if unspecified)."""
82
83 if paths is None:
84 paths = os.environ.get('PATH','')
85
86 # Check for absolute match first.
87 if os.path.exists(command):
88 return command
89
90 # Would be nice if Python had a lib function for this.
91 if not paths:
92 paths = os.defpath
93
94 # Get suffixes to search.
95 # On Cygwin, 'PATHEXT' may exist but it should not be used.
96 if os.pathsep == ';':
97 pathext = os.environ.get('PATHEXT', '').split(';')
98 else:
99 pathext = ['']
100
101 # Search the paths...
102 for path in paths.split(os.pathsep):
103 for ext in pathext:
104 p = os.path.join(path, command + ext)
105 if os.path.exists(p):
106 return p
107
108 return None
109
110# Find Clang for static analysis.
111Clang = which("clang", os.environ['PATH'])
112if not Clang:
113 print "Error: cannot find 'clang' in PATH"
114 sys.exit(-1)
115
Anna Zaksd3e29ef2012-01-10 18:10:25 +0000116# Make sure we flush the output after every print statement.
117class flushfile(object):
118 def __init__(self, f):
119 self.f = f
120 def write(self, x):
121 self.f.write(x)
122 self.f.flush()
123
124sys.stdout = flushfile(sys.stdout)
125
Anna Zaks1b417162011-10-06 23:26:27 +0000126def getProjectMapPath():
127 ProjectMapPath = os.path.join(os.path.abspath(os.curdir),
128 ProjectMapFile)
129 if not os.path.exists(ProjectMapPath):
130 print "Error: Cannot find the Project Map file " + ProjectMapPath +\
131 "\nRunning script for the wrong directory?"
132 sys.exit(-1)
133 return ProjectMapPath
134
135def getProjectDir(ID):
136 return os.path.join(os.path.abspath(os.curdir), ID)
137
Jordan Rose6d7e3722012-06-01 16:24:38 +0000138def getSBOutputDirName(IsReferenceBuild) :
Anna Zaks45518b12011-11-05 05:20:48 +0000139 if IsReferenceBuild == True :
140 return SBOutputDirReferencePrefix + SBOutputDirName
141 else :
142 return SBOutputDirName
143
Anna Zaks1b417162011-10-06 23:26:27 +0000144# Run pre-processing script if any.
Anna Zaks5fa3f132011-11-02 20:46:50 +0000145def runCleanupScript(Dir, PBuildLogFile):
146 ScriptPath = os.path.join(Dir, CleanupScript)
Anna Zaks1b417162011-10-06 23:26:27 +0000147 if os.path.exists(ScriptPath):
148 try:
149 if Verbose == 1:
150 print " Executing: %s" % (ScriptPath,)
151 check_call("chmod +x %s" % ScriptPath, cwd = Dir,
152 stderr=PBuildLogFile,
153 stdout=PBuildLogFile,
154 shell=True)
155 check_call(ScriptPath, cwd = Dir, stderr=PBuildLogFile,
156 stdout=PBuildLogFile,
157 shell=True)
158 except:
159 print "Error: The pre-processing step failed. See ", \
160 PBuildLogFile.name, " for details."
161 sys.exit(-1)
162
163# Build the project with scan-build by reading in the commands and
164# prefixing them with the scan-build options.
165def runScanBuild(Dir, SBOutputDir, PBuildLogFile):
166 BuildScriptPath = os.path.join(Dir, BuildScript)
167 if not os.path.exists(BuildScriptPath):
168 print "Error: build script is not defined: %s" % BuildScriptPath
Ted Kremenek5abd3d22012-08-28 20:20:52 +0000169 sys.exit(-1)
170 SBOptions = "--use-analyzer " + Clang + " "
171 SBOptions += "-plist-html -o " + SBOutputDir + " "
Anna Zaks8d4a5152011-11-08 22:41:25 +0000172 SBOptions += "-enable-checker " + Checkers + " "
Anna Zaks1b417162011-10-06 23:26:27 +0000173 try:
174 SBCommandFile = open(BuildScriptPath, "r")
175 SBPrefix = "scan-build " + SBOptions + " "
176 for Command in SBCommandFile:
177 SBCommand = SBPrefix + Command
178 if Verbose == 1:
179 print " Executing: %s" % (SBCommand,)
180 check_call(SBCommand, cwd = Dir, stderr=PBuildLogFile,
181 stdout=PBuildLogFile,
182 shell=True)
183 except:
184 print "Error: scan-build failed. See ",PBuildLogFile.name,\
185 " for details."
Anna Zaks45518b12011-11-05 05:20:48 +0000186 raise
Anna Zaks1b417162011-10-06 23:26:27 +0000187
Anna Zaks45518b12011-11-05 05:20:48 +0000188def hasNoExtension(FileName):
189 (Root, Ext) = os.path.splitext(FileName)
190 if ((Ext == "")) :
191 return True
192 return False
193
194def isValidSingleInputFile(FileName):
195 (Root, Ext) = os.path.splitext(FileName)
196 if ((Ext == ".i") | (Ext == ".ii") |
197 (Ext == ".c") | (Ext == ".cpp") |
198 (Ext == ".m") | (Ext == "")) :
199 return True
200 return False
Ted Kremenek5abd3d22012-08-28 20:20:52 +0000201
Anna Zaks45518b12011-11-05 05:20:48 +0000202# Run analysis on a set of preprocessed files.
203def runAnalyzePreprocessed(Dir, SBOutputDir):
204 if os.path.exists(os.path.join(Dir, BuildScript)):
205 print "Error: The preprocessed files project should not contain %s" % \
206 BuildScript
207 raise Exception()
208
Ted Kremenek5abd3d22012-08-28 20:20:52 +0000209 CmdPrefix = Clang + " -cc1 -analyze -analyzer-output=plist -w "
Anna Zaks8c345c02012-01-21 01:11:35 +0000210 CmdPrefix += "-analyzer-checker=" + Checkers +" -fcxx-exceptions -fblocks "
Anna Zaks45518b12011-11-05 05:20:48 +0000211
212 PlistPath = os.path.join(Dir, SBOutputDir, "date")
213 FailPath = os.path.join(PlistPath, "failures");
214 os.makedirs(FailPath);
215
216 for FullFileName in glob.glob(Dir + "/*"):
217 FileName = os.path.basename(FullFileName)
218 Failed = False
219
220 # Only run the analyzes on supported files.
221 if (hasNoExtension(FileName)):
222 continue
223 if (isValidSingleInputFile(FileName) == False):
224 print "Error: Invalid single input file %s." % (FullFileName,)
225 raise Exception()
226
227 # Build and call the analyzer command.
228 OutputOption = "-o " + os.path.join(PlistPath, FileName) + ".plist "
229 Command = CmdPrefix + OutputOption + os.path.join(Dir, FileName)
230 LogFile = open(os.path.join(FailPath, FileName + ".stderr.txt"), "w+b")
231 try:
232 if Verbose == 1:
233 print " Executing: %s" % (Command,)
234 check_call(Command, cwd = Dir, stderr=LogFile,
235 stdout=LogFile,
236 shell=True)
237 except CalledProcessError, e:
238 print "Error: Analyzes of %s failed. See %s for details." \
239 "Error code %d." % \
240 (FullFileName, LogFile.name, e.returncode)
241 Failed = True
242 finally:
243 LogFile.close()
244
245 # If command did not fail, erase the log file.
246 if Failed == False:
247 os.remove(LogFile.name);
248
Jordan Rose6d7e3722012-06-01 16:24:38 +0000249def buildProject(Dir, SBOutputDir, IsScanBuild, IsReferenceBuild):
Anna Zaks1b417162011-10-06 23:26:27 +0000250 TBegin = time.time()
251
Anna Zaks45518b12011-11-05 05:20:48 +0000252 BuildLogPath = os.path.join(SBOutputDir, LogFolderName, BuildLogName)
Anna Zaks1b417162011-10-06 23:26:27 +0000253 print "Log file: %s" % (BuildLogPath,)
Anna Zaks45518b12011-11-05 05:20:48 +0000254 print "Output directory: %s" %(SBOutputDir, )
255
Anna Zaks1b417162011-10-06 23:26:27 +0000256 # Clean up the log file.
257 if (os.path.exists(BuildLogPath)) :
258 RmCommand = "rm " + BuildLogPath
259 if Verbose == 1:
Anna Zaks5fa3f132011-11-02 20:46:50 +0000260 print " Executing: %s" % (RmCommand,)
Anna Zaks1b417162011-10-06 23:26:27 +0000261 check_call(RmCommand, shell=True)
Anna Zaks45518b12011-11-05 05:20:48 +0000262
263 # Clean up scan build results.
264 if (os.path.exists(SBOutputDir)) :
265 RmCommand = "rm -r " + SBOutputDir
266 if Verbose == 1:
267 print " Executing: %s" % (RmCommand,)
268 check_call(RmCommand, shell=True)
269 assert(not os.path.exists(SBOutputDir))
270 os.makedirs(os.path.join(SBOutputDir, LogFolderName))
Anna Zaks1b417162011-10-06 23:26:27 +0000271
272 # Open the log file.
273 PBuildLogFile = open(BuildLogPath, "wb+")
Anna Zaks1b417162011-10-06 23:26:27 +0000274
Anna Zaks45518b12011-11-05 05:20:48 +0000275 # Build and analyze the project.
276 try:
Anna Zaks5fa3f132011-11-02 20:46:50 +0000277 runCleanupScript(Dir, PBuildLogFile)
Anna Zaks5fa3f132011-11-02 20:46:50 +0000278
Anna Zaks45518b12011-11-05 05:20:48 +0000279 if IsScanBuild:
280 runScanBuild(Dir, SBOutputDir, PBuildLogFile)
281 else:
282 runAnalyzePreprocessed(Dir, SBOutputDir)
283
284 if IsReferenceBuild :
Anna Zaks5fa3f132011-11-02 20:46:50 +0000285 runCleanupScript(Dir, PBuildLogFile)
286
Anna Zaks1b417162011-10-06 23:26:27 +0000287 finally:
288 PBuildLogFile.close()
289
290 print "Build complete (time: %.2f). See the log for more details: %s" % \
291 ((time.time()-TBegin), BuildLogPath)
292
293# A plist file is created for each call to the analyzer(each source file).
294# We are only interested on the once that have bug reports, so delete the rest.
295def CleanUpEmptyPlists(SBOutputDir):
296 for F in glob.glob(SBOutputDir + "/*/*.plist"):
297 P = os.path.join(SBOutputDir, F)
298
299 Data = plistlib.readPlist(P)
300 # Delete empty reports.
301 if not Data['files']:
302 os.remove(P)
303 continue
304
305# Given the scan-build output directory, checks if the build failed
306# (by searching for the failures directories). If there are failures, it
307# creates a summary file in the output directory.
308def checkBuild(SBOutputDir):
309 # Check if there are failures.
310 Failures = glob.glob(SBOutputDir + "/*/failures/*.stderr.txt")
311 TotalFailed = len(Failures);
312 if TotalFailed == 0:
313 CleanUpEmptyPlists(SBOutputDir)
314 Plists = glob.glob(SBOutputDir + "/*/*.plist")
315 print "Number of bug reports (non empty plist files) produced: %d" %\
316 len(Plists)
317 return;
318
319 # Create summary file to display when the build fails.
Anna Zaks45518b12011-11-05 05:20:48 +0000320 SummaryPath = os.path.join(SBOutputDir, LogFolderName, FailuresSummaryFileName)
Anna Zaks1b417162011-10-06 23:26:27 +0000321 if (Verbose > 0):
Anna Zaks45518b12011-11-05 05:20:48 +0000322 print " Creating the failures summary file %s" % (SummaryPath,)
Anna Zaks1b417162011-10-06 23:26:27 +0000323
324 SummaryLog = open(SummaryPath, "w+")
325 try:
326 SummaryLog.write("Total of %d failures discovered.\n" % (TotalFailed,))
327 if TotalFailed > NumOfFailuresInSummary:
328 SummaryLog.write("See the first %d below.\n"
329 % (NumOfFailuresInSummary,))
330 # TODO: Add a line "See the results folder for more."
331
332 FailuresCopied = NumOfFailuresInSummary
333 Idx = 0
Jordan Rose04bc0142012-06-01 16:24:43 +0000334 for FailLogPathI in Failures:
Anna Zaks1b417162011-10-06 23:26:27 +0000335 if Idx >= NumOfFailuresInSummary:
336 break;
337 Idx += 1
338 SummaryLog.write("\n-- Error #%d -----------\n" % (Idx,));
339 FailLogI = open(FailLogPathI, "r");
340 try:
341 shutil.copyfileobj(FailLogI, SummaryLog);
342 finally:
343 FailLogI.close()
344 finally:
345 SummaryLog.close()
346
Anna Zaksf063a3b2012-01-04 23:53:50 +0000347 print "Error: analysis failed. See ", SummaryPath
Anna Zaks1b417162011-10-06 23:26:27 +0000348 sys.exit(-1)
349
350# Auxiliary object to discard stdout.
351class Discarder(object):
352 def write(self, text):
353 pass # do nothing
354
355# Compare the warnings produced by scan-build.
356def runCmpResults(Dir):
357 TBegin = time.time()
358
359 RefDir = os.path.join(Dir, SBOutputDirReferencePrefix + SBOutputDirName)
360 NewDir = os.path.join(Dir, SBOutputDirName)
361
362 # We have to go one level down the directory tree.
363 RefList = glob.glob(RefDir + "/*")
364 NewList = glob.glob(NewDir + "/*")
Anna Zaks45518b12011-11-05 05:20:48 +0000365
366 # Log folders are also located in the results dir, so ignore them.
367 RefList.remove(os.path.join(RefDir, LogFolderName))
368 NewList.remove(os.path.join(NewDir, LogFolderName))
369
Anna Zaks1b417162011-10-06 23:26:27 +0000370 if len(RefList) == 0 or len(NewList) == 0:
371 return False
372 assert(len(RefList) == len(NewList))
373
374 # There might be more then one folder underneath - one per each scan-build
375 # command (Ex: one for configure and one for make).
376 if (len(RefList) > 1):
377 # Assume that the corresponding folders have the same names.
378 RefList.sort()
379 NewList.sort()
380
381 # Iterate and find the differences.
Anna Zaksa7a25642011-11-08 19:56:31 +0000382 NumDiffs = 0
Anna Zaks1b417162011-10-06 23:26:27 +0000383 PairList = zip(RefList, NewList)
384 for P in PairList:
385 RefDir = P[0]
386 NewDir = P[1]
387
388 assert(RefDir != NewDir)
389 if Verbose == 1:
390 print " Comparing Results: %s %s" % (RefDir, NewDir)
391
392 DiffsPath = os.path.join(NewDir, DiffsSummaryFileName)
393 Opts = CmpRuns.CmpOptions(DiffsPath)
394 # Discard everything coming out of stdout (CmpRun produces a lot of them).
395 OLD_STDOUT = sys.stdout
396 sys.stdout = Discarder()
397 # Scan the results, delete empty plist files.
Anna Zaks7acc4072012-07-16 20:21:42 +0000398 NumDiffs = CmpRuns.dumpScanBuildResultsDiff(RefDir, NewDir, Opts, False)
Anna Zaks1b417162011-10-06 23:26:27 +0000399 sys.stdout = OLD_STDOUT
Anna Zaksa7a25642011-11-08 19:56:31 +0000400 if (NumDiffs > 0) :
401 print "Warning: %r differences in diagnostics. See %s" % \
402 (NumDiffs, DiffsPath,)
Anna Zaks1b417162011-10-06 23:26:27 +0000403
404 print "Diagnostic comparison complete (time: %.2f)." % (time.time()-TBegin)
Anna Zaksa7a25642011-11-08 19:56:31 +0000405 return (NumDiffs > 0)
Anna Zaks45518b12011-11-05 05:20:48 +0000406
Anna Zaks09e9cf02012-02-03 06:35:23 +0000407def updateSVN(Mode, ProjectsMap):
408 try:
409 ProjectsMap.seek(0)
410 for I in csv.reader(ProjectsMap):
411 ProjName = I[0]
Jordan Rose6d7e3722012-06-01 16:24:38 +0000412 Path = os.path.join(ProjName, getSBOutputDirName(True))
Anna Zaks09e9cf02012-02-03 06:35:23 +0000413
414 if Mode == "delete":
415 Command = "svn delete %s" % (Path,)
416 else:
417 Command = "svn add %s" % (Path,)
Anna Zaks1b417162011-10-06 23:26:27 +0000418
Anna Zaks09e9cf02012-02-03 06:35:23 +0000419 if Verbose == 1:
420 print " Executing: %s" % (Command,)
Jordan Rose04bc0142012-06-01 16:24:43 +0000421 check_call(Command, shell=True)
Anna Zaks09e9cf02012-02-03 06:35:23 +0000422
423 if Mode == "delete":
424 CommitCommand = "svn commit -m \"[analyzer tests] Remove " \
425 "reference results.\""
426 else:
427 CommitCommand = "svn commit -m \"[analyzer tests] Add new " \
428 "reference results.\""
429 if Verbose == 1:
430 print " Executing: %s" % (CommitCommand,)
Jordan Rose04bc0142012-06-01 16:24:43 +0000431 check_call(CommitCommand, shell=True)
Anna Zaks09e9cf02012-02-03 06:35:23 +0000432 except:
433 print "Error: SVN update failed."
434 sys.exit(-1)
435
Jordan Rose6d7e3722012-06-01 16:24:38 +0000436def testProject(ID, IsScanBuild, IsReferenceBuild=False, Dir=None):
Anna Zaks45518b12011-11-05 05:20:48 +0000437 print " \n\n--- Building project %s" % (ID,)
438
Anna Zaks1b417162011-10-06 23:26:27 +0000439 TBegin = time.time()
440
441 if Dir is None :
442 Dir = getProjectDir(ID)
443 if Verbose == 1:
444 print " Build directory: %s." % (Dir,)
445
446 # Set the build results directory.
Jordan Rose6d7e3722012-06-01 16:24:38 +0000447 RelOutputDir = getSBOutputDirName(IsReferenceBuild)
Anna Zaks09e9cf02012-02-03 06:35:23 +0000448 SBOutputDir = os.path.join(Dir, RelOutputDir)
449
Jordan Rose6d7e3722012-06-01 16:24:38 +0000450 buildProject(Dir, SBOutputDir, IsScanBuild, IsReferenceBuild)
Anna Zaks1b417162011-10-06 23:26:27 +0000451
452 checkBuild(SBOutputDir)
453
454 if IsReferenceBuild == False:
455 runCmpResults(Dir)
456
457 print "Completed tests for project %s (time: %.2f)." % \
458 (ID, (time.time()-TBegin))
459
Jordan Rose6d7e3722012-06-01 16:24:38 +0000460def testAll(IsReferenceBuild = False, UpdateSVN = False):
Anna Zaks1b417162011-10-06 23:26:27 +0000461 PMapFile = open(getProjectMapPath(), "rb")
Anna Zaks09e9cf02012-02-03 06:35:23 +0000462 try:
463 # Validate the input.
464 for I in csv.reader(PMapFile):
Anna Zaks45518b12011-11-05 05:20:48 +0000465 if (len(I) != 2) :
466 print "Error: Rows in the ProjectMapFile should have 3 entries."
467 raise Exception()
468 if (not ((I[1] == "1") | (I[1] == "0"))):
469 print "Error: Second entry in the ProjectMapFile should be 0 or 1."
470 raise Exception()
Anna Zaks09e9cf02012-02-03 06:35:23 +0000471
472 # When we are regenerating the reference results, we might need to
473 # update svn. Remove reference results from SVN.
474 if UpdateSVN == True:
Jordan Rose6d7e3722012-06-01 16:24:38 +0000475 assert(IsReferenceBuild == True);
Anna Zaks09e9cf02012-02-03 06:35:23 +0000476 updateSVN("delete", PMapFile);
477
478 # Test the projects.
479 PMapFile.seek(0)
480 for I in csv.reader(PMapFile):
Jordan Rose6d7e3722012-06-01 16:24:38 +0000481 testProject(I[0], int(I[1]), IsReferenceBuild)
Anna Zaks09e9cf02012-02-03 06:35:23 +0000482
483 # Add reference results to SVN.
484 if UpdateSVN == True:
485 updateSVN("add", PMapFile);
486
Anna Zaks45518b12011-11-05 05:20:48 +0000487 except:
488 print "Error occurred. Premature termination."
489 raise
Anna Zaks1b417162011-10-06 23:26:27 +0000490 finally:
491 PMapFile.close()
492
493if __name__ == '__main__':
Anna Zaks09e9cf02012-02-03 06:35:23 +0000494 IsReference = False
495 UpdateSVN = False
496 if len(sys.argv) >= 2:
497 if sys.argv[1] == "-r":
498 IsReference = True
499 elif sys.argv[1] == "-rs":
500 IsReference = True
501 UpdateSVN = True
502 else:
503 print >> sys.stderr, 'Usage: ', sys.argv[0],\
504 '[-r|-rs]' \
505 'Use -r to regenerate reference output' \
506 'Use -rs to regenerate reference output and update svn'
507
508 testAll(IsReference, UpdateSVN)