集成测试#

为了确保 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 文件的流程如下:

  1. 一个 C++ 可执行文件读取 JSON 文件,将其转换为 Arrow 内存数据并写入一个 Arrow IPC 文件(文件路径通常通过命令行指定)。

  2. 一个 Java 可执行文件读取相同的 JSON 文件,将其转换为 Arrow 内存数据;同时它也会读取由 C++ 生成的 Arrow IPC 文件。最后,它会验证两个 Arrow 内存数据集是否相等。

示例:C 数据接口#

现在,假设我们要测试作为生产者的 Arrow Go 和作为消费者的 Arrow C# 在 Arrow C 数据接口下的交互。

  1. 集成测试工具会在堆上分配一个 C ArrowArray 结构体。

  2. 一个 Go 进程内入口点(例如 C 兼容的函数调用)读取一个 JSON 文件,并将其中的一个 记录批次 (record batch) 导出到 ArrowArray 结构体中。

  3. 一个 C# 进程内入口点读取相同的 JSON 文件,将相同的记录批次转换为 Arrow 内存数据;同时它也会导入由 Arrow Go 在 ArrowArray 结构体中导出的记录批次。它会验证两个记录批次是否相等,然后释放导入的记录批次。

  4. 根据实现语言的能力,集成测试工具可能会断言内存消耗保持不变(即导出的记录批次没有发生内存泄漏)。

  5. 最后,集成测试工具会释放 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*/ ],
}

所有文件都包含 schemabatches,而 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 是一个空数组。

元数据

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 中定义的其他字段。

整数

