1use std::fs::{File, create_dir_all, read_dir};
2use std::io::{Read, Seek, SeekFrom, Write};
3use std::path::{Path, PathBuf};
4use std::sync::Arc;
5
6use itertools::multizip;
7use rand::prelude::*;
8use tempfile::TempDir;
9use xet_client::cas_client::{Client, LocalClient};
10#[cfg(feature = "simulation")]
11use xet_client::cas_client::{LocalTestServer, LocalTestServerBuilder};
12use xet_runtime::core::XetContext;
13
14use super::configurations::TranslatorConfig;
15use super::data_client::clean_file;
16use super::file_cleaner::Sha256Policy;
17use super::{FileDownloadSession, FileUploadSession, XetFileInfo};
18
19#[derive(Debug, Clone, Copy)]
28pub enum HydrationMode {
29 DirectClient,
30 #[cfg(feature = "simulation")]
31 ServerV2,
32 #[cfg(feature = "simulation")]
33 ServerV1Fallback,
34 #[cfg(feature = "simulation")]
35 ServerMaxRanges2,
36}
37
38impl HydrationMode {
39 pub fn all() -> &'static [HydrationMode] {
40 &[
41 HydrationMode::DirectClient,
42 #[cfg(feature = "simulation")]
43 HydrationMode::ServerV2,
44 #[cfg(feature = "simulation")]
45 HydrationMode::ServerV1Fallback,
46 #[cfg(feature = "simulation")]
47 HydrationMode::ServerMaxRanges2,
48 ]
49 }
50
51 pub fn uses_server(&self) -> bool {
52 match self {
53 HydrationMode::DirectClient => false,
54 #[cfg(feature = "simulation")]
55 _ => true,
56 }
57 }
58}
59
60impl std::fmt::Display for HydrationMode {
61 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62 match self {
63 HydrationMode::DirectClient => write!(f, "direct_client"),
64 #[cfg(feature = "simulation")]
65 HydrationMode::ServerV2 => write!(f, "server_v2"),
66 #[cfg(feature = "simulation")]
67 HydrationMode::ServerV1Fallback => write!(f, "server_v1_fallback"),
68 #[cfg(feature = "simulation")]
69 HydrationMode::ServerMaxRanges2 => write!(f, "server_max_ranges_2"),
70 }
71 }
72}
73
74pub fn create_random_file(path: impl AsRef<Path>, size: usize, seed: u64) -> usize {
77 let path = path.as_ref();
78
79 let dir = path.parent().unwrap();
80
81 create_dir_all(dir).unwrap();
83
84 let mut rng = StdRng::seed_from_u64(seed);
85
86 let mut file = File::create(path).unwrap();
88
89 let mut buffer = vec![0_u8; size];
90 rng.fill_bytes(&mut buffer);
91
92 file.write_all(&buffer).unwrap();
93
94 size
95}
96
97pub fn create_random_files(dir: impl AsRef<Path>, files: &[(impl AsRef<str>, usize)], seed: u64) -> usize {
100 let dir = dir.as_ref();
101
102 let mut total_bytes = 0;
103 let mut rng = SmallRng::seed_from_u64(seed);
104
105 for (file_name, size) in files {
106 total_bytes += create_random_file(dir.join(file_name.as_ref()), *size, rng.random());
107 }
108 total_bytes
109}
110
111pub fn create_random_multipart_file(path: impl AsRef<Path>, segments: &[(usize, u64)]) -> usize {
114 let path = path.as_ref();
115 let dir = path.parent().unwrap();
116
117 create_dir_all(dir).unwrap();
119
120 let mut file = File::create(path).unwrap();
122
123 let mut total_size = 0;
124 for &(size, seed) in segments {
125 let mut rng = StdRng::seed_from_u64(seed);
126
127 let mut buffer = vec![0_u8; size];
128 rng.fill_bytes(&mut buffer);
129 file.write_all(&buffer).unwrap();
130 total_size += size;
131 }
132 total_size
133}
134
135pub fn verify_directories_match(dir1: impl AsRef<Path>, dir2: impl AsRef<Path>) {
138 let dir1 = dir1.as_ref();
139 let dir2 = dir2.as_ref();
140
141 let mut files_in_dir1 = Vec::new();
142 for entry in read_dir(dir1).unwrap() {
143 let entry = entry.unwrap();
144 assert!(entry.file_type().unwrap().is_file());
145 files_in_dir1.push(entry.file_name());
146 }
147
148 let mut files_in_dir2 = Vec::new();
149 for entry in read_dir(dir2).unwrap() {
150 let entry = entry.unwrap();
151 assert!(entry.file_type().unwrap().is_file());
152 files_in_dir2.push(entry.file_name());
153 }
154
155 files_in_dir1.sort();
156 files_in_dir2.sort();
157
158 if files_in_dir1 != files_in_dir2 {
159 panic!(
160 "Directories differ: file sets are not the same.\n \
161 dir1: {files_in_dir1:?}\n dir2: {files_in_dir2:?}"
162 );
163 }
164
165 for file_name in &files_in_dir1 {
167 let path1 = dir1.join(file_name);
168 let path2 = dir2.join(file_name);
169
170 let mut buf1 = Vec::new();
171 let mut buf2 = Vec::new();
172
173 File::open(&path1).unwrap().read_to_end(&mut buf1).unwrap();
174 File::open(&path2).unwrap().read_to_end(&mut buf2).unwrap();
175
176 if buf1 != buf2 {
177 panic!(
178 "File contents differ for {file_name:?}\n \
179 dir1 path: {path1:?}\n dir2 path: {path2:?}"
180 );
181 }
182 }
183}
184
185pub struct HydrateDehydrateTest {
186 _temp_dir: TempDir,
187 pub cas_dir: PathBuf,
188 pub src_dir: PathBuf,
189 pub ptr_dir: PathBuf,
190 pub dest_dir: PathBuf,
191 ctx: XetContext,
192 use_test_server: bool,
193 #[cfg(feature = "simulation")]
195 test_server: Option<LocalTestServer>,
196}
197
198impl Default for HydrateDehydrateTest {
199 fn default() -> Self {
200 Self::new(false)
201 }
202}
203
204impl HydrateDehydrateTest {
205 pub fn new(use_test_server: bool) -> Self {
211 let _temp_dir = TempDir::new().unwrap();
212 let temp_path = _temp_dir.path();
213
214 let cas_dir = temp_path.join("cas");
215 let src_dir = temp_path.join("src");
216 let ptr_dir = temp_path.join("pointers");
217 let dest_dir = temp_path.join("dest");
218
219 std::fs::create_dir_all(&cas_dir).unwrap();
220 std::fs::create_dir_all(&src_dir).unwrap();
221 std::fs::create_dir_all(&ptr_dir).unwrap();
222 std::fs::create_dir_all(&dest_dir).unwrap();
223
224 Self {
225 cas_dir,
226 src_dir,
227 ptr_dir,
228 dest_dir,
229 ctx: XetContext::default().expect("xet context"),
230 _temp_dir,
231 use_test_server,
232 #[cfg(feature = "simulation")]
233 test_server: None,
234 }
235 }
236
237 pub fn for_mode(mode: HydrationMode) -> Self {
239 Self::new(mode.uses_server())
240 }
241
242 pub async fn apply_hydration_mode(&mut self, mode: HydrationMode) {
245 match mode {
246 HydrationMode::DirectClient => {},
247 #[cfg(feature = "simulation")]
248 HydrationMode::ServerV2 => {
249 self.ensure_server_created().await;
250 },
251 #[cfg(feature = "simulation")]
252 HydrationMode::ServerV1Fallback => {
253 self.ensure_server_created().await;
254 self.test_server.as_ref().unwrap().client().disable_v2_reconstruction(404);
255 },
256 #[cfg(feature = "simulation")]
257 HydrationMode::ServerMaxRanges2 => {
258 self.ensure_server_created().await;
259 self.test_server.as_ref().unwrap().client().set_max_ranges_per_fetch(2);
260 },
261 }
262 }
263
264 #[cfg(feature = "simulation")]
267 pub async fn ensure_server_created(&mut self) {
268 if self.use_test_server && self.test_server.is_none() {
269 let local_client = LocalClient::new(self.ctx.clone(), self.cas_dir.join("xet/xorbs"))
270 .await
271 .unwrap();
272 self.test_server = Some(LocalTestServerBuilder::new().with_client(local_client).start().await);
273 }
274 }
275
276 #[cfg(feature = "simulation")]
278 pub fn test_server(&self) -> Option<&LocalTestServer> {
279 self.test_server.as_ref()
280 }
281
282 async fn get_or_create_client(&mut self) -> Arc<dyn Client> {
284 if self.use_test_server {
285 #[cfg(feature = "simulation")]
286 {
287 if self.test_server.is_none() {
288 let local_client = LocalClient::new(self.ctx.clone(), self.cas_dir.join("xet/xorbs"))
289 .await
290 .unwrap();
291 self.test_server = Some(LocalTestServerBuilder::new().with_client(local_client).start().await);
292 }
293 self.test_server.as_ref().unwrap().remote_client().clone() as Arc<dyn Client>
294 }
295 #[cfg(not(feature = "simulation"))]
296 {
297 panic!("test server requires the 'simulation' feature");
298 }
299 } else {
300 LocalClient::new(self.ctx.clone(), self.cas_dir.join("xet/xorbs"))
301 .await
302 .unwrap() as Arc<dyn Client>
303 }
304 }
305
306 pub async fn new_upload_session(&self) -> Arc<FileUploadSession> {
307 let config = Arc::new(TranslatorConfig::local_config(&self.ctx, &self.cas_dir).unwrap());
308 FileUploadSession::new(config.clone()).await.unwrap()
309 }
310
311 pub async fn clean_all_files(&self, upload_session: &Arc<FileUploadSession>, sequential: bool) {
312 create_dir_all(&self.ptr_dir).unwrap();
313
314 if sequential {
315 for entry in read_dir(&self.src_dir).unwrap() {
316 let entry = entry.unwrap();
317 let out_file = self.ptr_dir.join(entry.file_name());
318 let upload_session = upload_session.clone();
319
320 if sequential {
321 let (pf, metrics) = clean_file(upload_session.clone(), entry.path(), Sha256Policy::Compute)
322 .await
323 .unwrap();
324 assert_eq!({ metrics.total_bytes }, entry.metadata().unwrap().len());
325 std::fs::write(out_file, pf.as_pointer_file().unwrap().as_bytes()).unwrap();
326
327 upload_session.checkpoint().await.unwrap();
329 }
330 }
331 } else {
332 let files: Vec<PathBuf> = read_dir(&self.src_dir)
333 .unwrap()
334 .map(|entry| self.src_dir.join(entry.unwrap().file_name()))
335 .collect();
336
337 let files_and_sha256 = multizip((files.iter(), std::iter::repeat_with(|| Sha256Policy::Compute)));
338
339 let clean_results = upload_session.upload_files(files_and_sha256).await.unwrap();
340
341 for (i, xf) in clean_results.into_iter().enumerate() {
342 std::fs::write(self.ptr_dir.join(files[i].file_name().unwrap()), serde_json::to_string(&xf).unwrap())
343 .unwrap();
344 }
345 }
346 }
347
348 pub async fn dehydrate(&mut self, sequential: bool) {
349 let upload_session = self.new_upload_session().await;
350 self.clean_all_files(&upload_session, sequential).await;
351
352 upload_session.finalize().await.unwrap();
353 }
354
355 pub async fn hydrate(&mut self) {
356 let client = self.get_or_create_client().await;
357 let session = FileDownloadSession::from_client(&self.ctx, client, None);
358
359 for entry in read_dir(&self.ptr_dir).unwrap() {
360 let entry = entry.unwrap();
361 let out_filename = self.dest_dir.join(entry.file_name());
362
363 let xf: XetFileInfo = serde_json::from_reader(File::open(entry.path()).unwrap()).unwrap();
364 let (_id, _) = session.download_file(&xf, &out_filename).await.unwrap();
365 }
366 }
367
368 pub async fn hydrate_partitioned_writers(&mut self, partitions: usize) {
369 let client = self.get_or_create_client().await;
370 let session = FileDownloadSession::from_client(&self.ctx, client, None);
371
372 for entry in read_dir(&self.ptr_dir).unwrap() {
373 let entry = entry.unwrap();
374 let out_filename = self.dest_dir.join(entry.file_name());
375 let xf: XetFileInfo = serde_json::from_reader(File::open(entry.path()).unwrap()).unwrap();
376 let file_size = xf.file_size().expect("file size required for partitioned hydration");
377
378 let out_file = File::create(&out_filename).unwrap();
379 out_file.set_len(file_size).unwrap();
380
381 if file_size == 0 {
382 continue;
383 }
384
385 let partition_count = partitions.max(1) as u64;
386 let mut tasks = Vec::new();
387
388 for idx in 0..partition_count {
389 let start = (idx * file_size) / partition_count;
390 let end = ((idx + 1) * file_size) / partition_count;
391
392 if start == end {
393 continue;
394 }
395
396 let session = session.clone();
397 let xf = xf.clone();
398 let out_filename = out_filename.clone();
399 tasks.push(tokio::spawn(async move {
400 let mut writer = std::fs::OpenOptions::new().write(true).open(out_filename).unwrap();
401 writer.seek(SeekFrom::Start(start)).unwrap();
402 session.download_to_writer(&xf, start..end, writer).await
403 }));
404 }
405
406 for task in tasks {
407 task.await.unwrap().unwrap();
408 }
409 }
410 }
411
412 pub async fn hydrate_stream(&mut self) {
413 let client = self.get_or_create_client().await;
414 let session = FileDownloadSession::from_client(&self.ctx, client, None);
415
416 for entry in read_dir(&self.ptr_dir).unwrap() {
417 let entry = entry.unwrap();
418 let out_filename = self.dest_dir.join(entry.file_name());
419
420 let xf: XetFileInfo = serde_json::from_reader(File::open(entry.path()).unwrap()).unwrap();
421 let (_id, mut stream) = session.download_stream(&xf, None).await.unwrap();
422
423 let mut file = File::create(&out_filename).unwrap();
424 while let Some(chunk) = stream.next().await.unwrap() {
425 file.write_all(&chunk).unwrap();
426 }
427 }
428 }
429
430 pub fn verify_src_dest_match(&self) {
431 verify_directories_match(&self.src_dir, &self.dest_dir);
432 }
433}
434
435pub struct TestEnvironment {
440 _temp_dir: TempDir,
441 pub base_dir: PathBuf,
442 pub ctx: XetContext,
443 pub config: Arc<super::configurations::TranslatorConfig>,
444 #[cfg(feature = "simulation")]
445 _server: Option<LocalTestServer>,
446}
447
448impl TestEnvironment {
449 pub async fn new() -> Self {
450 let temp_dir = TempDir::new().unwrap();
451 let base_dir = temp_dir.path().to_path_buf();
452 let ctx = XetContext::default().unwrap();
453
454 #[cfg(feature = "simulation")]
455 let (config, server) = {
456 let server = LocalTestServerBuilder::new().start().await;
457 let config = Arc::new(
458 super::configurations::TranslatorConfig::test_server_config(&ctx, server.http_endpoint(), &base_dir)
459 .unwrap(),
460 );
461 (config, Some(server))
462 };
463
464 #[cfg(not(feature = "simulation"))]
465 let config = Arc::new(super::configurations::TranslatorConfig::local_config(&ctx, &base_dir).unwrap());
466
467 Self {
468 _temp_dir: temp_dir,
469 base_dir,
470 ctx,
471 config,
472 #[cfg(feature = "simulation")]
473 _server: server,
474 }
475 }
476}