Skip to content

Commit 87d426f

Browse files
committed
Added: defensive-path test coverage for LU and LDLT solve_vec
Add unit tests to exercise internal safety nets in the LU and LDLT diagonal solve routines. These tests manually construct factors with invalid diagonals (NaN or sub-tolerance) to verify that solve_vec correctly surfaces NonFinite and Singular errors, even though these states are unreachable via the standard factorization APIs. Refs: #84
1 parent 1693307 commit 87d426f

2 files changed

Lines changed: 102 additions & 0 deletions

File tree

src/ldlt.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,4 +468,60 @@ mod tests {
468468
let a = Matrix::<3>::from_rows([[4.0, 2.0, 0.0], [-2.0, 5.0, 1.0], [0.0, 1.0, 3.0]]);
469469
let _ = a.ldlt(DEFAULT_SINGULAR_TOL);
470470
}
471+
472+
// -----------------------------------------------------------------------
473+
// Defensive-path coverage for `solve_vec`.
474+
//
475+
// `Ldlt::factor` guarantees that every stored diagonal is finite and
476+
// strictly greater than the recorded `tol`. `solve_vec` still re-checks
477+
// both invariants as a safety net (see the `!diag.is_finite()` and
478+
// `diag <= self.tol` guards in the diagonal solve). Those branches are
479+
// unreachable through the public API, so the only way to exercise them
480+
// is to construct `Ldlt` directly with corrupt factors. The tests below
481+
// document and verify that the safety nets return the documented error
482+
// variants.
483+
// -----------------------------------------------------------------------
484+
485+
macro_rules! gen_solve_vec_defensive_tests {
486+
($d:literal) => {
487+
paste! {
488+
/// `solve_vec` must surface `NonFinite` when a stored
489+
/// diagonal is NaN, even though `factor` cannot produce
490+
/// such a factorization.
491+
#[test]
492+
fn [<solve_vec_defensive_non_finite_diagonal_ $d d>]() {
493+
let mut factors = Matrix::<$d>::identity();
494+
factors.rows[$d - 1][$d - 1] = f64::NAN;
495+
let ldlt = Ldlt::<$d> {
496+
factors,
497+
tol: DEFAULT_SINGULAR_TOL,
498+
};
499+
let b = Vector::<$d>::new([1.0; $d]);
500+
let err = ldlt.solve_vec(b).unwrap_err();
501+
assert_eq!(err, LaError::NonFinite { row: None, col: $d - 1 });
502+
}
503+
504+
/// `solve_vec` must surface `Singular` when a stored
505+
/// diagonal is at or below the recorded tolerance, even
506+
/// though `factor` cannot produce such a factorization.
507+
#[test]
508+
fn [<solve_vec_defensive_sub_tolerance_diagonal_ $d d>]() {
509+
let mut factors = Matrix::<$d>::identity();
510+
factors.rows[$d - 1][$d - 1] = 0.0;
511+
let ldlt = Ldlt::<$d> {
512+
factors,
513+
tol: DEFAULT_SINGULAR_TOL,
514+
};
515+
let b = Vector::<$d>::new([1.0; $d]);
516+
let err = ldlt.solve_vec(b).unwrap_err();
517+
assert_eq!(err, LaError::Singular { pivot_col: $d - 1 });
518+
}
519+
}
520+
};
521+
}
522+
523+
gen_solve_vec_defensive_tests!(2);
524+
gen_solve_vec_defensive_tests!(3);
525+
gen_solve_vec_defensive_tests!(4);
526+
gen_solve_vec_defensive_tests!(5);
471527
}

src/lu.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,4 +501,50 @@ mod tests {
501501
let err = lu.solve_vec(b).unwrap_err();
502502
assert_eq!(err, LaError::NonFinite { row: None, col: 1 });
503503
}
504+
505+
// -----------------------------------------------------------------------
506+
// Defensive-path coverage for `solve_vec`.
507+
//
508+
// `Lu::factor` guarantees that every stored U diagonal satisfies
509+
// `|U[i,i]| > tol`. `solve_vec` still re-checks during back-substitution
510+
// as a safety net (see the `diag.abs() <= self.tol` guard). That branch
511+
// is unreachable through the public API, so the only way to exercise it
512+
// is to construct `Lu` directly with a corrupt U. The tests below
513+
// document and verify that the safety net returns `Singular`.
514+
// -----------------------------------------------------------------------
515+
516+
macro_rules! gen_solve_vec_defensive_singular_tests {
517+
($d:literal) => {
518+
paste! {
519+
/// `solve_vec` must surface `Singular` when a stored U
520+
/// diagonal is at or below the recorded tolerance, even
521+
/// though `factor` cannot produce such a factorization.
522+
#[test]
523+
fn [<solve_vec_defensive_sub_tolerance_diagonal_ $d d>]() {
524+
let mut factors = Matrix::<$d>::identity();
525+
factors.rows[$d - 1][$d - 1] = 0.0;
526+
527+
let mut piv = [0usize; $d];
528+
for (i, p) in piv.iter_mut().enumerate() {
529+
*p = i;
530+
}
531+
532+
let lu = Lu::<$d> {
533+
factors,
534+
piv,
535+
piv_sign: 1.0,
536+
tol: DEFAULT_PIVOT_TOL,
537+
};
538+
let b = Vector::<$d>::new([0.0; $d]);
539+
let err = lu.solve_vec(b).unwrap_err();
540+
assert_eq!(err, LaError::Singular { pivot_col: $d - 1 });
541+
}
542+
}
543+
};
544+
}
545+
546+
gen_solve_vec_defensive_singular_tests!(2);
547+
gen_solve_vec_defensive_singular_tests!(3);
548+
gen_solve_vec_defensive_singular_tests!(4);
549+
gen_solve_vec_defensive_singular_tests!(5);
504550
}

0 commit comments

Comments
 (0)