diff options
Diffstat (limited to 'utils/analyzer/CmpRuns.py')
-rwxr-xr-x | utils/analyzer/CmpRuns.py | 246 |
1 files changed, 246 insertions, 0 deletions
diff --git a/utils/analyzer/CmpRuns.py b/utils/analyzer/CmpRuns.py new file mode 100755 index 000000000000..8eba9ebc7bcd --- /dev/null +++ b/utils/analyzer/CmpRuns.py @@ -0,0 +1,246 @@ +#!/usr/bin/env python + +""" +CmpRuns - A simple tool for comparing two static analyzer runs to determine +which reports have been added, removed, or changed. + +This is designed to support automated testing using the static analyzer, from +two perspectives: + 1. To monitor changes in the static analyzer's reports on real code bases, for + regression testing. + + 2. For use by end users who want to integrate regular static analyzer testing + into a buildbot like environment. +""" + +import os +import plistlib + +# + +class multidict: + def __init__(self, elts=()): + self.data = {} + for key,value in elts: + self[key] = value + + def __getitem__(self, item): + return self.data[item] + def __setitem__(self, key, value): + if key in self.data: + self.data[key].append(value) + else: + self.data[key] = [value] + def items(self): + return self.data.items() + def values(self): + return self.data.values() + def keys(self): + return self.data.keys() + def __len__(self): + return len(self.data) + def get(self, key, default=None): + return self.data.get(key, default) + +# + +class CmpOptions: + def __init__(self, verboseLog=None, root=""): + self.root = root + self.verboseLog = verboseLog + +class AnalysisReport: + def __init__(self, run, files): + self.run = run + self.files = files + +class AnalysisDiagnostic: + def __init__(self, data, report, htmlReport): + self.data = data + self.report = report + self.htmlReport = htmlReport + + def getReadableName(self): + loc = self.data['location'] + filename = self.report.run.getSourceName(self.report.files[loc['file']]) + line = loc['line'] + column = loc['col'] + + # FIXME: Get a report number based on this key, to 'distinguish' + # reports, or something. + + return '%s:%d:%d' % (filename, line, column) + + def getReportData(self): + if self.htmlReport is None: + return "This diagnostic does not have any report data." + + return open(os.path.join(self.report.run.path, + self.htmlReport), "rb").read() + +class AnalysisRun: + def __init__(self, path, opts): + self.path = path + self.reports = [] + self.diagnostics = [] + self.opts = opts + + def getSourceName(self, path): + if path.startswith(self.opts.root): + return path[len(self.opts.root):] + return path + +def loadResults(path, opts, deleteEmpty=True): + run = AnalysisRun(path, opts) + + for f in os.listdir(path): + if (not f.startswith('report') or + not f.endswith('plist')): + continue + + p = os.path.join(path, f) + data = plistlib.readPlist(p) + + # Ignore/delete empty reports. + if not data['files']: + if deleteEmpty == True: + os.remove(p) + continue + + # Extract the HTML reports, if they exists. + if 'HTMLDiagnostics_files' in data['diagnostics'][0]: + htmlFiles = [] + for d in data['diagnostics']: + # FIXME: Why is this named files, when does it have multiple + # files? + assert len(d['HTMLDiagnostics_files']) == 1 + htmlFiles.append(d.pop('HTMLDiagnostics_files')[0]) + else: + htmlFiles = [None] * len(data['diagnostics']) + + report = AnalysisReport(run, data.pop('files')) + diagnostics = [AnalysisDiagnostic(d, report, h) + for d,h in zip(data.pop('diagnostics'), + htmlFiles)] + + assert not data + + run.reports.append(report) + run.diagnostics.extend(diagnostics) + + return run + +def compareResults(A, B): + """ + compareResults - Generate a relation from diagnostics in run A to + diagnostics in run B. + + The result is the relation as a list of triples (a, b, confidence) where + each element {a,b} is None or an element from the respective run, and + confidence is a measure of the match quality (where 0 indicates equality, + and None is used if either element is None). + """ + + res = [] + + # Quickly eliminate equal elements. + neqA = [] + neqB = [] + eltsA = list(A.diagnostics) + eltsB = list(B.diagnostics) + eltsA.sort(key = lambda d: d.data) + eltsB.sort(key = lambda d: d.data) + while eltsA and eltsB: + a = eltsA.pop() + b = eltsB.pop() + if a.data == b.data: + res.append((a, b, 0)) + elif a.data > b.data: + neqA.append(a) + eltsB.append(b) + else: + neqB.append(b) + eltsA.append(a) + neqA.extend(eltsA) + neqB.extend(eltsB) + + # FIXME: Add fuzzy matching. One simple and possible effective idea would be + # to bin the diagnostics, print them in a normalized form (based solely on + # the structure of the diagnostic), compute the diff, then use that as the + # basis for matching. This has the nice property that we don't depend in any + # way on the diagnostic format. + + for a in neqA: + res.append((a, None, None)) + for b in neqB: + res.append((None, b, None)) + + return res + +def cmpScanBuildResults(dirA, dirB, opts, deleteEmpty=True): + # Load the run results. + resultsA = loadResults(dirA, opts, deleteEmpty) + resultsB = loadResults(dirB, opts, deleteEmpty) + + # Open the verbose log, if given. + if opts.verboseLog: + auxLog = open(opts.verboseLog, "wb") + else: + auxLog = None + + diff = compareResults(resultsA, resultsB) + foundDiffs = False + for res in diff: + a,b,confidence = res + if a is None: + print "ADDED: %r" % b.getReadableName() + foundDiffs = True + if auxLog: + print >>auxLog, ("('ADDED', %r, %r)" % (b.getReadableName(), + b.getReportData())) + elif b is None: + print "REMOVED: %r" % a.getReadableName() + foundDiffs = True + if auxLog: + print >>auxLog, ("('REMOVED', %r, %r)" % (a.getReadableName(), + a.getReportData())) + elif confidence: + print "CHANGED: %r to %r" % (a.getReadableName(), + b.getReadableName()) + foundDiffs = True + if auxLog: + print >>auxLog, ("('CHANGED', %r, %r, %r, %r)" + % (a.getReadableName(), + b.getReadableName(), + a.getReportData(), + b.getReportData())) + else: + pass + + print "TOTAL REPORTS: %r" % len(resultsB.diagnostics) + if auxLog: + print >>auxLog, "('TOTAL REPORTS', %r)" % len(resultsB.diagnostics) + + return foundDiffs + +def main(): + from optparse import OptionParser + parser = OptionParser("usage: %prog [options] [dir A] [dir B]") + parser.add_option("", "--root", dest="root", + help="Prefix to ignore on source files", + action="store", type=str, default="") + parser.add_option("", "--verbose-log", dest="verboseLog", + help="Write additional information to LOG [default=None]", + action="store", type=str, default=None, + metavar="LOG") + (opts, args) = parser.parse_args() + + if len(args) != 2: + parser.error("invalid number of arguments") + + dirA,dirB = args + + cmpScanBuildResults(dirA, dirB, opts) + +if __name__ == '__main__': + main() |