Skip to content
This repository
Browse code

#1 use new gcovr.py script

  • Loading branch information...
commit 5fa4fe35c3d0416b37fe4787ccd12b4b3be47a1e 1 parent af4dcd6
franckbonin authored May 10, 2012

Showing 1 changed file with 530 additions and 149 deletions. Show diff stats Hide diff stats

  1. 679  src/main/resources/gcovr.py
679  src/main/resources/gcovr.py
... ...
@@ -1,46 +1,61 @@
1 1
 #! /usr/bin/env python
2  
-# 
  2
+#
3 3
 # A report generator for gcov 3.4
4 4
 #
5  
-# This routine generates a format that is similar to the format generated 
6  
-# by the Python coverage.py module.  This code is similar to the 
  5
+# This routine generates a format that is similar to the format generated
  6
+# by the Python coverage.py module.  This code is similar to the
7 7
 # data processing performed by lcov's geninfo command.  However, we
8 8
 # don't worry about parsing the *.gcna files, and backwards compatibility for
9 9
 # older versions of gcov is not supported.
10 10
 #
11  
-# Copyright (2008) Sandia Corporation. Under the terms of Contract
12  
-# DE-AC04-94AL85000 with Sandia Corporation, the U.S. Government
13  
-# retains certain rights in this software.
14  
-# Copyright (2010) Franck Bonin
15  
-# 
16 11
 # Outstanding issues
17 12
 #   - verify that gcov 3.4 or newer is being used
18 13
 #   - verify support for symbolic links
19 14
 #
20  
-#  _________________________________________________________________________
  15
+# gcovr is a FAST project.  For documentation, bug reporting, and
  16
+# updates, see https://software.sandia.gov/trac/fast/wiki/gcovr
  17
+#
  18
+# _________________________________________________________________________
  19
+#
  20
+# FAST: Utilities for Agile Software Development
  21
+# Copyright (c) 2008 Sandia Corporation.
  22
+# This software is distributed under the BSD License.
  23
+# Under the terms of Contract DE-AC04-94AL85000 with Sandia Corporation,
  24
+# the U.S. Government retains certain rights in this software.
  25
+# For more information, see the FAST README.txt file.
21 26
 #
22  
-#  FAST: Python tools for software testing.
23  
-#  Copyright (c) 2008 Sandia Corporation.
24  
-#  Copyright (c) 2010 Franck Bonin.
25  
-#  This software is distributed under the BSD License.
26  
-#  Under the terms of Contract DE-AC04-94AL85000 with Sandia Corporation,
27  
-#  the U.S. Government retains certain rights in this software.
28  
-#  For more information, see the FAST README.txt file.
29  
-#  _________________________________________________________________________
  27
+# $Revision: 2774 $
  28
+# $Date: 2012-04-14 01:26:55 +0200 (Sat, 14 Apr 2012) $
  29
+# _________________________________________________________________________
30 30
 #
31 31
 
32  
-import sys
33  
-from optparse import OptionParser
34  
-import subprocess
  32
+import copy
35 33
 import glob
36 34
 import os
37 35
 import re
38  
-import copy
  36
+import subprocess
  37
+import sys
  38
+import time
39 39
 import xml.dom.minidom
40 40
 
41  
-__version__ = "2.0.prerelease"
  41
+from optparse import OptionParser
  42
+from string import Template
  43
+from os.path import normpath
  44
+
  45
+__version__ = "2.5-prerelease"
  46
+src_revision = "$Revision: 2774 $"
42 47
 gcov_cmd = "gcov"
43 48
 
  49
+output_re = re.compile("[Cc]reating [`'](.*)'$")
  50
+source_re = re.compile("cannot open (source|graph) file")
  51
+
  52
+def version_str():
  53
+    ans = __version__
  54
+    m = re.match('\$Revision:\s*(\S+)\s*\$', src_revision)
  55
+    if m:
  56
+        ans = ans + " (r%s)" % (m.group(1))
  57
+    return ans
  58
+
44 59
 #
45 60
 # Container object for coverage statistics
46 61
 #
@@ -94,7 +109,6 @@ def uncovered_str(self):
94 109
         last = None
95 110
         ranges=[]
96 111
         for item in tmp:
97  
-            #print "HERE",item
98 112
             if last is None:
99 113
                 first=item
100 114
                 last=item
@@ -133,14 +147,12 @@ def coverage(self):
133 147
         percent = total and str(int(100.0*cover/total)) or "--"
134 148
         return (total, cover, percent)
135 149
 
136  
-    def summary(self,prefix):
137  
-        if prefix is not None:
138  
-            if prefix[-1] == os.sep:
139  
-                tmp = self.fname[len(prefix):]
140  
-            else:
141  
-                tmp = self.fname[(len(prefix)+1):]
142  
-        else:
143  
-            tmp=self.fname
  150
+    def summary(self):
  151
+        tmp = options.filter.sub('',self.fname)
  152
+        if not self.fname.endswith(tmp):
  153
+            # Do no truncation if the filter does not start matching at
  154
+            # the beginning of the string
  155
+            tmp = self.fname
144 156
         tmp = tmp.ljust(40)
145 157
         if len(tmp) > 40:
146 158
             tmp=tmp+"\n"+" "*40
