使用py.test测试编译库代码

12

我有一个Python库,其存储库结构如下:

repobase
 |- mylibrary
 |  |- __init__.py
 |- tests
    |- test_mylibrary.py

到目前为止,运行测试只需要在仓库基础目录中调用py.test即可。然后,在test_mylibrary.py中的import mylibrary将使用repobase/mylibrary中的本地代码。

现在,我已经扩展了库以使用编译代码。因此,repobase/mylibrary中的源代码本身不起作用。我必须执行setup.py build。这会创建repobase/build/lib.linux-x86_64-2.7/mylibrary

有没有合理的方法使py.test使用该目录来导入mylibrary?考虑以下限制:

  1. 我不想在test_mylibrary.py中包含任何sys.path/import魔法,因为这可能会破坏其他环境中的测试。

  2. 我不想放弃从repobase运行py.test的可能性。因此,修改PYTHONPATH无济于事,因为“。”仍将是sys.path中的第一个项。因此,优先考虑repobase/mylibrary而不是repobase/build/lib.linux-x86_64-2.7/mylibrary

如果没有,那么测试需要构建的Python库的标准方法是什么?


“…我已经扩展了库以使用编译代码…”这句话的意思不太清楚,即编译版本是否提供与Python版本相同的接口,还是Python版本导入编译版本?如果是前者,则您实际上正在测试两个不同的东西,因此即使测试套件相同,它们也应该具有不同的名称,例如Python的picklecPickle。如果是后者,则它们应该有不同的名称。常见的Python习惯用法是在编译部分前加下划线,例如Python的socket_socket - Aya
无论哪种方式,拥有两个不同的模块实现共享相同的名称是在寻求麻烦。即使你的解决方案今天适用于所有可能的运行时情况,你也无法预测所有可能的未来运行时情况,你可能会导入或测试错误的版本而没有意识到。 - Aya
@Aya 我只有一个实现。编译后的代码替换了以前的Python代码。正如我所写的“因此,repobase/mylibrary中的源代码本身不可用。” 我需要编译后的代码能够运行测试。 - Tim Hoffmann
所以,如果编译后的代码应该替换Python代码,那么你不能只是从存储库中删除Python代码吗?如果答案是否定的,因为它只替换了“一些”功能,那么它并不是真正的替代,而是一个扩展。 - Aya
Python代码被淘汰了。比如说,我有一个名为do_something(args)的函数,它是用Python实现的。现在,Python实现已被替换为对某个编译后的c代码的内部调用。do_something的API没有改变,我仍然想执行相同的测试。在进行测试之前,只需从“repobase/mylibrary”运行原始代码即可。更改后,这不再可行,因为原始Python代码不能再单独使用了。相反,我必须首先运行setup.py build,然后使用来自“build”子目录中的代码。问题是如何让py.test使用该代码。 - Tim Hoffmann
显示剩余3条评论
2个回答

6
我认为你的问题很简单,就是py.test没有将构建的共享对象复制到你的存储库根目录中。
我刚刚尝试了直接从Python wiki运行UT,使用py.test测试C扩展,如下所示:
python setup.py build
py.test test/examp_unittest.py

出现了AssertionError: No module named examp错误。

不过,当我按照维基百科的指示(并运行python setup.py test)进行操作时,我注意到它将.so文件复制到根目录下(在开始运行测试之前请注意最后一行):

running test
running egg_info
writing examp.egg-info/PKG-INFO
writing top-level names to examp.egg-info/top_level.txt
writing dependency_links to examp.egg-info/dependency_links.txt
reading manifest file 'examp.egg-info/SOURCES.txt'
writing manifest file 'examp.egg-info/SOURCES.txt'
running build_ext
copying build/lib.linux-x86_64-2.6/examp.so ->
runTest (test.examp_unittest.DeviceTest) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.001s

OK

在我的系统上运行过后,我现在可以很愉快地在同一代码库上运行py.test - 如下所示。
============================= test session starts ==============================
platform linux2 -- Python 2.7.3, pytest-2.9.2, py-1.4.31, pluggy-0.3.1
rootdir: /tmp/sotest, inifile: 
collected 1 items 

test/examp_unittest.py .

=========================== 1 passed in 0.01 seconds ===========================

因此解决方案是将您的共享对象复制到存储库的根目录中。
为了确保我从头开始运行整个过程,只需构建扩展程序,复制共享对象,然后运行py.test。所有这些都按预期工作。

我不能确定,因为我还没有时间去尝试它(希望明天可以)。从你的例子中,我得出结论,我需要运行 python setup.py test。但是这将首先运行标准的unittests(这已经过时了,因为它无法发现和/或正确处理我的所有测试)。这只会浪费额外的时间。也许我应该将pytest与setuptools集成在一起(https://pytest.org/latest/goodpractices.html#integrating-with-setuptools-python-setup-py-test-pytest-runner)。 - Tim Hoffmann
仍然感觉每次都要运行所有测试有些不对。我经常想更改一些代码,然后构建(因为我必须编译)然后运行单个测试,例如 py.test tests/test_mylibrary.py::test_do_something。也许我需要调整 python setup.py build 或者制作一个自定义的 python setup.py build_test 只需复制所需的 .so 文件。我真的很想知道为什么我找不到任何相关信息,因为我认为这是一个标准问题,只要你使用 py.test 和编译代码就会遇到。 - Tim Hoffmann
@TimHoffmann 您不需要每次都运行所有测试。只需运行构建并自己复制文件即可。如果您像我一样讨厌打字,可以将其放在Bash脚本中... - Peter Brittain
@TimHoffmann 另一个想法... 如果您的共享对象与包名称冲突,只需使用内部名称即可。例如,许多项目将 _mylibrary 作为 mylibrary 的 C 扩展名。 - Peter Brittain

3

从聊天讨论中可以得知,C实现只提供了Python实现功能的子集。

一个常见的解决方案是将模块分割成需要优化实现的部分和存在于单独模块中的其它部分。

考虑一个更具体的例子,一个需要在不同图像格式之间进行转换的库。

假设您的布局如下...

repobase
 |- image
 |  |- __init__.py
 |  |- pyJPEG.py
 |- build
 |  |- lib.linux-x86_64-2.7
 |     |- cJPEG.so
 |- tests
    |- test_image.py

如果你的PYTHONPATH包括/path/to/repobase:/path/to/repobase/build/lib.linux-x86_64-2.7,并且你的cJPEG.so导出符号为jpeg_decompressjpeg_compress,那么你的文件看起来像这样...

image/__init__.py

# Load the C implementation if we have it, otherwise fall back to
# a pure Python implementation
try:
    from cJPEG import jpeg_decompress, jpeg_compress
except ImportError:
    from pyJPEG import jpeg_decompress, jpeg_compress

def load_image(filename):
    data = open(filename, 'rb').read()
    if filename.endswidth('.jpg'):
        return jpeg_decompress(data)
    else:
        raise NotImplementedError

def save_image(data, filename, filetype='JPEG'):
    if filetype == 'JPEG':
        data = jpeg_compress(data)
    else:
        raise NotImplementedError
    open(filename, 'wb').write(data)

image/pyJPEG.py

def jpeg_decompress(data):
    # A pure Python implementation of a JPEG decoder

def jpeg_compress(data):
    # A pure Python implementation of a JPEG encoder

采用这种布局,测试套件不必关心库是否已构建 - 在两种情况下都可以使用相同的测试套件,存在(或不存在)cJPEG.so将决定测试哪个版本。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接