1#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]
2use copybook_error::{Error, ErrorCode};
9use std::collections::HashMap;
10use std::fmt;
11use std::fmt::Write as _;
12use tracing::{debug, error, warn};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum ErrorMode {
27 Strict,
29 Lenient,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
45pub enum ErrorSeverity {
46 Info,
48 Warning,
50 Error,
52 Fatal,
54}
55
56#[derive(Debug, Clone)]
58pub struct ErrorReport {
59 pub error: Error,
61 pub severity: ErrorSeverity,
63 pub timestamp: std::time::SystemTime,
65 pub metadata: HashMap<String, String>,
67}
68
69#[derive(Debug, Clone, Default)]
71pub struct ErrorSummary {
72 pub error_counts: HashMap<ErrorSeverity, u64>,
74 pub error_codes: HashMap<ErrorCode, u64>,
76 pub records_with_errors: u64,
78 pub total_records: u64,
80 pub first_error: Option<ErrorReport>,
82 pub last_error: Option<ErrorReport>,
84 pub corruption_warnings: u64,
86}
87
88pub struct ErrorReporter {
90 mode: ErrorMode,
92 max_errors: Option<u64>,
94 summary: ErrorSummary,
96 verbose_logging: bool,
98}
99
100impl ErrorReporter {
101 #[must_use]
103 #[inline]
104 pub fn new(mode: ErrorMode, max_errors: Option<u64>) -> Self {
105 Self {
106 mode,
107 max_errors,
108 summary: ErrorSummary::default(),
109 verbose_logging: true,
110 }
111 }
112
113 #[must_use]
115 #[inline]
116 pub fn with_verbose_logging(mut self, verbose: bool) -> Self {
117 self.verbose_logging = verbose;
118 self
119 }
120
121 #[inline]
131 #[must_use = "Handle the Result or propagate the error"]
132 pub fn report_error(&mut self, error: Error) -> Result<(), Error> {
133 let severity = self.determine_severity(&error);
134 let report = ErrorReport {
135 error: error.clone(),
136 severity,
137 timestamp: std::time::SystemTime::now(),
138 metadata: HashMap::new(),
139 };
140
141 let should_continue = match severity {
143 ErrorSeverity::Fatal => false,
144 ErrorSeverity::Error => {
145 if self.mode == ErrorMode::Strict {
146 false
147 } else if let Some(max) = self.max_errors {
148 let current_error_count = self
149 .summary
150 .error_counts
151 .get(&ErrorSeverity::Error)
152 .unwrap_or(&0);
153 *current_error_count < max
154 } else {
155 true
156 }
157 }
158 ErrorSeverity::Warning | ErrorSeverity::Info => true,
159 };
160
161 self.update_statistics(&report);
163
164 self.log_error(&report);
166
167 if should_continue {
169 Ok(())
170 } else if matches!(severity, ErrorSeverity::Error) && self.max_errors.is_some() {
171 Err(Error::new(
172 ErrorCode::CBKS141_RECORD_TOO_LARGE, format!(
174 "Maximum error limit reached: {}",
175 self.max_errors.unwrap_or(0)
176 ),
177 ))
178 } else {
179 Err(error)
180 }
181 }
182
183 #[inline]
185 pub fn report_warning(&mut self, error: Error) {
186 let mut report = ErrorReport {
187 error,
188 severity: ErrorSeverity::Warning,
189 timestamp: std::time::SystemTime::now(),
190 metadata: HashMap::new(),
191 };
192
193 if is_corruption_warning(&report.error) {
195 self.summary.corruption_warnings += 1;
196 report
197 .metadata
198 .insert("corruption_type".to_string(), "transfer".to_string());
199 }
200
201 self.update_statistics(&report);
202 self.log_error(&report);
203 }
204
205 #[inline]
207 pub fn start_record(&mut self, record_index: u64) {
208 self.summary.total_records = record_index;
209 debug!("Processing record {record_index}");
210 }
211
212 #[must_use]
214 #[inline]
215 pub fn summary(&self) -> &ErrorSummary {
216 &self.summary
217 }
218
219 #[must_use]
221 #[inline]
222 pub fn has_errors(&self) -> bool {
223 self.summary
224 .error_counts
225 .get(&ErrorSeverity::Error)
226 .unwrap_or(&0)
227 > &0
228 || self
229 .summary
230 .error_counts
231 .get(&ErrorSeverity::Fatal)
232 .unwrap_or(&0)
233 > &0
234 }
235
236 #[must_use]
238 #[inline]
239 pub fn has_warnings(&self) -> bool {
240 self.summary
241 .error_counts
242 .get(&ErrorSeverity::Warning)
243 .unwrap_or(&0)
244 > &0
245 }
246
247 #[must_use]
249 #[inline]
250 pub fn error_count(&self) -> u64 {
251 self.summary
252 .error_counts
253 .get(&ErrorSeverity::Error)
254 .unwrap_or(&0)
255 + self
256 .summary
257 .error_counts
258 .get(&ErrorSeverity::Fatal)
259 .unwrap_or(&0)
260 }
261
262 #[must_use]
264 #[inline]
265 pub fn warning_count(&self) -> u64 {
266 *self
267 .summary
268 .error_counts
269 .get(&ErrorSeverity::Warning)
270 .unwrap_or(&0)
271 }
272
273 #[must_use]
275 #[inline]
276 pub fn generate_report(&self) -> String {
277 let mut report = String::new();
278
279 report.push_str("=== Error Summary ===\n");
280 let _ = writeln!(
281 report,
282 "Total records processed: {}",
283 self.summary.total_records
284 );
285 let _ = writeln!(
286 report,
287 "Records with errors: {}",
288 self.summary.records_with_errors
289 );
290
291 if !self.summary.error_counts.is_empty() {
292 report.push_str("\nError counts by severity:\n");
293 for (severity, count) in &self.summary.error_counts {
294 if *count > 0 {
295 let _ = writeln!(report, " {severity:?}: {count}");
296 }
297 }
298 }
299
300 if !self.summary.error_codes.is_empty() {
301 report.push_str("\nError counts by code:\n");
302 for (code, count) in &self.summary.error_codes {
303 if *count > 0 {
304 let _ = writeln!(report, " {code}: {count}");
305 }
306 }
307 }
308
309 if self.summary.corruption_warnings > 0 {
310 report.push('\n');
311 let _ = writeln!(
312 report,
313 "Transfer corruption warnings: {}",
314 self.summary.corruption_warnings
315 );
316 }
317
318 if let Some(ref first_error) = self.summary.first_error {
319 report.push('\n');
320 let _ = writeln!(report, "First error: {}", first_error.error);
321 }
322
323 report
324 }
325
326 #[inline]
328 fn determine_severity(&self, error: &Error) -> ErrorSeverity {
329 match error.code {
330 ErrorCode::CBKP001_SYNTAX
332 | ErrorCode::CBKP011_UNSUPPORTED_CLAUSE
333 | ErrorCode::CBKP021_ODO_NOT_TAIL
334 | ErrorCode::CBKP022_NESTED_ODO
335 | ErrorCode::CBKP023_ODO_REDEFINES
336 | ErrorCode::CBKP051_UNSUPPORTED_EDITED_PIC
337 | ErrorCode::CBKP101_INVALID_PIC
338 | ErrorCode::CBKS121_COUNTER_NOT_FOUND
340 | ErrorCode::CBKS141_RECORD_TOO_LARGE
341 | ErrorCode::CBKS601_RENAME_UNKNOWN_FROM
342 | ErrorCode::CBKS602_RENAME_UNKNOWN_THRU
343 | ErrorCode::CBKS603_RENAME_NOT_CONTIGUOUS
344 | ErrorCode::CBKS604_RENAME_REVERSED_RANGE
345 | ErrorCode::CBKS605_RENAME_FROM_CROSSES_GROUP
346 | ErrorCode::CBKS606_RENAME_THRU_CROSSES_GROUP
347 | ErrorCode::CBKS607_RENAME_CROSSES_OCCURS
348 | ErrorCode::CBKS608_RENAME_QUALIFIED_NAME_NOT_FOUND
349 | ErrorCode::CBKS609_RENAME_OVER_REDEFINES
350 | ErrorCode::CBKS610_RENAME_MULTIPLE_REDEFINES
351 | ErrorCode::CBKS611_RENAME_PARTIAL_OCCURS
352 | ErrorCode::CBKS612_RENAME_ODO_NOT_SUPPORTED
353 | ErrorCode::CBKS701_PROJECTION_INVALID_ODO
354 | ErrorCode::CBKS702_PROJECTION_UNRESOLVED_ALIAS
355 | ErrorCode::CBKS703_PROJECTION_FIELD_NOT_FOUND
356 | ErrorCode::CBKE505_SCALE_MISMATCH
357 | ErrorCode::CBKE510_NUMERIC_OVERFLOW
358 | ErrorCode::CBKE515_STRING_LENGTH_VIOLATION
359 | ErrorCode::CBKC201_JSON_WRITE_ERROR
361 | ErrorCode::CBKI001_INVALID_STATE => ErrorSeverity::Fatal,
363
364 ErrorCode::CBKS301_ODO_CLIPPED
366 | ErrorCode::CBKS302_ODO_RAISED
367 | ErrorCode::CBKR211_RDW_RESERVED_NONZERO => {
369 if self.mode == ErrorMode::Strict {
370 ErrorSeverity::Fatal
371 } else {
372 ErrorSeverity::Warning
373 }
374 }
375
376 ErrorCode::CBKC301_INVALID_EBCDIC_BYTE
378 | ErrorCode::CBKD412_ZONED_BLANK_IS_ZERO
379 | ErrorCode::CBKD423_EDITED_PIC_BLANK_WHEN_ZERO
380 | ErrorCode::CBKF104_RDW_SUSPECT_ASCII => ErrorSeverity::Warning,
381
382 ErrorCode::CBKD101_INVALID_FIELD_TYPE
384 | ErrorCode::CBKD301_RECORD_TOO_SHORT
385 | ErrorCode::CBKD302_EDITED_PIC_NOT_IMPLEMENTED
386 | ErrorCode::CBKD401_COMP3_INVALID_NIBBLE
387 | ErrorCode::CBKD410_ZONED_OVERFLOW
388 | ErrorCode::CBKD411_ZONED_BAD_SIGN
389 | ErrorCode::CBKD421_EDITED_PIC_INVALID_FORMAT
390 | ErrorCode::CBKD422_EDITED_PIC_SIGN_MISMATCH
391 | ErrorCode::CBKD431_FLOAT_NAN
392 | ErrorCode::CBKD432_FLOAT_INFINITY
393 | ErrorCode::CBKD413_ZONED_INVALID_ENCODING
394 | ErrorCode::CBKD414_ZONED_MIXED_ENCODING
395 | ErrorCode::CBKD415_ZONED_ENCODING_AMBIGUOUS
396 | ErrorCode::CBKE501_JSON_TYPE_MISMATCH
397 | ErrorCode::CBKE521_ARRAY_LEN_OOB
398 | ErrorCode::CBKE530_SIGN_SEPARATE_ENCODE_ERROR
399 | ErrorCode::CBKE531_FLOAT_ENCODE_OVERFLOW
400 | ErrorCode::CBKF102_RECORD_LENGTH_INVALID
401 | ErrorCode::CBKF221_RDW_UNDERFLOW
402 | ErrorCode::CBKA001_BASELINE_ERROR
403 | ErrorCode::CBKW001_SCHEMA_CONVERSION
404 | ErrorCode::CBKW002_TYPE_MAPPING
405 | ErrorCode::CBKW003_DECIMAL_OVERFLOW
406 | ErrorCode::CBKW004_BATCH_BUILD
407 | ErrorCode::CBKW005_PARQUET_WRITE => ErrorSeverity::Error,
408 }
409 }
410
411 #[inline]
413 fn update_statistics(&mut self, report: &ErrorReport) {
414 *self
416 .summary
417 .error_counts
418 .entry(report.severity)
419 .or_insert(0) += 1;
420
421 *self
423 .summary
424 .error_codes
425 .entry(report.error.code)
426 .or_insert(0) += 1;
427
428 if matches!(report.severity, ErrorSeverity::Error | ErrorSeverity::Fatal)
430 && let Some(ref context) = report.error.context
431 && let Some(record_index) = context.record_index
432 {
433 if self.summary.records_with_errors < record_index {
435 self.summary.records_with_errors = record_index;
436 }
437 }
438
439 if self.summary.first_error.is_none() {
441 self.summary.first_error = Some(report.clone());
442 }
443 self.summary.last_error = Some(report.clone());
444 }
445
446 #[inline]
448 fn log_error(&self, report: &ErrorReport) {
449 let error_msg = if self.verbose_logging {
450 format!("{}", report.error)
451 } else {
452 format!("{}: {}", report.error.code, report.error.message)
453 };
454
455 match report.severity {
456 ErrorSeverity::Fatal | ErrorSeverity::Error => error!("{error_msg}"),
457 ErrorSeverity::Warning => warn!("{error_msg}"),
458 ErrorSeverity::Info => debug!("{error_msg}"),
459 }
460
461 if self.verbose_logging
463 && let Some(ref context) = report.error.context
464 && (context.record_index.is_some()
465 || context.field_path.is_some()
466 || context.byte_offset.is_some())
467 {
468 debug!(" Context: {context}");
469 }
470 }
471}
472
473#[inline]
475fn is_corruption_warning(error: &Error) -> bool {
476 matches!(
477 error.code,
478 ErrorCode::CBKF104_RDW_SUSPECT_ASCII | ErrorCode::CBKC301_INVALID_EBCDIC_BYTE
479 )
480}
481
482impl fmt::Display for ErrorSeverity {
483 #[inline]
484 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
485 match self {
486 ErrorSeverity::Info => write!(f, "INFO"),
487 ErrorSeverity::Warning => write!(f, "WARN"),
488 ErrorSeverity::Error => write!(f, "ERROR"),
489 ErrorSeverity::Fatal => write!(f, "FATAL"),
490 }
491 }
492}
493
494impl ErrorSummary {
495 #[must_use]
497 #[inline]
498 pub fn has_errors(&self) -> bool {
499 self.error_counts.get(&ErrorSeverity::Error).unwrap_or(&0) > &0
500 || self.error_counts.get(&ErrorSeverity::Fatal).unwrap_or(&0) > &0
501 }
502
503 #[must_use]
505 #[inline]
506 pub fn has_warnings(&self) -> bool {
507 self.error_counts.get(&ErrorSeverity::Warning).unwrap_or(&0) > &0
508 }
509
510 #[must_use]
512 #[inline]
513 pub fn error_count(&self) -> u64 {
514 self.error_counts.get(&ErrorSeverity::Error).unwrap_or(&0)
515 + self.error_counts.get(&ErrorSeverity::Fatal).unwrap_or(&0)
516 }
517
518 #[must_use]
520 #[inline]
521 pub fn warning_count(&self) -> u64 {
522 *self.error_counts.get(&ErrorSeverity::Warning).unwrap_or(&0)
523 }
524}
525
526#[cfg(test)]
527#[allow(clippy::expect_used)]
528#[allow(clippy::unwrap_used)]
529mod tests {
530 use super::*;
531 use copybook_error::ErrorCode;
532
533 #[test]
534 fn test_error_reporter_strict_mode() {
535 let mut reporter = ErrorReporter::new(ErrorMode::Strict, None);
536
537 let error = Error::new(ErrorCode::CBKD401_COMP3_INVALID_NIBBLE, "Invalid nibble")
538 .with_record(1)
539 .with_field("CUSTOMER.ID")
540 .with_offset(42);
541
542 let result = reporter.report_error(error);
544 assert!(result.is_err());
545 assert!(reporter.has_errors());
546 assert_eq!(reporter.error_count(), 1);
547 }
548
549 #[test]
550 fn test_error_reporter_lenient_mode() {
551 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
552
553 let error = Error::new(ErrorCode::CBKD401_COMP3_INVALID_NIBBLE, "Invalid nibble")
554 .with_record(1)
555 .with_field("CUSTOMER.ID")
556 .with_offset(42);
557
558 let result = reporter.report_error(error);
560 assert!(result.is_ok());
561 assert!(reporter.has_errors());
562 assert_eq!(reporter.error_count(), 1);
563 }
564
565 #[test]
566 fn test_max_errors_limit() {
567 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, Some(1));
568
569 let error1 = Error::new(ErrorCode::CBKD401_COMP3_INVALID_NIBBLE, "Error 1");
571 assert!(reporter.report_error(error1).is_ok());
572
573 let error2 = Error::new(ErrorCode::CBKD411_ZONED_BAD_SIGN, "Error 2");
575 assert!(reporter.report_error(error2).is_err());
576 }
577
578 #[test]
579 fn test_warning_reporting() {
580 let mut reporter = ErrorReporter::new(ErrorMode::Strict, None);
581
582 let warning = Error::new(ErrorCode::CBKD412_ZONED_BLANK_IS_ZERO, "Blank field");
583 reporter.report_warning(warning);
584
585 assert!(!reporter.has_errors());
586 assert!(reporter.has_warnings());
587 assert_eq!(reporter.warning_count(), 1);
588 }
589
590 #[test]
591 fn test_corruption_detection() {
592 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
593
594 let corruption_error = Error::new(
595 ErrorCode::CBKF104_RDW_SUSPECT_ASCII,
596 "ASCII corruption detected",
597 );
598 reporter.report_warning(corruption_error);
599
600 assert_eq!(reporter.summary().corruption_warnings, 1);
601 }
602
603 #[test]
604 fn test_error_summary_generation() {
605 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
606
607 reporter.start_record(1);
608 let error =
609 Error::new(ErrorCode::CBKD401_COMP3_INVALID_NIBBLE, "Test error").with_record(1);
610 let _ = reporter.report_error(error);
611
612 let report = reporter.generate_report();
613 assert!(report.contains("Total records processed: 1"));
614 assert!(report.contains("CBKD401_COMP3_INVALID_NIBBLE: 1"));
615 }
616
617 #[test]
620 fn test_empty_reporter_no_errors_no_warnings() {
621 let reporter = ErrorReporter::new(ErrorMode::Lenient, None);
622
623 assert!(!reporter.has_errors());
624 assert!(!reporter.has_warnings());
625 assert_eq!(reporter.error_count(), 0);
626 assert_eq!(reporter.warning_count(), 0);
627
628 let summary = reporter.summary();
629 assert!(!summary.has_errors());
630 assert!(!summary.has_warnings());
631 assert_eq!(summary.error_count(), 0);
632 assert_eq!(summary.warning_count(), 0);
633 assert!(summary.first_error.is_none());
634 assert!(summary.last_error.is_none());
635 assert_eq!(summary.corruption_warnings, 0);
636 assert_eq!(summary.total_records, 0);
637 assert_eq!(summary.records_with_errors, 0);
638 }
639
640 #[test]
641 fn test_empty_reporter_report_output() {
642 let reporter = ErrorReporter::new(ErrorMode::Strict, None);
643 let report = reporter.generate_report();
644
645 assert!(report.contains("=== Error Summary ==="));
646 assert!(report.contains("Total records processed: 0"));
647 assert!(report.contains("Records with errors: 0"));
648 assert!(!report.contains("Error counts by severity:"));
650 assert!(!report.contains("Error counts by code:"));
651 }
652
653 #[test]
654 fn test_fatal_error_stops_in_lenient_mode() {
655 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
656
657 let error = Error::new(ErrorCode::CBKP001_SYNTAX, "Unexpected token");
659 let result = reporter.report_error(error);
660 assert!(result.is_err());
661 assert!(reporter.has_errors());
662 }
663
664 #[test]
665 fn test_multiple_errors_summary_counts() {
666 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
667
668 let errors = [
669 Error::new(ErrorCode::CBKD401_COMP3_INVALID_NIBBLE, "nibble 1"),
670 Error::new(ErrorCode::CBKD401_COMP3_INVALID_NIBBLE, "nibble 2"),
671 Error::new(ErrorCode::CBKD411_ZONED_BAD_SIGN, "bad sign"),
672 ];
673 for e in errors {
674 let _ = reporter.report_error(e);
675 }
676
677 assert_eq!(reporter.error_count(), 3);
678 let summary = reporter.summary();
679 assert_eq!(
680 *summary
681 .error_codes
682 .get(&ErrorCode::CBKD401_COMP3_INVALID_NIBBLE)
683 .unwrap(),
684 2
685 );
686 assert_eq!(
687 *summary
688 .error_codes
689 .get(&ErrorCode::CBKD411_ZONED_BAD_SIGN)
690 .unwrap(),
691 1
692 );
693 }
694
695 #[test]
696 fn test_first_and_last_error_tracked() {
697 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
698
699 let _ = reporter.report_error(Error::new(
700 ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
701 "first error",
702 ));
703 let _ = reporter.report_error(Error::new(
704 ErrorCode::CBKD411_ZONED_BAD_SIGN,
705 "second error",
706 ));
707 let _ = reporter.report_error(Error::new(ErrorCode::CBKD410_ZONED_OVERFLOW, "third error"));
708
709 let summary = reporter.summary();
710 let first = summary.first_error.as_ref().unwrap();
711 let last = summary.last_error.as_ref().unwrap();
712 assert_eq!(first.error.code, ErrorCode::CBKD401_COMP3_INVALID_NIBBLE);
713 assert_eq!(first.error.message, "first error");
714 assert_eq!(last.error.code, ErrorCode::CBKD410_ZONED_OVERFLOW);
715 assert_eq!(last.error.message, "third error");
716 }
717
718 #[test]
719 fn test_max_errors_boundary_exact_limit() {
720 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, Some(3));
721
722 for i in 1..=3 {
724 let e = Error::new(ErrorCode::CBKD401_COMP3_INVALID_NIBBLE, format!("err {i}"));
725 assert!(reporter.report_error(e).is_ok(), "error {i} should succeed");
726 }
727
728 let e4 = Error::new(ErrorCode::CBKD401_COMP3_INVALID_NIBBLE, "err 4");
730 assert!(reporter.report_error(e4).is_err());
731 assert_eq!(reporter.error_count(), 4);
732 }
733
734 #[test]
735 fn test_max_errors_unlimited_in_lenient() {
736 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
737
738 for i in 0..100 {
739 let e = Error::new(
740 ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
741 format!("error {i}"),
742 );
743 assert!(reporter.report_error(e).is_ok());
744 }
745 assert_eq!(reporter.error_count(), 100);
746 }
747
748 #[test]
749 fn test_severity_display_formatting() {
750 assert_eq!(format!("{}", ErrorSeverity::Info), "INFO");
751 assert_eq!(format!("{}", ErrorSeverity::Warning), "WARN");
752 assert_eq!(format!("{}", ErrorSeverity::Error), "ERROR");
753 assert_eq!(format!("{}", ErrorSeverity::Fatal), "FATAL");
754 }
755
756 #[test]
757 fn test_severity_ordering() {
758 assert!(ErrorSeverity::Info < ErrorSeverity::Warning);
759 assert!(ErrorSeverity::Warning < ErrorSeverity::Error);
760 assert!(ErrorSeverity::Error < ErrorSeverity::Fatal);
761 }
762
763 #[test]
764 fn test_odo_clipped_severity_depends_on_mode() {
765 let mut lenient = ErrorReporter::new(ErrorMode::Lenient, None);
767 let e = Error::new(ErrorCode::CBKS301_ODO_CLIPPED, "ODO clipped");
768 assert!(lenient.report_error(e).is_ok());
769 assert!(!lenient.has_errors());
770 assert!(lenient.has_warnings());
771
772 let mut strict = ErrorReporter::new(ErrorMode::Strict, None);
774 let e = Error::new(ErrorCode::CBKS301_ODO_CLIPPED, "ODO clipped");
775 assert!(strict.report_error(e).is_err());
776 }
777
778 #[test]
779 fn test_mixed_errors_and_warnings_report() {
780 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
781
782 reporter.report_warning(Error::new(ErrorCode::CBKD412_ZONED_BLANK_IS_ZERO, "blank"));
783 let _ = reporter.report_error(Error::new(
784 ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
785 "bad nibble",
786 ));
787 reporter.report_warning(Error::new(
788 ErrorCode::CBKC301_INVALID_EBCDIC_BYTE,
789 "bad byte",
790 ));
791
792 let report = reporter.generate_report();
793 assert!(report.contains("Error counts by severity:"));
794 assert!(report.contains("Error counts by code:"));
795 assert_eq!(reporter.error_count(), 1);
796 assert_eq!(reporter.warning_count(), 2);
797 }
798
799 #[test]
800 fn test_corruption_detection_ebcdic_byte() {
801 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
802
803 reporter.report_warning(Error::new(
804 ErrorCode::CBKC301_INVALID_EBCDIC_BYTE,
805 "Invalid EBCDIC byte 0xFF",
806 ));
807
808 assert_eq!(reporter.summary().corruption_warnings, 1);
809 assert!(reporter.has_warnings());
810 assert!(!reporter.has_errors());
811 }
812
813 #[test]
814 fn test_multiple_corruption_warnings_counted() {
815 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
816
817 for _ in 0..5 {
818 reporter.report_warning(Error::new(
819 ErrorCode::CBKF104_RDW_SUSPECT_ASCII,
820 "ASCII suspected",
821 ));
822 }
823 reporter.report_warning(Error::new(
824 ErrorCode::CBKC301_INVALID_EBCDIC_BYTE,
825 "Bad EBCDIC",
826 ));
827
828 assert_eq!(reporter.summary().corruption_warnings, 6);
829 assert_eq!(reporter.warning_count(), 6);
830 }
831
832 #[test]
833 fn test_verbose_logging_toggle() {
834 let reporter = ErrorReporter::new(ErrorMode::Lenient, None).with_verbose_logging(false);
835 assert!(!reporter.has_errors());
837 assert_eq!(reporter.error_count(), 0);
838 }
839
840 #[test]
841 fn test_very_long_error_message() {
842 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
843 let long_msg = "x".repeat(10_000);
844 let e = Error::new(ErrorCode::CBKD401_COMP3_INVALID_NIBBLE, long_msg.clone());
845 let _ = reporter.report_error(e);
846
847 let first = reporter.summary().first_error.as_ref().unwrap();
848 assert_eq!(first.error.message.len(), 10_000);
849
850 let report = reporter.generate_report();
851 assert!(report.contains("First error:"));
852 }
853
854 #[test]
855 fn test_start_record_tracks_total_records() {
856 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
857
858 reporter.start_record(0);
859 assert_eq!(reporter.summary().total_records, 0);
860
861 reporter.start_record(42);
862 assert_eq!(reporter.summary().total_records, 42);
863
864 reporter.start_record(100);
865 assert_eq!(reporter.summary().total_records, 100);
866
867 let report = reporter.generate_report();
868 assert!(report.contains("Total records processed: 100"));
869 }
870
871 #[test]
872 fn test_error_report_contains_first_error_in_output() {
873 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
874
875 let _ = reporter.report_error(Error::new(
876 ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
877 "the very first problem",
878 ));
879 let _ = reporter.report_error(Error::new(
880 ErrorCode::CBKD411_ZONED_BAD_SIGN,
881 "second problem",
882 ));
883
884 let report = reporter.generate_report();
885 assert!(report.contains("First error:"));
886 assert!(report.contains("the very first problem"));
887 }
889
890 #[test]
891 fn test_error_summary_default_is_clean() {
892 let summary = ErrorSummary::default();
893 assert!(!summary.has_errors());
894 assert!(!summary.has_warnings());
895 assert_eq!(summary.error_count(), 0);
896 assert_eq!(summary.warning_count(), 0);
897 assert!(summary.first_error.is_none());
898 assert!(summary.last_error.is_none());
899 assert_eq!(summary.records_with_errors, 0);
900 assert_eq!(summary.total_records, 0);
901 assert_eq!(summary.corruption_warnings, 0);
902 }
903
904 #[test]
909 fn test_severity_parse_errors_are_fatal() {
910 let parse_codes = [
911 ErrorCode::CBKP001_SYNTAX,
912 ErrorCode::CBKP011_UNSUPPORTED_CLAUSE,
913 ErrorCode::CBKP021_ODO_NOT_TAIL,
914 ErrorCode::CBKP022_NESTED_ODO,
915 ErrorCode::CBKP023_ODO_REDEFINES,
916 ErrorCode::CBKP051_UNSUPPORTED_EDITED_PIC,
917 ErrorCode::CBKP101_INVALID_PIC,
918 ];
919 for code in parse_codes {
920 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
921 let err = Error::new(code, "parse error");
922 let result = reporter.report_error(err);
923 assert!(
924 result.is_err(),
925 "CBKP code {code} should be fatal even in lenient mode"
926 );
927 }
928 }
929
930 #[test]
931 fn test_severity_schema_errors_are_fatal() {
932 let schema_codes = [
933 ErrorCode::CBKS121_COUNTER_NOT_FOUND,
934 ErrorCode::CBKS141_RECORD_TOO_LARGE,
935 ErrorCode::CBKS601_RENAME_UNKNOWN_FROM,
936 ErrorCode::CBKS701_PROJECTION_INVALID_ODO,
937 ErrorCode::CBKS703_PROJECTION_FIELD_NOT_FOUND,
938 ];
939 for code in schema_codes {
940 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
941 let err = Error::new(code, "schema error");
942 let result = reporter.report_error(err);
943 assert!(result.is_err(), "CBKS code {code} should be fatal");
944 }
945 }
946
947 #[test]
948 fn test_severity_data_errors_are_error_level() {
949 let data_codes = [
950 ErrorCode::CBKD101_INVALID_FIELD_TYPE,
951 ErrorCode::CBKD301_RECORD_TOO_SHORT,
952 ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
953 ErrorCode::CBKD410_ZONED_OVERFLOW,
954 ErrorCode::CBKD411_ZONED_BAD_SIGN,
955 ErrorCode::CBKD421_EDITED_PIC_INVALID_FORMAT,
956 ErrorCode::CBKD431_FLOAT_NAN,
957 ErrorCode::CBKD432_FLOAT_INFINITY,
958 ];
959 for code in data_codes {
960 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
961 let err = Error::new(code, "data error");
962 let result = reporter.report_error(err);
963 assert!(
964 result.is_ok(),
965 "CBKD code {code} should continue in lenient mode"
966 );
967 assert!(reporter.has_errors());
968 }
969 }
970
971 #[test]
972 fn test_severity_warning_codes_are_warnings() {
973 let warning_codes = [
974 ErrorCode::CBKD412_ZONED_BLANK_IS_ZERO,
975 ErrorCode::CBKC301_INVALID_EBCDIC_BYTE,
976 ErrorCode::CBKD423_EDITED_PIC_BLANK_WHEN_ZERO,
977 ErrorCode::CBKF104_RDW_SUSPECT_ASCII,
978 ];
979 for code in warning_codes {
980 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
981 let err = Error::new(code, "warning");
982 let result = reporter.report_error(err);
983 assert!(result.is_ok(), "code {code} should continue");
984 assert!(
985 !reporter.has_errors(),
986 "code {code} should be warning not error"
987 );
988 assert!(reporter.has_warnings(), "code {code} should be a warning");
989 }
990 }
991
992 #[test]
993 fn test_severity_odo_clipped_warning_in_lenient_fatal_in_strict() {
994 for code in [
995 ErrorCode::CBKS301_ODO_CLIPPED,
996 ErrorCode::CBKS302_ODO_RAISED,
997 ] {
998 let mut lenient = ErrorReporter::new(ErrorMode::Lenient, None);
1000 assert!(lenient.report_error(Error::new(code, "odo")).is_ok());
1001 assert!(lenient.has_warnings());
1002 assert!(!lenient.has_errors());
1003
1004 let mut strict = ErrorReporter::new(ErrorMode::Strict, None);
1006 assert!(strict.report_error(Error::new(code, "odo")).is_err());
1007 }
1008 }
1009
1010 #[test]
1011 fn test_severity_encode_errors_are_error_level_in_lenient() {
1012 let encode_codes = [
1013 ErrorCode::CBKE501_JSON_TYPE_MISMATCH,
1014 ErrorCode::CBKE521_ARRAY_LEN_OOB,
1015 ErrorCode::CBKE530_SIGN_SEPARATE_ENCODE_ERROR,
1016 ErrorCode::CBKE531_FLOAT_ENCODE_OVERFLOW,
1017 ];
1018 for code in encode_codes {
1019 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
1020 let result = reporter.report_error(Error::new(code, "encode"));
1021 assert!(result.is_ok(), "{code} should continue in lenient");
1022 assert!(reporter.has_errors());
1023 }
1024 }
1025
1026 #[test]
1027 fn test_severity_infrastructure_error_is_fatal() {
1028 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
1029 let err = Error::new(ErrorCode::CBKI001_INVALID_STATE, "bad state");
1030 assert!(reporter.report_error(err).is_err());
1031 }
1032
1033 #[test]
1038 fn test_records_with_errors_tracking() {
1039 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
1040
1041 let _ = reporter.report_error(
1043 Error::new(ErrorCode::CBKD401_COMP3_INVALID_NIBBLE, "bad").with_record(5),
1044 );
1045 let _ = reporter
1046 .report_error(Error::new(ErrorCode::CBKD411_ZONED_BAD_SIGN, "bad").with_record(10));
1047
1048 assert_eq!(reporter.summary().records_with_errors, 10);
1049 }
1050
1051 #[test]
1052 fn test_records_with_errors_not_incremented_for_warnings() {
1053 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
1054 reporter.report_warning(
1055 Error::new(ErrorCode::CBKD412_ZONED_BLANK_IS_ZERO, "blank").with_record(5),
1056 );
1057 assert_eq!(reporter.summary().records_with_errors, 0);
1058 }
1059
1060 #[test]
1065 fn test_corruption_warning_adds_metadata() {
1066 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
1067 reporter.report_warning(Error::new(
1068 ErrorCode::CBKF104_RDW_SUSPECT_ASCII,
1069 "ASCII transfer",
1070 ));
1071 assert_eq!(reporter.summary().corruption_warnings, 1);
1072 }
1073
1074 #[test]
1075 fn test_non_corruption_warning_no_corruption_count() {
1076 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
1077 reporter.report_warning(Error::new(ErrorCode::CBKD412_ZONED_BLANK_IS_ZERO, "blanks"));
1078 assert_eq!(reporter.summary().corruption_warnings, 0);
1079 }
1080
1081 #[test]
1086 fn test_error_mode_equality() {
1087 assert_eq!(ErrorMode::Strict, ErrorMode::Strict);
1088 assert_eq!(ErrorMode::Lenient, ErrorMode::Lenient);
1089 assert_ne!(ErrorMode::Strict, ErrorMode::Lenient);
1090 }
1091
1092 #[test]
1093 fn test_error_severity_equality_and_hash() {
1094 use std::collections::HashSet;
1095 let mut set = HashSet::new();
1096 set.insert(ErrorSeverity::Info);
1097 set.insert(ErrorSeverity::Warning);
1098 set.insert(ErrorSeverity::Error);
1099 set.insert(ErrorSeverity::Fatal);
1100 set.insert(ErrorSeverity::Info); assert_eq!(set.len(), 4);
1102 }
1103
1104 #[test]
1109 fn test_generate_report_includes_corruption_section() {
1110 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
1111 reporter.report_warning(Error::new(ErrorCode::CBKF104_RDW_SUSPECT_ASCII, "ASCII"));
1112 reporter.report_warning(Error::new(ErrorCode::CBKC301_INVALID_EBCDIC_BYTE, "EBCDIC"));
1113
1114 let report = reporter.generate_report();
1115 assert!(report.contains("Transfer corruption warnings: 2"));
1116 }
1117
1118 #[test]
1119 fn test_generate_report_no_corruption_section_when_zero() {
1120 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
1121 reporter.report_warning(Error::new(ErrorCode::CBKD412_ZONED_BLANK_IS_ZERO, "blank"));
1122
1123 let report = reporter.generate_report();
1124 assert!(!report.contains("Transfer corruption warnings"));
1125 }
1126
1127 #[test]
1128 fn test_error_summary_has_errors_with_fatal_only() {
1129 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
1130 let _ = reporter.report_error(Error::new(ErrorCode::CBKP001_SYNTAX, "syntax"));
1132 assert!(reporter.summary().has_errors());
1133 assert_eq!(reporter.summary().error_count(), 1);
1134 }
1135
1136 #[test]
1141 fn test_error_code_json_roundtrip_in_summary() {
1142 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
1143 let _ = reporter.report_error(Error::new(
1144 ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
1145 "nibble",
1146 ));
1147
1148 for (code, count) in &reporter.summary().error_codes {
1150 let json = serde_json::to_string(code).unwrap();
1151 let rt: ErrorCode = serde_json::from_str(&json).unwrap();
1152 assert_eq!(*code, rt);
1153 assert_eq!(*count, 1);
1154 }
1155 }
1156
1157 #[test]
1158 fn test_error_code_json_serialization_format() {
1159 let test_cases = [
1161 (ErrorCode::CBKP001_SYNTAX, r#""CBKP001_SYNTAX""#),
1162 (
1163 ErrorCode::CBKS121_COUNTER_NOT_FOUND,
1164 r#""CBKS121_COUNTER_NOT_FOUND""#,
1165 ),
1166 (
1167 ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
1168 r#""CBKD401_COMP3_INVALID_NIBBLE""#,
1169 ),
1170 (
1171 ErrorCode::CBKE501_JSON_TYPE_MISMATCH,
1172 r#""CBKE501_JSON_TYPE_MISMATCH""#,
1173 ),
1174 (
1175 ErrorCode::CBKR211_RDW_RESERVED_NONZERO,
1176 r#""CBKR211_RDW_RESERVED_NONZERO""#,
1177 ),
1178 (
1179 ErrorCode::CBKF102_RECORD_LENGTH_INVALID,
1180 r#""CBKF102_RECORD_LENGTH_INVALID""#,
1181 ),
1182 ];
1183 for (code, expected_json) in test_cases {
1184 let json = serde_json::to_string(&code).unwrap();
1185 assert_eq!(json, expected_json, "serialization mismatch for {code}");
1186 }
1187 }
1188
1189 #[test]
1194 fn test_max_errors_zero_stops_immediately() {
1195 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, Some(0));
1196 let err = Error::new(ErrorCode::CBKD401_COMP3_INVALID_NIBBLE, "first");
1197 assert!(reporter.report_error(err).is_err());
1199 }
1200
1201 #[test]
1202 fn test_strict_mode_stops_on_every_data_error() {
1203 let data_codes = [
1204 ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
1205 ErrorCode::CBKD411_ZONED_BAD_SIGN,
1206 ErrorCode::CBKD410_ZONED_OVERFLOW,
1207 ErrorCode::CBKE501_JSON_TYPE_MISMATCH,
1208 ];
1209 for code in data_codes {
1210 let mut reporter = ErrorReporter::new(ErrorMode::Strict, None);
1211 let result = reporter.report_error(Error::new(code, "data error"));
1212 assert!(result.is_err(), "strict mode should stop on {code}");
1213 }
1214 }
1215
1216 #[test]
1217 fn test_warning_does_not_count_as_error() {
1218 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, Some(1));
1219 for _ in 0..10 {
1221 reporter.report_warning(Error::new(ErrorCode::CBKD412_ZONED_BLANK_IS_ZERO, "blank"));
1222 }
1223 assert_eq!(reporter.error_count(), 0);
1224 assert_eq!(reporter.warning_count(), 10);
1225 let err = Error::new(ErrorCode::CBKD401_COMP3_INVALID_NIBBLE, "first real error");
1227 assert!(reporter.report_error(err).is_ok());
1228 }
1229}