1use std::fmt;
10use thiserror::Error;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17#[repr(u16)]
18pub enum ErrorCode {
19 InvalidAccession = 1001,
22 UnknownVariantType = 1002,
24 InvalidPosition = 1003,
26 InvalidEdit = 1004,
28 UnexpectedEnd = 1005,
30 UnexpectedChar = 1006,
32 InvalidBase = 1007,
34 InvalidAminoAcid = 1008,
36
37 ReferenceNotFound = 2001,
40 SequenceNotFound = 2002,
42 ChromosomeNotFound = 2003,
44
45 PositionOutOfBounds = 3001,
48 ReferenceMismatch = 3002,
50 InvalidRange = 3003,
52 ExonIntronBoundary = 3004,
54 UtrCdsBoundary = 3005,
56
57 IntronicVariant = 4001,
60 UnsupportedVariant = 4002,
62
63 ConversionFailed = 5001,
66 NoOverlappingTranscript = 5002,
68
69 IoError = 9001,
72 JsonError = 9002,
74}
75
76impl ErrorCode {
77 pub fn as_str(&self) -> String {
79 format!("E{:04}", *self as u16)
80 }
81
82 pub fn description(&self) -> &'static str {
84 match self {
85 ErrorCode::InvalidAccession => "invalid accession format",
86 ErrorCode::UnknownVariantType => "unknown variant type prefix",
87 ErrorCode::InvalidPosition => "invalid position format",
88 ErrorCode::InvalidEdit => "invalid edit format",
89 ErrorCode::UnexpectedEnd => "unexpected end of input",
90 ErrorCode::UnexpectedChar => "unexpected character",
91 ErrorCode::InvalidBase => "invalid nucleotide base",
92 ErrorCode::InvalidAminoAcid => "invalid amino acid",
93 ErrorCode::ReferenceNotFound => "reference not found",
94 ErrorCode::SequenceNotFound => "sequence not available",
95 ErrorCode::ChromosomeNotFound => "chromosome not found",
96 ErrorCode::PositionOutOfBounds => "position out of bounds",
97 ErrorCode::ReferenceMismatch => "reference sequence mismatch",
98 ErrorCode::InvalidRange => "invalid coordinate range",
99 ErrorCode::ExonIntronBoundary => "variant crosses exon-intron boundary",
100 ErrorCode::UtrCdsBoundary => "variant crosses UTR-CDS boundary",
101 ErrorCode::IntronicVariant => "intronic variant not supported",
102 ErrorCode::UnsupportedVariant => "unsupported variant type",
103 ErrorCode::ConversionFailed => "coordinate conversion failed",
104 ErrorCode::NoOverlappingTranscript => "no overlapping transcript",
105 ErrorCode::IoError => "file I/O error",
106 ErrorCode::JsonError => "JSON parsing error",
107 }
108 }
109}
110
111impl fmt::Display for ErrorCode {
112 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113 write!(f, "{}", self.as_str())
114 }
115}
116
117#[derive(Debug, Clone, PartialEq, Eq, Default)]
119pub struct SourceSpan {
120 pub start: usize,
122 pub end: usize,
124}
125
126impl SourceSpan {
127 pub fn new(start: usize, end: usize) -> Self {
129 Self { start, end }
130 }
131
132 pub fn point(pos: usize) -> Self {
134 Self {
135 start: pos,
136 end: pos + 1,
137 }
138 }
139
140 pub fn highlight(&self, source: &str) -> String {
148 if source.is_empty() {
149 return String::new();
150 }
151
152 let safe_start = self.start.min(source.len());
153 let safe_end = self.end.min(source.len()).max(safe_start);
154
155 let mut pointer = String::with_capacity(source.len() + 4);
157 for _ in 0..safe_start {
158 pointer.push(' ');
159 }
160 if safe_start < safe_end {
161 pointer.push('^');
162 for _ in (safe_start + 1)..safe_end {
163 pointer.push('~');
164 }
165 } else {
166 pointer.push('^');
167 }
168
169 format!("{}\n{}", source, pointer)
170 }
171}
172
173#[derive(Debug, Clone, PartialEq, Eq, Default)]
175pub struct Diagnostic {
176 pub code: Option<ErrorCode>,
178 pub span: Option<SourceSpan>,
180 pub source: Option<String>,
182 pub hint: Option<String>,
184 pub suggestion: Option<String>,
186}
187
188impl Diagnostic {
189 pub fn new() -> Self {
191 Self::default()
192 }
193
194 pub fn with_code(mut self, code: ErrorCode) -> Self {
196 self.code = Some(code);
197 self
198 }
199
200 pub fn with_span(mut self, span: SourceSpan) -> Self {
202 self.span = Some(span);
203 self
204 }
205
206 pub fn with_source(mut self, source: impl Into<String>) -> Self {
208 self.source = Some(source.into());
209 self
210 }
211
212 pub fn with_hint(mut self, hint: impl Into<String>) -> Self {
214 self.hint = Some(hint.into());
215 self
216 }
217
218 pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
220 self.suggestion = Some(suggestion.into());
221 self
222 }
223
224 pub fn format(&self, primary_message: &str) -> String {
226 let mut result = String::new();
227
228 if let Some(code) = &self.code {
230 result.push_str(&format!("[{}] ", code));
231 }
232
233 result.push_str(primary_message);
235
236 if let (Some(span), Some(source)) = (&self.span, &self.source) {
238 result.push_str("\n\n");
239 result.push_str(&span.highlight(source));
240 }
241
242 if let Some(hint) = &self.hint {
244 result.push_str("\n\nHint: ");
245 result.push_str(hint);
246 }
247
248 if let Some(suggestion) = &self.suggestion {
250 result.push_str("\n\nDid you mean: ");
251 result.push_str(suggestion);
252 }
253
254 result
255 }
256}
257
258#[derive(Error, Debug, Clone, PartialEq)]
260pub enum FerroError {
261 #[error("Parse error at position {pos}: {msg}")]
263 Parse {
264 pos: usize,
265 msg: String,
266 diagnostic: Option<Box<Diagnostic>>,
268 },
269
270 #[error("Reference not found: {id}")]
272 ReferenceNotFound { id: String },
273
274 #[error("Variant spans exon-intron boundary at exon {exon}: {variant}")]
276 ExonIntronBoundary { exon: u32, variant: String },
277
278 #[error("Variant spans UTR-CDS boundary: {variant}")]
280 UtrCdsBoundary { variant: String },
281
282 #[error("Invalid coordinates: {msg}")]
284 InvalidCoordinates { msg: String },
285
286 #[error("Unsupported variant type: {variant_type}")]
288 UnsupportedVariant { variant_type: String },
289
290 #[error("Intronic variant normalization not supported: {variant}")]
292 IntronicVariant { variant: String },
293
294 #[error("Genomic reference not available for {contig}:{start}-{end}")]
296 GenomicReferenceNotAvailable {
297 contig: String,
298 start: u64,
299 end: u64,
300 },
301
302 #[error("Protein reference not available for {accession}:{start}-{end}")]
304 ProteinReferenceNotAvailable {
305 accession: String,
306 start: u64,
307 end: u64,
308 },
309
310 #[error("Amino acid mismatch at position {position}: expected {expected}, found {found} in {accession}")]
312 AminoAcidMismatch {
313 accession: String,
314 position: u64,
315 expected: String,
316 found: String,
317 },
318
319 #[error("Reference mismatch at {location}: expected {expected}, found {found}")]
321 ReferenceMismatch {
322 location: String,
323 expected: String,
324 found: String,
325 },
326
327 #[error("Coordinate conversion error: {msg}")]
329 ConversionError { msg: String },
330
331 #[error("IO error: {msg}")]
333 Io { msg: String },
334
335 #[error("JSON error: {msg}")]
337 Json { msg: String },
338}
339
340impl FerroError {
341 pub fn parse_with_diagnostic(
343 pos: usize,
344 msg: impl Into<String>,
345 diagnostic: Diagnostic,
346 ) -> Self {
347 FerroError::Parse {
348 pos,
349 msg: msg.into(),
350 diagnostic: Some(Box::new(diagnostic)),
351 }
352 }
353
354 pub fn parse(pos: usize, msg: impl Into<String>) -> Self {
356 FerroError::Parse {
357 pos,
358 msg: msg.into(),
359 diagnostic: None,
360 }
361 }
362
363 pub fn code(&self) -> Option<ErrorCode> {
365 match self {
366 FerroError::Parse {
367 diagnostic: Some(d),
368 ..
369 } => d.code,
370 FerroError::ReferenceNotFound { .. } => Some(ErrorCode::ReferenceNotFound),
371 FerroError::ExonIntronBoundary { .. } => Some(ErrorCode::ExonIntronBoundary),
372 FerroError::UtrCdsBoundary { .. } => Some(ErrorCode::UtrCdsBoundary),
373 FerroError::InvalidCoordinates { .. } => Some(ErrorCode::InvalidRange),
374 FerroError::UnsupportedVariant { .. } => Some(ErrorCode::UnsupportedVariant),
375 FerroError::IntronicVariant { .. } => Some(ErrorCode::IntronicVariant),
376 FerroError::ReferenceMismatch { .. } => Some(ErrorCode::ReferenceMismatch),
377 FerroError::ConversionError { .. } => Some(ErrorCode::ConversionFailed),
378 FerroError::Io { .. } => Some(ErrorCode::IoError),
379 FerroError::Json { .. } => Some(ErrorCode::JsonError),
380 _ => None,
381 }
382 }
383
384 pub fn detailed_message(&self) -> String {
386 match self {
387 FerroError::Parse {
388 pos,
389 msg,
390 diagnostic: Some(d),
391 } => d.format(&format!("Parse error at position {}: {}", pos, msg)),
392 _ => self.to_string(),
393 }
394 }
395}
396
397pub fn suggest_variant_type(found: &str) -> Option<&'static str> {
399 let found_lower = found.to_lowercase();
400 let prefixes = [
401 ("c", "c."),
402 ("g", "g."),
403 ("p", "p."),
404 ("n", "n."),
405 ("r", "r."),
406 ("m", "m."),
407 ("c:", "c."),
408 ("g:", "g."),
409 ("p:", "p."),
410 ];
411
412 for (pattern, suggestion) in prefixes {
413 if found_lower.starts_with(pattern) {
414 return Some(suggestion);
415 }
416 }
417 None
418}
419
420pub fn suggest_amino_acid(found: &str) -> Option<&'static str> {
422 let suggestions = [
423 ("ale", "Ala"),
424 ("arg", "Arg"),
425 ("asg", "Asn"),
426 ("asp", "Asp"),
427 ("cis", "Cys"),
428 ("cys", "Cys"),
429 ("gln", "Gln"),
430 ("glu", "Glu"),
431 ("gly", "Gly"),
432 ("his", "His"),
433 ("ile", "Ile"),
434 ("leu", "Leu"),
435 ("lys", "Lys"),
436 ("met", "Met"),
437 ("phe", "Phe"),
438 ("pro", "Pro"),
439 ("sec", "Sec"),
440 ("sel", "Sec"),
441 ("ser", "Ser"),
442 ("thr", "Thr"),
443 ("trp", "Trp"),
444 ("tyr", "Tyr"),
445 ("val", "Val"),
446 ("ter", "Ter"),
447 ("stop", "Ter"),
448 ("end", "Ter"),
449 ("xaa", "Xaa"),
450 ("unk", "Xaa"),
451 ];
452
453 let found_lower = found.to_lowercase();
454 for (pattern, suggestion) in suggestions {
455 if found_lower.starts_with(pattern) || pattern.starts_with(&found_lower) {
456 return Some(suggestion);
457 }
458 }
459 None
460}
461
462impl From<std::io::Error> for FerroError {
463 fn from(err: std::io::Error) -> Self {
464 FerroError::Io {
465 msg: err.to_string(),
466 }
467 }
468}
469
470impl From<serde_json::Error> for FerroError {
471 fn from(err: serde_json::Error) -> Self {
472 FerroError::Json {
473 msg: err.to_string(),
474 }
475 }
476}
477
478#[cfg(test)]
479mod tests {
480 use super::*;
481
482 #[test]
484 fn test_error_code_as_str() {
485 assert_eq!(ErrorCode::InvalidAccession.as_str(), "E1001");
486 assert_eq!(ErrorCode::UnknownVariantType.as_str(), "E1002");
487 assert_eq!(ErrorCode::ReferenceNotFound.as_str(), "E2001");
488 assert_eq!(ErrorCode::PositionOutOfBounds.as_str(), "E3001");
489 assert_eq!(ErrorCode::IntronicVariant.as_str(), "E4001");
490 assert_eq!(ErrorCode::ConversionFailed.as_str(), "E5001");
491 assert_eq!(ErrorCode::IoError.as_str(), "E9001");
492 }
493
494 #[test]
495 fn test_error_code_description() {
496 assert_eq!(
497 ErrorCode::InvalidAccession.description(),
498 "invalid accession format"
499 );
500 assert_eq!(
501 ErrorCode::InvalidPosition.description(),
502 "invalid position format"
503 );
504 assert_eq!(ErrorCode::InvalidEdit.description(), "invalid edit format");
505 assert_eq!(
506 ErrorCode::UnexpectedEnd.description(),
507 "unexpected end of input"
508 );
509 assert_eq!(
510 ErrorCode::UnexpectedChar.description(),
511 "unexpected character"
512 );
513 assert_eq!(
514 ErrorCode::InvalidBase.description(),
515 "invalid nucleotide base"
516 );
517 assert_eq!(
518 ErrorCode::InvalidAminoAcid.description(),
519 "invalid amino acid"
520 );
521 assert_eq!(
522 ErrorCode::SequenceNotFound.description(),
523 "sequence not available"
524 );
525 assert_eq!(
526 ErrorCode::ChromosomeNotFound.description(),
527 "chromosome not found"
528 );
529 assert_eq!(
530 ErrorCode::ReferenceMismatch.description(),
531 "reference sequence mismatch"
532 );
533 assert_eq!(
534 ErrorCode::InvalidRange.description(),
535 "invalid coordinate range"
536 );
537 assert_eq!(
538 ErrorCode::ExonIntronBoundary.description(),
539 "variant crosses exon-intron boundary"
540 );
541 assert_eq!(
542 ErrorCode::UtrCdsBoundary.description(),
543 "variant crosses UTR-CDS boundary"
544 );
545 assert_eq!(
546 ErrorCode::UnsupportedVariant.description(),
547 "unsupported variant type"
548 );
549 assert_eq!(
550 ErrorCode::NoOverlappingTranscript.description(),
551 "no overlapping transcript"
552 );
553 assert_eq!(ErrorCode::JsonError.description(), "JSON parsing error");
554 }
555
556 #[test]
557 fn test_error_code_display() {
558 assert_eq!(format!("{}", ErrorCode::InvalidAccession), "E1001");
559 assert_eq!(format!("{}", ErrorCode::IoError), "E9001");
560 }
561
562 #[test]
564 fn test_source_span_new() {
565 let span = SourceSpan::new(5, 10);
566 assert_eq!(span.start, 5);
567 assert_eq!(span.end, 10);
568 }
569
570 #[test]
571 fn test_source_span_point() {
572 let span = SourceSpan::point(7);
573 assert_eq!(span.start, 7);
574 assert_eq!(span.end, 8);
575 }
576
577 #[test]
578 fn test_source_span_highlight() {
579 let span = SourceSpan::new(12, 15);
580 let result = span.highlight("NM_000088.3:c.459A>G");
581 assert!(result.contains("NM_000088.3:c.459A>G"));
582 assert!(result.contains("^"));
583 }
584
585 #[test]
586 fn test_source_span_highlight_empty_source() {
587 let span = SourceSpan::new(0, 5);
588 let result = span.highlight("");
589 assert_eq!(result, "");
590 }
591
592 #[test]
593 fn test_source_span_highlight_single_position() {
594 let span = SourceSpan::new(0, 1);
595 let result = span.highlight("ABC");
596 assert!(result.contains("ABC"));
597 assert!(result.contains("^"));
598 }
599
600 #[test]
601 fn test_source_span_highlight_out_of_bounds() {
602 let span = SourceSpan::new(100, 200);
603 let result = span.highlight("short");
604 assert!(result.contains("short"));
605 }
606
607 #[test]
609 fn test_diagnostic_new() {
610 let diag = Diagnostic::new();
611 assert!(diag.code.is_none());
612 assert!(diag.span.is_none());
613 assert!(diag.source.is_none());
614 assert!(diag.hint.is_none());
615 assert!(diag.suggestion.is_none());
616 }
617
618 #[test]
619 fn test_diagnostic_builder() {
620 let diag = Diagnostic::new()
621 .with_code(ErrorCode::InvalidAccession)
622 .with_span(SourceSpan::new(0, 5))
623 .with_source("NM_invalid:c.100A>G")
624 .with_hint("Check accession format")
625 .with_suggestion("NM_000088.3:c.100A>G");
626
627 assert_eq!(diag.code, Some(ErrorCode::InvalidAccession));
628 assert!(diag.span.is_some());
629 assert_eq!(diag.source, Some("NM_invalid:c.100A>G".to_string()));
630 assert_eq!(diag.hint, Some("Check accession format".to_string()));
631 assert_eq!(diag.suggestion, Some("NM_000088.3:c.100A>G".to_string()));
632 }
633
634 #[test]
635 fn test_diagnostic_format_simple() {
636 let diag = Diagnostic::new();
637 let result = diag.format("Simple error");
638 assert_eq!(result, "Simple error");
639 }
640
641 #[test]
642 fn test_diagnostic_format_with_code() {
643 let diag = Diagnostic::new().with_code(ErrorCode::InvalidAccession);
644 let result = diag.format("Invalid format");
645 assert!(result.starts_with("[E1001]"));
646 }
647
648 #[test]
649 fn test_diagnostic_format_full() {
650 let diag = Diagnostic::new()
651 .with_code(ErrorCode::InvalidAccession)
652 .with_span(SourceSpan::new(0, 5))
653 .with_source("error_input")
654 .with_hint("Try this")
655 .with_suggestion("correct_input");
656
657 let result = diag.format("Test message");
658 assert!(result.contains("[E1001]"));
659 assert!(result.contains("Test message"));
660 assert!(result.contains("Hint: Try this"));
661 assert!(result.contains("Did you mean: correct_input"));
662 }
663
664 #[test]
666 fn test_ferro_error_parse() {
667 let err = FerroError::parse(10, "unexpected token");
668 assert!(matches!(err, FerroError::Parse { pos: 10, .. }));
669 }
670
671 #[test]
672 fn test_ferro_error_parse_with_diagnostic() {
673 let diag = Diagnostic::new().with_code(ErrorCode::InvalidEdit);
674 let err = FerroError::parse_with_diagnostic(5, "bad edit", diag);
675 assert!(matches!(err, FerroError::Parse { pos: 5, .. }));
676 }
677
678 #[test]
679 fn test_ferro_error_code() {
680 let err = FerroError::ReferenceNotFound {
681 id: "NM_123".to_string(),
682 };
683 assert_eq!(err.code(), Some(ErrorCode::ReferenceNotFound));
684
685 let err = FerroError::ExonIntronBoundary {
686 exon: 3,
687 variant: "c.100del".to_string(),
688 };
689 assert_eq!(err.code(), Some(ErrorCode::ExonIntronBoundary));
690
691 let err = FerroError::UtrCdsBoundary {
692 variant: "c.1-100del".to_string(),
693 };
694 assert_eq!(err.code(), Some(ErrorCode::UtrCdsBoundary));
695
696 let err = FerroError::InvalidCoordinates {
697 msg: "bad coords".to_string(),
698 };
699 assert_eq!(err.code(), Some(ErrorCode::InvalidRange));
700
701 let err = FerroError::UnsupportedVariant {
702 variant_type: "xyz".to_string(),
703 };
704 assert_eq!(err.code(), Some(ErrorCode::UnsupportedVariant));
705
706 let err = FerroError::IntronicVariant {
707 variant: "c.100+5del".to_string(),
708 };
709 assert_eq!(err.code(), Some(ErrorCode::IntronicVariant));
710
711 let err = FerroError::ReferenceMismatch {
712 location: "c.100".to_string(),
713 expected: "A".to_string(),
714 found: "G".to_string(),
715 };
716 assert_eq!(err.code(), Some(ErrorCode::ReferenceMismatch));
717
718 let err = FerroError::ConversionError {
719 msg: "conversion failed".to_string(),
720 };
721 assert_eq!(err.code(), Some(ErrorCode::ConversionFailed));
722
723 let err = FerroError::Io {
724 msg: "file error".to_string(),
725 };
726 assert_eq!(err.code(), Some(ErrorCode::IoError));
727
728 let err = FerroError::Json {
729 msg: "json error".to_string(),
730 };
731 assert_eq!(err.code(), Some(ErrorCode::JsonError));
732 }
733
734 #[test]
735 fn test_ferro_error_code_from_diagnostic() {
736 let diag = Diagnostic::new().with_code(ErrorCode::InvalidPosition);
737 let err = FerroError::parse_with_diagnostic(0, "test", diag);
738 assert_eq!(err.code(), Some(ErrorCode::InvalidPosition));
739 }
740
741 #[test]
742 fn test_ferro_error_code_none() {
743 let err = FerroError::parse(0, "simple error");
744 assert_eq!(err.code(), None);
745 }
746
747 #[test]
748 fn test_ferro_error_detailed_message() {
749 let diag = Diagnostic::new()
750 .with_code(ErrorCode::InvalidAccession)
751 .with_source("bad_input")
752 .with_span(SourceSpan::new(0, 3));
753 let err = FerroError::parse_with_diagnostic(0, "invalid", diag);
754 let msg = err.detailed_message();
755 assert!(msg.contains("[E1001]"));
756 assert!(msg.contains("bad_input"));
757 }
758
759 #[test]
760 fn test_ferro_error_detailed_message_simple() {
761 let err = FerroError::ReferenceNotFound {
762 id: "NM_test".to_string(),
763 };
764 let msg = err.detailed_message();
765 assert!(msg.contains("NM_test"));
766 }
767
768 #[test]
769 fn test_ferro_error_display() {
770 let err = FerroError::parse(10, "unexpected");
771 let display = format!("{}", err);
772 assert!(display.contains("10"));
773 assert!(display.contains("unexpected"));
774
775 let err = FerroError::ReferenceNotFound {
776 id: "test_id".to_string(),
777 };
778 let display = format!("{}", err);
779 assert!(display.contains("test_id"));
780 }
781
782 #[test]
784 fn test_suggest_variant_type() {
785 assert_eq!(suggest_variant_type("c:100A>G"), Some("c."));
786 assert_eq!(suggest_variant_type("g:123A>G"), Some("g."));
787 assert_eq!(suggest_variant_type("p:Val600Glu"), Some("p."));
788 assert_eq!(suggest_variant_type("c100A>G"), Some("c."));
789 assert_eq!(suggest_variant_type("C.100A>G"), Some("c."));
790 assert_eq!(suggest_variant_type("xyz"), None);
791 }
792
793 #[test]
794 fn test_suggest_amino_acid() {
795 assert_eq!(suggest_amino_acid("Ale"), Some("Ala"));
796 assert_eq!(suggest_amino_acid("arg"), Some("Arg"));
797 assert_eq!(suggest_amino_acid("asg"), Some("Asn"));
798 assert_eq!(suggest_amino_acid("cis"), Some("Cys"));
799 assert_eq!(suggest_amino_acid("stop"), Some("Ter"));
800 assert_eq!(suggest_amino_acid("end"), Some("Ter"));
801 assert_eq!(suggest_amino_acid("unk"), Some("Xaa"));
802 assert_eq!(suggest_amino_acid("sel"), Some("Sec"));
803 assert_eq!(suggest_amino_acid("xyz"), None);
804 }
805
806 #[test]
808 fn test_from_io_error() {
809 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
810 let ferro_err: FerroError = io_err.into();
811 assert!(matches!(ferro_err, FerroError::Io { .. }));
812 assert!(ferro_err.to_string().contains("not found"));
813 }
814
815 #[test]
816 fn test_ferro_error_equality() {
817 let err1 = FerroError::parse(10, "test");
818 let err2 = FerroError::parse(10, "test");
819 assert_eq!(err1, err2);
820
821 let err3 = FerroError::parse(11, "test");
822 assert_ne!(err1, err3);
823 }
824
825 #[test]
826 fn test_source_span_equality() {
827 let span1 = SourceSpan::new(5, 10);
828 let span2 = SourceSpan::new(5, 10);
829 assert_eq!(span1, span2);
830
831 let span3 = SourceSpan::new(5, 11);
832 assert_ne!(span1, span3);
833 }
834
835 #[test]
836 fn test_diagnostic_equality() {
837 let diag1 = Diagnostic::new().with_code(ErrorCode::InvalidAccession);
838 let diag2 = Diagnostic::new().with_code(ErrorCode::InvalidAccession);
839 assert_eq!(diag1, diag2);
840
841 let diag3 = Diagnostic::new().with_code(ErrorCode::InvalidEdit);
842 assert_ne!(diag1, diag3);
843 }
844
845 #[test]
846 fn test_error_code_hash() {
847 use std::collections::HashSet;
848 let mut set = HashSet::new();
849 set.insert(ErrorCode::InvalidAccession);
850 set.insert(ErrorCode::InvalidEdit);
851 assert!(set.contains(&ErrorCode::InvalidAccession));
852 assert!(set.contains(&ErrorCode::InvalidEdit));
853 assert!(!set.contains(&ErrorCode::InvalidPosition));
854 }
855}