math_opt: Add Matchers

still missing EqualsProto
This commit is contained in:
Corentin Le Molgat
2024-02-15 19:51:10 +01:00
parent 18b93a32bd
commit 34a524913d
4 changed files with 1539 additions and 0 deletions

View File

@@ -412,6 +412,28 @@ cc_library(
],
)
cc_library(
name = "matchers",
testonly = 1,
srcs = ["matchers.cc"],
hdrs = ["matchers.h"],
visibility = ["//visibility:public"],
deps = [
":linear_constraint",
":math_opt",
":update_result",
":variable_and_expressions",
"//ortools/base:logging",
"@com_google_absl//absl/container:flat_hash_map",
"@com_google_absl//absl/log",
"@com_google_absl//absl/log:check",
"@com_google_absl//absl/status:statusor",
"@com_google_absl//absl/strings",
"@com_google_absl//absl/types:span",
"@com_google_googletest//:gtest",
],
)
cc_library(
name = "enums",
hdrs = ["enums.h"],

View File

@@ -15,6 +15,8 @@ set(NAME ${PROJECT_NAME}_math_opt_cpp)
add_library(${NAME} OBJECT)
file(GLOB _SRCS "*.h" "*.cc")
list(FILTER _SRCS EXCLUDE REGEX "/matchers\\.")
target_sources(${NAME} PRIVATE ${_SRCS})
set_target_properties(${NAME} PROPERTIES POSITION_INDEPENDENT_CODE ON)
target_include_directories(${NAME} PUBLIC

View File

@@ -0,0 +1,966 @@
// Copyright 2010-2024 Google LLC
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "ortools/math_opt/cpp/matchers.h"
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <optional>
#include <ostream>
#include <sstream>
#include <string>
#include <utility>
#include <vector>
#include "absl/container/flat_hash_map.h"
#include "absl/log/check.h"
#include "absl/strings/str_cat.h"
#include "absl/types/span.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "ortools/base/logging.h"
#include "ortools/math_opt/cpp/math_opt.h"
#include "ortools/math_opt/cpp/update_result.h"
#include "ortools/math_opt/cpp/variable_and_expressions.h"
namespace operations_research {
namespace math_opt {
namespace {
using ::testing::AllOf;
using ::testing::AllOfArray;
using ::testing::AnyOf;
using ::testing::AnyOfArray;
using ::testing::Contains;
using ::testing::DoubleNear;
using ::testing::Eq;
using ::testing::ExplainMatchResult;
using ::testing::Field;
using ::testing::IsEmpty;
using ::testing::Matcher;
using ::testing::MatcherInterface;
using ::testing::MatchResultListener;
using ::testing::Optional;
using ::testing::PrintToString;
using ::testing::Property;
} // namespace
////////////////////////////////////////////////////////////////////////////////
// Printing
////////////////////////////////////////////////////////////////////////////////
namespace {
template <typename T>
struct Printer {
explicit Printer(const T& t) : value(t) {}
const T& value;
friend std::ostream& operator<<(std::ostream& os, const Printer& printer) {
os << PrintToString(printer.value);
return os;
}
};
template <typename T>
Printer<T> Print(const T& t) {
return Printer<T>(t);
}
} // namespace
void PrintTo(const PrimalSolution& primal_solution, std::ostream* const os) {
*os << "{variable_values: " << Print(primal_solution.variable_values)
<< ", objective_value: " << Print(primal_solution.objective_value)
<< ", feasibility_status: " << Print(primal_solution.feasibility_status)
<< "}";
}
void PrintTo(const DualSolution& dual_solution, std::ostream* const os) {
*os << "{dual_values: " << Print(dual_solution.dual_values)
<< ", reduced_costs: " << Print(dual_solution.reduced_costs)
<< ", objective_value: " << Print(dual_solution.objective_value)
<< ", feasibility_status: " << Print(dual_solution.feasibility_status)
<< "}";
}
void PrintTo(const PrimalRay& primal_ray, std::ostream* const os) {
*os << "{variable_values: " << Print(primal_ray.variable_values) << "}";
}
void PrintTo(const DualRay& dual_ray, std::ostream* const os) {
*os << "{dual_values: " << Print(dual_ray.dual_values)
<< ", reduced_costs: " << Print(dual_ray.reduced_costs) << "}";
}
void PrintTo(const Basis& basis, std::ostream* const os) {
*os << "{variable_status: " << Print(basis.variable_status)
<< ", constraint_status: " << Print(basis.constraint_status)
<< ", basic_dual_feasibility: " << Print(basis.basic_dual_feasibility)
<< "}";
}
void PrintTo(const Solution& solution, std::ostream* const os) {
*os << "{primal_solution: " << Print(solution.primal_solution)
<< ", dual_solution: " << Print(solution.dual_solution)
<< ", basis: " << Print(solution.basis) << "}";
}
void PrintTo(const SolveResult& result, std::ostream* const os) {
*os << "{termination: " << Print(result.termination)
<< ", solve_stats: " << Print(result.solve_stats)
<< ", solutions: " << Print(result.solutions)
<< ", primal_rays: " << Print(result.primal_rays)
<< ", dual_rays: " << Print(result.dual_rays) << "}";
}
////////////////////////////////////////////////////////////////////////////////
// absl::flat_hash_map<K, double> Matchers
////////////////////////////////////////////////////////////////////////////////
namespace {
template <typename K>
class MapToDoubleMatcher
: public MatcherInterface<absl::flat_hash_map<K, double>> {
public:
MapToDoubleMatcher(absl::flat_hash_map<K, double> expected,
const bool all_keys, const double tolerance)
: expected_(std::move(expected)),
all_keys_(all_keys),
tolerance_(tolerance) {
for (const auto [k, v] : expected_) {
CHECK(!std::isnan(v)) << "Illegal NaN for key: " << k;
}
}
bool MatchAndExplain(absl::flat_hash_map<K, double> actual,
MatchResultListener* const os) const override {
for (const auto& [key, value] : expected_) {
if (!actual.contains(key)) {
*os << "expected key " << key << " not found";
return false;
}
if (!(std::abs(value - actual.at(key)) <= tolerance_)) {
*os << "value for key " << key
<< " not within tolerance, expected: " << value
<< " but found: " << actual.at(key);
return false;
}
}
// Post condition: expected_ is a subset of actual.
if (all_keys_ && expected_.size() != actual.size()) {
for (const auto& [key, value] : actual) {
if (!expected_.contains(key)) {
*os << "found unexpected key " << key << " in actual";
return false;
}
}
// expected_ subset of actual && expected_.size() != actual.size() implies
// that there is a member A of actual not in expected. When the loop above
// hits A, it will return, thus this line is unreachable.
LOG(FATAL) << "unreachable";
}
return true;
}
void DescribeTo(std::ostream* const os) const override {
if (all_keys_) {
*os << "has identical keys to ";
} else {
*os << "keys are contained in ";
}
PrintTo(expected_, os);
*os << " and values within " << tolerance_;
}
void DescribeNegationTo(std::ostream* const os) const override {
if (all_keys_) {
*os << "either keys differ from ";
} else {
*os << "either has a key not in ";
}
PrintTo(expected_, os);
*os << " or a value differs by more than " << tolerance_;
}
private:
const absl::flat_hash_map<K, double> expected_;
const bool all_keys_;
const double tolerance_;
};
} // namespace
Matcher<VariableMap<double>> IsNearlySubsetOf(VariableMap<double> expected,
double tolerance) {
return Matcher<VariableMap<double>>(new MapToDoubleMatcher<Variable>(
std::move(expected), /*all_keys=*/false, tolerance));
}
Matcher<VariableMap<double>> IsNear(VariableMap<double> expected,
const double tolerance) {
return Matcher<VariableMap<double>>(new MapToDoubleMatcher<Variable>(
std::move(expected), /*all_keys=*/true, tolerance));
}
Matcher<LinearConstraintMap<double>> IsNearlySubsetOf(
LinearConstraintMap<double> expected, double tolerance) {
return Matcher<LinearConstraintMap<double>>(
new MapToDoubleMatcher<LinearConstraint>(std::move(expected),
/*all_keys=*/false, tolerance));
}
Matcher<LinearConstraintMap<double>> IsNear(
LinearConstraintMap<double> expected, const double tolerance) {
return Matcher<LinearConstraintMap<double>>(
new MapToDoubleMatcher<LinearConstraint>(std::move(expected),
/*all_keys=*/true, tolerance));
}
template <typename K>
Matcher<absl::flat_hash_map<K, double>> IsNear(
absl::flat_hash_map<K, double> expected, const double tolerance) {
return Matcher<absl::flat_hash_map<K, double>>(new MapToDoubleMatcher<K>(
std::move(expected), /*all_keys=*/true, tolerance));
}
template <typename K>
Matcher<absl::flat_hash_map<K, double>> IsNearlySubsetOf(
absl::flat_hash_map<K, double> expected, const double tolerance) {
return Matcher<absl::flat_hash_map<K, double>>(new MapToDoubleMatcher<K>(
std::move(expected), /*all_keys=*/false, tolerance));
}
////////////////////////////////////////////////////////////////////////////////
// Matchers for LinearExpression and QuadraticExpression
////////////////////////////////////////////////////////////////////////////////
testing::Matcher<LinearExpression> IsIdentical(LinearExpression expected) {
return LinearExpressionIsNear(expected, 0.0);
}
testing::Matcher<LinearExpression> LinearExpressionIsNear(
const LinearExpression expected, const double tolerance) {
CHECK(!std::isnan(expected.offset())) << "Illegal NaN-valued offset";
return AllOf(
Property("storage", &LinearExpression::storage, Eq(expected.storage())),
Property("offset", &LinearExpression::offset,
testing::DoubleNear(expected.offset(), tolerance)),
Property("terms", &LinearExpression::terms,
IsNear(expected.terms(), tolerance)));
}
namespace {
testing::Matcher<BoundedLinearExpression> IsNearForSign(
const BoundedLinearExpression& expected, const double tolerance) {
return AllOf(Property("upper_bound_minus_offset",
&BoundedLinearExpression::upper_bound_minus_offset,
testing::DoubleNear(expected.upper_bound_minus_offset(),
tolerance)),
Property("lower_bound_minus_offset",
&BoundedLinearExpression::lower_bound_minus_offset,
testing::DoubleNear(expected.lower_bound_minus_offset(),
tolerance)),
Field("expression", &BoundedLinearExpression::expression,
Property("terms", &LinearExpression::terms,
IsNear(expected.expression.terms(), tolerance))));
}
} // namespace
testing::Matcher<BoundedLinearExpression> IsNearlyEquivalent(
const BoundedLinearExpression& expected, const double tolerance) {
const BoundedLinearExpression expected_negation(
-expected.expression, /*lower_bound=*/-expected.upper_bound,
/*upper_bound=*/-expected.lower_bound);
return AnyOf(IsNearForSign(expected, tolerance),
IsNearForSign(expected_negation, tolerance));
}
testing::Matcher<QuadraticExpression> IsIdentical(
QuadraticExpression expected) {
CHECK(!std::isnan(expected.offset())) << "Illegal NaN-valued offset";
return AllOf(
Property("storage", &QuadraticExpression::storage,
Eq(expected.storage())),
Property("offset", &QuadraticExpression::offset,
testing::Eq(expected.offset())),
Property("linear_terms", &QuadraticExpression::linear_terms,
IsNear(expected.linear_terms(), /*tolerance=*/0)),
Property("quadratic_terms", &QuadraticExpression::quadratic_terms,
IsNear(expected.quadratic_terms(), /*tolerance=*/0)));
}
////////////////////////////////////////////////////////////////////////////////
// Matcher helpers
////////////////////////////////////////////////////////////////////////////////
namespace {
template <typename RayType>
class RayMatcher : public MatcherInterface<RayType> {
public:
RayMatcher(RayType expected, const double tolerance)
: expected_(std::move(expected)), tolerance_(tolerance) {}
void DescribeTo(std::ostream* os) const final {
*os << "after L_inf normalization, is within tolerance: " << tolerance_
<< " of expected: ";
PrintTo(expected_, os);
}
void DescribeNegationTo(std::ostream* const os) const final {
*os << "after L_inf normalization, is not within tolerance: " << tolerance_
<< " of expected: ";
PrintTo(expected_, os);
}
protected:
const RayType expected_;
const double tolerance_;
};
// Alias to use the std::optional templated adaptor.
Matcher<double> IsNear(double expected, const double tolerance) {
return DoubleNear(expected, tolerance);
}
template <typename Type>
Matcher<std::optional<Type>> IsNear(std::optional<Type> expected,
const double tolerance) {
if (expected.has_value()) {
return Optional(IsNear(*expected, tolerance));
}
return testing::Eq(std::nullopt);
}
template <typename Type>
Matcher<std::optional<Type>> IsNear(std::optional<Type> expected,
const double tolerance,
const bool allow_undetermined) {
if (expected.has_value()) {
return Optional(IsNear(*expected, tolerance, allow_undetermined));
}
return testing::Eq(std::nullopt);
}
// Custom std::optional for basis.
Matcher<std::optional<Basis>> BasisIs(const std::optional<Basis>& expected) {
if (expected.has_value()) {
return Optional(BasisIs(*expected));
}
return testing::Eq(std::nullopt);
}
testing::Matcher<std::vector<Solution>> IsNear(
const std::vector<Solution>& expected_solutions,
const SolutionMatcherOptions options) {
if (expected_solutions.empty()) {
return IsEmpty();
}
std::vector<Matcher<Solution>> matchers;
for (const Solution& sol : expected_solutions) {
matchers.push_back(IsNear(sol, options));
}
return ::testing::ElementsAreArray(matchers);
}
} // namespace
////////////////////////////////////////////////////////////////////////////////
// Matchers for Solutions
////////////////////////////////////////////////////////////////////////////////
Matcher<SolutionStatus> SolutionStatusIs(const SolutionStatus expected,
const bool allow_undetermined) {
if (allow_undetermined) {
return AnyOf(Eq(expected), Eq(SolutionStatus::kUndetermined));
}
return Eq(expected);
}
Matcher<PrimalSolution> IsNear(PrimalSolution expected, const double tolerance,
const bool allow_undetermined) {
return AllOf(
Field("variable_values", &PrimalSolution::variable_values,
IsNear(expected.variable_values, tolerance)),
Field("objective_value", &PrimalSolution::objective_value,
IsNear(expected.objective_value, tolerance)),
Field("feasibility_status", &PrimalSolution::feasibility_status,
SolutionStatusIs(expected.feasibility_status, allow_undetermined)));
}
Matcher<DualSolution> IsNear(DualSolution expected, const double tolerance,
const bool allow_undetermined) {
return AllOf(
Field("dual_values", &DualSolution::dual_values,
IsNear(expected.dual_values, tolerance)),
Field("reduced_costs", &DualSolution::reduced_costs,
IsNear(expected.reduced_costs, tolerance)),
Field("objective_value", &DualSolution::objective_value,
IsNear(expected.objective_value, tolerance)),
Field("feasibility_status", &DualSolution::feasibility_status,
SolutionStatusIs(expected.feasibility_status, allow_undetermined)));
}
Matcher<Basis> BasisIs(const Basis& expected) {
return AllOf(Field("variable_status", &Basis::variable_status,
expected.variable_status),
Field("constraint_status", &Basis::constraint_status,
expected.constraint_status),
Field("basic_dual_feasibility", &Basis::basic_dual_feasibility,
expected.basic_dual_feasibility));
}
Matcher<Solution> IsNear(Solution expected,
const SolutionMatcherOptions options) {
std::vector<Matcher<Solution>> to_check;
if (options.check_primal) {
to_check.push_back(Field("primal_solution", &Solution::primal_solution,
IsNear(expected.primal_solution, options.tolerance,
options.allow_undetermined)));
}
if (options.check_dual) {
to_check.push_back(Field("dual_solution", &Solution::dual_solution,
IsNear(expected.dual_solution, options.tolerance,
options.allow_undetermined)));
}
if (options.check_basis) {
to_check.push_back(
Field("basis", &Solution::basis, BasisIs(expected.basis)));
}
return AllOfArray(to_check);
}
////////////////////////////////////////////////////////////////////////////////
// Primal Ray Matcher
////////////////////////////////////////////////////////////////////////////////
namespace {
template <typename K>
double InfinityNorm(const absl::flat_hash_map<K, double>& vector) {
double infinity_norm = 0.0;
for (auto [id, value] : vector) {
infinity_norm = std::max(infinity_norm, std::abs(value));
}
return infinity_norm;
}
// Returns a normalized primal ray.
//
// The normalization is done using infinity norm:
//
// ray / ||ray||_inf
//
// If the input ray norm is zero, the ray is returned unchanged.
PrimalRay NormalizePrimalRay(PrimalRay ray) {
const double norm = InfinityNorm(ray.variable_values);
if (norm != 0.0) {
for (auto& entry : ray.variable_values) {
entry.second /= norm;
}
}
return ray;
}
class PrimalRayMatcher : public RayMatcher<PrimalRay> {
public:
PrimalRayMatcher(PrimalRay expected, const double tolerance)
: RayMatcher(std::move(expected), tolerance) {}
bool MatchAndExplain(PrimalRay actual,
MatchResultListener* const os) const override {
auto normalized_actual = NormalizePrimalRay(actual);
auto normalized_expected = NormalizePrimalRay(expected_);
if (os->IsInterested()) {
*os << "actual normalized: " << PrintToString(normalized_actual)
<< ", expected normalized: " << PrintToString(normalized_expected);
}
return ExplainMatchResult(
IsNear(normalized_expected.variable_values, tolerance_),
normalized_actual.variable_values, os);
}
};
} // namespace
Matcher<PrimalRay> IsNear(PrimalRay expected, const double tolerance) {
return Matcher<PrimalRay>(
new PrimalRayMatcher(std::move(expected), tolerance));
}
Matcher<PrimalRay> PrimalRayIsNear(VariableMap<double> expected_var_values,
const double tolerance) {
PrimalRay expected;
expected.variable_values = std::move(expected_var_values);
return IsNear(expected, tolerance);
}
////////////////////////////////////////////////////////////////////////////////
// Dual Ray Matcher
////////////////////////////////////////////////////////////////////////////////
namespace {
// Returns a normalized dual ray.
//
// The normalization is done using infinity norm:
//
// ray / ||ray||_inf
//
// If the input ray norm is zero, the ray is returned unchanged.
DualRay NormalizeDualRay(DualRay ray) {
const double norm =
std::max(InfinityNorm(ray.dual_values), InfinityNorm(ray.reduced_costs));
if (norm != 0.0) {
for (auto& entry : ray.dual_values) {
entry.second /= norm;
}
for (auto& entry : ray.reduced_costs) {
entry.second /= norm;
}
}
return ray;
}
class DualRayMatcher : public RayMatcher<DualRay> {
public:
DualRayMatcher(DualRay expected, const double tolerance)
: RayMatcher(std::move(expected), tolerance) {}
bool MatchAndExplain(DualRay actual, MatchResultListener* os) const override {
auto normalized_actual = NormalizeDualRay(actual);
auto normalized_expected = NormalizeDualRay(expected_);
if (os->IsInterested()) {
*os << "actual normalized: " << PrintToString(normalized_actual)
<< ", expected normalized: " << PrintToString(normalized_expected);
}
return ExplainMatchResult(
IsNear(normalized_expected.dual_values, tolerance_),
normalized_actual.dual_values, os) &&
ExplainMatchResult(
IsNear(normalized_expected.reduced_costs, tolerance_),
normalized_actual.reduced_costs, os);
}
};
} // namespace
Matcher<DualRay> IsNear(DualRay expected, const double tolerance) {
return Matcher<DualRay>(new DualRayMatcher(std::move(expected), tolerance));
}
////////////////////////////////////////////////////////////////////////////////
// SolveResult termination reason matchers
////////////////////////////////////////////////////////////////////////////////
Matcher<ObjectiveBounds> ObjectiveBoundsNear(const ObjectiveBounds& expected,
const double tolerance) {
return AllOf(Field("primal_bound", &ObjectiveBounds::primal_bound,
DoubleNear(expected.primal_bound, tolerance)),
Field("dual_bound", &ObjectiveBounds::dual_bound,
DoubleNear(expected.dual_bound, tolerance)));
}
Matcher<SolveResult> TerminatesWithOneOf(
const std::vector<TerminationReason>& allowed) {
return Field("termination", &SolveResult::termination,
Field("reason", &Termination::reason, AnyOfArray(allowed)));
}
Matcher<SolveResult> TerminatesWith(const TerminationReason expected) {
return Field("termination", &SolveResult::termination,
Field("reason", &Termination::reason, expected));
}
namespace {
testing::Matcher<SolveResult> LimitIs(const Limit expected,
const bool allow_limit_undetermined) {
if (allow_limit_undetermined) {
return Field("termination", &SolveResult::termination,
Field("limit", &Termination::limit,
AnyOf(Limit::kUndetermined, expected)));
}
return Field("termination", &SolveResult::termination,
Field("limit", &Termination::limit, expected));
}
} // namespace
testing::Matcher<SolveResult> TerminatesWithLimit(
const Limit expected, const bool allow_limit_undetermined) {
std::vector<Matcher<SolveResult>> matchers;
matchers.push_back(LimitIs(expected, allow_limit_undetermined));
matchers.push_back(TerminatesWithOneOf(
{TerminationReason::kFeasible, TerminationReason::kNoSolutionFound}));
return AllOfArray(matchers);
}
testing::Matcher<SolveResult> TerminatesWithReasonFeasible(
const Limit expected, const bool allow_limit_undetermined) {
std::vector<Matcher<SolveResult>> matchers;
matchers.push_back(LimitIs(expected, allow_limit_undetermined));
matchers.push_back(TerminatesWith(TerminationReason::kFeasible));
return AllOfArray(matchers);
}
testing::Matcher<SolveResult> TerminatesWithReasonNoSolutionFound(
const Limit expected, const bool allow_limit_undetermined) {
std::vector<Matcher<SolveResult>> matchers;
matchers.push_back(LimitIs(expected, allow_limit_undetermined));
matchers.push_back(TerminatesWith(TerminationReason::kNoSolutionFound));
return AllOfArray(matchers);
}
template <typename MatcherType>
std::string MatcherToStringImpl(const MatcherType& matcher, const bool negate) {
std::ostringstream os;
if (negate) {
matcher.DescribeNegationTo(&os);
} else {
matcher.DescribeTo(&os);
}
return os.str();
}
template <typename T>
std::string MatcherToString(const Matcher<T>& matcher, bool negate) {
return MatcherToStringImpl(matcher, negate);
}
// clang-format off
// Polymorphic matchers do not always define DescribeTo,
// The <T> type may not be a matcher, but it will implement DescribeTo.
// clang-format on
template <typename T>
std::string MatcherToString(const ::testing::PolymorphicMatcher<T>& matcher,
bool negate) {
return MatcherToStringImpl(matcher.impl(), negate);
}
MATCHER_P(FirstElementIs, first_element_matcher,
(negation
? absl::StrCat("is empty or first element ",
MatcherToString(first_element_matcher, true))
: absl::StrCat("has at least one element and first element ",
MatcherToString(first_element_matcher, false)))) {
return ExplainMatchResult(UnorderedElementsAre(first_element_matcher),
absl::MakeSpan(arg).subspan(0, 1), result_listener);
}
Matcher<Termination> LimitIs(math_opt::Limit limit,
const Matcher<std::string> detail_matcher) {
return AllOf(Field("reason", &Termination::reason,
AnyOf(TerminationReason::kFeasible,
TerminationReason::kNoSolutionFound)),
Field("limit", &Termination::limit, limit),
Field("detail", &Termination::detail, detail_matcher));
}
Matcher<Termination> ReasonIs(TerminationReason reason) {
return Field("reason", &Termination::reason, reason);
}
Matcher<Termination> ReasonIsOptimal() {
return ReasonIs(TerminationReason::kOptimal);
}
Matcher<ProblemStatus> ProblemStatusIs(const ProblemStatus& expected) {
return AllOf(
Field("primal_status", &ProblemStatus::primal_status,
expected.primal_status),
Field("dual_status", &ProblemStatus::dual_status, expected.dual_status),
Field("primal_or_dual_infeasible",
&ProblemStatus::primal_or_dual_infeasible,
expected.primal_or_dual_infeasible));
}
Matcher<Termination> TerminationIsOptimal() {
return AllOf(
Field("reason", &Termination::reason, TerminationReason::kOptimal),
Field("problem_status", &Termination::problem_status,
ProblemStatusIs({.primal_status = FeasibilityStatus::kFeasible,
.dual_status = FeasibilityStatus::kFeasible,
.primal_or_dual_infeasible = false})));
}
Matcher<Termination> TerminationIsOptimal(const double primal_objective_value,
const double dual_objective_value,
const double tolerance) {
return AllOf(
TerminationIsOptimal(),
Field("objective_bounds", &Termination::objective_bounds,
ObjectiveBoundsNear({.primal_bound = primal_objective_value,
.dual_bound = dual_objective_value},
tolerance)));
}
Matcher<Termination> TerminationIsIgnoreDetail(const Termination& expected) {
return AllOf(Field("reason", &Termination::reason, expected.reason),
Field("limit", &Termination::limit, expected.limit));
}
Matcher<SolveResult> IsOptimal(
const std::optional<double> expected_primal_objective,
const double tolerance) {
if (expected_primal_objective.has_value()) {
return AllOf(
Field("termination", &SolveResult::termination, TerminationIsOptimal()),
Property("has_primal_feasible_solution",
&SolveResult::has_primal_feasible_solution, true),
Property("objective_value", &SolveResult::objective_value,
DoubleNear(*expected_primal_objective, tolerance)));
}
return Field("termination", &SolveResult::termination,
TerminationIsOptimal());
}
Matcher<SolveResult> IsOptimalWithSolution(
const double expected_objective,
const VariableMap<double> expected_variable_values,
const double tolerance) {
return AllOf(
IsOptimal(std::make_optional(expected_objective), tolerance),
HasSolution(
PrimalSolution{.variable_values = expected_variable_values,
.objective_value = expected_objective,
.feasibility_status = SolutionStatus::kFeasible},
tolerance));
}
Matcher<SolveResult> IsOptimalWithDualSolution(
const double expected_objective,
const LinearConstraintMap<double> expected_dual_values,
const VariableMap<double> expected_reduced_costs, const double tolerance) {
return AllOf(
IsOptimal(std::make_optional(expected_objective), tolerance),
HasDualSolution(
DualSolution{
.dual_values = expected_dual_values,
.reduced_costs = expected_reduced_costs,
.objective_value = std::make_optional(expected_objective),
.feasibility_status = SolutionStatus::kFeasible},
tolerance));
}
Matcher<SolveResult> HasSolution(PrimalSolution expected,
const double tolerance) {
return Field(
"solutions", &SolveResult::solutions,
Contains(Field("primal_solution", &Solution::primal_solution,
Optional(IsNear(std::move(expected), tolerance)))));
}
Matcher<SolveResult> HasDualSolution(DualSolution expected,
const double tolerance) {
return Field(
"solutions", &SolveResult::solutions,
Contains(Field("dual_solution", &Solution::dual_solution,
Optional(IsNear(std::move(expected), tolerance)))));
}
Matcher<SolveResult> HasPrimalRay(PrimalRay expected, const double tolerance) {
return Field("primal_rays", &SolveResult::primal_rays,
Contains(IsNear(std::move(expected), tolerance)));
}
Matcher<SolveResult> HasPrimalRay(VariableMap<double> expected_vars,
const double tolerance) {
PrimalRay ray;
ray.variable_values = std::move(expected_vars);
return HasPrimalRay(std::move(ray), tolerance);
}
Matcher<SolveResult> HasDualRay(DualRay expected, const double tolerance) {
return Field("dual_rays", &SolveResult::dual_rays,
Contains(IsNear(std::move(expected), tolerance)));
}
namespace {
bool MightTerminateWithRays(const TerminationReason reason) {
switch (reason) {
case TerminationReason::kInfeasibleOrUnbounded:
case TerminationReason::kUnbounded:
case TerminationReason::kInfeasible:
return true;
default:
return false;
}
}
std::vector<TerminationReason> CompatibleReasons(
const TerminationReason expected, const bool inf_or_unb_soft_match) {
if (!inf_or_unb_soft_match) {
return {expected};
}
switch (expected) {
case TerminationReason::kUnbounded:
return {TerminationReason::kUnbounded,
TerminationReason::kInfeasibleOrUnbounded};
case TerminationReason::kInfeasible:
return {TerminationReason::kInfeasible,
TerminationReason::kInfeasibleOrUnbounded};
case TerminationReason::kInfeasibleOrUnbounded:
return {TerminationReason::kUnbounded, TerminationReason::kInfeasible,
TerminationReason::kInfeasibleOrUnbounded};
default:
return {expected};
}
}
Matcher<std::vector<Solution>> CheckSolutions(
const std::vector<Solution>& expected_solutions,
const SolveResultMatcherOptions& options) {
if (options.first_solution_only && !expected_solutions.empty()) {
return FirstElementIs(
IsNear(expected_solutions[0],
SolutionMatcherOptions{.tolerance = options.tolerance,
.check_primal = true,
.check_dual = options.check_dual,
.check_basis = options.check_basis}));
}
return IsNear(expected_solutions,
SolutionMatcherOptions{.tolerance = options.tolerance,
.check_primal = true,
.check_dual = options.check_dual,
.check_basis = options.check_basis});
}
template <typename RayType>
Matcher<std::vector<RayType>> AnyRayNear(
const std::vector<RayType>& expected_rays, const double tolerance) {
std::vector<Matcher<RayType>> matchers;
for (const RayType& ray : expected_rays) {
matchers.push_back(IsNear(ray, tolerance));
}
return Contains(AnyOfArray(matchers));
}
template <typename RayType>
Matcher<std::vector<RayType>> AllRaysNear(
const std::vector<RayType>& expected_rays, const double tolerance) {
std::vector<Matcher<RayType>> matchers;
for (const RayType& ray : expected_rays) {
matchers.push_back(IsNear(ray, tolerance));
}
return ::testing::UnorderedElementsAreArray(matchers);
}
template <typename RayType>
Matcher<std::vector<RayType>> CheckRays(
const std::vector<RayType>& expected_rays, const double tolerance,
bool check_all) {
if (expected_rays.empty()) {
return IsEmpty();
}
if (check_all) {
return AllRaysNear(expected_rays, tolerance);
}
return AnyRayNear(expected_rays, tolerance);
}
} // namespace
Matcher<SolveResult> IsConsistentWith(
const SolveResult& expected, const SolveResultMatcherOptions& options) {
std::vector<Matcher<SolveResult>> to_check;
to_check.push_back(TerminatesWithOneOf(CompatibleReasons(
expected.termination.reason, options.inf_or_unb_soft_match)));
const bool skip_solution =
MightTerminateWithRays(expected.termination.reason) &&
!options.check_solutions_if_inf_or_unbounded;
if (!skip_solution) {
to_check.push_back(Field("solutions", &SolveResult::solutions,
CheckSolutions(expected.solutions, options)));
}
if (options.check_rays) {
to_check.push_back(Field("primal_rays", &SolveResult::primal_rays,
CheckRays(expected.primal_rays, options.tolerance,
!options.first_solution_only)));
to_check.push_back(Field("dual_rays", &SolveResult::dual_rays,
CheckRays(expected.dual_rays, options.tolerance,
!options.first_solution_only)));
}
return AllOfArray(to_check);
}
////////////////////////////////////////////////////////////////////////////////
// ComputeInfeasibleSubsystemResult matchers
////////////////////////////////////////////////////////////////////////////////
testing::Matcher<ComputeInfeasibleSubsystemResult> IsFeasible() {
return AllOf(
Field("feasibility", &ComputeInfeasibleSubsystemResult::feasibility,
FeasibilityStatus::kFeasible),
Field("infeasible_subsystem",
&ComputeInfeasibleSubsystemResult::infeasible_subsystem,
Property("empty", &ModelSubset::empty, true)),
Field("is_minimal", &ComputeInfeasibleSubsystemResult::is_minimal,
false));
}
testing::Matcher<ComputeInfeasibleSubsystemResult> IsUndetermined() {
return AllOf(
Field("feasibility", &ComputeInfeasibleSubsystemResult::feasibility,
FeasibilityStatus::kUndetermined),
Field("infeasible_subsystem",
&ComputeInfeasibleSubsystemResult::infeasible_subsystem,
Property("empty", &ModelSubset::empty, true)),
Field("is_minimal", &ComputeInfeasibleSubsystemResult::is_minimal,
false));
}
testing::Matcher<ComputeInfeasibleSubsystemResult> IsInfeasible(
const std::optional<bool> expected_is_minimal,
const std::optional<ModelSubset> expected_infeasible_subsystem) {
std::vector<Matcher<ComputeInfeasibleSubsystemResult>> matchers;
matchers.push_back(Field("feasibility",
&ComputeInfeasibleSubsystemResult::feasibility,
FeasibilityStatus::kInfeasible));
matchers.push_back(
Field("infeasible_subsystem",
&ComputeInfeasibleSubsystemResult::infeasible_subsystem,
Property("empty", &ModelSubset::empty, false)));
if (expected_is_minimal.has_value()) {
matchers.push_back(Field("is_minimal",
&ComputeInfeasibleSubsystemResult::is_minimal,
Eq(expected_is_minimal.value())));
}
/* TODO(user) implement EqualsProto
if (expected_infeasible_subsystem.has_value()) {
matchers.push_back(
Field("infeasible_subsystem",
&ComputeInfeasibleSubsystemResult::infeasible_subsystem,
Property(&ModelSubset::Proto,
testing::EqualsProto(
expected_infeasible_subsystem.value().Proto()))));
}
*/
return AllOfArray(matchers);
}
////////////////////////////////////////////////////////////////////////////////
// Rarely used
////////////////////////////////////////////////////////////////////////////////
Matcher<UpdateResult> DidUpdate() {
return Field("did_update", &UpdateResult::did_update, true);
}
} // namespace math_opt
} // namespace operations_research

View File

@@ -0,0 +1,549 @@
// Copyright 2010-2024 Google LLC
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Matchers for MathOpt types, specifically SolveResult and nested fields.
//
// The matchers defined here are useful for writing unit tests checking that the
// result of Solve(), absl::StatusOr<SolveResult>, meets expectations. We give
// some examples below. All code is assumed with the following setup:
//
// namespace operations_research::math_opt {
// using ::testing::status::IsOkAndHolds;
//
// Model model;
// const Variable x = model.AddContinuousVariable(0.0, 1.0);
// const Variable y = model.AddContinuousVariable(0.0, 1.0);
// const LinearConstraint c = model.AddLinearConstraint(x + y <= 1);
// model.Maximize(2*x + y);
//
// Example 1.a: result is OK, optimal, and objective value approximately 42.
// EXPECT_THAT(Solve(model, SolverType::kGlop), IsOkAndHolds(IsOptimal(42)));
//
// Example 1.b: equivalent to 1.a.
// ASSERT_OK_AND_ASSIGN(const SolveResult result,
// Solve(model, SolverType::kGlop));
// EXPECT_THAT(result, IsOptimal(42));
//
// Example 2: result is OK, optimal, and best solution is x=1, y=0.
// ASSERT_OK_AND_ASSIGN(const SolveResult result,
// Solve(model, SolverType::kGlop));
// ASSERT_THAT(result, IsOptimal());
// EXPECT_THAT(result.variable_value(), IsNear({{x, 1}, {y, 0}});
// Note: the second ASSERT ensures that if the solution is not optimal, then
// result.variable_value() will not run (the function will crash if the solver
// didn't find a solution). Further, MathOpt guarantees there is a solution
// when the termination reason is optimal.
//
// Example 3: result is OK, check the solution without specifying termination.
// ASSERT_OK_AND_ASSIGN(const SolveResult result,
// Solve(model, SolverType::kGlop));
// EXPECT_THAT(result, HasBestSolution({{x, 1}, {y, 0}}));
//
// Example 4: multiple possible termination reason, primal ray optional:
// ASSERT_OK_AND_ASSIGN(const SolveResult result,
// Solve(model, SolverType::kGlop));
// ASSERT_THAT(result, TerminatesWithOneOf(
// TerminationReason::kUnbounded,
// TerminationReason::kInfeasibleOrUnbounded));
// if(!result.primal_rays.empty()) {
// EXPECT_THAT(result.primal_rays[0], PrimalRayIsNear({{x, 1,}, {y, 0}}));
// }
//
//
// Tips on writing good tests:
// * Use ASSERT_OK_AND_ASSIGN(const SolveResult result, Solve(...)) to ensure
// the test terminates immediately if Solve() does not return OK.
// * If you ASSERT_THAT(result, IsOptimal()), you can assume that you have a
// feasible primal solution afterwards. Otherwise, make no assumptions on
// the contents of result (e.g. do not assume result contains a primal ray
// just because the termination reason was UNBOUNDED).
// * For problems that are infeasible, the termination reasons INFEASIBLE and
// DUAL_INFEASIBLE are both possible. Likewise, for unbounded problems, you
// can get both UNBOUNDED and DUAL_INFEASIBLE. See TerminatesWithOneOf()
// below to make assertions in this case. Note also that some solvers have
// solver specific parameters to ensure that DUAL_INFEASIBLE will not be
// returned (e.g. for Gurobi, use DualReductions or InfUnbdInfo).
// * The objective value and variable values should always be compared up to
// a tolerance, even if your decision variables are integer. The matchers
// defined have a configurable tolerance with default value 1e-5.
// * Primal and dual rays are unique only up to a constant scaling. The
// matchers provided rescale both expected and actual before comparing.
// * Take care on problems with multiple optimal solutions. Do not rely on a
// particular solution being returned in your test, as the test will break
// when we upgrade the solver.
//
// This file also defines functions to let gunit print various MathOpt types.
//
// To see the error messages these matchers generate, run
// blaze test experimental/users/rander/math_opt:matchers_error_messages
// which is a fork of matchers_test.cc where the assertions are all negated
// (note that every test should fail).
#ifndef OR_TOOLS_MATH_OPT_CPP_MATCHERS_H_
#define OR_TOOLS_MATH_OPT_CPP_MATCHERS_H_
#include <optional>
#include <ostream>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>
#include "absl/container/flat_hash_map.h"
#include "absl/status/statusor.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "ortools/math_opt/cpp/linear_constraint.h"
#include "ortools/math_opt/cpp/math_opt.h"
#include "ortools/math_opt/cpp/update_result.h"
#include "ortools/math_opt/cpp/variable_and_expressions.h"
#define ASSERT_OK_AND_ASSIGN(lhs, rexpr) \
ASSERT_OK_AND_ASSIGN_IMPL(CONCAT_MACRO( \
_status_or, __COUNTER__), lhs, rexpr)
#define ASSERT_OK_AND_ASSIGN_IMPL(statusor, lhs, rexpr) \
auto statusor = (rexpr); \
ASSERT_TRUE(statusor.status().ok()) << \
statusor.status(); \
lhs = std::move(statusor.value())
namespace operations_research {
namespace math_opt {
constexpr double kMatcherDefaultTolerance = 1e-5;
////////////////////////////////////////////////////////////////////////////////
// Matchers for absl::flat_hash_map
////////////////////////////////////////////////////////////////////////////////
// Checks that the maps have identical keys and values within tolerance. This
// factory will CHECK-fail if expected contains any NaN values.
testing::Matcher<VariableMap<double>> IsNear(
VariableMap<double> expected, double tolerance = kMatcherDefaultTolerance);
// Checks that the keys of actual are a subset of the keys of expected, and that
// for all shared keys, the values are within tolerance. This factory will
// CHECK-fail if expected contains any NaN values, and any NaN values in the
// expression compared against will result in the matcher failing.
testing::Matcher<VariableMap<double>> IsNearlySubsetOf(
VariableMap<double> expected, double tolerance = kMatcherDefaultTolerance);
// Checks that the maps have identical keys and values within tolerance. This
// factory will CHECK-fail if expected contains any NaN values, and any NaN
// values in the expression compared against will result in the matcher failing.
testing::Matcher<LinearConstraintMap<double>> IsNear(
LinearConstraintMap<double> expected,
double tolerance = kMatcherDefaultTolerance);
// Checks that the keys of actual are a subset of the keys of expected, and that
// for all shared keys, the values are within tolerance. This factory will
// CHECK-fail if expected contains any NaN values, and any NaN values in the
// expression compared against will result in the matcher failing.
testing::Matcher<LinearConstraintMap<double>> IsNearlySubsetOf(
LinearConstraintMap<double> expected,
double tolerance = kMatcherDefaultTolerance);
// Checks that the maps have identical keys and values within tolerance. Works
// for VariableMap, LinearConstraintMap, among other realizations of
// absl::flat_hash_map. This factory will CHECK-fail if expected contains any
// NaN values, and any NaN values in the expression compared against will result
// in the matcher failing.
template <typename K>
testing::Matcher<absl::flat_hash_map<K, double>> IsNear(
absl::flat_hash_map<K, double> expected,
double tolerance = kMatcherDefaultTolerance);
// Checks that the keys of actual are a subset of the keys of expected, and that
// for all shared keys, the values are within tolerance. Works for VariableMap,
// LinearConstraintMap, among other realizations of absl::flat_hash_map. This
// factory will CHECK-fail if expected contains any NaN values, and any NaN
// values in the expression compared against will result in the matcher failing.
template <typename K>
testing::Matcher<absl::flat_hash_map<K, double>> IsNearlySubsetOf(
absl::flat_hash_map<K, double> expected,
double tolerance = kMatcherDefaultTolerance);
////////////////////////////////////////////////////////////////////////////////
// Matchers for various Variable expressions (e.g. LinearExpression)
////////////////////////////////////////////////////////////////////////////////
// Checks that the expressions are structurally identical (i.e., internal maps
// have the same keys and storage, coefficients are exactly equal). This factory
// will CHECK-fail if expected contains any NaN values, and any NaN values in
// the expression compared against will result in the matcher failing.
testing::Matcher<LinearExpression> IsIdentical(LinearExpression expected);
testing::Matcher<LinearExpression> LinearExpressionIsNear(
LinearExpression expected, double tolerance = kMatcherDefaultTolerance);
// Checks that the bounded linear expression is equivalent to expected, where
// equivalence is maintained by:
// * adding alpha to the lower bound, the linear expression and upper bound
// * multiplying the lower bound, linear expression, by -1 (and flipping the
// inequalities).
// Note that, as implemented, we do not allow for arbitrary multiplicative
// rescalings (this makes additive tolerance complicated).
testing::Matcher<BoundedLinearExpression> IsNearlyEquivalent(
const BoundedLinearExpression& expected,
double tolerance = kMatcherDefaultTolerance);
// Checks that the expressions are structurally identical (i.e., internal maps
// have the same keys and storage, coefficients are exactly equal). This factory
// will CHECK-fail if expected contains any NaN values, and any NaN values in
// the expression compared against will result in the matcher failing.
testing::Matcher<QuadraticExpression> IsIdentical(QuadraticExpression expected);
////////////////////////////////////////////////////////////////////////////////
// Matchers for solutions
////////////////////////////////////////////////////////////////////////////////
// Options for IsNear(Solution).
struct SolutionMatcherOptions {
double tolerance = kMatcherDefaultTolerance;
bool check_primal = true;
bool check_dual = true;
bool check_basis = true;
// If false, only match on primal/dual solutions that are feasible. If true,
// match on solutions that are either feasible or undetermined.
bool allow_undetermined = false;
};
testing::Matcher<Solution> IsNear(Solution expected,
SolutionMatcherOptions options = {});
// Checks variables match and variable/objective values are within tolerance and
// feasibility statuses are identical.
testing::Matcher<PrimalSolution> IsNear(
PrimalSolution expected, double tolerance = kMatcherDefaultTolerance,
bool allow_undetermined = false);
// Checks dual variables, reduced costs and objective are within tolerance and
// feasibility statuses are identical.
testing::Matcher<DualSolution> IsNear(
DualSolution expected, double tolerance = kMatcherDefaultTolerance,
bool allow_undetermined = false);
testing::Matcher<Basis> BasisIs(const Basis& expected);
////////////////////////////////////////////////////////////////////////////////
// Matchers for a Rays
////////////////////////////////////////////////////////////////////////////////
// Checks variables match and that after rescaling, variable values are within
// tolerance.
testing::Matcher<PrimalRay> IsNear(PrimalRay expected,
double tolerance = kMatcherDefaultTolerance);
// Checks variables match and that after rescaling, variable values are within
// tolerance.
testing::Matcher<PrimalRay> PrimalRayIsNear(
VariableMap<double> expected_var_values,
double tolerance = kMatcherDefaultTolerance);
// Checks that dual variables and reduced costs are defined for the same
// set of Variables/LinearConstraints, and that their rescaled values are within
// tolerance.
testing::Matcher<DualRay> IsNear(DualRay expected,
double tolerance = kMatcherDefaultTolerance);
////////////////////////////////////////////////////////////////////////////////
// Matchers for a Termination
////////////////////////////////////////////////////////////////////////////////
testing::Matcher<Termination> ReasonIs(TerminationReason reason);
// Checks that the termination reason is optimal, but does not check the other
// fields of Termination are consistent with optimality.
testing::Matcher<Termination> ReasonIsOptimal();
// Checks that the termination reason is optimal and that problem status is
// consistent with optimality (i.e. status is primal and dual feasible).
testing::Matcher<Termination> TerminationIsOptimal();
// Checks that the termination reason is optimal, that the objective bounds
// match the provided ones and that problem status is consistent with
// optimality (i.e. status is primal and dual feasible).
testing::Matcher<Termination> TerminationIsOptimal(
double primal_objective_value, double dual_objective_value,
double tolerance = kMatcherDefaultTolerance);
testing::Matcher<Termination> LimitIs(
Limit limit, testing::Matcher<std::string> detail_matcher = testing::_);
testing::Matcher<Termination> TerminationIsIgnoreDetail(
const Termination& expected);
testing::Matcher<ObjectiveBounds> ObjectiveBoundsNear(
const ObjectiveBounds& expected,
double tolerance = kMatcherDefaultTolerance);
testing::Matcher<ProblemStatus> ProblemStatusIs(const ProblemStatus& expected);
////////////////////////////////////////////////////////////////////////////////
// Matchers for a SolveResult
////////////////////////////////////////////////////////////////////////////////
// Checks the following:
// * The termination reason is optimal.
// * If expected_primal_objective contains a value, there is at least one
// primal feasible solution and that solution has an objective value within
// tolerance of expected_objective. However, primal and dual bounds are not
// checked.
// TODO(b/309658404): Note the bounds should be at least finite once we validate
// this.
testing::Matcher<SolveResult> IsOptimal(
std::optional<double> expected_primal_objective = std::nullopt,
double tolerance = kMatcherDefaultTolerance);
testing::Matcher<SolveResult> IsOptimalWithSolution(
double expected_objective, VariableMap<double> expected_variable_values,
double tolerance = kMatcherDefaultTolerance);
testing::Matcher<SolveResult> IsOptimalWithDualSolution(
double expected_objective, LinearConstraintMap<double> expected_dual_values,
VariableMap<double> expected_reduced_costs,
double tolerance = kMatcherDefaultTolerance);
// Checks the following:
// * The result has the expected termination reason.
testing::Matcher<SolveResult> TerminatesWith(TerminationReason expected);
// Checks that the result has one of the allowed termination reasons.
testing::Matcher<SolveResult> TerminatesWithOneOf(
const std::vector<TerminationReason>& allowed);
// Checks the following:
// * The result has termination reason kFeasible or kNoSolutionFound.
// * The limit is expected, or is kUndetermined if allow_limit_undetermined.
testing::Matcher<SolveResult> TerminatesWithLimit(
Limit expected, bool allow_limit_undetermined = false);
// Checks the following:
// * The result has termination reason kFeasible.
// * The limit is expected, or is kUndetermined if allow_limit_undetermined.
testing::Matcher<SolveResult> TerminatesWithReasonFeasible(
Limit expected, bool allow_limit_undetermined = false);
// Checks the following:
// * The result has termination reason kNoSolutionFound.
// * The limit is expected, or is kUndetermined if allow_limit_undetermined.
testing::Matcher<SolveResult> TerminatesWithReasonNoSolutionFound(
Limit expected, bool allow_limit_undetermined = false);
// SolveResult has a primal solution matching expected within tolerance.
testing::Matcher<SolveResult> HasSolution(
PrimalSolution expected, double tolerance = kMatcherDefaultTolerance);
// SolveResult has a dual solution matching expected within
// tolerance.
testing::Matcher<SolveResult> HasDualSolution(
DualSolution expected, double tolerance = kMatcherDefaultTolerance);
// Actual SolveResult contains a primal ray that matches expected within
// tolerance.
testing::Matcher<SolveResult> HasPrimalRay(
PrimalRay expected, double tolerance = kMatcherDefaultTolerance);
// Actual SolveResult contains a primal ray with variable values equivalent to
// (under L_inf scaling) expected_vars up to tolerance.
testing::Matcher<SolveResult> HasPrimalRay(
VariableMap<double> expected_vars,
double tolerance = kMatcherDefaultTolerance);
// Actual SolveResult contains a dual ray that matches expected within
// tolerance.
testing::Matcher<SolveResult> HasDualRay(
DualRay expected, double tolerance = kMatcherDefaultTolerance);
// Configures SolveResult matcher IsConsistentWith() below.
struct SolveResultMatcherOptions {
double tolerance = 1e-5;
bool first_solution_only = true;
bool check_dual = true;
bool check_rays = true;
// If the expected result has termination reason kInfeasible, kUnbounded, or
// kDualInfeasasible, the primal solution, dual solution, and basis are
// ignored unless check_solutions_if_inf_or_unbounded is true.
//
// TODO(b/201099290): this is perhaps not a good default. Gurobi as
// implemented is returning primal solutions for both unbounded and
// infeasible problems. We need to add unit tests that inspect this value
// and turn them on one solver at a time with a new parameter on
// SimpleLpTestParameters.
bool check_solutions_if_inf_or_unbounded = false;
bool check_basis = false;
// In linear programming, the following outcomes are all possible
//
// Primal LP | Dual LP | Possible MathOpt Termination Reasons
// -----------------------------------------------------------------
// 1. Infeasible | Unbounded | kInfeasible
// 2. Optimal | Optimal | kOptimal
// 3. Unbounded | Infeasible | kUnbounded, kInfeasibleOrUnbounded
// 4. Infeasible | Infeasible | kInfeasible, kInfeasibleOrUnbounded
//
// (Above "Optimal" means that an optimal solution exists. This is a statement
// about the existence of optimal solutions and certificates of
// infeasibility/unboundedness, not about the outcome of applying any
// particular algorithm.)
//
// When writing your unit test, you can typically tell which case of 1-4 you
// are in, but in cases 3-4 you do not know which termination reason will be
// returned. In some situations, it may not be clear if you are in case 1 or
// case 4 as well.
//
// When inf_or_unb_soft_match=false, the matcher must exactly specify the
// status returned by the solver. For cases 3-4, this is implementation
// dependent and we do not recommend this. When
// inf_or_unb_soft_match=true:
// * kInfeasible can also match kInfeasibleOrUnbounded
// * kUnbounded can also match kInfeasibleOrUnbounded
// * kInfeasibleOrUnbounded can also match kInfeasible and kUnbounded.
// For case 2, inf_or_unb_soft_match has no effect.
//
// To build the strongest possible matcher (accepting the minimal set of
// termination reasons):
// * If you know you are in case 1, se inf_or_unb_soft_match=false
// (soft_match=true over-matches)
// * For case 3, use inf_or_unb_soft_match=false and
// termination_reason=kUnbounded (kInfeasibleOrUnbounded over-matches).
// * For case 4 (or if you are unsure of case 1 vs case 4), use
// inf_or_unb_soft_match=true and
// termination_reason=kInfeasible (kInfeasibleOrUnbounded over-matches).
// * If you cannot tell if you are in case 3 or case 4, use
// inf_or_unb_soft_match=true and termination reason
// kInfeasibleOrUnbounded.
//
// If the above is too complicated, always setting
// inf_or_unb_soft_match=true and using any of the expected MathOpt
// termination reasons from the above table will give a matcher that is
// slightly too lenient.
bool inf_or_unb_soft_match = true;
};
// Tests that two SolveResults are equivalent. Basic use:
//
// SolveResult expected;
// // Fill in expected...
// ASSERT_OK_AND_ASSIGN(SolveResult actual, Solve(model, solver_type));
// EXPECT_THAT(actual, IsConsistentWith(expected));
//
// Equivalence is defined as follows:
// * The termination reasons are the same.
// - For infeasible and unbounded problems, see
// options.inf_or_unb_soft_match.
// * The solve stats are ignored.
// * For both primal and dual solutions, either expected and actual are
// both empty, or their first entries satisfy IsNear() at options.tolerance.
// - Not checked if options.check_solutions_if_inf_or_unbounded and the
// problem is infeasible or unbounded (default).
// - If options.first_solution_only is false, check the entire list of
// solutions matches in the same order.
// - Dual solution is not checked if options.check_dual=false
// * For both the primal and dual rays, either expected and actual are both
// empty, or any ray in expected IsNear() any ray in actual (which is up
// to a rescaling) at options.tolerance.
// - Not checked if options.check_rays=false
// - If options.first_solution_only is false, check the entire list of
// solutions matches in the same order.
// * The basis is not checked by default. If enabled, checked with BasisIs().
// - Enable with options.check_basis
//
// This function is symmetric in that:
// EXPECT_THAT(actual, IsConsistentWith(expected));
// EXPECT_THAT(expected, IsConsistentWith(actual));
// agree on matching, they only differ in strings produced. Per gmock
// conventions, prefer the former.
//
// For problems with either primal or dual infeasibility, see
// SolveResultMatcherOptions::inf_or_unb_soft_match for guidance on how to
// best set the termination reason and inf_or_unb_soft_match.
testing::Matcher<SolveResult> IsConsistentWith(
const SolveResult& expected, const SolveResultMatcherOptions& options = {});
///////////////////////////////////////////////////////////////////////////////
// Mocking
////////////////////////////////////////////////////////////////////////////////
using MockSolveFunction = testing::MockFunction<
absl::StatusOr<operations_research::math_opt::SolveResult>(
const operations_research::math_opt::Model&,
operations_research::math_opt::SolverType,
const operations_research::math_opt::SolveArguments&,
const operations_research::math_opt::SolverInitArguments&)>;
////////////////////////////////////////////////////////////////////////////////
// Matchers for a ComputeInfeasibleSubsystemResult
////////////////////////////////////////////////////////////////////////////////
// Checks that ComputeInfeasibleSubsystemResult.feasibility is kFeasible,
// that the .infeasible_subsystem is empty and that is_minimal is false.
testing::Matcher<ComputeInfeasibleSubsystemResult> IsFeasible();
// Checks that ComputeInfeasibleSubsystemResult.feasibility is kUndetermined,
// that the .infeasible_subsystem is empty and that is_minimal is false..
testing::Matcher<ComputeInfeasibleSubsystemResult> IsUndetermined();
// Checks that ComputeInfeasibleSubsystemResult.feasibility is kInfeasible and
// that .infeasible_subsystem is not empty. If expected_is_minimal is set, test
// that .is_minimal has the same value. If expected_infeasible_subsystem is set,
// test that the resulting ModelSubset::Proto() are EqualsProto().
testing::Matcher<ComputeInfeasibleSubsystemResult> IsInfeasible(
std::optional<bool> expected_is_minimal = std::nullopt,
std::optional<ModelSubset> expected_infeasible_subsystem = std::nullopt);
////////////////////////////////////////////////////////////////////////////////
// Rarely used
////////////////////////////////////////////////////////////////////////////////
// Actual UpdateResult.did_update is true.
testing::Matcher<UpdateResult> DidUpdate();
////////////////////////////////////////////////////////////////////////////////
// Implementation details
////////////////////////////////////////////////////////////////////////////////
void PrintTo(const PrimalSolution& primal_solution, std::ostream* os);
void PrintTo(const DualSolution& dual_solution, std::ostream* os);
void PrintTo(const PrimalRay& primal_ray, std::ostream* os);
void PrintTo(const DualRay& dual_ray, std::ostream* os);
void PrintTo(const Basis& basis, std::ostream* os);
void PrintTo(const Solution& solution, std::ostream* os);
void PrintTo(const SolveResult& result, std::ostream* os);
// We do not want to rely on ::testing::internal::ContainerPrinter because we
// want to sort the keys.
template <typename K, typename V, typename = std::enable_if_t<is_key_type_v<K>>>
void PrintTo(const absl::flat_hash_map<K, V>& id_map, std::ostream* const os) {
constexpr int kMaxPrint = 10;
int num_added = 0;
*os << "{";
for (const K k : SortedKeys(id_map)) {
if (num_added > 0) {
*os << ", ";
}
if (num_added >= kMaxPrint) {
*os << "...(size=" << id_map.size() << ")";
break;
}
*os << "{" << k << ", " << ::testing::PrintToString(id_map.at(k)) << "}";
++num_added;
}
*os << "}";
}
} // namespace math_opt
} // namespace operations_research
#endif // OR_TOOLS_MATH_OPT_CPP_MATCHERS_H_