1pub mod builder;
122pub mod extensions;
123pub mod methods;
124pub mod types;
125
126pub use builder::*;
127pub use extensions::*;
128pub use types::*;
129
130pub type Result<T> = core::result::Result<T, OxiGdalError>;
132
133#[cfg(test)]
134mod tests {
135 #![allow(clippy::expect_used, clippy::useless_vec)]
136
137 use super::*;
138
139 #[test]
140 fn test_error_display() {
141 let err = OxiGdalError::InvalidParameter {
142 parameter: "width",
143 message: "must be positive".to_string(),
144 };
145 assert!(err.to_string().contains("width"));
146 assert!(err.to_string().contains("must be positive"));
147 }
148
149 #[test]
150 fn test_io_error_conversion() {
151 let io_err = IoError::NotFound {
152 path: "/test/path".to_string(),
153 };
154 let gdal_err: OxiGdalError = io_err.into();
155 assert!(matches!(
156 gdal_err,
157 OxiGdalError::Io(IoError::NotFound { .. })
158 ));
159 }
160
161 #[test]
162 fn test_format_error_conversion() {
163 let format_err = FormatError::InvalidMagic {
164 expected: &[0x49, 0x49],
165 actual: [0x00, 0x00, 0x00, 0x00],
166 };
167 let gdal_err: OxiGdalError = format_err.into();
168 assert!(matches!(
169 gdal_err,
170 OxiGdalError::Format(FormatError::InvalidMagic { .. })
171 ));
172 }
173
174 #[test]
175 fn test_error_codes() {
176 let err = OxiGdalError::InvalidParameter {
177 parameter: "test",
178 message: "test message".to_string(),
179 };
180 assert_eq!(err.code(), "E001");
181
182 let err = OxiGdalError::NotSupported {
183 operation: "test".to_string(),
184 };
185 assert_eq!(err.code(), "E002");
186
187 let io_err = IoError::NotFound {
188 path: "/test".to_string(),
189 };
190 assert_eq!(io_err.code(), "E100");
191 }
192
193 #[test]
194 fn test_error_suggestions() {
195 let err = OxiGdalError::InvalidParameter {
196 parameter: "test",
197 message: "test message".to_string(),
198 };
199 assert!(err.suggestion().is_some());
200 assert!(err.suggestion().is_some_and(|s| s.contains("parameter")));
201
202 let io_err = IoError::NotFound {
203 path: "/test".to_string(),
204 };
205 assert!(io_err.suggestion().is_some());
206 assert!(io_err.suggestion().is_some_and(|s| s.contains("file")));
207 }
208
209 #[test]
210 fn test_error_context() {
211 let err = OxiGdalError::InvalidParameter {
212 parameter: "test_param",
213 message: "test message".to_string(),
214 };
215 let ctx = err.context();
216 assert_eq!(ctx.category, "parameter_validation");
217
218 let io_err = IoError::NotFound {
219 path: "/test/path".to_string(),
220 };
221 let ctx = io_err.context();
222 assert_eq!(ctx.category, "file_not_found");
223 }
224
225 #[test]
226 fn test_error_aggregator() {
227 let mut agg = ErrorAggregator::new();
228 assert!(!agg.has_errors());
229 assert_eq!(agg.count(), 0);
230
231 agg.add(OxiGdalError::InvalidParameter {
232 parameter: "test1",
233 message: "error 1".to_string(),
234 });
235 assert!(agg.has_errors());
236 assert_eq!(agg.count(), 1);
237
238 agg.add(OxiGdalError::InvalidParameter {
239 parameter: "test2",
240 message: "error 2".to_string(),
241 });
242 assert_eq!(agg.count(), 2);
243
244 let result = agg.into_result();
245 assert!(result.is_err());
246 }
247
248 #[test]
249 fn test_error_aggregator_with_results() {
250 let mut agg = ErrorAggregator::new();
251
252 let ok_result: Result<i32> = Ok(42);
253 let value = agg.add_result(ok_result);
254 assert_eq!(value, Some(42));
255 assert!(!agg.has_errors());
256
257 let err_result: Result<i32> = Err(OxiGdalError::InvalidParameter {
258 parameter: "test",
259 message: "error".to_string(),
260 });
261 let value = agg.add_result(err_result);
262 assert_eq!(value, None);
263 assert!(agg.has_errors());
264 assert_eq!(agg.count(), 1);
265 }
266
267 #[test]
268 fn test_result_ext_context() {
269 let result: Result<i32> = Err(OxiGdalError::InvalidParameter {
270 parameter: "test",
271 message: "original".to_string(),
272 });
273
274 let with_ctx = result.context("added context");
275 assert!(with_ctx.is_err());
276 if let Err(e) = with_ctx {
277 assert!(matches!(e, OxiGdalError::Internal { .. }));
278 }
279 }
280
281 #[test]
282 fn test_result_ext_with_context() {
283 let result: Result<i32> = Err(OxiGdalError::InvalidParameter {
284 parameter: "test",
285 message: "original".to_string(),
286 });
287
288 let with_ctx = result.with_context(|| "lazy context".to_string());
289 assert!(with_ctx.is_err());
290 if let Err(e) = with_ctx {
291 assert!(matches!(e, OxiGdalError::Internal { .. }));
292 }
293 }
294
295 #[test]
296 #[cfg(feature = "std")]
297 fn test_from_path() {
298 use std::path::Path;
299
300 let path = Path::new("/test/file.tif");
301 let err = OxiGdalError::from_path(path, std::io::ErrorKind::NotFound);
302 assert!(matches!(err, OxiGdalError::Io(IoError::NotFound { .. })));
303
304 let err = OxiGdalError::from_path(path, std::io::ErrorKind::PermissionDenied);
305 assert!(matches!(
306 err,
307 OxiGdalError::Io(IoError::PermissionDenied { .. })
308 ));
309 }
310
311 #[test]
312 fn test_error_builder_basic() {
313 let builder = OxiGdalError::io_error_builder("Test error");
314 let err = builder.build();
315 assert!(matches!(err, OxiGdalError::Io(IoError::Read { .. })));
316 }
317
318 #[test]
319 #[cfg(feature = "std")]
320 fn test_error_builder_with_path() {
321 use std::path::Path;
322
323 let builder = OxiGdalError::io_error_builder("Cannot read file")
324 .with_path(Path::new("/data/test.tif"));
325
326 assert_eq!(builder.file_path(), Some(Path::new("/data/test.tif")));
327
328 let err = builder.build();
329 assert!(matches!(err, OxiGdalError::Io(IoError::Read { .. })));
330 }
331
332 #[test]
333 fn test_error_builder_with_operation() {
334 let builder = OxiGdalError::io_error_builder("Test error").with_operation("read_raster");
335
336 assert_eq!(builder.operation_name(), Some("read_raster"));
337
338 let err = builder.build();
339 assert!(matches!(err, OxiGdalError::Io(IoError::Read { .. })));
340 }
341
342 #[test]
343 fn test_error_builder_with_parameters() {
344 let builder = OxiGdalError::invalid_parameter_builder("width", "must be positive")
345 .with_parameter("value", "-10")
346 .with_parameter("minimum", "1");
347
348 let params = builder.parameters();
349 assert_eq!(params.get("value"), Some(&"-10".to_string()));
350 assert_eq!(params.get("minimum"), Some(&"1".to_string()));
351
352 let err = builder.build();
353 assert!(matches!(err, OxiGdalError::InvalidParameter { .. }));
354 }
355
356 #[test]
357 fn test_error_builder_with_suggestion() {
358 let builder = OxiGdalError::io_error_builder("Cannot read file")
359 .with_suggestion("Check file permissions and ensure the file exists");
360
361 let suggestion = builder.suggestion();
362 assert_eq!(
363 suggestion,
364 Some("Check file permissions and ensure the file exists".to_string())
365 );
366
367 let err = builder.build();
368 assert!(matches!(err, OxiGdalError::Io(IoError::Read { .. })));
369 }
370
371 #[test]
372 fn test_error_builder_custom_suggestion_overrides_default() {
373 let builder = OxiGdalError::invalid_parameter_builder("test", "invalid")
374 .with_suggestion("Custom suggestion");
375
376 let suggestion = builder.suggestion();
377 assert_eq!(suggestion, Some("Custom suggestion".to_string()));
378 }
379
380 #[test]
381 #[cfg(feature = "std")]
382 fn test_error_builder_fluent_api() {
383 use std::path::Path;
384
385 let builder = OxiGdalError::io_error_builder("Cannot read file")
386 .with_path(Path::new("/data/test.tif"))
387 .with_operation("read_raster")
388 .with_parameter("band", "1")
389 .with_parameter("window", "0,0,512,512")
390 .with_suggestion("Verify file exists and is accessible");
391
392 assert_eq!(builder.file_path(), Some(Path::new("/data/test.tif")));
393 assert_eq!(builder.operation_name(), Some("read_raster"));
394 assert_eq!(builder.parameters().get("band"), Some(&"1".to_string()));
395 assert_eq!(
396 builder.parameters().get("window"),
397 Some(&"0,0,512,512".to_string())
398 );
399 assert!(builder.suggestion().is_some());
400
401 let err = builder.build();
402 assert!(matches!(err, OxiGdalError::Io(IoError::Read { .. })));
403 }
404
405 #[test]
406 fn test_error_builder_context() {
407 let builder = OxiGdalError::invalid_parameter_builder("width", "must be positive")
408 .with_parameter("value", "-10")
409 .with_operation("create_raster");
410
411 let ctx = builder.build_context();
412 assert_eq!(ctx.category, "parameter_validation");
413 assert!(ctx.operation.is_some());
414 assert_eq!(ctx.operation.as_deref(), Some("create_raster"));
415 assert!(!ctx.parameters.is_empty());
416 }
417
418 #[test]
419 fn test_error_builder_into_error() {
420 let builder = OxiGdalError::io_error_builder("Test error");
421 let err = builder.into_error();
422 assert!(matches!(err, OxiGdalError::Io(IoError::Read { .. })));
423 }
424
425 #[test]
426 fn test_error_builder_error_ref() {
427 let builder = OxiGdalError::io_error_builder("Test error");
428 let err_ref = builder.error();
429 assert!(matches!(err_ref, OxiGdalError::Io(IoError::Read { .. })));
430 }
431
432 #[test]
433 fn test_error_builder_with_multiple_parameters() {
434 let mut builder = OxiGdalError::invalid_parameter_builder("size", "invalid");
435 builder = builder.with_parameter("width", "1024");
436 builder = builder.with_parameter("height", "768");
437 builder = builder.with_parameter("bands", "3");
438
439 let params = builder.parameters();
440 assert_eq!(params.len(), 3);
441 assert_eq!(params.get("width"), Some(&"1024".to_string()));
442 assert_eq!(params.get("height"), Some(&"768".to_string()));
443 assert_eq!(params.get("bands"), Some(&"3".to_string()));
444 }
445
446 #[test]
447 fn test_error_builder_allocation_error() {
448 let builder = OxiGdalError::allocation_error_builder("Failed to allocate buffer");
449 let err = builder.build();
450 assert!(matches!(err, OxiGdalError::Internal { .. }));
451 assert!(err.to_string().contains("Allocation error"));
452 }
453
454 #[test]
455 fn test_error_builder_invalid_state() {
456 let builder = OxiGdalError::invalid_state_builder("Dataset already closed");
457 let err = builder.build();
458 assert!(matches!(err, OxiGdalError::Internal { .. }));
459 assert!(err.to_string().contains("Invalid state"));
460 }
461
462 #[test]
463 fn test_error_builder_not_supported() {
464 let builder = OxiGdalError::not_supported_builder("write_compressed_tiff");
465 let err = builder.build();
466 assert!(matches!(err, OxiGdalError::NotSupported { .. }));
467 }
468
469 #[test]
470 fn test_error_builder_edge_cases() {
471 let builder = OxiGdalError::io_error_builder("test").with_operation("");
473 assert_eq!(builder.operation_name(), Some(""));
474
475 let builder = OxiGdalError::io_error_builder("test").with_parameter("key", "");
477 assert_eq!(builder.parameters().get("key"), Some(&"".to_string()));
478
479 let builder = OxiGdalError::io_error_builder("test").with_suggestion("");
481 assert_eq!(builder.suggestion(), Some("".to_string()));
482 }
483
484 #[test]
485 #[cfg(feature = "std")]
486 fn test_error_context_with_builder_fields() {
487 use std::path::Path;
488
489 let builder = OxiGdalError::io_error_builder("Test")
490 .with_path(Path::new("/test"))
491 .with_operation("test_op")
492 .with_parameter("key", "value")
493 .with_suggestion("test suggestion");
494
495 let ctx = builder.build_context();
496 assert!(ctx.file_path.is_some());
497 assert_eq!(ctx.file_path.as_deref(), Some(Path::new("/test")));
498 assert_eq!(ctx.operation.as_deref(), Some("test_op"));
499 assert_eq!(ctx.parameters.get("key"), Some(&"value".to_string()));
500 assert_eq!(ctx.custom_suggestion.as_deref(), Some("test suggestion"));
501 }
502
503 #[test]
505 fn test_error_code_consistency_io_errors() {
506 let errors = vec![
508 IoError::NotFound {
509 path: "test".to_string(),
510 },
511 IoError::PermissionDenied {
512 path: "test".to_string(),
513 },
514 IoError::Network {
515 message: "test".to_string(),
516 },
517 IoError::UnexpectedEof { offset: 0 },
518 IoError::Read {
519 message: "test".to_string(),
520 },
521 IoError::Write {
522 message: "test".to_string(),
523 },
524 IoError::Seek { position: 0 },
525 IoError::Http {
526 status: 404,
527 message: "test".to_string(),
528 },
529 ];
530
531 let codes: Vec<&str> = errors.iter().map(|e| e.code()).collect();
532
533 for code in &codes {
535 assert!(code.starts_with("E1"));
536 }
537
538 for (i, code1) in codes.iter().enumerate() {
540 for (j, code2) in codes.iter().enumerate() {
541 if i != j {
542 assert_ne!(code1, code2, "Duplicate error codes found");
543 }
544 }
545 }
546 }
547
548 #[test]
549 fn test_error_code_consistency_format_errors() {
550 let errors = vec![
552 FormatError::InvalidMagic {
553 expected: &[0x49],
554 actual: [0, 0, 0, 0],
555 },
556 FormatError::InvalidHeader {
557 message: "test".to_string(),
558 },
559 FormatError::UnsupportedVersion { version: 1 },
560 FormatError::InvalidTag {
561 tag: 256,
562 message: "test".to_string(),
563 },
564 FormatError::MissingTag { tag: "test" },
565 FormatError::InvalidDataType { type_id: 1 },
566 FormatError::CorruptData {
567 offset: 0,
568 message: "test".to_string(),
569 },
570 FormatError::InvalidGeoKey {
571 key_id: 1024,
572 message: "test".to_string(),
573 },
574 ];
575
576 let codes: Vec<&str> = errors.iter().map(|e| e.code()).collect();
577
578 for code in &codes {
580 assert!(code.starts_with("E2"));
581 }
582
583 for (i, code1) in codes.iter().enumerate() {
585 for (j, code2) in codes.iter().enumerate() {
586 if i != j {
587 assert_ne!(code1, code2, "Duplicate error codes found");
588 }
589 }
590 }
591 }
592
593 #[test]
594 fn test_error_code_consistency_crs_errors() {
595 let errors = vec![
596 CrsError::UnknownCrs {
597 identifier: "test".to_string(),
598 },
599 CrsError::InvalidWkt {
600 message: "test".to_string(),
601 },
602 CrsError::InvalidEpsg { code: 0 },
603 CrsError::TransformationError {
604 source_crs: "EPSG:4326".to_string(),
605 target_crs: "EPSG:3857".to_string(),
606 message: "test".to_string(),
607 },
608 CrsError::DatumNotFound {
609 datum: "WGS84".to_string(),
610 },
611 ];
612
613 let codes: Vec<&str> = errors.iter().map(|e| e.code()).collect();
614
615 for code in &codes {
617 assert!(code.starts_with("E3"));
618 }
619
620 for (i, code1) in codes.iter().enumerate() {
622 for (j, code2) in codes.iter().enumerate() {
623 if i != j {
624 assert_ne!(code1, code2, "Duplicate error codes found");
625 }
626 }
627 }
628 }
629
630 #[test]
631 fn test_error_code_consistency_compression_errors() {
632 let errors = vec![
633 CompressionError::UnknownMethod { method: 99 },
634 CompressionError::DecompressionFailed {
635 message: "test".to_string(),
636 },
637 CompressionError::CompressionFailed {
638 message: "test".to_string(),
639 },
640 CompressionError::InvalidData {
641 message: "test".to_string(),
642 },
643 ];
644
645 let codes: Vec<&str> = errors.iter().map(|e| e.code()).collect();
646
647 for code in &codes {
649 assert!(code.starts_with("E4"));
650 }
651
652 for (i, code1) in codes.iter().enumerate() {
654 for (j, code2) in codes.iter().enumerate() {
655 if i != j {
656 assert_ne!(code1, code2, "Duplicate error codes found");
657 }
658 }
659 }
660 }
661
662 #[test]
663 fn test_error_code_consistency_top_level_errors() {
664 let errors = vec![
665 OxiGdalError::InvalidParameter {
666 parameter: "test",
667 message: "test".to_string(),
668 },
669 OxiGdalError::NotSupported {
670 operation: "test".to_string(),
671 },
672 OxiGdalError::OutOfBounds {
673 message: "test".to_string(),
674 },
675 OxiGdalError::Internal {
676 message: "test".to_string(),
677 },
678 ];
679
680 let codes: Vec<&str> = errors.iter().map(|e| e.code()).collect();
681
682 for code in &codes {
684 assert!(code.starts_with("E0"));
685 }
686
687 for (i, code1) in codes.iter().enumerate() {
689 for (j, code2) in codes.iter().enumerate() {
690 if i != j {
691 assert_ne!(code1, code2, "Duplicate error codes found");
692 }
693 }
694 }
695 }
696
697 #[test]
699 fn test_suggestion_quality_io_errors() {
700 let test_cases = vec![
701 (
702 IoError::NotFound {
703 path: "/test".to_string(),
704 },
705 vec!["file", "path", "exist"],
706 ),
707 (
708 IoError::PermissionDenied {
709 path: "/test".to_string(),
710 },
711 vec!["permission"],
712 ),
713 (
714 IoError::Network {
715 message: "timeout".to_string(),
716 },
717 vec!["network", "connectivity"],
718 ),
719 (
720 IoError::UnexpectedEof { offset: 100 },
721 vec!["truncated", "corrupted"],
722 ),
723 (
724 IoError::Http {
725 status: 404,
726 message: "Not Found".to_string(),
727 },
728 vec!["not found", "resource"],
729 ),
730 (
731 IoError::Http {
732 status: 403,
733 message: "Forbidden".to_string(),
734 },
735 vec!["forbidden", "authentication", "credentials"],
736 ),
737 (
738 IoError::Http {
739 status: 500,
740 message: "Server Error".to_string(),
741 },
742 vec!["server", "later"],
743 ),
744 ];
745
746 for (error, keywords) in test_cases {
747 let suggestion = error.suggestion();
748 assert!(
749 suggestion.is_some(),
750 "Error should have a suggestion: {:?}",
751 error
752 );
753
754 let suggestion_text = suggestion.expect("Expected suggestion").to_lowercase();
755 let has_keyword = keywords.iter().any(|kw| suggestion_text.contains(kw));
756 assert!(
757 has_keyword,
758 "Suggestion '{}' should contain at least one keyword from {:?}",
759 suggestion_text, keywords
760 );
761 }
762 }
763
764 #[test]
765 fn test_suggestion_quality_format_errors() {
766 let test_cases = vec![
767 (
768 FormatError::InvalidMagic {
769 expected: &[0x49, 0x49],
770 actual: [0, 0, 0, 0],
771 },
772 vec!["format", "file type", "verify"],
773 ),
774 (
775 FormatError::UnsupportedVersion { version: 999 },
776 vec!["version", "supported", "converting"],
777 ),
778 (
779 FormatError::MissingTag { tag: "ImageWidth" },
780 vec!["required", "missing", "incomplete", "corrupted"],
781 ),
782 (
783 FormatError::CorruptData {
784 offset: 1024,
785 message: "checksum mismatch".to_string(),
786 },
787 vec!["corruption", "backup", "recovering"],
788 ),
789 ];
790
791 for (error, keywords) in test_cases {
792 let suggestion = error.suggestion();
793 assert!(
794 suggestion.is_some(),
795 "Error should have a suggestion: {:?}",
796 error
797 );
798
799 let suggestion_text = suggestion.expect("Expected suggestion").to_lowercase();
800 let has_keyword = keywords.iter().any(|kw| suggestion_text.contains(kw));
801 assert!(
802 has_keyword,
803 "Suggestion '{}' should contain at least one keyword from {:?}",
804 suggestion_text, keywords
805 );
806 }
807 }
808
809 #[test]
810 fn test_suggestion_quality_crs_errors() {
811 let test_cases = vec![
812 (
813 CrsError::UnknownCrs {
814 identifier: "CUSTOM:123".to_string(),
815 },
816 vec!["verify", "epsg", "identifier"],
817 ),
818 (
819 CrsError::InvalidWkt {
820 message: "parse error".to_string(),
821 },
822 vec!["wkt", "syntax", "bracket"],
823 ),
824 (
825 CrsError::InvalidEpsg { code: 999999 },
826 vec!["valid", "epsg.io"],
827 ),
828 (
829 CrsError::TransformationError {
830 source_crs: "EPSG:4326".to_string(),
831 target_crs: "CUSTOM:1".to_string(),
832 message: "no transformation path".to_string(),
833 },
834 vec!["compatible", "transformation", "parameters"],
835 ),
836 ];
837
838 for (error, keywords) in test_cases {
839 let suggestion = error.suggestion();
840 assert!(
841 suggestion.is_some(),
842 "Error should have a suggestion: {:?}",
843 error
844 );
845
846 let suggestion_text = suggestion.expect("Expected suggestion").to_lowercase();
847 let has_keyword = keywords.iter().any(|kw| suggestion_text.contains(kw));
848 assert!(
849 has_keyword,
850 "Suggestion '{}' should contain at least one keyword from {:?}",
851 suggestion_text, keywords
852 );
853 }
854 }
855
856 #[test]
857 fn test_suggestion_quality_top_level_errors() {
858 let test_cases = vec![
859 (
860 OxiGdalError::InvalidParameter {
861 parameter: "width",
862 message: "must be positive".to_string(),
863 },
864 vec!["parameter", "documentation", "valid"],
865 ),
866 (
867 OxiGdalError::NotSupported {
868 operation: "write_jp2".to_string(),
869 },
870 vec!["feature", "enabled", "alternative"],
871 ),
872 (
873 OxiGdalError::OutOfBounds {
874 message: "index out of range".to_string(),
875 },
876 vec!["verify", "indices", "range", "valid"],
877 ),
878 (
879 OxiGdalError::Internal {
880 message: "unexpected null pointer".to_string(),
881 },
882 vec!["bug", "report"],
883 ),
884 ];
885
886 for (error, keywords) in test_cases {
887 let suggestion = error.suggestion();
888 assert!(
889 suggestion.is_some(),
890 "Error should have a suggestion: {:?}",
891 error
892 );
893
894 let suggestion_text = suggestion.expect("Expected suggestion").to_lowercase();
895 let has_keyword = keywords.iter().any(|kw| suggestion_text.contains(kw));
896 assert!(
897 has_keyword,
898 "Suggestion '{}' should contain at least one keyword from {:?}",
899 suggestion_text, keywords
900 );
901 }
902 }
903
904 #[test]
906 fn test_context_propagation_io_errors() {
907 let error = IoError::NotFound {
908 path: "/data/test.tif".to_string(),
909 };
910 let context = error.context();
911
912 assert_eq!(context.category, "file_not_found");
913 assert!(!context.details.is_empty());
914
915 let path_detail = context.details.iter().find(|(k, _)| k == "path");
916 assert!(path_detail.is_some());
917 assert_eq!(
918 path_detail.expect("Expected path detail").1,
919 "/data/test.tif"
920 );
921 }
922
923 #[test]
924 fn test_context_propagation_format_errors() {
925 let error = FormatError::InvalidTag {
926 tag: 256,
927 message: "unsupported tag type".to_string(),
928 };
929 let context = error.context();
930
931 assert_eq!(context.category, "invalid_tag");
932 assert!(!context.details.is_empty());
933
934 let tag_detail = context.details.iter().find(|(k, _)| k == "tag");
935 assert!(tag_detail.is_some());
936 assert_eq!(tag_detail.expect("Expected tag detail").1, "256");
937
938 let message_detail = context.details.iter().find(|(k, _)| k == "message");
939 assert!(message_detail.is_some());
940 assert_eq!(
941 message_detail.expect("Expected message detail").1,
942 "unsupported tag type"
943 );
944 }
945
946 #[test]
947 fn test_context_propagation_crs_errors() {
948 let error = CrsError::TransformationError {
949 source_crs: "EPSG:4326".to_string(),
950 target_crs: "EPSG:3857".to_string(),
951 message: "datum shift required".to_string(),
952 };
953 let context = error.context();
954
955 assert_eq!(context.category, "transformation_error");
956 assert!(!context.details.is_empty());
957
958 let src_detail = context.details.iter().find(|(k, _)| k == "source_crs");
959 assert!(src_detail.is_some());
960 assert_eq!(
961 src_detail.expect("Expected source_crs detail").1,
962 "EPSG:4326"
963 );
964
965 let tgt_detail = context.details.iter().find(|(k, _)| k == "target_crs");
966 assert!(tgt_detail.is_some());
967 assert_eq!(
968 tgt_detail.expect("Expected target_crs detail").1,
969 "EPSG:3857"
970 );
971 }
972
973 #[test]
974 fn test_context_propagation_through_conversion() {
975 let io_error = IoError::Network {
976 message: "connection timeout".to_string(),
977 };
978 let gdal_error: OxiGdalError = io_error.into();
979
980 let context = gdal_error.context();
981 assert_eq!(context.category, "network_error");
982
983 let msg_detail = context.details.iter().find(|(k, _)| k == "message");
984 assert!(msg_detail.is_some());
985 assert_eq!(
986 msg_detail.expect("Expected message detail").1,
987 "connection timeout"
988 );
989 }
990
991 #[test]
992 #[cfg(feature = "std")]
993 fn test_context_propagation_with_error_builder() {
994 use std::path::Path;
995
996 let builder = OxiGdalError::io_error_builder("Cannot read GeoTIFF")
997 .with_path(Path::new("/data/terrain.tif"))
998 .with_operation("read_geotiff")
999 .with_parameter("band", "1")
1000 .with_parameter("window", "0,0,512,512");
1001
1002 let context = builder.build_context();
1003
1004 assert_eq!(
1006 context.file_path,
1007 Some(Path::new("/data/terrain.tif").to_path_buf())
1008 );
1009
1010 assert_eq!(context.operation.as_deref(), Some("read_geotiff"));
1012
1013 assert_eq!(context.parameters.get("band"), Some(&"1".to_string()));
1015 assert_eq!(
1016 context.parameters.get("window"),
1017 Some(&"0,0,512,512".to_string())
1018 );
1019 }
1020
1021 #[test]
1022 fn test_error_builder_context_with_custom_suggestion() {
1023 let builder = OxiGdalError::invalid_parameter_builder("buffer_size", "must be power of 2")
1024 .with_parameter("value", "1000")
1025 .with_suggestion("Use 512, 1024, 2048, or 4096");
1026
1027 let context = builder.build_context();
1028
1029 assert_eq!(
1031 context.custom_suggestion.as_deref(),
1032 Some("Use 512, 1024, 2048, or 4096")
1033 );
1034 }
1035
1036 #[test]
1037 fn test_error_context_detail_chain() {
1038 let mut context = ErrorContext::new("test_category");
1039 context = context
1040 .with_detail("key1", "value1")
1041 .with_detail("key2", "value2")
1042 .with_detail("key3", "value3");
1043
1044 assert_eq!(context.category, "test_category");
1045 assert_eq!(context.details.len(), 3);
1046
1047 assert_eq!(
1049 context.details[0],
1050 ("key1".to_string(), "value1".to_string())
1051 );
1052 assert_eq!(
1053 context.details[1],
1054 ("key2".to_string(), "value2".to_string())
1055 );
1056 assert_eq!(
1057 context.details[2],
1058 ("key3".to_string(), "value3".to_string())
1059 );
1060 }
1061
1062 #[test]
1063 fn test_error_builder_into_conversion() {
1064 let builder = OxiGdalError::io_error_builder("Test error");
1065 let error: OxiGdalError = builder.into();
1066
1067 assert!(matches!(error, OxiGdalError::Io(IoError::Read { .. })));
1068 }
1069
1070 #[test]
1071 fn test_comprehensive_error_workflow() {
1072 #[cfg(feature = "std")]
1076 let error = {
1077 use std::path::Path;
1078 OxiGdalError::io_error_builder("Cannot open GeoTIFF file")
1079 .with_path(Path::new("/data/terrain.tif"))
1080 .with_operation("open_geotiff")
1081 .with_parameter("mode", "read")
1082 .with_suggestion("Check if file exists and is readable")
1083 .build()
1084 };
1085
1086 #[cfg(not(feature = "std"))]
1087 let error = OxiGdalError::io_error("Cannot open GeoTIFF file");
1088
1089 assert_eq!(error.code(), "E104");
1091
1092 let suggestion = error.suggestion();
1094 assert!(suggestion.is_some());
1095
1096 let context = error.context();
1098 assert!(!context.details.is_empty());
1099
1100 let error_string = error.to_string();
1102 assert!(!error_string.is_empty());
1103 }
1104}