测试 numpy.i 类型映射#

引言#

numpy.i SWIG 接口文件编写测试是一件组合式的头疼事。目前,支持 12 种不同的数据类型,每种数据类型有 74 种不同的参数签名,总共支持“开箱即用”的 888 种类型映射。每种类型映射反过来又可能需要几个单元测试来验证正确和不正确输入的预期行为。目前,这导致在 numpy/tools/swig 子目录下运行 make test 时执行超过 1000 个单独的单元测试。

为了方便进行大量类似的单元测试,采用了一些高级编程技术,包括 C 和 SWIG 宏,以及 Python 继承。本文档的目的是描述用于验证 numpy.i 类型映射是否按预期工作的测试基础设施。

测试组织#

支持三种独立的测试框架,分别用于一维、二维和三维数组。对于一维数组,有两个 C++ 文件,一个头文件和一个源文件,名为

Vector.h
Vector.cxx

包含用于各种函数的原型和代码,这些函数的形参为一维数组。文件

Vector.i

是一个 SWIG 接口文件,它定义了一个 Python 模块 Vector,该模块包装了 Vector.h 中的函数,同时利用 numpy.i 中的类型映射来正确处理 C 数组。

Makefile 调用 swig 来生成 Vector.pyVector_wrap.cxx,还会执行 setup.py 脚本,该脚本编译 Vector_wrap.cxx 并将扩展模块 _Vector.so_Vector.dylib 链接在一起(取决于平台)。此扩展模块和代理文件 Vector.py 都放置在 build 目录下的一个子目录中。

实际测试通过名为

testVector.py

的 Python 脚本进行,该脚本使用标准的 Python 库模块 unittest,对 Vector.h 中定义的每个函数针对每种支持的数据类型执行了多个测试。

二维数组的测试方式完全相同。上述描述适用,但将 Matrix 替换 Vector。对于三维测试,将 Tensor 替换 Vector。对于四维测试,将 SuperTensor 替换 Vector。对于扁平原地数组测试,将 Flat 替换 Vector。对于下面的描述,我们将引用 Vector 测试,但相同的信息也适用于 MatrixTensorSuperTensor 测试。

命令 make test 将确保构建所有测试软件,然后运行所有三个测试脚本。

测试头文件#

Vector.h 是一个 C++ 头文件,它定义了一个名为 TEST_FUNC_PROTOS 的 C 宏,该宏接受两个参数:TYPE,这是一个数据类型名称,如 unsigned int;以及 SNAME,这是同一数据类型的简短名称,没有空格,例如 uint。此宏定义了几个函数原型,其前缀为 SNAME,并且至少有一个参数是 TYPE 类型的数组。那些具有返回参数的函数返回一个 TYPE 值。

TEST_FUNC_PROTOS 然后为 numpy.i 支持的所有数据类型实现

  • signed char

  • unsigned char

  • short

  • unsigned short

  • int

  • unsigned int

  • long

  • unsigned long

  • long long

  • unsigned long long

  • float

  • double

测试源文件#

Vector.cxx 是一个 C++ 源文件,它为 Vector.h 中指定的每个函数原型实现可编译的代码。它定义了一个 C 宏 TEST_FUNCS,该宏具有与 Vector.h 中的 TEST_FUNC_PROTOS 相同的参数,并且工作方式也相同。如上所述,TEST_FUNCS 为 12 种数据类型中的每一种都实现了。

测试 SWIG 接口文件#

Vector.i 是一个 SWIG 接口文件,它定义了 Python 模块 Vector。它遵循本章所述的 numpy.i 使用约定。它定义了一个 SWIG%apply_numpy_typemaps,该宏有一个参数 TYPE。它使用 SWIG 指令 %apply 将提供的类型映射应用于 Vector.h 中找到的参数签名。然后,此宏为 numpy.i 支持的所有数据类型实现。然后,它执行 %include "Vector.h" 来使用 numpy.i 中的类型映射包装 Vector.h 中的所有函数原型。

测试 Python 脚本#

使用 make 构建测试扩展模块后,可以运行 testVector.py 来执行测试。与使用 unittest 来方便单元测试的其他脚本一样,testVector.py 定义了一个继承自 unittest.TestCase 的类。

class VectorTestCase(unittest.TestCase):

然而,这个类不是直接运行的。相反,它作为几个其他 Python 类的基类,每个类都特定于一种数据类型。 VectorTestCase 类存储两个字符串用于类型信息

self.typeStr

一个字符串,与 Vector.hVector.cxx 中使用的 SNAME 前缀之一匹配。例如,"double"

self.typeCode

一个简短的(通常是单个字符的)字符串,它在 numpy 中表示一种数据类型,并对应于 self.typeStr。例如,如果 self.typeStr"double",则 self.typeCode 应该是 "d"

VectorTestCase 类定义的每个测试通过访问 Vector 模块的字典来提取它正在测试的 Python 函数

length = Vector.__dict__[self.typeStr + "Length"]

在双精度测试的情况下,这将返回 Python 函数 Vector.doubleLength

然后,我们为每种支持的数据类型定义一个新的测试用例类,其定义简短,如下所示

class doubleTestCase(VectorTestCase):
    def __init__(self, methodName="runTest"):
        VectorTestCase.__init__(self, methodName)
        self.typeStr  = "double"
        self.typeCode = "d"

这 12 个类都被收集到一个 unittest.TestSuite 中,然后执行。错误和失败被加在一起,并作为退出参数返回。任何非零结果都表示至少有一个测试未通过。