C++ PyImport 的 Python 覆盖率

Posted

技术标签:

【中文标题】C++ PyImport 的 Python 覆盖率【英文标题】:Python Coverage for C++ PyImport 【发布时间】:2020-03-27 18:35:10 【问题描述】:

情况: 我正在尝试获取当前项目中所有 python 代码的覆盖率报告。在大多数情况下,我使用 Coverage.py 取得了巨大成功。目前我像this 一样使用它,利用了sitecustomize.py 进程。对于从命令行启动的所有内容,它的效果非常好。

问题: 我无法通过 PyImport_Import() 类型语句从 C++ 运行 python 模块来实际跟踪和输出覆盖率数据。

示例:

[test.cpp]

#include <stdio.h>
#include <iostream>
#include <Python.h>
int main()

    Py_Initialize();
    PyObject* sysPath = PySys_GetObject("path");
    PyList_Append(sysPath, PyString_FromString("."));
    // Load the module
    PyObject *pName = PyString_FromString("test_mod");
    PyObject *pModule = PyImport_Import(pName);
    if (pModule != NULL) 
        std::cout << "Python module found\n";
        // Load all module level attributes as a dictionary
        PyObject *pDict = PyModule_GetDict(pModule);
        PyObject *pFunc = PyObject_GetAttrString(pModule, "getInteger");
        if(pFunc)
        
            if(PyCallable_Check(pFunc))
            
                PyObject *pValue = PyObject_CallObject(pFunc, NULL);
                std::cout << PyLong_AsLong(pValue) << std::endl;
            
            else
            
                printf("ERROR: function getInteger()\n");
            

        
        else
        
            printf("ERROR: pFunc is NULL\n");
        
    
    else
        std::cout << "Python Module not found\n";
    return 0;

[test_mod.py]

#!/bin/python
def getInteger():
     print('Python function getInteger() called')
     c = 100*50/30
     return c
print('Randomness')

输出: 如果我手动运行 test_mod.py 它会按预期输出。但是,如果我运行编译后的 test.cpp 二进制文件,它不会输出任何覆盖数据。我知道 sitecustomize.py 仍然受到攻击,因为我添加了一些调试以确保我不会发疯。我还可以在覆盖调试日志中看到它确实想要跟踪模块..

[cov.log]

