Source code for bugzoo.core.coverage

__all__ = ['CoverageInstructions', 'TestCoverage', 'TestSuiteCoverage']

from typing import (Dict, List, Set, Iterator, Any, Iterable, FrozenSet, Type,
                    Optional, Mapping)
from collections import OrderedDict
import logging

import yaml
import attr

from .language import Language
from .fileline import FileLine, FileLineSet
from .test import TestSuite, TestOutcome
from ..util import indent
from .. import exceptions

logger = logging.getLogger(__name__)  # type: logging.Logger
logger.setLevel(logging.DEBUG)

_NAME_TO_INSTRUCTIONS = {}  # type: Dict[str, Type[CoverageInstructions]]
_INSTRUCTIONS_TO_NAME = {}  # type: Dict[Type[CoverageInstructions], str]
_LANGUAGE_TO_DEFAULT_INSTRUCTIONS = \
    {}  # type: Dict[Language, Type[CoverageInstructions]]


def _convert_files_to_instrument(files: Iterable[str]) -> FrozenSet[str]:
    return frozenset(files)


class CoverageInstructions:
    """Instructions for computing coverage."""
    @classmethod
    def registered_under_name(cls) -> str:
        """Returns the name that was used to register this class."""
        return _INSTRUCTIONS_TO_NAME[cls]

    @staticmethod
    def language_default(language: Language
                         ) -> Optional[Type['CoverageInstructions']]:
        """
        Returns the default coverage instructions class for a given language,
        if such a class has been registered for that language.
        """
        return _LANGUAGE_TO_DEFAULT_INSTRUCTIONS.get(language)

    @staticmethod
    def find(name: str) -> Type['CoverageInstructions']:
        """
        Retrieves the coverage instructions class registered for a given name.
        """
        return _NAME_TO_INSTRUCTIONS[name]

    @classmethod
    def register(cls, name: str) -> None:
        logger.debug("registering coverage instructions [%s] under name [%s]",
                     cls, name)
        if name in _NAME_TO_INSTRUCTIONS:
            raise exceptions.NameInUseError(name)
        if cls in _INSTRUCTIONS_TO_NAME:
            m = "coverage instructions already registered under name: {}"
            m = m.format(_INSTRUCTIONS_TO_NAME[cls])
            raise Exception(m)  # FIXME add new error class
        _NAME_TO_INSTRUCTIONS[name] = cls
        _INSTRUCTIONS_TO_NAME[cls] = name
        logger.debug("registered coverage instructions [%s] under name [%s]",
                     cls, name)

    @classmethod
    def register_as_default(cls, language: Language) -> None:
        logger.debug("registering coverage instructions [%s] as default for %s",  # noqa: pycodestyle
                     cls, language)

        if language in _LANGUAGE_TO_DEFAULT_INSTRUCTIONS:
            m = "language [{}] already has a default coverage instructions: {}"
            m = m.format(language.name,
                         _LANGUAGE_TO_DEFAULT_INSTRUCTIONS[language])
            raise Exception(m)

        if cls not in _INSTRUCTIONS_TO_NAME:
            m = "coverage instructions [{}] have not been registered"
            m = m.format(cls)
            raise Exception(m)
        name = _INSTRUCTIONS_TO_NAME[cls]

        _LANGUAGE_TO_DEFAULT_INSTRUCTIONS[language] = cls
        logger.debug("registered coverage instructions [%s] as default for %s",
                     name, language)

    @staticmethod
    def from_dict(d: Dict[str, Any]) -> 'CoverageInstructions':
        """
        Loads a set of coverage instructions from a given dictionary.

        Raises:
            BadCoverageInstructions: if the given coverage instructions are
                illegal.
        """
        name_type = d['type']
        cls = _NAME_TO_INSTRUCTIONS[name_type]
        return cls.from_dict(d)

    def to_dict(self) -> Dict[str, Any]:
        raise NotImplementedError


