1use crate::core::address::Address;
30use crate::core::distribution::*;
31use std::fmt;
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
40pub enum ErrorCode {
41 InvalidMean = 100,
43 InvalidVariance = 101,
44 InvalidProbability = 102,
45 InvalidRange = 103,
46 InvalidShape = 104,
47 InvalidRate = 105,
48 InvalidCount = 106,
49
50 AddressConflict = 301,
52 UnexpectedModelStructure = 302,
53
54 TraceAddressNotFound = 500,
56
57 TypeMismatch = 600,
59}
60
61impl ErrorCode {
62 pub fn description(&self) -> &'static str {
64 match self {
65 ErrorCode::InvalidMean => "Distribution mean parameter is invalid",
66 ErrorCode::InvalidVariance => "Distribution variance/scale parameter is invalid",
67 ErrorCode::InvalidProbability => "Probability parameter is invalid",
68 ErrorCode::InvalidRange => "Parameter range is invalid",
69 ErrorCode::InvalidShape => "Shape parameter is invalid",
70 ErrorCode::InvalidRate => "Rate parameter is invalid",
71 ErrorCode::InvalidCount => "Count parameter is invalid",
72
73 ErrorCode::AddressConflict => "Address already exists in trace",
74 ErrorCode::UnexpectedModelStructure => "Model structure is unexpected",
75
76 ErrorCode::TraceAddressNotFound => "Address not found in trace",
77
78 ErrorCode::TypeMismatch => "Type mismatch in trace value",
79 }
80 }
81
82 pub fn category(&self) -> ErrorCategory {
84 match (*self as u32) / 100 {
85 1 => ErrorCategory::DistributionValidation,
86 3 => ErrorCategory::ModelExecution,
87 5 => ErrorCategory::TraceManipulation,
88 6 => ErrorCategory::TypeSystem,
89 _ => ErrorCategory::Unknown,
90 }
91 }
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub enum ErrorCategory {
101 DistributionValidation,
102 ModelExecution,
103 TraceManipulation,
104 TypeSystem,
105 Unknown,
106}
107
108#[derive(Debug, Clone)]
110pub struct ErrorContext {
111 pub source_location: Option<(String, u32)>,
113 pub context: Vec<(String, String)>,
115 pub cause: Option<Box<FugueError>>,
117}
118
119impl ErrorContext {
120 pub fn new() -> Self {
122 Self {
123 source_location: None,
124 context: Vec::new(),
125 cause: None,
126 }
127 }
128
129 pub fn with_context(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
131 self.context.push((key.into(), value.into()));
132 self
133 }
134
135 pub fn with_source_location(mut self, file: impl Into<String>, line: u32) -> Self {
137 self.source_location = Some((file.into(), line));
138 self
139 }
140
141 pub fn with_cause(mut self, cause: FugueError) -> Self {
143 self.cause = Some(Box::new(cause));
144 self
145 }
146}
147
148impl Default for ErrorContext {
149 fn default() -> Self {
150 Self::new()
151 }
152}
153
154#[derive(Debug, Clone)]
160#[allow(clippy::result_large_err)]
161pub enum FugueError {
162 InvalidParameters {
164 distribution: String,
165 reason: String,
166 code: ErrorCode,
167 context: ErrorContext,
168 },
169 ModelError {
171 address: Option<Address>,
172 reason: String,
173 code: ErrorCode,
174 context: ErrorContext,
175 },
176 TraceError {
178 operation: String,
179 address: Option<Address>,
180 reason: String,
181 code: ErrorCode,
182 context: ErrorContext,
183 },
184 TypeMismatch {
186 address: Address,
187 expected: String,
188 found: String,
189 code: ErrorCode,
190 context: ErrorContext,
191 },
192}
193
194impl fmt::Display for FugueError {
195 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
196 match self {
197 FugueError::InvalidParameters {
198 distribution,
199 reason,
200 code,
201 context,
202 } => {
203 write!(
204 f,
205 "[{}] Invalid parameters for {}: {}",
206 *code as u32, distribution, reason
207 )?;
208 self.write_context(f, context)?;
209 Ok(())
210 }
211 FugueError::ModelError {
212 address,
213 reason,
214 code,
215 context,
216 } => {
217 if let Some(addr) = address {
218 write!(f, "[{}] Model error at {}: {}", *code as u32, addr, reason)?;
219 } else {
220 write!(f, "[{}] Model error: {}", *code as u32, reason)?;
221 }
222 self.write_context(f, context)?;
223 Ok(())
224 }
225 FugueError::TraceError {
226 operation,
227 address,
228 reason,
229 code,
230 context,
231 } => {
232 if let Some(addr) = address {
233 write!(
234 f,
235 "[{}] Trace error in {} at {}: {}",
236 *code as u32, operation, addr, reason
237 )?;
238 } else {
239 write!(
240 f,
241 "[{}] Trace error in {}: {}",
242 *code as u32, operation, reason
243 )?;
244 }
245 self.write_context(f, context)?;
246 Ok(())
247 }
248 FugueError::TypeMismatch {
249 address,
250 expected,
251 found,
252 code,
253 context,
254 } => {
255 write!(
256 f,
257 "[{}] Type mismatch at {}: expected {}, found {}",
258 *code as u32, address, expected, found
259 )?;
260 self.write_context(f, context)?;
261 Ok(())
262 }
263 }
264 }
265}
266
267impl FugueError {
268 fn write_context(&self, f: &mut fmt::Formatter<'_>, context: &ErrorContext) -> fmt::Result {
270 if let Some((file, line)) = &context.source_location {
272 write!(f, " (at {}:{})", file, line)?;
273 }
274
275 if !context.context.is_empty() {
277 write!(f, " [")?;
278 for (i, (key, value)) in context.context.iter().enumerate() {
279 if i > 0 {
280 write!(f, ", ")?;
281 }
282 write!(f, "{}={}", key, value)?;
283 }
284 write!(f, "]")?;
285 }
286
287 if let Some(cause) = &context.cause {
289 write!(f, "\n Caused by: {}", cause)?;
290 }
291
292 Ok(())
293 }
294
295 pub fn code(&self) -> ErrorCode {
297 match self {
298 FugueError::InvalidParameters { code, .. } => *code,
299 FugueError::ModelError { code, .. } => *code,
300 FugueError::TraceError { code, .. } => *code,
301 FugueError::TypeMismatch { code, .. } => *code,
302 }
303 }
304
305 pub fn category(&self) -> ErrorCategory {
307 self.code().category()
308 }
309
310 pub fn context(&self) -> &ErrorContext {
312 match self {
313 FugueError::InvalidParameters { context, .. } => context,
314 FugueError::ModelError { context, .. } => context,
315 FugueError::TraceError { context, .. } => context,
316 FugueError::TypeMismatch { context, .. } => context,
317 }
318 }
319
320 pub fn is_validation_error(&self) -> bool {
322 matches!(self.category(), ErrorCategory::DistributionValidation)
323 }
324
325 pub fn with_context(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
327 match &mut self {
328 FugueError::InvalidParameters { context, .. } => {
329 context.context.push((key.into(), value.into()));
330 }
331 FugueError::ModelError { context, .. } => {
332 context.context.push((key.into(), value.into()));
333 }
334 FugueError::TraceError { context, .. } => {
335 context.context.push((key.into(), value.into()));
336 }
337 FugueError::TypeMismatch { context, .. } => {
338 context.context.push((key.into(), value.into()));
339 }
340 }
341 self
342 }
343
344 pub fn with_source_location(mut self, file: impl Into<String>, line: u32) -> Self {
346 match &mut self {
347 FugueError::InvalidParameters { context, .. } => {
348 context.source_location = Some((file.into(), line));
349 }
350 FugueError::ModelError { context, .. } => {
351 context.source_location = Some((file.into(), line));
352 }
353 FugueError::TraceError { context, .. } => {
354 context.source_location = Some((file.into(), line));
355 }
356 FugueError::TypeMismatch { context, .. } => {
357 context.source_location = Some((file.into(), line));
358 }
359 }
360 self
361 }
362}
363
364impl std::error::Error for FugueError {}
365
366#[allow(clippy::result_large_err)]
368pub type FugueResult<T> = Result<T, FugueError>;
369
370impl FugueError {
375 pub fn invalid_parameters(
377 distribution: impl Into<String>,
378 reason: impl Into<String>,
379 code: ErrorCode,
380 ) -> Self {
381 Self::InvalidParameters {
382 distribution: distribution.into(),
383 reason: reason.into(),
384 code,
385 context: ErrorContext::new(),
386 }
387 }
388
389 pub fn invalid_parameters_with_context(
391 distribution: impl Into<String>,
392 reason: impl Into<String>,
393 code: ErrorCode,
394 context: ErrorContext,
395 ) -> Self {
396 Self::InvalidParameters {
397 distribution: distribution.into(),
398 reason: reason.into(),
399 code,
400 context,
401 }
402 }
403
404 pub fn trace_error(
406 operation: impl Into<String>,
407 address: Option<Address>,
408 reason: impl Into<String>,
409 code: ErrorCode,
410 ) -> Self {
411 Self::TraceError {
412 operation: operation.into(),
413 address,
414 reason: reason.into(),
415 code,
416 context: ErrorContext::new(),
417 }
418 }
419
420 pub fn type_mismatch(
422 address: Address,
423 expected: impl Into<String>,
424 found: impl Into<String>,
425 ) -> Self {
426 Self::TypeMismatch {
427 address,
428 expected: expected.into(),
429 found: found.into(),
430 code: ErrorCode::TypeMismatch,
431 context: ErrorContext::new(),
432 }
433 }
434}
435
436#[macro_export]
450macro_rules! invalid_params {
451 ($dist:expr, $reason:expr, $code:ident) => {
452 $crate::error::FugueError::invalid_parameters($dist, $reason, $crate::error::ErrorCode::$code)
453 };
454 ($dist:expr, $reason:expr, $code:ident, $($key:expr => $value:expr),+ $(,)?) => {
455 $crate::error::FugueError::invalid_parameters($dist, $reason, $crate::error::ErrorCode::$code)
456 $(.with_context($key, $value))*
457 };
458}
459
460#[macro_export]
468macro_rules! trace_error {
469 ($op:expr, $addr:expr, $reason:expr, $code:ident) => {
470 $crate::error::FugueError::trace_error($op, $addr, $reason, $crate::error::ErrorCode::$code)
471 };
472 ($op:expr, $addr:expr, $reason:expr, $code:ident, $($key:expr => $value:expr),+ $(,)?) => {
473 $crate::error::FugueError::trace_error($op, $addr, $reason, $crate::error::ErrorCode::$code)
474 $(.with_context($key, $value))*
475 };
476}
477
478pub trait Validate {
480 fn validate(&self) -> FugueResult<()>;
481}
482
483impl Validate for Normal {
484 fn validate(&self) -> FugueResult<()> {
485 if !self.mu().is_finite() {
486 return Err(invalid_params!(
487 "Normal",
488 "Mean (mu) must be finite",
489 InvalidMean,
490 "mu" => format!("{}", self.mu())
491 ));
492 }
493 if self.sigma() <= 0.0 || !self.sigma().is_finite() {
494 return Err(invalid_params!(
495 "Normal",
496 "Standard deviation (sigma) must be positive and finite",
497 InvalidVariance,
498 "sigma" => format!("{}", self.sigma()),
499 "expected" => "> 0.0 and finite"
500 ));
501 }
502 Ok(())
503 }
504}
505
506impl Validate for Exponential {
507 fn validate(&self) -> FugueResult<()> {
508 if self.rate() <= 0.0 || !self.rate().is_finite() {
509 return Err(invalid_params!(
510 "Exponential",
511 "Rate parameter must be positive and finite",
512 InvalidRate,
513 "rate" => format!("{}", self.rate()),
514 "expected" => "> 0.0 and finite"
515 ));
516 }
517 Ok(())
518 }
519}
520
521impl Validate for Beta {
522 fn validate(&self) -> FugueResult<()> {
523 if self.alpha() <= 0.0 || !self.alpha().is_finite() {
524 return Err(invalid_params!(
525 "Beta",
526 "Alpha parameter must be positive and finite",
527 InvalidShape,
528 "alpha" => format!("{}", self.alpha()),
529 "expected" => "> 0.0 and finite"
530 ));
531 }
532 if self.beta() <= 0.0 || !self.beta().is_finite() {
533 return Err(invalid_params!(
534 "Beta",
535 "Beta parameter must be positive and finite",
536 InvalidShape,
537 "beta" => format!("{}", self.beta()),
538 "expected" => "> 0.0 and finite"
539 ));
540 }
541 Ok(())
542 }
543}
544
545impl Validate for Gamma {
546 fn validate(&self) -> FugueResult<()> {
547 if self.shape() <= 0.0 || !self.shape().is_finite() {
548 return Err(invalid_params!(
549 "Gamma",
550 "Shape parameter must be positive and finite",
551 InvalidShape,
552 "shape" => format!("{}", self.shape()),
553 "expected" => "> 0.0 and finite"
554 ));
555 }
556 if self.rate() <= 0.0 || !self.rate().is_finite() {
557 return Err(invalid_params!(
558 "Gamma",
559 "Rate parameter must be positive and finite",
560 InvalidRate,
561 "rate" => format!("{}", self.rate()),
562 "expected" => "> 0.0 and finite"
563 ));
564 }
565 Ok(())
566 }
567}
568
569impl Validate for Uniform {
570 fn validate(&self) -> FugueResult<()> {
571 if !self.low().is_finite() || !self.high().is_finite() {
572 return Err(invalid_params!(
573 "Uniform",
574 "Bounds must be finite",
575 InvalidRange,
576 "low" => format!("{}", self.low()),
577 "high" => format!("{}", self.high())
578 ));
579 }
580 if self.low() >= self.high() {
581 return Err(invalid_params!(
582 "Uniform",
583 "Lower bound must be less than upper bound",
584 InvalidRange,
585 "low" => format!("{}", self.low()),
586 "high" => format!("{}", self.high())
587 ));
588 }
589 Ok(())
590 }
591}
592
593impl Validate for Bernoulli {
594 fn validate(&self) -> FugueResult<()> {
595 if !self.p().is_finite() || self.p() < 0.0 || self.p() > 1.0 {
596 return Err(invalid_params!(
597 "Bernoulli",
598 "Probability must be in [0, 1]",
599 InvalidProbability,
600 "p" => format!("{}", self.p()),
601 "expected" => "[0.0, 1.0]"
602 ));
603 }
604 Ok(())
605 }
606}
607
608impl Validate for Categorical {
609 fn validate(&self) -> FugueResult<()> {
610 if self.probs().is_empty() {
611 return Err(invalid_params!(
612 "Categorical",
613 "Probability vector cannot be empty",
614 InvalidProbability,
615 "length" => "0"
616 ));
617 }
618
619 let sum: f64 = self.probs().iter().sum();
620 if (sum - 1.0).abs() > 1e-6 {
621 return Err(invalid_params!(
622 "Categorical",
623 "Probabilities must sum to 1.0",
624 InvalidProbability,
625 "sum" => format!("{:.6}", sum),
626 "expected" => "1.0",
627 "tolerance" => "1e-6"
628 ));
629 }
630
631 for (i, &p) in self.probs().iter().enumerate() {
632 if !p.is_finite() || p < 0.0 {
633 return Err(invalid_params!(
634 "Categorical",
635 "All probabilities must be non-negative and finite",
636 InvalidProbability,
637 "index" => format!("{}", i),
638 "value" => format!("{}", p),
639 "expected" => ">= 0.0 and finite"
640 ));
641 }
642 }
643
644 Ok(())
645 }
646}
647
648impl Validate for LogNormal {
658 fn validate(&self) -> FugueResult<()> {
659 if !self.mu().is_finite() {
660 return Err(invalid_params!(
661 "LogNormal",
662 "Mean (mu) must be finite",
663 InvalidMean,
664 "mu" => format!("{}", self.mu())
665 ));
666 }
667 if self.sigma() <= 0.0 || !self.sigma().is_finite() {
668 return Err(invalid_params!(
669 "LogNormal",
670 "Standard deviation (sigma) must be positive and finite",
671 InvalidVariance,
672 "sigma" => format!("{}", self.sigma()),
673 "expected" => "> 0.0 and finite"
674 ));
675 }
676 Ok(())
677 }
678}
679
680impl Validate for Binomial {
681 fn validate(&self) -> FugueResult<()> {
682 if !self.p().is_finite() || !(0.0..=1.0).contains(&self.p()) {
683 return Err(invalid_params!(
684 "Binomial",
685 "Probability must be in [0, 1]",
686 InvalidProbability,
687 "p" => format!("{}", self.p()),
688 "expected" => "[0.0, 1.0]"
689 ));
690 }
691 Ok(())
692 }
693}
694
695impl Validate for Poisson {
696 fn validate(&self) -> FugueResult<()> {
697 if self.lambda() <= 0.0 || !self.lambda().is_finite() {
698 return Err(invalid_params!(
699 "Poisson",
700 "Rate parameter lambda must be positive and finite",
701 InvalidRate,
702 "lambda" => format!("{}", self.lambda()),
703 "expected" => "> 0.0 and finite"
704 ));
705 }
706 Ok(())
707 }
708}
709
710impl Validate for StudentT {
711 fn validate(&self) -> FugueResult<()> {
712 if self.df() <= 0.0 || !self.df().is_finite() {
713 return Err(invalid_params!(
714 "StudentT",
715 "Degrees of freedom must be positive and finite",
716 InvalidShape,
717 "df" => format!("{}", self.df()),
718 "expected" => "> 0.0 and finite"
719 ));
720 }
721 if !self.loc().is_finite() {
722 return Err(invalid_params!(
723 "StudentT",
724 "Location (loc) must be finite",
725 InvalidMean,
726 "loc" => format!("{}", self.loc())
727 ));
728 }
729 if self.scale() <= 0.0 || !self.scale().is_finite() {
730 return Err(invalid_params!(
731 "StudentT",
732 "Scale must be positive and finite",
733 InvalidVariance,
734 "scale" => format!("{}", self.scale()),
735 "expected" => "> 0.0 and finite"
736 ));
737 }
738 Ok(())
739 }
740}
741
742impl Validate for Cauchy {
743 fn validate(&self) -> FugueResult<()> {
744 if !self.loc().is_finite() {
745 return Err(invalid_params!(
746 "Cauchy",
747 "Location (loc) must be finite",
748 InvalidMean,
749 "loc" => format!("{}", self.loc())
750 ));
751 }
752 if self.scale() <= 0.0 || !self.scale().is_finite() {
753 return Err(invalid_params!(
754 "Cauchy",
755 "Scale must be positive and finite",
756 InvalidVariance,
757 "scale" => format!("{}", self.scale()),
758 "expected" => "> 0.0 and finite"
759 ));
760 }
761 Ok(())
762 }
763}
764
765impl Validate for Laplace {
766 fn validate(&self) -> FugueResult<()> {
767 if !self.loc().is_finite() {
768 return Err(invalid_params!(
769 "Laplace",
770 "Location (loc) must be finite",
771 InvalidMean,
772 "loc" => format!("{}", self.loc())
773 ));
774 }
775 if self.scale() <= 0.0 || !self.scale().is_finite() {
776 return Err(invalid_params!(
777 "Laplace",
778 "Scale must be positive and finite",
779 InvalidVariance,
780 "scale" => format!("{}", self.scale()),
781 "expected" => "> 0.0 and finite"
782 ));
783 }
784 Ok(())
785 }
786}
787
788impl Validate for Weibull {
789 fn validate(&self) -> FugueResult<()> {
790 if self.shape() <= 0.0 || !self.shape().is_finite() {
791 return Err(invalid_params!(
792 "Weibull",
793 "Shape parameter must be positive and finite",
794 InvalidShape,
795 "shape" => format!("{}", self.shape()),
796 "expected" => "> 0.0 and finite"
797 ));
798 }
799 if self.scale() <= 0.0 || !self.scale().is_finite() {
800 return Err(invalid_params!(
801 "Weibull",
802 "Scale parameter must be positive and finite",
803 InvalidVariance,
804 "scale" => format!("{}", self.scale()),
805 "expected" => "> 0.0 and finite"
806 ));
807 }
808 Ok(())
809 }
810}
811
812impl Validate for ChiSquared {
813 fn validate(&self) -> FugueResult<()> {
814 if self.k() <= 0.0 || !self.k().is_finite() {
815 return Err(invalid_params!(
816 "ChiSquared",
817 "Degrees of freedom must be positive and finite",
818 InvalidShape,
819 "k" => format!("{}", self.k()),
820 "expected" => "> 0.0 and finite"
821 ));
822 }
823 Ok(())
824 }
825}
826
827impl Validate for InverseGamma {
828 fn validate(&self) -> FugueResult<()> {
829 if self.shape() <= 0.0 || !self.shape().is_finite() {
830 return Err(invalid_params!(
831 "InverseGamma",
832 "Shape parameter must be positive and finite",
833 InvalidShape,
834 "shape" => format!("{}", self.shape()),
835 "expected" => "> 0.0 and finite"
836 ));
837 }
838 if self.rate() <= 0.0 || !self.rate().is_finite() {
839 return Err(invalid_params!(
840 "InverseGamma",
841 "Rate parameter must be positive and finite",
842 InvalidRate,
843 "rate" => format!("{}", self.rate()),
844 "expected" => "> 0.0 and finite"
845 ));
846 }
847 Ok(())
848 }
849}
850
851impl Validate for DiscreteUniform {
852 fn validate(&self) -> FugueResult<()> {
853 if self.high() < self.low() {
854 return Err(invalid_params!(
855 "DiscreteUniform",
856 "Upper bound must be >= lower bound",
857 InvalidRange,
858 "low" => format!("{}", self.low()),
859 "high" => format!("{}", self.high())
860 ));
861 }
862 Ok(())
863 }
864}
865
866#[cfg(test)]
867mod tests {
868 use super::*;
869 use crate::addr;
870
871 #[test]
877 fn error_code_taxonomy_is_exactly_the_live_set() {
878 let all = [
879 ErrorCode::InvalidMean,
880 ErrorCode::InvalidVariance,
881 ErrorCode::InvalidProbability,
882 ErrorCode::InvalidRange,
883 ErrorCode::InvalidShape,
884 ErrorCode::InvalidRate,
885 ErrorCode::InvalidCount,
886 ErrorCode::AddressConflict,
887 ErrorCode::UnexpectedModelStructure,
888 ErrorCode::TraceAddressNotFound,
889 ErrorCode::TypeMismatch,
890 ];
891 assert_eq!(all.len(), 11);
892 for code in all {
893 assert!(!code.description().is_empty());
895 assert_ne!(code.category(), ErrorCategory::Unknown);
896 }
897 }
898
899 #[test]
900 fn error_code_category_and_description() {
901 let code = ErrorCode::InvalidMean;
902 assert!(ErrorCode::InvalidMean.description().contains("mean"));
903 assert_eq!(code.category(), ErrorCategory::DistributionValidation);
904
905 assert_eq!(
906 ErrorCode::AddressConflict.category(),
907 ErrorCategory::ModelExecution
908 );
909 assert_eq!(
910 ErrorCode::TraceAddressNotFound.category(),
911 ErrorCategory::TraceManipulation
912 );
913 assert_eq!(
914 ErrorCode::TypeMismatch.category(),
915 ErrorCategory::TypeSystem
916 );
917 }
918
919 #[test]
920 fn invalid_parameters_constructor_and_context() {
921 let err = FugueError::invalid_parameters("Normal", "bad params", ErrorCode::InvalidMean)
922 .with_context("mu", "nan")
923 .with_source_location("file.rs", 10);
924
925 let msg = format!("{}", err);
926 assert!(msg.contains("Invalid parameters for Normal"));
927 assert!(msg.contains("mu=nan"));
928 assert_eq!(err.code(), ErrorCode::InvalidMean);
929 assert_eq!(err.category(), ErrorCategory::DistributionValidation);
930 assert!(err.is_validation_error());
931 }
932
933 #[test]
934 fn error_macros_create_expected_variants() {
935 let e1 = invalid_params!("Uniform", "bad range", InvalidRange, "low" => "1", "high" => "0");
936 match e1 {
937 FugueError::InvalidParameters { code, .. } => assert_eq!(code, ErrorCode::InvalidRange),
938 _ => panic!("expected InvalidParameters"),
939 }
940
941 let e2 = trace_error!("lookup", Some(addr!("x")), "missing", TraceAddressNotFound);
942 match e2 {
943 FugueError::TraceError { code, .. } => {
944 assert_eq!(code, ErrorCode::TraceAddressNotFound)
945 }
946 _ => panic!("expected TraceError"),
947 }
948 }
949
950 #[test]
951 fn type_mismatch_constructor() {
952 let e = FugueError::type_mismatch(addr!("a"), "f64", "bool");
953 assert_eq!(e.code(), ErrorCode::TypeMismatch);
954 assert_eq!(e.category(), ErrorCategory::TypeSystem);
955 let msg = format!("{}", e);
956 assert!(msg.contains("Type mismatch"));
957 }
958
959 #[test]
960 fn validate_trait_on_valid_distributions() {
961 assert!(Normal::new(0.0, 1.0).unwrap().validate().is_ok());
965 assert!(Exponential::new(1.0).unwrap().validate().is_ok());
966 assert!(Beta::new(2.0, 3.0).unwrap().validate().is_ok());
967 assert!(Gamma::new(2.0, 1.0).unwrap().validate().is_ok());
968 assert!(Uniform::new(0.0, 1.0).unwrap().validate().is_ok());
969 assert!(Bernoulli::new(0.5).unwrap().validate().is_ok());
970 assert!(Categorical::new(vec![0.2, 0.8]).unwrap().validate().is_ok());
971 assert!(LogNormal::new(0.0, 1.0).unwrap().validate().is_ok());
972 assert!(Binomial::new(10, 0.5).unwrap().validate().is_ok());
973 assert!(Poisson::new(3.0).unwrap().validate().is_ok());
974 assert!(StudentT::new(5.0, 0.0, 1.0).unwrap().validate().is_ok());
975 assert!(Cauchy::new(0.0, 1.0).unwrap().validate().is_ok());
976 assert!(Laplace::new(0.0, 1.0).unwrap().validate().is_ok());
977 assert!(Weibull::new(2.0, 1.5).unwrap().validate().is_ok());
978 assert!(ChiSquared::new(4.0).unwrap().validate().is_ok());
979 assert!(InverseGamma::new(3.0, 2.0).unwrap().validate().is_ok());
980 assert!(DiscreteUniform::new(1, 6).unwrap().validate().is_ok());
981 }
982
983 #[test]
984 fn error_cause_chaining_and_display_variants() {
985 let base = FugueError::invalid_parameters("Normal", "bad", ErrorCode::InvalidMean);
987 let ctx = ErrorContext::new().with_cause(base.clone());
988 let model_err = FugueError::ModelError {
989 address: Some(crate::addr!("x")),
990 reason: "failed".into(),
991 code: ErrorCode::UnexpectedModelStructure,
992 context: ctx,
993 };
994 let msg = format!("{}", model_err);
995 assert!(msg.contains("Model error"));
996 assert!(msg.contains("Caused by"));
997 }
998}