Skip to main content

iceberg/
test_utils.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! Test utilities.
19//! This module is pub just for internal testing.
20//! It is subject to change and is not intended to be used by external users.
21
22use std::sync::{Arc, OnceLock};
23
24use arrow_array::RecordBatch;
25use expect_test::Expect;
26use itertools::Itertools;
27
28use crate::TableIdent;
29use crate::encryption::SensitiveBytes;
30use crate::encryption::kms::{KeyManagementClient, MemoryKeyManagementClient};
31use crate::io::FileIO;
32use crate::runtime::Runtime;
33use crate::spec::TableMetadata;
34use crate::table::Table;
35
36/// Returns a process-wide [`Runtime`] suitable for tests that need to construct
37/// a [`Table`](crate::table::Table) outside a tokio context.
38///
39/// The returned [`Runtime`] wraps a single shared multi-thread tokio runtime
40/// that is lazily built on first call and lives until process exit. Cloning is
41/// cheap, so test code can call this every time it needs a runtime to feed
42/// into [`TableBuilder::runtime`](crate::table::TableBuilder::runtime).
43pub fn test_runtime() -> Runtime {
44    static TOKIO_RT: OnceLock<tokio::runtime::Runtime> = OnceLock::new();
45    let tokio_rt = TOKIO_RT.get_or_init(|| {
46        tokio::runtime::Builder::new_multi_thread()
47            .enable_all()
48            .build()
49            .expect("failed to build test tokio runtime")
50    });
51    Runtime::new(tokio_rt)
52}
53
54/// Snapshot testing to check the resulting record batch.
55///
56/// - `expected_schema/data`: put `expect![[""]]` as a placeholder,
57///   and then run test with `UPDATE_EXPECT=1 cargo test` to automatically update the result,
58///   or use rust-analyzer (see [video](https://github.com/rust-analyzer/expect-test)).
59///   Check the doc of [`expect_test`] for more details.
60/// - `ignore_check_columns`: Some columns are not stable, so we can skip them.
61/// - `sort_column`: The order of the data might be non-deterministic, so we can sort it by a column.
62pub fn check_record_batches(
63    record_batches: Vec<RecordBatch>,
64    expected_schema: Expect,
65    expected_data: Expect,
66    ignore_check_columns: &[&str],
67    sort_column: Option<&str>,
68) {
69    assert!(!record_batches.is_empty(), "Empty record batches");
70
71    // Combine record batches using the first batch's schema
72    let first_batch = record_batches.first().unwrap();
73    let record_batch =
74        arrow_select::concat::concat_batches(&first_batch.schema(), &record_batches).unwrap();
75
76    let mut columns = record_batch.columns().to_vec();
77    if let Some(sort_column) = sort_column {
78        let column = record_batch.column_by_name(sort_column).unwrap();
79        let indices = arrow_ord::sort::sort_to_indices(column, None, None).unwrap();
80        columns = columns
81            .iter()
82            .map(|column| arrow_select::take::take(column.as_ref(), &indices, None).unwrap())
83            .collect_vec();
84    }
85
86    expected_schema.assert_eq(&format!(
87        "{}",
88        record_batch.schema().fields().iter().format(",\n")
89    ));
90    expected_data.assert_eq(&format!(
91        "{}",
92        record_batch
93            .schema()
94            .fields()
95            .iter()
96            .zip_eq(columns)
97            .map(|(field, column)| {
98                if ignore_check_columns.contains(&field.name().as_str()) {
99                    format!("{}: (skipped)", field.name())
100                } else {
101                    format!("{}: {:?}", field.name(), column)
102                }
103            })
104            .format(",\n")
105    ));
106}
107
108/// Build a table backed by the V3 encryption fixture and an in-memory KMS,
109/// so it has an [`EncryptionManager`](crate::encryption::EncryptionManager).
110///
111/// The fixture's snapshot references an encrypted manifest list; its bytes
112/// (the `manifest-list-v3-encrypted.avro` testdata, an encrypted empty list)
113/// are seeded into the in-memory `FileIO` at that path so callers can read
114/// the current snapshot's manifest list.
115pub async fn make_encrypted_table() -> Table {
116    let metadata_json = std::fs::read_to_string(format!(
117        "{}/testdata/table_metadata/TableMetadataV3ValidEncryption.json",
118        env!("CARGO_MANIFEST_DIR"),
119    ))
120    .unwrap();
121    let metadata: TableMetadata = serde_json::from_str(&metadata_json).unwrap();
122
123    let kms: Arc<dyn KeyManagementClient> = {
124        let k = MemoryKeyManagementClient::new();
125        k.add_master_key_bytes(
126            "master-1",
127            SensitiveBytes::new([
128                0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
129                0x0e, 0x0f,
130            ]),
131        )
132        .unwrap();
133        Arc::new(k)
134    };
135
136    let file_io = FileIO::new_with_memory();
137
138    // Seed the encrypted (empty) manifest list at the path the snapshot references.
139    let manifest_list_bytes = std::fs::read(format!(
140        "{}/testdata/manifests_lists/manifest-list-v3-encrypted.avro",
141        env!("CARGO_MANIFEST_DIR"),
142    ))
143    .unwrap();
144    file_io
145        .new_output(metadata.current_snapshot().unwrap().manifest_list())
146        .unwrap()
147        .write(manifest_list_bytes.into())
148        .await
149        .unwrap();
150
151    Table::builder()
152        .metadata(metadata)
153        .metadata_location("memory:///table/metadata/v1.json")
154        .identifier(TableIdent::from_strs(["ns1", "test1"]).unwrap())
155        .file_io(file_io)
156        .kms_client(kms)
157        .runtime(test_runtime())
158        .build()
159        .unwrap()
160}