blob: be503499622d18ebbe4e684cf43fc030302c4d53 [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
Anna Zaks9b7d7142012-07-16 20:21:42 +000076 def getLine(self):
77 return self._loc['line']
Ted Kremenek3a0678e2015-09-08 03:50:52 +000078
Anna Zaks9b7d7142012-07-16 20:21:42 +000079 def getColumn(self):
80 return self._loc['col']
81
George Karpenkovb7043222018-02-01 22:25:18 +000082 def getPathLength(self):
83 return self._reportSize
84
Anna Zaks9b7d7142012-07-16 20:21:42 +000085 def getCategory(self):
86 return self._data['category']
87
88 def getDescription(self):
89 return self._data['description']
90
George Karpenkova8076602017-10-02 17:59:12 +000091 def getIssueIdentifier(self):
Anna Zaksc80313b2012-10-15 22:48:21 +000092 id = self.getFileName() + "+"
George Karpenkova8076602017-10-02 17:59:12 +000093 if 'issue_context' in self._data:
94 id += self._data['issue_context'] + "+"
95 if 'issue_hash_content_of_line_in_context' in self._data:
96 id += str(self._data['issue_hash_content_of_line_in_context'])
Anna Zaksc80313b2012-10-15 22:48:21 +000097 return id
Anna Zaks9b7d7142012-07-16 20:21:42 +000098
99 def getReport(self):
100 if self._htmlReport is None:
101 return " "
102 return os.path.join(self._report.run.path, self._htmlReport)
103
104 def getReadableName(self):
George Karpenkov986dd452018-02-06 17:22:09 +0000105 if 'issue_context' in self._data:
106 funcnamePostfix = "#" + self._data['issue_context']
107 else:
108 funcnamePostfix = ""
109 return '%s%s:%d:%d, %s: %s' % (self.getFileName(),
110 funcnamePostfix,
111 self.getLine(),
112 self.getColumn(), self.getCategory(),
113 self.getDescription())
Ted Kremenek3a0678e2015-09-08 03:50:52 +0000114
115 # Note, the data format is not an API and may change from one analyzer
116 # version to another.
Anna Zaks639b4042012-10-17 21:09:26 +0000117 def getRawData(self):
118 return self._data
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000119
George Karpenkova8076602017-10-02 17:59:12 +0000120
Serge Guelton09616bd2018-12-03 12:12:48 +0000121class AnalysisReport(object):
Anna Zaksfab9bb62012-11-15 22:42:44 +0000122 def __init__(self, run, files):
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000123 self.run = run
Anna Zaks639b4042012-10-17 21:09:26 +0000124 self.files = files
125 self.diagnostics = []
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000126
George Karpenkova8076602017-10-02 17:59:12 +0000127
Serge Guelton09616bd2018-12-03 12:12:48 +0000128class AnalysisRun(object):
Anna Zaksc80313b2012-10-15 22:48:21 +0000129 def __init__(self, info):
130 self.path = info.path
131 self.root = info.root
132 self.info = info
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000133 self.reports = []
Anna Zaks639b4042012-10-17 21:09:26 +0000134 # Cumulative list of all diagnostics from all the reports.
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000135 self.diagnostics = []
Anna Zaksfab9bb62012-11-15 22:42:44 +0000136 self.clang_version = None
Mikhail R. Gadelha8af2e692018-05-28 15:40:39 +0000137 self.stats = []
Ted Kremenek3a0678e2015-09-08 03:50:52 +0000138
Anna Zaksfab9bb62012-11-15 22:42:44 +0000139 def getClangVersion(self):
140 return self.clang_version
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000141
Jordan Roseb042cc72013-03-23 01:21:26 +0000142 def readSingleFile(self, p, deleteEmpty):
143 data = plistlib.readPlist(p)
Mikhail R. Gadelha8af2e692018-05-28 15:40:39 +0000144 if 'statistics' in data:
145 self.stats.append(json.loads(data['statistics']))
146 data.pop('statistics')
Jordan Roseb042cc72013-03-23 01:21:26 +0000147
Ted Kremenek3a0678e2015-09-08 03:50:52 +0000148 # We want to retrieve the clang version even if there are no
149 # reports. Assume that all reports were created using the same
Jordan Roseb042cc72013-03-23 01:21:26 +0000150 # clang version (this is always true and is more efficient).
151 if 'clang_version' in data:
George Karpenkova8076602017-10-02 17:59:12 +0000152 if self.clang_version is None:
Jordan Roseb042cc72013-03-23 01:21:26 +0000153 self.clang_version = data.pop('clang_version')
154 else:
155 data.pop('clang_version')
156
157 # Ignore/delete empty reports.
158 if not data['files']:
George Karpenkova8076602017-10-02 17:59:12 +0000159 if deleteEmpty:
Jordan Roseb042cc72013-03-23 01:21:26 +0000160 os.remove(p)
161 return
162
163 # Extract the HTML reports, if they exists.
164 if 'HTMLDiagnostics_files' in data['diagnostics'][0]:
165 htmlFiles = []
166 for d in data['diagnostics']:
167 # FIXME: Why is this named files, when does it have multiple
168 # files?
169 assert len(d['HTMLDiagnostics_files']) == 1
170 htmlFiles.append(d.pop('HTMLDiagnostics_files')[0])
171 else:
172 htmlFiles = [None] * len(data['diagnostics'])
Ted Kremenek3a0678e2015-09-08 03:50:52 +0000173
Jordan Roseb042cc72013-03-23 01:21:26 +0000174 report = AnalysisReport(self, data.pop('files'))
Ted Kremenek3a0678e2015-09-08 03:50:52 +0000175 diagnostics = [AnalysisDiagnostic(d, report, h)
George Karpenkova8076602017-10-02 17:59:12 +0000176 for d, h in zip(data.pop('diagnostics'), htmlFiles)]
Jordan Roseb042cc72013-03-23 01:21:26 +0000177
178 assert not data
179
180 report.diagnostics.extend(diagnostics)
181 self.reports.append(report)
182 self.diagnostics.extend(diagnostics)
183
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000184
George Karpenkova8076602017-10-02 17:59:12 +0000185def loadResults(path, opts, root="", deleteEmpty=True):
186 """
187 Backwards compatibility API.
188 """
Anna Zaksc80313b2012-10-15 22:48:21 +0000189 return loadResultsFromSingleRun(SingleRunInfo(path, root, opts.verboseLog),
190 deleteEmpty)
191
George Karpenkova8076602017-10-02 17:59:12 +0000192
Anna Zaksc80313b2012-10-15 22:48:21 +0000193def loadResultsFromSingleRun(info, deleteEmpty=True):
George Karpenkova8076602017-10-02 17:59:12 +0000194 """
195 # Load results of the analyzes from a given output folder.
196 # - info is the SingleRunInfo object
197 # - deleteEmpty specifies if the empty plist files should be deleted
198
199 """
Anna Zaksc80313b2012-10-15 22:48:21 +0000200 path = info.path
201 run = AnalysisRun(info)
Jordan Roseb042cc72013-03-23 01:21:26 +0000202
203 if os.path.isfile(path):
204 run.readSingleFile(path, deleteEmpty)
205 else:
206 for (dirpath, dirnames, filenames) in os.walk(path):
207 for f in filenames:
208 if (not f.endswith('plist')):
209 continue
210 p = os.path.join(dirpath, f)
211 run.readSingleFile(p, deleteEmpty)
212
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000213 return run
214
George Karpenkova8076602017-10-02 17:59:12 +0000215
216def cmpAnalysisDiagnostic(d):
Anna Zaks9b7d7142012-07-16 20:21:42 +0000217 return d.getIssueIdentifier()
Anna Zaksd60367b2012-06-08 01:50:49 +0000218
George Karpenkova8076602017-10-02 17:59:12 +0000219
George Karpenkovb7043222018-02-01 22:25:18 +0000220def compareResults(A, B, opts):
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000221 """
222 compareResults - Generate a relation from diagnostics in run A to
223 diagnostics in run B.
224
George Karpenkovf37c07c2018-02-01 22:40:01 +0000225 The result is the relation as a list of triples (a, b) where
226 each element {a,b} is None or a matching element from the respective run
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000227 """
228
229 res = []
230
George Karpenkovb7043222018-02-01 22:25:18 +0000231 # Map size_before -> size_after
232 path_difference_data = []
233
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000234 # Quickly eliminate equal elements.
235 neqA = []
236 neqB = []
237 eltsA = list(A.diagnostics)
238 eltsB = list(B.diagnostics)
George Karpenkova8076602017-10-02 17:59:12 +0000239 eltsA.sort(key=cmpAnalysisDiagnostic)
240 eltsB.sort(key=cmpAnalysisDiagnostic)
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000241 while eltsA and eltsB:
242 a = eltsA.pop()
243 b = eltsB.pop()
George Karpenkova8076602017-10-02 17:59:12 +0000244 if (a.getIssueIdentifier() == b.getIssueIdentifier()):
George Karpenkovb7043222018-02-01 22:25:18 +0000245 if a.getPathLength() != b.getPathLength():
246 if opts.relative_path_histogram:
247 path_difference_data.append(
248 float(a.getPathLength()) / b.getPathLength())
249 elif opts.relative_log_path_histogram:
250 path_difference_data.append(
251 log(float(a.getPathLength()) / b.getPathLength()))
252 elif opts.absolute_path_histogram:
253 path_difference_data.append(
254 a.getPathLength() - b.getPathLength())
255
George Karpenkovf37c07c2018-02-01 22:40:01 +0000256 res.append((a, b))
Anna Zaks639b4042012-10-17 21:09:26 +0000257 elif a.getIssueIdentifier() > b.getIssueIdentifier():
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000258 eltsB.append(b)
Anna Zaks639b4042012-10-17 21:09:26 +0000259 neqA.append(a)
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000260 else:
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000261 eltsA.append(a)
Anna Zaks639b4042012-10-17 21:09:26 +0000262 neqB.append(b)
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000263 neqA.extend(eltsA)
264 neqB.extend(eltsB)
265
George Karpenkova8076602017-10-02 17:59:12 +0000266 # FIXME: Add fuzzy matching. One simple and possible effective idea would
267 # be to bin the diagnostics, print them in a normalized form (based solely
268 # on the structure of the diagnostic), compute the diff, then use that as
269 # the basis for matching. This has the nice property that we don't depend
270 # in any way on the diagnostic format.
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000271
272 for a in neqA:
George Karpenkovf37c07c2018-02-01 22:40:01 +0000273 res.append((a, None))
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000274 for b in neqB:
George Karpenkovf37c07c2018-02-01 22:40:01 +0000275 res.append((None, b))
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000276
George Karpenkovb7043222018-02-01 22:25:18 +0000277 if opts.relative_log_path_histogram or opts.relative_path_histogram or \
278 opts.absolute_path_histogram:
279 from matplotlib import pyplot
280 pyplot.hist(path_difference_data, bins=100)
281 pyplot.show()
282
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000283 return res
284
George Karpenkov6a2a1972018-10-23 01:30:26 +0000285def computePercentile(l, percentile):
286 """
287 Return computed percentile.
288 """
289 return sorted(l)[int(round(percentile * len(l) + 0.5)) - 1]
290
Mikhail R. Gadelha8af2e692018-05-28 15:40:39 +0000291def deriveStats(results):
292 # Assume all keys are the same in each statistics bucket.
293 combined_data = defaultdict(list)
George Karpenkov6a2a1972018-10-23 01:30:26 +0000294
295 # Collect data on paths length.
296 for report in results.reports:
297 for diagnostic in report.diagnostics:
298 combined_data['PathsLength'].append(diagnostic.getPathLength())
299
Mikhail R. Gadelha8af2e692018-05-28 15:40:39 +0000300 for stat in results.stats:
Serge Gueltond4589742018-12-18 16:04:21 +0000301 for key, value in stat.items():
Mikhail R. Gadelha8af2e692018-05-28 15:40:39 +0000302 combined_data[key].append(value)
303 combined_stats = {}
Serge Gueltond4589742018-12-18 16:04:21 +0000304 for key, values in combined_data.items():
Mikhail R. Gadelha8af2e692018-05-28 15:40:39 +0000305 combined_stats[str(key)] = {
306 "max": max(values),
307 "min": min(values),
308 "mean": sum(values) / len(values),
George Karpenkov6a2a1972018-10-23 01:30:26 +0000309 "90th %tile": computePercentile(values, 0.9),
310 "95th %tile": computePercentile(values, 0.95),
Serge Guelton3744de52018-12-18 08:38:50 +0000311 "median": sorted(values)[len(values) // 2],
Mikhail R. Gadelha8af2e692018-05-28 15:40:39 +0000312 "total": sum(values)
313 }
314 return combined_stats
315
316
317def compareStats(resultsA, resultsB):
318 statsA = deriveStats(resultsA)
319 statsB = deriveStats(resultsB)
320 keys = sorted(statsA.keys())
321 for key in keys:
Serge Gueltonc0ebe772018-12-18 08:36:33 +0000322 print(key)
Mikhail R. Gadelha8af2e692018-05-28 15:40:39 +0000323 for kkey in statsA[key]:
324 valA = float(statsA[key][kkey])
325 valB = float(statsB[key][kkey])
326 report = "%.3f -> %.3f" % (valA, valB)
327 # Only apply highlighting when writing to TTY and it's not Windows
328 if sys.stdout.isatty() and os.name != 'nt':
Mikhail R. Gadelha690a99a2018-05-30 11:17:55 +0000329 if valB != 0:
George Karpenkov13d37482018-07-30 23:01:20 +0000330 ratio = (valB - valA) / valB
331 if ratio < -0.2:
332 report = Colors.GREEN + report + Colors.CLEAR
333 elif ratio > 0.2:
334 report = Colors.RED + report + Colors.CLEAR
Serge Gueltonc0ebe772018-12-18 08:36:33 +0000335 print("\t %s %s" % (kkey, report))
George Karpenkova8076602017-10-02 17:59:12 +0000336
George Karpenkovb7120c92018-02-13 23:36:01 +0000337def dumpScanBuildResultsDiff(dirA, dirB, opts, deleteEmpty=True,
338 Stdout=sys.stdout):
Anna Zaksb80d8362011-09-12 21:32:41 +0000339 # Load the run results.
Anna Zaks45a992b2012-08-02 00:41:40 +0000340 resultsA = loadResults(dirA, opts, opts.rootA, deleteEmpty)
341 resultsB = loadResults(dirB, opts, opts.rootB, deleteEmpty)
George Karpenkov8f6d65c2018-07-30 23:01:47 +0000342 if opts.show_stats:
Mikhail R. Gadelha8af2e692018-05-28 15:40:39 +0000343 compareStats(resultsA, resultsB)
344 if opts.stats_only:
345 return
Ted Kremenek3a0678e2015-09-08 03:50:52 +0000346
Anna Zaksb80d8362011-09-12 21:32:41 +0000347 # Open the verbose log, if given.
348 if opts.verboseLog:
349 auxLog = open(opts.verboseLog, "wb")
350 else:
351 auxLog = None
352
George Karpenkovb7043222018-02-01 22:25:18 +0000353 diff = compareResults(resultsA, resultsB, opts)
Anna Zaks767d3562011-11-08 19:56:31 +0000354 foundDiffs = 0
George Karpenkovdece62a2018-02-01 02:38:42 +0000355 totalAdded = 0
356 totalRemoved = 0
Anna Zaksb80d8362011-09-12 21:32:41 +0000357 for res in diff:
George Karpenkovf37c07c2018-02-01 22:40:01 +0000358 a, b = res
Anna Zaksb80d8362011-09-12 21:32:41 +0000359 if a is None:
George Karpenkovb7120c92018-02-13 23:36:01 +0000360 Stdout.write("ADDED: %r\n" % b.getReadableName())
Anna Zaks767d3562011-11-08 19:56:31 +0000361 foundDiffs += 1
George Karpenkovdece62a2018-02-01 02:38:42 +0000362 totalAdded += 1
Anna Zaksb80d8362011-09-12 21:32:41 +0000363 if auxLog:
George Karpenkovb7120c92018-02-13 23:36:01 +0000364 auxLog.write("('ADDED', %r, %r)\n" % (b.getReadableName(),
365 b.getReport()))
Anna Zaksb80d8362011-09-12 21:32:41 +0000366 elif b is None:
George Karpenkovb7120c92018-02-13 23:36:01 +0000367 Stdout.write("REMOVED: %r\n" % a.getReadableName())
Anna Zaks767d3562011-11-08 19:56:31 +0000368 foundDiffs += 1
George Karpenkovdece62a2018-02-01 02:38:42 +0000369 totalRemoved += 1
Anna Zaksb80d8362011-09-12 21:32:41 +0000370 if auxLog:
George Karpenkovb7120c92018-02-13 23:36:01 +0000371 auxLog.write("('REMOVED', %r, %r)\n" % (a.getReadableName(),
372 a.getReport()))
Anna Zaksb80d8362011-09-12 21:32:41 +0000373 else:
374 pass
375
Anna Zaks767d3562011-11-08 19:56:31 +0000376 TotalReports = len(resultsB.diagnostics)
George Karpenkovb7120c92018-02-13 23:36:01 +0000377 Stdout.write("TOTAL REPORTS: %r\n" % TotalReports)
378 Stdout.write("TOTAL ADDED: %r\n" % totalAdded)
379 Stdout.write("TOTAL REMOVED: %r\n" % totalRemoved)
Anna Zaksb80d8362011-09-12 21:32:41 +0000380 if auxLog:
George Karpenkovb7120c92018-02-13 23:36:01 +0000381 auxLog.write("('TOTAL NEW REPORTS', %r)\n" % TotalReports)
382 auxLog.write("('TOTAL DIFFERENCES', %r)\n" % foundDiffs)
383 auxLog.close()
Ted Kremenek3a0678e2015-09-08 03:50:52 +0000384
Gabor Horvath93fde942015-06-30 15:31:17 +0000385 return foundDiffs, len(resultsA.diagnostics), len(resultsB.diagnostics)
Anna Zaksb80d8362011-09-12 21:32:41 +0000386
George Karpenkovfc782a32018-02-09 18:39:47 +0000387def generate_option_parser():
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000388 parser = OptionParser("usage: %prog [options] [dir A] [dir B]")
Anna Zaks45a992b2012-08-02 00:41:40 +0000389 parser.add_option("", "--rootA", dest="rootA",
390 help="Prefix to ignore on source files for directory A",
391 action="store", type=str, default="")
392 parser.add_option("", "--rootB", dest="rootB",
393 help="Prefix to ignore on source files for directory B",
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000394 action="store", type=str, default="")
395 parser.add_option("", "--verbose-log", dest="verboseLog",
George Karpenkova8076602017-10-02 17:59:12 +0000396 help="Write additional information to LOG \
George Karpenkovfc782a32018-02-09 18:39:47 +0000397 [default=None]",
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000398 action="store", type=str, default=None,
399 metavar="LOG")
George Karpenkovb7043222018-02-01 22:25:18 +0000400 parser.add_option("--relative-path-differences-histogram",
401 action="store_true", dest="relative_path_histogram",
402 default=False,
403 help="Show histogram of relative paths differences. \
George Karpenkovfc782a32018-02-09 18:39:47 +0000404 Requires matplotlib")
George Karpenkovb7043222018-02-01 22:25:18 +0000405 parser.add_option("--relative-log-path-differences-histogram",
406 action="store_true", dest="relative_log_path_histogram",
407 default=False,
408 help="Show histogram of log relative paths differences. \
George Karpenkovfc782a32018-02-09 18:39:47 +0000409 Requires matplotlib")
George Karpenkovb7043222018-02-01 22:25:18 +0000410 parser.add_option("--absolute-path-differences-histogram",
411 action="store_true", dest="absolute_path_histogram",
412 default=False,
413 help="Show histogram of absolute paths differences. \
George Karpenkovfc782a32018-02-09 18:39:47 +0000414 Requires matplotlib")
Mikhail R. Gadelha8af2e692018-05-28 15:40:39 +0000415 parser.add_option("--stats-only", action="store_true", dest="stats_only",
416 default=False, help="Only show statistics on reports")
George Karpenkov8f6d65c2018-07-30 23:01:47 +0000417 parser.add_option("--show-stats", action="store_true", dest="show_stats",
418 default=False, help="Show change in statistics")
George Karpenkovfc782a32018-02-09 18:39:47 +0000419 return parser
420
421
422def main():
423 parser = generate_option_parser()
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000424 (opts, args) = parser.parse_args()
425
426 if len(args) != 2:
427 parser.error("invalid number of arguments")
428
George Karpenkova8076602017-10-02 17:59:12 +0000429 dirA, dirB = args
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000430
Ted Kremenek3a0678e2015-09-08 03:50:52 +0000431 dumpScanBuildResultsDiff(dirA, dirB, opts)
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000432
George Karpenkova8076602017-10-02 17:59:12 +0000433
Daniel Dunbar1a9db992009-08-06 21:15:33 +0000434if __name__ == '__main__':
435 main()