测试 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,它具有与 TEST_FUNC_PROTOSVector.h 中相同的参数并以相同的方式工作。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 中,然后执行。错误和失败将相加并作为退出参数返回。任何非零结果都表明至少有一个测试未通过。