2015-08-27 59 views
1

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

第二个想法是在一个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测试运行做ES发现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代码呢?

+0

只是一个简单的说明:我发现这样做的方法很糟糕,但我需要一些时间来记录它。 – liori

+0

看一看[pytest-cpp](https://github.com/pytest-dev/pytest-cpp)以获得关于如何编写可以直接从'.pyx'文件运行测试的插件的一些想法。 –

+0

@BrunoOliveira:您可能对我在下面发布的解决方案感兴趣。 Cython编译模块与纯Python模块没有什么不同,所以我决定进行一个短期的黑客攻击,我希望在未来版本的Python中稍作调整时不需要这么做(这样'inspect.getargspec'与用户 - 定义类型),Cython(实现'inspect.getargspec'并去除它的内部'__test__'属性)和py.test(调整一些断言)。 – liori

回答

2

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

首先是击败py.test关注.py文件。最初,py.test拒绝导入任何没有扩展名为.py的文件。其他问题是py.test验证模块的__FILE__是否与.py文件的位置匹配。虽然Cython的__FILE__通常不包含源文件的名称。我必须重写此检查。我不知道这个重写是否会破坏任何东西 - 我只能说测试似乎运行良好,但如果您担心,请咨询当地的开发人员。这部分是作为当地的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 

如上所述,请记住给自己留下一个评论,并附上这个答案的链接 - 我会尽量保持更新以防万一有更好的解决方案。