blob: c2209cc93cfc9b232b4822bc6466bb83a8a0112d [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.
32 The compiler for scan-build is in the PATH.
33 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
48from subprocess import check_call
49
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.
55PreprocessScript = "pre_run_static_analyzer.sh"
56# This is a file containing commands for scan-build.
57BuildScript = "run_static_analyzer.cmd"
58
59# The log file name.
60BuildLogName = "run_static_analyzer.log"
61# Summary file - contains the summary of the failures. Ex: This info can be be
62# displayed when buildbot detects a build failure.
63NumOfFailuresInSummary = 10
64FailuresSummaryFileName = "failures.txt"
65# Summary of the result diffs.
66DiffsSummaryFileName = "diffs.txt"
67
68# The scan-build result directory.
69SBOutputDirName = "ScanBuildResults"
70SBOutputDirReferencePrefix = "Ref"
71
72Verbose = 1
73
74def getProjectMapPath():
75 ProjectMapPath = os.path.join(os.path.abspath(os.curdir),
76 ProjectMapFile)
77 if not os.path.exists(ProjectMapPath):
78 print "Error: Cannot find the Project Map file " + ProjectMapPath +\
79 "\nRunning script for the wrong directory?"
80 sys.exit(-1)
81 return ProjectMapPath
82
83def getProjectDir(ID):
84 return os.path.join(os.path.abspath(os.curdir), ID)
85
86# Run pre-processing script if any.
87def runPreProcessingScript(Dir, PBuildLogFile):
88 ScriptPath = os.path.join(Dir, PreprocessScript)
89 if os.path.exists(ScriptPath):
90 try:
91 if Verbose == 1:
92 print " Executing: %s" % (ScriptPath,)
93 check_call("chmod +x %s" % ScriptPath, cwd = Dir,
94 stderr=PBuildLogFile,
95 stdout=PBuildLogFile,
96 shell=True)
97 check_call(ScriptPath, cwd = Dir, stderr=PBuildLogFile,
98 stdout=PBuildLogFile,
99 shell=True)
100 except:
101 print "Error: The pre-processing step failed. See ", \
102 PBuildLogFile.name, " for details."
103 sys.exit(-1)
104
105# Build the project with scan-build by reading in the commands and
106# prefixing them with the scan-build options.
107def runScanBuild(Dir, SBOutputDir, PBuildLogFile):
108 BuildScriptPath = os.path.join(Dir, BuildScript)
109 if not os.path.exists(BuildScriptPath):
110 print "Error: build script is not defined: %s" % BuildScriptPath
111 sys.exit(-1)
112 SBOptions = "-plist -o " + SBOutputDir + " "
113 SBOptions += "-enable-checker core,deadcode.DeadStores"
114 try:
115 SBCommandFile = open(BuildScriptPath, "r")
116 SBPrefix = "scan-build " + SBOptions + " "
117 for Command in SBCommandFile:
118 SBCommand = SBPrefix + Command
119 if Verbose == 1:
120 print " Executing: %s" % (SBCommand,)
121 check_call(SBCommand, cwd = Dir, stderr=PBuildLogFile,
122 stdout=PBuildLogFile,
123 shell=True)
124 except:
125 print "Error: scan-build failed. See ",PBuildLogFile.name,\
126 " for details."
127 sys.exit(-1)
128
129def buildProject(Dir, SBOutputDir):
130 TBegin = time.time()
131
132 BuildLogPath = os.path.join(Dir, BuildLogName)
133 print "Log file: %s" % (BuildLogPath,)
134
135 # Clean up the log file.
136 if (os.path.exists(BuildLogPath)) :
137 RmCommand = "rm " + BuildLogPath
138 if Verbose == 1:
139 print " Executing: %s." % (RmCommand,)
140 check_call(RmCommand, shell=True)
141
142 # Open the log file.
143 PBuildLogFile = open(BuildLogPath, "wb+")
144 try:
145 # Clean up scan build results.
146 if (os.path.exists(SBOutputDir)) :
147 RmCommand = "rm -r " + SBOutputDir
148 if Verbose == 1:
149 print " Executing: %s" % (RmCommand,)
150 check_call(RmCommand, stderr=PBuildLogFile,
151 stdout=PBuildLogFile, shell=True)
152
153 runPreProcessingScript(Dir, PBuildLogFile)
154 runScanBuild(Dir, SBOutputDir, PBuildLogFile)
155 finally:
156 PBuildLogFile.close()
157
158 print "Build complete (time: %.2f). See the log for more details: %s" % \
159 ((time.time()-TBegin), BuildLogPath)
160
161# A plist file is created for each call to the analyzer(each source file).
162# We are only interested on the once that have bug reports, so delete the rest.
163def CleanUpEmptyPlists(SBOutputDir):
164 for F in glob.glob(SBOutputDir + "/*/*.plist"):
165 P = os.path.join(SBOutputDir, F)
166
167 Data = plistlib.readPlist(P)
168 # Delete empty reports.
169 if not Data['files']:
170 os.remove(P)
171 continue
172
173# Given the scan-build output directory, checks if the build failed
174# (by searching for the failures directories). If there are failures, it
175# creates a summary file in the output directory.
176def checkBuild(SBOutputDir):
177 # Check if there are failures.
178 Failures = glob.glob(SBOutputDir + "/*/failures/*.stderr.txt")
179 TotalFailed = len(Failures);
180 if TotalFailed == 0:
181 CleanUpEmptyPlists(SBOutputDir)
182 Plists = glob.glob(SBOutputDir + "/*/*.plist")
183 print "Number of bug reports (non empty plist files) produced: %d" %\
184 len(Plists)
185 return;
186
187 # Create summary file to display when the build fails.
188 SummaryPath = os.path.join(SBOutputDir, FailuresSummaryFileName);
189 if (Verbose > 0):
190 print " Creating the failures summary file %s." % (SummaryPath,)
191
192 SummaryLog = open(SummaryPath, "w+")
193 try:
194 SummaryLog.write("Total of %d failures discovered.\n" % (TotalFailed,))
195 if TotalFailed > NumOfFailuresInSummary:
196 SummaryLog.write("See the first %d below.\n"
197 % (NumOfFailuresInSummary,))
198 # TODO: Add a line "See the results folder for more."
199
200 FailuresCopied = NumOfFailuresInSummary
201 Idx = 0
202 for FailLogPathI in glob.glob(SBOutputDir + "/*/failures/*.stderr.txt"):
203 if Idx >= NumOfFailuresInSummary:
204 break;
205 Idx += 1
206 SummaryLog.write("\n-- Error #%d -----------\n" % (Idx,));
207 FailLogI = open(FailLogPathI, "r");
208 try:
209 shutil.copyfileobj(FailLogI, SummaryLog);
210 finally:
211 FailLogI.close()
212 finally:
213 SummaryLog.close()
214
215 print "Error: Scan-build failed. See ", \
216 os.path.join(SBOutputDir, FailuresSummaryFileName)
217 sys.exit(-1)
218
219# Auxiliary object to discard stdout.
220class Discarder(object):
221 def write(self, text):
222 pass # do nothing
223
224# Compare the warnings produced by scan-build.
225def runCmpResults(Dir):
226 TBegin = time.time()
227
228 RefDir = os.path.join(Dir, SBOutputDirReferencePrefix + SBOutputDirName)
229 NewDir = os.path.join(Dir, SBOutputDirName)
230
231 # We have to go one level down the directory tree.
232 RefList = glob.glob(RefDir + "/*")
233 NewList = glob.glob(NewDir + "/*")
234 if len(RefList) == 0 or len(NewList) == 0:
235 return False
236 assert(len(RefList) == len(NewList))
237
238 # There might be more then one folder underneath - one per each scan-build
239 # command (Ex: one for configure and one for make).
240 if (len(RefList) > 1):
241 # Assume that the corresponding folders have the same names.
242 RefList.sort()
243 NewList.sort()
244
245 # Iterate and find the differences.
246 HaveDiffs = False
247 PairList = zip(RefList, NewList)
248 for P in PairList:
249 RefDir = P[0]
250 NewDir = P[1]
251
252 assert(RefDir != NewDir)
253 if Verbose == 1:
254 print " Comparing Results: %s %s" % (RefDir, NewDir)
255
256 DiffsPath = os.path.join(NewDir, DiffsSummaryFileName)
257 Opts = CmpRuns.CmpOptions(DiffsPath)
258 # Discard everything coming out of stdout (CmpRun produces a lot of them).
259 OLD_STDOUT = sys.stdout
260 sys.stdout = Discarder()
261 # Scan the results, delete empty plist files.
262 HaveDiffs = CmpRuns.cmpScanBuildResults(RefDir, NewDir, Opts, False)
263 sys.stdout = OLD_STDOUT
264 if HaveDiffs:
265 print "Warning: difference in diagnostics. See %s" % (DiffsPath,)
266 HaveDiffs=True
267
268 print "Diagnostic comparison complete (time: %.2f)." % (time.time()-TBegin)
269 return HaveDiffs
270
271def testProject(ID, IsReferenceBuild, Dir=None):
272 TBegin = time.time()
273
274 if Dir is None :
275 Dir = getProjectDir(ID)
276 if Verbose == 1:
277 print " Build directory: %s." % (Dir,)
278
279 # Set the build results directory.
280 if IsReferenceBuild == True :
281 SBOutputDir = os.path.join(Dir, SBOutputDirReferencePrefix + \
282 SBOutputDirName)
283 else :
284 SBOutputDir = os.path.join(Dir, SBOutputDirName)
285
286 buildProject(Dir, SBOutputDir)
287
288 checkBuild(SBOutputDir)
289
290 if IsReferenceBuild == False:
291 runCmpResults(Dir)
292
293 print "Completed tests for project %s (time: %.2f)." % \
294 (ID, (time.time()-TBegin))
295
296def testAll(IsReferenceBuild=False):
297 PMapFile = open(getProjectMapPath(), "rb")
298 try:
299 PMapReader = csv.reader(PMapFile)
300 for I in PMapReader:
301 print " --- Building project %s" % (I[0],)
302 testProject(I[0], IsReferenceBuild)
303 finally:
304 PMapFile.close()
305
306if __name__ == '__main__':
307 testAll()