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::CBKR101_FIXED_RECORD_ERROR
363 | ErrorCode::CBKR201_RDW_READ_ERROR
364 | ErrorCode::CBKI001_INVALID_STATE => ErrorSeverity::Fatal,
366
367 ErrorCode::CBKS301_ODO_CLIPPED
369 | ErrorCode::CBKS302_ODO_RAISED
370 | ErrorCode::CBKR211_RDW_RESERVED_NONZERO => {
372 if self.mode == ErrorMode::Strict {
373 ErrorSeverity::Fatal
374 } else {
375 ErrorSeverity::Warning
376 }
377 }
378
379 ErrorCode::CBKC301_INVALID_EBCDIC_BYTE
381 | ErrorCode::CBKD412_ZONED_BLANK_IS_ZERO
382 | ErrorCode::CBKD423_EDITED_PIC_BLANK_WHEN_ZERO
383 | ErrorCode::CBKF104_RDW_SUSPECT_ASCII => ErrorSeverity::Warning,
384
385 ErrorCode::CBKD101_INVALID_FIELD_TYPE
387 | ErrorCode::CBKD301_RECORD_TOO_SHORT
388 | ErrorCode::CBKD302_EDITED_PIC_NOT_IMPLEMENTED
389 | ErrorCode::CBKD401_COMP3_INVALID_NIBBLE
390 | ErrorCode::CBKD410_ZONED_OVERFLOW
391 | ErrorCode::CBKD411_ZONED_BAD_SIGN
392 | ErrorCode::CBKD421_EDITED_PIC_INVALID_FORMAT
393 | ErrorCode::CBKD422_EDITED_PIC_SIGN_MISMATCH
394 | ErrorCode::CBKD431_FLOAT_NAN
395 | ErrorCode::CBKD432_FLOAT_INFINITY
396 | ErrorCode::CBKD413_ZONED_INVALID_ENCODING
397 | ErrorCode::CBKD414_ZONED_MIXED_ENCODING
398 | ErrorCode::CBKD415_ZONED_ENCODING_AMBIGUOUS
399 | ErrorCode::CBKE501_JSON_TYPE_MISMATCH
400 | ErrorCode::CBKE521_ARRAY_LEN_OOB
401 | ErrorCode::CBKE530_SIGN_SEPARATE_ENCODE_ERROR
402 | ErrorCode::CBKE531_FLOAT_ENCODE_OVERFLOW
403 | ErrorCode::CBKF102_RECORD_LENGTH_INVALID
404 | ErrorCode::CBKF221_RDW_UNDERFLOW
405 | ErrorCode::CBKA001_BASELINE_ERROR
406 | ErrorCode::CBKW001_SCHEMA_CONVERSION
407 | ErrorCode::CBKW002_TYPE_MAPPING
408 | ErrorCode::CBKW003_DECIMAL_OVERFLOW
409 | ErrorCode::CBKW004_BATCH_BUILD
410 | ErrorCode::CBKW005_PARQUET_WRITE => ErrorSeverity::Error,
411 }
412 }
413
414 #[inline]
416 fn update_statistics(&mut self, report: &ErrorReport) {
417 *self
419 .summary
420 .error_counts
421 .entry(report.severity)
422 .or_insert(0) += 1;
423
424 *self
426 .summary
427 .error_codes
428 .entry(report.error.code)
429 .or_insert(0) += 1;
430
431 if matches!(report.severity, ErrorSeverity::Error | ErrorSeverity::Fatal)
433 && let Some(ref context) = report.error.context
434 && let Some(record_index) = context.record_index
435 {
436 if self.summary.records_with_errors < record_index {
438 self.summary.records_with_errors = record_index;
439 }
440 }
441
442 if self.summary.first_error.is_none() {
444 self.summary.first_error = Some(report.clone());
445 }
446 self.summary.last_error = Some(report.clone());
447 }
448
449 #[inline]
451 fn log_error(&self, report: &ErrorReport) {
452 let error_msg = if self.verbose_logging {
453 format!("{}", report.error)
454 } else {
455 format!("{}: {}", report.error.code, report.error.message)
456 };
457
458 match report.severity {
459 ErrorSeverity::Fatal | ErrorSeverity::Error => error!("{error_msg}"),
460 ErrorSeverity::Warning => warn!("{error_msg}"),
461 ErrorSeverity::Info => debug!("{error_msg}"),
462 }
463
464 if self.verbose_logging
466 && let Some(ref context) = report.error.context
467 && (context.record_index.is_some()
468 || context.field_path.is_some()
469 || context.byte_offset.is_some())
470 {
471 debug!(" Context: {context}");
472 }
473 }
474}
475
476#[inline]
478fn is_corruption_warning(error: &Error) -> bool {
479 matches!(
480 error.code,
481 ErrorCode::CBKF104_RDW_SUSPECT_ASCII | ErrorCode::CBKC301_INVALID_EBCDIC_BYTE
482 )
483}
484
485impl fmt::Display for ErrorSeverity {
486 #[inline]
487 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
488 match self {
489 ErrorSeverity::Info => write!(f, "INFO"),
490 ErrorSeverity::Warning => write!(f, "WARN"),
491 ErrorSeverity::Error => write!(f, "ERROR"),
492 ErrorSeverity::Fatal => write!(f, "FATAL"),
493 }
494 }
495}
496
497impl ErrorSummary {
498 #[must_use]
500 #[inline]
501 pub fn has_errors(&self) -> bool {
502 self.error_counts.get(&ErrorSeverity::Error).unwrap_or(&0) > &0
503 || self.error_counts.get(&ErrorSeverity::Fatal).unwrap_or(&0) > &0
504 }
505
506 #[must_use]
508 #[inline]
509 pub fn has_warnings(&self) -> bool {
510 self.error_counts.get(&ErrorSeverity::Warning).unwrap_or(&0) > &0
511 }
512
513 #[must_use]
515 #[inline]
516 pub fn error_count(&self) -> u64 {
517 self.error_counts.get(&ErrorSeverity::Error).unwrap_or(&0)
518 + self.error_counts.get(&ErrorSeverity::Fatal).unwrap_or(&0)
519 }
520
521 #[must_use]
523 #[inline]
524 pub fn warning_count(&self) -> u64 {
525 *self.error_counts.get(&ErrorSeverity::Warning).unwrap_or(&0)
526 }
527}
528
529#[cfg(test)]
530#[allow(clippy::expect_used)]
531#[allow(clippy::unwrap_used)]
532mod tests {
533 use super::*;
534 use copybook_error::ErrorCode;
535
536 #[test]
537 fn test_error_reporter_strict_mode() {
538 let mut reporter = ErrorReporter::new(ErrorMode::Strict, None);
539
540 let error = Error::new(ErrorCode::CBKD401_COMP3_INVALID_NIBBLE, "Invalid nibble")
541 .with_record(1)
542 .with_field("CUSTOMER.ID")
543 .with_offset(42);
544
545 let result = reporter.report_error(error);
547 assert!(result.is_err());
548 assert!(reporter.has_errors());
549 assert_eq!(reporter.error_count(), 1);
550 }
551
552 #[test]
553 fn test_error_reporter_lenient_mode() {
554 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
555
556 let error = Error::new(ErrorCode::CBKD401_COMP3_INVALID_NIBBLE, "Invalid nibble")
557 .with_record(1)
558 .with_field("CUSTOMER.ID")
559 .with_offset(42);
560
561 let result = reporter.report_error(error);
563 assert!(result.is_ok());
564 assert!(reporter.has_errors());
565 assert_eq!(reporter.error_count(), 1);
566 }
567
568 #[test]
569 fn test_max_errors_limit() {
570 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, Some(1));
571
572 let error1 = Error::new(ErrorCode::CBKD401_COMP3_INVALID_NIBBLE, "Error 1");
574 assert!(reporter.report_error(error1).is_ok());
575
576 let error2 = Error::new(ErrorCode::CBKD411_ZONED_BAD_SIGN, "Error 2");
578 assert!(reporter.report_error(error2).is_err());
579 }
580
581 #[test]
582 fn test_warning_reporting() {
583 let mut reporter = ErrorReporter::new(ErrorMode::Strict, None);
584
585 let warning = Error::new(ErrorCode::CBKD412_ZONED_BLANK_IS_ZERO, "Blank field");
586 reporter.report_warning(warning);
587
588 assert!(!reporter.has_errors());
589 assert!(reporter.has_warnings());
590 assert_eq!(reporter.warning_count(), 1);
591 }
592
593 #[test]
594 fn test_corruption_detection() {
595 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
596
597 let corruption_error = Error::new(
598 ErrorCode::CBKF104_RDW_SUSPECT_ASCII,
599 "ASCII corruption detected",
600 );
601 reporter.report_warning(corruption_error);
602
603 assert_eq!(reporter.summary().corruption_warnings, 1);
604 }
605
606 #[test]
607 fn test_error_summary_generation() {
608 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
609
610 reporter.start_record(1);
611 let error =
612 Error::new(ErrorCode::CBKD401_COMP3_INVALID_NIBBLE, "Test error").with_record(1);
613 let _ = reporter.report_error(error);
614
615 let report = reporter.generate_report();
616 assert!(report.contains("Total records processed: 1"));
617 assert!(report.contains("CBKD401_COMP3_INVALID_NIBBLE: 1"));
618 }
619
620 #[test]
623 fn test_empty_reporter_no_errors_no_warnings() {
624 let reporter = ErrorReporter::new(ErrorMode::Lenient, None);
625
626 assert!(!reporter.has_errors());
627 assert!(!reporter.has_warnings());
628 assert_eq!(reporter.error_count(), 0);
629 assert_eq!(reporter.warning_count(), 0);
630
631 let summary = reporter.summary();
632 assert!(!summary.has_errors());
633 assert!(!summary.has_warnings());
634 assert_eq!(summary.error_count(), 0);
635 assert_eq!(summary.warning_count(), 0);
636 assert!(summary.first_error.is_none());
637 assert!(summary.last_error.is_none());
638 assert_eq!(summary.corruption_warnings, 0);
639 assert_eq!(summary.total_records, 0);
640 assert_eq!(summary.records_with_errors, 0);
641 }
642
643 #[test]
644 fn test_empty_reporter_report_output() {
645 let reporter = ErrorReporter::new(ErrorMode::Strict, None);
646 let report = reporter.generate_report();
647
648 assert!(report.contains("=== Error Summary ==="));
649 assert!(report.contains("Total records processed: 0"));
650 assert!(report.contains("Records with errors: 0"));
651 assert!(!report.contains("Error counts by severity:"));
653 assert!(!report.contains("Error counts by code:"));
654 }
655
656 #[test]
657 fn test_fatal_error_stops_in_lenient_mode() {
658 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
659
660 let error = Error::new(ErrorCode::CBKP001_SYNTAX, "Unexpected token");
662 let result = reporter.report_error(error);
663 assert!(result.is_err());
664 assert!(reporter.has_errors());
665 }
666
667 #[test]
668 fn test_multiple_errors_summary_counts() {
669 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
670
671 let errors = [
672 Error::new(ErrorCode::CBKD401_COMP3_INVALID_NIBBLE, "nibble 1"),
673 Error::new(ErrorCode::CBKD401_COMP3_INVALID_NIBBLE, "nibble 2"),
674 Error::new(ErrorCode::CBKD411_ZONED_BAD_SIGN, "bad sign"),
675 ];
676 for e in errors {
677 let _ = reporter.report_error(e);
678 }
679
680 assert_eq!(reporter.error_count(), 3);
681 let summary = reporter.summary();
682 assert_eq!(
683 *summary
684 .error_codes
685 .get(&ErrorCode::CBKD401_COMP3_INVALID_NIBBLE)
686 .unwrap(),
687 2
688 );
689 assert_eq!(
690 *summary
691 .error_codes
692 .get(&ErrorCode::CBKD411_ZONED_BAD_SIGN)
693 .unwrap(),
694 1
695 );
696 }
697
698 #[test]
699 fn test_first_and_last_error_tracked() {
700 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
701
702 let _ = reporter.report_error(Error::new(
703 ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
704 "first error",
705 ));
706 let _ = reporter.report_error(Error::new(
707 ErrorCode::CBKD411_ZONED_BAD_SIGN,
708 "second error",
709 ));
710 let _ = reporter.report_error(Error::new(ErrorCode::CBKD410_ZONED_OVERFLOW, "third error"));
711
712 let summary = reporter.summary();
713 let first = summary.first_error.as_ref().unwrap();
714 let last = summary.last_error.as_ref().unwrap();
715 assert_eq!(first.error.code, ErrorCode::CBKD401_COMP3_INVALID_NIBBLE);
716 assert_eq!(first.error.message, "first error");
717 assert_eq!(last.error.code, ErrorCode::CBKD410_ZONED_OVERFLOW);
718 assert_eq!(last.error.message, "third error");
719 }
720
721 #[test]
722 fn test_max_errors_boundary_exact_limit() {
723 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, Some(3));
724
725 for i in 1..=3 {
727 let e = Error::new(ErrorCode::CBKD401_COMP3_INVALID_NIBBLE, format!("err {i}"));
728 assert!(reporter.report_error(e).is_ok(), "error {i} should succeed");
729 }
730
731 let e4 = Error::new(ErrorCode::CBKD401_COMP3_INVALID_NIBBLE, "err 4");
733 assert!(reporter.report_error(e4).is_err());
734 assert_eq!(reporter.error_count(), 4);
735 }
736
737 #[test]
738 fn test_max_errors_unlimited_in_lenient() {
739 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
740
741 for i in 0..100 {
742 let e = Error::new(
743 ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
744 format!("error {i}"),
745 );
746 assert!(reporter.report_error(e).is_ok());
747 }
748 assert_eq!(reporter.error_count(), 100);
749 }
750
751 #[test]
752 fn test_severity_display_formatting() {
753 assert_eq!(format!("{}", ErrorSeverity::Info), "INFO");
754 assert_eq!(format!("{}", ErrorSeverity::Warning), "WARN");
755 assert_eq!(format!("{}", ErrorSeverity::Error), "ERROR");
756 assert_eq!(format!("{}", ErrorSeverity::Fatal), "FATAL");
757 }
758
759 #[test]
760 fn test_severity_ordering() {
761 assert!(ErrorSeverity::Info < ErrorSeverity::Warning);
762 assert!(ErrorSeverity::Warning < ErrorSeverity::Error);
763 assert!(ErrorSeverity::Error < ErrorSeverity::Fatal);
764 }
765
766 #[test]
767 fn test_odo_clipped_severity_depends_on_mode() {
768 let mut lenient = ErrorReporter::new(ErrorMode::Lenient, None);
770 let e = Error::new(ErrorCode::CBKS301_ODO_CLIPPED, "ODO clipped");
771 assert!(lenient.report_error(e).is_ok());
772 assert!(!lenient.has_errors());
773 assert!(lenient.has_warnings());
774
775 let mut strict = ErrorReporter::new(ErrorMode::Strict, None);
777 let e = Error::new(ErrorCode::CBKS301_ODO_CLIPPED, "ODO clipped");
778 assert!(strict.report_error(e).is_err());
779 }
780
781 #[test]
782 fn test_mixed_errors_and_warnings_report() {
783 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
784
785 reporter.report_warning(Error::new(ErrorCode::CBKD412_ZONED_BLANK_IS_ZERO, "blank"));
786 let _ = reporter.report_error(Error::new(
787 ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
788 "bad nibble",
789 ));
790 reporter.report_warning(Error::new(
791 ErrorCode::CBKC301_INVALID_EBCDIC_BYTE,
792 "bad byte",
793 ));
794
795 let report = reporter.generate_report();
796 assert!(report.contains("Error counts by severity:"));
797 assert!(report.contains("Error counts by code:"));
798 assert_eq!(reporter.error_count(), 1);
799 assert_eq!(reporter.warning_count(), 2);
800 }
801
802 #[test]
803 fn test_corruption_detection_ebcdic_byte() {
804 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
805
806 reporter.report_warning(Error::new(
807 ErrorCode::CBKC301_INVALID_EBCDIC_BYTE,
808 "Invalid EBCDIC byte 0xFF",
809 ));
810
811 assert_eq!(reporter.summary().corruption_warnings, 1);
812 assert!(reporter.has_warnings());
813 assert!(!reporter.has_errors());
814 }
815
816 #[test]
817 fn test_multiple_corruption_warnings_counted() {
818 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
819
820 for _ in 0..5 {
821 reporter.report_warning(Error::new(
822 ErrorCode::CBKF104_RDW_SUSPECT_ASCII,
823 "ASCII suspected",
824 ));
825 }
826 reporter.report_warning(Error::new(
827 ErrorCode::CBKC301_INVALID_EBCDIC_BYTE,
828 "Bad EBCDIC",
829 ));
830
831 assert_eq!(reporter.summary().corruption_warnings, 6);
832 assert_eq!(reporter.warning_count(), 6);
833 }
834
835 #[test]
836 fn test_verbose_logging_toggle() {
837 let reporter = ErrorReporter::new(ErrorMode::Lenient, None).with_verbose_logging(false);
838 assert!(!reporter.has_errors());
840 assert_eq!(reporter.error_count(), 0);
841 }
842
843 #[test]
844 fn test_very_long_error_message() {
845 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
846 let long_msg = "x".repeat(10_000);
847 let e = Error::new(ErrorCode::CBKD401_COMP3_INVALID_NIBBLE, long_msg.clone());
848 let _ = reporter.report_error(e);
849
850 let first = reporter.summary().first_error.as_ref().unwrap();
851 assert_eq!(first.error.message.len(), 10_000);
852
853 let report = reporter.generate_report();
854 assert!(report.contains("First error:"));
855 }
856
857 #[test]
858 fn test_start_record_tracks_total_records() {
859 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
860
861 reporter.start_record(0);
862 assert_eq!(reporter.summary().total_records, 0);
863
864 reporter.start_record(42);
865 assert_eq!(reporter.summary().total_records, 42);
866
867 reporter.start_record(100);
868 assert_eq!(reporter.summary().total_records, 100);
869
870 let report = reporter.generate_report();
871 assert!(report.contains("Total records processed: 100"));
872 }
873
874 #[test]
875 fn test_error_report_contains_first_error_in_output() {
876 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
877
878 let _ = reporter.report_error(Error::new(
879 ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
880 "the very first problem",
881 ));
882 let _ = reporter.report_error(Error::new(
883 ErrorCode::CBKD411_ZONED_BAD_SIGN,
884 "second problem",
885 ));
886
887 let report = reporter.generate_report();
888 assert!(report.contains("First error:"));
889 assert!(report.contains("the very first problem"));
890 }
892
893 #[test]
894 fn test_error_summary_default_is_clean() {
895 let summary = ErrorSummary::default();
896 assert!(!summary.has_errors());
897 assert!(!summary.has_warnings());
898 assert_eq!(summary.error_count(), 0);
899 assert_eq!(summary.warning_count(), 0);
900 assert!(summary.first_error.is_none());
901 assert!(summary.last_error.is_none());
902 assert_eq!(summary.records_with_errors, 0);
903 assert_eq!(summary.total_records, 0);
904 assert_eq!(summary.corruption_warnings, 0);
905 }
906
907 #[test]
912 fn test_severity_parse_errors_are_fatal() {
913 let parse_codes = [
914 ErrorCode::CBKP001_SYNTAX,
915 ErrorCode::CBKP011_UNSUPPORTED_CLAUSE,
916 ErrorCode::CBKP021_ODO_NOT_TAIL,
917 ErrorCode::CBKP022_NESTED_ODO,
918 ErrorCode::CBKP023_ODO_REDEFINES,
919 ErrorCode::CBKP051_UNSUPPORTED_EDITED_PIC,
920 ErrorCode::CBKP101_INVALID_PIC,
921 ];
922 for code in parse_codes {
923 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
924 let err = Error::new(code, "parse error");
925 let result = reporter.report_error(err);
926 assert!(
927 result.is_err(),
928 "CBKP code {code} should be fatal even in lenient mode"
929 );
930 }
931 }
932
933 #[test]
934 fn test_severity_schema_errors_are_fatal() {
935 let schema_codes = [
936 ErrorCode::CBKS121_COUNTER_NOT_FOUND,
937 ErrorCode::CBKS141_RECORD_TOO_LARGE,
938 ErrorCode::CBKS601_RENAME_UNKNOWN_FROM,
939 ErrorCode::CBKS701_PROJECTION_INVALID_ODO,
940 ErrorCode::CBKS703_PROJECTION_FIELD_NOT_FOUND,
941 ];
942 for code in schema_codes {
943 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
944 let err = Error::new(code, "schema error");
945 let result = reporter.report_error(err);
946 assert!(result.is_err(), "CBKS code {code} should be fatal");
947 }
948 }
949
950 #[test]
951 fn test_severity_data_errors_are_error_level() {
952 let data_codes = [
953 ErrorCode::CBKD101_INVALID_FIELD_TYPE,
954 ErrorCode::CBKD301_RECORD_TOO_SHORT,
955 ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
956 ErrorCode::CBKD410_ZONED_OVERFLOW,
957 ErrorCode::CBKD411_ZONED_BAD_SIGN,
958 ErrorCode::CBKD421_EDITED_PIC_INVALID_FORMAT,
959 ErrorCode::CBKD431_FLOAT_NAN,
960 ErrorCode::CBKD432_FLOAT_INFINITY,
961 ];
962 for code in data_codes {
963 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
964 let err = Error::new(code, "data error");
965 let result = reporter.report_error(err);
966 assert!(
967 result.is_ok(),
968 "CBKD code {code} should continue in lenient mode"
969 );
970 assert!(reporter.has_errors());
971 }
972 }
973
974 #[test]
975 fn test_severity_warning_codes_are_warnings() {
976 let warning_codes = [
977 ErrorCode::CBKD412_ZONED_BLANK_IS_ZERO,
978 ErrorCode::CBKC301_INVALID_EBCDIC_BYTE,
979 ErrorCode::CBKD423_EDITED_PIC_BLANK_WHEN_ZERO,
980 ErrorCode::CBKF104_RDW_SUSPECT_ASCII,
981 ];
982 for code in warning_codes {
983 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
984 let err = Error::new(code, "warning");
985 let result = reporter.report_error(err);
986 assert!(result.is_ok(), "code {code} should continue");
987 assert!(
988 !reporter.has_errors(),
989 "code {code} should be warning not error"
990 );
991 assert!(reporter.has_warnings(), "code {code} should be a warning");
992 }
993 }
994
995 #[test]
996 fn test_severity_odo_clipped_warning_in_lenient_fatal_in_strict() {
997 for code in [
998 ErrorCode::CBKS301_ODO_CLIPPED,
999 ErrorCode::CBKS302_ODO_RAISED,
1000 ] {
1001 let mut lenient = ErrorReporter::new(ErrorMode::Lenient, None);
1003 assert!(lenient.report_error(Error::new(code, "odo")).is_ok());
1004 assert!(lenient.has_warnings());
1005 assert!(!lenient.has_errors());
1006
1007 let mut strict = ErrorReporter::new(ErrorMode::Strict, None);
1009 assert!(strict.report_error(Error::new(code, "odo")).is_err());
1010 }
1011 }
1012
1013 #[test]
1014 fn test_severity_encode_errors_are_error_level_in_lenient() {
1015 let encode_codes = [
1016 ErrorCode::CBKE501_JSON_TYPE_MISMATCH,
1017 ErrorCode::CBKE521_ARRAY_LEN_OOB,
1018 ErrorCode::CBKE530_SIGN_SEPARATE_ENCODE_ERROR,
1019 ErrorCode::CBKE531_FLOAT_ENCODE_OVERFLOW,
1020 ];
1021 for code in encode_codes {
1022 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
1023 let result = reporter.report_error(Error::new(code, "encode"));
1024 assert!(result.is_ok(), "{code} should continue in lenient");
1025 assert!(reporter.has_errors());
1026 }
1027 }
1028
1029 #[test]
1030 fn test_severity_infrastructure_error_is_fatal() {
1031 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
1032 let err = Error::new(ErrorCode::CBKI001_INVALID_STATE, "bad state");
1033 assert!(reporter.report_error(err).is_err());
1034 }
1035
1036 #[test]
1041 fn test_records_with_errors_tracking() {
1042 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
1043
1044 let _ = reporter.report_error(
1046 Error::new(ErrorCode::CBKD401_COMP3_INVALID_NIBBLE, "bad").with_record(5),
1047 );
1048 let _ = reporter
1049 .report_error(Error::new(ErrorCode::CBKD411_ZONED_BAD_SIGN, "bad").with_record(10));
1050
1051 assert_eq!(reporter.summary().records_with_errors, 10);
1052 }
1053
1054 #[test]
1055 fn test_records_with_errors_not_incremented_for_warnings() {
1056 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
1057 reporter.report_warning(
1058 Error::new(ErrorCode::CBKD412_ZONED_BLANK_IS_ZERO, "blank").with_record(5),
1059 );
1060 assert_eq!(reporter.summary().records_with_errors, 0);
1061 }
1062
1063 #[test]
1068 fn test_corruption_warning_adds_metadata() {
1069 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
1070 reporter.report_warning(Error::new(
1071 ErrorCode::CBKF104_RDW_SUSPECT_ASCII,
1072 "ASCII transfer",
1073 ));
1074 assert_eq!(reporter.summary().corruption_warnings, 1);
1075 }
1076
1077 #[test]
1078 fn test_non_corruption_warning_no_corruption_count() {
1079 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
1080 reporter.report_warning(Error::new(ErrorCode::CBKD412_ZONED_BLANK_IS_ZERO, "blanks"));
1081 assert_eq!(reporter.summary().corruption_warnings, 0);
1082 }
1083
1084 #[test]
1089 fn test_error_mode_equality() {
1090 assert_eq!(ErrorMode::Strict, ErrorMode::Strict);
1091 assert_eq!(ErrorMode::Lenient, ErrorMode::Lenient);
1092 assert_ne!(ErrorMode::Strict, ErrorMode::Lenient);
1093 }
1094
1095 #[test]
1096 fn test_error_severity_equality_and_hash() {
1097 use std::collections::HashSet;
1098 let mut set = HashSet::new();
1099 set.insert(ErrorSeverity::Info);
1100 set.insert(ErrorSeverity::Warning);
1101 set.insert(ErrorSeverity::Error);
1102 set.insert(ErrorSeverity::Fatal);
1103 set.insert(ErrorSeverity::Info); assert_eq!(set.len(), 4);
1105 }
1106
1107 #[test]
1112 fn test_generate_report_includes_corruption_section() {
1113 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
1114 reporter.report_warning(Error::new(ErrorCode::CBKF104_RDW_SUSPECT_ASCII, "ASCII"));
1115 reporter.report_warning(Error::new(ErrorCode::CBKC301_INVALID_EBCDIC_BYTE, "EBCDIC"));
1116
1117 let report = reporter.generate_report();
1118 assert!(report.contains("Transfer corruption warnings: 2"));
1119 }
1120
1121 #[test]
1122 fn test_generate_report_no_corruption_section_when_zero() {
1123 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
1124 reporter.report_warning(Error::new(ErrorCode::CBKD412_ZONED_BLANK_IS_ZERO, "blank"));
1125
1126 let report = reporter.generate_report();
1127 assert!(!report.contains("Transfer corruption warnings"));
1128 }
1129
1130 #[test]
1131 fn test_error_summary_has_errors_with_fatal_only() {
1132 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
1133 let _ = reporter.report_error(Error::new(ErrorCode::CBKP001_SYNTAX, "syntax"));
1135 assert!(reporter.summary().has_errors());
1136 assert_eq!(reporter.summary().error_count(), 1);
1137 }
1138
1139 #[test]
1144 fn test_error_code_json_roundtrip_in_summary() {
1145 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, None);
1146 let _ = reporter.report_error(Error::new(
1147 ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
1148 "nibble",
1149 ));
1150
1151 for (code, count) in &reporter.summary().error_codes {
1153 let json = serde_json::to_string(code).unwrap();
1154 let rt: ErrorCode = serde_json::from_str(&json).unwrap();
1155 assert_eq!(*code, rt);
1156 assert_eq!(*count, 1);
1157 }
1158 }
1159
1160 #[test]
1161 fn test_error_code_json_serialization_format() {
1162 let test_cases = [
1164 (ErrorCode::CBKP001_SYNTAX, r#""CBKP001_SYNTAX""#),
1165 (
1166 ErrorCode::CBKS121_COUNTER_NOT_FOUND,
1167 r#""CBKS121_COUNTER_NOT_FOUND""#,
1168 ),
1169 (
1170 ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
1171 r#""CBKD401_COMP3_INVALID_NIBBLE""#,
1172 ),
1173 (
1174 ErrorCode::CBKE501_JSON_TYPE_MISMATCH,
1175 r#""CBKE501_JSON_TYPE_MISMATCH""#,
1176 ),
1177 (
1178 ErrorCode::CBKR211_RDW_RESERVED_NONZERO,
1179 r#""CBKR211_RDW_RESERVED_NONZERO""#,
1180 ),
1181 (
1182 ErrorCode::CBKF102_RECORD_LENGTH_INVALID,
1183 r#""CBKF102_RECORD_LENGTH_INVALID""#,
1184 ),
1185 ];
1186 for (code, expected_json) in test_cases {
1187 let json = serde_json::to_string(&code).unwrap();
1188 assert_eq!(json, expected_json, "serialization mismatch for {code}");
1189 }
1190 }
1191
1192 #[test]
1197 fn test_max_errors_zero_stops_immediately() {
1198 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, Some(0));
1199 let err = Error::new(ErrorCode::CBKD401_COMP3_INVALID_NIBBLE, "first");
1200 assert!(reporter.report_error(err).is_err());
1202 }
1203
1204 #[test]
1205 fn test_strict_mode_stops_on_every_data_error() {
1206 let data_codes = [
1207 ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
1208 ErrorCode::CBKD411_ZONED_BAD_SIGN,
1209 ErrorCode::CBKD410_ZONED_OVERFLOW,
1210 ErrorCode::CBKE501_JSON_TYPE_MISMATCH,
1211 ];
1212 for code in data_codes {
1213 let mut reporter = ErrorReporter::new(ErrorMode::Strict, None);
1214 let result = reporter.report_error(Error::new(code, "data error"));
1215 assert!(result.is_err(), "strict mode should stop on {code}");
1216 }
1217 }
1218
1219 #[test]
1220 fn test_warning_does_not_count_as_error() {
1221 let mut reporter = ErrorReporter::new(ErrorMode::Lenient, Some(1));
1222 for _ in 0..10 {
1224 reporter.report_warning(Error::new(ErrorCode::CBKD412_ZONED_BLANK_IS_ZERO, "blank"));
1225 }
1226 assert_eq!(reporter.error_count(), 0);
1227 assert_eq!(reporter.warning_count(), 10);
1228 let err = Error::new(ErrorCode::CBKD401_COMP3_INVALID_NIBBLE, "first real error");
1230 assert!(reporter.report_error(err).is_ok());
1231 }
1232}