@@ -151,49 +163,184 @@ def summary(self,prefix):
151 163
                  percent.rjust(6) + "%   " + self.uncovered_str() )
152 164
 
153 165
 
154  
-def search_file(expr, path=None, abspath=False, follow_links=False):
  166
+def resolve_symlinks(orig_path):
  167
+    """
  168
+    Return the normalized absolute path name with all symbolic links resolved
  169
+    """
  170
+    drive,tmp = os.path.splitdrive(os.path.abspath(orig_path))
  171
+    if not drive:
  172
+        drive = os.path.sep
  173
+    parts = tmp.split(os.path.sep)
  174
+    actual_path = [drive]
  175
+    while parts:
  176
+        actual_path.append(parts.pop(0))
  177
+        if not os.path.islink(os.path.join(*actual_path)):
  178
+            continue
  179
+        actual_path[-1] = os.readlink(os.path.join(*actual_path))
  180
+        tmp_drive, tmp_path = os.path.splitdrive(
  181
+            resolve_symlinks(os.path.join(*actual_path)) )
  182
+        if tmp_drive:
  183
+            drive = tmp_drive
  184
+        actual_path = [drive] + tmp_path.split(os.path.sep)
  185
+    return os.path.join(*actual_path)
  186
+
  187
+
  188
+def path_startswith(path, base):
  189
+    return path.startswith(base) and (
  190
+        len(base) == len(path) or path[len(base)] == os.path.sep )
  191
+
  192
+
  193
+class PathAliaser(object):
  194
+    def __init__(self):
  195
+        self.aliases = {}
  196
+        self.master_targets = set()
  197
+        self.preferred_name = {}
  198
+
  199
+    def master_path(self, path):
  200
+        match_found = False
  201
+        while True:
  202
+            for base, alias in self.aliases.items():
  203
+                if path_startswith(path, base):
  204
+                    path = alias + path[len(base):]
  205
+                    match_found = True
  206
+                    break
  207
+            for master_base in self.master_targets:
  208
+                if path_startswith(path, master_base):
  209
+                    return path, master_base, True
  210
+            if match_found:
  211
+                sys.stderr.write(
  212
+                    "(ERROR) violating fundamental assumption while walking "
  213
+                    "directory tree.\n\tPlease report this to the gcovr "
  214
+                    "developers.\n" )
  215
+            return path, None, match_found
  216
+
  217
+    def unalias_path(self, path):
  218
+        path = resolve_symlinks(path)
  219
+        path, master_base, known_path = self.master_path(path)
  220
+        if not known_path:
  221
+            return path
  222
+        # Try and resolve the preferred name for this location
  223
+        if master_base in self.preferred_name:
  224
+            return self.preferred_name[master_base] + path[len(master_base):]
  225
+        return path
  226
+
  227
+    def add_master_target(self, master):
  228
+        self.master_targets.add(master)
  229
+
  230
+    def add_alias(self, target, master):
  231
+        self.aliases[target] = master
  232
+
  233
+    def set_preferred(self, master, preferred):
  234
+        self.preferred_name[master] = preferred
  235
+
  236
+aliases = PathAliaser()
  237
+
  238
+# This is UGLY.  Here's why: UNIX resolves symbolic links by walking the
  239
+# entire directory structure.  What that means is that relative links
  240
+# are always relative to the actual directory inode, and not the
  241
+# "virtual" path that the user might have traversed (over symlinks) on
  242
+# the way to that directory.  Here's the canonical example:
  243
+#
  244
+#   a / b / c / testfile
  245
+#   a / d / e --> ../../a/b
  246
+#   m / n --> /a
  247
+#   x / y / z --> /m/n/d
  248
+#
  249
+# If we start in "y", we will see the following directory structure:
  250
+#   y
  251
+#   |-- z
  252
+#       |-- e
  253
+#           |-- c
  254
+#               |-- testfile
  255
+#
  256
+# The problem is that using a simple traversal based on the Python
  257
+# documentation:
  258
+#
  259
+#    (os.path.join(os.path.dirname(path), os.readlink(result)))
  260
+#
  261
+# will not work: we will see a link to /m/n/d from /x/y, but completely
  262
+# miss the fact that n is itself a link.  If we then naively attempt to
  263
+# apply the "c" relative link, we get an intermediate path that looks
  264
+# like "/m/n/d/e/../../a/b", which would get normalized to "/m/n/a/b"; a
  265
+# nonexistant path.  The solution is that we need to walk the original
  266
+# path, along with the full path of all links 1 directory at a time and
  267
+# check for embedded symlinks.
  268
+#
  269
+def link_walker(path):
  270
+    targets = [os.path.abspath(path)]
  271
+    while targets:
  272
+        target_dir = targets.pop(0)
  273
+        actual_dir = resolve_symlinks(target_dir)
  274
+        #print "target dir: %s  (%s)" % (target_dir, actual_dir)
  275
+        master_name, master_base, visited = aliases.master_path(actual_dir)
  276
+        if visited:
  277
+            #print "  ...root already visited as %s" % master_name
  278
+            aliases.add_alias(target_dir, master_name)
  279
+            continue
  280
+        if master_name != target_dir:
  281
+            aliases.set_preferred(master_name, target_dir)
  282
