py.test to test Cython C API modules

lio*_*ori 2 unit-testing cython pytest

I'm trying to set up unit tests for a Cython module to test some functions that do not have python interface. The first idea was to check if .pyx files could directly be used by py.test's test runner, but apparently it only scans for .py files.

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

cdef int is_true():
    return False
Run Code Online (Sandbox Code Playgroud)

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

cimport foo

def test_foo():
    assert foo.is_true()
Run Code Online (Sandbox Code Playgroud)

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

from foo_test import *
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

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

lio*_*ori 5

所以,最后,我设法py.test直接从Cython编译的.pyx文件中运行测试.然而,这种方法是一种可怕的黑客攻击,旨在尽可能地使用py.testPython测试运行器.它可能会停止使用与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)
Run Code Online (Sandbox Code Playgroud)

第二个主要问题是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)
Run Code Online (Sandbox Code Playgroud)

之后,我可以实现如下测试:

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
Run Code Online (Sandbox Code Playgroud)

如果你决定使用上面描述的黑客攻击,请记住给自己留下一个评论,并附上这个答案的链接 - 如果有更好的解决方案,我会尽量保持更新.