arrow/util/test_util.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//! Utils to make testing easier
19
20use rand::{RngExt, SeedableRng, rngs::StdRng};
21use std::{env, error::Error, fs, io::Write, path::PathBuf};
22
23/// Returns a vector of size `n`, filled with randomly generated bytes.
24pub fn random_bytes(n: usize) -> Vec<u8> {
25 let mut result = vec![];
26 let mut rng = seedable_rng();
27 for _ in 0..n {
28 result.push(rng.random_range(0..255));
29 }
30 result
31}
32
33/// Returns fixed seedable RNG
34pub fn seedable_rng() -> StdRng {
35 StdRng::seed_from_u64(42)
36}
37
38/// Returns file handle for a temp file in 'target' directory with a provided content
39///
40/// TODO: Originates from `parquet` utils, can be merged in [ARROW-4064]
41pub fn get_temp_file(file_name: &str, content: &[u8]) -> fs::File {
42 // build tmp path to a file in "target/debug/testdata"
43 let dir = env::current_dir().unwrap().join("target/debug/testdata");
44 fs::create_dir_all(&dir).unwrap();
45 let path_buf = dir.join(file_name);
46
47 // write file content
48 let mut tmp_file = fs::File::create(path_buf.as_path()).unwrap();
49 tmp_file.write_all(content).unwrap();
50 tmp_file.sync_all().unwrap();
51
52 // return file handle for both read and write
53 let file = fs::OpenOptions::new()
54 .read(true)
55 .write(true)
56 .open(path_buf.as_path());
57 assert!(file.is_ok());
58 file.unwrap()
59}
60
61/// Returns the arrow test data directory, which is by default stored
62/// in a git submodule rooted at `arrow/testing/data`.
63///
64/// The default can be overridden by the optional environment
65/// variable `ARROW_TEST_DATA`
66///
67/// Example:
68/// ```
69/// let testdata = arrow::util::test_util::arrow_test_data();
70/// let csvdata = format!("{}/csv/aggregate_test_100.csv", testdata);
71/// assert!(std::path::PathBuf::from(csvdata).exists());
72/// ```
73///
74/// # Panics
75///
76/// Panics if the directory can not be found
77pub fn arrow_test_data() -> String {
78 match get_data_dir("ARROW_TEST_DATA", "../testing/data") {
79 Ok(pb) => pb.display().to_string(),
80 Err(err) => panic!("failed to get arrow data dir: {err}"),
81 }
82}
83
84/// Returns the parquest test data directory, which is by default
85/// stored in a git submodule rooted at
86/// `arrow/parquest-testing/data`.
87///
88/// The default can be overridden by the optional environment variable
89/// `PARQUET_TEST_DATA`
90///
91/// Example:
92/// ```
93/// let testdata = arrow::util::test_util::parquet_test_data();
94/// let filename = format!("{}/binary.parquet", testdata);
95/// assert!(std::path::PathBuf::from(filename).exists());
96/// ```
97///
98/// # Panics
99///
100/// Panics if the directory can not be found
101pub fn parquet_test_data() -> String {
102 match get_data_dir("PARQUET_TEST_DATA", "../parquet-testing/data") {
103 Ok(pb) => pb.display().to_string(),
104 Err(err) => panic!("failed to get parquet data dir: {err}"),
105 }
106}
107
108/// Returns a directory path for finding test data.
109///
110/// udf_env: name of an environment variable
111///
112/// submodule_dir: fallback path (relative to CARGO_MANIFEST_DIR)
113///
114/// Returns either:
115/// The path referred to in `udf_env` if that variable is set and refers to a directory
116/// The submodule_data directory relative to CARGO_MANIFEST_PATH
117fn get_data_dir(udf_env: &str, submodule_data: &str) -> Result<PathBuf, Box<dyn Error>> {
118 // Try user defined env.
119 if let Ok(dir) = env::var(udf_env) {
120 let trimmed = dir.trim().to_string();
121 if !trimmed.is_empty() {
122 let pb = PathBuf::from(trimmed);
123 return if pb.is_dir() {
124 Ok(pb)
125 } else {
126 Err(format!(
127 "the data dir `{}` defined by env {} not found",
128 pb.display(),
129 udf_env
130 )
131 .into())
132 };
133 }
134 }
135
136 // The env is undefined or its value is trimmed to empty, let's try default dir.
137
138 // env "CARGO_MANIFEST_DIR" is "the directory containing the manifest of your package",
139 // set by `cargo run` or `cargo test`, see:
140 // https://doc.rust-lang.org/cargo/reference/environment-variables.html
141 let dir = env!("CARGO_MANIFEST_DIR");
142
143 let pb = PathBuf::from(dir).join(submodule_data);
144 if pb.is_dir() {
145 Ok(pb)
146 } else {
147 Err(format!(
148 "env `{}` is undefined or has empty value, and the pre-defined data dir `{}` not found\n\
149 HINT: try running `git submodule update --init`",
150 udf_env,
151 pb.display(),
152 ).into())
153 }
154}
155
156/// An iterator that is untruthful about its actual length
157#[derive(Debug, Clone)]
158pub struct BadIterator<T> {
159 /// where the iterator currently is
160 cur: usize,
161 /// How many items will this iterator *actually* make
162 limit: usize,
163 /// How many items this iterator claims it will make
164 claimed: usize,
165 /// The items to return. If there are fewer items than `limit`
166 /// they will be repeated
167 pub items: Vec<T>,
168}
169
170impl<T> BadIterator<T> {
171 /// Create a new iterator for `<limit>` items, but that reports to
172 /// produce `<claimed>` items. Must provide at least 1 item.
173 ///
174 /// # Panics
175 ///
176 /// Panics if `items` is empty
177 pub fn new(limit: usize, claimed: usize, items: Vec<T>) -> Self {
178 assert!(!items.is_empty());
179 Self {
180 cur: 0,
181 limit,
182 claimed,
183 items,
184 }
185 }
186}
187
188impl<T: Clone> Iterator for BadIterator<T> {
189 type Item = T;
190
191 fn next(&mut self) -> Option<Self::Item> {
192 if self.cur < self.limit {
193 let next_item_idx = self.cur % self.items.len();
194 let next_item = self.items[next_item_idx].clone();
195 self.cur += 1;
196 Some(next_item)
197 } else {
198 None
199 }
200 }
201
202 /// report whatever the iterator says to
203 fn size_hint(&self) -> (usize, Option<usize>) {
204 (0, Some(self.claimed))
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211
212 #[test]
213 fn test_data_dir() {
214 let udf_env = "get_data_dir";
215 let cwd = env::current_dir().unwrap();
216
217 let existing_pb = cwd.join("..");
218 let existing = existing_pb.display().to_string();
219 let existing_str = existing.as_str();
220
221 let non_existing = cwd.join("non-existing-dir").display().to_string();
222 let non_existing_str = non_existing.as_str();
223
224 unsafe { env::set_var(udf_env, non_existing_str) };
225 let res = get_data_dir(udf_env, existing_str);
226 assert!(res.is_err());
227
228 unsafe { env::set_var(udf_env, "") };
229 let res = get_data_dir(udf_env, existing_str);
230 assert!(res.is_ok());
231 assert_eq!(res.unwrap(), existing_pb);
232
233 unsafe { env::set_var(udf_env, " ") };
234 let res = get_data_dir(udf_env, existing_str);
235 assert!(res.is_ok());
236 assert_eq!(res.unwrap(), existing_pb);
237
238 unsafe { env::set_var(udf_env, existing_str) };
239 let res = get_data_dir(udf_env, existing_str);
240 assert!(res.is_ok());
241 assert_eq!(res.unwrap(), existing_pb);
242
243 unsafe { env::remove_var(udf_env) };
244 let res = get_data_dir(udf_env, non_existing_str);
245 assert!(res.is_err());
246
247 let res = get_data_dir(udf_env, existing_str);
248 assert!(res.is_ok());
249 assert_eq!(res.unwrap(), existing_pb);
250 }
251
252 #[test]
253 fn test_happy() {
254 let res = arrow_test_data();
255 assert!(PathBuf::from(res).is_dir());
256
257 let res = parquet_test_data();
258 assert!(PathBuf::from(res).is_dir());
259 }
260}