集成测试#
为了确保 Arrow 实现之间可以互操作,Arrow 项目包含跨语言集成测试,这些测试会定期作为持续集成任务运行。
集成测试会对几个 Arrow 规范的合规性进行测试:IPC 格式、Flight RPC 协议以及 C 数据接口。
策略#
我们针对 Arrow 实现之间进行集成测试的策略是:
测试数据集以自定义的可读的、基于 JSON 的格式 指定,该格式专为 Arrow 的集成测试而设计。
JSON 文件由集成测试工具生成。不同的文件用于表示不同的数据类型和特性,例如数值、列表、字典编码等。这比将所有数据类型都表示在单个文件中更容易查明不兼容性。
每个实现都提供入口点,能够在 JSON 和 Arrow 内存表示之间进行转换,并使用所需的格式公开 Arrow 内存数据。
每种格式(无论是 Arrow IPC、Flight 还是 C 数据接口)都会针对所有支持的(生产者,消费者)实现对进行测试。生产者通常读取 JSON 文件,将其转换为内存中的 Arrow 数据,并使用测试中的格式公开此数据。消费者以所述格式读取数据并将其转换回内存中的 Arrow 数据;它还会读取与生产者相同的 JSON 文件,并验证两个数据集是否相同。
示例:IPC 格式#
假设我们正在测试 Arrow C++ 作为生产者,Arrow Java 作为 Arrow IPC 格式的消费者。测试 JSON 文件的过程如下:
一个 C++ 可执行文件读取 JSON 文件,将其转换为内存中的 Arrow 数据,并写入 Arrow IPC 文件(文件路径通常在命令行中给出)。
一个 Java 可执行文件读取 JSON 文件,将其转换为内存中的 Arrow 数据;它还会读取 C++ 生成的 Arrow IPC 文件。最后,它验证两个内存中的 Arrow 数据集是否相等。
示例:C 数据接口#
现在,假设我们正在测试 Arrow Go 作为生产者,Arrow C# 作为 Arrow C 数据接口的消费者。
集成测试工具在堆上分配一个 C ArrowArray 结构。
一个 Go 进程内入口点(例如,与 C 兼容的函数调用)读取 JSON 文件并将其 记录批次 之一导出到
ArrowArray
结构中。一个 C# 进程内入口点读取相同的 JSON 文件,将相同的记录批次转换为内存中的 Arrow 数据;它还会导入 Arrow Go 在
ArrowArray
结构中导出的记录批次。它验证两个记录批次是否相等,然后释放导入的记录批次。根据实现语言的能力,集成测试工具可以断言内存消耗保持不变(即,导出的记录批次没有泄漏)。
最后,集成测试工具释放
ArrowArray
结构。
运行集成测试#
集成测试数据生成器和运行器在 Archery 实用程序中实现。您需要安装 archery 的 integration
组件
$ pip install -e "dev/archery[integration]"
使用 archery integration
命令运行集成测试。
$ archery integration --help
为了运行集成测试,您首先需要构建要包含的每个组件。有关构建这些组件的说明,请参阅 C++、Java 等各自的开发者文档。
某些语言可能需要额外的构建选项才能启用集成测试。例如,对于 C++,您需要在 cmake 命令中添加 -DARROW_BUILD_INTEGRATION=ON
。
根据您构建的组件,您可以启用并将它们添加到 archery 测试运行中。例如,如果您只构建了 C++ 项目并想运行 Arrow IPC 集成测试,请运行
archery integration --run-ipc --with-cpp=1
对于 Java,它可能类似于
VERSION=14.0.0-SNAPSHOT
export ARROW_JAVA_INTEGRATION_JAR=$JAVA_DIR/tools/target/arrow-tools-$VERSION-jar-with-dependencies.jar
archery integration --run-ipc --with-cpp=1 --with-java=1
要运行所有测试,包括 Flight 和 C 数据接口集成测试,请执行
archery integration --with-all --run-flight --run-ipc --run-c-data
请注意,我们在持续集成中运行这些测试,CI 作业使用 Docker Compose。您也可以在本地运行 Docker Compose 作业,或者如果您对如何构建其他语言或启用某些测试有疑问,可以参考它。
有关项目 docker compose
配置的更多信息,请参阅 运行 Docker 构建。
JSON 测试数据格式#
为了跨语言集成测试的目的,提供了 Arrow 列式数据的 JSON 表示形式。此表示形式不是规范的,但它提供了一种人工可读的方式来验证语言实现。
有关此 JSON 数据的一些示例,请参阅此处。
JSON 集成测试文件的高级结构如下:
数据文件
{
"schema": /*Schema*/,
"batches": [ /*RecordBatch*/ ],
"dictionaries": [ /*DictionaryBatch*/ ],
}
所有文件都包含 schema
和 batches
,而 dictionaries
仅在模式中存在字典类型字段时才存在。
模式(Schema)
{
"fields" : [
/* Field */
],
"metadata" : /* Metadata */
}
字段(Field)
{
"name" : "name_of_the_field",
"nullable" : /* boolean */,
"type" : /* Type */,
"children" : [ /* Field */ ],
"dictionary": {
"id": /* integer */,
"indexType": /* Type */,
"isOrdered": /* boolean */
},
"metadata" : /* Metadata */
}
当且仅当 Field
对应于字典类型时,才会出现 dictionary
属性,并且其 id
映射到 DictionaryBatch
中的列。在这种情况下,type
属性描述字典的值类型。
对于基本类型,children
是一个空数组。
元数据(Metadata)
null |
[ {
"key": /* string */,
"value": /* string */
} ]
自定义元数据的键值映射。它可以省略或为空,在这种情况下,它被认为等同于 []
(无元数据)。此处不禁止重复的键。
类型(Type):
{
"name" : "null|struct|list|largelist|listview|largelistview|fixedsizelist|union|int|floatingpoint|utf8|largeutf8|binary|largebinary|utf8view|binaryview|fixedsizebinary|bool|decimal|date|time|timestamp|interval|duration|map|runendencoded"
}
Type
将根据其名称在 Schema.fbs 中定义的其他字段。
整数(Int)
{
"name" : "int",
"bitWidth" : /* integer */,
"isSigned" : /* boolean */
}
浮点数(FloatingPoint)
{
"name" : "floatingpoint",
"precision" : "HALF|SINGLE|DOUBLE"
}
定长二进制(FixedSizeBinary)
{
"name" : "fixedsizebinary",
"byteWidth" : /* byte width */
}
小数(Decimal)
{
"name" : "decimal",
"precision" : /* integer */,
"scale" : /* integer */
}
时间戳(Timestamp)
{
"name" : "timestamp",
"unit" : "$TIME_UNIT",
"timezone": "$timezone"
}
$TIME_UNIT
是 "SECOND|MILLISECOND|MICROSECOND|NANOSECOND"
之一
“timezone” 是一个可选字符串。
持续时间 (Duration)
{
"name" : "duration",
"unit" : "$TIME_UNIT"
}
日期 (Date)
{
"name" : "date",
"unit" : "DAY|MILLISECOND"
}
时间 (Time)
{
"name" : "time",
"unit" : "$TIME_UNIT",
"bitWidth": /* integer: 32 or 64 */
}
间隔 (Interval)
{
"name" : "interval",
"unit" : "YEAR_MONTH|DAY_TIME"
}
联合 (Union)
{
"name" : "union",
"mode" : "SPARSE|DENSE",
"typeIds" : [ /* integer */ ]
}
Union
中的 typeIds
字段是用于表示联合中哪个成员在每个数组槽中处于活动状态的代码。请注意,通常这些鉴别器与相应子数组的索引不相同。
列表 (List)
{
"name": "list"
}
列表是“列表”的类型将包含在 Field
的“children”成员中,作为那里的单个 Field
。例如,对于 int32
的列表,
{
"name": "list_nullable",
"type": {
"name": "list"
},
"nullable": true,
"children": [
{
"name": "item",
"type": {
"name": "int",
"isSigned": true,
"bitWidth": 32
},
"nullable": true,
"children": []
}
]
}
定长列表 (FixedSizeList)
{
"name": "fixedsizelist",
"listSize": /* integer */
}
此类型同样带有一个长度为 1 的“children”数组。
结构体 (Struct)
{
"name": "struct"
}
Field
的 “children” 包含一个 Field
数组,每个 `Field` 都有其对应的名称和类型。
Map(映射)
{
"name": "map",
"keysSorted": /* boolean */
}
Field
的 “children” 包含一个单独的 struct
字段,该字段本身包含两个子字段,分别名为 “key” 和 “value”。
Null(空值)
{
"name": "null"
}
RunEndEncoded(行程长度编码)
{
"name": "runendencoded"
}
Field
的 “children” 应该恰好包含两个子字段。第一个子字段必须命名为 “run_ends”,不能为空,并且必须是 int16
、int32
或 int64
类型字段。第二个子字段必须命名为 “values”,但可以是任何类型。
与 IPC 格式一样,扩展类型表示为其底层存储类型加上一些专用的字段元数据,以便重建扩展类型。例如,假设一个 “rational” 扩展类型由 struct<numer: int32, denom: int32>
存储支持,则 “rational” 字段将按如下方式表示
{
"name" : "name_of_the_field",
"nullable" : /* boolean */,
"type" : {
"name" : "struct"
},
"children" : [
{
"name": "numer",
"type": {
"name": "int",
"bitWidth": 32,
"isSigned": true
}
},
{
"name": "denom",
"type": {
"name": "int",
"bitWidth": 32,
"isSigned": true
}
}
],
"metadata" : [
{"key": "ARROW:extension:name", "value": "rational"},
{"key": "ARROW:extension:metadata", "value": "rational-serialized"}
]
}
RecordBatch(记录批次):
{
"count": /* integer number of rows */,
"columns": [ /* FieldData */ ]
}
DictionaryBatch(字典批次):
{
"id": /* integer */,
"data": [ /* RecordBatch */ ]
}
FieldData(字段数据):
{
"name": "field_name",
"count" "field_length",
"$BUFFER_TYPE": /* BufferData */
...
"$BUFFER_TYPE": /* BufferData */
"children": [ /* FieldData */ ]
}
Schema
中 Field
的 “name” 成员对应于 RecordBatch
的 “columns” 中包含的 FieldData
的 “name”。对于嵌套类型(列表、结构体等),Field
的 “children” 中的每个子字段都有一个 “name”,该名称对应于该 FieldData
的 “children” 中的 FieldData
的 “name”。对于 DictionaryBatch
中的 FieldData
,“name” 字段不对应任何内容。
这里,$BUFFER_TYPE
可以是 VALIDITY
、OFFSET
(用于变长类型,例如字符串和列表)、TYPE_ID
(用于联合)或 DATA
之一。
BufferData
根据缓冲区的类型进行编码
VALIDITY
:一个 JSON 数组,包含 1(有效)和 0(空值)。即使所有值都为 1,不可为空的Field
的数据仍然具有VALIDITY
数组。OFFSET
:一个 JSON 数组,包含 32 位偏移量的整数或 64 位偏移量的字符串格式整数。TYPE_ID
:一个 JSON 数组,包含整数。DATA
:一个 JSON 数组,包含编码值。VARIADIC_DATA_BUFFERS
: 一个 JSON 数组,包含表示为十六进制编码字符串的数据缓冲区。VIEWS
: 一个 JSON 数组,包含编码的视图,这些视图是具有以下内容的 JSON 对象:SIZE
: 指示视图大小的整数,INLINED
: 一个编码值(如果SIZE
小于 12,则会出现此字段,否则将出现接下来的三个字段),PREFIX_HEX
: 视图的前四个字节编码为十六进制,BUFFER_INDEX
:VARIADIC_DATA_BUFFERS
中被查看缓冲区的索引,OFFSET
: 被查看缓冲区中的偏移量。
DATA
的值编码根据逻辑类型的不同而不同
对于布尔类型:一个包含 1(真)和 0(假)的数组。
对于基于整数的类型(包括时间戳):一个 JSON 数字数组。
对于 64 位整数:一个格式化为 JSON 字符串的整数数组,以避免精度损失。
对于浮点类型:一个 JSON 数字数组。值限制为 3 个小数位,以避免精度损失。
对于二进制类型:一个大写十六进制编码字符串数组,以便表示任意二进制数据。
对于 UTF-8 字符串类型:一个 JSON 字符串数组。
对于 “list” 和 “largelist” 类型,BufferData
具有 VALIDITY
和 OFFSET
,其余数据位于 “children” 中。这些子 FieldData
包含与非子数据相同的所有属性,因此在 int32
列表的示例中,子数据具有 VALIDITY
和 DATA
。
对于 “fixedsizelist”,没有 OFFSET
成员,因为偏移量由字段的 “listSize” 隐含。
请注意,这些子数据的 “count” 可能与父级的 “count” 不匹配。例如,如果一个 RecordBatch
有 7 行并且包含一个 FixedSizeList
,其 listSize
为 4,则该 FieldData
的 “children” 中的数据的计数将为 28。
对于 “null” 类型,BufferData
不包含任何缓冲区。
Archery 集成测试用例#
通过了解自动化集成测试实际测试的用例,此列表可以更轻松地理解未来 Arrow 格式更改可能需要进行哪些手动测试。
集成测试用例有两种类型:由 Archery 实用程序中的数据生成器动态填充的测试用例,以及位于 arrow-testing 仓库中的 *gold* 文件。
数据生成器测试#
这是使用 archery integration
命令生成和测试的用例的高级描述(请参阅 datagen.py
中的 get_generated_json_files
)
基本类型 - 无批次 - 各种基本值 - 零长度批次 - 字符串和二进制大偏移量情况
空类型 * 简单的空批次
Decimal128(128 位十进制数)
Decimal256(256 位十进制数)
具有各种单位的 DateTime(日期时间)
具有各种单位的 Durations(持续时间)
Intervals(区间) - MonthDayNano 区间是一个单独的用例
Map 类型 - 非规范映射
嵌套类型 - 列表 - 结构体 - 具有大偏移量的列表
Unions(联合)
自定义元数据
具有重复字段名称的模式
字典类型 - 有符号索引 - 无符号索引 - 嵌套字典
行程长度编码
二进制视图和字符串视图
列表视图和大型列表视图
扩展类型
Gold 文件集成测试#
预先生成的 json 和 arrow IPC 文件(文件和流格式)位于 arrow-testing 仓库的 data/arrow-ipc-stream/integration
目录中。它们用作 *gold* 文件,假定其正确用于测试。 Archery 实用程序代码中的 runner.py
引用了它们。以下是它们涵盖的测试用例
向后兼容性
以下用例使用 0.14.1 格式进行测试
日期时间
十进制数
字典
区间
映射
嵌套类型(列表、结构体)
基本类型
无批次的基本类型
零长度批次的基本类型
以下内容针对 0.17.1 格式进行测试
联合
字节序
以下用例使用小端序和大端序版本进行自动转换测试
自定义元数据
日期时间
十进制数
256 位十进制数
字典
具有无符号索引的字典
具有重复字段名称的记录批次
扩展类型
区间类型
映射类型
非规范映射数据
嵌套类型(列表、结构体)
嵌套字典
嵌套大偏移量类型
空值
基本数据
大偏移量二进制和字符串
不包含批次的基本类型
零长度的基本类型批次
递归嵌套类型
联合类型
压缩测试
LZ4
ZSTD
具有共享字典的批次