py.test来测试Cython C API模块

时间:2015-08-27 13:16:04

标签: unit-testing cython pytest

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

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

cdef int is_true():
    return False

然后使用C API测试test_foo.pyx模块的foo模块:

cimport foo

def test_foo():
    assert foo.is_true()

然后在只包含此行的普通cython_test.py模块中导入它们:

from foo_test import *

py.test测试运行器以这种方式找到test_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

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

1 个答案:

答案 0 :(得分:5)

所以,最后,我设法让py.test直接从Cython编译的.pyx文件中运行测试。然而,这种方法是一种可怕的黑客攻击,旨在尽可能地使用py.test Python测试运行器。它可能会停止使用与我准备工作的任何py.test版本不同的版本(即2.7.2)。

首先是打败py.test关注.py文件。最初,py.test拒绝导入任何没有.py扩展名文件的内容。另外一个问题是py.test验证模块的__FILE__是否与.py文件的位置匹配。 Cython的__FILE__通常不包含源文件的名称。我不得不重写这个检查。我不知道这种覆盖是否会破坏任何东西 - 我只能说测试似乎运行良好,但如果您担心,请咨询您当地的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这样做可以注入固定装置,这是一个非常漂亮的功能,值得保留。 inspect不适用于Cython,并且通常似乎没有简单的方法可以使原始inspect与Cython一起使用。也没有任何其他好的方法来检查Cython函数的参数列表。现在我决定做一个小的解决方法,我将所有测试函数包装在一个具有所需签名的纯Python函数中。

除此之外,似乎Cython会自动为每个__test__模块添加.pyx属性。 Cython的做法干扰py.test,需要修复。据我所知,__test__是Cython的内部细节,没有任何地方暴露,所以我们覆盖它并不重要。在我的例子中,我将以下函数放入.pxi文件中以包含在任何*_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

如果您决定使用上述黑客攻击,请记住给自己留下评论,并附上此答案的链接 - 我会尽量保持更新以备更好的解决方案。