Unit testing py.test测试Cython C API模块

Unit testing py.test测试Cython C API模块,unit-testing,cython,pytest,Unit Testing,Cython,Pytest,我正在尝试为Cython模块设置单元测试,以测试一些没有python接口的函数。第一个想法是检查.pyx文件是否可以直接由py.test的测试运行程序使用,但显然它只扫描.py文件 第二个想法是在一个Cython模块中编写test.*方法,然后将其导入到一个普通的.py文件中。假设我们有一个foo.pyx模块,其中包含我们要测试的内容: cdef int is_true(): return False 然后是一个test\u foo.pyx模块,它使用C API测试foo模块: cim

我正在尝试为Cython模块设置单元测试,以测试一些没有python接口的函数。第一个想法是检查
.pyx
文件是否可以直接由
py.test
的测试运行程序使用,但显然它只扫描
.py
文件

第二个想法是在一个Cython模块中编写
test.*
方法,然后将其导入到一个普通的
.py
文件中。假设我们有一个
foo.pyx
模块,其中包含我们要测试的内容:

cdef int is_true():
    return False
然后是一个
test\u foo.pyx
模块,它使用C API测试
foo
模块:

cimport foo

def test_foo():
    assert foo.is_true()
然后将它们导入一个普通的
cython_test.py
模块,该模块只包含以下行:

from foo_test import *
py.test
测试运行程序确实以这种方式找到了
test\u foo
,但随后报告:

/usr/lib/python2.7/inspect.py:752: in getargs
    raise TypeError('{!r} is not a code object'.format(co))
E   TypeError: <built-in function test_foo> is not a code object
/usr/lib/python2.7/inspect.py:752:in-getargs
raise TypeError(“{!r}不是代码对象”。格式(co))
E TypeError:不是代码对象

有没有更好的方法可以使用
py.test
测试Cython C-API代码?

因此,最后,我设法让
py.test
直接从Cython编译的
.pyx
文件运行测试。然而,这种方法是一种可怕的黑客行为,旨在尽可能多地利用Python测试运行程序
py.test
Python。它可能会停止使用任何与我准备的hack(2.7.2)不同的
py.test
版本

第一件事是击败
py.test
.py
文件的关注。最初,
py.test
拒绝导入任何没有扩展名为
.py
的文件。另一个问题是
py.test
验证模块的
\uuuuuuuuuu文件是否与
.py
文件的位置匹配。Cython的
\uuuuu文件\uuuuu
通常不包含源文件的名称。我不得不取消这张支票。我不知道这个覆盖是否会破坏任何东西,我能说的就是测试似乎运行良好,但是如果您担心,请咨询您当地的
py.test
开发人员。此部分作为本地
conftest.py
文件实现

import _pytest
import importlib


class Module(_pytest.python.Module):
    # Source: http://stackoverflow.com/questions/32250450/
    def _importtestmodule(self):
        # Copy-paste from py.test, edited to avoid throwing ImportMismatchError.
        # Defensive programming in py.test tries to ensure the module's __file__
        # matches the location of the source code. Cython's __file__ is
        # different.
        # https://github.com/pytest-dev/pytest/blob/2.7.2/_pytest/python.py#L485
        path = self.fspath
        pypkgpath = path.pypkgpath()
        modname = '.'.join(
            [pypkgpath.basename] +
            path.new(ext='').relto(pypkgpath).split(path.sep))
        mod = importlib.import_module(modname)
        self.config.pluginmanager.consider_module(mod)
        return mod

    def collect(self):
        # Defeat defensive programming.
        # https://github.com/pytest-dev/pytest/blob/2.7.2/_pytest/python.py#L286
        assert self.name.endswith('.pyx')
        self.name = self.name[:-1]
        return super(Module, self).collect()


def pytest_collect_file(parent, path):
    # py.test by default limits all test discovery to .py files.
    # I should probably have introduced a new setting for .pyx paths to match,
    # for simplicity I am hard-coding a single path.
    if path.fnmatch('*_test.pyx'):
        return Module(path, parent)
第二个主要问题是
py.test
使用Python的
inspect
模块检查单元测试的函数参数名称。记住,
py.test
这样做是为了注入fixture,这是一个非常漂亮的特性,值得保留
inspect
不适用于Cython,一般来说,似乎没有简单的方法使original
inspect
适用于Cython。也没有其他好的方法来检查Cython函数的参数列表。现在,我决定做一个小的变通方法,将所有测试函数包装在一个带有所需签名的纯Python函数中

除此之外,Cython似乎会自动为每个
.pyx
模块添加一个
\uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu
属性。Cython的做法会干扰py.test,需要修复。据我所知,
\uuuuu test\uuuu
是Cython的一个内部细节,没有在任何地方公开,所以我们覆盖它并不重要。在我的例子中,我将以下函数放入
.pxi
文件中,以包含在任何
*\u test.pyx
文件中:

from functools import wraps


# For https://github.com/pytest-dev/pytest/blob/2.7.2/_pytest/python.py#L340
# Apparently Cython injects its own __test__ attribute that's {} by default.
# bool({}) == False, and py.test thinks the developer doesn't want to run
# tests from this module.
__test__ = True


def cython_test(signature=""):
    ''' Wrap a Cython test function in a pure Python call, so that py.test
    can inspect its argument list and run the test properly.

    Source: http://stackoverflow.com/questions/32250450/'''
    if isinstance(signature, basestring):
        code = "lambda {signature}: func({signature})".format(
            signature=signature)

        def decorator(func):
            return wraps(func)(eval(code, {'func': func}, {}))

        return decorator

    # case when cython_test was used as a decorator directly, getting
    # a function passed as `signature`
    return cython_test()(signature)
之后,我可以执行以下测试:

include "cython_test_helpers.pxi"
from pytest import fixture

cdef returns_true():
    return False

@cython_test
def test_returns_true():
    assert returns_true() == True

@fixture
def fixture_of_true():
    return True

@cython_test('fixture_of_true')
def test_fixture(fixture_of_true):
    return fixture_of_true == True

如果您决定使用上述黑客,请记住给自己留下一条评论,并附上此答案的链接——我会尽力更新它,以备有更好的解决方案可用。

简单说明一下:我找到了一种黑客方法,但是我需要一些时间来记录它。请看一下,了解如何编写一个可以直接从
.pyx
文件运行测试的插件。@BrunoOliveira:您可能对我在下面发布的解决方案感兴趣。Cython编译的模块与纯Python模块没有太大区别,因此我决定采用一种短期的破解方法,我希望在Python的未来版本中不必进行轻微调整(以便
inspect.getargspec
使用用户定义的类型),Cython(实现
inspect.getargspec
并去掉它的内部
\uuuuuu测试\uuuuuuu
属性)以及py.test(调整一些断言)。