+            aliases.add_alias(target_dir, master_name)
  283
+        aliases.add_master_target(master_name)
  284
+        #print "  ...master name = %s" % master_name
  285
+        #print "  ...walking %s" % target_dir
  286
+        for root, dirs, files in os.walk(target_dir, topdown=True):
  287
+            #print "    ...reading %s" % root
  288
+            for d in dirs:
  289
+                tmp = os.path.abspath(os.path.join(root, d))
  290
+                #print "    ...checking %s" % tmp
  291
+                if os.path.islink(tmp):
  292
+                    #print "      ...buffering link %s" % tmp
  293
+                    targets.append(tmp)
  294
+            yield root, dirs, files
  295
+
  296
+
  297
+def search_file(expr, path):
155 298
     """
156 299
     Given a search path, recursively descend to find files that match a
157 300
     regular expression.
158  
-
159  
-    Can specify the following options:
160  
-       path - The directory that is searched recursively
161  
-       executable_extension - This string is used to see if there is an
162  
-           implicit extension in the filename
163  
-       executable - Test if the file is an executable (default=False)
164  
-       isfile - Test if the file is file (default=True)
165 301
     """
166 302
     ans = []
167 303
     pattern = re.compile(expr)
168 304
     if path is None or path == ".":
169 305
         path = os.getcwd()
170  
-    elif os.path.exists(path):
171  
-        raise IOError, "Unknown directory '"+path+"'"
172  
-    for root, dirs, files in os.walk(path, topdown=True):
  306
+    elif not os.path.exists(path):
  307
+        raise IOError("Unknown directory '"+path+"'")
  308
+    for root, dirs, files in link_walker(path):
173 309
         for name in files:
174  
-           if pattern.match(name):
  310
+            if pattern.match(name):
175 311
                 name = os.path.join(root,name)
176  
-                if follow_links and os.path.islink(name):
  312
+                if os.path.islink(name):
177 313
                     ans.append( os.path.abspath(os.readlink(name)) )
178  
-                elif abspath:
179  
-                    ans.append( os.path.abspath(name) )
180 314
                 else:
181  
-                    ans.append( name )
  315
+                    ans.append( os.path.abspath(name) )
182 316
     return ans
183 317
 
184 318
 
185 319
 #
186 320
 # Get the list of datafiles in the directories specified by the user
187 321
 #
188  
-def get_datafiles(flist, options, ext="gcda"):
  322
+def get_datafiles(flist, options):
189 323
     allfiles=[]
190 324
     for dir in flist:
191 325
         if options.verbose:
192  
-            print "Scanning directory "+dir+" for "+ext+" files..."
193  
-        files = search_file(".*\."+ext, dir, abspath=True, follow_links=True)
  326
+            sys.stdout.write( "Scanning directory %s for gcda/gcno files...\n"
  327
+                              % (dir, ) )
  328
+        files = search_file(".*\.gc(da|no)$", dir)
  329
+        # gcno files will *only* produce uncovered results; however,
  330
+        # that is useful information for the case where a compilation
  331
+        # unit is never actually exercised by the test code.  So, we
  332
+        # will process gcno files, but ONLY if there is no corresponding
  333
+        # gcda file.
  334
+        gcda_files = [file for file in files if file.endswith('gcda')]
  335
+        tmp = set(gcda_files)
  336
+        gcno_files = [ file for file in files if
  337
+                       file.endswith('gcno') and file[:-2]+'da' not in tmp ]
194 338
         if options.verbose:
195  
-            print "Found %d files " % len(files)
196  
-        allfiles += files
  339
+            sys.stdout.write(
  340
+                "Found %d files (and will process %d)\n" %
  341
+                ( len(files), len(gcda_files) + len(gcno_files) ) )
  342
+        allfiles.extend(gcda_files)
  343
+        allfiles.extend(gcno_files)
197 344
     return allfiles
198 345
 
199 346
 
@@ -203,27 +350,31 @@ def process_gcov_data(file, covdata, options):
203 350
     # Get the filename
204 351
     #
205 352
     line = INPUT.readline()
206  
-    segments=line.split(":")
207  
-    fname = (segments[-1]).strip()
208  
-    if fname[0] != os.sep:
209  
-        #line = INPUT.readline()
210  
-        #segments=line.split(":")
211  
-        #fname = os.path.dirname((segments[-1]).strip())+os.sep+fname
212  
-        fname = os.path.abspath(fname)
  353
+    segments=line.split(':',3)
  354
+    if len(segments) != 4 or not segments[2].lower().strip().endswith('source'):
  355
+        raise RuntimeError('Fatal error parsing gcov file, line 1: \n\t"%s"' % line.rstrip())
  356
+    fname = aliases.unalias_path(os.path.abspath((segments[-1]).strip()))
  357
+
  358
+    fname = os.path.normpath(fname)
  359
+    
  360
+    if options.verbose:
  361
+        sys.stdout.write("Parsing coverage data for file %s\n" % fname)
213 362
     #
214 363
     # Return if the filename does not match the filter
215 364
     #
216  
-    if options.filter is not None and not options.filter.match(fname):
  365
+    if not options.filter.match(fname):
217 366
         if options.verbose:
218  
-            print "  Ignoring coverage data for file "+fname
  367
