Google OR-Tools v9.11
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
matchers.cc
Go to the documentation of this file.
1// Copyright 2010-2024 Google LLC
2// Licensed under the Apache License, Version 2.0 (the "License");
3// you may not use this file except in compliance with the License.
4// You may obtain a copy of the License at
5//
6// http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
15
16#include <algorithm>
17#include <cmath>
18#include <cstdlib>
19#include <optional>
20#include <ostream>
21#include <sstream>
22#include <string>
23#include <utility>
24#include <vector>
25
26#include "absl/container/flat_hash_map.h"
27#include "absl/log/check.h"
28#include "absl/strings/str_cat.h"
29#include "absl/types/span.h"
30#include "gtest/gtest.h"
31#include "ortools/base/gmock.h"
36
37namespace operations_research {
38namespace math_opt {
39
40namespace {
41
42using ::testing::AllOf;
43using ::testing::AllOfArray;
44using ::testing::AnyOf;
45using ::testing::AnyOfArray;
46using ::testing::Contains;
47using ::testing::DoubleNear;
48using ::testing::Eq;
49using ::testing::ExplainMatchResult;
50using ::testing::Field;
51using ::testing::IsEmpty;
52using ::testing::Matcher;
53using ::testing::MatcherInterface;
54using ::testing::MatchResultListener;
55using ::testing::Optional;
56using ::testing::PrintToString;
57using ::testing::Property;
58
59} // namespace
60
62// Printing
64
65namespace {
66
67template <typename T>
68struct Printer {
69 explicit Printer(const T& t) : value(t) {}
70
71 const T& value;
72
73 friend std::ostream& operator<<(std::ostream& os, const Printer& printer) {
74 os << PrintToString(printer.value);
75 return os;
76 }
77};
78
79template <typename T>
80Printer<T> Print(const T& t) {
81 return Printer<T>(t);
82}
83
84} // namespace
85
86void PrintTo(const PrimalSolution& primal_solution, std::ostream* const os) {
87 *os << "{variable_values: " << Print(primal_solution.variable_values)
88 << ", objective_value: " << Print(primal_solution.objective_value)
89 << ", feasibility_status: " << Print(primal_solution.feasibility_status)
90 << "}";
91}
92
93void PrintTo(const DualSolution& dual_solution, std::ostream* const os) {
94 *os << "{dual_values: " << Print(dual_solution.dual_values)
95 << ", quadratic_dual_values: "
96 << Print(dual_solution.quadratic_dual_values)
97 << ", reduced_costs: " << Print(dual_solution.reduced_costs)
98 << ", objective_value: " << Print(dual_solution.objective_value)
99 << ", feasibility_status: " << Print(dual_solution.feasibility_status)
100 << "}";
101}
102
103void PrintTo(const PrimalRay& primal_ray, std::ostream* const os) {
104 *os << "{variable_values: " << Print(primal_ray.variable_values) << "}";
106
107void PrintTo(const DualRay& dual_ray, std::ostream* const os) {
108 *os << "{dual_values: " << Print(dual_ray.dual_values)
109 << ", reduced_costs: " << Print(dual_ray.reduced_costs) << "}";
110}
111
112void PrintTo(const Basis& basis, std::ostream* const os) {
113 *os << "{variable_status: " << Print(basis.variable_status)
114 << ", constraint_status: " << Print(basis.constraint_status)
115 << ", basic_dual_feasibility: " << Print(basis.basic_dual_feasibility)
116 << "}";
117}
118
119void PrintTo(const Solution& solution, std::ostream* const os) {
120 *os << "{primal_solution: " << Print(solution.primal_solution)
121 << ", dual_solution: " << Print(solution.dual_solution)
122 << ", basis: " << Print(solution.basis) << "}";
123}
124
125void PrintTo(const SolveResult& result, std::ostream* const os) {
126 *os << "{termination: " << Print(result.termination)
127 << ", solve_stats: " << Print(result.solve_stats)
128 << ", solutions: " << Print(result.solutions)
129 << ", primal_rays: " << Print(result.primal_rays)
130 << ", dual_rays: " << Print(result.dual_rays) << "}";
131}
132
134// absl::flat_hash_map<K, double> Matchers
136
137namespace {
138
139template <typename K>
140class MapToDoubleMatcher
141 : public MatcherInterface<absl::flat_hash_map<K, double>> {
142 public:
143 MapToDoubleMatcher(absl::flat_hash_map<K, double> expected,
144 const bool all_keys, const double tolerance)
145 : expected_(std::move(expected)),
146 all_keys_(all_keys),
147 tolerance_(tolerance) {
148 for (const auto [k, v] : expected_) {
149 CHECK(!std::isnan(v)) << "Illegal NaN for key: " << k;
150 }
151 }
152
153 bool MatchAndExplain(absl::flat_hash_map<K, double> actual,
154 MatchResultListener* const os) const override {
155 for (const auto& [key, value] : expected_) {
156 if (!actual.contains(key)) {
157 *os << "expected key " << key << " not found";
158 return false;
159 }
160 if (!(std::abs(value - actual.at(key)) <= tolerance_)) {
161 *os << "value for key " << key
162 << " not within tolerance, expected: " << value
163 << " but found: " << actual.at(key);
164 return false;
165 }
166 }
167 // Post condition: expected_ is a subset of actual.
168 if (all_keys_ && expected_.size() != actual.size()) {
169 for (const auto& [key, value] : actual) {
170 if (!expected_.contains(key)) {
171 *os << "found unexpected key " << key << " in actual";
172 return false;
173 }
174 }
175 // expected_ subset of actual && expected_.size() != actual.size() implies
176 // that there is a member A of actual not in expected. When the loop above
177 // hits A, it will return, thus this line is unreachable.
178 LOG(FATAL) << "unreachable";
179 }
180 return true;
181 }
182
183 void DescribeTo(std::ostream* const os) const override {
184 if (all_keys_) {
185 *os << "has identical keys to ";
186 } else {
187 *os << "keys are contained in ";
188 }
189 PrintTo(expected_, os);
190 *os << " and values within " << tolerance_;
191 }
192
193 void DescribeNegationTo(std::ostream* const os) const override {
194 if (all_keys_) {
195 *os << "either keys differ from ";
196 } else {
197 *os << "either has a key not in ";
198 }
199 PrintTo(expected_, os);
200 *os << " or a value differs by more than " << tolerance_;
201 }
202
203 private:
204 const absl::flat_hash_map<K, double> expected_;
205 const bool all_keys_;
206 const double tolerance_;
207};
208
209} // namespace
210
211Matcher<VariableMap<double>> IsNearlySubsetOf(VariableMap<double> expected,
212 double tolerance) {
213 return Matcher<VariableMap<double>>(new MapToDoubleMatcher<Variable>(
214 std::move(expected), /*all_keys=*/false, tolerance));
216
217Matcher<VariableMap<double>> IsNear(VariableMap<double> expected,
218 const double tolerance) {
219 return Matcher<VariableMap<double>>(new MapToDoubleMatcher<Variable>(
220 std::move(expected), /*all_keys=*/true, tolerance));
222
223Matcher<LinearConstraintMap<double>> IsNearlySubsetOf(
224 LinearConstraintMap<double> expected, double tolerance) {
225 return Matcher<LinearConstraintMap<double>>(
226 new MapToDoubleMatcher<LinearConstraint>(std::move(expected),
227 /*all_keys=*/false, tolerance));
228}
229
230Matcher<LinearConstraintMap<double>> IsNear(
231 LinearConstraintMap<double> expected, const double tolerance) {
232 return Matcher<LinearConstraintMap<double>>(
233 new MapToDoubleMatcher<LinearConstraint>(std::move(expected),
234 /*all_keys=*/true, tolerance));
235}
236
237Matcher<absl::flat_hash_map<QuadraticConstraint, double>> IsNear(
238 absl::flat_hash_map<QuadraticConstraint, double> expected,
239 const double tolerance) {
240 return Matcher<absl::flat_hash_map<QuadraticConstraint, double>>(
241 new MapToDoubleMatcher<QuadraticConstraint>(
242 std::move(expected), /*all_keys=*/true, tolerance));
243}
244
245Matcher<absl::flat_hash_map<QuadraticConstraint, double>> IsNearlySubsetOf(
246 absl::flat_hash_map<QuadraticConstraint, double> expected,
247 double tolerance) {
248 return Matcher<absl::flat_hash_map<QuadraticConstraint, double>>(
249 new MapToDoubleMatcher<QuadraticConstraint>(std::move(expected),
250 /*all_keys=*/false,
251 tolerance));
252}
253
254template <typename K>
255Matcher<absl::flat_hash_map<K, double>> IsNear(
256 absl::flat_hash_map<K, double> expected, const double tolerance) {
257 return Matcher<absl::flat_hash_map<K, double>>(new MapToDoubleMatcher<K>(
258 std::move(expected), /*all_keys=*/true, tolerance));
260
261template <typename K>
262Matcher<absl::flat_hash_map<K, double>> IsNearlySubsetOf(
263 absl::flat_hash_map<K, double> expected, const double tolerance) {
264 return Matcher<absl::flat_hash_map<K, double>>(new MapToDoubleMatcher<K>(
265 std::move(expected), /*all_keys=*/false, tolerance));
267
269// Matchers for LinearExpression and QuadraticExpression
271
272testing::Matcher<LinearExpression> IsIdentical(LinearExpression expected) {
273 return LinearExpressionIsNear(expected, 0.0);
274}
275
276testing::Matcher<LinearExpression> LinearExpressionIsNear(
277 const LinearExpression expected, const double tolerance) {
278 CHECK(!std::isnan(expected.offset())) << "Illegal NaN-valued offset";
279 return AllOf(
280 Property("storage", &LinearExpression::storage, Eq(expected.storage())),
281 Property("offset", &LinearExpression::offset,
282 testing::DoubleNear(expected.offset(), tolerance)),
283 Property("terms", &LinearExpression::terms,
284 IsNear(expected.terms(), tolerance)));
285}
286
287namespace {
288testing::Matcher<BoundedLinearExpression> IsNearForSign(
289 const BoundedLinearExpression& expected, const double tolerance) {
290 return AllOf(Property("upper_bound_minus_offset",
292 testing::DoubleNear(expected.upper_bound_minus_offset(),
293 tolerance)),
294 Property("lower_bound_minus_offset",
296 testing::DoubleNear(expected.lower_bound_minus_offset(),
297 tolerance)),
298 Field("expression", &BoundedLinearExpression::expression,
299 Property("terms", &LinearExpression::terms,
300 IsNear(expected.expression.terms(), tolerance))));
301}
302} // namespace
303
304testing::Matcher<BoundedLinearExpression> IsNearlyEquivalent(
305 const BoundedLinearExpression& expected, const double tolerance) {
306 const BoundedLinearExpression expected_negation(
307 -expected.expression, /*lower_bound=*/-expected.upper_bound,
308 /*upper_bound=*/-expected.lower_bound);
309 return AnyOf(IsNearForSign(expected, tolerance),
310 IsNearForSign(expected_negation, tolerance));
311}
312
313testing::Matcher<QuadraticExpression> IsIdentical(
314 QuadraticExpression expected) {
315 CHECK(!std::isnan(expected.offset())) << "Illegal NaN-valued offset";
316 return AllOf(
317 Property("storage", &QuadraticExpression::storage,
318 Eq(expected.storage())),
319 Property("offset", &QuadraticExpression::offset,
320 testing::Eq(expected.offset())),
321 Property("linear_terms", &QuadraticExpression::linear_terms,
322 IsNear(expected.linear_terms(), /*tolerance=*/0)),
323 Property("quadratic_terms", &QuadraticExpression::quadratic_terms,
324 IsNear(expected.quadratic_terms(), /*tolerance=*/0)));
325}
326
328// Matcher helpers
330
331namespace {
332
333template <typename RayType>
334class RayMatcher : public MatcherInterface<RayType> {
335 public:
336 RayMatcher(RayType expected, const double tolerance)
337 : expected_(std::move(expected)), tolerance_(tolerance) {}
338 void DescribeTo(std::ostream* os) const final {
339 *os << "after L_inf normalization, is within tolerance: " << tolerance_
340 << " of expected: ";
341 PrintTo(expected_, os);
342 }
343 void DescribeNegationTo(std::ostream* const os) const final {
344 *os << "after L_inf normalization, is not within tolerance: " << tolerance_
345 << " of expected: ";
346 PrintTo(expected_, os);
347 }
348
349 protected:
350 const RayType expected_;
351 const double tolerance_;
352};
353
354// Alias to use the std::optional templated adaptor.
355Matcher<double> IsNear(double expected, const double tolerance) {
356 return DoubleNear(expected, tolerance);
357}
358
359template <typename Type>
360Matcher<std::optional<Type>> IsNear(std::optional<Type> expected,
361 const double tolerance) {
362 if (expected.has_value()) {
363 return Optional(IsNear(*expected, tolerance));
364 }
365 return testing::Eq(std::nullopt);
366}
367
368template <typename Type>
369Matcher<std::optional<Type>> IsNear(std::optional<Type> expected,
370 const double tolerance,
371 const bool allow_undetermined) {
372 if (expected.has_value()) {
373 return Optional(IsNear(*expected, tolerance, allow_undetermined));
374 }
375 return testing::Eq(std::nullopt);
376}
377
378// Custom std::optional for basis.
379Matcher<std::optional<Basis>> BasisIs(const std::optional<Basis>& expected) {
380 if (expected.has_value()) {
381 return Optional(BasisIs(*expected));
382 }
383 return testing::Eq(std::nullopt);
384}
385
386testing::Matcher<std::vector<Solution>> IsNear(
387 absl::Span<const Solution> expected_solutions,
388 const SolutionMatcherOptions options) {
389 if (expected_solutions.empty()) {
390 return IsEmpty();
391 }
392 std::vector<Matcher<Solution>> matchers;
393 for (const Solution& sol : expected_solutions) {
394 matchers.push_back(IsNear(sol, options));
395 }
396 return ::testing::ElementsAreArray(matchers);
397}
398
399} // namespace
400
402// Matchers for Solutions
404
405Matcher<SolutionStatus> SolutionStatusIs(const SolutionStatus expected,
406 const bool allow_undetermined) {
407 if (allow_undetermined) {
408 return AnyOf(Eq(expected), Eq(SolutionStatus::kUndetermined));
409 }
410 return Eq(expected);
411}
412
413Matcher<PrimalSolution> IsNear(PrimalSolution expected, const double tolerance,
414 const bool allow_undetermined) {
415 return AllOf(
416 Field("variable_values", &PrimalSolution::variable_values,
417 IsNear(expected.variable_values, tolerance)),
418 Field("objective_value", &PrimalSolution::objective_value,
419 IsNear(expected.objective_value, tolerance)),
420 Field("feasibility_status", &PrimalSolution::feasibility_status,
421 SolutionStatusIs(expected.feasibility_status, allow_undetermined)));
422}
424Matcher<DualSolution> IsNear(DualSolution expected, const double tolerance,
425 const bool allow_undetermined) {
426 return AllOf(
427 Field("dual_values", &DualSolution::dual_values,
428 IsNear(expected.dual_values, tolerance)),
429 Field("quadratic_dual_values", &DualSolution::quadratic_dual_values,
430 IsNear(expected.quadratic_dual_values, tolerance)),
431 Field("reduced_costs", &DualSolution::reduced_costs,
432 IsNear(expected.reduced_costs, tolerance)),
433 Field("objective_value", &DualSolution::objective_value,
434 IsNear(expected.objective_value, tolerance)),
435 Field("feasibility_status", &DualSolution::feasibility_status,
436 SolutionStatusIs(expected.feasibility_status, allow_undetermined)));
437}
438
439Matcher<Basis> BasisIs(const Basis& expected) {
440 return AllOf(Field("variable_status", &Basis::variable_status,
441 expected.variable_status),
442 Field("constraint_status", &Basis::constraint_status,
443 expected.constraint_status),
444 Field("basic_dual_feasibility", &Basis::basic_dual_feasibility,
445 expected.basic_dual_feasibility));
446}
447
448Matcher<Solution> IsNear(Solution expected,
449 const SolutionMatcherOptions options) {
450 std::vector<Matcher<Solution>> to_check;
451 if (options.check_primal) {
452 to_check.push_back(Field("primal_solution", &Solution::primal_solution,
453 IsNear(expected.primal_solution, options.tolerance,
454 options.allow_undetermined)));
455 }
456 if (options.check_dual) {
457 to_check.push_back(Field("dual_solution", &Solution::dual_solution,
458 IsNear(expected.dual_solution, options.tolerance,
459 options.allow_undetermined)));
460 }
461 if (options.check_basis) {
462 to_check.push_back(
463 Field("basis", &Solution::basis, BasisIs(expected.basis)));
464 }
465 return AllOfArray(to_check);
466}
467
469// Primal Ray Matcher
471
472namespace {
473
474template <typename K>
475double InfinityNorm(const absl::flat_hash_map<K, double>& vector) {
476 double infinity_norm = 0.0;
477 for (auto [id, value] : vector) {
478 infinity_norm = std::max(infinity_norm, std::abs(value));
479 }
480 return infinity_norm;
481}
482
483// Returns a normalized primal ray.
484//
485// The normalization is done using infinity norm:
486//
487// ray / ||ray||_inf
488//
489// If the input ray norm is zero, the ray is returned unchanged.
490PrimalRay NormalizePrimalRay(PrimalRay ray) {
491 const double norm = InfinityNorm(ray.variable_values);
492 if (norm != 0.0) {
493 for (auto& entry : ray.variable_values) {
494 entry.second /= norm;
495 }
496 }
497 return ray;
498}
499
500class PrimalRayMatcher : public RayMatcher<PrimalRay> {
501 public:
502 PrimalRayMatcher(PrimalRay expected, const double tolerance)
503 : RayMatcher(std::move(expected), tolerance) {}
504
505 bool MatchAndExplain(PrimalRay actual,
506 MatchResultListener* const os) const override {
507 auto normalized_actual = NormalizePrimalRay(actual);
508 auto normalized_expected = NormalizePrimalRay(expected_);
509 if (os->IsInterested()) {
510 *os << "actual normalized: " << PrintToString(normalized_actual)
511 << ", expected normalized: " << PrintToString(normalized_expected);
512 }
513 return ExplainMatchResult(
514 IsNear(normalized_expected.variable_values, tolerance_),
515 normalized_actual.variable_values, os);
516 }
517};
518
519} // namespace
520
521Matcher<PrimalRay> IsNear(PrimalRay expected, const double tolerance) {
522 return Matcher<PrimalRay>(
523 new PrimalRayMatcher(std::move(expected), tolerance));
524}
525
526Matcher<PrimalRay> PrimalRayIsNear(VariableMap<double> expected_var_values,
527 const double tolerance) {
528 PrimalRay expected;
529 expected.variable_values = std::move(expected_var_values);
530 return IsNear(expected, tolerance);
531}
532
534// Dual Ray Matcher
536
537namespace {
539// Returns a normalized dual ray.
540//
541// The normalization is done using infinity norm:
542//
543// ray / ||ray||_inf
544//
545// If the input ray norm is zero, the ray is returned unchanged.
546DualRay NormalizeDualRay(DualRay ray) {
547 const double norm =
548 std::max(InfinityNorm(ray.dual_values), InfinityNorm(ray.reduced_costs));
549 if (norm != 0.0) {
550 for (auto& entry : ray.dual_values) {
551 entry.second /= norm;
552 }
553 for (auto& entry : ray.reduced_costs) {
554 entry.second /= norm;
555 }
556 }
557 return ray;
558}
559
560class DualRayMatcher : public RayMatcher<DualRay> {
561 public:
562 DualRayMatcher(DualRay expected, const double tolerance)
563 : RayMatcher(std::move(expected), tolerance) {}
564
565 bool MatchAndExplain(DualRay actual, MatchResultListener* os) const override {
566 auto normalized_actual = NormalizeDualRay(actual);
567 auto normalized_expected = NormalizeDualRay(expected_);
568 if (os->IsInterested()) {
569 *os << "actual normalized: " << PrintToString(normalized_actual)
570 << ", expected normalized: " << PrintToString(normalized_expected);
571 }
572 return ExplainMatchResult(
573 IsNear(normalized_expected.dual_values, tolerance_),
574 normalized_actual.dual_values, os) &&
575 ExplainMatchResult(
576 IsNear(normalized_expected.reduced_costs, tolerance_),
577 normalized_actual.reduced_costs, os);
578 }
579};
580
581} // namespace
582
583Matcher<DualRay> IsNear(DualRay expected, const double tolerance) {
584 return Matcher<DualRay>(new DualRayMatcher(std::move(expected), tolerance));
585}
586
588// SolveResult termination reason matchers
590
591Matcher<ObjectiveBounds> ObjectiveBoundsNear(const ObjectiveBounds& expected,
592 const double tolerance) {
593 return AllOf(Field("primal_bound", &ObjectiveBounds::primal_bound,
594 DoubleNear(expected.primal_bound, tolerance)),
595 Field("dual_bound", &ObjectiveBounds::dual_bound,
596 DoubleNear(expected.dual_bound, tolerance)));
598
599Matcher<SolveResult> TerminatesWithOneOf(
600 const std::vector<TerminationReason>& allowed) {
601 return Field("termination", &SolveResult::termination,
602 Field("reason", &Termination::reason, AnyOfArray(allowed)));
603}
604
605Matcher<SolveResult> TerminatesWith(const TerminationReason expected) {
606 return Field("termination", &SolveResult::termination,
607 Field("reason", &Termination::reason, expected));
608}
609
610namespace {
611
612// Returns a matcher matching only Termination.limit.
613//
614// Note that this is different from LimitIs() which tests both the
615// Termination.limit and the Termination.reason.
616//
617// It matches if either the limit is the expected one, or if it is kUndetermined
618// and when allow_limit_undetermined is true.
619testing::Matcher<SolveResult> TerminationLimitIs(
620 const Limit expected, const bool allow_limit_undetermined) {
621 if (allow_limit_undetermined) {
622 return Field("termination", &SolveResult::termination,
623 Field("limit", &Termination::limit,
624 AnyOf(Limit::kUndetermined, expected)));
625 }
626 return Field("termination", &SolveResult::termination,
627 Field("limit", &Termination::limit, expected));
628}
629
630} // namespace
631
632testing::Matcher<SolveResult> TerminatesWithLimit(
633 const Limit expected, const bool allow_limit_undetermined) {
634 std::vector<Matcher<SolveResult>> matchers;
635 matchers.push_back(TerminationLimitIs(expected, allow_limit_undetermined));
636 matchers.push_back(TerminatesWithOneOf(
638 return AllOfArray(matchers);
639}
640
641testing::Matcher<SolveResult> TerminatesWithReasonFeasible(
642 const Limit expected, const bool allow_limit_undetermined) {
643 std::vector<Matcher<SolveResult>> matchers;
644 matchers.push_back(TerminationLimitIs(expected, allow_limit_undetermined));
645 matchers.push_back(TerminatesWith(TerminationReason::kFeasible));
646 return AllOfArray(matchers);
647}
649testing::Matcher<SolveResult> TerminatesWithReasonNoSolutionFound(
650 const Limit expected, const bool allow_limit_undetermined) {
651 std::vector<Matcher<SolveResult>> matchers;
652 matchers.push_back(TerminationLimitIs(expected, allow_limit_undetermined));
654 return AllOfArray(matchers);
655}
656
657template <typename MatcherType>
658std::string MatcherToStringImpl(const MatcherType& matcher, const bool negate) {
659 std::ostringstream os;
660 if (negate) {
661 matcher.DescribeNegationTo(&os);
662 } else {
663 matcher.DescribeTo(&os);
664 }
665 return os.str();
666}
667
668template <typename T>
669std::string MatcherToString(const Matcher<T>& matcher, bool negate) {
670 return MatcherToStringImpl(matcher, negate);
671}
672
673// clang-format off
674// Polymorphic matchers do not always define DescribeTo,
675// The <T> type may not be a matcher, but it will implement DescribeTo.
676// clang-format on
677template <typename T>
678std::string MatcherToString(const ::testing::PolymorphicMatcher<T>& matcher,
679 bool negate) {
680 return MatcherToStringImpl(matcher.impl(), negate);
681}
682
683MATCHER_P(FirstElementIs, first_element_matcher,
684 (negation
685 ? absl::StrCat("is empty or first element ",
686 MatcherToString(first_element_matcher, true))
687 : absl::StrCat("has at least one element and first element ",
688 MatcherToString(first_element_matcher, false)))) {
689 return ExplainMatchResult(UnorderedElementsAre(first_element_matcher),
690 absl::MakeSpan(arg).subspan(0, 1), result_listener);
691}
692
693Matcher<Termination> LimitIs(math_opt::Limit limit,
694 const Matcher<std::string> detail_matcher) {
695 return AllOf(Field("reason", &Termination::reason,
698 Field("limit", &Termination::limit, limit),
699 Field("detail", &Termination::detail, detail_matcher));
700}
701
702Matcher<Termination> ReasonIs(TerminationReason reason) {
703 return Field("reason", &Termination::reason, reason);
704}
705
706Matcher<Termination> ReasonIsOptimal() {
708}
710Matcher<ProblemStatus> ProblemStatusIs(const ProblemStatus& expected) {
711 return AllOf(
712 Field("primal_status", &ProblemStatus::primal_status,
713 expected.primal_status),
714 Field("dual_status", &ProblemStatus::dual_status, expected.dual_status),
715 Field("primal_or_dual_infeasible",
717 expected.primal_or_dual_infeasible));
719
720Matcher<Termination> TerminationIsOptimal() {
721 return AllOf(
725 .dual_status = FeasibilityStatus::kFeasible,
726 .primal_or_dual_infeasible = false})));
727}
728
729Matcher<Termination> TerminationIsOptimal(
730 const double primal_objective_value,
731 const std::optional<double> dual_objective_value, const double tolerance) {
732 return AllOf(
734 Field("objective_bounds", &Termination::objective_bounds,
735 ObjectiveBoundsNear({.primal_bound = primal_objective_value,
736 .dual_bound = dual_objective_value.value_or(
737 primal_objective_value)},
738 tolerance)));
739}
740
741Matcher<Termination> TerminationIsIgnoreDetail(const Termination& expected) {
742 return AllOf(Field("reason", &Termination::reason, expected.reason),
743 Field("limit", &Termination::limit, expected.limit));
744}
746Matcher<SolveResult> IsOptimal(
747 const std::optional<double> expected_primal_objective,
748 const double tolerance) {
749 if (expected_primal_objective.has_value()) {
750 return AllOf(
751 Field("termination", &SolveResult::termination, TerminationIsOptimal()),
752 Property("has_primal_feasible_solution",
753 &SolveResult::has_primal_feasible_solution, true),
754 Property("objective_value", &SolveResult::objective_value,
755 DoubleNear(*expected_primal_objective, tolerance)));
756 }
757 return Field("termination", &SolveResult::termination,
759}
760
761Matcher<SolveResult> IsOptimalWithSolution(
762 const double expected_objective,
763 const VariableMap<double> expected_variable_values,
764 const double tolerance) {
765 return AllOf(
766 IsOptimal(std::make_optional(expected_objective), tolerance),
768 PrimalSolution{.variable_values = expected_variable_values,
769 .objective_value = expected_objective,
770 .feasibility_status = SolutionStatus::kFeasible},
771 tolerance));
772}
773
774Matcher<SolveResult> IsOptimalWithDualSolution(
775 const double expected_objective,
776 const LinearConstraintMap<double> expected_dual_values,
777 const VariableMap<double> expected_reduced_costs, const double tolerance) {
778 return AllOf(
779 IsOptimal(std::make_optional(expected_objective), tolerance),
782 .dual_values = expected_dual_values,
783 .reduced_costs = expected_reduced_costs,
784 .objective_value = std::make_optional(expected_objective),
785 .feasibility_status = SolutionStatus::kFeasible},
786 tolerance));
787}
788
789Matcher<SolveResult> IsOptimalWithDualSolution(
790 const double expected_objective,
791 const LinearConstraintMap<double> expected_dual_values,
792 const absl::flat_hash_map<QuadraticConstraint, double>
793 expected_quadratic_dual_values,
794 const VariableMap<double> expected_reduced_costs, const double tolerance) {
795 return AllOf(
796 IsOptimal(std::make_optional(expected_objective), tolerance),
799 .dual_values = expected_dual_values,
800 .quadratic_dual_values = expected_quadratic_dual_values,
801 .reduced_costs = expected_reduced_costs,
802 .objective_value = std::make_optional(expected_objective),
803 .feasibility_status = SolutionStatus::kFeasible},
804 tolerance));
806
807Matcher<SolveResult> HasSolution(PrimalSolution expected,
808 const double tolerance) {
809 return Field(
810 "solutions", &SolveResult::solutions,
811 Contains(Field("primal_solution", &Solution::primal_solution,
812 Optional(IsNear(std::move(expected), tolerance)))));
813}
814
815Matcher<SolveResult> HasDualSolution(DualSolution expected,
816 const double tolerance) {
817 return Field(
818 "solutions", &SolveResult::solutions,
819 Contains(Field("dual_solution", &Solution::dual_solution,
820 Optional(IsNear(std::move(expected), tolerance)))));
821}
822
823Matcher<SolveResult> HasPrimalRay(PrimalRay expected, const double tolerance) {
824 return Field("primal_rays", &SolveResult::primal_rays,
825 Contains(IsNear(std::move(expected), tolerance)));
826}
827
828Matcher<SolveResult> HasPrimalRay(VariableMap<double> expected_vars,
829 const double tolerance) {
830 PrimalRay ray;
831 ray.variable_values = std::move(expected_vars);
832 return HasPrimalRay(std::move(ray), tolerance);
833}
834
835Matcher<SolveResult> HasDualRay(DualRay expected, const double tolerance) {
836 return Field("dual_rays", &SolveResult::dual_rays,
837 Contains(IsNear(std::move(expected), tolerance)));
838}
840namespace {
841
842bool MightTerminateWithRays(const TerminationReason reason) {
843 switch (reason) {
847 return true;
848 default:
849 return false;
850 }
852
853std::vector<TerminationReason> CompatibleReasons(
854 const TerminationReason expected, const bool inf_or_unb_soft_match) {
855 if (!inf_or_unb_soft_match) {
856 return {expected};
857 }
858 switch (expected) {
868 default:
869 return {expected};
870 }
871}
872
873Matcher<std::vector<Solution>> CheckSolutions(
874 const std::vector<Solution>& expected_solutions,
875 const SolveResultMatcherOptions& options) {
876 if (options.first_solution_only && !expected_solutions.empty()) {
877 return FirstElementIs(
878 IsNear(expected_solutions[0],
879 SolutionMatcherOptions{.tolerance = options.tolerance,
880 .check_primal = true,
881 .check_dual = options.check_dual,
882 .check_basis = options.check_basis}));
883 }
884 return IsNear(expected_solutions,
885 SolutionMatcherOptions{.tolerance = options.tolerance,
886 .check_primal = true,
887 .check_dual = options.check_dual,
888 .check_basis = options.check_basis});
889}
890
891template <typename RayType>
892Matcher<std::vector<RayType>> AnyRayNear(
893 const std::vector<RayType>& expected_rays, const double tolerance) {
894 std::vector<Matcher<RayType>> matchers;
895 for (const RayType& ray : expected_rays) {
896 matchers.push_back(IsNear(ray, tolerance));
897 }
898 return Contains(AnyOfArray(matchers));
899}
900
901template <typename RayType>
902Matcher<std::vector<RayType>> AllRaysNear(
903 const std::vector<RayType>& expected_rays, const double tolerance) {
904 std::vector<Matcher<RayType>> matchers;
905 for (const RayType& ray : expected_rays) {
906 matchers.push_back(IsNear(ray, tolerance));
907 }
908 return ::testing::UnorderedElementsAreArray(matchers);
909}
910
911template <typename RayType>
912Matcher<std::vector<RayType>> CheckRays(
913 const std::vector<RayType>& expected_rays, const double tolerance,
914 bool check_all) {
915 if (expected_rays.empty()) {
916 return IsEmpty();
917 }
918 if (check_all) {
919 return AllRaysNear(expected_rays, tolerance);
920 }
921 return AnyRayNear(expected_rays, tolerance);
922}
923
924} // namespace
925
926Matcher<SolveResult> IsConsistentWith(
927 const SolveResult& expected, const SolveResultMatcherOptions& options) {
928 std::vector<Matcher<SolveResult>> to_check;
929 to_check.push_back(TerminatesWithOneOf(CompatibleReasons(
930 expected.termination.reason, options.inf_or_unb_soft_match)));
931 const bool skip_solution =
932 MightTerminateWithRays(expected.termination.reason) &&
933 !options.check_solutions_if_inf_or_unbounded;
934 if (!skip_solution) {
935 to_check.push_back(Field("solutions", &SolveResult::solutions,
936 CheckSolutions(expected.solutions, options)));
937 }
938 if (options.check_rays) {
939 to_check.push_back(Field("primal_rays", &SolveResult::primal_rays,
940 CheckRays(expected.primal_rays, options.tolerance,
941 !options.first_solution_only)));
942 to_check.push_back(Field("dual_rays", &SolveResult::dual_rays,
943 CheckRays(expected.dual_rays, options.tolerance,
944 !options.first_solution_only)));
945 }
946
947 return AllOfArray(to_check);
948}
949
951// ComputeInfeasibleSubsystemResult matchers
953
954testing::Matcher<ComputeInfeasibleSubsystemResult> IsFeasible() {
955 return AllOf(
958 Field("infeasible_subsystem",
960 Property("empty", &ModelSubset::empty, true)),
962 false));
963}
964
965testing::Matcher<ComputeInfeasibleSubsystemResult> IsUndetermined() {
966 return AllOf(
969 Field("infeasible_subsystem",
971 Property("empty", &ModelSubset::empty, true)),
973 false));
974}
975
976testing::Matcher<ComputeInfeasibleSubsystemResult> IsInfeasible(
977 const std::optional<bool> expected_is_minimal,
978 const std::optional<ModelSubset> expected_infeasible_subsystem) {
979 std::vector<Matcher<ComputeInfeasibleSubsystemResult>> matchers;
980 matchers.push_back(Field("feasibility",
983 matchers.push_back(
984 Field("infeasible_subsystem",
986 Property("empty", &ModelSubset::empty, false)));
987 if (expected_is_minimal.has_value()) {
988 matchers.push_back(Field("is_minimal",
990 Eq(expected_is_minimal.value())));
991 }
992 if (expected_infeasible_subsystem.has_value()) {
993 matchers.push_back(
994 Field("infeasible_subsystem",
996 Property(&ModelSubset::Proto,
998 expected_infeasible_subsystem.value().Proto()))));
999 }
1000 return AllOfArray(matchers);
1001}
1002
1004// Rarely used
1006
1007Matcher<UpdateResult> DidUpdate() {
1008 return Field("did_update", &UpdateResult::did_update, true);
1009}
1010
1011} // namespace math_opt
1012} // namespace operations_research
const VariableMap< double > & terms() const
Returns the terms in this expression.
const QuadraticTermMap< double > & quadratic_terms() const
int64_t value
double solution
Fractional InfinityNorm(const DenseColumn &v)
Returns the maximum of the coefficients of 'v'.
Definition lp_utils.cc:151
Matcher< SolveResult > IsOptimalWithSolution(const double expected_objective, const VariableMap< double > expected_variable_values, const double tolerance)
Definition matchers.cc:777
Matcher< SolveResult > HasPrimalRay(PrimalRay expected, const double tolerance)
Definition matchers.cc:839
Matcher< Termination > TerminationIsOptimal()
Definition matchers.cc:736
absl::flat_hash_map< Variable, V > VariableMap
Matcher< Termination > ReasonIsOptimal()
Definition matchers.cc:722
Matcher< ObjectiveBounds > ObjectiveBoundsNear(const ObjectiveBounds &expected, const double tolerance)
Definition matchers.cc:607
Matcher< SolutionStatus > SolutionStatusIs(const SolutionStatus expected, const bool allow_undetermined)
Definition matchers.cc:415
TerminationReason
The reason a call to Solve() terminates.
@ kOptimal
A provably optimal solution (up to numerical tolerances) has been found.
@ kInfeasible
The primal problem has no feasible solutions.
absl::flat_hash_map< LinearConstraint, V > LinearConstraintMap
Matcher< Termination > ReasonIs(TerminationReason reason)
Definition matchers.cc:718
Matcher< SolveResult > TerminatesWithOneOf(const std::vector< TerminationReason > &allowed)
Checks that the result has one of the allowed termination reasons.
Definition matchers.cc:615
Matcher< ProblemStatus > ProblemStatusIs(const ProblemStatus &expected)
Definition matchers.cc:726
testing::Matcher< ComputeInfeasibleSubsystemResult > IsInfeasible(const std::optional< bool > expected_is_minimal, const std::optional< ModelSubset > expected_infeasible_subsystem)
Definition matchers.cc:994
Matcher< Termination > LimitIs(math_opt::Limit limit, const Matcher< std::string > detail_matcher)
Definition matchers.cc:709
testing::Matcher< BoundedLinearExpression > IsNearlyEquivalent(const BoundedLinearExpression &expected, const double tolerance)
Definition matchers.cc:310
std::ostream & operator<<(std::ostream &ostr, const IndicatorConstraint &constraint)
Matcher< SolveResult > IsConsistentWith(const SolveResult &expected, const SolveResultMatcherOptions &options)
Definition matchers.cc:942
Matcher< Termination > TerminationIsIgnoreDetail(const Termination &expected)
Definition matchers.cc:757
Matcher< SolveResult > HasSolution(PrimalSolution expected, const double tolerance)
SolveResult has a primal solution matching expected within tolerance.
Definition matchers.cc:823
SolutionStatus
Feasibility of a primal or dual solution as claimed by the solver.
Definition solution.h:39
@ kUndetermined
Solver does not claim a feasibility status.
@ kFeasible
Solver claims the solution is feasible.
Matcher< SolveResult > HasDualRay(DualRay expected, const double tolerance)
Definition matchers.cc:851
testing::Matcher< SolveResult > TerminatesWithReasonFeasible(const Limit expected, const bool allow_limit_undetermined)
Definition matchers.cc:657
Matcher< SolveResult > TerminatesWith(const TerminationReason expected)
Definition matchers.cc:621
std::string MatcherToString(const Matcher< T > &matcher, bool negate)
Definition matchers.cc:685
testing::Matcher< SolveResult > TerminatesWithLimit(const Limit expected, const bool allow_limit_undetermined)
Definition matchers.cc:648
Matcher< PrimalRay > PrimalRayIsNear(VariableMap< double > expected_var_values, const double tolerance)
Definition matchers.cc:538
std::string MatcherToStringImpl(const MatcherType &matcher, const bool negate)
Definition matchers.cc:674
testing::Matcher< LinearExpression > IsIdentical(LinearExpression expected)
Definition matchers.cc:278
Matcher< VariableMap< double > > IsNear(VariableMap< double > expected, const double tolerance)
Definition matchers.cc:221
Matcher< VariableMap< double > > IsNearlySubsetOf(VariableMap< double > expected, double tolerance)
Definition matchers.cc:215
Matcher< UpdateResult > DidUpdate()
Actual UpdateResult.did_update is true.
Definition matchers.cc:1027
void PrintTo(const PrimalSolution &primal_solution, std::ostream *const os)
Definition matchers.cc:88
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))))
Definition matchers.cc:699
testing::Matcher< LinearExpression > LinearExpressionIsNear(const LinearExpression expected, const double tolerance)
Definition matchers.cc:282
Matcher< SolveResult > IsOptimalWithDualSolution(const double expected_objective, const LinearConstraintMap< double > expected_dual_values, const VariableMap< double > expected_reduced_costs, const double tolerance)
Definition matchers.cc:790
Matcher< SolveResult > HasDualSolution(DualSolution expected, const double tolerance)
Definition matchers.cc:831
Matcher< Basis > BasisIs(const Basis &expected)
Definition matchers.cc:449
testing::Matcher< ComputeInfeasibleSubsystemResult > IsFeasible()
Definition matchers.cc:972
testing::Matcher< ComputeInfeasibleSubsystemResult > IsUndetermined()
Definition matchers.cc:983
Matcher< SolveResult > IsOptimal(const std::optional< double > expected_primal_objective, const double tolerance)
Definition matchers.cc:762
@ kUndetermined
Solver does not claim a status.
@ kFeasible
Solver claims the problem is feasible.
@ kInfeasible
Solver claims the problem is infeasible.
testing::Matcher< SolveResult > TerminatesWithReasonNoSolutionFound(const Limit expected, const bool allow_limit_undetermined)
Definition matchers.cc:665
In SWIG mode, we don't want anything besides these top-level includes.
STL namespace.
internal::ProtoMatcher EqualsProto(const ::google::protobuf::Message &message)
LinearConstraintMap< BasisStatus > constraint_status
Definition solution.h:234
VariableMap< BasisStatus > variable_status
Definition solution.h:235
std::optional< SolutionStatus > basic_dual_feasibility
Definition solution.h:249
FeasibilityStatus feasibility
The primal feasibility status of the model, as determined by the solver.
LinearConstraintMap< double > dual_values
Definition solution.h:188
VariableMap< double > reduced_costs
Definition solution.h:189
LinearConstraintMap< double > dual_values
Definition solution.h:147
absl::flat_hash_map< QuadraticConstraint, double > quadratic_dual_values
Definition solution.h:152
std::optional< double > objective_value
Definition solution.h:154
bool empty() const
True if this object corresponds to the empty subset.
FeasibilityStatus primal_status
Status for the primal problem.
FeasibilityStatus dual_status
Status for the dual problem (or for the dual of a continuous relaxation).
std::optional< PrimalSolution > primal_solution
Definition solution.h:270
std::optional< DualSolution > dual_solution
Definition solution.h:271
ProblemStatus problem_status
Feasibility statuses for primal and dual problems.
ObjectiveBounds objective_bounds
Bounds on the optimal objective value.