[ Web Proxy ]
URL:
Viewing: https://raw.githubusercontent.com/GiacomoPope/python-flint/l_function_name/coverage_plugin.py [Back]  [Original]

"""
A Cython plugin for coverage.py suitable for a spin/meson project.

This follows the same general approach as Cython's coverage plugin and uses the
Cython plugin for parsing the C files. The difference here is that files are
laid out very differently in a meson project. Assuming meson makes it a lot
easier to find all the C files because we can just parse the build.ninja file.

https://coverage.readthedocs.io/en/latest/api_plugin.html
https://github.com/cython/cython/blob/master/Cython/Coverage.py
"""
import re
from collections import defaultdict

from coverage.plugin import CoveragePlugin, FileTracer, FileReporter

from functools import cache
from pathlib import Path


# Paths used by spin/meson in a src-layout:
root_dir = Path(__file__).parent
build_dir = root_dir / 'build'
build_install_dir = root_dir / 'build-install'
src_dir = root_dir / 'src'


def get_ninja_build_rules():
    """Read all build rules from build.ninja."""
    rules = []
    with open(build_dir / 'build.ninja') as build_ninja:
        for line in build_ninja:
            line = line.strip()
            if line.startswith('build '):
                line = line[len('build '):]
                target, rule = line.split(': ')
                if target == 'PHONY':
                    continue
                compiler, *srcfiles = rule.split(' ')
                # target is a path relative to the build directory. We will
                # turn that into an absolute path so that all paths in target
                # and srcfiles are absolute.
                target = str(build_dir / target)
                rule = (target, compiler, srcfiles)
                rules.append(rule)
    return rules


def get_cython_build_rules():
    """Get all Cython build rules."""
    cython_rules = []

    for target, compiler, srcfiles in get_ninja_build_rules():
        if compiler == 'cython_COMPILER':
            assert target.endswith('.c')
            assert len(srcfiles) == 1 and srcfiles[0].endswith('.pyx')
            c_file = target
            [cython_file] = srcfiles
            cython_rules.append((c_file, cython_file))

    return cython_rules


@cache
def parse_all_cfile_lines():
    """Parse all generated C files from the build directory."""
    #
    # Each .c file can include code generated from multiple Cython files (e.g.
    # because of .pxd files) being cimported. Each Cython file can contribute
    # to more than one .c file. Here we parse all .c files and then collect
    # together all the executable lines from all of the Cython files into a
    # dict like this:
    #
    #   {filename: {lineno: linestr, ...}, ...}
    #
    # This function is cached because it only needs calling once and is
    # expensive.
    #
    all_code_lines = {}

    for c_file, _ in get_cython_build_rules():

        cfile_lines = parse_cfile_lines(c_file)

        for cython_file, line_map in cfile_lines.items():
            if cython_file == '(tree fragment)':
                continue
            elif cython_file in all_code_lines:
                # Possibly need to merge the lines?
                assert all_code_lines[cython_file] == line_map
            else:
                all_code_lines[cython_file] = line_map

    return all_code_lines


def parse_cfile_lines(c_file):
    """Use Cython's coverage plugin to parse the C code."""
    from Cython.Coverage import Plugin
    return Plugin()._parse_cfile_lines(c_file)


class Plugin(CoveragePlugin):
    """A coverage plugin for a spin/meson project with Cython code."""

    def file_tracer(self, filename):
        """Find a tracer for filename to handle trace events."""
        path = Path(filename)

        if path.suffix in ('.pyx', '.pxd') and root_dir in path.parents:
            # A .pyx file from the src directory. The path has src
            # stripped out and is not a real absolute path but it looks
            # like one. Remove the root prefix and then we have a path
            # relative to src_dir.
            srcpath = path.relative_to(root_dir)
            return CyFileTracer(srcpath)
        else:
            # All sorts of paths come here and we reject them
            return None

    def file_reporter(self, filename):
        """Return a file reporter for filename."""
        srcfile = Path(filename).relative_to(src_dir)
        return CyFileReporter(srcfile)


class CyFileTracer(FileTracer):
    """File tracer for Cython files (.pyx,.pxd)."""

    def __init__(self, srcpath):
        assert (src_dir / srcpath).exists()
        self.srcpath = srcpath

    def source_filename(self):
        return self.srcpath

    def has_dynamic_source_filename(self):
        return True

    def dynamic_source_filename(self, filename, frame):
        """Get filename from frame and return abspath to file."""
        # What is returned here needs to match CyFileReporter.filename
        path = frame.f_code.co_filename
        return self.get_source_filename(path)

    # This is called for every traced line. Cache it:
    @staticmethod
    @cache
    def get_source_filename(filename):
        """Get src-relative path for filename from trace event."""
        path = src_dir / filename
        assert src_dir in path.parents
        assert path.exists()
        return str(path)


class CyFileReporter(FileReporter):
    """File reporter for Cython or Python files (.pyx,.pxd,.py)."""

    def __init__(self, srcpath):
        abspath = (src_dir / srcpath)
        assert abspath.exists()

        # filepath here needs to match dynamic_source_filename
        super().__init__(str(abspath))

        self.srcpath = srcpath

    def relative_filename(self):
        """Path displayed in the coverage reports."""
        return str(self.srcpath)

    def lines(self):
        """Set of line numbers for possibly traceable lines."""
        srcpath = str(self.srcpath)
        all_line_maps = parse_all_cfile_lines()
        line_map = all_line_maps[srcpath]
        return set(line_map)


def coverage_init(reg, options):
    plugin = Plugin()
    reg.add_configurer(plugin)
    reg.add_file_tracer(plugin)

Web Proxy Viewer  |  New URL  |  Original Page