+            sys.stdout.write("  Filtering coverage data for file %s\n" % fname)
219 368
         return
220 369
     #
221 370
     # Return if the filename matches the exclude pattern
222 371
     #
223 372
     for i in range(0,len(options.exclude)):
224  
-        if options.exclude[i].match(fname):
  373
+        if options.exclude[i].match(options.filter.sub('',fname)) or \
  374
+               options.exclude[i].match(fname) or \
  375
+               options.exclude[i].match(os.path.abspath(fname)):
225 376
             if options.verbose:
226  
-                print "  Ignoring coverage data for file "+fname
  377
+                sys.stdout.write("  Excluding coverage data for file %s\n" % fname)
227 378
             return
228 379
     #
229 380
     # Parse each line, and record the lines
@@ -236,12 +387,13 @@ def process_gcov_data(file, covdata, options):
236 387
     #first_record=True
237 388
     lineno = 0
238 389
     for line in INPUT:
239  
-        segments=line.split(":")
  390
+        segments=line.split(":",2)
240 391
         tmp = segments[0].strip()
241  
-        try:
242  
-            lineno = int(segments[1].strip())
243  
-        except:
244  
-            pass # keep previous line number!
  392
+        if len(segments) > 1:
  393
+            try:
  394
+                lineno = int(segments[1].strip())
  395
+            except:
  396
+                pass # keep previous line number!
245 397
             
246 398
         if tmp[0] == '#':
247 399
             uncovered.add( lineno )
@@ -276,61 +428,191 @@ def process_gcov_data(file, covdata, options):
276 428
                     #uncovered.remove(prev)
277 429
             #prev = int(segments[1].strip())
278 430
             #first_record=True
279  
-        #$FB print this only in verbose mode !
280  
-        elif options.verbose:
281  
-            print "UNKNOWN LINE DATA:",tmp
  431
+        else:
  432
+            sys.stderr.write(
  433
+                "(WARNING) Unrecognized GCOV output: '%s'\n"
  434
+                "\tThis is indicitive of a gcov output parse error.\n"
  435
+                "\tPlease report this to the gcovr developers." % tmp )
282 436
     #
283 437
     # If the file is already in covdata, then we
284 438
     # remove lines that are covered here.  Otherwise,
285 439
     # initialize covdata
286 440
     #
287  
-    #print "HERE",fname
288  
-    #print "HERE uncovered",uncovered
289  
-    #print "HERE   covered",covered
290 441
     if not fname in covdata:
291 442
         covdata[fname] = CoverageData(fname,uncovered,covered,branches,noncode)
292 443
     else:
293  
-        #print "HERE B uncovered",covdata[fname].uncovered
294  
-        #print "HERE B   covered",covdata[fname].covered
295 444
         covdata[fname].update(uncovered,covered,branches,noncode)
296  
-        #print "HERE A uncovered",covdata[fname].uncovered
297  
-        #print "HERE A   covered",covdata[fname].covered
298 445
     INPUT.close()
299 446
 
300 447
 #
301  
-# Process a datafile and run gcov with the corresponding arguments
  448
+# Process a datafile (generated by running the instrumented application)
  449
+# and run gcov with the corresponding arguments
  450
+#
  451
+# This is trickier than it sounds: The gcda/gcno files are stored in the
  452
+# same directory as the object files; however, gcov must be run from the
  453
+# same directory where gcc/g++ was run.  Normally, the user would know
  454
+# where gcc/g++ was invoked from and could tell gcov the path to the
  455
+# object (and gcda) files with the --object-directory command.
  456
+# Unfortunately, we do everything backwards: gcovr looks for the gcda
  457
+# files and then has to infer the original gcc working directory.
  458
+#
  459
+# In general, (but not always) we can assume that the gcda file is in a
  460
+# subdirectory of the original gcc working directory, so we will first
  461
+# try ".", and on error, move up the directory tree looking for the
  462
+# correct working directory (letting gcov's own error codes dictate when
  463
+# we hit the right directory).  This covers 90+% of the "normal" cases.
  464
+# The exception to this is if gcc was invoked with "-o ../[...]" (i.e.,
  465
+# the object directory was a peer (not a parent/child) of the cwd.  In
  466
+# this case, things are really tough.  We accept an argument
  467
+# (--object-directory) that SHOULD BE THE SAME as the one povided to
  468
+# gcc.  We will then walk that path (backwards) in the hopes of
  469
+# identifying the original gcc working directory (there is a bit of
  470
+# trial-and-error here)
302 471
 #
303  
-def process_datafile(file, covdata, options):
  472
+def process_datafile(filename, covdata, options):
304 473
     #
305 474
     # Launch gcov
306 475
     #
307  
-    (dir,base) = os.path.split(file)
  476
+    abs_filename = os.path.abspath(filename)
  477
+    (dirname,fname) = os.path.split(abs_filename)
308 478
     #$FB take .gcda file directly since .gcda ext truncature can leeds to a unreachable .cpp file located elsewhere but next to gcda file
309 479
     #(name,ext) = os.path.splitext(base)
310  
-    name = base
311  
-    prevdir = os.getcwd()
312  
-    os.chdir(dir)
313  
-    (head, tail) = os.path.split(name)
  480
+
  481
+    potential_wd = []
  482