{
  "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”是一个可选字符串。

持续时间

{
  "name" : "duration",
  "unit" : "$TIME_UNIT"
}

日期型 (Date)

{
  "name" : "date",
  "unit" : "DAY|MILLISECOND"
}

时间

{
  "name" : "time",
  "unit" : "$TIME_UNIT",
  "bitWidth": /* integer: 32 or 64 */
}

间隔

{
  "name" : "interval",
  "unit" : "YEAR_MONTH|DAY_TIME"
}

联合

{
  "name" : "union",
  "mode" : "SPARSE|DENSE",
  "typeIds" : [ /* integer */ ]
}

Union 中的 typeIds 字段是用于表示数组槽位中哪个联合体成员处于活动状态的代码。请注意,通常这些判别式并不等同于相应子数组的索引。

列表

{
  "name": "list"
}

列表所包含的类型将作为单个 Field 包含在 Field 的 “children” 成员中。例如,对于 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” 数组。

结构体

{
  "name": "struct"
}

Field 的 “children” 包含一个具有有意义名称和类型的 Fields 数组。

Map

{
  "name": "map",
  "keysSorted": /* boolean */
}

Field 的 “children” 包含单个 struct 字段,该字段本身包含 2 个名为 “key” 和 “value” 的子字段。

Null

{
  "name": "null"
}

行程编码

{
  "name": "runendencoded"
}

Field 的 “children” 应该恰好有两个子字段。第一个子字段必须命名为 “run_ends”,不可为空,且为 int16int32int64 类型字段。第二个子字段必须命名为 “values”,可以是任何类型。

与 IPC 格式一样,扩展类型表示为它们的底层存储类型加上一些专门的字段元数据以重构扩展类型。例如,假设一个由 struct<numer: int32, denom: int32> 存储支持的“有理数”扩展类型,下面是“有理数”字段的表示方式:

{
  "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 */ ]
}

SchemaField 的 “name” 成员对应于 RecordBatch 的 “columns” 中包含的 FieldData 的 “name”。对于嵌套类型(列表、结构体等),Field 的 “children” 每个都有一个 “name”,对应于该 FieldData 的 “children” 内 FieldData 的 “name”。对于 DictionaryBatch 内的 FieldData,“name” 字段不对应任何内容。

此处 $BUFFER_TYPEVALIDITYOFFSET(用于可变长度类型,如字符串和列表)、TYPE_ID(用于联合体)或 DATA 之一。

BufferData 根据缓冲区类型进行编码:

  • VALIDITY:由 1(有效)和 0(空值)组成的 JSON 数组。不可为空的 Field 的数据仍然具有 VALIDITY 数组,即使所有值均为 1。

  • OFFSET:32 位偏移量为整数 JSON 数组,64 位偏移量为字符串格式的整数 JSON 数组。

  • 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 具有 VALIDITYOFFSET,其余数据位于 “children” 中。这些子 FieldData 包含与非子数据相同的所有属性,因此在 int32 列表的示例中,子数据具有 VALIDITYDATA

对于 “fixedsizelist”,没有 OFFSET 成员,因为偏移量由字段的 “listSize” 隐含。

请注意,这些子数据的 “count” 可能与父级的 “count” 不匹配。例如,如果一个 RecordBatch 有 7 行且包含一个 listSize 为 4 的 FixedSizeList,则该 FieldData “children” 内的数据计数将为 28。

对于 “null” 类型,BufferData 不包含任何缓冲区。

Archery 集成测试用例#

此列表可以帮助了解未来的 Arrow 格式更改可能需要进行哪些手动测试,因为我们可以确切地知道自动化集成测试涵盖了哪些情况。

有两种类型的集成测试用例:由 Archery 工具中的数据生成器即时生成的用例,以及存在于 arrow-testing 仓库中的 黄金 (gold) 文件。

数据生成器测试#

这是使用 archery integration 命令生成和测试的用例的高层描述(参见 datagen.py 中的 get_generated_json_files)。

  • 原始类型 - 无批次 - 各种原始值 - 零长度批次 - 字符串和二进制大偏移量情况

  • Null 类型 * 平凡 Null 批次

  • Decimal128

  • 十进制数256

  • 具有不同单位的日期时间

  • 具有不同单位的持续时间

  • 间隔 - MonthDayNano 间隔是一个单独的情况

  • 映射类型 - 非规范映射

  • 嵌套类型 - 列表 - 结构体 - 大偏移量列表

  • 联合体

  • 自定义元数据

  • 具有重复字段名称的模式

  • 字典类型 - 有符号索引 - 无符号索引 - 嵌套字典

  • 游程编码 (Run end encoded)

  • 二进制视图和字符串视图

  • 列表视图和大列表视图

  • 扩展类型

黄金文件集成测试#

预生成的 json 和 arrow IPC 文件(文件格式和流格式)存在于 arrow-testing 仓库的 data/arrow-ipc-stream/integration 目录中。这些用作被假定为测试正确性的 黄金 文件。它们被 Archery 工具代码中的 runner.py 引用。以下是它们涵盖的测试用例:

  • 向后兼容性

    • 以下情况使用 0.14.1 格式进行测试

      • 日期时间

      • 小数

      • 字典

      • 间隔

      • 映射

      • 嵌套类型 (list, struct)

      • 原始类型

      • 无批次的原始类型

      • 零长度批次的原始类型

    • 以下针对 0.17.1 格式进行测试

      • 联合体

  • 字节序

    • 以下情况使用小端和大端版本进行自动转换测试

      • 自定义元数据

      • 日期时间

      • 小数

      • decimal256

      • 字典

      • 具有无符号索引的字典

      • 具有重复字段名称的记录批次

      • 扩展类型

      • 间隔类型

      • 映射类型

      • 非规范映射数据

      • 嵌套类型 (lists, structs)

      • 嵌套字典

      • 嵌套大偏移量类型

      • null 值

      • 原始数据

      • 大偏移量二进制和字符串

      • 未包含批次的原始类型

      • 零长度的原始批次

      • 递归嵌套类型

      • 联合类型

  • 压缩测试

    • LZ4

    • ZSTD

  • 具有共享字典的批次

生成新的黄金文件#

不时地,有必要添加新的黄金文件,例如当列式格式或 IPC 规范更新时。Archery 提供了一个专门的选项来执行此操作。

建议使用已知版本的 Arrow 实现来生成黄金文件。例如,如果 Arrow C++ 的构建存在于 ./build/release/ 中,可以使用以下命令在 /tmp/gold-files 目录中生成新的黄金文件:

export ARROW_CPP_EXE_PATH=./build/release/
archery integration --with-cpp 1 --write-gold-files=/tmp/gold-files