use crate::error::{SolverError, error_code_to_str};
use crate::solver::{
AsCoreReport, IterationReport, SolveReport, SolveStatus, StepOutcome, TerminationReason,
};
#[test]
fn size_budgets() {
use core::mem::size_of;
assert!(size_of::<StepOutcome>() <= 2);
assert!(size_of::<SolveStatus>() <= 2);
assert!(size_of::<TerminationReason>() <= 2);
assert!(size_of::<IterationReport>() <= 12);
assert!(size_of::<SolveReport>() <= 16);
}
#[test]
fn data_free_enums_are_one_byte_under_repr_u8() {
use core::mem::size_of;
assert_eq!(size_of::<StepOutcome>(), 1);
assert_eq!(size_of::<SolveStatus>(), 1);
assert_eq!(size_of::<TerminationReason>(), 1);
}
#[test]
fn step_outcomes_are_distinct() {
assert_ne!(StepOutcome::Continue, StepOutcome::Converged);
assert_ne!(StepOutcome::Converged, StepOutcome::NoProgress);
assert_ne!(StepOutcome::Continue, StepOutcome::NoProgress);
}
#[test]
fn is_converged_reflects_status() {
assert!(SolveStatus::Converged.is_converged());
assert!(!SolveStatus::NotConverged.is_converged());
}
#[test]
fn constructors_cover_the_four_valid_combinations() {
let pairs = [
SolveReport::converged_early(5),
SolveReport::converged_at_cap(100),
SolveReport::not_converged_cap(100),
SolveReport::not_converged_stalled(7),
]
.map(|r| (r.status(), r.termination()));
use SolveStatus::*;
use TerminationReason::*;
assert_eq!(pairs[0], (Converged, ConvergenceCriterion));
assert_eq!(pairs[1], (Converged, IterationCap));
assert_eq!(pairs[2], (NotConverged, IterationCap));
assert_eq!(pairs[3], (NotConverged, NoProgress));
for (status, termination) in pairs {
let valid = matches!(
(status, termination),
(Converged, ConvergenceCriterion)
| (Converged, IterationCap)
| (NotConverged, IterationCap)
| (NotConverged, NoProgress)
);
assert!(valid, "invalid pair produced: {status:?} + {termination:?}");
}
}
#[test]
fn report_accessors_round_trip_fields() {
let r = SolveReport::not_converged_cap(42);
assert_eq!(r.status(), SolveStatus::NotConverged);
assert_eq!(r.termination(), TerminationReason::IterationCap);
assert_eq!(r.iterations_executed(), 42);
assert_eq!(
r.iteration(),
IterationReport::new(42, TerminationReason::IterationCap)
);
assert_eq!(r.iteration().iterations_executed(), 42);
assert_eq!(r.iteration().termination(), TerminationReason::IterationCap);
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
struct WrapReport {
core: SolveReport,
}
impl AsCoreReport for WrapReport {
fn as_core_report(&self) -> SolveReport {
self.core
}
}
#[test]
fn as_core_report_is_lossless_for_every_valid_report() {
let reports = [
SolveReport::converged_early(3),
SolveReport::converged_at_cap(64),
SolveReport::not_converged_cap(64),
SolveReport::not_converged_stalled(9),
];
for core in reports {
let wrapped = WrapReport { core };
assert_eq!(wrapped.as_core_report(), core, "status/termination lost");
assert_eq!(wrapped.core_status(), core.status());
}
}
#[test]
fn converged_at_cap_separates_status_from_termination() {
let r = SolveReport::converged_at_cap(100);
assert!(r.status().is_converged());
assert_eq!(r.termination(), TerminationReason::IterationCap);
}
#[test]
fn non_convergence_is_a_status_not_an_error() {
let report = SolveReport::not_converged_cap(256);
assert_eq!(report.status(), SolveStatus::NotConverged);
let error_codes: Vec<&str> = [
SolverError::DimensionMismatch { lhs: 1, rhs: 2 },
SolverError::InvalidDimension,
SolverError::InvalidInput,
SolverError::NonFiniteInput,
SolverError::UnsupportedProblemStructure,
SolverError::SingularMatrix,
SolverError::IllConditioned,
SolverError::NumericalDomain,
SolverError::Overflow,
SolverError::WorkspaceTooSmall,
SolverError::Cancelled,
SolverError::BackendUnavailable,
SolverError::InternalInvariantViolation,
]
.iter()
.copied()
.map(error_code_to_str)
.collect();
assert!(!error_codes.iter().any(|c| c.contains("converged")));
assert!(!error_codes.iter().any(|c| c.contains("iteration")));
assert!(!error_codes.contains(&"panic_gate_violation"));
}