You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			287 lines
		
	
	
		
			9.8 KiB
		
	
	
	
		
			Python
		
	
			
		
		
	
	
			287 lines
		
	
	
		
			9.8 KiB
		
	
	
	
		
			Python
		
	
"""Results of coverage measurement."""
 | 
						|
 | 
						|
import os
 | 
						|
 | 
						|
from coverage.backward import iitems, set, sorted       # pylint: disable=W0622
 | 
						|
from coverage.misc import format_lines, join_regex, NoSource
 | 
						|
from coverage.parser import CodeParser
 | 
						|
 | 
						|
 | 
						|
class Analysis(object):
 | 
						|
    """The results of analyzing a code unit."""
 | 
						|
 | 
						|
    def __init__(self, cov, code_unit):
 | 
						|
        self.coverage = cov
 | 
						|
        self.code_unit = code_unit
 | 
						|
 | 
						|
        self.filename = self.code_unit.filename
 | 
						|
        actual_filename, source = self.find_source(self.filename)
 | 
						|
 | 
						|
        self.parser = CodeParser(
 | 
						|
            text=source, filename=actual_filename,
 | 
						|
            exclude=self.coverage._exclude_regex('exclude')
 | 
						|
            )
 | 
						|
        self.statements, self.excluded = self.parser.parse_source()
 | 
						|
 | 
						|
        # Identify missing statements.
 | 
						|
        executed = self.coverage.data.executed_lines(self.filename)
 | 
						|
        exec1 = self.parser.first_lines(executed)
 | 
						|
        self.missing = sorted(set(self.statements) - set(exec1))
 | 
						|
 | 
						|
        if self.coverage.data.has_arcs():
 | 
						|
            self.no_branch = self.parser.lines_matching(
 | 
						|
                join_regex(self.coverage.config.partial_list),
 | 
						|
                join_regex(self.coverage.config.partial_always_list)
 | 
						|
                )
 | 
						|
            n_branches = self.total_branches()
 | 
						|
            mba = self.missing_branch_arcs()
 | 
						|
            n_partial_branches = sum(
 | 
						|
                [len(v) for k,v in iitems(mba) if k not in self.missing]
 | 
						|
                )
 | 
						|
            n_missing_branches = sum([len(v) for k,v in iitems(mba)])
 | 
						|
        else:
 | 
						|
            n_branches = n_partial_branches = n_missing_branches = 0
 | 
						|
            self.no_branch = set()
 | 
						|
 | 
						|
        self.numbers = Numbers(
 | 
						|
            n_files=1,
 | 
						|
            n_statements=len(self.statements),
 | 
						|
            n_excluded=len(self.excluded),
 | 
						|
            n_missing=len(self.missing),
 | 
						|
            n_branches=n_branches,
 | 
						|
            n_partial_branches=n_partial_branches,
 | 
						|
            n_missing_branches=n_missing_branches,
 | 
						|
            )
 | 
						|
 | 
						|
    def find_source(self, filename):
 | 
						|
        """Find the source for `filename`.
 | 
						|
 | 
						|
        Returns two values: the actual filename, and the source.
 | 
						|
 | 
						|
        The source returned depends on which of these cases holds:
 | 
						|
 | 
						|
            * The filename seems to be a non-source file: returns None
 | 
						|
 | 
						|
            * The filename is a source file, and actually exists: returns None.
 | 
						|
 | 
						|
            * The filename is a source file, and is in a zip file or egg:
 | 
						|
              returns the source.
 | 
						|
 | 
						|
            * The filename is a source file, but couldn't be found: raises
 | 
						|
              `NoSource`.
 | 
						|
 | 
						|
        """
 | 
						|
        source = None
 | 
						|
 | 
						|
        base, ext = os.path.splitext(filename)
 | 
						|
        TRY_EXTS = {
 | 
						|
            '.py':  ['.py', '.pyw'],
 | 
						|
            '.pyw': ['.pyw'],
 | 
						|
        }
 | 
						|
        try_exts = TRY_EXTS.get(ext)
 | 
						|
        if not try_exts:
 | 
						|
            return filename, None
 | 
						|
 | 
						|
        for try_ext in try_exts:
 | 
						|
            try_filename = base + try_ext
 | 
						|
            if os.path.exists(try_filename):
 | 
						|
                return try_filename, None
 | 
						|
            source = self.coverage.file_locator.get_zip_data(try_filename)
 | 
						|
            if source:
 | 
						|
                return try_filename, source
 | 
						|
        raise NoSource("No source for code: '%s'" % filename)
 | 
						|
 | 
						|
    def missing_formatted(self):
 | 
						|
        """The missing line numbers, formatted nicely.
 | 
						|
 | 
						|
        Returns a string like "1-2, 5-11, 13-14".
 | 
						|
 | 
						|
        """
 | 
						|
        return format_lines(self.statements, self.missing)
 | 
						|
 | 
						|
    def has_arcs(self):
 | 
						|
        """Were arcs measured in this result?"""
 | 
						|
        return self.coverage.data.has_arcs()
 | 
						|
 | 
						|
    def arc_possibilities(self):
 | 
						|
        """Returns a sorted list of the arcs in the code."""
 | 
						|
        arcs = self.parser.arcs()
 | 
						|
        return arcs
 | 
						|
 | 
						|
    def arcs_executed(self):
 | 
						|
        """Returns a sorted list of the arcs actually executed in the code."""
 | 
						|
        executed = self.coverage.data.executed_arcs(self.filename)
 | 
						|
        m2fl = self.parser.first_line
 | 
						|
        executed = [(m2fl(l1), m2fl(l2)) for (l1,l2) in executed]
 | 
						|
        return sorted(executed)
 | 
						|
 | 
						|
    def arcs_missing(self):
 | 
						|
        """Returns a sorted list of the arcs in the code not executed."""
 | 
						|
        possible = self.arc_possibilities()
 | 
						|
        executed = self.arcs_executed()
 | 
						|
        missing = [
 | 
						|
            p for p in possible
 | 
						|
                if p not in executed
 | 
						|
                    and p[0] not in self.no_branch
 | 
						|
            ]
 | 
						|
        return sorted(missing)
 | 
						|
 | 
						|
    def arcs_unpredicted(self):
 | 
						|
        """Returns a sorted list of the executed arcs missing from the code."""
 | 
						|
        possible = self.arc_possibilities()
 | 
						|
        executed = self.arcs_executed()
 | 
						|
        # Exclude arcs here which connect a line to itself.  They can occur
 | 
						|
        # in executed data in some cases.  This is where they can cause
 | 
						|
        # trouble, and here is where it's the least burden to remove them.
 | 
						|
        unpredicted = [
 | 
						|
            e for e in executed
 | 
						|
                if e not in possible
 | 
						|
                    and e[0] != e[1]
 | 
						|
            ]
 | 
						|
        return sorted(unpredicted)
 | 
						|
 | 
						|
    def branch_lines(self):
 | 
						|
        """Returns a list of line numbers that have more than one exit."""
 | 
						|
        exit_counts = self.parser.exit_counts()
 | 
						|
        return [l1 for l1,count in iitems(exit_counts) if count > 1]
 | 
						|
 | 
						|
    def total_branches(self):
 | 
						|
        """How many total branches are there?"""
 | 
						|
        exit_counts = self.parser.exit_counts()
 | 
						|
        return sum([count for count in exit_counts.values() if count > 1])
 | 
						|
 | 
						|
    def missing_branch_arcs(self):
 | 
						|
        """Return arcs that weren't executed from branch lines.
 | 
						|
 | 
						|
        Returns {l1:[l2a,l2b,...], ...}
 | 
						|
 | 
						|
        """
 | 
						|
        missing = self.arcs_missing()
 | 
						|
        branch_lines = set(self.branch_lines())
 | 
						|
        mba = {}
 | 
						|
        for l1, l2 in missing:
 | 
						|
            if l1 in branch_lines:
 | 
						|
                if l1 not in mba:
 | 
						|
                    mba[l1] = []
 | 
						|
                mba[l1].append(l2)
 | 
						|
        return mba
 | 
						|
 | 
						|
    def branch_stats(self):
 | 
						|
        """Get stats about branches.
 | 
						|
 | 
						|
        Returns a dict mapping line numbers to a tuple:
 | 
						|
        (total_exits, taken_exits).
 | 
						|
        """
 | 
						|
 | 
						|
        exit_counts = self.parser.exit_counts()
 | 
						|
        missing_arcs = self.missing_branch_arcs()
 | 
						|
        stats = {}
 | 
						|
        for lnum in self.branch_lines():
 | 
						|
            exits = exit_counts[lnum]
 | 
						|
            try:
 | 
						|
                missing = len(missing_arcs[lnum])
 | 
						|
            except KeyError:
 | 
						|
                missing = 0
 | 
						|
            stats[lnum] = (exits, exits - missing)
 | 
						|
        return stats
 | 
						|
 | 
						|
 | 
						|
