def __init__(self, report, source=None, source_prefix=None): """ Initialize a Cobertura report given a coverage report `report`. It can be either a file object or the path to an XML file that is in the Cobertura format. The optional argument `source` is the location of the source code provided as a directory path or a file object zip archive containing the source code. The optional argument `source_prefix` will be used to lookup source files if a zip archive is provided and will be prepended to filenames found in the coverage report. """ self.xml = ET.parse(report).getroot() if source is None: if isinstance(report, basestring): # get the directory in which the coverage file lives source = os.path.dirname(report) self.filesystem = DirectoryFileSystem( source, source_prefix=source_prefix ) elif zipfile.is_zipfile(source): self.filesystem = ZipFileSystem( source, source_prefix=source_prefix ) else: self.filesystem = DirectoryFileSystem( source, source_prefix=source_prefix )
def test_filesystem_directory__returns_fileobject(): from pycobertura.filesystem import DirectoryFileSystem fs = DirectoryFileSystem('tests/dummy') expected_filepaths = { 'dummy/dummy.py': 'dummy/dummy/dummy.py', } for filename in expected_filepaths: with fs.open(filename) as f: assert hasattr(f, 'read')
def test_filesystem_directory__with_source_prefix(): from pycobertura.filesystem import DirectoryFileSystem fs = DirectoryFileSystem( 'tests/', source_prefix='dummy' # should resolve to tests/dummy/ ) expected_filepaths = { 'dummy/dummy.py': 'dummy/dummy/dummy.py', } for filename in expected_filepaths: with fs.open(filename) as f: assert hasattr(f, 'read')
def test_filesystem_directory__file_not_found(): from pycobertura.filesystem import DirectoryFileSystem fs = DirectoryFileSystem('foo/bar/baz') expected_filepaths = { 'Main.java': 'foo/bar/baz/Main.java', 'search/BinarySearch.java': 'foo/bar/baz/search/BinarySearch.java', 'search/ISortedArraySearch.java': 'foo/bar/baz/search/ISortedArraySearch.java', 'search/LinearSearch.java': 'foo/bar/baz/search/LinearSearch.java', } for filename in expected_filepaths: try: with fs.open(filename) as f: pass except DirectoryFileSystem.FileNotFound as fnf: assert fnf.path == expected_filepaths[filename]
class Cobertura(object): """ An XML Cobertura parser. """ def __init__(self, report, source=None, source_prefix=None): """ Initialize a Cobertura report given a coverage report `report`. It can be either a file object or the path to an XML file that is in the Cobertura format. The optional argument `source` is the location of the source code provided as a directory path or a file object zip archive containing the source code. The optional argument `source_prefix` will be used to lookup source files if a zip archive is provided and will be prepended to filenames found in the coverage report. """ self.xml = ET.parse(report).getroot() if source is None: if isinstance(report, basestring): # get the directory in which the coverage file lives source = os.path.dirname(report) self.filesystem = DirectoryFileSystem( source, source_prefix=source_prefix ) elif zipfile.is_zipfile(source): self.filesystem = ZipFileSystem( source, source_prefix=source_prefix ) else: self.filesystem = DirectoryFileSystem( source, source_prefix=source_prefix ) @memoize def _get_class_element_by_filename(self, filename): syntax = "./packages/package/classes/class[@filename='%s'][1]" % ( filename ) return self.xml.xpath(syntax)[0] @memoize def _get_lines_by_filename(self, filename): el = self._get_class_element_by_filename(filename) return el.xpath('./lines/line') @property def version(self): """Return the version number of the coverage report.""" return self.xml.attrib['version'] def line_rate(self, filename=None): """ Return the global line rate of the coverage report. If the `filename` file is given, return the line rate of the file. """ if filename is None: el = self.xml else: el = self._get_class_element_by_filename(filename) return float(el.attrib['line-rate']) def branch_rate(self, filename=None): """ Return the global branch rate of the coverage report. If the `filename` file is given, return the branch rate of the file. """ if filename is None: el = self.xml else: el = self._get_class_element_by_filename(filename) return float(el.attrib['branch-rate']) @memoize def missed_statements(self, filename): """ Return a list of uncovered line numbers for each of the missed statements found for the file `filename`. """ el = self._get_class_element_by_filename(filename) lines = el.xpath('./lines/line[@hits=0]') return [int(l.attrib['number']) for l in lines] @memoize def hit_statements(self, filename): """ Return a list of covered line numbers for each of the hit statements found for the file `filename`. """ el = self._get_class_element_by_filename(filename) lines = el.xpath('./lines/line[@hits>0]') return [int(l.attrib['number']) for l in lines] def line_statuses(self, filename): """ Return a list of tuples `(lineno, status)` of all the lines found in the Cobertura report for the given file `filename` where `lineno` is the line number and `status` is coverage status of the line which can be either `True` (line hit) or `False` (line miss). """ line_elements = self._get_lines_by_filename(filename) lines_w_status = [] for line in line_elements: lineno = int(line.attrib['number']) status = line.attrib['hits'] != '0' lines_w_status.append((lineno, status)) return lines_w_status def missed_lines(self, filename): """ Return a list of extrapolated uncovered line numbers for the file `filename` according to `Cobertura.line_statuses`. """ statuses = self.line_statuses(filename) statuses = extrapolate_coverage(statuses) return [lno for lno, status in statuses if status is False] @memoize def file_source(self, filename): """ Return a list of namedtuple `Line` for each line of code found in the source file with the given `filename`. """ lines = [] try: with self.filesystem.open(filename) as f: line_statuses = dict(self.line_statuses(filename)) for lineno, source in enumerate(f, start=1): line_status = line_statuses.get(lineno) line = Line(lineno, source, line_status, None) lines.append(line) except self.filesystem.FileNotFound as file_not_found: lines.append( Line(0, '%s not found' % file_not_found.path, None, None) ) return lines def total_misses(self, filename=None): """ Return the total number of uncovered statements for the file `filename`. If `filename` is not given, return the total number of uncovered statements for all files. """ if filename is not None: return len(self.missed_statements(filename)) total = 0 for filename in self.files(): total += len(self.missed_statements(filename)) return total def total_hits(self, filename=None): """ Return the total number of covered statements for the file `filename`. If `filename` is not given, return the total number of covered statements for all files. """ if filename is not None: return len(self.hit_statements(filename)) total = 0 for filename in self.files(): total += len(self.hit_statements(filename)) return total def total_statements(self, filename=None): """ Return the total number of statements for the file `filename`. If `filename` is not given, return the total number of statements for all files. """ if filename is not None: statements = self._get_lines_by_filename(filename) return len(statements) total = 0 for filename in self.files(): statements = self._get_lines_by_filename(filename) total += len(statements) return total @memoize def files(self): """ Return the list of available files in the coverage report. """ return [el.attrib['filename'] for el in self.xml.xpath("//class")] def has_file(self, filename): """ Return `True` if the file `filename` is present in the report, return `False` otherwise. """ # FIXME: this will lookup a list which is slow, make it O(1) return filename in self.files() @memoize def source_lines(self, filename): """ Return a list for source lines of file `filename`. """ with self.filesystem.open(filename) as f: return f.readlines() @memoize def packages(self): """ Return the list of available packages in the coverage report. """ return [el.attrib['name'] for el in self.xml.xpath("//package")]