diff --git a/docs/src/submodules/Test/overview.md b/docs/src/submodules/Test/overview.md index 46a9561f75..581a4c3504 100644 --- a/docs/src/submodules/Test/overview.md +++ b/docs/src/submodules/Test/overview.md @@ -178,6 +178,14 @@ function test_unit_optimize!_twice( # `config.solve == false`. return end + # Use the `@requires` macro to check conditions that the test function + # requires in order to run. Models failing this `@requires` check will + # silently skip the test. + @requires MOI.supports_constraint( + model, + MOI.SingleVariable, + MOI.GreaterThan{Float64}, + ) # If needed, you can test that the model is empty at the start of the test. # You can assume that this will be the case for tests run via `runtests`. # User's calling tests individually need to call `MOI.empty!` themselves. diff --git a/docs/src/submodules/Test/reference.md b/docs/src/submodules/Test/reference.md index ccfeb3d797..9013fd45de 100644 --- a/docs/src/submodules/Test/reference.md +++ b/docs/src/submodules/Test/reference.md @@ -16,4 +16,6 @@ Functions to help test implementations of MOI. See Test.Config Test.runtests Test.setup_test +Test.@requires +Test.RequirementUnmet ``` diff --git a/src/Test/Test.jl b/src/Test/Test.jl index 933d53e4c6..bdb8ea163e 100644 --- a/src/Test/Test.jl +++ b/src/Test/Test.jl @@ -128,6 +128,7 @@ setup_test(::Any, ::MOI.ModelLike, ::Config) = nothing config::Config; include::Vector{String} = String[], exclude::Vector{String} = String[], + warn_unsupported::Bool = false, ) Run all tests in `MathOptInterface.Test` on `model`. @@ -141,6 +142,13 @@ Run all tests in `MathOptInterface.Test` on `model`. * If `exclude` is not empty, skip tests that contain an element from `exclude` in their name. * `exclude` takes priority over `include`. + * If `warn_unsupported` is `false`, `runtests` will silently skip tests that + fail with `UnsupportedConstraint` or `UnsupportedAttribute`. When + `warn_unsupported` is `true`, a warning will be printed. For most cases the + default behavior (`false`) is what you want, since these tests likely test + functionality that is not supported by `model`. However, it can be useful to + run `warn_unsupported = true` to check you are not skipping tests due to a + missing `supports_constraint` method or equivalent. See also: [`setup_test`](@ref). @@ -153,6 +161,7 @@ MathOptInterface.Test.runtests( config; include = ["test_linear_"], exclude = ["VariablePrimalStart"], + warn_unsupported = true, ) ``` """ @@ -161,6 +170,7 @@ function runtests( config::Config; include::Vector{String} = String[], exclude::Vector{String} = String[], + warn_unsupported::Bool = false, ) for name_sym in names(@__MODULE__; all = true) name = string(name_sym) @@ -177,7 +187,11 @@ function runtests( tear_down = setup_test(test_function, model, c) # Make sure to empty the model before every test! MOI.empty!(model) - test_function(model, c) + try + test_function(model, c) + catch err + _error_handler(err, name, warn_unsupported) + end if tear_down !== nothing tear_down() end @@ -186,6 +200,57 @@ function runtests( return end +""" + RequirementUnmet(msg::String) <: Exception + +An error for throwing in tests to indicate that the model does not support some +requirement expected by the test function. +""" +struct RequirementUnmet <: Exception + msg::String +end + +function Base.show(io::IO, err::RequirementUnmet) + print(io, "RequirementUnmet: $(err.msg)") + return +end + +""" + @requires(x) + +Check that the condition `x` is `true`. Otherwise, throw an [`RequirementUnmet`](@ref) +error to indicate that the model does not support something required by the test +function. + +## Examples + +```julia +@requires MOI.supports(model, MOI.Silent()) +@test MOI.get(model, MOI.Silent()) +``` +""" +macro requires(x) + msg = string(x) + return quote + if !$(esc(x)) + throw(RequirementUnmet($msg)) + end + end +end + +function _error_handler( + err::Union{MOI.NotAllowedError,MOI.UnsupportedError,RequirementUnmet}, + name::String, + warn_unsupported::Bool, +) + if warn_unsupported + @warn("Skipping $(name): $(err)") + end + return +end + +_error_handler(err, ::String, ::Bool) = rethrow(err) + ### ### The following are helpful utilities for writing tests in MOI.Test. ### @@ -195,8 +260,8 @@ end A three argument version of `isapprox` for use in MOI.Test. """ -function Base.isapprox(x, y, config::Config) - return Base.isapprox(x, y, atol = config.atol, rtol = config.rtol) +function Base.isapprox(x, y, config::Config{T}) where {T} + return Base.isapprox(x, y; atol = config.atol, rtol = config.rtol) end """ @@ -328,4 +393,14 @@ function _test_model_solution( return end +### +### Include all the test files! +### + +for file in readdir(@__DIR__) + if startswith(file, "test_") + include(file) + end +end + end # module diff --git a/src/Test/test_attribute.jl b/src/Test/test_attribute.jl new file mode 100644 index 0000000000..6aa4c9615a --- /dev/null +++ b/src/Test/test_attribute.jl @@ -0,0 +1,155 @@ +""" + test_attribute_NumberThreads(model::MOI.ModelLike, config::Config) + +Test that the [`MOI.NumberOfThreads`](@ref) attribute is implemented for +`model`. +""" +function test_attribute_NumberThreads(model::MOI.ModelLike, ::Config) + @requires MOI.supports(model, MOI.NumberOfThreads()) + # Get the current value to restore it at the end of the test + value = MOI.get(model, MOI.NumberOfThreads()) + MOI.set(model, MOI.NumberOfThreads(), 1) + @test MOI.get(model, MOI.NumberOfThreads()) == 1 + MOI.set(model, MOI.NumberOfThreads(), 3) + @test MOI.get(model, MOI.NumberOfThreads()) == 3 + MOI.set(model, MOI.NumberOfThreads(), value) + @test value == MOI.get(model, MOI.NumberOfThreads()) + return +end + +function setup_test( + ::typeof(test_attribute_NumberThreads), + model::MOIU.MockOptimizer, + ::Config, +) + MOI.set(model, MOI.NumberOfThreads(), nothing) + return +end + +""" + test_attribute_RawStatusString(model::MOI.ModelLike, config::Config) + +Test that the [`MOI.RawStatusString`](@ref) attribute is implemented for +`model`. +""" +function test_attribute_RawStatusString(model::MOI.ModelLike, config::Config) + if !config.supports_optimize || !_supports(config, MOI.RawStatusString()) + return + end + MOI.add_variable(model) + MOI.optimize!(model) + @test MOI.get(model, MOI.RawStatusString()) isa AbstractString + return +end + +function setup_test( + ::typeof(test_attribute_RawStatusString), + model::MOIU.MockOptimizer, + ::Config, +) + MOIU.set_mock_optimize!( + model, + (mock::MOIU.MockOptimizer) -> begin + MOI.set( + mock, + MOI.RawStatusString(), + "Mock solution set by `mock_optimize!`.", + ) + end, + ) + return +end + +""" + test_attribute_Silent(model::MOI.ModelLike, config::Config) + +Test that the [`MOI.Silent`](@ref) attribute is implemented for `model`. +""" +function test_attribute_Silent(model::MOI.ModelLike, ::Config) + @requires MOI.supports(model, MOI.Silent()) + # Get the current value to restore it at the end of the test + value = MOI.get(model, MOI.Silent()) + MOI.set(model, MOI.Silent(), !value) + @test !value == MOI.get(model, MOI.Silent()) + # Check that `set` does not just take `!` of the current value + MOI.set(model, MOI.Silent(), !value) + @test !value == MOI.get(model, MOI.Silent()) + MOI.set(model, MOI.Silent(), value) + @test value == MOI.get(model, MOI.Silent()) + return +end + +function setup_test( + ::typeof(test_attribute_Silent), + model::MOIU.MockOptimizer, + ::Config, +) + MOI.set(model, MOI.Silent(), true) + return +end + +""" + test_attribute_SolverName(model::MOI.ModelLike, config::Config) + +Test that the [`MOI.SolverName`](@ref) attribute is implemented for `model`. +""" +function test_attribute_SolverName(model::MOI.ModelLike, config::Config) + if _supports(config, MOI.SolverName()) + @test MOI.get(model, MOI.SolverName()) isa AbstractString + end + return +end + +""" + test_attribute_SolveTimeSec(model::MOI.ModelLike, config::Config) + +Test that the [`MOI.SolveTimeSec`](@ref) attribute is implemented for `model`. +""" +function test_attribute_SolveTimeSec(model::MOI.ModelLike, config::Config) + if !config.supports_optimize || !_supports(config, MOI.SolveTimeSec()) + return + end + MOI.add_variable(model) + MOI.optimize!(model) + @test MOI.get(model, MOI.SolveTimeSec()) >= 0.0 + return +end + +function setup_test( + ::typeof(test_attribute_SolveTimeSec), + model::MOIU.MockOptimizer, + ::Config, +) + MOIU.set_mock_optimize!( + model, + (mock::MOIU.MockOptimizer) -> MOI.set(mock, MOI.SolveTimeSec(), 0.0), + ) + return +end + +""" + test_attribute_TimeLimitSec(model::MOI.ModelLike, config::Config) + +Test that the [`MOI.TimeLimitSec`](@ref) attribute is implemented for `model`. +""" +function test_attribute_TimeLimitSec(model::MOI.ModelLike, ::Config) + @requires MOI.supports(model, MOI.TimeLimitSec()) + # Get the current value to restore it at the end of the test + value = MOI.get(model, MOI.TimeLimitSec()) + MOI.set(model, MOI.TimeLimitSec(), 0.0) + @test MOI.get(model, MOI.TimeLimitSec()) == 0.0 + MOI.set(model, MOI.TimeLimitSec(), 1.0) + @test MOI.get(model, MOI.TimeLimitSec()) == 1.0 + MOI.set(model, MOI.TimeLimitSec(), value) + @test value == MOI.get(model, MOI.TimeLimitSec()) # Equality should hold + return +end + +function setup_test( + ::typeof(test_attribute_TimeLimitSec), + model::MOIU.MockOptimizer, + ::Config, +) + MOI.set(model, MOI.TimeLimitSec(), nothing) + return +end diff --git a/src/Test/test_variable.jl b/src/Test/test_variable.jl new file mode 100644 index 0000000000..99b3c6bae0 --- /dev/null +++ b/src/Test/test_variable.jl @@ -0,0 +1,428 @@ +""" + test_variable_add_variable(model::MOI.ModelLike, config::Config) + +Test adding a single variable. +""" +function test_variable_add_variable(model::MOI.ModelLike, ::Config) + @test MOI.get(model, MOI.NumberOfVariables()) == 0 + v = MOI.add_variable(model) + @test MOI.get(model, MOI.NumberOfVariables()) == 1 + return +end + +""" + test_variable_add_variables(model::MOI.ModelLike, config::Config) + +Test adding multiple variables. +""" +function test_variable_add_variables(model::MOI.ModelLike, ::Config) + @test MOI.get(model, MOI.NumberOfVariables()) == 0 + v = MOI.add_variables(model, 2) + @test MOI.get(model, MOI.NumberOfVariables()) == 2 + return +end + +""" + test_variable_delete(model::MOI.ModelLike, config::Config) + +Tess adding, and then deleting, a single variable. +""" +function test_variable_delete(model::MOI.ModelLike, ::Config) + @test MOI.get(model, MOI.NumberOfVariables()) == 0 + v = MOI.add_variable(model) + @test MOI.get(model, MOI.NumberOfVariables()) == 1 + MOI.delete(model, v) + @test MOI.get(model, MOI.NumberOfVariables()) == 0 + return +end + +""" + test_variable_delete_variables(model::MOI.ModelLike, config::Config) + +Test adding, and then deleting, multiple variables. +""" +function test_variable_delete_variables(model::MOI.ModelLike, ::Config) + @test MOI.get(model, MOI.NumberOfVariables()) == 0 + v = MOI.add_variables(model, 2) + @test MOI.get(model, MOI.NumberOfVariables()) == 2 + MOI.delete(model, v) + @test MOI.get(model, MOI.NumberOfVariables()) == 0 + v = MOI.add_variables(model, 2) + @test MOI.get(model, MOI.NumberOfVariables()) == 2 + MOI.delete(model, v[1]) + @test MOI.get(model, MOI.NumberOfVariables()) == 1 + @test_throws MOI.InvalidIndex{MOI.VariableIndex} MOI.delete(model, v[1]) + try + MOI.delete(model, v[1]) + catch err + @test err.index == v[1] + end + @test !MOI.is_valid(model, v[1]) + @test MOI.is_valid(model, v[2]) + return +end + +""" + test_variable_delete_Nonnegatives(model::MOI.ModelLike, config::Config) + +Test adding, and then deleting, nonnegative variables. +""" +function test_variable_delete_Nonnegatives(model::MOI.ModelLike, ::Config) + @requires MOI.supports_add_constrained_variables(model, MOI.Nonnegatives) + @test MOI.get(model, MOI.NumberOfVariables()) == 0 + v, cv = MOI.add_constrained_variables(model, MOI.Nonnegatives(2)) + @test MOI.get(model, MOI.NumberOfVariables()) == 2 + MOI.delete(model, v) + @test MOI.get(model, MOI.NumberOfVariables()) == 0 + @test !MOI.is_valid(model, v[1]) + @test_throws MOI.InvalidIndex(v[1]) MOI.delete(model, v[1]) + @test !MOI.is_valid(model, v[2]) + @test_throws MOI.InvalidIndex(v[2]) MOI.delete(model, v[2]) + @test !MOI.is_valid(model, cv) + v, cv = MOI.add_constrained_variables(model, MOI.Nonnegatives(1)) + @test MOI.get(model, MOI.NumberOfVariables()) == 1 + MOI.delete(model, v[1]) + @test !MOI.is_valid(model, v[1]) + @test_throws MOI.InvalidIndex(v[1]) MOI.delete(model, v[1]) + @test !MOI.is_valid(model, cv) + @test MOI.get(model, MOI.NumberOfVariables()) == 0 + return +end + +""" + test_variable_delete_Nonnegatives_row(model::MOI.ModelLike, ::Config) + +Test adding, and then deleting one by one, nonnegative variables. +""" +function test_variable_delete_Nonnegatives_row(model::MOI.ModelLike, ::Config) + @requires MOI.supports_add_constrained_variables(model, MOI.Nonnegatives) + @test MOI.get(model, MOI.NumberOfVariables()) == 0 + v, cv = MOI.add_constrained_variables(model, MOI.Nonnegatives(2)) + @test MOI.get(model, MOI.NumberOfVariables()) == 2 + MOI.delete(model, v[1]) + @test !MOI.is_valid(model, v[1]) + @test_throws MOI.InvalidIndex(v[1]) MOI.delete(model, v[1]) + @test MOI.is_valid(model, cv) + @test MOI.is_valid(model, v[2]) + MOI.delete(model, v[2]) + @test MOI.get(model, MOI.NumberOfVariables()) == 0 + @test !MOI.is_valid(model, v[1]) + @test_throws MOI.InvalidIndex(v[1]) MOI.delete(model, v[1]) + @test !MOI.is_valid(model, v[2]) + @test_throws MOI.InvalidIndex(v[2]) MOI.delete(model, v[2]) + @test !MOI.is_valid(model, cv) + return +end + +""" + test_variable_delete_SecondOrderCone(model::MOI.ModelLike, config::Config) + +Test adding, and then deleting, second-order cone variables. +""" +function test_variable_delete_SecondOrderCone(model::MOI.ModelLike, ::Config) + @requires MOI.supports_add_constrained_variables(model, MOI.SecondOrderCone) + @test MOI.get(model, MOI.NumberOfVariables()) == 0 + v, cv = MOI.add_constrained_variables(model, MOI.SecondOrderCone(3)) + @test MOI.get(model, MOI.NumberOfVariables()) == 3 + MOI.delete(model, v) + @test MOI.get(model, MOI.NumberOfVariables()) == 0 + @test !MOI.is_valid(model, v[1]) + @test_throws MOI.InvalidIndex(v[1]) MOI.delete(model, v[1]) + @test !MOI.is_valid(model, v[2]) + @test_throws MOI.InvalidIndex(v[2]) MOI.delete(model, v[2]) + @test !MOI.is_valid(model, cv) + v, cv = MOI.add_constrained_variables(model, MOI.SecondOrderCone(3)) + @test MOI.get(model, MOI.NumberOfVariables()) == 3 + @test_throws MOI.DeleteNotAllowed MOI.delete(model, v[1]) + return +end + +""" + test_variable_get_VariableIndex(model::MOI.ModelLike, config::Config) + +Test getting variables by name. +""" +function test_variable_get_VariableIndex(model::MOI.ModelLike, config::Config) + if !_supports(config, MOI.VariableName()) + return + end + variable = MOI.add_variable(model) + MOI.set(model, MOI.VariableName(), variable, "x") + x = MOI.get(model, MOI.VariableIndex, "x") + @test MOI.is_valid(model, x) + @test MOI.get(model, MOI.VariableIndex, "y") === nothing + return +end + +""" + test_variable_VariableName(model::MOI.ModelLike, config::Config) + +Test getting and setting variable names. +""" +function test_variable_VariableName(model::MOI.ModelLike, ::Config) + @requires MOI.supports(model, MOI.VariableName(), MOI.VariableIndex) + v = MOI.add_variable(model) + @test MOI.get(model, MOI.VariableName(), v) == "" + MOI.set(model, MOI.VariableName(), v, "x") + @test MOI.get(model, MOI.VariableName(), v) == "x" + MOI.set(model, MOI.VariableName(), v, "y") + @test MOI.get(model, MOI.VariableName(), v) == "y" + x = MOI.add_variable(model) + MOI.set(model, MOI.VariableName(), x, "x") + @test MOI.get(model, MOI.VariableName(), x) == "x" + return +end + +""" + test_variable_solve_with_upperbound(model::MOI.ModelLike, config::Config) + +Test setting the upper bound of a variable, confirm that it solves correctly. +""" +function test_variable_solve_with_upperbound( + model::MOI.ModelLike, + config::Config, +) + x = MOI.add_variable(model) + c1 = MOI.add_constraint(model, MOI.SingleVariable(x), MOI.LessThan(1.0)) + @test x.value == c1.value + c2 = MOI.add_constraint(model, MOI.SingleVariable(x), MOI.GreaterThan(0.0)) + @test x.value == c2.value + f = MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(2.0, x)], 0.0) + MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f) + MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE) + _test_model_solution( + model, + config; + objective_value = 2.0, + variable_primal = [(x, 1.0)], + constraint_primal = [(c1, 1.0), (c2, 1.0)], + constraint_dual = [(c1, -2.0), (c2, 0.0)], + ) + return +end + +function setup_test( + ::typeof(test_variable_solve_with_upperbound), + model::MOIU.MockOptimizer, + ::Config, +) + MOIU.set_mock_optimize!( + model, + (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!( + mock, + MOI.OPTIMAL, + (MOI.FEASIBLE_POINT, [1]), + MOI.FEASIBLE_POINT, + (MOI.SingleVariable, MOI.LessThan{Float64}) => [-2.0], + (MOI.SingleVariable, MOI.GreaterThan{Float64}) => [0.0], + ), + ) + model.eval_variable_constraint_dual = false + return () -> model.eval_variable_constraint_dual = true +end + +""" + test_variable_solve_with_lowerbound(model::MOI.ModelLike, config::Config) + +Test setting the lower bound of a variable, confirm that it solves correctly. +""" +function test_variable_solve_with_lowerbound( + model::MOI.ModelLike, + config::Config, +) + x = MOI.add_variable(model) + c1 = MOI.add_constraint(model, MOI.SingleVariable(x), MOI.GreaterThan(1.0)) + c2 = MOI.add_constraint(model, MOI.SingleVariable(x), MOI.LessThan(2.0)) + f = MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(2.0, x)], 0.0) + MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f) + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + _test_model_solution( + model, + config; + objective_value = 2.0, + variable_primal = [(x, 1.0)], + constraint_primal = [(c1, 1.0), (c2, 1.0)], + constraint_dual = [(c1, 2.0), (c2, 0.0)], + ) + return +end + +function setup_test( + ::typeof(test_variable_solve_with_lowerbound), + model::MOIU.MockOptimizer, + ::Config, +) + MOIU.set_mock_optimize!( + model, + (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!( + mock, + MOI.OPTIMAL, + (MOI.FEASIBLE_POINT, [1]), + MOI.FEASIBLE_POINT, + (MOI.SingleVariable, MOI.GreaterThan{Float64}) => [2.0], + (MOI.SingleVariable, MOI.LessThan{Float64}) => [0.0], + ), + ) + model.eval_variable_constraint_dual = false + return () -> model.eval_variable_constraint_dual = true +end + +""" + test_variable_solve_Integer_with_lower_bound( + model::MOI.ModelLike, + config::Config, + ) + +Test an integer variable with fractional lower bound. +""" +function test_variable_solve_Integer_with_lower_bound( + model::MOI.ModelLike, + config::Config, +) + x = MOI.add_variable(model) + MOI.add_constraint(model, MOI.SingleVariable(x), MOI.GreaterThan(1.5)) + f = MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(2.0, x)], 0.0) + MOI.add_constraint(model, MOI.SingleVariable(x), MOI.Integer()) + MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f) + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + _test_model_solution( + model, + config; + objective_value = 4.0, + variable_primal = [(x, 2.0)], + ) + return +end + +function setup_test( + ::typeof(test_variable_solve_Integer_with_lower_bound), + model::MOIU.MockOptimizer, + ::Config, +) + MOIU.set_mock_optimize!( + model, + (mock::MOIU.MockOptimizer) -> + MOIU.mock_optimize!(mock, MOI.OPTIMAL, (MOI.FEASIBLE_POINT, [2.0])), + ) + return +end + +""" + test_variable_solve_Integer_with_upper_bound( + model::MOI.ModelLike, + config::Config, + ) + +Test an integer variable with fractional upper bound. +""" +function test_variable_solve_Integer_with_upper_bound( + model::MOI.ModelLike, + config::Config, +) + x = MOI.add_variable(model) + MOI.add_constraint(model, MOI.SingleVariable(x), MOI.LessThan(1.5)) + f = MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(-2.0, x)], 0.0) + MOI.add_constraint(model, MOI.SingleVariable(x), MOI.Integer()) + MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f) + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + _test_model_solution( + model, + config; + objective_value = -2.0, + variable_primal = [(x, 1.0)], + ) + return +end + +function setup_test( + ::typeof(test_variable_solve_Integer_with_upper_bound), + model::MOIU.MockOptimizer, + ::Config, +) + MOIU.set_mock_optimize!( + model, + (mock::MOIU.MockOptimizer) -> + MOIU.mock_optimize!(mock, MOI.OPTIMAL, (MOI.FEASIBLE_POINT, [1.0])), + ) + return +end + +""" + test_variable_solve_ZeroOne_with_upper_bound( + model::MOI.ModelLike, + config::Config, + ) + +Test a binary variable `<= 2`. +""" +function test_variable_solve_ZeroOne_with_upper_bound( + model::MOI.ModelLike, + config::Config, +) + x = MOI.add_variable(model) + MOI.add_constraint(model, MOI.SingleVariable(x), MOI.LessThan(2.0)) + f = MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(-2.0, x)], 0.0) + MOI.add_constraint(model, MOI.SingleVariable(x), MOI.ZeroOne()) + MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f) + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + _test_model_solution( + model, + config; + objective_value = -2.0, + variable_primal = [(x, 1.0)], + ) + return +end + +function setup_test( + ::typeof(test_variable_solve_ZeroOne_with_upper_bound), + model::MOIU.MockOptimizer, + ::Config, +) + MOIU.set_mock_optimize!( + model, + (mock::MOIU.MockOptimizer) -> + MOIU.mock_optimize!(mock, MOI.OPTIMAL, (MOI.FEASIBLE_POINT, [1.0])), + ) + return +end + +""" + test_variable_solve_ZeroOne_with_0_upper_bound( + model::MOI.ModelLike, + config::Config, + ) + +Test a binary variable `<= 0`. +""" +function test_variable_solve_ZeroOne_with_0_upper_bound( + model::MOI.ModelLike, + config::Config, +) + x = MOI.add_variable(model) + MOI.add_constraint(model, MOI.SingleVariable(x), MOI.LessThan(0.0)) + f = MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.0, x)], 0.0) + MOI.add_constraint(model, MOI.SingleVariable(x), MOI.ZeroOne()) + MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f) + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + _test_model_solution( + model, + config; + objective_value = 0.0, + variable_primal = [(x, 0.0)], + ) + return +end + +function setup_test( + ::typeof(test_variable_solve_ZeroOne_with_0_upper_bound), + model::MOIU.MockOptimizer, + ::Config, +) + MOIU.set_mock_optimize!( + model, + (mock::MOIU.MockOptimizer) -> + MOIU.mock_optimize!(mock, MOI.OPTIMAL, (MOI.FEASIBLE_POINT, [0.0])), + ) + return +end