集成测试#
为了确保 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 数据;它还会从
ArrowArray
结构中导入由 Arrow Go 导出的记录批次。它验证两个记录批次是否相等,然后释放导入的记录批次。根据实现语言的功能,集成测试工具可能会断言内存使用量保持不变(即导出的记录批次没有泄漏)。
最后,集成测试工具释放
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
仅在架构中存在字典类型字段时才存在。
架构
{
"fields" : [
/* Field */
],
"metadata" : /* Metadata */
}
字段
{
"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
是一个空数组。
元数据
null |
[ {
"key": /* string */,
"value": /* string */
} ]
自定义元数据的键值映射。它可以省略或为 null,在这种情况下,它被认为等效于 []
(无元数据)。此处不禁止重复键。
类型:
{
"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 */
}
浮点数
{
"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"
“时区”是一个可选字符串。
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
。
{
"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”包含一个具有有意义名称和类型的 Fields
数组。
Map
{
"name": "map",
"keysSorted": /* boolean */
}
Field
的“children”包含一个单一的 struct
字段,该字段本身包含 2 个子字段,名为“key”和“value”。
Null
{
"name": "null"
}
RunEndEncoded
{
"name": "runendencoded"
}
Field
的“children”应正好是两个子字段。第一个子字段必须命名为“run_ends”,不可为空,并且必须是 int16
、int32
或 int64
类型字段。第二个子字段必须命名为“values”,但可以是任何类型。
扩展类型与 IPC 格式一样,表示为其底层存储类型加上一些专门的字段元数据,以重建扩展类型。例如,假设一个以 struct<numer: int32, denom: int32>
存储为后备的“rational”扩展类型,以下是“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(空)。不可为空的Field
的数据仍然具有VALIDITY
数组,即使所有值都为 1。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 存储库中的金文件。
数据生成器测试#
这是使用 archery integration
命令生成和测试的情况的概述(参见 datagen.py
中的 get_generated_json_files
)
基本类型 - 无批处理 - 各种基本值 - 长度为零的批处理 - 字符串和二进制大偏移量情况
空类型 * 微不足道的空批处理
Decimal128
Decimal256
具有各种单位的日期时间
具有各种单位的持续时间
间隔 - 月日纳秒间隔是一个单独的情况
映射类型 - 非规范映射
嵌套类型 - 列表 - 结构 - 具有大偏移量的列表
联合
自定义元数据
具有重复字段名称的模式
字典类型 - 有符号索引 - 无符号索引 - 嵌套字典
运行结束编码
二进制视图和字符串视图
列表视图和大列表视图
扩展类型
金文件集成测试#
预生成的 json 和 arrow IPC 文件(文件和流格式)位于 arrow-testing 存储库的 data/arrow-ipc-stream/integration
目录中。这些用作金文件,被认为是用于测试的正确文件。它们由 Archery 实用程序代码中的 runner.py
引用。以下是它们所涵盖的测试用例
向后兼容性
以下情况使用 0.14.1 格式测试
日期时间
小数
字典
间隔
映射
嵌套类型(列表、结构)
基本类型
没有批处理的基本类型
长度为零的批处理的基本类型
以下是针对 0.17.1 格式测试的
联合
字节序
以下情况使用小端和大端版本进行测试以进行自动转换
自定义元数据
日期时间
小数
decimal256
字典
具有无符号索引的字典
具有重复字段名称的记录批处理
扩展类型
间隔类型
映射类型
非规范映射数据
嵌套类型(列表、结构)
嵌套字典
嵌套大偏移量类型
空值
基本数据
大偏移量二进制和字符串
没有包含批处理的基本类型
长度为零的批处理的基本类型
递归嵌套类型
联合类型
压缩测试
LZ4
ZSTD
具有共享字典的批处理