New process: executable: /usr/bin/python
New process: cmd: ???
New process: parent pid: 69073
-- config ----------------------------------------------------
_include: None
_omit: None
attempted_config_files: /tmp/.coveragerc
branch: True
concurrency: thread
multiprocessing
config_files: /tmp/.coveragerc
cover_pylib: False
data_file: /tmp/python_data/.coverage
debug: process
trace
sys
config
callers
dataop
dataio
disable_warnings: -none-
exclude_list: #\s*(pragma|PRAGMA)[:\s]?\s*(no|NO)\s*(cover|COVER)
extra_css: None
fail_under: 0.0
html_dir: htmlcov
html_title: Coverage report
ignore_errors: False
note: None
New Section 1 Page 2note: None
parallel: True
partial_always_list: while (True|1|False|0):
if (True|1|False|0):
partial_list: #\s*(pragma|PRAGMA)[:\s]?\s*(no|NO)\s*(branch|BRANCH)
paths: 'source': ['/tmp/python_source', '/opt/test']
plugin_options: 
plugins: -none-
precision: 0
report_include: None
report_omit: None
run_include: None
run_omit: None
show_missing: False
skip_covered: False
source: /opt/test/
timid: False
xml_output: coverage.xml
xml_package_depth: 99
-- sys -------------------------------------------------------
version: 4.5.4
coverage: /usr/lib64/python2.7/site-packages/coverage/__init__.pyc
cover_paths: /usr/lib64/python2.7/site-packages/coverage
pylib_paths: /usr/lib64/python2.7
tracer: PyTracer
plugins.file_tracers: -none-
plugins.configurers: -none-
config_files: /tmp/.coveragerc
configs_read: /tmp/.coveragerc
data_path: /tmp/python_data/.coverage
python: 2.7.5 (default, Jun 11 2019, 14:33:56) [GCC 4.8.5 20150623 (Red Hat 4.8.5-39)]
platform: Linux-3.10.0-1062.el7.x86_64-x86_64-with-redhat-7.7-Maipo
implementation: CPython
executable: /usr/bin/python
cwd: /opt/test
path: /usr/lib64/python27.zip
/usr/lib64/python2.7
/usr/lib64/python2.7/plat-linux2
/usr/lib64/python2.7/lib-tk
/usr/lib64/python2.7/lib-old
/usr/lib64/python2.7/lib-dynload
/usr/lib64/python2.7/site-packages
environment: COVERAGE_DEBUG = process,trace,sys,config,callers,dataop,dataio
COVERAGE_DEBUG_FILE = /tmp/cov.log
COVERAGE_PROCESS_START = /tmp/.coveragerc
command_line: ???
source_match: /opt/test
source_pkgs_match: -none-
include_match: -none-
omit_match: -none-
cover_match: -none-
pylib_match: -none-
-- end -------------------------------------------------------
<module> : /usr/lib64/python2.7/site.py @556
New Section 1 Page 3<module> : /usr/lib64/python2.7/site.py @556
main : /usr/lib64/python2.7/site.py @539
addsitepackages : /usr/lib64/python2.7/site.py @317
addsitedir : /usr/lib64/python2.7/site.py @190
addpackage : /usr/lib64/python2.7/site.py @152
<module> : <string> @1
process_startup : /usr/lib64/python2.7/site-packages/coverage/control.py @1289
start : /usr/lib64/python2.7/site-packages/coverage/control.py @690
_init : /usr/lib64/python2.7/site-packages/coverage/control.py @362
_write_startup_debug : /usr/lib64/python2.7/site-packages/coverage/control.py @382
write_formatted_info : /usr/lib64/python2.7/site-packages/coverage/debug.py @120
Not tracing '/usr/lib64/python2.7/threading.py': falls outside the --source trees
<module> : /usr/lib64/python2.7/site.py @556
main : /usr/lib64/python2.7/site.py @539
addsitepackages : /usr/lib64/python2.7/site.py @317
addsitedir : /usr/lib64/python2.7/site.py @190
addpackage : /usr/lib64/python2.7/site.py @152
<module> : <string> @1
process_startup : /usr/lib64/python2.7/site-packages/coverage/control.py @1289
start : /usr/lib64/python2.7/site-packages/coverage/control.py @701
start : /usr/lib64/python2.7/site-packages/coverage/collector.py @318
settrace : /usr/lib64/python2.7/threading.py @99
_trace : /usr/lib64/python2.7/site-packages/coverage/pytracer.py @111
_should_trace : /usr/lib64/python2.7/site-packages/coverage/control.py @593


[... Not tracing a bunch of common python code ...]


Tracing './test_mod.py'
<module> : ./test_mod.py @3
_trace : /usr/lib64/python2.7/site-packages/coverage/pytracer.py @111
_should_trace : /usr/lib64/python2.7/site-packages/coverage/control.py @593

【问题讨论】:

代码中的“New Section 1 Page 1”是什么? 哈哈,我不知道,一定是复制/粘贴问题。甚至不会像那样编译,所以我相信它在测试期间不存在。 有什么理由建议可以做到吗? gcov 文档似乎暗示这是不可能的,并指出:“虽然在 Python 测试代码中跟踪代码覆盖率很常见,但由于 Python 代码覆盖率工具无法跟踪 C/C++ 扩展代码,因此使用 Cpython 扩展会变得更加棘手。 "。 并不是故意的。只是你可能会叫错树。我尝试过的一个想法,但无法获得很好的输出,尝试将 python 调用转换为 python 的模块以导入和运行覆盖测试(尽管这有点反模式)。 是的,ned 自己说过,不管是从命令行运行还是从 c++ 运行,它都应该可以工作。另一个答案看起来很有希望,基本上我可能忽略了 finalize 方面。 【参考方案1】:

PyObject *PySys_GetObject(char *name) 返回一个借用的引用。不是应该增加引用计数吗?怎么样:

// ...
PyObject* sysPath = PySys_GetObject("path");
Py_INCREF(sysPath);
PyList_Append(sysPath, PyString_FromString("."));
Py_DECREF(sysPath);
// sysPath = NULL;
// ...