[docs]class TestCoverage: """ Provides complete line coverage information for all files and across all tests within a given project. """ @staticmethod def from_dict(d: dict) -> 'TestCoverage': """ Example Input: { "test": "p1", "outcome": { }, "coverage": { "foo.c": [1, 2, 6, 10] } } """ assert 'test' in d assert 'outcome' in d assert 'coverage' in d test = d['test'] outcome = TestOutcome.from_dict(d['outcome']) coverage = FileLineSet.from_dict(d['coverage']) return TestCoverage(test, outcome, coverage) def __init__(self, test: str, outcome: TestOutcome, coverage: FileLineSet ) -> None: self.__test = test self.__outcome = outcome self.__coverage = coverage def __repr__(self) -> str: coverage = repr(self.__coverage) coverage = indent(coverage, 2) status = 'PASSED' if self.__outcome.passed else 'FAILED' s = "[{}: {}]\n{}".format(self.__test, status, coverage) return s def __contains__(self, fileline: FileLine) -> bool: return fileline in self.__coverage @property def test(self) -> str: """Name of the test case used to generate this coverage.""" return self.__test @property def outcome(self) -> TestOutcome: """Outcome of the test associated with this coverage.""" return self.__outcome @property def lines(self) -> FileLineSet: """Set of file-lines that were covered.""" return self.__coverage coverage = lines def restricted_to_files(self, filenames: List[str]) -> 'TestCoverage': """ Returns a variant of this coverage that is restricted to a given list of files. """ return TestCoverage(self.__test, self.__outcome, self.__coverage.restricted_to_files(filenames)) def to_dict(self) -> dict: return {'test': self.__test, 'outcome': self.__outcome.to_dict(), 'coverage': self.__coverage.to_dict()}
[docs]class TestSuiteCoverage(Mapping[str, TestCoverage]): """ Holds coverage information for all tests belonging to a particular program version. """ @staticmethod def from_dict(d: dict) -> 'TestSuiteCoverage': coverage_by_test = {} for test_coverage_dict in d.values(): test_coverage = TestCoverage.from_dict(test_coverage_dict) coverage_by_test[test_coverage.test] = test_coverage return TestSuiteCoverage(coverage_by_test) @staticmethod def from_file(fn: str) -> 'TestSuiteCoverage': with open(fn, 'r') as f: d = yaml.safe_load(f) return TestSuiteCoverage.from_dict(d) def __init__(self, test_coverage: Mapping[str, TestCoverage]) -> None: self.__test_coverage = \ OrderedDict() # type: OrderedDict[str, TestCoverage] for test_name in sorted(test_coverage): self.__test_coverage[test_name] = test_coverage[test_name] def __repr__(self) -> str: output = [repr(self[name_test]) for name_test in self] return '\n'.join(output) def covering_tests(self, line: FileLine) -> Set[str]: """Returns the names of all test cases that cover a given line.""" return set(test for (test, cov) in self.__test_coverage.items() \ if line in cov) def __iter__(self) -> Iterator[str]: """ Returns an iterator over the names of the test cases that are represented by this coverage report. """ return self.__test_coverage.keys().__iter__()
[docs] def __getitem__(self, name: str) -> TestCoverage: """Retrieves coverage information for a given test case. Parameters: name: the name of the test case. Raises: KeyError: if there is no coverage information for the given test case. """ return self.__test_coverage[name]
def to_dict(self) -> dict: return {test: cov.to_dict() \ for (test, cov) in self.__test_coverage.items()} def restricted_to_files(self, filenames: List[str] ) -> 'TestSuiteCoverage': """ Returns a variant of this coverage that is restricted to a given list of files. """ cov_suite = {} for test in self: cov_suite[test] = self[test].restricted_to_files(filenames) return TestSuiteCoverage(cov_suite) @property def failing(self) -> 'TestSuiteCoverage': """ Returns a variant of this coverage report that only contains coverage for failing test executions. """ return TestSuiteCoverage({t: cov \ for (t, cov) in self.__test_coverage.items() \ if not cov.outcome.passed}) @property def passing(self) -> 'TestSuiteCoverage': """ Returns a variant of this coverage report that only contains coverage for failing test executions. """ return TestSuiteCoverage({t: cov \ for (t, cov) in self.__test_coverage.items() \ if cov.outcome.passed}) def __len__(self) -> int: """ Returns a count of the number of test executions that are included within this coverage report. """ return len(self.__test_coverage) @property def lines(self) -> Set[FileLine]: """Returns the set of all file lines that were covered.""" assert len(self) > 0 output = FileLineSet() # type: Set[FileLine] for coverage in self.__test_coverage.values(): output = output.union(coverage.lines) return output