+    starting_dir = os.getcwd()
  483
+    errors=[]
  484
+    Done = False
  485
+
  486
+    if options.objdir:
  487
+        src_components = abs_filename.split(os.sep)
  488
+        components = normpath(options.objdir).split(os.sep)
  489
+        idx = 1
  490
+        while idx <= len(components):
  491
+            if idx > len(src_components):
  492
+                break
  493
+            if components[-1*idx] != src_components[-1*idx]:
  494
+                break
  495
+            idx += 1
  496
+        if idx > len(components):
  497
+            pass # a parent dir; the normal process will find it
  498
+        elif components[-1*idx] == '..':
  499
+            dirs = [ os.path.join(src_components[:len(src_components)-idx+1]) ]
  500
+            while idx <= len(components) and components[-1*idx] == '..':
  501
+                tmp = []
  502
+                for d in dirs:
  503
+                    for f in os.listdir(d):
  504
+                        x = os.path.join(d,f)
  505
+                        if os.path.isdir(x):
  506
+                            tmp.append(x)
  507
+                dirs = tmp
  508
+                idx += 1
  509
+            potential_wd = dirs
  510
+        else:
  511
+            if components[0] == '':
  512
+                # absolute path
  513
+                tmp = [ options.objdir ]
  514
+            else:
  515
+                # relative path: check relative to both the cwd and the
  516
+                # gcda file
  517
+                tmp = [ os.path.join(x, options.objdir) for x in
  518
+                        [os.path.dirname(abs_filename), os.getcwd()] ]
  519
+            potential_wd = [ testdir for testdir in tmp
  520
+                             if os.path.isdir(testdir) ]
  521
+            if len(potential_wd) == 0:
  522
+                errors.append("ERROR: cannot identify the location where GCC "
  523
+                              "was run using --object-directory=%s\n" %
  524
+                              options.objdir)
  525
+            # Revert to the normal 
  526
+            #sys.exit(1)
  527
+
  528
+    # no objdir was specified (or it was a parent dir); walk up the dir tree
  529
+    if len(potential_wd) == 0:
  530
+        wd = os.path.split(abs_filename)[0]
  531
+        while True:
  532
+            potential_wd.append(wd)
  533
+            wd = os.path.split(wd)[0]
  534
+            if wd == potential_wd[-1]:
  535
+                break
314 536
 #$FB --preserve-paths not usefull with out of source build
315  
-#    cmd = gcov_cmd + \
316  
-#          " --branch-counts --branch-probabilities --preserve-paths " + tail
317  
-    cmd = gcov_cmd + \
318  
-          " --branch-counts --branch-probabilities " + tail
319  
-    output = subprocess.Popen( cmd.split(" "),
320  
-                               stdout=subprocess.PIPE ).communicate()[0]
321  
-    #output = subprocess.call(cmd)
322  
-    #print "HERE",cmd
323  
-    #print "X",output
324  
-    #
325  
-    # Process *.gcov files
326  
-    #
327  
-    for fname in glob.glob("*.gcov"):
328  
-        process_gcov_data(fname, covdata, options)
  537
+#    cmd = [ gcov_cmd, abs_filename,
  538
+#            "--branch-counts", "--branch-probabilities", "--preserve-paths", 
  539
+#            '--object-directory', dirname ]
  540
+    cmd = [ gcov_cmd, abs_filename,
  541
+            "--branch-counts", "--branch-probabilities", 
  542
+            '--object-directory', dirname ]
  543
+
  544
+    # NB: We are lazy English speakers, so we will only parse English output
  545
+    env = dict(os.environ)
  546
+    env['LC_ALL'] = 'en_US'
  547
+    
  548
+
  549
+    while len(potential_wd) > 0 and not Done:
  550
+        # NB: either len(potential_wd) == 1, or all entires are absolute
  551
+        # paths, so we don't have to chdir(starting_dir) at every
  552
+        # iteration.
  553
+        os.chdir(potential_wd.pop(0)) 
  554
+        
  555
+        
  556
+        #if options.objdir:
  557
+        #    cmd.extend(["--object-directory", Template(options.objdir).substitute(filename=filename, head=dirname, tail=base, root=name, ext=ext)])
  558
+
  559
+        if options.verbose:
  560
+            sys.stdout.write("Running gcov: '%s' in '%s'\n" % ( ' '.join(cmd), os.getcwd() ))
  561
+        (out, err) = subprocess.Popen( cmd, env=env,
  562
+                                       stdout=subprocess.PIPE,
  563
+                                       stderr=subprocess.PIPE ).communicate()
  564
+        out=out.decode('utf-8')
  565
+        err=err.decode('utf-8')
  566
+
  567
+        # find the files that gcov created
  568
+        gcov_files = {'active':[], 'filter':[], 'exclude':[]}
  569
+        for line in out.splitlines():
  570
+            found = output_re.search(line.strip())
  571
+            if found is not None:
  572
+                fname = found.group(1)
  573
+                if not options.gcov_filter.match(fname):
  574
+                    if options.verbose:
  575
+                        sys.stdout.write("Filtering gcov file %s\n" % fname)
  576
+                    gcov_files['filter'].append(fname)
  577
+                    continue
  578
+                exclude=False
  579