【讨论】:

不确定这如何应用于 Coverage.py 方面,但在任何一种情况下都无济于事。【参考方案2】:

我自己只是从 Python-C API 开始,但我的理解是导入模块实际上并没有将它们添加到您的主模块中。您需要单独执行此操作。我不确定这是否有助于解决您的问题,但我的有效方法(减去错误检查)如下:

// Initialize main module
PyObject* mainModule = PyImport_AddModule("__main__");;

//  Initialize module to be added
PyObject* moduleNamePyObject= PyUnicode_DecodeFSDefault("moduleName");
PyImport_Import(moduleNamePyObject);

// Add module to main module
PyObject_SetAttrString(mainModulePtr, "moduleName", modulePyObject);

【讨论】:

模块导入运行正常,只是没有得到coverage.py报告的覆盖率数据?我不确定这种方法会改变这一点,但会调查一下,谢谢。【参考方案3】:

通常,在导入模块时,Python 会尝试在导入模块(包含导入语句的模块)旁边查找模块文件。然后 Python 会尝试“sys.path”中的目录。通常不考虑当前工作目录。在我们的例子中,导入是通过 API 执行的,因此 Python 可以在其目录中搜索“test_mod.py”的导入模块不存在。该插件也不在“sys.path”上。使 Python 能够找到插件的一种方法是通过 API 执行相当于“sys.path.append('.')”的操作,将当前工作目录添加到模块搜索路径中。

Py_Initialize();
PyObject* sysPath = PySys_GetObject((char*)"path");
PyObject* programName = PyString_FromString(<DIRECTORY>.c_str());
PyList_Append(sysPath, programName);
Py_DECREF(programName);

如果你使用的是 python3 ,

PyString_FromString 更改为PyUnicode_FromString.

来源:

https://realmike.org/blog/2012/07/08/embedding-python-tutorial-part-1/

Python Embedding: PyImport_Import not from the current directory

【讨论】:

PySys_GetObject((char*)"path"); const char * : docs.python.org/3/c-api/sys.html ,并且 C-casting 去掉字符串文字的 constness 听起来很可怕。 模块导入运行正常,只是没有得到coverage.py报告的覆盖率数据?我不确定这种方法会改变这一点,但会调查一下,谢谢。【参考方案4】:

我使用您的代码重现了该问题,您只是忘记调用 Py_Finalize()。因此,在收集数据时永远不会生成报告。

它适用于以下代码:

#include <stdio.h>
#include <iostream>
#include <Python.h>
int main()

    Py_Initialize();
    PyEval_InitThreads();
    PyObject* sysPath = PySys_GetObject("path");
    PyList_Append(sysPath, PyString_FromString("."));
    // Load the module
    PyObject *pName = PyString_FromString("test_mod");
    PyObject *pModule = PyImport_Import(pName);
    if (pModule != NULL) 
        std::cout << "Python module found\n";
        // Load all module level attributes as a dictionary
        PyObject *pDict = PyModule_GetDict(pModule);
        PyObject *pFunc = PyObject_GetAttrString(pModule, "getInteger");
        if(pFunc)
        
            if(PyCallable_Check(pFunc))
            
                PyObject *pValue = PyObject_CallObject(pFunc, NULL);
                std::cout << PyLong_AsLong(pValue) << std::endl;
            
            else
            
                printf("ERROR: function getInteger()\n");
            

        
        else
        
            printf("ERROR: pFunc is NULL\n");
        
    
    else
        std::cout << "Python Module not found\n";
    Py_Finalize();
    return 0;

【讨论】:

以上是关于C++ PyImport 的 Python 覆盖率的主要内容,如果未能解决你的问题,请参考以下文章

PyImport_ImportModule,可以从内存中加载模块吗?

从 C++ 调用 Python

Python 的 C API 的 const 正确性

C调用python——PyImport_ImportModule返回空指针

Julia Plots: PyCall.PyError("PyImport_ImportModule\n\npyimport 找不到 Python 包 matplotlib.pyplot

如何在 C++ 程序将使用的 Python 脚本中导入 cpython 模块?