Skip to content

Commit ef70537

Browse files
fix coverage
1 parent 227691c commit ef70537

2 files changed

Lines changed: 220 additions & 5 deletions

File tree

src/Infeasibility/analyze.jl

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,12 @@ end
8888
Return `true` if the constraint index is a variable-level constraint
8989
(i.e., `F == MOI.VariableIndex`).
9090
"""
91-
_is_variable_constraint(::MOI.ConstraintIndex{MOI.VariableIndex}) = true
92-
_is_variable_constraint(::MOI.ConstraintIndex) = false
91+
function _is_variable_constraint(::MOI.ConstraintIndex{MOI.VariableIndex})
92+
return true
93+
end
94+
function _is_variable_constraint(::MOI.ConstraintIndex)
95+
return false
96+
end
9397

9498
"""
9599
_is_integrality_constraint(ci::MOI.ConstraintIndex)
@@ -102,7 +106,9 @@ function _is_integrality_constraint(
102106
) where {S}
103107
return S <: Union{MOI.Integer,MOI.ZeroOne}
104108
end
105-
_is_integrality_constraint(::MOI.ConstraintIndex) = false
109+
function _is_integrality_constraint(::MOI.ConstraintIndex)
110+
return false
111+
end
106112

107113
"""
108114
_is_bound_constraint(ci::MOI.ConstraintIndex)
@@ -115,7 +121,9 @@ function _is_bound_constraint(
115121
) where {S}
116122
return S <: Union{MOI.LessThan,MOI.GreaterThan,MOI.EqualTo,MOI.Interval}
117123
end
118-
_is_bound_constraint(::MOI.ConstraintIndex) = false
124+
function _is_bound_constraint(::MOI.ConstraintIndex)
125+
return false
126+
end
119127

120128
"""
121129
_classify_variable_conflict!(out, model, x, bound_cis, has_integrality, integrality_set)
@@ -271,7 +279,7 @@ function MathOptAnalyzer.analyze(
271279
catch err
272280
# Only swallow errors indicating the solver doesn't support
273281
# compute_conflict! — rethrow anything else
274-
if !(err isa Union{MethodError,MOI.UnsupportedError,ErrorException})
282+
if !(err isa Union{MethodError,MOI.UnsupportedError,ErrorException,ArgumentError})
275283
rethrow(err)
276284
end
277285
@warn(

test/test_Infeasibility.jl

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -762,6 +762,213 @@ function test_native_iis_summarize()
762762
return
763763
end
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+
765972
end # module TestInfeasibility
766973

767974
TestInfeasibility.runtests()

0 commit comments

Comments
 (0)