集成测试#
为了确保 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 IPC 格式的生产者,Arrow Java 作为消费者。测试 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++,您需要将 -DARROW_BUILD_INTEGRATION=ON
添加到您的 cmake 命令。
根据您构建的组件,您可以启用这些组件并将其添加到 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 */
} ]
自定义元数据的键值映射。它可以省略或为空,在这种情况下,它被认为等同于 []
(没有元数据)。此处不禁止重复的键。
类型:
{
"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"
}
列表的 “list of” 类型将包含在 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” 包含一个 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 格式中一样)表示为其底层存储类型,加上一些专用字段元数据,以重建扩展类型。例如,假设一个 “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 (true) 和 0 (false) 的数组。
对于基于整数的类型(包括时间戳):一个 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 行,并且包含 listSize
为 4 的 FixedSizeList
,则该 FieldData
的 “children” 内部的数据将具有 count 28。
对于 “null” 类型,BufferData
不包含任何缓冲区。
Archery 集成测试用例#
通过了解自动化集成测试实际测试的内容,此列表可以更轻松地了解任何未来 Arrow 格式更改可能需要完成哪些手动测试。
集成测试用例有两种类型:一种是由 Archery 实用程序中的数据生成器动态填充的测试用例,另一种是位于 arrow-testing 存储库中的 *gold* 文件。
数据生成器测试#
这是使用 archery integration
命令生成和测试的用例的高级描述(请参阅 datagen.py
中的 get_generated_json_files
)。
基本类型 - 无批次 - 各种基本值 - 零长度批次 - 字符串和二进制大偏移量用例
Null 类型 * 简单的 Null 批次
Decimal128
Decimal256
具有各种单位的 DateTime
具有各种单位的 Durations
Intervals - MonthDayNano interval 是一个单独的用例
Map 类型 - 非规范 Map
嵌套类型 - 列表 - 结构 - 具有大偏移量的列表
Unions
自定义元数据
具有重复字段名称的 Schema
字典类型 - 有符号索引 - 无符号索引 - 嵌套字典
游程编码
二进制视图和字符串视图
列表视图和大型列表视图
扩展类型
Gold 文件集成测试#
预生成的 json 和 arrow IPC 文件(文件和流格式)存在于 arrow-testing 存储库中的 data/arrow-ipc-stream/integration
目录中。 这些充当 *gold* 文件,假定它们是正确的,可用于测试。 它们由 Archery 实用程序代码中的 runner.py
引用。 以下是它们涵盖的测试用例
向后兼容性
以下用例使用 0.14.1 格式进行测试
datetime
decimals
dictionaries
intervals
maps
nested types (list, struct)
primitives
primitive with no batches
primitive with zero length batches
以下内容针对 0.17.1 格式进行测试
unions
字节序
以下用例使用小端和大端版本进行测试,以进行自动转换
自定义元数据
datetime
decimals
decimal256
dictionaries
具有无符号索引的字典
具有重复字段名称的记录批次
扩展类型
时间间隔类型
map 类型
非规范 map 数据
嵌套类型(列表、结构)
嵌套字典
嵌套大型偏移类型
nulls
primitive data
大型偏移二进制文件和字符串
未包含批次的基本类型
零长度基本批次
递归嵌套类型
union 类型
压缩测试
LZ4
ZSTD
具有共享字典的批次