@@ -762,6 +762,213 @@ function test_native_iis_summarize()
762762 return
763763end
764764
765+ # ==============================================================================
766+ # Unit tests for internal helper functions (coverage of dead-code / fallback paths)
767+ # ==============================================================================
768+
769+ """
770+ Direct unit tests for `_is_variable_constraint`, `_is_integrality_constraint`,
771+ and `_is_bound_constraint` (lines 91–92, 100, 103, 105, 118).
772+ """
773+ function test_helper_is_variable_constraint ()
774+ # Both dispatch methods of _is_variable_constraint
775+ ci_vi =
776+ MOI. ConstraintIndex {MOI.VariableIndex,MOI.LessThan{Float64}} (1 )
777+ ci_saf =
778+ MOI. ConstraintIndex {MOI.ScalarAffineFunction{Float64},MOI.LessThan{Float64}} (1 )
779+ @test MathOptAnalyzer. Infeasibility. _is_variable_constraint (ci_vi) == true
780+ @test MathOptAnalyzer. Infeasibility. _is_variable_constraint (ci_saf) == false
781+ return
782+ end
783+
784+ function test_helper_is_integrality_constraint ()
785+ # Parametric version returning true (lines 100, 103)
786+ ci_int = MOI. ConstraintIndex {MOI.VariableIndex,MOI.Integer} (1 )
787+ @test MathOptAnalyzer. Infeasibility. _is_integrality_constraint (ci_int) ==
788+ true
789+ # Parametric version returning false (lines 100, 103)
790+ ci_lt = MOI. ConstraintIndex {MOI.VariableIndex,MOI.LessThan{Float64}} (1 )
791+ @test MathOptAnalyzer. Infeasibility. _is_integrality_constraint (ci_lt) ==
792+ false
793+ # Non-VariableIndex fallback returning false (line 105)
794+ ci_saf =
795+ MOI. ConstraintIndex {MOI.ScalarAffineFunction{Float64},MOI.LessThan{Float64}} (1 )
796+ @test MathOptAnalyzer. Infeasibility. _is_integrality_constraint (ci_saf) ==
797+ false
798+ return
799+ end
800+
801+ function test_helper_is_bound_constraint ()
802+ # Non-VariableIndex fallback returning false (line 118)
803+ ci_saf =
804+ MOI. ConstraintIndex {MOI.ScalarAffineFunction{Float64},MOI.LessThan{Float64}} (1 )
805+ @test MathOptAnalyzer. Infeasibility. _is_bound_constraint (ci_saf) == false
806+ return
807+ end
808+
809+ # ==============================================================================
810+ # Direct tests for _classify_variable_conflict! branches
811+ # ==============================================================================
812+
813+ """
814+ Exercise the `EqualTo` branch (lines 144–146), the `Interval` branch
815+ (lines 147–149), and the `has_integrality` path (line 153) of
816+ `_classify_variable_conflict!`.
817+ """
818+ function test_classify_variable_conflict_equalto ()
819+ # MOI.Utilities.Model rejects two lower-bound constraints on the same
820+ # variable. Add the two conflicting constraints on separate variables so
821+ # both are stored; _classify_variable_conflict! only queries the set of
822+ # each ConstraintIndex, it does not check variable ownership.
823+ model_moi = MOI. Utilities. Model {Float64} ()
824+ x = MOI. add_variable (model_moi) # will be the "target" variable
825+ y = MOI. add_variable (model_moi) # proxy variable for the GreaterThan
826+ ci_eq = MOI. add_constraint (model_moi, x, MOI. EqualTo (2.0 )) # ub = lb = 2.0
827+ ci_gt = MOI. add_constraint (model_moi, y, MOI. GreaterThan (3.0 )) # lb = 3.0
828+ out = MathOptAnalyzer. Infeasibility. Data ()
829+ MathOptAnalyzer. Infeasibility. _classify_variable_conflict! (
830+ out,
831+ model_moi,
832+ x,
833+ MOI. ConstraintIndex[ci_eq, ci_gt],
834+ false ,
835+ nothing ,
836+ )
837+ # EqualTo(2.0) → ub=2, GreaterThan(3.0) → lb=3 ⟹ lb > ub
838+ @test length (out. infeasible_bounds) == 1
839+ @test out. infeasible_bounds[1 ]. lb == 3.0
840+ @test out. infeasible_bounds[1 ]. ub == 2.0
841+ return
842+ end
843+
844+ function test_classify_variable_conflict_interval ()
845+ model_moi = MOI. Utilities. Model {Float64} ()
846+ x = MOI. add_variable (model_moi)
847+ # Interval(lower=5, upper=3) has lb > ub → infeasible
848+ ci_ivl = MOI. add_constraint (model_moi, x, MOI. Interval (5.0 , 3.0 ))
849+ out = MathOptAnalyzer. Infeasibility. Data ()
850+ MathOptAnalyzer. Infeasibility. _classify_variable_conflict! (
851+ out,
852+ model_moi,
853+ x,
854+ MOI. ConstraintIndex[ci_ivl],
855+ false ,
856+ nothing ,
857+ )
858+ @test length (out. infeasible_bounds) == 1
859+ @test out. infeasible_bounds[1 ]. lb == 5.0
860+ @test out. infeasible_bounds[1 ]. ub == 3.0
861+ return
862+ end
863+
864+ function test_classify_variable_conflict_integrality ()
865+ model_moi = MOI. Utilities. Model {Float64} ()
866+ x = MOI. add_variable (model_moi)
867+ ci_gt = MOI. add_constraint (model_moi, x, MOI. GreaterThan (2.2 ))
868+ ci_lt = MOI. add_constraint (model_moi, x, MOI. LessThan (2.9 ))
869+ out = MathOptAnalyzer. Infeasibility. Data ()
870+ # Explicit Vector{ConstraintIndex} annotation so dispatch matches the
871+ # function signature (avoid Vector{ConstraintIndex{VariableIndex}} mismatch)
872+ MathOptAnalyzer. Infeasibility. _classify_variable_conflict! (
873+ out,
874+ model_moi,
875+ x,
876+ MOI. ConstraintIndex[ci_gt, ci_lt],
877+ true , # has_integrality
878+ MOI. Integer (),
879+ )
880+ @test length (out. infeasible_integrality) == 1
881+ @test out. infeasible_integrality[1 ]. lb ≈ 2.2
882+ @test out. infeasible_integrality[1 ]. ub ≈ 2.9
883+ @test out. infeasible_integrality[1 ]. set == MOI. Integer ()
884+ return
885+ end
886+
887+ # ==============================================================================
888+ # Direct tests for _categorize_native_iis!
889+ # ==============================================================================
890+
891+ """
892+ Exercise the integrality branch of `_categorize_native_iis!` (lines 184–186)
893+ and the scalar-constraint path that uses the non-VariableIndex fallbacks of
894+ `_is_bound_constraint` (line 118) and `_is_integrality_constraint` (line 105).
895+ """
896+ function test_categorize_native_iis_integrality ()
897+ model_moi = MOI. Utilities. Model {Float64} ()
898+ x = MOI. add_variable (model_moi)
899+ ci_gt = MOI. add_constraint (model_moi, x, MOI. GreaterThan (2.2 ))
900+ ci_lt = MOI. add_constraint (model_moi, x, MOI. LessThan (2.9 ))
901+ ci_int = MOI. add_constraint (model_moi, x, MOI. Integer ())
902+ out = MathOptAnalyzer. Infeasibility. Data ()
903+ # Explicit element type so the vector is Vector{ConstraintIndex} not
904+ # Vector{ConstraintIndex{VariableIndex}}
905+ conflicting = MOI. ConstraintIndex[ci_gt, ci_lt, ci_int]
906+ MathOptAnalyzer. Infeasibility. _categorize_native_iis! (out, model_moi, conflicting)
907+ # The integer + bound conflict should be classified as InfeasibleIntegrality
908+ @test length (out. infeasible_integrality) == 1
909+ @test out. infeasible_integrality[1 ]. lb ≈ 2.2
910+ @test out. infeasible_integrality[1 ]. ub ≈ 2.9
911+ return
912+ end
913+
914+ function test_categorize_native_iis_scalar_constraint ()
915+ # A scalar (SAF) constraint exercises the non-VariableIndex fallbacks:
916+ # _is_bound_constraint → line 118 (false), _is_integrality_constraint → line 105 (false)
917+ model_moi = MOI. Utilities. Model {Float64} ()
918+ x = MOI. add_variable (model_moi)
919+ y = MOI. add_variable (model_moi)
920+ f = MOI. ScalarAffineFunction (
921+ [MOI. ScalarAffineTerm (1.0 , x), MOI. ScalarAffineTerm (1.0 , y)],
922+ 0.0 ,
923+ )
924+ ci_saf = MOI. add_constraint (model_moi, f, MOI. LessThan (1.0 ))
925+ out = MathOptAnalyzer. Infeasibility. Data ()
926+ conflicting = MOI. ConstraintIndex[ci_saf]
927+ MathOptAnalyzer. Infeasibility. _categorize_native_iis! (out, model_moi, conflicting)
928+ # Scalar constraint → pushed as an IrreducibleInfeasibleSubset
929+ @test length (out. iis) == 1
930+ @test length (out. iis[1 ]. constraint) == 1
931+ return
932+ end
933+
934+ # ==============================================================================
935+ # Test for the native_iis fallback / warning path
936+ # ==============================================================================
937+
938+ """
939+ When `native_iis = true` is requested with a solver that does not support
940+ `compute_conflict!` (SCS throws `ArgumentError`), the code should emit a
941+ warning + error log and then fall back gracefully to the MathOptIIS path
942+ (lines 274, 277, 281 of analyze.jl).
943+ """
944+ function test_native_iis_fallback_warning ()
945+ model = Model (HiGHS. Optimizer)
946+ set_silent (model)
947+ @variable (model, 0 <= x <= 10 )
948+ @variable (model, 0 <= y <= 20 )
949+ @constraint (model, c1, x + y <= 1 )
950+ @constraint (model, c2, x + y >= 2 )
951+ @objective (model, Max, x + y)
952+ optimize! (model)
953+ # SCS.Optimizer does not support compute_conflict! and raises ArgumentError.
954+ # With the fixed catch clause the code should warn, log the error, and fall
955+ # back to MathOptIIS instead of rethrowing.
956+ data = @test_logs (
957+ (:warn , r" Native IIS computation failed" ),
958+ (:error , r" Error details" ),
959+ match_mode = :any ,
960+ MathOptAnalyzer. analyze (
961+ MathOptAnalyzer. Infeasibility. Analyzer (),
962+ model;
963+ optimizer = SCS. Optimizer,
964+ native_iis = true ,
965+ ),
966+ )
967+ list = MathOptAnalyzer. list_of_issue_types (data)
968+ @test length (list) >= 1
969+ return
970+ end
971+
765972end # module TestInfeasibility
766973
767974TestInfeasibility. runtests ()
0 commit comments