diff --git a/docs/make.jl b/docs/make.jl index d7862fb8d9..99094185c7 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -63,6 +63,7 @@ makedocs( ], "Test" => [ "Overview" => "submodules/Test/overview.md", + "API Reference" => "submodules/Test/reference.md", ], ], ], diff --git a/docs/src/submodules/Test/overview.md b/docs/src/submodules/Test/overview.md index d325260c6d..46a9561f75 100644 --- a/docs/src/submodules/Test/overview.md +++ b/docs/src/submodules/Test/overview.md @@ -20,9 +20,7 @@ so that all solvers can benefit. ## How to test a solver The skeleton below can be used for the wrapper test file of a solver named -`FooBar`. Remove unnecessary tests as appropriate, for example tests for -features that the solver does not support (tests are not skipped depending -on the value of `supports`). +`FooBar`. ```julia # ============================ /test/MOI_wrapper.jl ============================ @@ -34,219 +32,208 @@ using Test const MOI = MathOptInterface -const OPTIMIZER_CONSTRUCTOR = MOI.OptimizerWithAttributes( - FooBar.Optimizer, - MOI.Silent() => true +const OPTIMIZER = MOI.instantiate( + MOI.OptimizerWithAttributes(FooBar.Optimizer, MOI.Silent() => true), ) -const OPTIMIZER = MOI.instantiate(OPTIMIZER_CONSTRUCTOR) const BRIDGED = MOI.instantiate( - OPTIMIZER_CONSTRUCTOR, with_bridge_type = Float64 + MOI.OptimizerWithAttributes(FooBar.Optimizer, MOI.Silent() => true), + with_bridge_type = Float64, ) -const CONFIG = MOI.DeprecatedTest.Config( + +# See the docstring of MOI.Test.Config for other arguments. +const CONFIG = MOI.Test.Config( # Modify tolerances as necessary. atol = 1e-6, rtol = 1e-6, - # Set false if dual solutions are not generated - duals = true, - # Set false if infeasibility certificates are not generated - infeas_certificates = true, # Use MOI.LOCALLY_SOLVED for local solvers. optimal_status = MOI.OPTIMAL, - # Set true if basis information is available - basis = false, ) -function test_SolverName() - @test MOI.get(OPTIMIZER, MOI.SolverName()) == "FooBar" -end +""" + runtests() -function test_supports_incremental_interface() - @test MOI.supports_incremental_interface(OPTIMIZER, false) - # Use `@test !...` if names are not supported - @test MOI.supports_incremental_interface(OPTIMIZER, true) +This function runs all functions in the this Module starting with `test_`. +""" +function runtests() + for name in names(@__MODULE__; all = true) + if startswith("$(name)", "test_") + @testset "$(name)" begin + getfield(@__MODULE__, name)() + end + end + end end -function test_unittest() - # Test all the functions included in dictionary `MOI.DeprecatedTest.unittests`, - # except functions "number_threads" and "solve_qcp_edge_cases." - MOI.DeprecatedTest.unittest( +""" + test_runtests() + +This function runs all the tests in MathOptInterface.Test. + +Pass arguments to `exclude` to skip tests for functionality that is not +implemented or that your solver doesn't support. +""" +function test_runtests() + MOI.Test.runtests( BRIDGED, CONFIG, - ["number_threads", "solve_qcp_edge_cases"] + exclude = [ + "test_attribute_NumberOfThreads", + "test_quadratic_", + ] ) + return end -function test_modification() - MOI.DeprecatedTest.modificationtest(BRIDGED, CONFIG) -end - -function test_contlinear() - MOI.DeprecatedTest.contlineartest(BRIDGED, CONFIG) -end +""" + test_SolverName() -function test_contquadratictest() - MOI.DeprecatedTest.contquadratictest(OPTIMIZER, CONFIG) +You can also write new tests for solver-specific functionality. Write each new +test as a function with a name beginning with `test_`. +""" +function test_SolverName() + @test MOI.get(FooBar.Optimizer(), MOI.SolverName()) == "FooBar" + return end -function test_contconic() - MOI.DeprecatedTest.contlineartest(BRIDGED, CONFIG) -end +end # module TestFooBar -function test_intconic() - MOI.DeprecatedTest.intconictest(BRIDGED, CONFIG) -end +# This line at tne end of the file runs all the tests! +TestFooBar.runtests() +``` -function test_default_objective_test() - MOI.DeprecatedTest.default_objective_test(OPTIMIZER) -end +Then modify your `runtests.jl` file to include the `MOI_wrapper.jl` file: +```julia +# ============================ /test/runtests.jl ============================ -function test_default_status_test() - MOI.DeprecatedTest.default_status_test(OPTIMIZER) -end +using Test -function test_nametest() - MOI.DeprecatedTest.nametest(OPTIMIZER) +@testset "MOI" begin + include("test/MOI_wrapper.jl") end +``` -function test_validtest() - MOI.DeprecatedTest.validtest(OPTIMIZER) -end +!!! info + The optimizer `BRIDGED` constructed with [`instantiate`](@ref) + automatically bridges constraints that are not supported by `OPTIMIZER` + using the bridges listed in [Bridges](@ref). It is recommended for an + implementation of MOI to only support constraints that are natively + supported by the solver and let bridges transform the constraint to the + appropriate form. For this reason it is expected that tests may not pass if + `OPTIMIZER` is used instead of `BRIDGED`. -function test_emptytest() - MOI.DeprecatedTest.emptytest(OPTIMIZER) -end +## How to add a test -function test_orderedindicestest() - MOI.DeprecatedTest.orderedindicestest(OPTIMIZER) -end +To detect bugs in solvers, we add new tests to `MOI.Test`. -function test_scalar_function_constant_not_zero() - MOI.DeprecatedTest.scalar_function_constant_not_zero(OPTIMIZER) -end +As an example, ECOS errored calling [`optimize!`](@ref) twice in a row. (See +[ECOS.jl PR #72](https://github.com/jump-dev/ECOS.jl/pull/72).) We could add a +test to ECOS.jl, but that would only stop us from re-introducing the bug to +ECOS.jl in the future, but it would not catch other solvers in the ecosystem +with the same bug! Instead, if we add a test to `MOI.Test`, then all solvers +will also check that they handle a double optimize call! -# This function runs all functions in this module starting with `test_`. -function runtests() - for name in names(@__MODULE__; all = true) - if startswith("$(name)", "test_") - @testset "$(name)" begin - getfield(@__MODULE__, name)() - end - end - end -end +For this test, we care about correctness, rather than performance. therefore, we +don't expect solvers to efficiently decide that they have already solved the +problem, only that calling [`optimize!`](@ref) twice doesn't throw an error or +give the wrong answer. -end # module TestFooBar +**Step 1** -TestFooBar.runtests() +Install the `MathOptInterface` julia package in `dev` mode +([ref](https://julialang.github.io/Pkg.jl/v1/managing-packages/#developing-1)): +```julia +julia> ] +(@v1.6) pkg> dev MathOptInterface ``` -Test functions like `MOI.DeprecatedTest.unittest` and `MOI.DeprecatedTest.modificationtest` are -wrappers around corresponding dictionaries `MOI.DeprecatedTest.unittests` and -`MOI.DeprecatedTest.modificationtests`. Exclude tests by passing a vector of strings -corresponding to the test keys you want to exclude as the third positional -argument to the test function. +**Step 2** -!!! tip - Print a list of all keys using `println.(keys(MOI.DeprecatedTest.unittests))` - -The optimizer `BRIDGED` constructed with [`instantiate`](@ref) -automatically bridges constraints that are not supported by `OPTIMIZER` -using the bridges listed in [Bridges](@ref). It is recommended for an -implementation of MOI to only support constraints that are natively supported -by the solver and let bridges transform the constraint to the appropriate form. -For this reason it is expected that tests may not pass if `OPTIMIZER` is used -instead of `BRIDGED`. - -To test that a specific problem can be solved without bridges, a specific test -can be added with `OPTIMIZER` instead of `BRIDGED`. For example: +From here on, proceed with making the following changes in the +`~/.julia/dev/MathOptInterface` folder (or equivalent `dev` path on your +machine). + +**Step 3** + +Since the double-optimize error involves solving an optimization problem, +add a new test to [src/Test/UnitTests/solve.jl](https://github.com/jump-dev/MathOptInterface.jl/blob/master/src/Test/UnitTests/solve.jl). + +The test should be something like ```julia -function test_interval_constraints() - MOI.DeprecatedTest.linear10test(OPTIMIZER, CONFIG) +""" + test_unit_optimize!_twice(model::MOI.ModelLike, config::Config) + +Test that calling `MOI.optimize!` twice does not error. + +This problem was first detected in ECOS.jl PR#72: +https://github.com/jump-dev/ECOS.jl/pull/72 +""" +function test_unit_optimize!_twice( + model::MOI.ModelLike, + config::Config{T}, +) where {T} + if !config.supports_optimize + # Use `config` to modify the behavior of the tests. Since this test is + # concerned with `optimize!`, we should skip the test if + # `config.solve == false`. + return + end + # 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. + @test MOI.is_empty(model) + # Create a simple model. Try to make this as simple as possible so that the + # majority of solvers can run the test. + x = MOI.add_variable(model) + MOI.add_constraint(model, MOI.SingleVariable(x), MOI.GreaterThan(one(T))) + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + MOI.set( + model, + MOI.ObjectiveFunction{MOI.SingleVariable}(), + MOI.SingleVariable(x), + ) + # The main component of the test: does calling `optimize!` twice error? + MOI.optimize!(model) + MOI.optimize!(model) + # Check we have a solution. + @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMAL + # There is a three-argument version of `Base.isapprox` for checking + # approximate equality based on the tolerances defined in `config`: + @test isapprox(MOI.get(model, MOI.VariablePrimal(), x), one(T), config) + # For code-style, these tests should always `return` `nothing`. + return end ``` -checks that `OPTIMIZER` implements support for -[`ScalarAffineFunction`](@ref)-in-[`Interval`](@ref). - -## How to add a test -To give an example, ECOS errored calling [`optimize!`](@ref) twice in a row. -(See [ECOS.jl PR #72](https://github.com/jump-dev/ECOS.jl/pull/72).) +!!! info + Make sure the function is agnoistic to the number type `T`! Don't assume it + is a `Float64` capable solver! -We could add a test to ECOS.jl, but that would only stop us from re-introducing -the bug to ECOS.jl in the future. +We also need to write a test for the test. Place this function immediately below +the test you just wrote in the same file: +```julia +function setup_test( + ::typeof(test_unit_optimize!_twice), + model::MOI.Utilities.MockOptimizer, + ::Config, +) + MOI.Utilities.set_mock_optimize!( + model, + (mock::MOI.Utilities.MockOptimizer) -> MOIU.mock_optimize!( + mock, + MOI.OPTIMAL, + (MOI.FEASIBLE_POINT, [1.0]), + ), + ) + return +end +``` -Instead, if we add a test to `MOI.DeprecatedTest`, then all solvers will also check that -they handle a double optimize call! +**Step 6** -For this test, we care about correctness, rather than performance. therefore, we -don't expect solvers to efficiently decide that they have already solved the -problem, only that calling [`optimize!`](@ref) twice doesn't throw an error or -give the wrong answer. +Commit the changes to git from `~/.julia/dev/MathOptInterface` and +submit the PR for review. -To resolve this issue, follow these steps (tested on Julia v1.5): - -1. Install the `MathOptInterface` julia package in `dev` mode - ([ref](https://julialang.github.io/Pkg.jl/v1/managing-packages/#developing-1)): - ```julia - julia> ] - (@v1.5) pkg> dev ECOS - (@v1.5) pkg> dev MathOptInterface - ``` -2. From here on, proceed with making the following changes in the - `~/.julia/dev/MathOptInterface` folder (or equivalent `dev` path on your - machine). -3. Since the double-optimize error involves solving an optimization problem, - add a new test to [src/Test/UnitTests/solve.jl](https://github.com/jump-dev/MathOptInterface.jl/blob/master/src/Test/UnitTests/solve.jl). - The test should be something like - ```julia - function solve_twice(model::MOI.ModelLike, config::Config) - MOI.empty!(model) - x = MOI.add_variable(model) - c = MOI.add_constraint(model, MOI.SingleVariable(x), MOI.GreaterThan(1.0)) - MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) - MOI.set(model, MOI.ObjectiveFunction{MOI.SingleVariable}(), MOI.SingleVariable(x)) - if config.solve - MOI.optimize!(model) - MOI.optimize!(model) - MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMAL - MOI.get(model, MOI.VariablePrimal(), x) == 1.0 - end - end - unittests["solve_twice"] = solve_twice - ``` -2. Add a test for the test you just wrote. (We test the tests!) - a. Add the name of the test (`"solve_twice"`) to the end of the array in - `MOI.DeprecatedTest.unittest(...)` ([link](https://github.com/jump-dev/MathOptInterface.jl/blob/7543afe4b5151cf36bbd18181c1bb5c83266ae2f/test/Test/unit.jl#L51-L52)). - b. Add a test for the test towards the end of the "Unit Tests" test set - ([link](https://github.com/jump-dev/MathOptInterface.jl/blob/7543afe4b5151cf36bbd18181c1bb5c83266ae2f/test/Test/unit.jl#L394)). - The test should look something like - ```julia - @testset "solve_twice" begin - MOI.Utilities.set_mock_optimize!(mock, - (mock::MOI.Utilities.MockOptimizer) -> MOI.Utilities.mock_optimize!( - mock, - MOI.OPTIMAL, - (MOI.FEASIBLE_POINT, [1.0]), - ), - (mock::MOI.Utilities.MockOptimizer) -> MOI.Utilities.mock_optimize!( - mock, - MOI.OPTIMAL, - (MOI.FEASIBLE_POINT, [1.0]), - ) - ) - MOI.DeprecatedTest.solve_twice(mock, config) - end - ``` - In the above `mock` is a `MOI.Utilities.MockOptimizer` that is defined - tesearlier in the file. In this test, `MOI.Utilities.set_mock_optimize!` loads - `mock` with two results. Each says that the - [`TerminationStatus`](@ref) is `MOI.OPTIMAL`, that the - [`PrimalStatus`](@ref) is `MOI.FEASIBLE_POINT`, and that there is one - variable with a `MOI.VariableValue` or `1.0` -3. Run the tests: - ```julia - (@v1.5) pkg> test ECOS - ``` -4. Finally, commit the changes to git from `~/.julia/dev/MathOptInterface` and - submit the PR for review. +!!! tip + If you need help writing a test, [open an issue on GitHub](https://github.com/jump-dev/MathOptInterface.jl/issues/new), + or ask the [Developer Chatroom](https://gitter.im/JuliaOpt/JuMP.jl) diff --git a/docs/src/submodules/Test/reference.md b/docs/src/submodules/Test/reference.md new file mode 100644 index 0000000000..ccfeb3d797 --- /dev/null +++ b/docs/src/submodules/Test/reference.md @@ -0,0 +1,19 @@ +```@meta +CurrentModule = MathOptInterface +DocTestSetup = quote + using MathOptInterface + const MOI = MathOptInterface +end +DocTestFilters = [r"MathOptInterface|MOI"] +``` + +# [The Test submodule](@id test_reference) + +Functions to help test implementations of MOI. See +[The Test submodule](@ref test_module) for more details. + +```@docs +Test.Config +Test.runtests +Test.setup_test +``` diff --git a/src/MathOptInterface.jl b/src/MathOptInterface.jl index 5e0c71f3b5..f5475b130f 100644 --- a/src/MathOptInterface.jl +++ b/src/MathOptInterface.jl @@ -175,7 +175,7 @@ end # submodules include("Utilities/Utilities.jl") # MOI.Utilities -# include("Test/Test.jl") # MOI.Test +include("Test/Test.jl") include("Bridges/Bridges.jl") # MOI.Bridges include("Benchmarks/Benchmarks.jl") include("FileFormats/FileFormats.jl") diff --git a/src/Test/Test.jl b/src/Test/Test.jl new file mode 100644 index 0000000000..933d53e4c6 --- /dev/null +++ b/src/Test/Test.jl @@ -0,0 +1,331 @@ +module Test + +using MathOptInterface +const MOI = MathOptInterface +const MOIU = MOI.Utilities + +using Test + +# Be wary of adding new fields to this Config struct. Always think: can it be +# achieved a different way? +mutable struct Config{T<:Real} + atol::T + rtol::T + supports_optimize::Bool + optimal_status::MOI.TerminationStatusCode + excluded_attributes::Vector{Any} +end + +""" + Config( + ::Type{T} = Float64; + atol::Real = Base.rtoldefault(T), + rtol::Real = Base.rtoldefault(T), + supports_optimize::Bool = true, + optimal_status::MOI.TerminationStatusCode = MOI.OPTIMAL, + excluded_attributes::Vector{Any} = Any[], + ) where {T} + +Return an object that is used to configure various tests. + +## Configuration arguments + + * `atol::Real = Base.rtoldefault(T)`: Control the absolute tolerance used + when comparing solutions. + * `rtol::Real = Base.rtoldefault(T)`: Control the relative tolerance used + when comparing solutions. + * `supports_optimize::Bool = true`: Set to `false` to skip tests requiring a + call to [`MOI.optimize!`](@ref) + * `optimal_status = MOI.OPTIMAL`: Set to `MOI.LOCALLY_SOLVED` if the solver + cannot prove global optimality. + +## Examples + +For a nonlinear solver that finds local optima and does not support finding +dual variables or constraint names: +```julia +Config( + Float64; + optimal_status = MOI.LOCALLY_SOLVED, + excluded_attributes = Any[ + MOI.ConstraintDual(), + MOI.VariableName(), + MOI.ConstraintName(), + ], +) +``` +""" +function Config( + ::Type{T} = Float64; + atol::Real = Base.rtoldefault(T), + rtol::Real = Base.rtoldefault(T), + supports_optimize::Bool = true, + optimal_status::MOI.TerminationStatusCode = MOI.OPTIMAL, + excluded_attributes::Vector{Any} = Any[], +) where {T<:Real} + return Config{T}( + atol, + rtol, + supports_optimize, + optimal_status, + excluded_attributes, + ) +end + +function Base.copy(config::Config{T}) where {T} + return Config{T}( + config.atol, + config.rtol, + config.supports_optimize, + config.optimal_status, + copy(config.excluded_attributes), + ) +end + +""" + setup_test(::typeof(f), model::MOI.ModelLike, config::Config) + +Overload this method to modify `model` before running the test function `f` on +`model` with `config`. You can also modify the fields in `config` (e.g., to +loosen the default tolerances). + +This function should either return `nothing`, or return a function which, when +called with zero arguments, undoes the setup to return the model to its +previous state. You do not need to undo any modifications to `config`. + +This function is most useful when writing new tests of the tests for MOI, but it +can also be used to set test-specific tolerances, etc. + +See also: [`runtests`](@ref) + +## Example + +```julia +function MOI.Test.setup_test( + ::typeof(MOI.Test.test_linear_VariablePrimalStart_partial), + mock::MOIU.MockOptimizer, + ::MOI.Test.Config, +) + MOIU.set_mock_optimize!( + mock, + (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [1.0, 0.0]), + ) + mock.eval_variable_constraint_dual = false + + function reset_function() + mock.eval_variable_constraint_dual = true + return + end + return reset_function +end +``` +""" +setup_test(::Any, ::MOI.ModelLike, ::Config) = nothing + +""" + runtests( + model::MOI.ModelLike, + config::Config; + include::Vector{String} = String[], + exclude::Vector{String} = String[], + ) + +Run all tests in `MathOptInterface.Test` on `model`. + +## Configuration arguments + + * `config` is a [`Test.Config`](@ref) object that can be used to modify the + behavior of tests. + * If `include` is not empty, only run tests that contain an element from + `include` in their name. + * If `exclude` is not empty, skip tests that contain an element from `exclude` + in their name. + * `exclude` takes priority over `include`. + +See also: [`setup_test`](@ref). + +## Example + +```julia +config = MathOptInterface.Test.Config() +MathOptInterface.Test.runtests( + model, + config; + include = ["test_linear_"], + exclude = ["VariablePrimalStart"], +) +``` +""" +function runtests( + model::MOI.ModelLike, + config::Config; + include::Vector{String} = String[], + exclude::Vector{String} = String[], +) + for name_sym in names(@__MODULE__; all = true) + name = string(name_sym) + if !startswith(name, "test_") + continue # All test functions start with test_ + elseif !isempty(include) && !any(s -> occursin(s, name), include) + continue + elseif !isempty(exclude) && any(s -> occursin(s, name), exclude) + continue + end + @testset "$(name)" begin + test_function = getfield(@__MODULE__, name_sym) + c = copy(config) + tear_down = setup_test(test_function, model, c) + # Make sure to empty the model before every test! + MOI.empty!(model) + test_function(model, c) + if tear_down !== nothing + tear_down() + end + end + end + return +end + +### +### The following are helpful utilities for writing tests in MOI.Test. +### + +""" + Base.isapprox(x, y, config::Config) + +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) +end + +""" + _supports(config::Config, attribute::MOI.AnyAttribute) + +Return `true` if the `attribute` is supported by the `config`. + +This is helpful when writing tests. + +## Example + +```julia +if MOI.Test._supports(config, MOI.Silent()) + @test MOI.get(model, MOI.Silent()) == true +end +``` +""" +function _supports(config::Config, attribute::MOI.AnyAttribute) + return !(attribute in config.excluded_attributes) +end + +""" + _test_model_solution( + model::MOI.ModelLike, + config::Config; + objective_value = nothing, + variable_primal = nothing, + constraint_primal = nothing, + constraint_dual = nothing, + ) + +Solve, and then test, various aspects of a model. + +First, check that `TerminationStatus == MOI.OPTIMAL`. + +If `objective_value` is not nothing, check that the attribute `ObjectiveValue()` +is approximately `objective_value`. + +If `variable_primal` is not nothing, check that the attribute `PrimalStatus` is +`MOI.FEASIBLE_POINT`. Then for each `(index, value)` in `variable_primal`, check +that the primal value of the variable `index` is approximately `value`. + +If `constraint_primal` is not nothing, check that the attribute `PrimalStatus` +is `MOI.FEASIBLE_POINT`. Then for each `(index, value)` in `constraint_primal`, +check that the primal value of the constraint `index` is approximately `value`. + +Finally, if `config.duals = true`, and if `constraint_dual` is not nothing, +check that the attribute `DualStatus` is `MOI.FEASIBLE_POINT`. Then for each +`(index, value)` in `constraint_dual`, check that the dual of the constraint +`index` is approximately `value`. + +### Example + +```julia +MOIU.loadfromstring!(model, \"\"\" + variables: x + minobjective: 2.0x + 1.0 + c: x >= 1.0 +\"\"\") +x = MOI.get(model, MOI.VariableIndex, "x") +c = MOI.get( + model, + MOI.ConstraintIndex{MOI.SingleVariable,MOI.GreaterThan{Float64}}, + "c", +) +_test_model_solution( + model, + config; + objective_value = 3.0, + variable_primal = [(x, 1.0)], + constraint_primal = [(c, 1.0)], + constraint_dual = [(c, 2.0)], +) +``` +""" +function _test_model_solution( + model::MOI.ModelLike, + config::Config{T}; + objective_value = nothing, + variable_primal = nothing, + constraint_primal = nothing, + constraint_dual = nothing, +) where {T} + if !config.supports_optimize + return + end + MOI.optimize!(model) + # No need to check supports. Everyone _must_ implement ObjectiveValue. + @test MOI.get(model, MOI.TerminationStatus()) == config.optimal_status + if objective_value !== nothing && _supports(config, MOI.ObjectiveValue()) + @test isapprox( + MOI.get(model, MOI.ObjectiveValue()), + objective_value, + config, + ) + end + # No need to check supports. Everyone _must_ implement VariablePrimal. + if variable_primal !== nothing + @test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT + for (index, solution_value) in variable_primal + @test isapprox( + MOI.get(model, MOI.VariablePrimal(), index), + solution_value, + config, + ) + end + end + if constraint_primal !== nothing && + _supports(config, MOI.ConstraintPrimal()) + @test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT + for (index, solution_value) in constraint_primal + @test isapprox( + MOI.get(model, MOI.ConstraintPrimal(), index), + solution_value, + config, + ) + end + end + if constraint_dual !== nothing && _supports(config, MOI.ConstraintDual()) + @test MOI.get(model, MOI.DualStatus()) == MOI.FEASIBLE_POINT + for (index, solution_value) in constraint_dual + @test isapprox( + MOI.get(model, MOI.ConstraintDual(), index), + solution_value, + config, + ) + end + end + return +end + +end # module diff --git a/test/Test/Test.jl b/test/Test/Test.jl new file mode 100644 index 0000000000..18be65b164 --- /dev/null +++ b/test/Test/Test.jl @@ -0,0 +1,9 @@ +using MathOptInterface +const MOI = MathOptInterface + +MOI.Test.runtests( + MOI.Utilities.MockOptimizer( + MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()), + ), + MOI.Test.Config(), +) diff --git a/test/runtests.jl b/test/runtests.jl index c07753033b..e511ef0b57 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -17,7 +17,7 @@ end @testset "MOI.$(submodule)" for submodule in [ "Bridges", "FileFormats", - # "Test", + "Test", "Utilities", "Benchmarks", "DeprecatedTest",