use crate::context::variable::ContextVariable;
#[derive(Debug, thiserror::Error)]
pub enum OxiflowError {
#[error("missing calculator for variable: {0}")]
MissingCalculator(ContextVariable),
#[error("computation failed for {variable}: {source}")]
ComputationFailed {
variable: ContextVariable,
#[source]
source: Box<dyn std::error::Error + Send + Sync>,
},
#[error("circular dependency detected involving: {0}")]
CircularDependency(ContextVariable),
#[error("type mismatch: expected {expected}, actual {actual}")]
TypeMismatch {
expected: &'static str,
actual: &'static str,
},
#[error("invalid domain: {0}")]
InvalidDomain(String),
#[error("precondition failed in {context}: {message}")]
PreconditionFailed {
context: &'static str,
message: String,
},
#[error("external data error: {0}")]
ExternalData(String),
#[error("solver divergence at t={time:.4e}: {reason}")]
SolverDivergence { time: f64, reason: String },
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn missing_calculator_matches_variable() {
let err = OxiflowError::MissingCalculator(ContextVariable::Time);
assert!(matches!(
err,
OxiflowError::MissingCalculator(ContextVariable::Time)
));
}
#[test]
fn missing_calculator_display_contains_variable() {
let err = OxiflowError::MissingCalculator(ContextVariable::TimeStep);
assert!(err.to_string().contains("TimeStep"));
}
#[test]
fn computation_failed_is_matchable() {
let source: Box<dyn std::error::Error + Send + Sync> =
Box::new(std::io::Error::other("calculator error"));
let err = OxiflowError::ComputationFailed {
variable: ContextVariable::SpatialGradient {
dimension: 0,
component: None,
},
source,
};
assert!(matches!(err, OxiflowError::ComputationFailed { .. }));
}
#[test]
fn computation_failed_display_contains_variable_and_source() {
let source: Box<dyn std::error::Error + Send + Sync> =
Box::new(std::io::Error::other("overflow"));
let err = OxiflowError::ComputationFailed {
variable: ContextVariable::SpatialGradient {
dimension: 1,
component: None,
},
source,
};
let msg = err.to_string();
assert!(msg.contains("SpatialGradient"));
assert!(msg.contains("overflow"));
}
#[test]
fn circular_dependency_matches_variable() {
let err = OxiflowError::CircularDependency(ContextVariable::External {
name: "flux".into(),
});
assert!(matches!(err, OxiflowError::CircularDependency(_)));
}
#[test]
fn circular_dependency_display_contains_variable() {
let err = OxiflowError::CircularDependency(ContextVariable::Time);
assert!(err.to_string().contains("Time"));
}
#[test]
fn type_mismatch_fields_are_accessible() {
let err = OxiflowError::TypeMismatch {
expected: "Scalar",
actual: "Vector",
};
assert!(matches!(
err,
OxiflowError::TypeMismatch {
expected: "Scalar",
actual: "Vector"
}
));
}
#[test]
fn type_mismatch_display_contains_both_types() {
let err = OxiflowError::TypeMismatch {
expected: "Matrix",
actual: "ScalarField",
};
let msg = err.to_string();
assert!(msg.contains("Matrix"));
assert!(msg.contains("ScalarField"));
}
#[test]
fn invalid_domain_display_contains_reason() {
let err = OxiflowError::InvalidDomain("n_points must be > 1".into());
assert!(err.to_string().contains("n_points must be > 1"));
}
#[test]
fn external_data_display_contains_reason() {
let err = OxiflowError::ExternalData("file not found".into());
assert!(err.to_string().contains("file not found"));
}
#[test]
fn solver_divergence_display_contains_time_and_reason() {
let err = OxiflowError::SolverDivergence {
time: 1.23e-4,
reason: "NaN detected in state vector".into(),
};
let msg = err.to_string();
assert!(msg.contains("NaN detected"));
assert!(msg.contains("1.23"));
}
#[test]
fn solver_divergence_time_formatted_scientific() {
let err = OxiflowError::SolverDivergence {
time: 0.001,
reason: "diverged".into(),
};
assert!(err.to_string().contains("e-"));
}
#[test]
fn precondition_failed_is_matchable() {
let err = OxiflowError::PreconditionFailed {
context: "DanckwertsInlet",
message: "velocity must be non-zero".into(),
};
assert!(matches!(err, OxiflowError::PreconditionFailed { .. }));
}
#[test]
fn precondition_failed_display_contains_context_and_message() {
let err = OxiflowError::PreconditionFailed {
context: "DanckwertsInlet",
message: "velocity must be non-zero".into(),
};
let msg = err.to_string();
assert!(msg.contains("DanckwertsInlet"));
assert!(msg.contains("velocity must be non-zero"));
}
#[test]
fn precondition_failed_context_is_static_str() {
let err = OxiflowError::PreconditionFailed {
context: "UniformGrid1D",
message: "n_points must be >= 2".into(),
};
assert!(matches!(
err,
OxiflowError::PreconditionFailed {
context: "UniformGrid1D",
..
}
));
}
#[test]
fn all_variants_implement_debug() {
let variants: Vec<Box<dyn std::fmt::Debug>> = vec![
Box::new(OxiflowError::MissingCalculator(ContextVariable::Time)),
Box::new(OxiflowError::CircularDependency(ContextVariable::TimeStep)),
Box::new(OxiflowError::TypeMismatch {
expected: "Scalar",
actual: "Boolean",
}),
Box::new(OxiflowError::InvalidDomain("test".into())),
Box::new(OxiflowError::PreconditionFailed {
context: "test",
message: "test condition".into(),
}),
Box::new(OxiflowError::ExternalData("test".into())),
Box::new(OxiflowError::SolverDivergence {
time: 0.0,
reason: "test".into(),
}),
];
for v in &variants {
assert!(!format!("{:?}", v).is_empty());
}
}
}