blob: 3fab6ef520c42e05464947103268bd72aaf04538 [file] [log] [blame]
Daniel Dunbar1a9db992009-08-06 21:15:33 +00001#!/usr/bin/env python
2
3"""
4CmpRuns - A simple tool for comparing two static analyzer runs to determine
5which reports have been added, removed, or changed.
6
7This is designed to support automated testing using the static analyzer, from
Ted Kremenek3a0678e2015-09-08 03:50:52 +00008two perspectives:
George Karpenkova8076602017-10-02 17:59:12 +00009 1. To monitor changes in the static analyzer's reports on real code bases,
10 for regression testing.
Daniel Dunbar1a9db992009-08-06 21:15:33 +000011
12 2. For use by end users who want to integrate regular static analyzer testing
13 into a buildbot like environment.
Anna Zaks9b7d7142012-07-16 20:21:42 +000014
15Usage:
16
17 # Load the results of both runs, to obtain lists of the corresponding
18 # AnalysisDiagnostic objects.
Anna Zaks45a992b2012-08-02 00:41:40 +000019 #
Anna Zaksc80313b2012-10-15 22:48:21 +000020 resultsA = loadResultsFromSingleRun(singleRunInfoA, deleteEmpty)
21 resultsB = loadResultsFromSingleRun(singleRunInfoB, deleteEmpty)
Ted Kremenek3a0678e2015-09-08 03:50:52 +000022
23 # Generate a relation from diagnostics in run A to diagnostics in run B
24 # to obtain a list of triples (a, b, confidence).
Anna Zaks9b7d7142012-07-16 20:21:42 +000025 diff = compareResults(resultsA, resultsB)
Ted Kremenek3a0678e2015-09-08 03:50:52 +000026
Daniel Dunbar1a9db992009-08-06 21:15:33 +000027"""
Serge Guelton3744de52018-12-18 08:38:50 +000028from __future__ import division, print_function
Daniel Dunbar1a9db992009-08-06 21:15:33 +000029
Mikhail R. Gadelha8af2e692018-05-28 15:40:39 +000030from collections import defaultdict
31
George Karpenkovb7043222018-02-01 22:25:18 +000032from math import log
George Karpenkov39590412018-02-09 18:48:31 +000033from optparse import OptionParser
Mikhail R. Gadelha8af2e692018-05-28 15:40:39 +000034import json
35import os
36import plistlib
37import re
38import sys
Daniel Dunbar1a9db992009-08-06 21:15:33 +000039
Mikhail R. Gadelha8af2e692018-05-28 15:40:39 +000040STATS_REGEXP = re.compile(r"Statistics: (\{.+\})", re.MULTILINE | re.DOTALL)
41
Serge Guelton09616bd2018-12-03 12:12:48 +000042class Colors(object):
Mikhail R. Gadelha8af2e692018-05-28 15:40:39 +000043 """
44 Color for terminal highlight.
45 """
46 RED = '\x1b[2;30;41m'
47 GREEN = '\x1b[6;30;42m'
48 CLEAR = '\x1b[0m'
George Karpenkova8076602017-10-02 17:59:12 +000049
Anna Zaksc80313b2012-10-15 22:48:21 +000050# Information about analysis run:
51# path - the analysis output directory
Ted Kremenek3a0678e2015-09-08 03:50:52 +000052# root - the name of the root directory, which will be disregarded when
Anna Zaksc80313b2012-10-15 22:48:21 +000053# determining the source file name
Serge Guelton09616bd2018-12-03 12:12:48 +000054class SingleRunInfo(object):
Anna Zaksc80313b2012-10-15 22:48:21 +000055 def __init__(self, path, root="", verboseLog=None):
56 self.path = path
Gabor Horvathc3177f22015-07-08 18:39:31 +000057 self.root = root.rstrip("/\\")
Anna Zaksc80313b2012-10-15 22:48:21 +000058 self.verboseLog = verboseLog
59
George Karpenkova8076602017-10-02 17:59:12 +000060
Serge Guelton09616bd2018-12-03 12:12:48 +000061class AnalysisDiagnostic(object):
Anna Zaks9b7d7142012-07-16 20:21:42 +000062 def __init__(self, data, report, htmlReport):
63 self._data = data
64 self._loc = self._data['location']
65 self._report = report
66 self._htmlReport = htmlReport
George Karpenkovb7043222018-02-01 22:25:18 +000067 self._reportSize = len(self._data['path'])
Anna Zaks9b7d7142012-07-16 20:21:42 +000068
69 def getFileName(self):
Anna Zaksc80313b2012-10-15 22:48:21 +000070 root = self._report.run.root
Anna Zaks639b4042012-10-17 21:09:26 +000071 fileName = self._report.files[self._loc['file']]
Gabor Horvathc3177f22015-07-08 18:39:31 +000072 if fileName.startswith(root) and len(root) > 0:
George Karpenkova8076602017-10-02 17:59:12 +000073 return fileName[len(root) + 1:]
Anna Zaksc80313b2012-10-15 22:48:21 +000074 return fileName
75
George Karpenkova64b2052019-02-05 22:26:57 +000076 def getRootFileName(self):
77 path = self._data['path']
78 if not path:
79 return self.getFileName()
80 p = path[0]
81 if 'location' in p:
82 fIdx = p['location']['file']
83 else: # control edge
84 fIdx = path[0]['edges'][0]['start'][0]['file']
85 out = self._report.files[fIdx]
86 root = self._report.run.root
87 if out.startswith(root):
88 return out[len(root):]
89 return out
90
Anna Zaks9b7d7142012-07-16 20:21:42 +000091 def getLine(self):
92 return self._loc['line']
Ted Kremenek3a0678e2015-09-08 03:50:52 +000093
Anna Zaks9b7d7142012-07-16 20:21:42 +000094 def getColumn(self):
95 return self._loc['col']
96
George Karpenkovb7043222018-02-01 22:25:18 +000097 def getPathLength(self):
98 return self._reportSize
99
Anna Zaks9b7d7142012-07-16 20:21:42 +0000100 def getCategory(self):
101 return self._data['category']
102
103 def getDescription(self):
104 return self._data['description']
105
George Karpenkova8076602017-10-02 17:59:12 +0000106 def getIssueIdentifier(self):
Anna Zaksc80313b2012-10-15 22:48:21 +0000107 id = self.getFileName() + "+"
George Karpenkova8076602017-10-02 17:59:12 +0000108 if 'issue_context' in self._data:
109 id += self._data['issue_context'] + "+"
110 if 'issue_hash_content_of_line_in_context' in self._data:
111 id += str(self._data['issue_hash_content_of_line_in_context'])
Anna Zaksc80313b2012-10-15 22:48:21 +0000112 return id
Anna Zaks9b7d7142012-07-16 20:21:42 +0000113
114 def getReport(self):
115 if self._htmlReport is None:
116 return " "
117 return os.path.join(self._report.run.path, self._htmlReport)
118
119 def getReadableName(self):
George Karpenkov986dd452018-02-06 17:22:09 +0000120 if 'issue_context' in self._data:
121 funcnamePostfix = "#" + self._data['issue_context']
122 else:
123 funcnamePostfix = ""
George Karpenkova64b2052019-02-05 22:26:57 +0000124 rootFilename = self.getRootFileName()
125 fileName = self.getFileName()
126 if rootFilename != fileName:
127 filePrefix = "[%s] %s" % (rootFilename, fileName)
128 else:
129 filePrefix = rootFilename
130 return '%s%s:%d:%d, %s: %s' % (filePrefix,
George Karpenkov986dd452018-02-06 17:22:09 +0000131 funcnamePostfix,
132 self.getLine(),
133 self.getColumn(), self.getCategory(),
134 self.getDescription())
Ted Kremenek3a0678e2015-09-08 03:50:52 +0000135
136 # Note, the data format is not an API and may change from one analyzer
137 # version to another.
Anna Zaks639b4042012-10-17 21:09:26 +0000138 def getRawData(self):
139 return self._data
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000140
George Karpenkova8076602017-10-02 17:59:12 +0000141
Serge Guelton09616bd2018-12-03 12:12:48 +0000142class AnalysisReport(object):
Anna Zaksfab9bb62012-11-15 22:42:44 +0000143 def __init__(self, run, files):
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000144 self.run = run
Anna Zaks639b4042012-10-17 21:09:26 +0000145 self.files = files
146 self.diagnostics = []
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000147
George Karpenkova8076602017-10-02 17:59:12 +0000148
Serge Guelton09616bd2018-12-03 12:12:48 +0000149class AnalysisRun(object):
Anna Zaksc80313b2012-10-15 22:48:21 +0000150 def __init__(self, info):
151 self.path = info.path
152 self.root = info.root
153 self.info = info
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000154 self.reports = []
Anna Zaks639b4042012-10-17 21:09:26 +0000155 # Cumulative list of all diagnostics from all the reports.
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000156 self.diagnostics = []
Anna Zaksfab9bb62012-11-15 22:42:44 +0000157 self.clang_version = None
Mikhail R. Gadelha8af2e692018-05-28 15:40:39 +0000158 self.stats = []
Ted Kremenek3a0678e2015-09-08 03:50:52 +0000159
Anna Zaksfab9bb62012-11-15 22:42:44 +0000160 def getClangVersion(self):
161 return self.clang_version
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000162
Jordan Roseb042cc72013-03-23 01:21:26 +0000163 def readSingleFile(self, p, deleteEmpty):
164 data = plistlib.readPlist(p)
Mikhail R. Gadelha8af2e692018-05-28 15:40:39 +0000165 if 'statistics' in data:
166 self.stats.append(json.loads(data['statistics']))
167 data.pop('statistics')
Jordan Roseb042cc72013-03-23 01:21:26 +0000168
Ted Kremenek3a0678e2015-09-08 03:50:52 +0000169 # We want to retrieve the clang version even if there are no
170 # reports. Assume that all reports were created using the same
Jordan Roseb042cc72013-03-23 01:21:26 +0000171 # clang version (this is always true and is more efficient).
172 if 'clang_version' in data:
George Karpenkova8076602017-10-02 17:59:12 +0000173 if self.clang_version is None:
Jordan Roseb042cc72013-03-23 01:21:26 +0000174 self.clang_version = data.pop('clang_version')
175 else:
176 data.pop('clang_version')
177
178 # Ignore/delete empty reports.
179 if not data['files']:
George Karpenkova8076602017-10-02 17:59:12 +0000180 if deleteEmpty:
Jordan Roseb042cc72013-03-23 01:21:26 +0000181 os.remove(p)
182 return
183
184 # Extract the HTML reports, if they exists.
185 if 'HTMLDiagnostics_files' in data['diagnostics'][0]:
186 htmlFiles = []
187 for d in data['diagnostics']:
188 # FIXME: Why is this named files, when does it have multiple
189 # files?
190 assert len(d['HTMLDiagnostics_files']) == 1
191 htmlFiles.append(d.pop('HTMLDiagnostics_files')[0])
192 else:
193 htmlFiles = [None] * len(data['diagnostics'])
Ted Kremenek3a0678e2015-09-08 03:50:52 +0000194
Jordan Roseb042cc72013-03-23 01:21:26 +0000195 report = AnalysisReport(self, data.pop('files'))
Ted Kremenek3a0678e2015-09-08 03:50:52 +0000196 diagnostics = [AnalysisDiagnostic(d, report, h)
George Karpenkova8076602017-10-02 17:59:12 +0000197 for d, h in zip(data.pop('diagnostics'), htmlFiles)]
Jordan Roseb042cc72013-03-23 01:21:26 +0000198
199 assert not data
200
201 report.diagnostics.extend(diagnostics)
202 self.reports.append(report)
203 self.diagnostics.extend(diagnostics)
204
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000205
George Karpenkova8076602017-10-02 17:59:12 +0000206def loadResults(path, opts, root="", deleteEmpty=True):
207 """
208 Backwards compatibility API.
209 """
Anna Zaksc80313b2012-10-15 22:48:21 +0000210 return loadResultsFromSingleRun(SingleRunInfo(path, root, opts.verboseLog),
211 deleteEmpty)
212
George Karpenkova8076602017-10-02 17:59:12 +0000213
Anna Zaksc80313b2012-10-15 22:48:21 +0000214def loadResultsFromSingleRun(info, deleteEmpty=True):
George Karpenkova8076602017-10-02 17:59:12 +0000215 """
216 # Load results of the analyzes from a given output folder.
217 # - info is the SingleRunInfo object
218 # - deleteEmpty specifies if the empty plist files should be deleted
219
220 """
Anna Zaksc80313b2012-10-15 22:48:21 +0000221 path = info.path
222 run = AnalysisRun(info)
Jordan Roseb042cc72013-03-23 01:21:26 +0000223
224 if os.path.isfile(path):
225 run.readSingleFile(path, deleteEmpty)
226 else:
227 for (dirpath, dirnames, filenames) in os.walk(path):
228 for f in filenames:
229 if (not f.endswith('plist')):
230 continue
231 p = os.path.join(dirpath, f)
232 run.readSingleFile(p, deleteEmpty)
233
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000234 return run
235
George Karpenkova8076602017-10-02 17:59:12 +0000236
237def cmpAnalysisDiagnostic(d):
Anna Zaks9b7d7142012-07-16 20:21:42 +0000238 return d.getIssueIdentifier()
Anna Zaksd60367b2012-06-08 01:50:49 +0000239
George Karpenkova8076602017-10-02 17:59:12 +0000240
George Karpenkovb7043222018-02-01 22:25:18 +0000241def compareResults(A, B, opts):
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000242 """
243 compareResults - Generate a relation from diagnostics in run A to
244 diagnostics in run B.
245
George Karpenkovf37c07c2018-02-01 22:40:01 +0000246 The result is the relation as a list of triples (a, b) where
247 each element {a,b} is None or a matching element from the respective run
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000248 """
249
250 res = []
251
George Karpenkovb7043222018-02-01 22:25:18 +0000252 # Map size_before -> size_after
253 path_difference_data = []
254
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000255 # Quickly eliminate equal elements.
256 neqA = []
257 neqB = []
258 eltsA = list(A.diagnostics)
259 eltsB = list(B.diagnostics)
George Karpenkova8076602017-10-02 17:59:12 +0000260 eltsA.sort(key=cmpAnalysisDiagnostic)
261 eltsB.sort(key=cmpAnalysisDiagnostic)
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000262 while eltsA and eltsB:
263 a = eltsA.pop()
264 b = eltsB.pop()
George Karpenkova8076602017-10-02 17:59:12 +0000265 if (a.getIssueIdentifier() == b.getIssueIdentifier()):
George Karpenkovb7043222018-02-01 22:25:18 +0000266 if a.getPathLength() != b.getPathLength():
267 if opts.relative_path_histogram:
268 path_difference_data.append(
269 float(a.getPathLength()) / b.getPathLength())
270 elif opts.relative_log_path_histogram:
271 path_difference_data.append(
272 log(float(a.getPathLength()) / b.getPathLength()))
273 elif opts.absolute_path_histogram:
274 path_difference_data.append(
275 a.getPathLength() - b.getPathLength())
276
George Karpenkovf37c07c2018-02-01 22:40:01 +0000277 res.append((a, b))
Anna Zaks639b4042012-10-17 21:09:26 +0000278 elif a.getIssueIdentifier() > b.getIssueIdentifier():
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000279 eltsB.append(b)
Anna Zaks639b4042012-10-17 21:09:26 +0000280 neqA.append(a)
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000281 else:
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000282 eltsA.append(a)
Anna Zaks639b4042012-10-17 21:09:26 +0000283 neqB.append(b)
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000284 neqA.extend(eltsA)
285 neqB.extend(eltsB)
286
George Karpenkova8076602017-10-02 17:59:12 +0000287 # FIXME: Add fuzzy matching. One simple and possible effective idea would
288 # be to bin the diagnostics, print them in a normalized form (based solely
289 # on the structure of the diagnostic), compute the diff, then use that as
290 # the basis for matching. This has the nice property that we don't depend
291 # in any way on the diagnostic format.
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000292
293 for a in neqA:
George Karpenkovf37c07c2018-02-01 22:40:01 +0000294 res.append((a, None))
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000295 for b in neqB:
George Karpenkovf37c07c2018-02-01 22:40:01 +0000296 res.append((None, b))
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000297
George Karpenkovb7043222018-02-01 22:25:18 +0000298 if opts.relative_log_path_histogram or opts.relative_path_histogram or \
299 opts.absolute_path_histogram:
300 from matplotlib import pyplot
301 pyplot.hist(path_difference_data, bins=100)
302 pyplot.show()
303
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000304 return res
305
George Karpenkov6a2a1972018-10-23 01:30:26 +0000306def computePercentile(l, percentile):
307 """
308 Return computed percentile.
309 """
310 return sorted(l)[int(round(percentile * len(l) + 0.5)) - 1]
311
Mikhail R. Gadelha8af2e692018-05-28 15:40:39 +0000312def deriveStats(results):
313 # Assume all keys are the same in each statistics bucket.
314 combined_data = defaultdict(list)
George Karpenkov6a2a1972018-10-23 01:30:26 +0000315
316 # Collect data on paths length.
317 for report in results.reports:
318 for diagnostic in report.diagnostics:
319 combined_data['PathsLength'].append(diagnostic.getPathLength())
320
Mikhail R. Gadelha8af2e692018-05-28 15:40:39 +0000321 for stat in results.stats:
Serge Gueltond4589742018-12-18 16:04:21 +0000322 for key, value in stat.items():
Mikhail R. Gadelha8af2e692018-05-28 15:40:39 +0000323 combined_data[key].append(value)
324 combined_stats = {}
Serge Gueltond4589742018-12-18 16:04:21 +0000325 for key, values in combined_data.items():
Mikhail R. Gadelha8af2e692018-05-28 15:40:39 +0000326 combined_stats[str(key)] = {
327 "max": max(values),
328 "min": min(values),
329 "mean": sum(values) / len(values),
George Karpenkov6a2a1972018-10-23 01:30:26 +0000330 "90th %tile": computePercentile(values, 0.9),
331 "95th %tile": computePercentile(values, 0.95),
Serge Guelton3744de52018-12-18 08:38:50 +0000332 "median": sorted(values)[len(values) // 2],
Mikhail R. Gadelha8af2e692018-05-28 15:40:39 +0000333 "total": sum(values)
334 }
335 return combined_stats
336
337
338def compareStats(resultsA, resultsB):
339 statsA = deriveStats(resultsA)
340 statsB = deriveStats(resultsB)
341 keys = sorted(statsA.keys())
342 for key in keys:
Serge Gueltonc0ebe772018-12-18 08:36:33 +0000343 print(key)
Mikhail R. Gadelha8af2e692018-05-28 15:40:39 +0000344 for kkey in statsA[key]:
345 valA = float(statsA[key][kkey])
346 valB = float(statsB[key][kkey])
347 report = "%.3f -> %.3f" % (valA, valB)
348 # Only apply highlighting when writing to TTY and it's not Windows
349 if sys.stdout.isatty() and os.name != 'nt':
Mikhail R. Gadelha690a99a2018-05-30 11:17:55 +0000350 if valB != 0:
George Karpenkov13d37482018-07-30 23:01:20 +0000351 ratio = (valB - valA) / valB
352 if ratio < -0.2:
353 report = Colors.GREEN + report + Colors.CLEAR
354 elif ratio > 0.2:
355 report = Colors.RED + report + Colors.CLEAR
Serge Gueltonc0ebe772018-12-18 08:36:33 +0000356 print("\t %s %s" % (kkey, report))
George Karpenkova8076602017-10-02 17:59:12 +0000357
George Karpenkovb7120c92018-02-13 23:36:01 +0000358def dumpScanBuildResultsDiff(dirA, dirB, opts, deleteEmpty=True,
359 Stdout=sys.stdout):
Anna Zaksb80d8362011-09-12 21:32:41 +0000360 # Load the run results.
Anna Zaks45a992b2012-08-02 00:41:40 +0000361 resultsA = loadResults(dirA, opts, opts.rootA, deleteEmpty)
362 resultsB = loadResults(dirB, opts, opts.rootB, deleteEmpty)
George Karpenkov8f6d65c2018-07-30 23:01:47 +0000363 if opts.show_stats:
Mikhail R. Gadelha8af2e692018-05-28 15:40:39 +0000364 compareStats(resultsA, resultsB)
365 if opts.stats_only:
366 return
Ted Kremenek3a0678e2015-09-08 03:50:52 +0000367
Anna Zaksb80d8362011-09-12 21:32:41 +0000368 # Open the verbose log, if given.
369 if opts.verboseLog:
370 auxLog = open(opts.verboseLog, "wb")
371 else:
372 auxLog = None
373
George Karpenkovb7043222018-02-01 22:25:18 +0000374 diff = compareResults(resultsA, resultsB, opts)
Anna Zaks767d3562011-11-08 19:56:31 +0000375 foundDiffs = 0
George Karpenkovdece62a2018-02-01 02:38:42 +0000376 totalAdded = 0
377 totalRemoved = 0
Anna Zaksb80d8362011-09-12 21:32:41 +0000378 for res in diff:
George Karpenkovf37c07c2018-02-01 22:40:01 +0000379 a, b = res
Anna Zaksb80d8362011-09-12 21:32:41 +0000380 if a is None:
George Karpenkovb7120c92018-02-13 23:36:01 +0000381 Stdout.write("ADDED: %r\n" % b.getReadableName())
Anna Zaks767d3562011-11-08 19:56:31 +0000382 foundDiffs += 1
George Karpenkovdece62a2018-02-01 02:38:42 +0000383 totalAdded += 1
Anna Zaksb80d8362011-09-12 21:32:41 +0000384 if auxLog:
George Karpenkovb7120c92018-02-13 23:36:01 +0000385 auxLog.write("('ADDED', %r, %r)\n" % (b.getReadableName(),
386 b.getReport()))
Anna Zaksb80d8362011-09-12 21:32:41 +0000387 elif b is None:
George Karpenkovb7120c92018-02-13 23:36:01 +0000388 Stdout.write("REMOVED: %r\n" % a.getReadableName())
Anna Zaks767d3562011-11-08 19:56:31 +0000389 foundDiffs += 1
George Karpenkovdece62a2018-02-01 02:38:42 +0000390 totalRemoved += 1
Anna Zaksb80d8362011-09-12 21:32:41 +0000391 if auxLog:
George Karpenkovb7120c92018-02-13 23:36:01 +0000392 auxLog.write("('REMOVED', %r, %r)\n" % (a.getReadableName(),
393 a.getReport()))
Anna Zaksb80d8362011-09-12 21:32:41 +0000394 else:
395 pass
396
Anna Zaks767d3562011-11-08 19:56:31 +0000397 TotalReports = len(resultsB.diagnostics)
George Karpenkovb7120c92018-02-13 23:36:01 +0000398 Stdout.write("TOTAL REPORTS: %r\n" % TotalReports)
399 Stdout.write("TOTAL ADDED: %r\n" % totalAdded)
400 Stdout.write("TOTAL REMOVED: %r\n" % totalRemoved)
Anna Zaksb80d8362011-09-12 21:32:41 +0000401 if auxLog:
George Karpenkovb7120c92018-02-13 23:36:01 +0000402 auxLog.write("('TOTAL NEW REPORTS', %r)\n" % TotalReports)
403 auxLog.write("('TOTAL DIFFERENCES', %r)\n" % foundDiffs)
404 auxLog.close()
Ted Kremenek3a0678e2015-09-08 03:50:52 +0000405
Gabor Horvath93fde942015-06-30 15:31:17 +0000406 return foundDiffs, len(resultsA.diagnostics), len(resultsB.diagnostics)
Anna Zaksb80d8362011-09-12 21:32:41 +0000407
George Karpenkovfc782a32018-02-09 18:39:47 +0000408def generate_option_parser():
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000409 parser = OptionParser("usage: %prog [options] [dir A] [dir B]")
Anna Zaks45a992b2012-08-02 00:41:40 +0000410 parser.add_option("", "--rootA", dest="rootA",
411 help="Prefix to ignore on source files for directory A",
412 action="store", type=str, default="")
413 parser.add_option("", "--rootB", dest="rootB",
414 help="Prefix to ignore on source files for directory B",
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000415 action="store", type=str, default="")
416 parser.add_option("", "--verbose-log", dest="verboseLog",
George Karpenkova8076602017-10-02 17:59:12 +0000417 help="Write additional information to LOG \
George Karpenkovfc782a32018-02-09 18:39:47 +0000418 [default=None]",
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000419 action="store", type=str, default=None,
420 metavar="LOG")
George Karpenkovb7043222018-02-01 22:25:18 +0000421 parser.add_option("--relative-path-differences-histogram",
422 action="store_true", dest="relative_path_histogram",
423 default=False,
424 help="Show histogram of relative paths differences. \
George Karpenkovfc782a32018-02-09 18:39:47 +0000425 Requires matplotlib")
George Karpenkovb7043222018-02-01 22:25:18 +0000426 parser.add_option("--relative-log-path-differences-histogram",
427 action="store_true", dest="relative_log_path_histogram",
428 default=False,
429 help="Show histogram of log relative paths differences. \
George Karpenkovfc782a32018-02-09 18:39:47 +0000430 Requires matplotlib")
George Karpenkovb7043222018-02-01 22:25:18 +0000431 parser.add_option("--absolute-path-differences-histogram",
432 action="store_true", dest="absolute_path_histogram",
433 default=False,
434 help="Show histogram of absolute paths differences. \
George Karpenkovfc782a32018-02-09 18:39:47 +0000435 Requires matplotlib")
Mikhail R. Gadelha8af2e692018-05-28 15:40:39 +0000436 parser.add_option("--stats-only", action="store_true", dest="stats_only",
437 default=False, help="Only show statistics on reports")
George Karpenkov8f6d65c2018-07-30 23:01:47 +0000438 parser.add_option("--show-stats", action="store_true", dest="show_stats",
439 default=False, help="Show change in statistics")
George Karpenkovfc782a32018-02-09 18:39:47 +0000440 return parser
441
442
443def main():
444 parser = generate_option_parser()
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000445 (opts, args) = parser.parse_args()
446
447 if len(args) != 2:
448 parser.error("invalid number of arguments")
449
George Karpenkova8076602017-10-02 17:59:12 +0000450 dirA, dirB = args
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000451
Ted Kremenek3a0678e2015-09-08 03:50:52 +0000452 dumpScanBuildResultsDiff(dirA, dirB, opts)
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000453
George Karpenkova8076602017-10-02 17:59:12 +0000454
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000455if __name__ == '__main__':
456 main()