+                for i in range(0,len(options.gcov_exclude)):
  580
+                    if options.gcov_exclude[i].match(options.gcov_filter.sub('',fname)) or \
  581
+                           options.gcov_exclude[i].match(fname) or \
  582
+                           options.gcov_exclude[i].match(os.path.abspath(fname)):
  583
+                        exclude=True
  584
+                        break
  585
+                if not exclude:
  586
+                    gcov_files['active'].append(fname)
  587
+                elif options.verbose:
  588
+                    sys.stdout.write("Excluding gcov file %s\n" % fname)
  589
+                    gcov_files['exclude'].append(fname)
  590
+
  591
+        if source_re.search(err):
  592
+            # gcov tossed errors: try the next potential_wd
  593
+            errors.append(err)
  594
+        else:
  595
+            # Process *.gcov files
  596
+            for fname in gcov_files['active']:
  597
+                process_gcov_data(fname, covdata, options)
  598
+            Done = True
  599
+
329 600
         if not options.keep:
330  
-            os.remove(fname)
331  
-    os.chdir(prevdir)
  601
+            for group in gcov_files.values():
  602
+                for fname in group:
  603
+                    os.remove(fname)
  604
+
  605
+    os.chdir(starting_dir)
332 606
     if options.delete:
333  
-        os.remove(file)
  607
+        if not abs_filename.endswith('gcno'):
  608
+            os.remove(abs_filename)
  609
+        
  610
+    if not Done:
  611
+        sys.stderr.write(
  612
+            "(WARNING) GCOV produced the following errors processing %s:\n"
  613
+            "\t   %s" 
  614
+            "\t(gcovr could not infer a working directory that resolved it.)\n"
  615
+            % ( filename, "\t   ".join(errors) ) )
334 616
 
335 617
 #
336 618
 # Produce the classic gcovr text report
@@ -348,69 +630,102 @@ def _percent_uncovered(key):
348 630
     def _alpha(key):
349 631
         return key
350 632
 
  633
+    if options.output:
  634
+        OUTPUT = open(options.output,'w')
  635
+    else:
  636
+        OUTPUT = sys.stdout
351 637
     total_lines=0
352 638
     total_covered=0
353 639
     # Header
354  
-    print "-"*78
  640
+    OUTPUT.write("-"*78 + '\n')
355 641
     a = options.show_branch and "Branch" or "Lines"
356 642
     b = options.show_branch and "Taken" or "Exec"
357  
-    print "File".ljust(40) + a.rjust(8) + b.rjust(8)+ "  Cover   Missing"
358  
-    print "-"*78
  643
+    OUTPUT.write("File".ljust(40) + a.rjust(8) + b.rjust(8)+ "  Cover   Missing\n")
  644
+    OUTPUT.write("-"*78 + '\n')
359 645
 
360 646
     # Data
361  
-    keys = covdata.keys()
  647
+    keys = list(covdata.keys())
362 648
     keys.sort(key=options.sort_uncovered and _num_uncovered or \
363 649
               options.sort_percent and _percent_uncovered or _alpha)
364 650
     for key in keys:
365  
-        (t, n, txt) = covdata[key].summary(options.root)
  651
+        (t, n, txt) = covdata[key].summary()
366 652
         total_lines += t
367 653
         total_covered += n
368  
-        print txt
  654
+        OUTPUT.write(txt + '\n')
369 655
 
370 656
     # Footer & summary
371  
-    print "-"*78
  657
+    OUTPUT.write("-"*78 + '\n')
372 658
     percent = total_lines and str(int(100.0*total_covered/total_lines)) or "--"
373  
-    print "TOTAL".ljust(40) + str(total_lines).rjust(8) + \
374  
-          str(total_covered).rjust(8) + str(percent).rjust(6)+"%"
375  
-    print "-"*78
  659
+    OUTPUT.write("TOTAL".ljust(40) + str(total_lines).rjust(8) + \
  660
+          str(total_covered).rjust(8) + str(percent).rjust(6)+"%" + '\n')
  661
+    OUTPUT.write("-"*78 + '\n')
  662
+
  663
+    # Close logfile
  664
+    if options.output:
  665
+        OUTPUT.close()
376 666
 
377 667
 #
378 668
 # Produce an XML report in the Cobertura format
379 669
 #
380 670
 def print_xml_report(covdata):
  671
+    branchTotal = 0
  672
+    branchCovered = 0
  673
+    lineTotal = 0
  674
+    lineCovered = 0
381 675
 
382  
-    if options.root is not None:
383  
-        if options.root[-1] == os.sep:
384  
-            prefix = len(options.root)
385  
-        else:
386  
-            prefix = len(options.root) + 1
387  
-    else:
388  
-        prefix = 0
  676
+    options.show_branch = True
  677
+    for key in covdata.keys():
  678
+        (total, covered, percent) = covdata[key].coverage()
  679
+        branchTotal += total
  680
+        branchCovered += covered
389 681
 
  682
+    options.show_branch = False
  683
+    for key in covdata.keys():
  684
+        (total, covered, percent) = covdata[key].coverage()
  685
+        lineTotal += total
  686
+        lineCovered += covered
  687
+    
390 688
     impl = xml.dom.minidom.getDOMImplementation()
