diff --git a/Project.toml b/Project.toml index 1a508d0..5819e93 100644 --- a/Project.toml +++ b/Project.toml @@ -19,7 +19,8 @@ MathOptInterface = "0.9.6" julia = "1" [extras] +Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Test"] +test = ["Test", "Pkg"] diff --git a/bench/runbench.jl b/bench/runbench.jl index 3d0f0d0..c37b064 100644 --- a/bench/runbench.jl +++ b/bench/runbench.jl @@ -36,7 +36,7 @@ function generate_moi_problem(model, At, b, c; else for row in 1:rows MOI.add_constraint(model, MOI.VectorAffineFunction( - [MOI.VectorAffineTerm(1, + [MOI.VectorAffineTerm(1, MOI.ScalarAffineTerm(A_vals[i], x[A_cols[i]]) ) for i in nzrange(At, row)], [-b[row]]), MOI.Nonpositives(1)) @@ -112,7 +112,7 @@ function time_build_and_solve(to_build, to_solve, At, b, c, scalar = true) end @time @timeit "opt" MOI.optimize!(to_solve) MOI.get(to_solve, MOI.ObjectiveValue()) - val = MOI.get(to_solve, MOI.SolveTime()) + val = MOI.get(to_solve, MOI.SolveTimeSec()) println(val) end @@ -145,4 +145,4 @@ function solve_clp(seed, data; time_limit_sec=Inf) end -solve_clp(10, RandomLP(10000, 20000, 0.01); time_limit_sec=5) \ No newline at end of file +solve_clp(10, RandomLP(10000, 20000, 0.01); time_limit_sec=5) diff --git a/src/Clp.jl b/src/Clp.jl index 14e96fe..01b8ff6 100644 --- a/src/Clp.jl +++ b/src/Clp.jl @@ -52,4 +52,9 @@ for sym in names(@__MODULE__, all = true) end end +if VERSION > v"1.4.2" + include("precompile.jl") + _precompile_() +end + end diff --git a/src/MOI_wrapper/MOI_wrapper.jl b/src/MOI_wrapper/MOI_wrapper.jl index 169b348..077d1da 100644 --- a/src/MOI_wrapper/MOI_wrapper.jl +++ b/src/MOI_wrapper/MOI_wrapper.jl @@ -3,6 +3,28 @@ import SparseArrays const MOI = MathOptInterface +MOI.Utilities.@product_of_sets( + _LPProductOfSets, + MOI.EqualTo{T}, + MOI.LessThan{T}, + MOI.GreaterThan{T}, + MOI.Interval{T}, +) + +const OptimizerCache = MOI.Utilities.GenericModel{ + Float64, + MOI.Utilities.MatrixOfConstraints{ + Float64, + MOI.Utilities.MutableSparseMatrixCSC{ + Float64, + Cint, + MOI.Utilities.ZeroBasedIndexing, + }, + MOI.Utilities.Box{Float64}, + _LPProductOfSets{Float64}, + }, +} + # Supported scalar sets const SCALAR_SETS = Union{ MOI.GreaterThan{Float64}, @@ -25,7 +47,7 @@ mutable struct Optimizer <: MOI.AbstractOptimizer Create a new Optimizer object. - Set optimizer attributes using `MOI.RawParameter` or + Set optimizer attributes using `MOI.RawOptimizerAttribute` or `JuMP.set_optimizer_atttribute`. For a list of supported parameter names, see `Clp.SUPPORTED_PARAMETERS`. @@ -41,14 +63,14 @@ mutable struct Optimizer <: MOI.AbstractOptimizer if length(kwargs) > 0 @warn("""Passing optimizer attributes as keyword arguments to Clp.Optimizer is deprecated. Use - MOI.set(model, MOI.RawParameter("key"), value) + MOI.set(model, MOI.RawOptimizerAttribute("key"), value) or JuMP.set_optimizer_attribute(model, "key", value) instead. """) end for (key, value) in kwargs - MOI.set(model, MOI.RawParameter(String(key)), value) + MOI.set(model, MOI.RawOptimizerAttribute(String(key)), value) end finalizer(model) do m Clp_deleteModel(m) @@ -58,6 +80,10 @@ mutable struct Optimizer <: MOI.AbstractOptimizer end end +function MOI.default_cache(::Optimizer, ::Type{Float64}) + return MOI.Utilities.UniversalFallback(OptimizerCache()) +end + Base.cconvert(::Type{Ptr{Cvoid}}, model::Optimizer) = model function Base.unsafe_convert(::Type{Ptr{Cvoid}}, model::Optimizer) return model.inner::Ptr{Cvoid} @@ -75,7 +101,7 @@ end function MOI.empty!(model::Optimizer) # Copy parameters from old model into new model old_options = Dict( - key => MOI.get(model, MOI.RawParameter(key)) for + key => MOI.get(model, MOI.RawOptimizerAttribute(key)) for key in model.options_set ) empty!(model.options_set) @@ -84,7 +110,7 @@ function MOI.empty!(model::Optimizer) model.optimize_called = false model.solve_time = 0.0 for (key, value) in old_options - MOI.set(model, MOI.RawParameter(key), value) + MOI.set(model, MOI.RawOptimizerAttribute(key), value) end # Work-around for maximumSeconds Clp_setMaximumSeconds(model, model.maximumSeconds) @@ -111,11 +137,11 @@ const SUPPORTED_PARAMETERS = ( "InfeasibleReturn", ) -function MOI.supports(::Optimizer, param::MOI.RawParameter) +function MOI.supports(::Optimizer, param::MOI.RawOptimizerAttribute) return param.name in SUPPORTED_PARAMETERS end -function MOI.set(model::Optimizer, param::MOI.RawParameter, value) +function MOI.set(model::Optimizer, param::MOI.RawOptimizerAttribute, value) name = String(param.name) push!(model.options_set, name) if name == "PrimalTolerance" @@ -148,7 +174,7 @@ function MOI.set(model::Optimizer, param::MOI.RawParameter, value) return end -function MOI.get(model::Optimizer, param::MOI.RawParameter) +function MOI.get(model::Optimizer, param::MOI.RawOptimizerAttribute) name = String(param.name) if name == "PrimalTolerance" return Clp_primalTolerance(model) @@ -242,149 +268,55 @@ end # `copy_to` function # ======================= -function _add_bounds(::Vector{Float64}, ub, i, s::MOI.LessThan{Float64}) - return ub[i] = s.upper -end -function _add_bounds(lb, ::Vector{Float64}, i, s::MOI.GreaterThan{Float64}) - return lb[i] = s.lower -end -function _add_bounds(lb, ub, i, s::MOI.EqualTo{Float64}) - return lb[i], ub[i] = s.value, s.value -end -function _add_bounds(lb, ub, i, s::MOI.Interval{Float64}) - return lb[i], ub[i] = s.lower, s.upper -end - -function _extract_bound_data(src, mapping, lb, ub, ::Type{S}) where {S} - for con_index in - MOI.get(src, MOI.ListOfConstraintIndices{MOI.SingleVariable,S}()) - f = MOI.get(src, MOI.ConstraintFunction(), con_index) - s = MOI.get(src, MOI.ConstraintSet(), con_index) - column = mapping.varmap[f.variable].value - _add_bounds(lb, ub, column, s) - mapping.conmap[con_index] = - MOI.ConstraintIndex{MOI.SingleVariable,S}(column) - end -end - -function _copy_to_columns(dest::Optimizer, src, mapping) - x_src = MOI.get(src, MOI.ListOfVariableIndices()) - N = Cint(length(x_src)) - for i in 1:N - mapping.varmap[x_src[i]] = MOI.VariableIndex(i) - end - - fobj = - MOI.get(src, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}()) - c = fill(0.0, N) - for term in fobj.terms - i = mapping.varmap[term.variable_index].value - c[i] += term.coefficient - end - # Clp seems to negates the objective offset - Clp_setObjectiveOffset(dest, -fobj.constant) - return N, c -end - -_bounds(s::MOI.GreaterThan{Float64}) = (s.lower, Inf) -_bounds(s::MOI.LessThan{Float64}) = (-Inf, s.upper) -_bounds(s::MOI.EqualTo{Float64}) = (s.value, s.value) -_bounds(s::MOI.Interval{Float64}) = (s.lower, s.upper) - -function add_sizehint!(vec, n) - len = length(vec) - return sizehint!(vec, len + n) -end +""" + _index_map(src::OptimizerCache) -function _extract_row_data(src, mapping, lb, ub, I, J, V, ::Type{S}) where {S} - row = length(I) == 0 ? 1 : I[end] + 1 - list = MOI.get( - src, - MOI.ListOfConstraintIndices{MOI.ScalarAffineFunction{Float64},S}(), - ) - add_sizehint!(lb, length(list)) - add_sizehint!(ub, length(list)) - n_terms = 0 - fs = Array{MOI.ScalarAffineFunction{Float64}}(undef, length(list)) - for (i, c_index) in enumerate(list) - f = MOI.get(src, MOI.ConstraintFunction(), c_index) - fs[i] = f - l, u = _bounds(MOI.get(src, MOI.ConstraintSet(), c_index)) - push!(lb, l - f.constant) - push!(ub, u - f.constant) - n_terms += length(f.terms) +Create an `IndexMap` mapping the variables and constraints in `OptimizerCache` +to their corresponding 1-based columns and rows. +""" +function _index_map(src::OptimizerCache) + index_map = MOI.Utilities.IndexMap() + for (i, x) in enumerate(MOI.get(src, MOI.ListOfVariableIndices())) + index_map[x] = MOI.VariableIndex(i) end - add_sizehint!(I, n_terms) - add_sizehint!(J, n_terms) - add_sizehint!(V, n_terms) - for (i, c_index) in enumerate(list) - f = fs[i]#MOI.get(src, MOI.ConstraintFunction(), c_index) - for term in f.terms - push!(I, row) - push!(J, Cint(mapping.varmap[term.variable_index].value)) - push!(V, term.coefficient) + for (F, S) in MOI.get(src, MOI.ListOfConstraintTypesPresent()) + for ci in MOI.get(src, MOI.ListOfConstraintIndices{F,S}()) + if F == MOI.SingleVariable + col = index_map[MOI.VariableIndex(ci.value)].value + index_map[ci] = MOI.ConstraintIndex{F,S}(col) + else + row = MOI.Utilities.rows(src.constraints, ci) + index_map[ci] = MOI.ConstraintIndex{F,S}(row) + end end - mapping.conmap[c_index] = - MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64},S}(row) - row += 1 end - return + return index_map end -function test_data(src, dest) - for (F, S) in MOI.get(src, MOI.ListOfConstraints()) - if !MOI.supports_constraint(dest, F, S) - throw( - MOI.UnsupportedConstraint{F,S}( - "Clp.Optimizer does not support constraints of type $F-in-$S.", - ), - ) - end - end - fobj_type = MOI.get(src, MOI.ObjectiveFunctionType()) - if !MOI.supports(dest, MOI.ObjectiveFunction{fobj_type}()) - throw(MOI.UnsupportedAttribute(MOI.ObjectiveFunction(fobj_type))) +function _copy_to(dest::Optimizer, src::OptimizerCache) + MOI.empty!(dest) + A = src.constraints.coefficients + row_bounds = src.constraints.constants + obj = + MOI.get(src, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}()) + c = zeros(A.n) + for term in obj.terms + c[term.variable.value] += term.coefficient end -end - -function MOI.copy_to( - dest::Optimizer, - src::MOI.ModelLike; - copy_names::Bool = false, -) - @assert MOI.is_empty(dest) - test_data(src, dest) - - mapping = MOI.Utilities.IndexMap() - N, c = _copy_to_columns(dest, src, mapping) - cl, cu = fill(-Inf, N), fill(Inf, N) - rl, ru, I, J, V = Float64[], Float64[], Cint[], Cint[], Float64[] - - _extract_bound_data(src, mapping, cl, cu, MOI.GreaterThan{Float64}) - _extract_row_data(src, mapping, rl, ru, I, J, V, MOI.GreaterThan{Float64}) - _extract_bound_data(src, mapping, cl, cu, MOI.LessThan{Float64}) - _extract_row_data(src, mapping, rl, ru, I, J, V, MOI.LessThan{Float64}) - _extract_bound_data(src, mapping, cl, cu, MOI.EqualTo{Float64}) - _extract_row_data(src, mapping, rl, ru, I, J, V, MOI.EqualTo{Float64}) - _extract_bound_data(src, mapping, cl, cu, MOI.Interval{Float64}) - _extract_row_data(src, mapping, rl, ru, I, J, V, MOI.Interval{Float64}) - - M = Cint(length(rl)) - A = SparseArrays.sparse(I, J, V, M, N) + Clp_setObjectiveOffset(dest, -obj.constant) Clp_loadProblem( dest, A.n, A.m, - A.colptr .- Cint(1), - A.rowval .- Cint(1), + A.colptr, + A.rowval, A.nzval, - cl, - cu, + src.variable_bounds.lower, + src.variable_bounds.upper, c, - rl, - ru, + row_bounds.lower, + row_bounds.upper, ) - sense = MOI.get(src, MOI.ObjectiveSense()) if sense == MOI.MIN_SENSE Clp_setObjSense(dest, 1) @@ -394,7 +326,41 @@ function MOI.copy_to( @assert sense == MOI.FEASIBILITY_SENSE Clp_setObjSense(dest, 0) end - return mapping + return _index_map(src) +end + +function MOI.copy_to( + dest::Optimizer, + src::OptimizerCache; + copy_names::Bool = false, +) + return _copy_to(dest, src) +end + +function MOI.copy_to( + dest::Optimizer, + src::MOI.Utilities.UniversalFallback{OptimizerCache}; + copy_names::Bool = false, +) + return MOI.copy_to(dest, src.model) +end + +function MOI.copy_to( + dest::Optimizer, + src::MOI.ModelLike; + copy_names::Bool = false, +) + cache = OptimizerCache() + src_cache = MOI.copy_to(cache, src) + cache_dest = _copy_to(dest, cache) + index_map = MOI.Utilities.IndexMap() + for (src_x, cache_x) in src_cache.var_map + index_map[src_x] = cache_dest[cache_x] + end + for (src_ci, cache_ci) in src_cache.con_map + index_map[src_ci] = cache_dest[cache_ci] + end + return index_map end # =============================== @@ -409,7 +375,7 @@ function MOI.optimize!(model::Optimizer) return end -function MOI.get(model::Optimizer, ::MOI.SolveTime) +function MOI.get(model::Optimizer, ::MOI.SolveTimeSec) return model.solve_time end @@ -475,7 +441,7 @@ function MOI.get(model::Optimizer, ::MOI.ResultCount) end function MOI.get(model::Optimizer, attr::MOI.PrimalStatus) - if attr.N != 1 + if attr.result_index != 1 return MOI.NO_SOLUTION elseif Clp_isProvenDualInfeasible(model) != 0 return MOI.INFEASIBILITY_CERTIFICATE @@ -487,7 +453,7 @@ function MOI.get(model::Optimizer, attr::MOI.PrimalStatus) end function MOI.get(model::Optimizer, attr::MOI.DualStatus) - if attr.N != 1 + if attr.result_index != 1 return MOI.NO_SOLUTION elseif Clp_isProvenPrimalInfeasible(model) != 0 return MOI.INFEASIBILITY_CERTIFICATE @@ -759,14 +725,9 @@ end function MOI.get( model::Optimizer, - ::MOI.ConstraintBasisStatus, - c::MOI.ConstraintIndex{MOI.SingleVariable,S}, -) where {S} - code = Clp_getColumnStatus(model, c.value - 1) - status = _CLP_BASIS_STATUS[code] - if status == MOI.NONBASIC_AT_UPPER || status == MOI.NONBASIC_AT_LOWER - return _nonbasic_status(status, S) - else - return status - end + ::MOI.VariableBasisStatus, + vi::MOI.VariableIndex, +) + code = Clp_getColumnStatus(model, vi.value - 1) + return _CLP_BASIS_STATUS[code] end diff --git a/src/precompile.jl b/src/precompile.jl new file mode 100644 index 0000000..3c50fa8 --- /dev/null +++ b/src/precompile.jl @@ -0,0 +1,27 @@ +function _fake_precompile() + cache = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()) + clp = MOI.instantiate(Clp.Optimizer, with_bridge_type = Float64) + MOI.copy_to(clp, cache; copy_names = false) + return clp +end + +function _precompile_() + ccall(:jl_generating_output, Cint, ()) == 1 || return nothing + T = Float64 + scalar_sets = ( + MOI.LessThan{T}, + MOI.GreaterThan{T}, + MOI.EqualTo{T}, + MOI.Interval{T}, + ) + scalar_functions = (MOI.SingleVariable, MOI.ScalarAffineFunction{T}) + MOI.precompile_model( + MOI.Bridges.LazyBridgeOptimizer{MOI.Utilities.CachingOptimizer{ + Optimizer, + MOI.Utilities.UniversalFallback{MOI.Utilities.Model{Float64}}, + }}, + [(F, S) for F in scalar_functions, S in scalar_sets], + ) + @assert Base.precompile(_fake_precompile, ()) + return +end diff --git a/test/MOI_wrapper.jl b/test/MOI_wrapper.jl index 1a05d70..b850b92 100644 --- a/test/MOI_wrapper.jl +++ b/test/MOI_wrapper.jl @@ -9,14 +9,15 @@ const MOI = MathOptInterface const OPTIMIZER = Clp.Optimizer() MOI.set(OPTIMIZER, MOI.Silent(), true) +const BRIDGED = MOI.instantiate(Clp.Optimizer, with_bridge_type=Float64) +MOI.set(BRIDGED, MOI.Silent(), true) + const CACHED = MOI.Utilities.CachingOptimizer( MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()), - OPTIMIZER, + BRIDGED, ) -const BRIDGED = MOI.Bridges.full_bridge_optimizer(CACHED, Float64) - -const CONFIG = MOI.Test.TestConfig(dual_objective_value = false, basis = true) +const CONFIG = MOI.DeprecatedTest.Config(dual_objective_value = false, basis = true) function test_SolverName() @test MOI.get(OPTIMIZER, MOI.SolverName()) == "Clp" @@ -25,17 +26,24 @@ end function test_supports_default_copy_to() @test !MOI.Utilities.supports_allocate_load(OPTIMIZER, false) @test !MOI.Utilities.supports_allocate_load(OPTIMIZER, true) - @test !MOI.Utilities.supports_default_copy_to(OPTIMIZER, false) - @test !MOI.Utilities.supports_default_copy_to(OPTIMIZER, true) + @test !MOI.supports_incremental_interface(OPTIMIZER, false) + @test !MOI.supports_incremental_interface(OPTIMIZER, true) +end + +function test_cache() + @test BRIDGED.model.model_cache isa MOI.Utilities.UniversalFallback{Clp.OptimizerCache} end function test_basicconstraint() - return MOI.Test.basic_constraint_tests(CACHED, CONFIG) + return MOI.DeprecatedTest.basic_constraint_tests(CACHED, CONFIG) end function test_unittest() - return MOI.Test.unittest( - BRIDGED, + # `CACHED` may be in `EMPTY_OPTIMIZER` state while `BRIDGED` was modified + # by some other test + MOI.empty!(BRIDGED) + return MOI.DeprecatedTest.unittest( + CACHED, CONFIG, [ # Not supported by upstream. @@ -57,22 +65,25 @@ function test_unittest() end function test_contlinear() - return MOI.Test.contlineartest(BRIDGED, CONFIG, [ + # `CACHED` may be in `EMPTY_OPTIMIZER` state while `BRIDGED` was modified + # by some other test + MOI.empty!(BRIDGED) + return MOI.DeprecatedTest.contlineartest(CACHED, CONFIG, [ # MOI.VariablePrimalStart not supported. "partial_start", ]) end function test_nametest() - return MOI.Test.nametest(BRIDGED) + return MOI.DeprecatedTest.nametest(BRIDGED, delete=false) end function test_validtest() - return MOI.Test.validtest(BRIDGED) + return MOI.DeprecatedTest.validtest(CACHED) end function test_emptytest() - return MOI.Test.emptytest(BRIDGED) + return MOI.DeprecatedTest.emptytest(BRIDGED) end function test_Nonexistant_unbounded_ray() @@ -89,41 +100,35 @@ function test_Nonexistant_unbounded_ray() @test status == MOI.DUAL_INFEASIBLE end -function test_RawParameter() +function test_RawOptimizerAttribute() model = Clp.Optimizer() - MOI.set(model, MOI.RawParameter("LogLevel"), 1) - @test MOI.get(model, MOI.RawParameter("LogLevel")) == 1 - @test MOI.get(model, MOI.RawParameter(:LogLevel)) == 1 - MOI.set(model, MOI.RawParameter(:LogLevel), 2) - @test MOI.get(model, MOI.RawParameter("LogLevel")) == 2 - @test MOI.get(model, MOI.RawParameter(:LogLevel)) == 2 - - MOI.set(model, MOI.RawParameter("SolveType"), 1) - @test MOI.get(model, MOI.RawParameter("SolveType")) == 1 - @test MOI.get(model, MOI.RawParameter(:SolveType)) == 1 - MOI.set(model, MOI.RawParameter("SolveType"), 4) - @test MOI.get(model, MOI.RawParameter("SolveType")) == 4 - @test MOI.get(model, MOI.RawParameter(:SolveType)) == 4 - - MOI.set(model, MOI.RawParameter("PresolveType"), 1) - @test MOI.get(model, MOI.RawParameter("PresolveType")) == 1 - @test MOI.get(model, MOI.RawParameter(:PresolveType)) == 1 - MOI.set(model, MOI.RawParameter("PresolveType"), 0) - @test MOI.get(model, MOI.RawParameter("PresolveType")) == 0 - @test MOI.get(model, MOI.RawParameter(:PresolveType)) == 0 + MOI.set(model, MOI.RawOptimizerAttribute("LogLevel"), 1) + @test MOI.get(model, MOI.RawOptimizerAttribute("LogLevel")) == 1 + MOI.set(model, MOI.RawOptimizerAttribute("LogLevel"), 2) + @test MOI.get(model, MOI.RawOptimizerAttribute("LogLevel")) == 2 + + MOI.set(model, MOI.RawOptimizerAttribute("SolveType"), 1) + @test MOI.get(model, MOI.RawOptimizerAttribute("SolveType")) == 1 + MOI.set(model, MOI.RawOptimizerAttribute("SolveType"), 4) + @test MOI.get(model, MOI.RawOptimizerAttribute("SolveType")) == 4 + + MOI.set(model, MOI.RawOptimizerAttribute("PresolveType"), 1) + @test MOI.get(model, MOI.RawOptimizerAttribute("PresolveType")) == 1 + MOI.set(model, MOI.RawOptimizerAttribute("PresolveType"), 0) + @test MOI.get(model, MOI.RawOptimizerAttribute("PresolveType")) == 0 end function test_All_parameters() model = Clp.Optimizer() - param = MOI.RawParameter("NotAnOption") + param = MOI.RawOptimizerAttribute("NotAnOption") @test !MOI.supports(model, param) @test_throws MOI.UnsupportedAttribute(param) MOI.get(model, param) @test_throws MOI.UnsupportedAttribute(param) MOI.set(model, param, false) for key in Clp.SUPPORTED_PARAMETERS - @test MOI.supports(model, MOI.RawParameter(key)) - value = MOI.get(model, MOI.RawParameter(key)) - MOI.set(model, MOI.RawParameter(key), value) - @test MOI.get(model, MOI.RawParameter(key)) == value + @test MOI.supports(model, MOI.RawOptimizerAttribute(key)) + value = MOI.get(model, MOI.RawOptimizerAttribute(key)) + MOI.set(model, MOI.RawOptimizerAttribute(key), value) + @test MOI.get(model, MOI.RawOptimizerAttribute(key)) == value end end @@ -330,7 +335,6 @@ function test_farkas_dual_max_ii() @test MOI.get(model, MOI.DualStatus()) == MOI.INFEASIBILITY_CERTIFICATE clb_dual = MOI.get.(model, MOI.ConstraintDual(), clb) c_dual = MOI.get(model, MOI.ConstraintDual(), c) - @show clb_dual, c_dual @test clb_dual[1] < -1e-6 @test clb_dual[2] < -1e-6 @test c_dual[1] < -1e-6 diff --git a/test/runtests.jl b/test/runtests.jl index 6db7034..cd80fc3 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,3 +1,8 @@ +if get(ENV, "GITHUB_ACTIONS", "") == "true" + import Pkg + Pkg.add(Pkg.PackageSpec(name = "MathOptInterface", rev = "master")) +end + using Test function runtests(mod)