class Numbers(object):
 | 
						|
    """The numerical results of measuring coverage.
 | 
						|
 | 
						|
    This holds the basic statistics from `Analysis`, and is used to roll
 | 
						|
    up statistics across files.
 | 
						|
 | 
						|
    """
 | 
						|
    # A global to determine the precision on coverage percentages, the number
 | 
						|
    # of decimal places.
 | 
						|
    _precision = 0
 | 
						|
    _near0 = 1.0              # These will change when _precision is changed.
 | 
						|
    _near100 = 99.0
 | 
						|
 | 
						|
    def __init__(self, n_files=0, n_statements=0, n_excluded=0, n_missing=0,
 | 
						|
                    n_branches=0, n_partial_branches=0, n_missing_branches=0
 | 
						|
                    ):
 | 
						|
        self.n_files = n_files
 | 
						|
        self.n_statements = n_statements
 | 
						|
        self.n_excluded = n_excluded
 | 
						|
        self.n_missing = n_missing
 | 
						|
        self.n_branches = n_branches
 | 
						|
        self.n_partial_branches = n_partial_branches
 | 
						|
        self.n_missing_branches = n_missing_branches
 | 
						|
 | 
						|
    def set_precision(cls, precision):
 | 
						|
        """Set the number of decimal places used to report percentages."""
 | 
						|
        assert 0 <= precision < 10
 | 
						|
        cls._precision = precision
 | 
						|
        cls._near0 = 1.0 / 10**precision
 | 
						|
        cls._near100 = 100.0 - cls._near0
 | 
						|
    set_precision = classmethod(set_precision)
 | 
						|
 | 
						|
    def _get_n_executed(self):
 | 
						|
        """Returns the number of executed statements."""
 | 
						|
        return self.n_statements - self.n_missing
 | 
						|
    n_executed = property(_get_n_executed)
 | 
						|
 | 
						|
    def _get_n_executed_branches(self):
 | 
						|
        """Returns the number of executed branches."""
 | 
						|
        return self.n_branches - self.n_missing_branches
 | 
						|
    n_executed_branches = property(_get_n_executed_branches)
 | 
						|
 | 
						|
    def _get_pc_covered(self):
 | 
						|
        """Returns a single percentage value for coverage."""
 | 
						|
        if self.n_statements > 0:
 | 
						|
            pc_cov = (100.0 * (self.n_executed + self.n_executed_branches) /
 | 
						|
                        (self.n_statements + self.n_branches))
 | 
						|
        else:
 | 
						|
            pc_cov = 100.0
 | 
						|
        return pc_cov
 | 
						|
    pc_covered = property(_get_pc_covered)
 | 
						|
 | 
						|
    def _get_pc_covered_str(self):
 | 
						|
        """Returns the percent covered, as a string, without a percent sign.
 | 
						|
 | 
						|
        Note that "0" is only returned when the value is truly zero, and "100"
 | 
						|
        is only returned when the value is truly 100.  Rounding can never
 | 
						|
        result in either "0" or "100".
 | 
						|
 | 
						|
        """
 | 
						|
        pc = self.pc_covered
 | 
						|
        if 0 < pc < self._near0:
 | 
						|
            pc = self._near0
 | 
						|
        elif self._near100 < pc < 100:
 | 
						|
            pc = self._near100
 | 
						|
        else:
 | 
						|
            pc = round(pc, self._precision)
 | 
						|
        return "%.*f" % (self._precision, pc)
 | 
						|
    pc_covered_str = property(_get_pc_covered_str)
 | 
						|
 | 
						|
    def pc_str_width(cls):
 | 
						|
        """How many characters wide can pc_covered_str be?"""
 | 
						|
        width = 3   # "100"
 | 
						|
        if cls._precision > 0:
 | 
						|
            width += 1 + cls._precision
 | 
						|
        return width
 | 
						|
    pc_str_width = classmethod(pc_str_width)
 | 
						|
 | 
						|
    def __add__(self, other):
 | 
						|
        nums = Numbers()
 | 
						|
        nums.n_files = self.n_files + other.n_files
 | 
						|
        nums.n_statements = self.n_statements + other.n_statements
 | 
						|
        nums.n_excluded = self.n_excluded + other.n_excluded
 | 
						|
        nums.n_missing = self.n_missing + other.n_missing
 | 
						|
        nums.n_branches = self.n_branches + other.n_branches
 | 
						|
        nums.n_partial_branches = (
 | 
						|
            self.n_partial_branches + other.n_partial_branches
 | 
						|
            )
 | 
						|
        nums.n_missing_branches = (
 | 
						|
            self.n_missing_branches + other.n_missing_branches
 | 
						|
            )
 | 
						|
        return nums
 | 
						|
 | 
						|
    def __radd__(self, other):
 | 
						|
        # Implementing 0+Numbers allows us to sum() a list of Numbers.
 | 
						|
        if other == 0:
 | 
						|
            return self
 | 
						|
        return NotImplemented
 |