python在永无止境的进程上运行覆盖

时间:2016-09-14 08:23:38

标签: python multithreading python-multiprocessing coverage.py

我有一个多处理的Web服务器,其流程永无止境,我想在实时环境中检查整个项目的代码覆盖率(不仅仅是测试)。

问题是,由于进程永远不会结束,我没有一个设置cov.start() cov.stop() cov.save()挂钩的好地方。

因此,我考虑产生一个线程,在无限循环中保存并组合覆盖数据然后休眠一段时间,但这种方法不起作用,覆盖报告似乎是空的,除了睡眠线

我很乐意收到有关如何获取代码覆盖率的任何想法, 或任何关于为什么我的想法不起作用的建议。以下是我的代码片段:

import coverage
cov = coverage.Coverage()
import time
import threading
import os

class CoverageThread(threading.Thread):
    _kill_now = False
    _sleep_time = 2

@classmethod
def exit_gracefully(cls):
    cls._kill_now = True

def sleep_some_time(self):
    time.sleep(CoverageThread._sleep_time)

def run(self):
    while True:
        cov.start()
        self.sleep_some_time()
        cov.stop()
        if os.path.exists('.coverage'):
            cov.combine()
        cov.save()
        if self._kill_now:
            break
    cov.stop()
    if os.path.exists('.coverage'):
        cov.combine()
    cov.save()
    cov.html_report(directory="coverage_report_data.html")
    print "End of the program. I was killed gracefully :)"

3 个答案:

答案 0 :(得分:8)

显然,使用多个coverage无法很好地控制Threads。 一旦启动了不同的线程,停止Coverage对象将停止所有覆盖,start将仅在“启动”线程中重新启动它。 因此,除Thread之外的所有CoverageThread,您的代码基本上会在2秒后停止覆盖。

我玩了一些API,可以在不停止Coverage对象的情况下访问测量。 因此,您可以使用API​​启动定期保存coverage数据的线程。 第一个实现就像在这个

import threading
from time import sleep
from coverage import Coverage
from coverage.data import CoverageData, CoverageDataFiles
from coverage.files import abs_file

cov = Coverage(config_file=True)
cov.start()


def get_data_dict(d):
    """Return a dict like d, but with keys modified by `abs_file` and
    remove the copied elements from d.
    """
    res = {}
    keys = list(d.keys())
    for k in keys:
        a = {}
        lines = list(d[k].keys())
        for l in lines:
            v = d[k].pop(l)
            a[l] = v
        res[abs_file(k)] = a
    return res


class CoverageLoggerThread(threading.Thread):
    _kill_now = False
    _delay = 2

    def __init__(self, main=True):
        self.main = main
        self._data = CoverageData()
        self._fname = cov.config.data_file
        self._suffix = None
        self._data_files = CoverageDataFiles(basename=self._fname,
                                             warn=cov._warn)
        self._pid = os.getpid()
        super(CoverageLoggerThread, self).__init__()

    def shutdown(self):
        self._kill_now = True

    def combine(self):
        aliases = None
        if cov.config.paths:
            from coverage.aliases import PathAliases
            aliases = PathAliases()
            for paths in self.config.paths.values():
                result = paths[0]
                for pattern in paths[1:]:
                    aliases.add(pattern, result)

        self._data_files.combine_parallel_data(self._data, aliases=aliases)

    def export(self, new=True):
        cov_report = cov
        if new:
            cov_report = Coverage(config_file=True)
            cov_report.load()
        self.combine()
        self._data_files.write(self._data)
        cov_report.data.update(self._data)
        cov_report.html_report(directory="coverage_report_data.html")
        cov_report.report(show_missing=True)

    def _collect_and_export(self):
        new_data = get_data_dict(cov.collector.data)
        if cov.collector.branch:
            self._data.add_arcs(new_data)
        else:
            self._data.add_lines(new_data)
        self._data.add_file_tracers(get_data_dict(cov.collector.file_tracers))
        self._data_files.write(self._data, self._suffix)

        if self.main:
            self.export()

    def run(self):
        while True:
            sleep(CoverageLoggerThread._delay)
            if self._kill_now:
                break

            self._collect_and_export()

        cov.stop()

        if not self.main:
            self._collect_and_export()
            return

        self.export(new=False)
        print("End of the program. I was killed gracefully :)")

在此GIST中可以找到更稳定的版本。 此代码基本上抓取收集器收集的信息而不停止它。 get_data_dict函数获取Coverage.collector中的字典并弹出可用数据。这应该足够安全,这样你就不会丢失任何测量值 报告文件每_delay秒更新一次。

但是如果你有多个进程在运行,你需要额外的努力来确保所有进程都运行CoverageLoggerThread。这是patch_multiprocessing函数,猴子从coverage猴子补丁修补... 代码位于GIST。它基本上用自定义进程替换原始进程,该进程在运行CoverageLoggerThread方法之前启动run并在进程结束时加入线程。 脚本main.py允许使用线程和进程启动不同的测试。

此代码存在2/3的缺点,需要注意:

  • 同时使用combine函数是一个坏主意,因为它对.coverage.*文件执行comcurrent读/写/删除访问。这意味着函数export不是超级安全的。它应该没问题,因为数据被多次复制,但我会在生产中使用之前进行一些测试。

  • 导出数据后,它将保留在内存中。因此,如果代码库很大,它可能会吃掉一些资源。可以转储所有数据并重新加载它但我假设如果要每2秒记录一次,则不希望每次都重新加载所有数据。如果您在几分钟内延迟,我会每次创建一个新的_data,使用CoverageData.read_file重新加载此过程的覆盖范围的先前状态。

  • 自定义流程将在结束前等待_delay,因为我们在流程结束时加入CoverageThreadLogger,因此如果您有大量快速流程,则需要增加粒度睡眠能够更快地检测到过程的结束。它只需要一个在_kill_now上打破的自定义睡眠循环。

请告诉我这是否可以帮助您,或者是否有可能改善这一要点。

修改: 看来你不需要修补多处理模块以自动启动记录器。在python安装中使用.pth,您可以使用环境变量自动启动新进程的记录器:

# Content of coverage.pth in your site-package folder
import os
if "COVERAGE_LOGGER_START" in os.environ:
    import atexit
    from coverage_logger import CoverageLoggerThread
    thread_cov = CoverageLoggerThread(main=False)
    thread_cov.start()
    def close_cov()
        thread_cov.shutdown()
        thread_cov.join()
    atexit.register(close_cov)

然后,您可以使用COVERAGE_LOGGER_START=1 python main.y

启动覆盖记录器

答案 1 :(得分:3)

由于您愿意为测试运行不同的代码,为什么不添加一种方法来结束测试过程?这似乎比试图破解报道更简单。

答案 2 :(得分:1)

您可以使用以下两个程序直接使用pyrasite

# start.py
import sys
import coverage

sys.cov = cov = coverage.coverage()
cov.start()

这一个

# stop.py
import sys

sys.cov.stop()
sys.cov.save()
sys.cov.html_report()

另一种方法是使用lptrace跟踪程序,即使它只打印它可能有用的调用。