整合測試#
為了確保 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++,您需要將 -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 建置以取得關於專案 docker compose 設定的更多資訊。
JSON 測試資料格式#
為了跨語言整合測試的目的,提供了 Arrow 欄狀資料的 JSON 表示法。此表示法非標準,但它提供了一種人類可讀的方式來驗證語言實作。
請參閱這裡以取得此 JSON 資料的一些範例。
JSON 整合測試檔案的高階結構如下
資料檔案
{
"schema": /*Schema*/,
"batches": [ /*RecordBatch*/ ],
"dictionaries": [ /*DictionaryBatch*/ ],
}
所有檔案都包含 schema
和 batches
,而 dictionaries
僅在 schema
中存在字典類型欄位時才會出現。
綱要
{
"fields" : [
/* Field */
],
"metadata" : /* Metadata */
}
欄位
{
"name" : "name_of_the_field",
"nullable" : /* boolean */,
"type" : /* Type */,
"children" : [ /* Field */ ],
"dictionary": {
"id": /* integer */,
"indexType": /* Type */,
"isOrdered": /* boolean */
},
"metadata" : /* Metadata */
}
dictionary
屬性僅在 Field
對應於字典類型時才會出現,並且其 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 */
}
浮點數
{
"name" : "floatingpoint",
"precision" : "HALF|SINGLE|DOUBLE"
}
固定大小二進位
{
"name" : "fixedsizebinary",
"byteWidth" : /* byte width */
}
十進位
{
"name" : "decimal",
"precision" : /* integer */,
"scale" : /* integer */
}
時間戳記
{
"name" : "timestamp",
"unit" : "$TIME_UNIT",
"timezone": "$timezone"
}
$TIME_UNIT
是 "SECOND|MILLISECOND|MICROSECOND|NANOSECOND"
之一
“timezone” 是一個選填的字串。
持續時間
{
"name" : "duration",
"unit" : "$TIME_UNIT"
}
日期
{
"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
的 “children” 成員中,作為那裡的一個單個 Field
。例如,對於 int32
的列表,
{
"name": "list_nullable",
"type": {
"name": "list"
},
"nullable": true,
"children": [
{
"name": "item",
"type": {
"name": "int",
"isSigned": true,
"bitWidth": 32
},
"nullable": true,
"children": []
}
]
}
固定大小列表
{
"name": "fixedsizelist",
"listSize": /* integer */
}
這種類型同樣帶有一個長度為 1 的 “children” 陣列。
結構
{
"name": "struct"
}
Field
的 “children” 包含一個 Fields
陣列,其中包含有意義的名稱和類型。
映射
{
"name": "map",
"keysSorted": /* boolean */
}
Field
的 “children” 包含一個單個 struct
欄位,該欄位本身包含 2 個子欄位,分別命名為 “key” 和 “value”。
空值
{
"name": "null"
}
行程結束編碼
{
"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"}
]
}
記錄批次:
{
"count": /* integer number of rows */,
"columns": [ /* FieldData */ ]
}
字典批次:
{
"id": /* integer */,
"data": [ /* RecordBatch */ ]
}
欄位資料:
{
"name": "field_name",
"count" "field_length",
"$BUFFER_TYPE": /* BufferData */
...
"$BUFFER_TYPE": /* BufferData */
"children": [ /* FieldData */ ]
}
綱要中 Field
的 “name” 成員對應於 RecordBatch
的 “columns” 中包含的 FieldData
的 “name”。對於巢狀類型(列表、結構等),Field
的 “children” 中的每個 Field
都有一個 “name”,該 “name” 對應於該 FieldData
的 “children” 內的 FieldData
的 “name”。對於 DictionaryBatch
內的 FieldData
,“name” 欄位不對應於任何內容。
這裡 $BUFFER_TYPE
是 VALIDITY
、OFFSET
(對於可變長度類型,例如字串和列表)、TYPE_ID
(對於聯合)或 DATA
之一。
BufferData
根據緩衝區的類型進行編碼
VALIDITY
:由 1(有效)和 0(空值)組成的 JSON 陣列。即使所有值都為 1,不可為空的Field
的資料仍然具有VALIDITY
陣列。OFFSET
:由整數組成的 JSON 陣列,用於 32 位元偏移量;或由字串格式化的整數組成的 JSON 陣列,用於 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” 內的資料計數將為 28。
對於 “null” 類型,BufferData
不包含任何緩衝區。
Archery 整合測試案例#
透過了解自動化整合測試實際測試的案例,此列表可以更輕鬆地了解未來 Arrow 格式變更可能需要進行哪些手動測試。
整合測試案例有兩種:一種是由 Archery 工具中的資料產生器即時填充的案例,另一種是存在於 arrow-testing 儲存庫中的黃金檔案。
資料產生器測試#
這是使用 archery integration
命令產生和測試的案例的高階描述(請參閱 datagen.py
中的 get_generated_json_files
)
基本類型 - 無批次 - 各種基本值 - 零長度批次 - 字串和二進位大偏移案例
空值類型 * 簡單空值批次
Decimal128
Decimal256
具有各種單位的日期時間
具有各種單位的持續時間
間隔 - MonthDayNano 間隔是一個單獨的案例
映射類型 - 非標準映射
巢狀類型 - 列表 - 結構 - 具有大偏移量的列表
聯合
自訂元數據
具有重複欄位名稱的綱要
字典類型 - 帶符號索引 - 無符號索引 - 巢狀字典
行程結束編碼
二進位視圖和字串視圖
列表視圖和大型列表視圖
擴充類型
黃金檔案整合測試#
預先產生的 json 和 arrow IPC 檔案(包括檔案和串流格式)存在於 arrow-testing 儲存庫的 data/arrow-ipc-stream/integration
目錄中。這些檔案作為黃金檔案,被認為是正確的,可以用於測試。它們在 Archery 工具的程式碼中由 runner.py
引用。以下是它們涵蓋的測試案例
向後相容性
以下案例使用 0.14.1 格式進行測試
日期時間
十進位
字典
間隔
映射
巢狀類型(列表、結構)
基本類型
沒有批次的基本類型
具有零長度批次的基本類型
以下內容使用 0.17.1 格式進行測試
聯合
位元組順序
以下案例使用小端和大端版本進行自動轉換測試
自訂元數據
日期時間
十進位
decimal256
字典
具有無符號索引的字典
具有重複欄位名稱的記錄批次
擴充類型
間隔類型
映射類型
非標準映射資料
巢狀類型(列表、結構)
巢狀字典
巢狀大偏移量類型
空值
基本類型資料
大偏移量二進位和字串
不包含批次的基本類型
具有零長度批次的基本類型
遞迴巢狀類型
聯合類型
壓縮測試
LZ4
ZSTD
具有共享字典的批次