391 689
     docType = impl.createDocumentType(
392 690
         "coverage", None,
393 691
         "http://cobertura.sourceforge.net/xml/coverage-03.dtd" )
394 692
     doc = impl.createDocument(None, "coverage", docType)
395 693
     root = doc.documentElement
396  
-
397  
-    if options.root is not None:
398  
-        source = doc.createElement("source")
399  
-        source.appendChild(doc.createTextNode(options.root))
400  
-        sources = doc.createElement("sources")
401  
-        sources.appendChild(source)
402  
-        root.appendChild(sources)
403  
-
  694
+    root.setAttribute( "line-rate", lineTotal == 0 and '0.0' or
  695
+                       str(float(lineCovered) / lineTotal) )
  696
+    root.setAttribute( "branch-rate", branchTotal == 0 and '0.0' or
  697
+                       str(float(branchCovered) / branchTotal) )
  698
+    root.setAttribute( "timestamp", str(int(time.time())) )
  699
+    root.setAttribute( "version", "gcovr %s" % (version_str(),) )
  700
+
  701
+    # Generate the <sources> element: this is either the root directory
  702
+    # (specified by --root), or the CWD.
  703
+    sources = doc.createElement("sources")
  704
+    root.appendChild(sources)
  705
+
  706
+    # Generate the coverage output (on a per-package basis)
404 707
     packageXml = doc.createElement("packages")
405 708
     root.appendChild(packageXml)
406 709
     packages = {}
  710
+    source_dirs = set()
407 711
 
408  
-    keys = covdata.keys()
  712
+    keys = list(covdata.keys())
409 713
     keys.sort()
410 714
     for f in keys:
411 715
         data = covdata[f]
412  
-        (dir, fname) = os.path.split(f)
413  
-        dir = dir[prefix:]
  716
+        dir = options.filter.sub('',f)
  717
+        if f.endswith(dir):
  718
+            src_path = f[:-1*len(dir)]
  719
+            if len(src_path) > 0:
  720
+                while dir.startswith(os.path.sep):
  721
+                    src_path += os.path.sep
  722
+                    dir = dir[len(os.path.sep):]
  723
+                source_dirs.add(src_path)
  724
+        else:
  725
+            # Do no truncation if the filter does not start matching at
  726
+            # the beginning of the string
  727
+            dir = f
  728
+        (dir, fname) = os.path.split(dir)
414 729
         
