blob: 8f008f19473c6eeeab9001be2b833bb9ee1c699c [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
Ted Kremenek7c14c442012-08-28 20:40:02 +000050#------------------------------------------------------------------------------
51# Helper functions.
52#------------------------------------------------------------------------------
Anna Zaks1b417162011-10-06 23:26:27 +000053
Ted Kremenek5abd3d22012-08-28 20:20:52 +000054def which(command, paths = None):
55 """which(command, [paths]) - Look up the given command in the paths string
56 (or the PATH environment variable, if unspecified)."""
57
58 if paths is None:
59 paths = os.environ.get('PATH','')
60
61 # Check for absolute match first.
62 if os.path.exists(command):
63 return command
64
65 # Would be nice if Python had a lib function for this.
66 if not paths:
67 paths = os.defpath
68
69 # Get suffixes to search.
70 # On Cygwin, 'PATHEXT' may exist but it should not be used.
71 if os.pathsep == ';':
72 pathext = os.environ.get('PATHEXT', '').split(';')
73 else:
74 pathext = ['']
75
76 # Search the paths...
77 for path in paths.split(os.pathsep):
78 for ext in pathext:
79 p = os.path.join(path, command + ext)
80 if os.path.exists(p):
81 return p
82
83 return None
84
Anna Zaksd3e29ef2012-01-10 18:10:25 +000085# Make sure we flush the output after every print statement.
86class flushfile(object):
87 def __init__(self, f):
88 self.f = f
89 def write(self, x):
90 self.f.write(x)
91 self.f.flush()
92
93sys.stdout = flushfile(sys.stdout)
94
Anna Zaks1b417162011-10-06 23:26:27 +000095def getProjectMapPath():
96 ProjectMapPath = os.path.join(os.path.abspath(os.curdir),
97 ProjectMapFile)
98 if not os.path.exists(ProjectMapPath):
99 print "Error: Cannot find the Project Map file " + ProjectMapPath +\
100 "\nRunning script for the wrong directory?"
101 sys.exit(-1)
102 return ProjectMapPath
103
104def getProjectDir(ID):
105 return os.path.join(os.path.abspath(os.curdir), ID)
106
Jordan Rose6d7e3722012-06-01 16:24:38 +0000107def getSBOutputDirName(IsReferenceBuild) :
Anna Zaks45518b12011-11-05 05:20:48 +0000108 if IsReferenceBuild == True :
109 return SBOutputDirReferencePrefix + SBOutputDirName
110 else :
111 return SBOutputDirName
112
Ted Kremenek7c14c442012-08-28 20:40:02 +0000113#------------------------------------------------------------------------------
114# Configuration setup.
115#------------------------------------------------------------------------------
116
117# Find Clang for static analysis.
118Clang = which("clang", os.environ['PATH'])
119if not Clang:
120 print "Error: cannot find 'clang' in PATH"
121 sys.exit(-1)
122
123# Project map stores info about all the "registered" projects.
124ProjectMapFile = "projectMap.csv"
125
126# Names of the project specific scripts.
127# The script that needs to be executed before the build can start.
128CleanupScript = "cleanup_run_static_analyzer.sh"
129# This is a file containing commands for scan-build.
130BuildScript = "run_static_analyzer.cmd"
131
132# The log file name.
133LogFolderName = "Logs"
134BuildLogName = "run_static_analyzer.log"
135# Summary file - contains the summary of the failures. Ex: This info can be be
136# displayed when buildbot detects a build failure.
137NumOfFailuresInSummary = 10
138FailuresSummaryFileName = "failures.txt"
139# Summary of the result diffs.
140DiffsSummaryFileName = "diffs.txt"
141
142# The scan-build result directory.
143SBOutputDirName = "ScanBuildResults"
144SBOutputDirReferencePrefix = "Ref"
145
146# The list of checkers used during analyzes.
147# Currently, consists of all the non experimental checkers.
148Checkers="alpha.security.taint,core,deadcode,security,unix,osx"
149
150Verbose = 1
151
152#------------------------------------------------------------------------------
153# Test harness logic.
154#------------------------------------------------------------------------------
155
Anna Zaks1b417162011-10-06 23:26:27 +0000156# Run pre-processing script if any.
Anna Zaks5fa3f132011-11-02 20:46:50 +0000157def runCleanupScript(Dir, PBuildLogFile):
158 ScriptPath = os.path.join(Dir, CleanupScript)
Anna Zaks1b417162011-10-06 23:26:27 +0000159 if os.path.exists(ScriptPath):
160 try:
161 if Verbose == 1:
162 print " Executing: %s" % (ScriptPath,)
163 check_call("chmod +x %s" % ScriptPath, cwd = Dir,
164 stderr=PBuildLogFile,
165 stdout=PBuildLogFile,
166 shell=True)
167 check_call(ScriptPath, cwd = Dir, stderr=PBuildLogFile,
168 stdout=PBuildLogFile,
169 shell=True)
170 except:
171 print "Error: The pre-processing step failed. See ", \
172 PBuildLogFile.name, " for details."
173 sys.exit(-1)
174
175# Build the project with scan-build by reading in the commands and
176# prefixing them with the scan-build options.
177def runScanBuild(Dir, SBOutputDir, PBuildLogFile):
178 BuildScriptPath = os.path.join(Dir, BuildScript)
179 if not os.path.exists(BuildScriptPath):
180 print "Error: build script is not defined: %s" % BuildScriptPath
Ted Kremenek5abd3d22012-08-28 20:20:52 +0000181 sys.exit(-1)
182 SBOptions = "--use-analyzer " + Clang + " "
183 SBOptions += "-plist-html -o " + SBOutputDir + " "
Anna Zaks8d4a5152011-11-08 22:41:25 +0000184 SBOptions += "-enable-checker " + Checkers + " "
Anna Zaks1b417162011-10-06 23:26:27 +0000185 try:
186 SBCommandFile = open(BuildScriptPath, "r")
187 SBPrefix = "scan-build " + SBOptions + " "
188 for Command in SBCommandFile:
189 SBCommand = SBPrefix + Command
190 if Verbose == 1:
191 print " Executing: %s" % (SBCommand,)
192 check_call(SBCommand, cwd = Dir, stderr=PBuildLogFile,
193 stdout=PBuildLogFile,
194 shell=True)
195 except:
196 print "Error: scan-build failed. See ",PBuildLogFile.name,\
197 " for details."
Anna Zaks45518b12011-11-05 05:20:48 +0000198 raise
Anna Zaks1b417162011-10-06 23:26:27 +0000199
Anna Zaks45518b12011-11-05 05:20:48 +0000200def hasNoExtension(FileName):
201 (Root, Ext) = os.path.splitext(FileName)
202 if ((Ext == "")) :
203 return True
204 return False
205
206def isValidSingleInputFile(FileName):
207 (Root, Ext) = os.path.splitext(FileName)
208 if ((Ext == ".i") | (Ext == ".ii") |
209 (Ext == ".c") | (Ext == ".cpp") |
210 (Ext == ".m") | (Ext == "")) :
211 return True
212 return False
Ted Kremenek5abd3d22012-08-28 20:20:52 +0000213
Anna Zaks45518b12011-11-05 05:20:48 +0000214# Run analysis on a set of preprocessed files.
215def runAnalyzePreprocessed(Dir, SBOutputDir):
216 if os.path.exists(os.path.join(Dir, BuildScript)):
217 print "Error: The preprocessed files project should not contain %s" % \
218 BuildScript
219 raise Exception()
220
Ted Kremenek5abd3d22012-08-28 20:20:52 +0000221 CmdPrefix = Clang + " -cc1 -analyze -analyzer-output=plist -w "
Anna Zaks8c345c02012-01-21 01:11:35 +0000222 CmdPrefix += "-analyzer-checker=" + Checkers +" -fcxx-exceptions -fblocks "
Anna Zaks45518b12011-11-05 05:20:48 +0000223
224 PlistPath = os.path.join(Dir, SBOutputDir, "date")
225 FailPath = os.path.join(PlistPath, "failures");
226 os.makedirs(FailPath);
227
228 for FullFileName in glob.glob(Dir + "/*"):
229 FileName = os.path.basename(FullFileName)
230 Failed = False
231
232 # Only run the analyzes on supported files.
233 if (hasNoExtension(FileName)):
234 continue
235 if (isValidSingleInputFile(FileName) == False):
236 print "Error: Invalid single input file %s." % (FullFileName,)
237 raise Exception()
238
239 # Build and call the analyzer command.
240 OutputOption = "-o " + os.path.join(PlistPath, FileName) + ".plist "
241 Command = CmdPrefix + OutputOption + os.path.join(Dir, FileName)
242 LogFile = open(os.path.join(FailPath, FileName + ".stderr.txt"), "w+b")
243 try:
244 if Verbose == 1:
245 print " Executing: %s" % (Command,)
246 check_call(Command, cwd = Dir, stderr=LogFile,
247 stdout=LogFile,
248 shell=True)
249 except CalledProcessError, e:
250 print "Error: Analyzes of %s failed. See %s for details." \
251 "Error code %d." % \
252 (FullFileName, LogFile.name, e.returncode)
253 Failed = True
254 finally:
255 LogFile.close()
256
257 # If command did not fail, erase the log file.
258 if Failed == False:
259 os.remove(LogFile.name);
260
Jordan Rose6d7e3722012-06-01 16:24:38 +0000261def buildProject(Dir, SBOutputDir, IsScanBuild, IsReferenceBuild):
Anna Zaks1b417162011-10-06 23:26:27 +0000262 TBegin = time.time()
263
Anna Zaks45518b12011-11-05 05:20:48 +0000264 BuildLogPath = os.path.join(SBOutputDir, LogFolderName, BuildLogName)
Anna Zaks1b417162011-10-06 23:26:27 +0000265 print "Log file: %s" % (BuildLogPath,)
Anna Zaks45518b12011-11-05 05:20:48 +0000266 print "Output directory: %s" %(SBOutputDir, )
267
Anna Zaks1b417162011-10-06 23:26:27 +0000268 # Clean up the log file.
269 if (os.path.exists(BuildLogPath)) :
270 RmCommand = "rm " + BuildLogPath
271 if Verbose == 1:
Anna Zaks5fa3f132011-11-02 20:46:50 +0000272 print " Executing: %s" % (RmCommand,)
Anna Zaks1b417162011-10-06 23:26:27 +0000273 check_call(RmCommand, shell=True)
Anna Zaks45518b12011-11-05 05:20:48 +0000274
275 # Clean up scan build results.
276 if (os.path.exists(SBOutputDir)) :
277 RmCommand = "rm -r " + SBOutputDir
278 if Verbose == 1:
279 print " Executing: %s" % (RmCommand,)
280 check_call(RmCommand, shell=True)
281 assert(not os.path.exists(SBOutputDir))
282 os.makedirs(os.path.join(SBOutputDir, LogFolderName))
Anna Zaks1b417162011-10-06 23:26:27 +0000283
284 # Open the log file.
285 PBuildLogFile = open(BuildLogPath, "wb+")
Anna Zaks1b417162011-10-06 23:26:27 +0000286
Anna Zaks45518b12011-11-05 05:20:48 +0000287 # Build and analyze the project.
288 try:
Anna Zaks5fa3f132011-11-02 20:46:50 +0000289 runCleanupScript(Dir, PBuildLogFile)
Anna Zaks5fa3f132011-11-02 20:46:50 +0000290
Anna Zaks45518b12011-11-05 05:20:48 +0000291 if IsScanBuild:
292 runScanBuild(Dir, SBOutputDir, PBuildLogFile)
293 else:
294 runAnalyzePreprocessed(Dir, SBOutputDir)
295
296 if IsReferenceBuild :
Anna Zaks5fa3f132011-11-02 20:46:50 +0000297 runCleanupScript(Dir, PBuildLogFile)
298
Anna Zaks1b417162011-10-06 23:26:27 +0000299 finally:
300 PBuildLogFile.close()
301
302 print "Build complete (time: %.2f). See the log for more details: %s" % \
303 ((time.time()-TBegin), BuildLogPath)
304
305# A plist file is created for each call to the analyzer(each source file).
306# We are only interested on the once that have bug reports, so delete the rest.
307def CleanUpEmptyPlists(SBOutputDir):
308 for F in glob.glob(SBOutputDir + "/*/*.plist"):
309 P = os.path.join(SBOutputDir, F)
310
311 Data = plistlib.readPlist(P)
312 # Delete empty reports.
313 if not Data['files']:
314 os.remove(P)
315 continue
316
317# Given the scan-build output directory, checks if the build failed
318# (by searching for the failures directories). If there are failures, it
319# creates a summary file in the output directory.
320def checkBuild(SBOutputDir):
321 # Check if there are failures.
322 Failures = glob.glob(SBOutputDir + "/*/failures/*.stderr.txt")
323 TotalFailed = len(Failures);
324 if TotalFailed == 0:
325 CleanUpEmptyPlists(SBOutputDir)
326 Plists = glob.glob(SBOutputDir + "/*/*.plist")
327 print "Number of bug reports (non empty plist files) produced: %d" %\
328 len(Plists)
329 return;
330
331 # Create summary file to display when the build fails.
Anna Zaks45518b12011-11-05 05:20:48 +0000332 SummaryPath = os.path.join(SBOutputDir, LogFolderName, FailuresSummaryFileName)
Anna Zaks1b417162011-10-06 23:26:27 +0000333 if (Verbose > 0):
Anna Zaks45518b12011-11-05 05:20:48 +0000334 print " Creating the failures summary file %s" % (SummaryPath,)
Anna Zaks1b417162011-10-06 23:26:27 +0000335
336 SummaryLog = open(SummaryPath, "w+")
337 try:
338 SummaryLog.write("Total of %d failures discovered.\n" % (TotalFailed,))
339 if TotalFailed > NumOfFailuresInSummary:
340 SummaryLog.write("See the first %d below.\n"
341 % (NumOfFailuresInSummary,))
342 # TODO: Add a line "See the results folder for more."
343
344 FailuresCopied = NumOfFailuresInSummary
345 Idx = 0
Jordan Rose04bc0142012-06-01 16:24:43 +0000346 for FailLogPathI in Failures:
Anna Zaks1b417162011-10-06 23:26:27 +0000347 if Idx >= NumOfFailuresInSummary:
348 break;
349 Idx += 1
350 SummaryLog.write("\n-- Error #%d -----------\n" % (Idx,));
351 FailLogI = open(FailLogPathI, "r");
352 try:
353 shutil.copyfileobj(FailLogI, SummaryLog);
354 finally:
355 FailLogI.close()
356 finally:
357 SummaryLog.close()
358
Anna Zaksf063a3b2012-01-04 23:53:50 +0000359 print "Error: analysis failed. See ", SummaryPath
Anna Zaks1b417162011-10-06 23:26:27 +0000360 sys.exit(-1)
361
362# Auxiliary object to discard stdout.
363class Discarder(object):
364 def write(self, text):
365 pass # do nothing
366
367# Compare the warnings produced by scan-build.
368def runCmpResults(Dir):
369 TBegin = time.time()
370
371 RefDir = os.path.join(Dir, SBOutputDirReferencePrefix + SBOutputDirName)
372 NewDir = os.path.join(Dir, SBOutputDirName)
373
374 # We have to go one level down the directory tree.
375 RefList = glob.glob(RefDir + "/*")
376 NewList = glob.glob(NewDir + "/*")
Anna Zaks45518b12011-11-05 05:20:48 +0000377
378 # Log folders are also located in the results dir, so ignore them.
379 RefList.remove(os.path.join(RefDir, LogFolderName))
380 NewList.remove(os.path.join(NewDir, LogFolderName))
381
Anna Zaks1b417162011-10-06 23:26:27 +0000382 if len(RefList) == 0 or len(NewList) == 0:
383 return False
384 assert(len(RefList) == len(NewList))
385
386 # There might be more then one folder underneath - one per each scan-build
387 # command (Ex: one for configure and one for make).
388 if (len(RefList) > 1):
389 # Assume that the corresponding folders have the same names.
390 RefList.sort()
391 NewList.sort()
392
393 # Iterate and find the differences.
Anna Zaksa7a25642011-11-08 19:56:31 +0000394 NumDiffs = 0
Anna Zaks1b417162011-10-06 23:26:27 +0000395 PairList = zip(RefList, NewList)
396 for P in PairList:
397 RefDir = P[0]
398 NewDir = P[1]
399
400 assert(RefDir != NewDir)
401 if Verbose == 1:
402 print " Comparing Results: %s %s" % (RefDir, NewDir)
403
404 DiffsPath = os.path.join(NewDir, DiffsSummaryFileName)
405 Opts = CmpRuns.CmpOptions(DiffsPath)
406 # Discard everything coming out of stdout (CmpRun produces a lot of them).
407 OLD_STDOUT = sys.stdout
408 sys.stdout = Discarder()
409 # Scan the results, delete empty plist files.
Anna Zaks7acc4072012-07-16 20:21:42 +0000410 NumDiffs = CmpRuns.dumpScanBuildResultsDiff(RefDir, NewDir, Opts, False)
Anna Zaks1b417162011-10-06 23:26:27 +0000411 sys.stdout = OLD_STDOUT
Anna Zaksa7a25642011-11-08 19:56:31 +0000412 if (NumDiffs > 0) :
413 print "Warning: %r differences in diagnostics. See %s" % \
414 (NumDiffs, DiffsPath,)
Anna Zaks1b417162011-10-06 23:26:27 +0000415
416 print "Diagnostic comparison complete (time: %.2f)." % (time.time()-TBegin)
Anna Zaksa7a25642011-11-08 19:56:31 +0000417 return (NumDiffs > 0)
Anna Zaks45518b12011-11-05 05:20:48 +0000418
Anna Zaks09e9cf02012-02-03 06:35:23 +0000419def updateSVN(Mode, ProjectsMap):
420 try:
421 ProjectsMap.seek(0)
422 for I in csv.reader(ProjectsMap):
423 ProjName = I[0]
Jordan Rose6d7e3722012-06-01 16:24:38 +0000424 Path = os.path.join(ProjName, getSBOutputDirName(True))
Anna Zaks09e9cf02012-02-03 06:35:23 +0000425
426 if Mode == "delete":
427 Command = "svn delete %s" % (Path,)
428 else:
429 Command = "svn add %s" % (Path,)
Anna Zaks1b417162011-10-06 23:26:27 +0000430
Anna Zaks09e9cf02012-02-03 06:35:23 +0000431 if Verbose == 1:
432 print " Executing: %s" % (Command,)
Jordan Rose04bc0142012-06-01 16:24:43 +0000433 check_call(Command, shell=True)
Anna Zaks09e9cf02012-02-03 06:35:23 +0000434
435 if Mode == "delete":
436 CommitCommand = "svn commit -m \"[analyzer tests] Remove " \
437 "reference results.\""
438 else:
439 CommitCommand = "svn commit -m \"[analyzer tests] Add new " \
440 "reference results.\""
441 if Verbose == 1:
442 print " Executing: %s" % (CommitCommand,)
Jordan Rose04bc0142012-06-01 16:24:43 +0000443 check_call(CommitCommand, shell=True)
Anna Zaks09e9cf02012-02-03 06:35:23 +0000444 except:
445 print "Error: SVN update failed."
446 sys.exit(-1)
447
Jordan Rose6d7e3722012-06-01 16:24:38 +0000448def testProject(ID, IsScanBuild, IsReferenceBuild=False, Dir=None):
Anna Zaks45518b12011-11-05 05:20:48 +0000449 print " \n\n--- Building project %s" % (ID,)
450
Anna Zaks1b417162011-10-06 23:26:27 +0000451 TBegin = time.time()
452
453 if Dir is None :
454 Dir = getProjectDir(ID)
455 if Verbose == 1:
456 print " Build directory: %s." % (Dir,)
457
458 # Set the build results directory.
Jordan Rose6d7e3722012-06-01 16:24:38 +0000459 RelOutputDir = getSBOutputDirName(IsReferenceBuild)
Anna Zaks09e9cf02012-02-03 06:35:23 +0000460 SBOutputDir = os.path.join(Dir, RelOutputDir)
461
Jordan Rose6d7e3722012-06-01 16:24:38 +0000462 buildProject(Dir, SBOutputDir, IsScanBuild, IsReferenceBuild)
Anna Zaks1b417162011-10-06 23:26:27 +0000463
464 checkBuild(SBOutputDir)
465
466 if IsReferenceBuild == False:
467 runCmpResults(Dir)
468
469 print "Completed tests for project %s (time: %.2f)." % \
470 (ID, (time.time()-TBegin))
471
Jordan Rose6d7e3722012-06-01 16:24:38 +0000472def testAll(IsReferenceBuild = False, UpdateSVN = False):
Anna Zaks1b417162011-10-06 23:26:27 +0000473 PMapFile = open(getProjectMapPath(), "rb")
Anna Zaks09e9cf02012-02-03 06:35:23 +0000474 try:
475 # Validate the input.
476 for I in csv.reader(PMapFile):
Anna Zaks45518b12011-11-05 05:20:48 +0000477 if (len(I) != 2) :
478 print "Error: Rows in the ProjectMapFile should have 3 entries."
479 raise Exception()
480 if (not ((I[1] == "1") | (I[1] == "0"))):
481 print "Error: Second entry in the ProjectMapFile should be 0 or 1."
482 raise Exception()
Anna Zaks09e9cf02012-02-03 06:35:23 +0000483
484 # When we are regenerating the reference results, we might need to
485 # update svn. Remove reference results from SVN.
486 if UpdateSVN == True:
Jordan Rose6d7e3722012-06-01 16:24:38 +0000487 assert(IsReferenceBuild == True);
Anna Zaks09e9cf02012-02-03 06:35:23 +0000488 updateSVN("delete", PMapFile);
489
490 # Test the projects.
491 PMapFile.seek(0)
492 for I in csv.reader(PMapFile):
Jordan Rose6d7e3722012-06-01 16:24:38 +0000493 testProject(I[0], int(I[1]), IsReferenceBuild)
Anna Zaks09e9cf02012-02-03 06:35:23 +0000494
495 # Add reference results to SVN.
496 if UpdateSVN == True:
497 updateSVN("add", PMapFile);
498
Anna Zaks45518b12011-11-05 05:20:48 +0000499 except:
500 print "Error occurred. Premature termination."
501 raise
Anna Zaks1b417162011-10-06 23:26:27 +0000502 finally:
503 PMapFile.close()
504
505if __name__ == '__main__':
Anna Zaks09e9cf02012-02-03 06:35:23 +0000506 IsReference = False
507 UpdateSVN = False
508 if len(sys.argv) >= 2:
509 if sys.argv[1] == "-r":
510 IsReference = True
511 elif sys.argv[1] == "-rs":
512 IsReference = True
513 UpdateSVN = True
514 else:
515 print >> sys.stderr, 'Usage: ', sys.argv[0],\
516 '[-r|-rs]' \
517 'Use -r to regenerate reference output' \
518 'Use -rs to regenerate reference output and update svn'
519
520 testAll(IsReference, UpdateSVN)