diff --git a/configure/CONFIG_SITE b/configure/CONFIG_SITE index 0e6b629..076f0ed 100644 --- a/configure/CONFIG_SITE +++ b/configure/CONFIG_SITE @@ -24,8 +24,9 @@ # take effect. #IOCS_APPL_TOP = -ifeq ($(DEBUG),1) - DEBUG_CFLAGS=-g -ggdb +ifeq ($(EPICS_TEST_COVERAGE),1) +USR_CPPFLAGS += --coverage +USR_LDFLAGS += --coverage endif ifeq ($(EPICS_HOST_ARCH),linux-x86) diff --git a/scripts/gcovr b/scripts/gcovr new file mode 100755 index 0000000..5c1642b --- /dev/null +++ b/scripts/gcovr @@ -0,0 +1,648 @@ +#! /usr/bin/env python +# +# A report generator for gcov 3.4 +# +# This routine generates a format that is similar to the format generated +# by the Python coverage.py module. This code is similar to the +# data processing performed by lcov's geninfo command. However, we +# don't worry about parsing the *.gcna files, and backwards compatibility for +# older versions of gcov is not supported. +# +# Copyright (2008) Sandia Corporation. Under the terms of Contract +# DE-AC04-94AL85000 with Sandia Corporation, the U.S. Government +# retains certain rights in this software. +# +# Outstanding issues +# - verify that gcov 3.4 or newer is being used +# - verify support for symbolic links +# +# _________________________________________________________________________ +# +# FAST: Python tools for software testing. +# Copyright (c) 2008 Sandia Corporation. +# This software is distributed under the BSD License. +# Under the terms of Contract DE-AC04-94AL85000 with Sandia Corporation, +# the U.S. Government retains certain rights in this software. +# For more information, see the FAST README.txt file. +# _________________________________________________________________________ +# + +import sys +from optparse import OptionParser +import subprocess +import glob +import os +import re +import copy +import xml.dom.minidom + +__version__ = "2.0.prerelease" +gcov_cmd = "gcov" + +output_re = re.compile("creating '(.*)'$") +source_re = re.compile("cannot open source file") + +# +# Container object for coverage statistics +# +class CoverageData(object): + + def __init__(self, fname, uncovered, covered, branches, noncode): + self.fname=fname + # Shallow copies are cheap & "safe" because the caller will + # throw away their copies of covered & uncovered after calling + # us exactly *once* + self.uncovered = copy.copy(uncovered) + self.covered = copy.copy(covered) + self.noncode = copy.copy(noncode) + # But, a deep copy is required here + self.all_lines = copy.deepcopy(uncovered) + self.all_lines.update(covered.keys()) + self.branches = copy.deepcopy(branches) + + def update(self, uncovered, covered, branches, noncode): + self.all_lines.update(uncovered) + self.all_lines.update(covered.keys()) + self.uncovered.update(uncovered) + self.noncode.intersection_update(noncode) + for k in covered.keys(): + self.covered[k] = self.covered.get(k,0) + covered[k] + for k in branches.keys(): + for b in branches[k]: + d = self.branches.setdefault(k, {}) + d[b] = d.get(b, 0) + branches[k][b] + self.uncovered.difference_update(self.covered.keys()) + + def uncovered_str(self): + if options.show_branch: + # Don't do any aggregation on branch results + tmp = [] + for line in self.branches.keys(): + for branch in self.branches[line]: + if self.branches[line][branch] == 0: + tmp.append(line) + break + + tmp.sort() + return ",".join([str(x) for x in tmp]) or "" + + tmp = list(self.uncovered) + if len(tmp) == 0: + return "" + + tmp.sort() + first = None + last = None + ranges=[] + for item in tmp: + #print "HERE",item + if last is None: + first=item + last=item + elif item == (last+1): + last=item + else: + if len(self.noncode.intersection(range(last+1,item))) \ + == item - last - 1: + last = item + continue + + if first==last: + ranges.append(str(first)) + else: + ranges.append(str(first)+"-"+str(last)) + first=item + last=item + if first==last: + ranges.append(str(first)) + else: + ranges.append(str(first)+"-"+str(last)) + return ",".join(ranges) + + def coverage(self): + if ( options.show_branch ): + total = 0 + cover = 0 + for line in self.branches.keys(): + for branch in self.branches[line].keys(): + total += 1 + cover += self.branches[line][branch] > 0 and 1 or 0 + else: + total = len(self.all_lines) + cover = len(self.covered) + + percent = total and str(int(100.0*cover/total)) or "--" + return (total, cover, percent) + + def summary(self,prefix): + if prefix is not None: + if prefix[-1] == os.sep: + tmp = self.fname[len(prefix):] + else: + tmp = self.fname[(len(prefix)+1):] + else: + tmp=self.fname + tmp = tmp.ljust(40) + if len(tmp) > 40: + tmp=tmp+"\n"+" "*40 + + (total, cover, percent) = self.coverage() + return ( total, cover, + tmp + str(total).rjust(8) + str(cover).rjust(8) + \ + percent.rjust(6) + "% " + self.uncovered_str() ) + + +def search_file(expr, path=None, abspath=False, follow_links=False): + """ + Given a search path, recursively descend to find files that match a + regular expression. + + Can specify the following options: + path - The directory that is searched recursively + executable_extension - This string is used to see if there is an + implicit extension in the filename + executable - Test if the file is an executable (default=False) + isfile - Test if the file is file (default=True) + """ + ans = [] + pattern = re.compile(expr) + if path is None or path == ".": + path = os.getcwd() + elif not os.path.exists(path): + raise IOError, "Unknown directory '"+path+"'" + for root, dirs, files in os.walk(path, topdown=True): + for name in files: + if pattern.match(name): + name = os.path.join(root,name) + if follow_links and os.path.islink(name): + ans.append( os.path.abspath(os.readlink(name)) ) + elif abspath: + ans.append( os.path.abspath(name) ) + else: + ans.append( name ) + return ans + + +# +# Get the list of datafiles in the directories specified by the user +# +def get_datafiles(flist, options, ext="gcda"): + allfiles=[] + for dir in flist: + if options.verbose: + print "Scanning directory "+dir+" for "+ext+" files..." + files = search_file(".*\."+ext, dir, abspath=True, follow_links=True) + if options.verbose: + print "Found %d files " % len(files) + allfiles += files + return allfiles + + +def process_gcov_data(file, covdata, options): + INPUT = open(file,"r") + # + # Get the filename + # + line = INPUT.readline() + segments=line.split(":") + fname = (segments[-1]).strip() + if fname[0] != os.sep: + #line = INPUT.readline() + #segments=line.split(":") + #fname = os.path.dirname((segments[-1]).strip())+os.sep+fname + fname = os.path.abspath(fname) + if options.verbose: + print "Parsing coverage data for file "+fname + # + # Return if the filename does not match the filter + # + if options.filter is not None and not options.filter.match(fname): + if options.verbose: + print " Filtering coverage data for file "+fname + return + # + # Return if the filename matches the exclude pattern + # + for i in range(0,len(options.exclude)): + if options.exclude[i].match(fname[filter_pattern_size:]) or \ + options.exclude[i].match(fname): + if options.verbose: + print " Excluding coverage data for file "+fname + return + # + # Parse each line, and record the lines + # that are uncovered + # + noncode = set() + uncovered = set() + covered = {} + branches = {} + #first_record=True + lineno = 0 + for line in INPUT: + segments=line.split(":") + tmp = segments[0].strip() + try: + lineno = int(segments[1].strip()) + except: + pass # keep previous line number! + + if tmp[0] == '#': + uncovered.add( lineno ) + elif tmp[0] in "0123456789": + covered[lineno] = int(segments[0].strip()) + elif tmp[0] == '-': + # remember certain non-executed lines + code = segments[2].strip() + if len(code) == 0 or code == "{" or code == "}" or \ + code.startswith("//") or code == 'else': + noncode.add( lineno ) + elif tmp.startswith('branch'): + fields = line.split() + try: + count = int(fields[3]) + branches.setdefault(lineno, {})[int(fields[1])] = count + except: + # We ignore branches that were "never executed" + pass + elif tmp.startswith('call'): + pass + elif tmp.startswith('function'): + pass + elif tmp[0] == 'f': + pass + #if first_record: + #first_record=False + #uncovered.add(prev) + #if prev in uncovered: + #tokens=re.split('[ \t]+',tmp) + #if tokens[3] != "0": + #uncovered.remove(prev) + #prev = int(segments[1].strip()) + #first_record=True + else: + print "UNKNOWN LINE DATA:",tmp + # + # If the file is already in covdata, then we + # remove lines that are covered here. Otherwise, + # initialize covdata + # + #print "HERE",fname + #print "HERE uncovered",uncovered + #print "HERE covered",covered + if not fname in covdata: + covdata[fname] = CoverageData(fname,uncovered,covered,branches,noncode) + else: + #print "HERE B uncovered",covdata[fname].uncovered + #print "HERE B covered",covdata[fname].covered + covdata[fname].update(uncovered,covered,branches,noncode) + #print "HERE A uncovered",covdata[fname].uncovered + #print "HERE A covered",covdata[fname].covered + INPUT.close() + +# +# Process a datafile and run gcov with the corresponding arguments +# +def process_datafile(filename, covdata, options): + # + # Launch gcov + # + (dirname,base) = os.path.split(filename) + (name,ext) = os.path.splitext(base) + prevdir = os.getcwd() + objdir = '' + (head, tail) = os.path.split(name) + errors=[] + + while True: + os.chdir(dirname) + cmd = [gcov_cmd, "--branch-counts", "--branch-probabilities", + "--preserve-paths"] + if objdir: + cmd.extend(["--object-directory", objdir]) + cmd.append(tail) + + if options.verbose: + print "running gcov: '"+" ".join(cmd)+"' in '"+os.getcwd()+"'" + (out, err) = subprocess.Popen( cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE ).communicate() + + # find the files that gcov created + gcov_files = [] + for line in out.split("\n"): + found = output_re.search(line) + if found is not None: + gcov_files.append(found.group(1)) + #print "Output files\n" + "\n".join(gcov_files) + + if source_re.search(err): + # gcov tossed errors: try running from 1 dir up the hierarchy + errors.append(err) + (tmp1, tmp2) = os.path.split(dirname) + if ( tmp1 == dirname ): + print "GCOV produced the following errors:\n %s" \ + "(gcovr could not infer a working directory " \ + "that resolved it.)" % ( " ".join(errors) ) + break + dirname = tmp1 + objdir = objdir and os.path.join(tmp2, objdir) or tmp2 + # something went wrong with gcov; remove all generated files + # and try again (regardless of options.keep) + for fname in gcov_files: + os.remove(fname) + continue + + # + # Process *.gcov files + # + #for fname in glob.glob("*.gcov"): + for fname in gcov_files: + process_gcov_data(fname, covdata, options) + if not options.keep: + os.remove(fname) + break + + os.chdir(prevdir) + if options.delete: + os.remove(filename) + +# +# Produce the classic gcovr text report +# +def print_text_report(covdata): + def _num_uncovered(key): + (total, covered, percent) = covdata[key].coverage() + return total - covered + def _percent_uncovered(key): + (total, covered, percent) = covdata[key].coverage() + if covered: + return -1.0*covered/total + else: + return total or 1e6 + def _alpha(key): + return key + + total_lines=0 + total_covered=0 + # Header + print "-"*78 + a = options.show_branch and "Branch" or "Lines" + b = options.show_branch and "Taken" or "Exec" + print "File".ljust(40) + a.rjust(8) + b.rjust(8)+ " Cover Missing" + print "-"*78 + + # Data + keys = covdata.keys() + keys.sort(key=options.sort_uncovered and _num_uncovered or \ + options.sort_percent and _percent_uncovered or _alpha) + for key in keys: + (t, n, txt) = covdata[key].summary(options.root) + total_lines += t + total_covered += n + print txt + + # Footer & summary + print "-"*78 + percent = total_lines and str(int(100.0*total_covered/total_lines)) or "--" + print "TOTAL".ljust(40) + str(total_lines).rjust(8) + \ + str(total_covered).rjust(8) + str(percent).rjust(6)+"%" + print "-"*78 + +# +# Produce an XML report in the Cobertura format +# +def print_xml_report(covdata): + + prefix = filter_pattern_size + + impl = xml.dom.minidom.getDOMImplementation() + docType = impl.createDocumentType( + "coverage", None, + "http://cobertura.sourceforge.net/xml/coverage-03.dtd" ) + doc = impl.createDocument(None, "coverage", docType) + root = doc.documentElement + + if options.root is not None: + source = doc.createElement("source") + source.appendChild(doc.createTextNode(options.root)) + sources = doc.createElement("sources") + sources.appendChild(source) + root.appendChild(sources) + + packageXml = doc.createElement("packages") + root.appendChild(packageXml) + packages = {} + + keys = covdata.keys() + keys.sort() + for f in keys: + data = covdata[f] + (dir, fname) = os.path.split(f) + dir = dir[prefix:] + + package = packages.setdefault( + dir, [ doc.createElement("package"), {}, + 0, 0, 0, 0 ] ) + c = doc.createElement("class") + lines = doc.createElement("lines") + c.appendChild(lines) + + class_lines = 0 + class_hits = 0 + class_branches = 0 + class_branch_hits = 0 + for line in data.all_lines: + hits = data.covered.get(line, 0) + class_lines += 1 + if hits > 0: + class_hits += 1 + l = doc.createElement("line") + l.setAttribute("number", str(line)) + l.setAttribute("hits", str(hits)) + branches = data.branches.get(line) + if branches is None: + l.setAttribute("branch", "false") + else: + b_hits = 0 + for v in branches.values(): + if v > 0: + b_hits += 1 + coverage = 100*b_hits/len(branches) + l.setAttribute("branch", "true") + l.setAttribute( "condition-coverage", + "%i%% (%i/%i)" % + (coverage, b_hits, len(branches)) ) + cond = doc.createElement('condition') + cond.setAttribute("number", "0") + cond.setAttribute("type", "jump") + cond.setAttribute("coverage", "%i%%" % ( coverage ) ) + class_branch_hits += b_hits + class_branches += float(len(branches)) + conditions = doc.createElement("conditions") + conditions.appendChild(cond) + l.appendChild(conditions) + + lines.appendChild(l) + + className = fname.replace('.', '_') + c.setAttribute("name", className) + c.setAttribute("filename", dir+os.sep+fname) + c.setAttribute("line-rate", str(class_hits / (1.0*class_lines or 1.0))) + c.setAttribute( "branch-rate", + str(class_branch_hits / (1.0*class_branches or 1.0)) ) + c.setAttribute("complexity", "0.0") + + package[1][className] = c + package[2] += class_hits + package[3] += class_lines + package[4] += class_branch_hits + package[5] += class_branches + + for packageName, packageData in packages.items(): + package = packageData[0]; + packageXml.appendChild(package) + classes = doc.createElement("classes") + package.appendChild(classes) + classNames = packageData[1].keys() + classNames.sort() + for className in classNames: + classes.appendChild(packageData[1][className]) + package.setAttribute("name", packageName.replace(os.sep, '.')) + package.setAttribute("line-rate", str(packageData[2]/(1.0*packageData[3] or 1.0))) + package.setAttribute( "branch-rate", str(packageData[4] / (1.0*packageData[5] or 1.0) )) + package.setAttribute("complexity", "0.0") + + + xmlString = doc.toprettyxml() + print xmlString + #xml.dom.ext.PrettyPrint(doc) + + +## +## MAIN +## + +# +# Create option parser +# +parser = OptionParser() +parser.add_option("--version", + help="Print the version number, then exit", + action="store_true", + dest="version", + default=False) +parser.add_option("-v","--verbose", + help="Print progress messages", + action="store_true", + dest="verbose", + default=False) +parser.add_option("-o","--output", + help="Print output to this filename", + action="store", + dest="output", + default=None) +parser.add_option("-k","--keep", + help="Keep temporary gcov files", + action="store_true", + dest="keep", + default=False) +parser.add_option("-d","--delete", + help="Delete the coverage files after they are processed", + action="store_true", + dest="delete", + default=False) +parser.add_option("-f","--filter", + help="Keep only the data files that match this regular expression", + action="store", + dest="filter", + default=None) +parser.add_option("-e","--exclude", + help="Exclude data files that match this regular expression", + action="append", + dest="exclude", + default=[]) +parser.add_option("-r","--root", + help="Defines the root directory. This is used to filter the files, and to standardize the output.", + action="store", + dest="root", + default=None) +parser.add_option("-x","--xml", + help="Generate XML instead of the normal tabular output.", + action="store_true", + dest="xml", + default=None) +parser.add_option("-b","--branches", + help="Tabulate the branch coverage instead of the line coverage.", + action="store_true", + dest="show_branch", + default=None) +parser.add_option("-u","--sort-uncovered", + help="Sort entries by increasing number of uncovered lines.", + action="store_true", + dest="sort_uncovered", + default=None) +parser.add_option("-p","--sort-percentage", + help="Sort entries by decreasing percentage of covered lines.", + action="store_true", + dest="sort_percent", + default=None) +parser.usage="gcovr [options]" +parser.description="A utility to run gcov and generate a simple report that summarizes the coverage" +# +# Process options +# +(options, args) = parser.parse_args(args=sys.argv) +if options.version: + print "gcovr "+__version__ + print "" + print "Copyright (2008) Sandia Corporation. Under the terms of Contract " + print "DE-AC04-94AL85000 with Sandia Corporation, the U.S. Government " + print "retains certain rights in this software." + sys.exit(0) +# +# Setup filter +# +for i in range(0,len(options.exclude)): + options.exclude[i] = re.compile(options.exclude[i]) +if options.filter is not None: + options.filter = re.compile(options.filter) +elif options.root is not None: + #if options.root[0] != os.sep: + # dir=os.getcwd()+os.sep+options.root + # dir=os.path.abspath(dir) + # options.root=dir + #else: + # options.root=os.path.abspath(options.root) + options.root=os.path.abspath(options.root) + #print "HERE",options.root + options.filter = re.compile(options.root.replace("\\","\\\\")) +if options.filter is not None: + if options.filter.pattern[-1] == os.sep: + filter_pattern_size = len(options.filter.pattern) + else: + filter_pattern_size = len(options.filter.pattern) + len(os.sep) +else: + filter_pattern_size = 0 +# +# Get data files +# +if len(args) == 1: + datafiles = get_datafiles(["."], options) +else: + datafiles = get_datafiles(args[1:], options) +# +# Get coverage data +# +covdata = {} +for file in datafiles: + process_datafile(file,covdata,options) +if options.verbose: + print "Gathered coveraged data for "+str(len(covdata))+" files" +# +# Print report +# +if options.xml: + print_xml_report(covdata) +else: + print_text_report(covdata)