415 730
         package = packages.setdefault(
416 731
             dir, [ doc.createElement("package"), {},
@@ -458,7 +773,7 @@ def print_xml_report(covdata):
458 773
 
459 774
         className = fname.replace('.', '_')
460 775
         c.setAttribute("name", className)
461  
-        c.setAttribute("filename", dir+os.sep+fname)
  776
+        c.setAttribute("filename", os.path.join(dir, fname))
462 777
         c.setAttribute("line-rate", str(class_hits / (1.0*class_lines or 1.0)))
463 778
         c.setAttribute( "branch-rate",
464 779
                         str(class_branch_hits / (1.0*class_branches or 1.0)) )
@@ -475,7 +790,7 @@ def print_xml_report(covdata):
475 790
         packageXml.appendChild(package)
476 791
         classes = doc.createElement("classes")
477 792
         package.appendChild(classes)
478  
-        classNames = packageData[1].keys()
  793
+        classNames = list(packageData[1].keys())
479 794
         classNames.sort()
480 795
         for className in classNames:
481 796
             classes.appendChild(packageData[1][className])
@@ -485,9 +800,42 @@ def print_xml_report(covdata):
485 800
         package.setAttribute("complexity", "0.0")
486 801
 
487 802
 
  803
+    # Populate the <sources> element: this is either the root directory
  804
+    # (specified by --root), or relative directories based
  805
+    # on the filter, or the CWD
  806
+    if options.root is not None:
  807
+        source = doc.createElement("source")
  808
+        source.appendChild(doc.createTextNode(options.root))
  809
+        sources.appendChild(source)
  810
+    elif len(source_dirs) > 0:
  811
+        cwd = os.getcwd()
  812
+        for d in source_dirs:
  813
+            source = doc.createElement("source")
  814
+            if d.startswith(cwd):
  815
+                reldir = d[len(cwd):].lstrip(os.path.sep)
  816
+            elif cwd.startswith(d):
  817
+                i = 1
  818
+                while normpath(d) != \
  819
+                          normpath(os.path.join(*tuple([cwd]+['..']*i))):
  820
+                    i += 1
  821
+                reldir = os.path.join(*tuple(['..']*i))
  822
+            else:
  823
+                reldir = d
  824
+            source.appendChild(doc.createTextNode(reldir))
  825
+            sources.appendChild(source)
  826
+    else:
  827
+        source = doc.createElement("source")
  828
+        source.appendChild(doc.createTextNode('.'))
  829
+        sources.appendChild(source)
  830
+
488 831
     xmlString = doc.toprettyxml()
489  
-    print xmlString
490 832
     #xml.dom.ext.PrettyPrint(doc)
  833
+    if options.output is None:
  834
+        sys.stdout.write(xmlString+'\n')
  835
+    else:
  836
+        OUTPUT = open(options.output, 'w')
  837
+        OUTPUT.write(xmlString +'\n')
  838
+        OUTPUT.close()
491 839
 
492 840
 
493 841
 ##
@@ -508,6 +856,11 @@ def print_xml_report(covdata):
508 856
         action="store_true",
509 857
         dest="verbose",
510 858
         default=False)
  859
+parser.add_option('--object-directory',
  860
+        help="Specify the directory that contains the gcov data files.  gcovr must be able to identify the path between the *.gcda files and the directory where gcc was originally run.  Normally, gcovr can guess correctly.  This option overrides gcovr's normal path detection and can specify either the path from gcc to the gcda file (i.e. what was passed to gcc's '-o' option), or the path from the gcda file to gcc's original working directory.",
  861
+        action="store",
  862
+        dest="objdir",
  863
+        default=None)
511 864
 parser.add_option("-o","--output",
512 865
         help="Print output to this filename",
513 866
         action="store",
@@ -533,6 +886,16 @@ def print_xml_report(covdata):
533 886
         action="append",
534 887
         dest="exclude",
535 888
         default=[])
  889
+parser.add_option("--gcov-filter",
  890
+        help="Keep only gcov data files that match this regular expression",
  891
+        action="store",
  892
+        dest="gcov_filter",
  893
+        default=None)
  894
+parser.add_option("--gcov-exclude",
  895
+        help="Exclude gcov data files that match this regular expression",
  896
+        action="append",
  897
+        dest="gcov_exclude",
  898
+        default=[])
536 899
 parser.add_option("-r","--root",
537 900
         help="Defines the root directory.  This is used to filter the files, and to standardize the output.",
538 901
         action="store",
@@ -565,29 +928,47 @@ def print_xml_report(covdata):
565 928
 #
566 929
 (options, args) = parser.parse_args(args=sys.argv)
567 930
 if options.version:
568  
-    print "gcovr "+__version__
569  
-    print ""
570  
-    print "Copyright (2008) Sandia Corporation. Under the terms of Contract "
571  
-    print "DE-AC04-94AL85000 with Sandia Corporation, the U.S. Government "
572  
-    print "retains certain rights in this software."
  931
+    sys.stdout.write(
  932
+        "gcovr %s\n"
  933
+        "\n"
  934
+        "Copyright (2008) Sandia Corporation. Under the terms of Contract\n"
  935
+        "DE-AC04-94AL85000 with Sandia Corporation, the U.S. Government\n"
  936
+        "retains certain rights in this software.\n"
  937
+        % (version_str(),) )
573 938
     sys.exit(0)
  939
+if options.objdir:
  940
+    tmp = options.objdir.replace('/',os.sep).replace('\\',os.sep)
  941
+    while os.sep+os.sep in tmp:
  942
+        tmp = tmp.replace(os.sep+os.sep, os.sep)
  943
+    if normpath(options.objdir) != tmp:
  944
+        sys.stderr.write(
  945
+            "(WARNING) relative referencing in --object-directory.\n"
  946
+            "\tthis could cause strange errors when gcovr attempts to\n"
  947
+            "\tidentify the original gcc working directory.\n")
574 948
 #
575  
-# Setup filter
  949
+# Setup filters
576 950
 #
577 951
 for i in range(0,len(options.exclude)):
578 952
     options.exclude[i] = re.compile(options.exclude[i])
579 953
 if options.filter is not None:
580 954
     options.filter = re.compile(options.filter)
581 955
 elif options.root is not None:
582  
-    #if options.root[0] != os.sep:
583  
-    #    dir=os.getcwd()+os.sep+options.root
584  
-    #    dir=os.path.abspath(dir)
585  
-    #    options.root=dir
586  
-    #else:
587  
-    #    options.root=os.path.abspath(options.root)
588  
-    options.root=os.path.abspath(options.root)
589  
-    #print "HERE",options.root
590  
-    options.filter = re.compile(options.root.replace("\\","\\\\"))
  956
+    if not options.root:
  957
+        sys.stderr.write(
  958
+            "(ERROR) empty --root option.\n"
  959
+            "\tRoot specifies the path to the root directory of your project\n"
  960
+            "\tand cannot be an empty string.\n")
  961
+        sys.exit(1)
  962
+    options.filter = re.compile(re.escape(os.path.abspath(options.root)+os.sep))
  963
+if options.filter is None:
  964
+    options.filter = re.compile('')
  965
+#
  966
+for i in range(0,len(options.gcov_exclude)):
  967
+    options.gcov_exclude[i] = re.compile(options.gcov_exclude[i])
  968
+if options.gcov_filter is not None:
  969
+    options.gcov_filter = re.compile(options.gcov_filter)
  970
+else:
  971
+    options.gcov_filter = re.compile('')
591 972
 #
592 973
 # Get data files
593 974
 #
@@ -602,7 +983,7 @@ def print_xml_report(covdata):
602 983
 for file in datafiles:
603 984
     process_datafile(file,covdata,options)
604 985
 if options.verbose:
605  
-    print "Gathered coveraged data for "+str(len(covdata))+" files"
  986
+    sys.stdout.write("Gathered coveraged data for "+str(len(covdata))+" files\n")
606 987
 #
607 988
 # Print report
608 989
 #

0 notes on commit 5fa4fe3

Please sign in to comment.
Something went wrong with that request. Please try again.