//! Integration tests for Newton-Raphson solver (Story 4.2). //! //! Tests cover: //! - AC #1: Solver trait and strategy dispatch //! - AC #2: Configuration options //! - AC #3: Timeout enforcement //! - AC #4: Error handling for empty/invalid systems //! - AC #5: Pre-allocated buffers (no panic) use approx::assert_relative_eq; use entropyk_solver::{NewtonConfig, Solver, SolverError, System}; use std::time::Duration; // ───────────────────────────────────────────────────────────────────────────── // AC #1: Solver Trait and Strategy Dispatch // ───────────────────────────────────────────────────────────────────────────── #[test] fn test_newton_config_default() { let cfg = NewtonConfig::default(); assert_eq!(cfg.max_iterations, 100); assert_relative_eq!(cfg.tolerance, 1e-6); assert!(!cfg.line_search); assert!(cfg.timeout.is_none()); assert!(!cfg.use_numerical_jacobian); assert_relative_eq!(cfg.line_search_armijo_c, 1e-4); assert_eq!(cfg.line_search_max_backtracks, 20); assert_relative_eq!(cfg.divergence_threshold, 1e10); } #[test] fn test_newton_config_with_timeout() { let timeout = Duration::from_millis(500); let cfg = NewtonConfig::default().with_timeout(timeout); assert_eq!(cfg.timeout, Some(timeout)); } #[test] fn test_newton_config_custom_values() { let cfg = NewtonConfig { max_iterations: 50, tolerance: 1e-8, line_search: true, timeout: Some(Duration::from_millis(500)), use_numerical_jacobian: true, line_search_armijo_c: 1e-3, line_search_max_backtracks: 10, divergence_threshold: 1e8, ..Default::default() }; assert_eq!(cfg.max_iterations, 50); assert_relative_eq!(cfg.tolerance, 1e-8); assert!(cfg.line_search); assert_eq!(cfg.timeout, Some(Duration::from_millis(500))); assert!(cfg.use_numerical_jacobian); assert_relative_eq!(cfg.line_search_armijo_c, 1e-3); assert_eq!(cfg.line_search_max_backtracks, 10); assert_relative_eq!(cfg.divergence_threshold, 1e8); } // ───────────────────────────────────────────────────────────────────────────── // AC #2: Empty System Handling // ───────────────────────────────────────────────────────────────────────────── #[test] fn test_empty_system_returns_invalid() { let mut sys = System::new(); sys.finalize().unwrap(); let mut solver = NewtonConfig::default(); let result = solver.solve(&mut sys); assert!(result.is_err()); match result { Err(SolverError::InvalidSystem { message }) => { assert!(message.contains("Empty") || message.contains("no state")); } other => panic!("Expected InvalidSystem, got {:?}", other), } } #[test] #[should_panic(expected = "finalize")] fn test_empty_system_without_finalize_panics() { // System panics if solve() is called without finalize() // This is expected behavior - the solver requires a finalized system let mut sys = System::new(); // Don't call finalize let mut solver = NewtonConfig::default(); let _ = solver.solve(&mut sys); } // ───────────────────────────────────────────────────────────────────────────── // AC #3: Timeout Enforcement // ───────────────────────────────────────────────────────────────────────────── #[test] fn test_timeout_value_in_error() { let mut sys = System::new(); sys.finalize().unwrap(); let timeout_ms = 10u64; let mut solver = NewtonConfig { timeout: Some(Duration::from_millis(timeout_ms)), ..Default::default() }; let result = solver.solve(&mut sys); // Empty system returns InvalidSystem immediately (before timeout check) assert!(result.is_err()); } // ───────────────────────────────────────────────────────────────────────────── // AC #4: Error Types // ───────────────────────────────────────────────────────────────────────────── #[test] fn test_error_display_non_convergence() { let err = SolverError::NonConvergence { iterations: 42, final_residual: 1.23e-3, }; let msg = err.to_string(); assert!(msg.contains("42")); assert!(msg.contains("1.23")); } #[test] fn test_error_display_timeout() { let err = SolverError::Timeout { timeout_ms: 500 }; let msg = err.to_string(); assert!(msg.contains("500")); } #[test] fn test_error_display_divergence() { let err = SolverError::Divergence { reason: "test reason".to_string(), }; let msg = err.to_string(); assert!(msg.contains("test reason")); } #[test] fn test_error_display_invalid_system() { let err = SolverError::InvalidSystem { message: "test message".to_string(), }; let msg = err.to_string(); assert!(msg.contains("test message")); } #[test] fn test_error_equality() { let e1 = SolverError::NonConvergence { iterations: 10, final_residual: 1e-3, }; let e2 = SolverError::NonConvergence { iterations: 10, final_residual: 1e-3, }; assert_eq!(e1, e2); let e3 = SolverError::Timeout { timeout_ms: 100 }; assert_ne!(e1, e3); } // ───────────────────────────────────────────────────────────────────────────── // AC #5: Pre-Allocated Buffers (No Panic) // ───────────────────────────────────────────────────────────────────────────── #[test] fn test_solver_does_not_panic_on_empty_system() { let mut sys = System::new(); sys.finalize().unwrap(); let mut solver = NewtonConfig::default(); // Should complete without panic let result = solver.solve(&mut sys); assert!(result.is_err()); } #[test] fn test_solver_does_not_panic_with_line_search() { let mut sys = System::new(); sys.finalize().unwrap(); let mut solver = NewtonConfig { line_search: true, ..Default::default() }; // Should complete without panic let result = solver.solve(&mut sys); assert!(result.is_err()); } #[test] fn test_solver_does_not_panic_with_numerical_jacobian() { let mut sys = System::new(); sys.finalize().unwrap(); let mut solver = NewtonConfig { use_numerical_jacobian: true, ..Default::default() }; // Should complete without panic let result = solver.solve(&mut sys); assert!(result.is_err()); } // ───────────────────────────────────────────────────────────────────────────── // AC #6: ConvergedState // ───────────────────────────────────────────────────────────────────────────── #[test] fn test_converged_state_is_converged() { use entropyk_solver::ConvergedState; use entropyk_solver::ConvergenceStatus; let state = ConvergedState::new(vec![1.0, 2.0, 3.0], 10, 1e-8, ConvergenceStatus::Converged, entropyk_solver::SimulationMetadata::new("".to_string())); assert!(state.is_converged()); assert_eq!(state.iterations, 10); assert_eq!(state.state, vec![1.0, 2.0, 3.0]); } #[test] fn test_converged_state_timed_out() { use entropyk_solver::ConvergedState; use entropyk_solver::ConvergenceStatus; let state = ConvergedState::new( vec![1.0], 50, 1e-3, ConvergenceStatus::TimedOutWithBestState, entropyk_solver::SimulationMetadata::new("".to_string()), ); assert!(!state.is_converged()); }