1use std::{
62 collections::HashMap,
63 fs::File,
64 path::{Path, PathBuf},
65};
66
67use walkdir::WalkDir;
68
69use crate::Error;
70
71pub const IMAGE_EXTENSIONS: &[&str] = &[
73 "jpg",
74 "jpeg",
75 "png",
76 "camera.jpeg",
77 "camera.png",
78 "camera.jpg",
79];
80
81#[cfg(feature = "polars")]
118pub fn resolve_arrow_files(arrow_path: &Path) -> Result<HashMap<String, PathBuf>, Error> {
119 use polars::prelude::*;
120
121 let mut file = File::open(arrow_path).map_err(|e| {
122 Error::InvalidParameters(format!("Cannot open Arrow file {:?}: {}", arrow_path, e))
123 })?;
124
125 let df = IpcReader::new(&mut file).finish().map_err(|e| {
126 Error::InvalidParameters(format!("Failed to read Arrow file {:?}: {}", arrow_path, e))
127 })?;
128
129 let names = df
131 .column("name")
132 .map_err(|e| Error::InvalidParameters(format!("Missing 'name' column: {}", e)))?
133 .str()
134 .map_err(|e| Error::InvalidParameters(format!("Invalid 'name' column type: {}", e)))?;
135
136 let frames = df.column("frame").ok();
138
139 let mut result = HashMap::new();
140
141 for idx in 0..df.height() {
142 let name = match names.get(idx) {
144 Some(n) => n.to_string(),
145 None => continue, };
147
148 if result.contains_key(&name) {
150 continue;
151 }
152
153 let frame = frames.and_then(|col| {
155 col.u64()
157 .ok()
158 .and_then(|s| s.get(idx))
159 .or_else(|| col.u32().ok().and_then(|s| s.get(idx).map(|v| v as u64)))
160 });
161
162 let relative_path = if let Some(frame_num) = frame {
164 PathBuf::from(&name).join(format!("{}_{:03}.camera.jpeg", name, frame_num))
167 } else {
168 PathBuf::from(format!("{}.camera.jpeg", name))
170 };
171
172 result.insert(name, relative_path);
173 }
174
175 Ok(result)
176}
177
178#[derive(Debug, Clone)]
180pub struct ResolvedFile {
181 pub name: String,
183 pub frame: Option<u64>,
185 pub path: Option<PathBuf>,
187 pub expected_path: PathBuf,
189}
190
191#[cfg(feature = "polars")]
225pub fn resolve_files_with_container(
226 arrow_path: &Path,
227 sensor_container: &Path,
228) -> Result<Vec<ResolvedFile>, Error> {
229 use polars::prelude::*;
230
231 let mut file = File::open(arrow_path).map_err(|e| {
232 Error::InvalidParameters(format!("Cannot open Arrow file {:?}: {}", arrow_path, e))
233 })?;
234
235 let df = IpcReader::new(&mut file).finish().map_err(|e| {
236 Error::InvalidParameters(format!("Failed to read Arrow file {:?}: {}", arrow_path, e))
237 })?;
238
239 let file_index = build_file_index(sensor_container)?;
241
242 let names = df
244 .column("name")
245 .map_err(|e| Error::InvalidParameters(format!("Missing 'name' column: {}", e)))?
246 .str()
247 .map_err(|e| Error::InvalidParameters(format!("Invalid 'name' column type: {}", e)))?;
248
249 let frames = df.column("frame").ok();
251
252 let mut result = Vec::new();
253 let mut seen_samples: HashMap<String, bool> = HashMap::new();
254
255 for idx in 0..df.height() {
256 let name = match names.get(idx) {
257 Some(n) => n.to_string(),
258 None => continue,
259 };
260
261 let frame = frames.and_then(|col| {
263 col.u64()
264 .ok()
265 .and_then(|s| s.get(idx))
266 .or_else(|| col.u32().ok().and_then(|s| s.get(idx).map(|v| v as u64)))
267 });
268
269 let sample_key = match frame {
270 Some(f) => format!("{}_{}", name, f),
271 None => name.clone(),
272 };
273
274 if seen_samples.contains_key(&sample_key) {
276 continue;
277 }
278 seen_samples.insert(sample_key.clone(), true);
279
280 let expected_path = if let Some(frame_num) = frame {
282 PathBuf::from(&name).join(format!("{}_{:03}.camera.jpeg", name, frame_num))
283 } else {
284 PathBuf::from(format!("{}.camera.jpeg", name))
285 };
286
287 let actual_path = find_matching_file(&file_index, &name, frame);
289
290 result.push(ResolvedFile {
291 name,
292 frame,
293 path: actual_path,
294 expected_path,
295 });
296 }
297
298 Ok(result)
299}
300
301fn build_file_index(root: &Path) -> Result<HashMap<String, PathBuf>, Error> {
303 let mut index = HashMap::new();
304
305 if !root.exists() {
306 return Ok(index);
307 }
308
309 for entry in WalkDir::new(root)
310 .into_iter()
311 .filter_map(|e| e.ok())
312 .filter(|e| e.file_type().is_file())
313 {
314 let path = entry.path().to_path_buf();
315 if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
316 index.insert(filename.to_lowercase(), path.clone());
318
319 if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
321 let clean_stem = stem.strip_suffix(".camera").unwrap_or(stem).to_lowercase();
323 index.entry(clean_stem).or_insert_with(|| path.clone());
324 }
325 }
326 }
327
328 Ok(index)
329}
330
331fn find_matching_file(
333 index: &HashMap<String, PathBuf>,
334 name: &str,
335 frame: Option<u64>,
336) -> Option<PathBuf> {
337 let search_key = match frame {
338 Some(f) => format!("{}_{:03}", name, f).to_lowercase(),
339 None => name.to_lowercase(),
340 };
341
342 for ext in IMAGE_EXTENSIONS {
344 let key = format!("{}.{}", search_key, ext);
345 if let Some(path) = index.get(&key) {
346 return Some(path.clone());
347 }
348 }
349
350 if let Some(path) = index.get(&search_key) {
352 return Some(path.clone());
353 }
354
355 None
356}
357
358#[derive(Debug, Clone, PartialEq, Eq)]
360pub enum ValidationIssue {
361 MissingArrowFile { expected: PathBuf },
363 MissingSensorContainer { expected: PathBuf },
365 MissingFile { name: String, expected: PathBuf },
367 UnreferencedFile { path: PathBuf },
369 InvalidStructure { message: String },
371}
372
373impl std::fmt::Display for ValidationIssue {
374 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
375 match self {
376 ValidationIssue::MissingArrowFile { expected } => {
377 write!(f, "Missing Arrow file: {:?}", expected)
378 }
379 ValidationIssue::MissingSensorContainer { expected } => {
380 write!(f, "Missing sensor container directory: {:?}", expected)
381 }
382 ValidationIssue::MissingFile { name, expected } => {
383 write!(f, "Missing file for sample '{}': {:?}", name, expected)
384 }
385 ValidationIssue::UnreferencedFile { path } => {
386 write!(f, "Unreferenced file in container: {:?}", path)
387 }
388 ValidationIssue::InvalidStructure { message } => {
389 write!(f, "Invalid structure: {}", message)
390 }
391 }
392 }
393}
394
395#[cfg(feature = "polars")]
428pub fn validate_dataset_structure(dataset_dir: &Path) -> Result<Vec<ValidationIssue>, Error> {
429 let mut issues = Vec::new();
430
431 let dataset_name = dataset_dir
433 .file_name()
434 .and_then(|n| n.to_str())
435 .ok_or_else(|| Error::InvalidParameters("Invalid dataset directory path".to_owned()))?;
436
437 let arrow_path = dataset_dir.join(format!("{}.arrow", dataset_name));
439 if !arrow_path.exists() {
440 issues.push(ValidationIssue::MissingArrowFile {
441 expected: arrow_path.clone(),
442 });
443 return Ok(issues);
445 }
446
447 let container_path = dataset_dir.join(dataset_name);
449 if !container_path.exists() {
450 issues.push(ValidationIssue::MissingSensorContainer {
451 expected: container_path.clone(),
452 });
453 return Ok(issues);
455 }
456
457 let resolved = resolve_files_with_container(&arrow_path, &container_path)?;
459
460 let mut referenced_files: std::collections::HashSet<PathBuf> = std::collections::HashSet::new();
462
463 for file in &resolved {
464 match &file.path {
465 Some(path) => {
466 referenced_files.insert(path.clone());
467 }
468 None => {
469 issues.push(ValidationIssue::MissingFile {
470 name: file.name.clone(),
471 expected: file.expected_path.clone(),
472 });
473 }
474 }
475 }
476
477 for entry in WalkDir::new(&container_path)
479 .into_iter()
480 .filter_map(|e| e.ok())
481 .filter(|e| e.file_type().is_file())
482 {
483 let path = entry.path().to_path_buf();
484
485 let is_image = path
487 .extension()
488 .and_then(|e| e.to_str())
489 .map(|e| {
490 matches!(
491 e.to_lowercase().as_str(),
492 "jpg" | "jpeg" | "png" | "pcd" | "bin"
493 )
494 })
495 .unwrap_or(false);
496
497 if is_image && !referenced_files.contains(&path) {
498 issues.push(ValidationIssue::UnreferencedFile { path });
499 }
500 }
501
502 Ok(issues)
503}
504
505#[cfg(feature = "polars")]
544pub fn generate_arrow_from_folder(
545 folder: &Path,
546 output: &Path,
547 detect_sequences: bool,
548) -> Result<usize, Error> {
549 use polars::prelude::*;
550 use std::io::BufWriter;
551
552 let image_files: Vec<PathBuf> = WalkDir::new(folder)
554 .into_iter()
555 .filter_map(|e| e.ok())
556 .filter(|e| e.file_type().is_file())
557 .filter(|e| {
558 e.path()
559 .extension()
560 .and_then(|ext| ext.to_str())
561 .map(|ext| {
562 matches!(
563 ext.to_lowercase().as_str(),
564 "jpg" | "jpeg" | "png" | "pcd" | "bin"
565 )
566 })
567 .unwrap_or(false)
568 })
569 .map(|e| e.path().to_path_buf())
570 .collect();
571
572 if image_files.is_empty() {
573 return Err(Error::InvalidParameters(
574 "No image files found in folder".to_owned(),
575 ));
576 }
577
578 let mut names: Vec<String> = Vec::new();
580 let mut frames: Vec<Option<u64>> = Vec::new();
581
582 for path in &image_files {
583 let (name, frame) = parse_image_filename(path, folder, detect_sequences);
584 names.push(name);
585 frames.push(frame);
586 }
587
588 let name_series = Series::new("name".into(), &names);
592 let frame_series = Series::new("frame".into(), &frames);
593
594 let mut df = DataFrame::new_infer_height(vec![name_series.into(), frame_series.into()])?;
595
596 if let Some(parent) = output.parent() {
598 std::fs::create_dir_all(parent)?;
599 }
600
601 let file = File::create(output)?;
603 let writer = BufWriter::new(file);
604 IpcWriter::new(writer)
605 .finish(&mut df)
606 .map_err(|e| Error::InvalidParameters(format!("Failed to write Arrow file: {}", e)))?;
607
608 Ok(image_files.len())
609}
610
611fn parse_image_filename(path: &Path, root: &Path, detect_sequences: bool) -> (String, Option<u64>) {
613 let stem = path
614 .file_stem()
615 .and_then(|s| s.to_str())
616 .unwrap_or("unknown");
617
618 let clean_stem = stem.strip_suffix(".camera").unwrap_or(stem);
620
621 if !detect_sequences {
622 return (clean_stem.to_string(), None);
623 }
624
625 if let Some(idx) = clean_stem.rfind('_') {
628 let (name_part, frame_part) = clean_stem.split_at(idx);
629 let frame_str = &frame_part[1..]; if let Ok(frame) = frame_str.parse::<u64>() {
632 let relative = path.strip_prefix(root).unwrap_or(path);
634 if relative.components().count() > 1 {
635 return (name_part.to_string(), Some(frame));
637 }
638
639 return (name_part.to_string(), Some(frame));
642 }
643 }
644
645 (clean_stem.to_string(), None)
647}
648
649pub fn get_sensor_container_path(dataset_dir: &Path) -> Option<PathBuf> {
659 let dataset_name = dataset_dir.file_name()?.to_str()?;
660 Some(dataset_dir.join(dataset_name))
661}
662
663pub fn get_arrow_path(dataset_dir: &Path) -> Option<PathBuf> {
673 let dataset_name = dataset_dir.file_name()?.to_str()?;
674 Some(dataset_dir.join(format!("{}.arrow", dataset_name)))
675}
676
677#[cfg(test)]
678mod tests {
679 use super::*;
680 use std::io::Write;
681 use tempfile::TempDir;
682
683 fn create_test_image(path: &Path) {
685 let jpeg_data: &[u8] = &[
687 0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00,
688 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0xFF, 0xDB, 0x00, 0x43, 0x00, 0x08, 0x06, 0x06,
689 0x07, 0x06, 0x05, 0x08, 0x07, 0x07, 0x07, 0x09, 0x09, 0x08, 0x0A, 0x0C, 0x14, 0x0D,
690 0x0C, 0x0B, 0x0B, 0x0C, 0x19, 0x12, 0x13, 0x0F, 0x14, 0x1D, 0x1A, 0x1F, 0x1E, 0x1D,
691 0x1A, 0x1C, 0x1C, 0x20, 0x24, 0x2E, 0x27, 0x20, 0x22, 0x2C, 0x23, 0x1C, 0x1C, 0x28,
692 0x37, 0x29, 0x2C, 0x30, 0x31, 0x34, 0x34, 0x34, 0x1F, 0x27, 0x39, 0x3D, 0x38, 0x32,
693 0x3C, 0x2E, 0x33, 0x34, 0x32, 0xFF, 0xC0, 0x00, 0x0B, 0x08, 0x00, 0x01, 0x00, 0x01,
694 0x01, 0x01, 0x11, 0x00, 0xFF, 0xC4, 0x00, 0x1F, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01,
695 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02,
696 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0xFF, 0xC4, 0x00, 0xB5, 0x10,
697 0x00, 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, 0x00,
698 0x01, 0x7D, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06,
699 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xA1, 0x08, 0x23, 0x42,
700 0xB1, 0xC1, 0x15, 0x52, 0xD1, 0xF0, 0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0A, 0x16,
701 0x17, 0x18, 0x19, 0x1A, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x34, 0x35, 0x36, 0x37,
702 0x38, 0x39, 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x53, 0x54, 0x55,
703 0x56, 0x57, 0x58, 0x59, 0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x73,
704 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89,
705 0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5,
706 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA,
707 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6,
708 0xD7, 0xD8, 0xD9, 0xDA, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA,
709 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFF, 0xDA, 0x00, 0x08,
710 0x01, 0x01, 0x00, 0x00, 0x3F, 0x00, 0xFB, 0xD5, 0xDB, 0x20, 0xA8, 0xF1, 0x4D, 0x9E,
711 0xBA, 0x79, 0xC5, 0x14, 0x51, 0x40, 0xFF, 0xD9,
712 ];
713
714 if let Some(parent) = path.parent() {
715 std::fs::create_dir_all(parent).unwrap();
716 }
717 let mut file = File::create(path).unwrap();
718 file.write_all(jpeg_data).unwrap();
719 }
720
721 #[test]
722 fn test_get_arrow_path() {
723 let dir = Path::new("/data/my_dataset");
724 let arrow = get_arrow_path(dir).unwrap();
725 assert_eq!(arrow, PathBuf::from("/data/my_dataset/my_dataset.arrow"));
726 }
727
728 #[test]
729 fn test_get_sensor_container_path() {
730 let dir = Path::new("/data/my_dataset");
731 let container = get_sensor_container_path(dir).unwrap();
732 assert_eq!(container, PathBuf::from("/data/my_dataset/my_dataset"));
733 }
734
735 #[test]
736 fn test_parse_image_filename_standalone() {
737 let root = Path::new("/data");
738 let path = Path::new("/data/image.jpg");
739
740 let (name, frame) = parse_image_filename(path, root, true);
741 assert_eq!(name, "image");
742 assert_eq!(frame, None);
743 }
744
745 #[test]
746 fn test_parse_image_filename_camera_extension() {
747 let root = Path::new("/data");
748 let path = Path::new("/data/sample.camera.jpeg");
749
750 let (name, frame) = parse_image_filename(path, root, true);
751 assert_eq!(name, "sample");
752 assert_eq!(frame, None);
753 }
754
755 #[test]
756 fn test_parse_image_filename_sequence() {
757 let root = Path::new("/data");
758 let path = Path::new("/data/seq/seq_001.camera.jpeg");
759
760 let (name, frame) = parse_image_filename(path, root, true);
761 assert_eq!(name, "seq");
762 assert_eq!(frame, Some(1));
763 }
764
765 #[test]
766 fn test_parse_image_filename_no_sequence_detection() {
767 let root = Path::new("/data");
768 let path = Path::new("/data/seq/seq_001.camera.jpeg");
769
770 let (name, frame) = parse_image_filename(path, root, false);
771 assert_eq!(name, "seq_001");
772 assert_eq!(frame, None);
773 }
774
775 #[test]
776 fn test_build_file_index() {
777 let temp_dir = TempDir::new().unwrap();
778 let root = temp_dir.path();
779
780 create_test_image(&root.join("image1.jpg"));
782 create_test_image(&root.join("sub/image2.camera.jpeg"));
783
784 let index = build_file_index(root).unwrap();
785
786 assert!(index.contains_key("image1.jpg"));
788 assert!(index.contains_key("image2.camera.jpeg"));
789
790 assert!(index.contains_key("image1"));
792 assert!(index.contains_key("image2"));
793 }
794
795 #[test]
796 fn test_find_matching_file() {
797 let temp_dir = TempDir::new().unwrap();
798 let root = temp_dir.path();
799
800 create_test_image(&root.join("sample.camera.jpeg"));
802 create_test_image(&root.join("seq/seq_001.camera.jpeg"));
803
804 let index = build_file_index(root).unwrap();
805
806 let found = find_matching_file(&index, "sample", None);
808 assert!(found.is_some());
809
810 let found = find_matching_file(&index, "seq", Some(1));
812 assert!(found.is_some());
813
814 let found = find_matching_file(&index, "nonexistent", None);
816 assert!(found.is_none());
817 }
818
819 #[cfg(feature = "polars")]
820 #[test]
821 fn test_generate_arrow_from_folder() {
822 use polars::prelude::*;
823
824 let temp_dir = TempDir::new().unwrap();
825 let root = temp_dir.path();
826
827 let images_dir = root.join("images");
829 create_test_image(&images_dir.join("photo1.jpg"));
830 create_test_image(&images_dir.join("photo2.png"));
831 create_test_image(&images_dir.join("seq/seq_001.camera.jpeg"));
832 create_test_image(&images_dir.join("seq/seq_002.camera.jpeg"));
833
834 let arrow_path = root.join("output.arrow");
836 let count = generate_arrow_from_folder(&images_dir, &arrow_path, true).unwrap();
837
838 assert_eq!(count, 4);
839 assert!(arrow_path.exists());
840
841 let mut file = File::open(&arrow_path).unwrap();
843 let df = IpcReader::new(&mut file).finish().unwrap();
844
845 assert_eq!(df.height(), 4);
846 assert_eq!(df.width(), 2); assert!(df.column("name").is_ok());
848 assert!(df.column("frame").is_ok());
849 }
850
851 #[cfg(feature = "polars")]
852 #[test]
853 fn test_resolve_arrow_files() {
854 use polars::prelude::*;
855 use std::io::BufWriter;
856
857 let temp_dir = TempDir::new().unwrap();
858 let root = temp_dir.path();
859
860 let names = Series::new("name".into(), &["sample1", "sample2", "seq"]);
862 let frames: Vec<Option<u64>> = vec![None, None, Some(1)];
863 let frame_series = Series::new("frame".into(), &frames);
864
865 let mut df = DataFrame::new_infer_height(vec![names.into(), frame_series.into()]).unwrap();
866
867 let arrow_path = root.join("test.arrow");
868 let file = File::create(&arrow_path).unwrap();
869 let writer = BufWriter::new(file);
870 IpcWriter::new(writer).finish(&mut df).unwrap();
871
872 let resolved = resolve_arrow_files(&arrow_path).unwrap();
874
875 assert_eq!(resolved.len(), 3);
876 assert!(resolved.contains_key("sample1"));
877 assert!(resolved.contains_key("sample2"));
878 assert!(resolved.contains_key("seq"));
879 }
880
881 #[cfg(feature = "polars")]
882 #[test]
883 fn test_validate_dataset_structure_valid() {
884 use polars::prelude::*;
885 use std::io::BufWriter;
886
887 let temp_dir = TempDir::new().unwrap();
888 let dataset_dir = temp_dir.path().join("my_dataset");
889 std::fs::create_dir_all(&dataset_dir).unwrap();
890
891 let names = Series::new("name".into(), &["image1"]);
893 let frames: Vec<Option<u64>> = vec![None];
894 let frame_series = Series::new("frame".into(), &frames);
895
896 let mut df = DataFrame::new_infer_height(vec![names.into(), frame_series.into()]).unwrap();
897
898 let arrow_path = dataset_dir.join("my_dataset.arrow");
899 let file = File::create(&arrow_path).unwrap();
900 let writer = BufWriter::new(file);
901 IpcWriter::new(writer).finish(&mut df).unwrap();
902
903 let container = dataset_dir.join("my_dataset");
905 create_test_image(&container.join("image1.camera.jpeg"));
906
907 let issues = validate_dataset_structure(&dataset_dir).unwrap();
909
910 let missing_files: Vec<_> = issues
912 .iter()
913 .filter(|i| matches!(i, ValidationIssue::MissingFile { .. }))
914 .collect();
915 assert!(
916 missing_files.is_empty(),
917 "Unexpected missing files: {:?}",
918 missing_files
919 );
920 }
921
922 #[cfg(feature = "polars")]
923 #[test]
924 fn test_validate_dataset_structure_missing_arrow() {
925 let temp_dir = TempDir::new().unwrap();
926 let dataset_dir = temp_dir.path().join("my_dataset");
927 std::fs::create_dir_all(&dataset_dir).unwrap();
928
929 let issues = validate_dataset_structure(&dataset_dir).unwrap();
930
931 assert_eq!(issues.len(), 1);
932 assert!(matches!(
933 &issues[0],
934 ValidationIssue::MissingArrowFile { .. }
935 ));
936 }
937
938 #[test]
939 fn test_image_extensions() {
940 assert!(IMAGE_EXTENSIONS.contains(&"jpg"));
941 assert!(IMAGE_EXTENSIONS.contains(&"jpeg"));
942 assert!(IMAGE_EXTENSIONS.contains(&"png"));
943 assert!(IMAGE_EXTENSIONS.contains(&"camera.jpeg"));
944 }
945
946 #[test]
947 fn test_validation_issue_display() {
948 let issue = ValidationIssue::MissingFile {
949 name: "test".to_string(),
950 expected: PathBuf::from("test.jpg"),
951 };
952 let display = format!("{}", issue);
953 assert!(display.contains("test"));
954 assert!(display.contains("test.jpg"));
955 }
956}