Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/src/submodules/Test/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions docs/src/submodules/Test/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ Functions to help test implementations of MOI. See
Test.Config
Test.runtests
Test.setup_test
Test.@requires
Test.RequirementUnmet
```
81 changes: 78 additions & 3 deletions src/Test/Test.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand All @@ -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).

Expand All @@ -153,6 +161,7 @@ MathOptInterface.Test.runtests(
config;
include = ["test_linear_"],
exclude = ["VariablePrimalStart"],
warn_unsupported = true,
)
```
"""
Expand All @@ -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)
Expand All @@ -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)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@blegat I think this is easier than defining every condition that you need to pass the test. Why not just trap UnsupportedConstraint and UnsupportedAttribute errors?

end
if tear_down !== nothing
tear_down()
end
Expand All @@ -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.
###
Expand All @@ -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

"""
Expand Down Expand Up @@ -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
155 changes: 155 additions & 0 deletions src/Test/test_attribute.jl
Original file line number Diff line number Diff line change
@@ -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
Loading