From cd1e38ac4eaa70e823a4bd6430d2f628737459be Mon Sep 17 00:00:00 2001 From: Guillaume Chatelet Date: Wed, 22 Jul 2026 12:01:11 +0000 Subject: [PATCH 1/3] Add more util tests --- ortools/util/BUILD.bazel | 440 +++++++++++++++ ortools/util/README | 7 - .../util/adaptative_parameter_value_test.cc | 121 ++++ ortools/util/affine_relation_test.cc | 70 +++ ortools/util/cached_log_test.cc | 36 ++ ortools/util/dense_set_benchmark.cc | 100 ++++ ortools/util/dense_set_test.cc | 146 +++++ ortools/util/file_util_test.cc | 396 +++++++++++++ ortools/util/flat_matrix_benchmark.cc | 50 ++ ortools/util/flat_matrix_test.cc | 212 +++++++ .../util/fp_roundtrip_conv_testing_test.cc | 39 ++ ortools/util/fp_utils_benchmark.cc | 194 +++++++ ortools/util/fp_utils_test.cc | 502 +++++++++++++++++ ortools/util/lazy_mutable_copy_test.cc | 245 ++++++++ ortools/util/logging_test.cc | 130 +++++ ortools/util/permutation_test.cc | 35 ++ ortools/util/random_engine_test.cc | 27 + ortools/util/random_subset.h | 231 ++++++++ ortools/util/random_subset_benchmark.cc | 47 ++ ortools/util/random_subset_test.cc | 526 ++++++++++++++++++ ortools/util/range_query_function_test.cc | 238 ++++++++ ortools/util/rational_approximation_test.cc | 54 ++ ortools/util/rev_test.cc | 230 ++++++++ ortools/util/running_stat_test.cc | 121 ++++ .../util/saturated_arithmetic_benchmark.cc | 127 +++++ ortools/util/saturated_arithmetic_test.cc | 229 ++++++++ ortools/util/solve_interrupter_test.cc | 204 +++++++ ortools/util/status_macros_test.cc | 61 ++ ortools/util/status_streaming_test.cc | 90 +++ ortools/util/time_limit_benchmark.cc | 104 ++++ ortools/util/time_limit_test.cc | 425 ++++++++++++++ ortools/util/tuple_set_test.cc | 236 ++++++++ ortools/util/zvector_test.cc | 171 ++++++ 33 files changed, 5837 insertions(+), 7 deletions(-) delete mode 100644 ortools/util/README create mode 100644 ortools/util/adaptative_parameter_value_test.cc create mode 100644 ortools/util/affine_relation_test.cc create mode 100644 ortools/util/cached_log_test.cc create mode 100644 ortools/util/dense_set_benchmark.cc create mode 100644 ortools/util/dense_set_test.cc create mode 100644 ortools/util/file_util_test.cc create mode 100644 ortools/util/flat_matrix_benchmark.cc create mode 100644 ortools/util/flat_matrix_test.cc create mode 100644 ortools/util/fp_roundtrip_conv_testing_test.cc create mode 100644 ortools/util/fp_utils_benchmark.cc create mode 100644 ortools/util/fp_utils_test.cc create mode 100644 ortools/util/lazy_mutable_copy_test.cc create mode 100644 ortools/util/logging_test.cc create mode 100644 ortools/util/permutation_test.cc create mode 100644 ortools/util/random_engine_test.cc create mode 100644 ortools/util/random_subset.h create mode 100644 ortools/util/random_subset_benchmark.cc create mode 100644 ortools/util/random_subset_test.cc create mode 100644 ortools/util/range_query_function_test.cc create mode 100644 ortools/util/rational_approximation_test.cc create mode 100644 ortools/util/rev_test.cc create mode 100644 ortools/util/running_stat_test.cc create mode 100644 ortools/util/saturated_arithmetic_benchmark.cc create mode 100644 ortools/util/saturated_arithmetic_test.cc create mode 100644 ortools/util/solve_interrupter_test.cc create mode 100644 ortools/util/status_macros_test.cc create mode 100644 ortools/util/status_streaming_test.cc create mode 100644 ortools/util/time_limit_benchmark.cc create mode 100644 ortools/util/time_limit_test.cc create mode 100644 ortools/util/tuple_set_test.cc create mode 100644 ortools/util/zvector_test.cc diff --git a/ortools/util/BUILD.bazel b/ortools/util/BUILD.bazel index 9d2fc2e4a51..343273aa916 100644 --- a/ortools/util/BUILD.bazel +++ b/ortools/util/BUILD.bazel @@ -15,7 +15,9 @@ load("@protobuf//bazel:cc_proto_library.bzl", "cc_proto_library") load("@protobuf//bazel:java_proto_library.bzl", "java_proto_library") load("@protobuf//bazel:proto_library.bzl", "proto_library") load("@protobuf//bazel:py_proto_library.bzl", "py_proto_library") +load("@rules_cc//cc:cc_binary.bzl", "cc_binary") load("@rules_cc//cc:cc_library.bzl", "cc_library") +load("@rules_cc//cc:cc_test.bzl", "cc_test") load("@rules_go//proto:def.bzl", "go_proto_library") load("//ortools/copts:configure_copts.bzl", "ORTOOLS_DEFAULT_COPTS", "ORTOOLS_DEFAULT_LINKOPTS", "ORTOOLS_TEST_COPTS") @@ -453,6 +455,25 @@ cc_proto_library( deps = [":file_util_test_proto"], ) +cc_test( + name = "file_util_test", + size = "small", + srcs = ["file_util_test.cc"], + copts = ORTOOLS_TEST_COPTS, + linkopts = ORTOOLS_DEFAULT_LINKOPTS, + deps = [ + ":file_util", + ":file_util_test_cc_proto", + "//ortools/base:file", + "//ortools/base:gmock_main", + "//ortools/base:gzipstring", + "//ortools/base:parse_test_proto", + "//ortools/base:temp_file", + "@abseil-cpp//absl/status", + "@abseil-cpp//absl/strings", + "@protobuf", + ], +) cc_library( name = "random_engine", @@ -626,6 +647,369 @@ cc_library( deps = ["@abseil-cpp//absl/log:check"], ) +cc_test( + name = "dense_set_test", + srcs = ["dense_set_test.cc"], + copts = ORTOOLS_TEST_COPTS, + linkopts = ORTOOLS_DEFAULT_LINKOPTS, + deps = [ + ":dense_set", + "//ortools/base:gmock_main", + "//ortools/base:strong_int", + "@abseil-cpp//absl/container:flat_hash_set", + "@abseil-cpp//absl/random", + "@abseil-cpp//absl/random:bit_gen_ref", + ], +) + +cc_binary( + name = "dense_set_benchmark", + srcs = ["dense_set_benchmark.cc"], + copts = ORTOOLS_DEFAULT_COPTS, + linkopts = ORTOOLS_DEFAULT_LINKOPTS, + deps = [ + ":dense_set", + "//ortools/base:benchmark_main", + "@abseil-cpp//absl/container:flat_hash_set", + "@abseil-cpp//absl/random", + "@abseil-cpp//absl/random:bit_gen_ref", + "@google_benchmark//:benchmark", + ], +) + +cc_test( + name = "solve_interrupter_test", + srcs = ["solve_interrupter_test.cc"], + copts = ORTOOLS_TEST_COPTS, + linkopts = ORTOOLS_DEFAULT_LINKOPTS, + deps = [ + ":solve_interrupter", + "//ortools/base:gmock_main", + "@abseil-cpp//absl/strings", + ], +) + +cc_test( + name = "fp_roundtrip_conv_testing_test", + srcs = ["fp_roundtrip_conv_testing_test.cc"], + copts = ORTOOLS_TEST_COPTS, + linkopts = ORTOOLS_DEFAULT_LINKOPTS, + deps = [ + ":fp_roundtrip_conv", + ":fp_roundtrip_conv_testing", + "//ortools/base:gmock_main", + ], +) + +cc_test( + name = "flat_matrix_test", + srcs = ["flat_matrix_test.cc"], + copts = ORTOOLS_TEST_COPTS, + linkopts = ORTOOLS_DEFAULT_LINKOPTS, + deps = [ + ":flat_matrix", + ":random_engine", + "//ortools/base:dump_vars", + "//ortools/base:gmock_main", + "@abseil-cpp//absl/base", + "@abseil-cpp//absl/log:check", + ], +) + +cc_binary( + name = "flat_matrix_benchmark", + srcs = ["flat_matrix_benchmark.cc"], + copts = ORTOOLS_DEFAULT_COPTS, + linkopts = ORTOOLS_DEFAULT_LINKOPTS, + deps = [ + ":flat_matrix", + ":random_engine", + "//ortools/base:benchmark_main", + "@abseil-cpp//absl/base", + "@abseil-cpp//absl/log:check", + "@google_benchmark//:benchmark", + ], +) + +cc_test( + name = "status_macros_test", + srcs = ["status_macros_test.cc"], + copts = ORTOOLS_TEST_COPTS, + linkopts = ORTOOLS_DEFAULT_LINKOPTS, + deps = [ + ":status_macros", + "//ortools/base:gmock_main", + "@abseil-cpp//absl/status", + "@abseil-cpp//absl/status:status_macros", + "@abseil-cpp//absl/status:statusor", + ], +) + +cc_test( + name = "logging_test", + size = "small", + srcs = ["logging_test.cc"], + copts = ORTOOLS_TEST_COPTS, + linkopts = ORTOOLS_DEFAULT_LINKOPTS, + deps = [ + ":logging", + "//ortools/base:gmock_main", + "//ortools/port:scoped_std_stream_capture", + "@abseil-cpp//absl/log:check", + "@abseil-cpp//absl/strings", + ], +) + +cc_test( + name = "lazy_mutable_copy_test", + srcs = ["lazy_mutable_copy_test.cc"], + copts = ORTOOLS_TEST_COPTS, + linkopts = ORTOOLS_DEFAULT_LINKOPTS, + deps = [ + ":lazy_mutable_copy", + "//ortools/base:gmock_main", + "@abseil-cpp//absl/strings", + ], +) + +cc_test( + name = "adaptative_parameter_value_test", + srcs = ["adaptative_parameter_value_test.cc"], + copts = ORTOOLS_TEST_COPTS, + linkopts = ORTOOLS_DEFAULT_LINKOPTS, + deps = [ + ":adaptative_parameter_value", + "//ortools/base:gmock_main", + "@abseil-cpp//absl/random", + ], +) + +cc_test( + name = "random_engine_test", + srcs = ["random_engine_test.cc"], + copts = ORTOOLS_TEST_COPTS, + linkopts = ORTOOLS_DEFAULT_LINKOPTS, + deps = [ + ":random_engine", + "//ortools/base:gmock_main", + ], +) + +cc_test( + name = "rev_test", + srcs = ["rev_test.cc"], + copts = ORTOOLS_TEST_COPTS, + linkopts = ORTOOLS_DEFAULT_LINKOPTS, + deps = [ + ":rev", + ":strong_integers", + "//ortools/base:gmock_main", + "@abseil-cpp//absl/container:btree", + "@abseil-cpp//absl/container:flat_hash_map", + "@abseil-cpp//absl/random:distributions", + ], +) + +cc_test( + name = "range_query_function_test", + srcs = ["range_query_function_test.cc"], + copts = ORTOOLS_TEST_COPTS, + linkopts = ORTOOLS_DEFAULT_LINKOPTS, + deps = [ + ":range_query_function", + "//ortools/base:gmock_main", + "//ortools/base:types", + "@abseil-cpp//absl/log:check", + ], +) + +cc_test( + name = "running_stat_test", + size = "small", + srcs = ["running_stat_test.cc"], + copts = ORTOOLS_TEST_COPTS, + linkopts = ORTOOLS_DEFAULT_LINKOPTS, + deps = [ + ":running_stat", + "//ortools/base:gmock_main", + ], +) + +cc_test( + name = "fp_utils_test", + size = "small", + srcs = ["fp_utils_test.cc"], + copts = ORTOOLS_TEST_COPTS, + linkopts = ORTOOLS_DEFAULT_LINKOPTS, + deps = [ + ":fp_utils", + "//ortools/base:gmock_main", + "//ortools/base:types", + "@abseil-cpp//absl/base", + "@abseil-cpp//absl/log", + "@abseil-cpp//absl/log:check", + "@abseil-cpp//absl/random", + "@abseil-cpp//absl/strings:str_format", + "@abseil-cpp//absl/types:span", + ], +) + +cc_binary( + name = "fp_utils_benchmark", + srcs = ["fp_utils_benchmark.cc"], + copts = ORTOOLS_DEFAULT_COPTS, + linkopts = ORTOOLS_DEFAULT_LINKOPTS, + deps = [ + ":fp_utils", + "//ortools/base:benchmark_main", + "@abseil-cpp//absl/log:check", + "@google_benchmark//:benchmark", + ], +) + +cc_test( + name = "time_limit_test", + size = "small", + srcs = ["time_limit_test.cc"], + copts = ORTOOLS_TEST_COPTS, + flaky = 1, # 2014-05-21: 102 failures over 10000 runs (forge, fastbuild). + linkopts = ORTOOLS_DEFAULT_LINKOPTS, + deps = [ + ":testing_utils", + ":time_limit", + "//ortools/base:gmock_main", + "//ortools/base:log_severity", + "//ortools/base:threadpool", + "@abseil-cpp//absl/base:core_headers", + "@abseil-cpp//absl/flags:flag", + "@abseil-cpp//absl/log:log_streamer", + "@abseil-cpp//absl/random", + "@abseil-cpp//absl/strings", + "@abseil-cpp//absl/strings:str_format", + "@abseil-cpp//absl/time", + ], +) + +cc_binary( + name = "time_limit_benchmark", + srcs = ["time_limit_benchmark.cc"], + copts = ORTOOLS_DEFAULT_COPTS, + linkopts = ORTOOLS_DEFAULT_LINKOPTS, + deps = [ + ":time_limit", + "//ortools/base:benchmark_main", + "@abseil-cpp//absl/log:check", + "@abseil-cpp//absl/strings", + "@google_benchmark//:benchmark", + ], +) + +cc_test( + name = "tuple_set_test", + size = "small", + srcs = ["tuple_set_test.cc"], + copts = ORTOOLS_TEST_COPTS, + linkopts = ORTOOLS_DEFAULT_LINKOPTS, + deps = [ + ":tuple_set", + "//ortools/base:gmock_main", + ], +) + +cc_test( + name = "rational_approximation_test", + size = "small", + srcs = ["rational_approximation_test.cc"], + copts = ORTOOLS_TEST_COPTS, + linkopts = ORTOOLS_DEFAULT_LINKOPTS, + deps = [ + ":fp_utils", + ":rational_approximation", + "//ortools/base:gmock_main", + ], +) + +cc_test( + name = "saturated_arithmetic_test", + srcs = ["saturated_arithmetic_test.cc"], + copts = ORTOOLS_TEST_COPTS, + linkopts = ORTOOLS_DEFAULT_LINKOPTS, + deps = [ + ":saturated_arithmetic", + ":strong_integers", + "//ortools/base:gmock_main", + "//ortools/base:types", + "@abseil-cpp//absl/log", + "@abseil-cpp//absl/numeric:int128", + "@abseil-cpp//absl/random", + ], +) + +cc_binary( + name = "saturated_arithmetic_benchmark", + srcs = ["saturated_arithmetic_benchmark.cc"], + copts = ORTOOLS_DEFAULT_COPTS, + linkopts = ORTOOLS_DEFAULT_LINKOPTS, + deps = [ + ":saturated_arithmetic", + "//ortools/base:benchmark_main", + "//ortools/base:types", + "@abseil-cpp//absl/log", + "@google_benchmark//:benchmark", + ], +) + +cc_test( + name = "permutation_test", + size = "small", + srcs = ["permutation_test.cc"], + copts = ORTOOLS_TEST_COPTS, + linkopts = ORTOOLS_DEFAULT_LINKOPTS, + deps = [ + ":permutation", + "//ortools/base:gmock_main", + ], +) + +cc_test( + name = "zvector_test", + size = "large", + srcs = ["zvector_test.cc"], + copts = ORTOOLS_TEST_COPTS, + linkopts = ORTOOLS_DEFAULT_LINKOPTS, + deps = [ + ":zvector", + "//ortools/base:gmock_main", + "@abseil-cpp//absl/random", + ], +) + +cc_test( + name = "cached_log_test", + size = "small", + srcs = ["cached_log_test.cc"], + copts = ORTOOLS_TEST_COPTS, + linkopts = ORTOOLS_DEFAULT_LINKOPTS, + deps = [ + ":cached_log", + "//ortools/base:gmock_main", + ], +) + +cc_test( + name = "affine_relation_test", + size = "small", + srcs = [ + "affine_relation_test.cc", + ], + copts = ORTOOLS_TEST_COPTS, + linkopts = ORTOOLS_DEFAULT_LINKOPTS, + deps = [ + ":affine_relation", + "//ortools/base:gmock_main", + ], +) + cc_library( name = "status_streaming", srcs = ["status_streaming.cc"], @@ -643,6 +1027,22 @@ cc_library( ], ) +cc_test( + name = "status_streaming_test", + srcs = ["status_streaming_test.cc"], + copts = ORTOOLS_TEST_COPTS, + linkopts = ORTOOLS_DEFAULT_LINKOPTS, + deps = [ + ":status_cc_proto", + ":status_streaming", + "//ortools/base:gmock_main", + "//ortools/base:log_severity", + "@abseil-cpp//absl/log:scoped_mock_log", + "@abseil-cpp//absl/status", + "@abseil-cpp//absl/types:source_location", + ], +) + cc_library( name = "random_subset", hdrs = ["random_subset.h"], @@ -673,3 +1073,43 @@ cc_library( "@rules_cc//cc/runfiles", ], ) + +cc_test( + name = "random_subset_test", + srcs = ["random_subset_test.cc"], + copts = ORTOOLS_TEST_COPTS, + linkopts = ORTOOLS_DEFAULT_LINKOPTS, + tags = ["noci"], + deps = [ + ":random_engine", + ":random_subset", + ":testing_utils", + "//ortools/algorithms:n_choose_k", + "//ortools/base:dump_vars", + "//ortools/base:gmock_main", + "//ortools/base:log_severity", + "//ortools/base:mathutil", + "@abseil-cpp//absl/algorithm:container", + "@abseil-cpp//absl/container:btree", + "@abseil-cpp//absl/container:flat_hash_map", + "@abseil-cpp//absl/container:flat_hash_set", + "@abseil-cpp//absl/numeric:int128", + "@abseil-cpp//absl/random", + "@abseil-cpp//absl/random:distributions", + "@abseil-cpp//absl/strings:str_format", + "@abseil-cpp//absl/time", + ], +) + +cc_binary( + name = "random_subset_benchmark", + srcs = ["random_subset_benchmark.cc"], + copts = ORTOOLS_DEFAULT_COPTS, + linkopts = ORTOOLS_DEFAULT_LINKOPTS, + deps = [ + ":random_subset", + "//ortools/base:benchmark_main", + "@abseil-cpp//absl/random", + "@google_benchmark//:benchmark", + ], +) diff --git a/ortools/util/README b/ortools/util/README deleted file mode 100644 index e33c2e52792..00000000000 --- a/ortools/util/README +++ /dev/null @@ -1,7 +0,0 @@ - Various utilities to be used by libraries - -This directory contains various utilities needed by the operations -research libraries. - - - const_int_array.h : A read-only container of integer values. - - const_ptr_array.h : A read-only container of pointers. diff --git a/ortools/util/adaptative_parameter_value_test.cc b/ortools/util/adaptative_parameter_value_test.cc new file mode 100644 index 00000000000..814fb01641b --- /dev/null +++ b/ortools/util/adaptative_parameter_value_test.cc @@ -0,0 +1,121 @@ +// Copyright 2010-2025 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/util/adaptative_parameter_value.h" + +#include + +#include "absl/random/random.h" +#include "gtest/gtest.h" + +namespace operations_research { +namespace { + +TEST(AdaptiveParameterValueTest, StayBelowOne) { + AdaptiveParameterValue param(0.5); + for (int i = 0; i < 1e6; ++i) param.Increase(); + EXPECT_GE(param.value(), 0.99); + EXPECT_LE(param.value(), 1.0); +} + +TEST(AdaptiveParameterValueTest, StayAboveZero) { + AdaptiveParameterValue param(0.5); + for (int i = 0; i < 1e6; ++i) param.Decrease(); + EXPECT_GE(param.value(), 0.0); + EXPECT_LE(param.value(), 0.001); +} + +TEST(AdaptiveParameterValueTest, ConvergeToTarget) { + absl::BitGen random; + AdaptiveParameterValue param(0.5); + for (int i = 0; i < 50; ++i) { + const double target = absl::Uniform(random, 0.0, 1.0); + for (int j = 0; j < 10000; ++j) { + if (param.value() < target) param.Increase(); + if (param.value() > target) param.Decrease(); + } + + // We expect convergence even after a lot of updates. + EXPECT_GE(param.value(), target - 0.1); + EXPECT_LE(param.value(), target + 0.1); + } +} + +TEST(AdaptiveParameterValueTest, ExponentialConvergenceToOne) { + AdaptiveParameterValue param(0.5); + for (int i = 0; i < 100; ++i) param.Increase(); + EXPECT_GE(param.value(), 1.0 - 1e-6); +} + +TEST(AdaptiveParameterValueTest, ExponentialConvergenceToZero) { + AdaptiveParameterValue param(0.5); + for (int i = 0; i < 100; ++i) param.Decrease(); + EXPECT_LE(param.value(), 1e-6); +} + +TEST(AdaptiveParameterValueTest, ConvergenceToMedian) { + AdaptiveParameterValue param(0.5); + + // The test should work (modulo tolerances) for any increasing function f in + // [0, 1] with f(0) = 0 and f(1) = 1. + const auto f = [](double x) { return x * x; }; + + // The final value should be around f(param.value) = 0.5 ! + // The convergence is pretty slow though. + absl::BitGen random; + for (int i = 0; i < 1e6; ++i) { + const double decrease = absl::Bernoulli(random, f(param.value())); + if (decrease) { + param.Decrease(); + } else { + param.Increase(); + } + } + + // No flakiness in 1000 runs with this tolerance. + EXPECT_NEAR(param.value(), sqrt(0.5), 0.05); +} + +TEST(AdaptiveParameterValueTest, UpdateVsIncrease) { + AdaptiveParameterValue param1(0.1); + AdaptiveParameterValue param2(0.1); + param1.Increase(); + param1.Increase(); + param1.Increase(); + param2.Update(/*num_decreases=*/0, /*num_increases=*/3); + EXPECT_EQ(param1.value(), param2.value()); +} + +TEST(AdaptiveParameterValueTest, UpdateVsDecrease) { + AdaptiveParameterValue param1(0.1); + AdaptiveParameterValue param2(0.1); + param1.Decrease(); + param1.Decrease(); + param2.Update(/*num_decreases=*/2, /*num_increases=*/0); + EXPECT_EQ(param1.value(), param2.value()); +} + +TEST(AdaptiveParameterValueTest, GenericUpdate) { + AdaptiveParameterValue param1(0.1); + AdaptiveParameterValue param2(0.1); + param1.Decrease(); + param1.Increase(); + param1.Increase(); + param1.Decrease(); + param2.Update(/*num_decreases=*/2, /*num_increases=*/2); + EXPECT_EQ(param2.value(), 0.1); + EXPECT_NE(param1.value(), param2.value()); +} + +} // namespace +} // namespace operations_research diff --git a/ortools/util/affine_relation_test.cc b/ortools/util/affine_relation_test.cc new file mode 100644 index 00000000000..79c2dffdcb7 --- /dev/null +++ b/ortools/util/affine_relation_test.cc @@ -0,0 +1,70 @@ +// Copyright 2010-2025 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/util/affine_relation.h" + +#include "gtest/gtest.h" + +namespace operations_research { +namespace { + +TEST(AffineRelationTest, Empty) { + AffineRelation r; + EXPECT_EQ(r.Get(10), AffineRelation::Relation(10, 1, 0)); +} + +TEST(AffineRelationTest, CoeffOfMagnitudeOne) { + AffineRelation r; + r.TryAdd(1, 0, /*coeff=*/1, /*offset=*/1); // r1 = r0 + 1 + r.TryAdd(2, 0, /*coeff=*/1, /*offset=*/2); // r2 = r0 + 2 + r.TryAdd(3, 0, /*coeff=*/-1, /*offset=*/3); // r3 = -r0 + 3 + r.TryAdd(4, 0, /*coeff=*/-1, /*offset=*/4); // r4 = -r0 + 4 + r.TryAdd(5, 2, /*coeff=*/-1, /*offset=*/-3); // r5 = -r2 - 3 = -r0 - 5 + r.TryAdd(6, 3, /*coeff=*/-1, /*offset=*/-3); // r6 = -r3 - 3 = -r0 - 8 + r.TryAdd(7, 4, /*coeff=*/-1, /*offset=*/-3); // r7 = -r4 - 3 = r0 - 7 + + EXPECT_EQ(r.Get(0), AffineRelation::Relation(0, 1, 0)); + EXPECT_EQ(r.Get(1), AffineRelation::Relation(0, 1, 1)); + EXPECT_EQ(r.Get(2), AffineRelation::Relation(0, 1, 2)); + EXPECT_EQ(r.Get(3), AffineRelation::Relation(0, -1, 3)); + EXPECT_EQ(r.Get(4), AffineRelation::Relation(0, -1, 4)); + EXPECT_EQ(r.Get(5), AffineRelation::Relation(0, -1, -1 * 2 - 3)); + EXPECT_EQ(r.Get(6), AffineRelation::Relation(0, 1, -1 * 3 - 3)); + EXPECT_EQ(r.Get(7), AffineRelation::Relation(0, 1, -1 * 4 - 3)); +} + +TEST(AffineRelationTest, ChainOfMultiple) { + AffineRelation r; + EXPECT_TRUE(r.TryAdd(1, 0, /*coeff=*/2, /*offset=*/1)); + EXPECT_TRUE(r.TryAdd(2, 1, /*coeff=*/2, /*offset=*/1)); + EXPECT_TRUE(r.TryAdd(3, 2, /*coeff=*/2, /*offset=*/1)); + EXPECT_TRUE(r.TryAdd(4, 3, /*coeff=*/2, /*offset=*/1)); + + for (int i = 0; i < 4; ++i) { + EXPECT_EQ(r.Get(i), AffineRelation::Relation(0, 1 << i, (1 << i) - 1)); + } + + EXPECT_TRUE(r.TryAdd(6, 5, /*coeff=*/3, /*offset=*/0)); + + // We can't add any of the following. + EXPECT_FALSE(r.TryAdd(1, 0, 2, 1)); + EXPECT_FALSE(r.TryAdd(3, 4, 2, 1)); + EXPECT_FALSE(r.TryAdd(1, 6, 1, 0)); + EXPECT_FALSE(r.TryAdd(6, 1, 1, 0)); + + // But we can connect the root of the chain. + EXPECT_TRUE(r.TryAdd(0, 6, 1, 0)); +} + +} // namespace +} // namespace operations_research diff --git a/ortools/util/cached_log_test.cc b/ortools/util/cached_log_test.cc new file mode 100644 index 00000000000..431e3de47d4 --- /dev/null +++ b/ortools/util/cached_log_test.cc @@ -0,0 +1,36 @@ +// Copyright 2010-2025 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/util/cached_log.h" + +#include + +#include + +#include "gtest/gtest.h" + +namespace { + +TEST(CachedLogTest, LogAccess) { + const int kSize = 100; + operations_research::CachedLog cache; + for (int64_t i = 1; i < kSize; ++i) { + EXPECT_DOUBLE_EQ(log2(i), cache.Log2(i)); + } + cache.Init(kSize / 2); + for (int64_t i = 1; i < kSize; ++i) { + EXPECT_DOUBLE_EQ(log2(i), cache.Log2(i)); + } +} + +} // namespace diff --git a/ortools/util/dense_set_benchmark.cc b/ortools/util/dense_set_benchmark.cc new file mode 100644 index 00000000000..3d92b96b64c --- /dev/null +++ b/ortools/util/dense_set_benchmark.cc @@ -0,0 +1,100 @@ +// Copyright 2010-2025 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 +#include + +#include "absl/container/flat_hash_set.h" +#include "absl/random/bit_gen_ref.h" +#include "absl/random/random.h" +#include "benchmark/benchmark.h" +#include "ortools/util/dense_set.h" + +namespace operations_research { +namespace { + +template +int64_t DoOneBMIteration(Set& set, absl::BitGenRef gen) { + int64_t sum = 0; + // Don't use generate random numbers inside the innermost loop to avoid + // benchmarking absl::Uniform. + int64_t base = absl::Uniform(gen, 0, 1024 * 1024); + for (int i = 1; i <= 100; ++i) { + set.insert((i * base) % (1024 * 1024)); + } + for (int v : set) { + sum += v; + } + while (set.size() > 1000) { + set.erase(set.begin()); + } + return sum; +} + +template +void BM_SetIteration(benchmark::State& state, Set set = Set()) { + absl::BitGen gen; + int64_t sum = 0; + int base = absl::Uniform(gen, 1, 1024 * 1024); + for (int i = 0; i < 1000; ++i) { + set.insert(i * base % (1024 * 1024)); + } + for (auto s : state) { + sum += DoOneBMIteration(set, gen); + } +} + +void BM_HashSetIteration(benchmark::State& state) { + BM_SetIteration>(state); +} +void BM_DenseSetIteration(benchmark::State& state) { + BM_SetIteration>(state); +} +void BM_HashSetIterationPreReserved(benchmark::State& state) { + absl::flat_hash_set set; + set.reserve(1500); + BM_SetIteration(state, std::move(set)); +} +void BM_DenseSetIterationPreReserved(benchmark::State& state) { + DenseSet set; + set.reserve(1024 * 1024); + BM_SetIteration(state, std::move(set)); +} +void BM_UnsafeDenseSetIterationPreReserved(benchmark::State& state) { + UnsafeDenseSet set; + set.reserve(1024 * 1024); + BM_SetIteration(state, std::move(set)); +} +void BM_HashSetIterationPreReservedRehash(benchmark::State& state) { + absl::flat_hash_set set; + set.reserve(1500); + absl::BitGen gen; + int64_t sum = 0; + for (int i = 0; i < 1000; ++i) { + set.insert(absl::Uniform(gen, 0, 1024 * 1024)); + } + for (auto s : state) { + set.rehash(1500); + sum += DoOneBMIteration(set, gen); + } +} + +BENCHMARK(BM_HashSetIteration); +BENCHMARK(BM_DenseSetIteration); +BENCHMARK(BM_HashSetIterationPreReserved); +BENCHMARK(BM_DenseSetIterationPreReserved); +BENCHMARK(BM_UnsafeDenseSetIterationPreReserved); +BENCHMARK(BM_HashSetIterationPreReservedRehash); + +} // namespace +} // namespace operations_research diff --git a/ortools/util/dense_set_test.cc b/ortools/util/dense_set_test.cc new file mode 100644 index 00000000000..650ace527a5 --- /dev/null +++ b/ortools/util/dense_set_test.cc @@ -0,0 +1,146 @@ +// Copyright 2010-2025 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/util/dense_set.h" + +#include + +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/base/strong_int.h" + +namespace operations_research { +namespace { + +using ::testing::ElementsAre; +using ::testing::IsEmpty; + +DEFINE_STRONG_INT_TYPE(TestStrongInt, int); + +template +class DenseSetTest : public testing::Test {}; + +using SetTypes = + ::testing::Types, DenseSet, + UnsafeDenseSet, UnsafeDenseSet>; + +TYPED_TEST_SUITE(DenseSetTest, SetTypes); + +TYPED_TEST(DenseSetTest, TestInsert) { + TypeParam set; + if (!TypeParam::kAutoResize) set.reserve(2); + + auto [it, inserted] = set.insert(static_cast(1)); + + EXPECT_TRUE(inserted); + EXPECT_TRUE(set.contains(static_cast(1))); + EXPECT_EQ(*it, static_cast(1)); + EXPECT_EQ(set.size(), 1); + EXPECT_THAT(set, ElementsAre(static_cast(1))); + EXPECT_THAT(set.values(), ElementsAre(static_cast(1))); +} + +TYPED_TEST(DenseSetTest, TestInsertDuplicate) { + TypeParam set; + if (!TypeParam::kAutoResize) set.reserve(2); + + auto [it, inserted] = set.insert(static_cast(1)); + auto [it2, inserted2] = set.insert(static_cast(1)); + + EXPECT_TRUE(inserted); + EXPECT_FALSE(inserted2); + EXPECT_EQ(it, it2); +} + +TYPED_TEST(DenseSetTest, TestFindPresent) { + TypeParam set; + if (!TypeParam::kAutoResize) set.reserve(2); + set.insert(static_cast(1)); + + auto it = set.find(static_cast(1)); + + EXPECT_EQ(*it, static_cast(1)); +} + +TYPED_TEST(DenseSetTest, TestFindAbsent) { + TypeParam set; + if (!TypeParam::kAutoResize) set.reserve(3); + set.insert(static_cast(1)); + + auto it = set.find(static_cast(2)); + + EXPECT_EQ(it, set.end()); +} + +TYPED_TEST(DenseSetTest, TestEraseValue) { + TypeParam set; + if (!TypeParam::kAutoResize) set.reserve(2001); + set.insert(static_cast(1)); + + EXPECT_EQ(set.erase(static_cast(0)), 0); + EXPECT_EQ(set.erase(static_cast(2000)), 0); + EXPECT_TRUE(set.contains(static_cast(1))); + EXPECT_EQ(set.erase(static_cast(1)), 1); + EXPECT_FALSE(set.contains(static_cast(1))); + EXPECT_EQ(set.size(), 0); + EXPECT_THAT(set, IsEmpty()); + EXPECT_THAT(set.values(), IsEmpty()); +} + +TYPED_TEST(DenseSetTest, TestEraseIterator) { + TypeParam set; + if (!TypeParam::kAutoResize) set.reserve(2); + auto [it, inserted] = set.insert(static_cast(1)); + + set.erase(it); + + EXPECT_EQ(set.size(), 0); + EXPECT_THAT(set, IsEmpty()); + EXPECT_THAT(set.values(), IsEmpty()); +} + +TYPED_TEST(DenseSetTest, TestClear) { + TypeParam set; + if (!TypeParam::kAutoResize) set.reserve(3); + set.insert(static_cast(1)); + set.insert(static_cast(2)); + + set.clear(); + + EXPECT_EQ(set.size(), 0); + EXPECT_THAT(set, IsEmpty()); + EXPECT_THAT(set.values(), IsEmpty()); +} + +TYPED_TEST(DenseSetTest, TestReserve) { + TypeParam set; + + set.reserve(100); + + EXPECT_EQ(set.size(), 0); + EXPECT_THAT(set, IsEmpty()); + EXPECT_THAT(set.values(), IsEmpty()); + EXPECT_EQ(set.capacity(), 100); +} + +TYPED_TEST(DenseSetTest, TestConstIterators) { + TypeParam set; + if (!TypeParam::kAutoResize) set.reserve(2); + auto [it, inserted] = set.insert(static_cast(1)); + + // Check that `*it = TypeParam::value_type();` would not compile. + EXPECT_FALSE((std::is_assignable::value)); +} + +} // namespace +} // namespace operations_research diff --git a/ortools/util/file_util_test.cc b/ortools/util/file_util_test.cc new file mode 100644 index 00000000000..58206f5ce63 --- /dev/null +++ b/ortools/util/file_util_test.cc @@ -0,0 +1,396 @@ +// Copyright 2010-2025 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/util/file_util.h" + +#include +#include + +#include "absl/status/status.h" +#include "absl/strings/str_replace.h" +#include "absl/strings/string_view.h" +#include "google/protobuf/text_format.h" +#include "gtest/gtest.h" +#include "ortools/base/filesystem.h" +#include "ortools/base/gmock.h" +#include "ortools/base/gzipstring.h" +#include "ortools/base/helpers.h" +#include "ortools/base/options.h" +#include "ortools/base/parse_test_proto.h" +#include "ortools/base/temp_file.h" +#include "ortools/util/file_util_test.pb.h" + +namespace operations_research { +namespace { + +using ::google::protobuf::contrib::parse_proto::ParseTestProto; +using ::testing::AllOf; +using ::testing::EqualsProto; +using ::testing::HasSubstr; +using ::testing::status::IsOkAndHolds; +using ::testing::status::StatusIs; + +class FileUtilsTest : public ::testing::Test { + protected: + void SetUp() override { + ASSERT_OK_AND_ASSIGN(file_name_, + file::MakeTempFilename(::testing::TempDir(), "file")); + ASSERT_OK_AND_ASSIGN(empty_file_name_, + file::MakeTempFilename(::testing::TempDir(), "empty")); + unknown_file_name_ = file_name_ + "_does_not_exist"; + + ASSERT_OK(file::SetContents(empty_file_name_, "", file::Defaults())); + } + + std::string file_name_; + std::string empty_file_name_; + std::string unknown_file_name_; +}; + +typedef FileUtilsTest ReadFileToStringTest; + +TEST_F(ReadFileToStringTest, SmallTextFile) { + constexpr absl::string_view contents = "line 1\nline 2"; + ASSERT_OK(file::SetContents(file_name_, contents, file::Defaults())); + + EXPECT_THAT(ReadFileToString(file_name_), IsOkAndHolds(contents)); +} + +TEST_F(ReadFileToStringTest, EmptyFile) { + EXPECT_THAT(ReadFileToString(empty_file_name_), IsOkAndHolds("")); +} + +TEST_F(ReadFileToStringTest, UnknownFile) { + EXPECT_THAT(ReadFileToString(unknown_file_name_), + StatusIs(absl::StatusCode::kNotFound)); +} + +TEST_F(ReadFileToStringTest, GzippedTextFile) { + constexpr absl::string_view contents = "line 1\nline 2"; + std::string compressed; + GzipString(contents, &compressed); + ASSERT_OK(file::SetContents(file_name_, compressed, file::Defaults())); + + EXPECT_THAT(ReadFileToString(file_name_), IsOkAndHolds(contents)); +} + +TEST(StringToProtoTest, Binary) { + const FileUtilTestProto1 original_proto = ParseTestProto("some_field:42"); + FileUtilTestProto1 parsed_proto; + ASSERT_OK(StringToProto(original_proto.SerializeAsString(), &parsed_proto)); + EXPECT_THAT(parsed_proto, EqualsProto(original_proto)); +} + +TEST(StringToProtoTest, Text) { + FileUtilTestProto1 proto; + ASSERT_OK(StringToProto("some_field:42", &proto)); + EXPECT_THAT(proto, EqualsProto("some_field:42")); +} + +constexpr char kTestTextProto[] = R"pb( + optional_field: 123 + repeated_fields { some_field: 42 } + repeated_fields { some_field: 43 } + repeated_fields { some_field: 44 } + repeated_fields { some_field: 45 } + repeated_fields { some_field: 46 } +)pb"; + +constexpr char kTestJson[] = R"json({ + "optional_field": 123, + "repeated_fields": [ + { + "some_field": 42 + }, + { + "some_field": 43 + }, + { + "some_field": 44 + }, + { + "some_field": 45 + }, + { + "some_field": 46 + } + ] +} +)json"; + +constexpr char kTestJsonProto3[] = R"json({ + "optionalField": 123, + "repeatedFields": [ + { + "someField": 42 + }, + { + "someField": 43 + }, + { + "someField": 44 + }, + { + "someField": 45 + }, + { + "someField": 46 + } + ] +} +)json"; + +std::string RemoveWhitespaceFromString(const std::string input) { + return absl::StrReplaceAll(input, {{"\n", ""}, {" ", ""}}); +} + +TEST(StringToProtoTest, JSon) { + FileUtilTestProto2 proto; + ASSERT_OK(StringToProto(kTestJson, &proto)); + EXPECT_THAT(proto, EqualsProto(kTestTextProto)); +} + +TEST(StringToProtoTest, JSonProto3) { + FileUtilTestProto2 proto; + ASSERT_OK(StringToProto(kTestJsonProto3, &proto)); + EXPECT_THAT(proto, EqualsProto(kTestTextProto)); +} + +TEST(StringToProtoTest, JSonWithSingleValueInRepeatedField) { + absl::string_view contents = R"json({ + "optional_field": 123, + "repeated_fields": { + "some_field": 42 + } +} +)json"; + FileUtilTestProto2 proto; + ASSERT_OK(StringToProto(contents, &proto)); + EXPECT_THAT(proto, EqualsProto(R"pb( + optional_field: 123 + repeated_fields { some_field: 42 } + )pb")); +} + +TEST(StringToProtoTest, EmptyFile) { + FileUtilTestProto2 proto; + ASSERT_OK(StringToProto("", &proto)); + EXPECT_THAT(proto, EqualsProto("")); +} + +TEST(StringToProtoTest, WrongContents) { + FileUtilTestProto2 proto; + EXPECT_THAT(StringToProto("Not a proto.", &proto), + StatusIs(absl::StatusCode::kInvalidArgument, + AllOf(HasSubstr("binary format error: "), + HasSubstr("text format error: "), + HasSubstr("json error: ")))); +} + +TEST(StringToProtoTest, WrongProtoText) { + FileUtilTestProto2 proto; + // This is a valid text proto, but not a FileUtilTestProto2. + EXPECT_THAT(StringToProto("foobar: 42", &proto), + StatusIs(absl::StatusCode::kInvalidArgument, + AllOf(HasSubstr("1:6"), + HasSubstr("no field named \"foobar\"")))); + EXPECT_THAT(proto, EqualsProto("")); +} + +TEST(StringToProtoTest, WrongProtoJson) { + FileUtilTestProto1 proto; + EXPECT_THAT(StringToProto(kTestJson, &proto), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("'optional_field'"))); + EXPECT_THAT(proto, EqualsProto("")); +} + +TEST(StringToProtoTest, WrongProtoJsonProto3) { + FileUtilTestProto1 proto; + EXPECT_THAT(StringToProto(kTestJsonProto3, &proto), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("'optionalField'"))); + EXPECT_THAT(proto, EqualsProto("")); +} + +TEST(StringToProtoTest, WrongProtoBinary) { + // To make the proto verification catch a wrong proto in binary format, we + // have to populate a reasonable proto -- those that are too trivial are + // likely to be parsed as a payload of almost similar size, even as a wrong + // proto. + FileUtilTestProto2 proto2; + for (int i = 0; i < 10; ++i) { + proto2.add_repeated_fields()->set_some_field(i + 42); + } + FileUtilTestProto1 proto1; + EXPECT_THAT(StringToProto(proto2.SerializeAsString(), &proto1), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("binary format error"))); + EXPECT_THAT(proto1, EqualsProto("")); +} + +typedef FileUtilsTest ReadFileToProtoTest; + +TEST_F(ReadFileToProtoTest, FileError) { + FileUtilTestProto2 proto; + EXPECT_THAT( + ReadFileToProto(unknown_file_name_, &proto), + StatusIs(absl::StatusCode::kNotFound, HasSubstr(unknown_file_name_))); +} + +TEST_F(ReadFileToProtoTest, OutputsFileNameInErrorStatus) { + FileUtilTestProto2 proto; + ASSERT_OK(file::SetContents(file_name_, "gloubi-boulga", file::Defaults())); + EXPECT_THAT( + ReadFileToProto(file_name_, &proto), + StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr(file_name_))); +} + +TEST(WriteProtoToFileTest, TextProtoOnRealFile) { + ASSERT_OK_AND_ASSIGN(const std::string filename, + file::MakeTempFilename(::testing::TempDir(), "")); + FileUtilTestProto2 proto = ParseTestProto(kTestTextProto); + ASSERT_OK(WriteProtoToFile(filename, proto, ProtoWriteFormat::kProtoText)); + FileUtilTestProto2 written_proto; + ASSERT_OK(file::GetTextProto(filename, &written_proto, file::Defaults())); + EXPECT_THAT(written_proto, EqualsProto(proto)); +} + +TEST(WriteProtoToFileTest, JsonProtoOnRealFile) { + ASSERT_OK_AND_ASSIGN(const std::string filename, + file::MakeTempFilename(::testing::TempDir(), "")); + FileUtilTestProto2 proto = ParseTestProto(kTestTextProto); + ASSERT_OK(WriteProtoToFile(filename, proto, ProtoWriteFormat::kJson)); + std::string written_json; + ASSERT_OK( + file::GetContents(filename + ".json", &written_json, file::Defaults())); + EXPECT_EQ(written_json, kTestJson); +} + +TEST(WriteProtoToFileTest, CanonicalJsonProtoOnRealFile) { + ASSERT_OK_AND_ASSIGN(const std::string filename, + file::MakeTempFilename(::testing::TempDir(), "")); + FileUtilTestProto2 proto = ParseTestProto(kTestTextProto); + ASSERT_OK( + WriteProtoToFile(filename, proto, ProtoWriteFormat::kCanonicalJson)); + std::string written_json; + ASSERT_OK( + file::GetContents(filename + ".json", &written_json, file::Defaults())); + EXPECT_EQ(RemoveWhitespaceFromString(written_json), + RemoveWhitespaceFromString(kTestJsonProto3)); +} + +TEST(WriteProtoToFileTest, BinaryProtoOnRealFile) { + ASSERT_OK_AND_ASSIGN(const std::string filename, + file::MakeTempFilename(::testing::TempDir(), "")); + FileUtilTestProto2 proto = ParseTestProto(kTestTextProto); + ASSERT_OK(WriteProtoToFile(filename, proto, ProtoWriteFormat::kProtoBinary)); + FileUtilTestProto2 written_proto; + ASSERT_OK(file::GetBinaryProto(filename + ".bin", &written_proto, + file::Defaults())); + EXPECT_THAT(written_proto, EqualsProto(proto)); +} + +TEST(WriteProtoToFileTest, TextProtoOnRealFileNoAppendExtension) { + ASSERT_OK_AND_ASSIGN(const std::string filename, + file::MakeTempFilename(::testing::TempDir(), "")); + FileUtilTestProto2 proto = ParseTestProto(kTestTextProto); + ASSERT_OK(WriteProtoToFile(filename, proto, ProtoWriteFormat::kProtoText, + /*gzipped=*/false, + /*append_extension_to_file_name=*/false)); + FileUtilTestProto2 written_proto; + ASSERT_OK(file::GetTextProto(filename, &written_proto, file::Defaults())); + EXPECT_THAT(written_proto, EqualsProto(proto)); +} + +TEST(WriteProtoToFileTest, JsonProtoOnRealFileNoAppendExtension) { + ASSERT_OK_AND_ASSIGN(const std::string filename, + file::MakeTempFilename(::testing::TempDir(), "")); + FileUtilTestProto2 proto = ParseTestProto(kTestTextProto); + ASSERT_OK(WriteProtoToFile(filename, proto, ProtoWriteFormat::kJson, + /*gzipped=*/false, + /*append_extension_to_file_name=*/false)); + std::string written_json; + ASSERT_OK(file::GetContents(filename, &written_json, file::Defaults())); + EXPECT_EQ(written_json, kTestJson); +} + +TEST(WriteProtoToFileTest, BinaryProtoOnRealFileNoAppendExtension) { + ASSERT_OK_AND_ASSIGN(const std::string filename, + file::MakeTempFilename(::testing::TempDir(), "")); + FileUtilTestProto2 proto = ParseTestProto(kTestTextProto); + ASSERT_OK(WriteProtoToFile(filename, proto, ProtoWriteFormat::kProtoBinary, + /*gzipped=*/false, + /*append_extension_to_file_name=*/false)); + FileUtilTestProto2 written_proto; + ASSERT_OK(file::GetBinaryProto(filename, &written_proto, file::Defaults())); + EXPECT_THAT(written_proto, EqualsProto(proto)); +} + +TEST(ReadAndWriteProtoToFileTest, Gzipped) { + ASSERT_OK_AND_ASSIGN(const std::string filename, + file::MakeTempFilename(::testing::TempDir(), "")); + ASSERT_OK_AND_ASSIGN(const std::string gzipped_filename, + file::MakeTempFilename(::testing::TempDir(), "")); + ASSERT_OK_AND_ASSIGN(const std::string gzipped_filename2, + file::MakeTempFilename(::testing::TempDir(), "")); + FileUtilTestProto2 proto = ParseTestProto(kTestTextProto); + ASSERT_OK(WriteProtoToFile(filename, proto, ProtoWriteFormat::kProtoText)); + ASSERT_OK(WriteProtoToFile(gzipped_filename, proto, + ProtoWriteFormat::kProtoText, + /*gzipped=*/true)); + ASSERT_OK(WriteProtoToFile( + gzipped_filename2, proto, ProtoWriteFormat::kProtoText, + /*gzipped=*/true, /*append_extension_to_file_name=*/false)); + FileUtilTestProto2 written_proto; + ASSERT_OK(file::GetTextProto(filename, &written_proto, file::Defaults())); + EXPECT_THAT(written_proto, EqualsProto(proto)); + written_proto.Clear(); + ASSERT_OK(ReadFileToProto(gzipped_filename + ".gz", &written_proto)); + EXPECT_THAT(written_proto, EqualsProto(proto)); + written_proto.Clear(); + ASSERT_OK(ReadFileToProto(gzipped_filename2, &written_proto)); + EXPECT_THAT(written_proto, EqualsProto(proto)); + + // Verify that the gzipped file size is significantly smaller. + ASSERT_OK_AND_ASSIGN( + const int64_t gzipped_size, + file::GetSize(gzipped_filename + ".gz", file::Defaults())); + ASSERT_OK_AND_ASSIGN(const int64_t regular_size, + file::GetSize(filename, file::Defaults())); + ASSERT_OK_AND_ASSIGN(const int64_t gzipped_size2, + file::GetSize(gzipped_filename2, file::Defaults())); + + EXPECT_LE(gzipped_size, 0.8 * regular_size); + EXPECT_EQ(gzipped_size, gzipped_size2); +} + +typedef FileUtilsTest ReadFileToProtoStatusOrTest; + +TEST_F(ReadFileToProtoStatusOrTest, Success) { + const FileUtilTestProto1 model = ParseTestProto("some_field:42"); + std::string model_text; + google::protobuf::TextFormat::PrintToString(model, &model_text); + ASSERT_OK(file::SetContents(file_name_, model_text, file::Defaults())); + EXPECT_THAT(ReadFileToProto(file_name_), + IsOkAndHolds(EqualsProto(model))); +} + +TEST_F(ReadFileToProtoStatusOrTest, FileError) { + EXPECT_THAT( + ReadFileToProto(unknown_file_name_).status(), + StatusIs(absl::StatusCode::kNotFound, HasSubstr(unknown_file_name_))); +} + +} // namespace +} // namespace operations_research diff --git a/ortools/util/flat_matrix_benchmark.cc b/ortools/util/flat_matrix_benchmark.cc new file mode 100644 index 00000000000..d4c15334480 --- /dev/null +++ b/ortools/util/flat_matrix_benchmark.cc @@ -0,0 +1,50 @@ +// Copyright 2010-2025 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 + +#include "absl/base/casts.h" +#include "absl/log/check.h" +#include "benchmark/benchmark.h" +#include "ortools/util/flat_matrix.h" +#include "ortools/util/random_engine.h" + +namespace operations_research { +namespace { + +template +void BM_FlatMatrixRandomAccess(benchmark::State& state) { + const int size = state.range(0); + const uint64_t mask = size - 1; + CHECK(!(mask & size)) << "size must be a power of 2: " << size; + FlatMatrix m(size, size); + random_engine_t random; + double sum = 0; + for (const auto unused : state) { + const uint64_t x = random(); + if (do_work) { + sum += m[x & mask][(x >> 32) & mask]; + } else { + // The 'baseline' timing does similar operations, without the matrix + // lookup. + sum += absl::bit_cast(x & ((mask << 32) | mask)); + } + } + benchmark::DoNotOptimize(sum); +} + +BENCHMARK(BM_FlatMatrixRandomAccess)->Range(1, 1 << 12); +BENCHMARK(BM_FlatMatrixRandomAccess)->Range(1, 1 << 12); + +} // namespace +} // namespace operations_research diff --git a/ortools/util/flat_matrix_test.cc b/ortools/util/flat_matrix_test.cc new file mode 100644 index 00000000000..1fcede63118 --- /dev/null +++ b/ortools/util/flat_matrix_test.cc @@ -0,0 +1,212 @@ +// Copyright 2010-2025 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/util/flat_matrix.h" + +#include +#include +#include +#include +#include + +#include "gtest/gtest.h" +#include "ortools/base/dump_vars.h" +#include "ortools/base/gmock.h" + +namespace operations_research { +namespace { + +using ::testing::ElementsAre; +using ::testing::IsEmpty; + +TEST(FlatMatrixTest, ConstructionAndSizeGetters) { + FlatMatrix m(2, 3); + EXPECT_EQ(m.num_rows(), 2); + EXPECT_EQ(m.num_cols(), 3); +} + +TEST(FlatMatrixTest, ZeroRows) { + FlatMatrix m(0, 42); + EXPECT_EQ(m.num_rows(), 0); + EXPECT_EQ(m.num_cols(), 42); +} + +TEST(FlatMatrixTest, ZeroCols) { + FlatMatrix m(42, 0); + EXPECT_EQ(m.num_rows(), 42); + EXPECT_EQ(m.num_cols(), 0); +} + +TEST(FlatMatrixTest, ZeroRowsAndZeroCols) { + FlatMatrix m(0, 0); + EXPECT_EQ(m.num_rows(), 0); + EXPECT_EQ(m.num_cols(), 0); +} + +TEST(FlatMatrixTest, DefaultConstructor) { + FlatMatrix m; + EXPECT_EQ(m.num_rows(), 0); + EXPECT_EQ(m.num_cols(), 0); +} + +TEST(FlatMatrixTest, ElementsAreZeroedAtConstruction) { + FlatMatrix m(2, 3); + for (int row = 0; row < 2; ++row) { + for (int col = 0; col < 3; ++col) { + ASSERT_EQ(m[row][col], 0) << DUMP_VARS(row, col); + } + } +} + +TEST(FlatMatrixTest, InitializeWithNonDefault) { + FlatMatrix m(5, 4, "Hello"); + for (int row = 0; row < 5; ++row) { + for (int col = 0; col < 4; ++col) { + ASSERT_EQ(m[row][col], "Hello") << DUMP_VARS(row, col); + } + } +} + +TEST(FlatMatrixTest, WriteAndReadElements) { + FlatMatrix m(2, 3); + for (int row = 0; row < 2; ++row) { + for (int col = 0; col < 3; ++col) { + m[row][col] = 100 * row + 10 * col; + } + } + for (int row = 0; row < 2; ++row) { + for (int col = 0; col < 3; ++col) { + EXPECT_EQ(m[row][col], 100 * row + 10 * col) << DUMP_VARS(row, col); + } + } +} + +TEST(FlatMatrixTest, CopyAndMoveOperators) { + FlatMatrix m(2, 3); + for (int row = 0; row < 2; ++row) { + for (int col = 0; col < 3; ++col) { + m[row][col] = 100 * row + 10 * col; + } + } + // We chain a copy and a move to reduce the amount of test code. + FlatMatrix copied_construction = m; + FlatMatrix copied_assigned(4, 5); + copied_assigned = m; + FlatMatrix moved_construction = std::move(copied_construction); + FlatMatrix moved_assigned(4, 5); + moved_assigned = std::move(copied_assigned); + + ASSERT_EQ(moved_construction.num_rows(), 2); + ASSERT_EQ(moved_construction.num_cols(), 3); + ASSERT_EQ(moved_assigned.num_rows(), 2); + ASSERT_EQ(moved_assigned.num_cols(), 3); + for (int row = 0; row < 2; ++row) { + for (int col = 0; col < 3; ++col) { + ASSERT_EQ(moved_construction[row][col], 100 * row + 10 * col) + << DUMP_VARS(row, col); + ASSERT_EQ(moved_assigned[row][col], 100 * row + 10 * col) + << DUMP_VARS(row, col); + } + } +} + +TEST(FlatMatrixTest, MoveDoesPerformRealMove) { + // Move-construct a ~7M element matrix a million times: if it wasn't a move, + // it would be far too slow and need huge amounts of memory. + std::vector>> matrices(1'000'000); + matrices[0] = std::make_unique>(2345, 3456); + (*matrices[0])[2344][3455] = 42.42; + for (int i = 1; i < 1000000; ++i) { + matrices[i] = + std::make_unique>(std::move(*matrices[i - 1])); + } + EXPECT_EQ((*matrices.back())[2344][3455], 42.42); +} + +TEST(FlatMatrixTest, RowIteration) { + FlatMatrix m(2, 3); + for (int row = 0; row < 2; ++row) { + for (int col = 0; col < 3; ++col) { + m[row][col] = 100 * row + 10 * col; + } + } + EXPECT_THAT(m[1], ElementsAre(100, 110, 120)); +} + +TEST(FlatMatrixTest, RowIterationOnEmptyRow) { + FlatMatrix m(3, 0); + EXPECT_THAT(m[2], IsEmpty()); +} + +TEST(FlatMatrixTest, AllElementsConst) { + FlatMatrix m(2, 3); + for (int row = 0; row < 2; ++row) { + for (int col = 0; col < 3; ++col) { + m[row][col] = 100 * row + 10 * col; + } + } + const FlatMatrix const_m = m; + EXPECT_THAT(const_m.all_elements(), ElementsAre(0, 10, 20, 100, 110, 120)); +} + +TEST(FlatMatrixTest, AllElementsMutable) { + FlatMatrix m(2, 3); + int value = 10; + for (int& i : m.all_elements()) { + i = value++; + } + EXPECT_THAT(m.all_elements(), ElementsAre(10, 11, 12, 13, 14, 15)); +} + +TEST(FlatMatrixTest, AllElementsInlineMutation) { + FlatMatrix m(2, 3); + for (int row = 0; row < 2; ++row) { + for (int col = 0; col < 3; ++col) { + m[row][col] = 100 * row + 10 * col; + } + } + for (int& i : m.all_elements()) { + i *= 10; + } + EXPECT_THAT(m.all_elements(), ElementsAre(0, 100, 200, 1000, 1100, 1200)); +} + +TEST(FlatMatrixTest, AllElementsEmptyMatrix) { + FlatMatrix m; + EXPECT_THAT(m.all_elements(), ElementsAre()); +} + +TEST(FlatMatrixTest, RowIterator) { + FlatMatrix m(2, 3); + for (int row = 0; row < 2; ++row) { + for (int col = 0; col < 3; ++col) { + m[row][col] = 100 * row + 10 * col; + } + } + EXPECT_THAT(m.rows(), + ElementsAre(ElementsAre(0, 10, 20), ElementsAre(100, 110, 120))); +} + +TEST(FlatMatrixTest, RowIteratorOnEmptyMatrix) { + FlatMatrix m; + EXPECT_THAT(m.rows(), ElementsAre()); +} + +TEST(FlatMatrixTest, RowIteratorOnZeroColumnMatrix) { + FlatMatrix m(3, 0); + EXPECT_THAT(m.rows(), + ElementsAre(ElementsAre(), ElementsAre(), ElementsAre())); +} + +} // namespace +} // namespace operations_research diff --git a/ortools/util/fp_roundtrip_conv_testing_test.cc b/ortools/util/fp_roundtrip_conv_testing_test.cc new file mode 100644 index 00000000000..b9450248979 --- /dev/null +++ b/ortools/util/fp_roundtrip_conv_testing_test.cc @@ -0,0 +1,39 @@ +// Copyright 2010-2025 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/util/fp_roundtrip_conv_testing.h" + +#include + +#include "gtest/gtest.h" +#include "ortools/util/fp_roundtrip_conv.h" + +namespace operations_research { +namespace { + +TEST(TestNumberTest, Test) { + { + std::ostringstream oss; + oss << kRoundTripTestNumber; + EXPECT_NE(oss.str(), kRoundTripTestNumberStr); + EXPECT_EQ(oss.str(), "0.1"); + } + { + std::ostringstream oss; + oss << RoundTripDoubleFormat(kRoundTripTestNumber); + EXPECT_EQ(oss.str(), kRoundTripTestNumberStr); + } +} + +} // namespace +} // namespace operations_research diff --git a/ortools/util/fp_utils_benchmark.cc b/ortools/util/fp_utils_benchmark.cc new file mode 100644 index 00000000000..b379f246cd6 --- /dev/null +++ b/ortools/util/fp_utils_benchmark.cc @@ -0,0 +1,194 @@ +// Copyright 2010-2025 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 + +#include +#include +#include + +#include "absl/log/check.h" +#include "benchmark/benchmark.h" +#include "ortools/util/fp_utils.h" + +namespace operations_research { +namespace { + +void BM_fast_ilogb(benchmark::State& state) { + const int length = 1024; + std::vector values; + values.reserve(length); + for (int i = 0; i < length; ++i) { + values.push_back(exp2(i - 512)); + } + for (auto s : state) { + int total = 0; + for (int i = 0; i < length; ++i) { + total += fast_ilogb(values[i]); + } + CHECK_EQ(total, (length * (length - 1) / 2 - 512 * length)); + } +} +BENCHMARK(BM_fast_ilogb); + +void BM_Ilogb(benchmark::State& state) { + const int length = 1024; + std::vector values; + values.reserve(length); + for (int i = 0; i < length; ++i) { + values.push_back(exp2(i - 512)); + } + + for (auto s : state) { + int total = 0; + for (int i = 0; i < length; ++i) { + total += ilogb(values[i]); + } + CHECK_EQ(total, (length * (length - 1) / 2 - 512 * length)); + } +} +BENCHMARK(BM_Ilogb); + +void BM_frexp(benchmark::State& state) { + const int length = 1024; + std::vector values; + values.reserve(length); + for (int i = 0; i < length; ++i) { + values.push_back(exp2(i - 512)); + } + + for (auto s : state) { + int total = 0; + for (int i = 0; i < length; ++i) { + const double value = values[i]; + int exponent; + if (value == 0.0) { + exponent = FP_ILOGB0; + } else { + (void)frexp(values[i], &exponent); + } + total += exponent - 1; + } + CHECK_EQ(total, (length * (length - 1) / 2 - 512 * length)); + } +} +BENCHMARK(BM_frexp); + +void BM_log2(benchmark::State& state) { + const int length = 1024; + std::vector values; + values.reserve(length); + for (int i = 0; i < length; ++i) { + values.push_back(exp2(i - 512)); + } + + for (auto s : state) { + int total = 0; + for (int i = 0; i < length; ++i) { + total += log2(std::abs(values[i])); + } + CHECK_EQ(total, (length * (length - 1) / 2 - 512 * length)); + } +} +BENCHMARK(BM_log2); + +// To scale numbers with pure power of two exponents the code relies on scalbn, +// the alternatives are ldexp and exp2. We benchmark here the alternatives to +// ensure scalbn is indeed faster. +void BM_fast_scalbn(benchmark::State& state) { + const int length = 1024; + std::vector values; + values.reserve(length); + for (int i = 0; i < length; ++i) { + values.push_back(i - 512); + } + + for (auto s : state) { + double total = 0; + for (int i = 0; i < length; ++i) { + total += fast_scalbn(1.0, values[i]); + } + CHECK_EQ(total, 0x1.ffffffffffffffp511); + } +} +BENCHMARK(BM_fast_scalbn); + +void BM_scalbn(benchmark::State& state) { + const int length = 1024; + std::vector values; + values.reserve(length); + for (int i = 0; i < length; ++i) { + values.push_back(i - 512); + } + + for (auto s : state) { + double total = 0; + for (int i = 0; i < length; ++i) { + total += scalbn(1.0, values[i]); + } + CHECK_EQ(total, 0x1.ffffffffffffffp511); + } +} +BENCHMARK(BM_scalbn); + +void BM_ldexp(benchmark::State& state) { + const int length = 1024; + std::vector values; + values.reserve(length); + for (int i = 0; i < length; ++i) { + values.push_back(i - 512); + } + + for (auto s : state) { + double total = 0; + for (int i = 0; i < length; ++i) { + total += ldexp(1.0, values[i]); + } + CHECK_EQ(total, 0x1.ffffffffffffffp511); + } +} +BENCHMARK(BM_ldexp); + +void BM_exp2(benchmark::State& state) { + const int length = 1024; + std::vector values; + values.reserve(length); + for (int i = 0; i < length; ++i) { + values.push_back(i - 512); + } + + for (auto s : state) { + double total = 0; + for (int i = 0; i < length; ++i) { + total += 1.0 * exp2(values[i]); + } + CHECK_EQ(total, 0x1.ffffffffffffffp511); + } +} +BENCHMARK(BM_exp2); + +// (Generated by http://go/benchy. Settings: --runs 20 --guitar_cluster +// chamber-experiments-guitar3 //ortools/util:fp_utils_test) +// +// name time/op +// BM_fast_ilogb 1.95µs ± 1% +// BM_Ilogb 3.58µs ± 3% +// BM_frexp 3.51µs ± 1% +// BM_log2 7.74µs ± 1% +// BM_fast_scalbn 3.47µs ± 1% +// BM_scalbn 3.89µs ± 2% +// BM_ldexp 6.19µs ± 1% +// BM_exp2 6.19µs ± 2% + +} // namespace +} // namespace operations_research diff --git a/ortools/util/fp_utils_test.cc b/ortools/util/fp_utils_test.cc new file mode 100644 index 00000000000..5233c1e2123 --- /dev/null +++ b/ortools/util/fp_utils_test.cc @@ -0,0 +1,502 @@ +// Copyright 2010-2025 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/util/fp_utils.h" + +#include + +#include // NOLINT(build/c++11) +#include +#include +#include +#include +#include + +#include "absl/base/casts.h" +#include "absl/log/check.h" +#include "absl/log/log.h" +#include "absl/random/random.h" +#include "absl/strings/str_format.h" +#include "absl/types/span.h" +#include "gtest/gtest.h" +#include "ortools/base/types.h" + +namespace operations_research { +namespace { + +// To have more readable one-liners below. +const double kInfinity = std::numeric_limits::infinity(); + +// An intricate way to set 'result' to zero, to avoid compiler optimization. +double GenerateZero() { + const int n = 100; + double result = n * (n - 1) / 2; + for (int i = 0; i < n; ++i) { + result -= i; + } + CHECK_EQ(0.0, result); + return result; +} + +double GenerateNaN() { return GenerateZero() / GenerateZero(); } + +double GenerateDivisionByZero() { return 1.0 / GenerateZero(); } + +void RunFloatingPointExceptionTest(int exception_flags, bool detect_nan, + bool detect_div_by_zero) { + ScopedFloatingPointEnv scoped_fenv; + if (!scoped_fenv.EnableExceptions(exception_flags)) { + GTEST_SKIP() << "Floating point exceptions are not supported."; + } + if (detect_nan) { + EXPECT_DEATH( + { + const double x = GenerateNaN(); + EXPECT_NE(x, x); // Use the value to prevent removal by optimization. + }, + ""); + } else { + const double x = GenerateNaN(); + EXPECT_NE(x, x); + } + if (detect_div_by_zero) { + EXPECT_DEATH( + { + // Use the value to prevent removal by optimization. + EXPECT_EQ(kInfinity, GenerateDivisionByZero()); + }, + ""); + } else { + EXPECT_EQ(kInfinity, GenerateDivisionByZero()); + } +} + +TEST(ScopedFpEnv, NoDetection) { + RunFloatingPointExceptionTest(0, false, false); +} + +TEST(ScopedFpEnv, NanDetection) { + RunFloatingPointExceptionTest(FE_INVALID, true, false); +} + +TEST(ScopedFpEnv, DivisionByZeroDetection) { + RunFloatingPointExceptionTest(FE_DIVBYZERO, false, true); +} + +TEST(ScopedFpEnv, NanDetectionDivisionByZeroDetection) { + RunFloatingPointExceptionTest(FE_INVALID | FE_DIVBYZERO, true, true); +} + +TEST(Comparable, ComparisonsWithinTolerances) { + EXPECT_COMPARABLE(-kInfinity, -kInfinity, 1e-6); + EXPECT_COMPARABLE(kInfinity, kInfinity, 1e-6); + EXPECT_NOTCOMPARABLE(-kInfinity, kInfinity, 1e-6); + EXPECT_NOTCOMPARABLE(kInfinity, -kInfinity, 1e-6); + EXPECT_NOTCOMPARABLE(1.0, kInfinity, 1e-6); + EXPECT_NOTCOMPARABLE(-kInfinity, 1.0, 1e-6); + EXPECT_COMPARABLE(1.0, 1.0 + 1e-7, 1e-6); + EXPECT_NOTCOMPARABLE(1.0, 1.0 + 1e-5, 1e-6); + EXPECT_COMPARABLE(0.0, 0.0 + 1e-7, 1e-6); + EXPECT_NOTCOMPARABLE(0.0, 0.0 + 1e-5, 1e-6); + EXPECT_NOTCOMPARABLE(1.0, std::numeric_limits::quiet_NaN(), 1e-6); +} + +TEST(Comparable, ComparisonsWithinAbolusteTolerance) { + EXPECT_TRUE(AreWithinAbsoluteTolerance(-kInfinity, -kInfinity, 1e-6)); + EXPECT_TRUE(AreWithinAbsoluteTolerance(kInfinity, kInfinity, 1e-6)); + EXPECT_FALSE(AreWithinAbsoluteTolerance(-kInfinity, kInfinity, 1e-6)); + EXPECT_FALSE(AreWithinAbsoluteTolerance(kInfinity, -kInfinity, 1e-6)); + EXPECT_FALSE(AreWithinAbsoluteTolerance(1.0, kInfinity, 1e-6)); + EXPECT_FALSE(AreWithinAbsoluteTolerance(-kInfinity, 1.0, 1e-6)); + EXPECT_TRUE(AreWithinAbsoluteTolerance(1.0, 1.0 + 1e-7, 1e-6)); + EXPECT_FALSE(AreWithinAbsoluteTolerance(1.0, 1.0 + 1e-5, 1e-6)); + EXPECT_TRUE(AreWithinAbsoluteTolerance(0.0, 0.0 + 1e-7, 1e-6)); + EXPECT_FALSE(AreWithinAbsoluteTolerance(0.0, 0.0 + 1e-5, 1e-6)); + EXPECT_FALSE(AreWithinAbsoluteTolerance( + 1.0, std::numeric_limits::quiet_NaN(), 1e-6)); +} + +TEST(IsSmallerWithinToleranceTest, BasicTest) { + EXPECT_TRUE(IsSmallerWithinTolerance(kInfinity, kInfinity, 1e-6)); + EXPECT_TRUE(IsSmallerWithinTolerance(-kInfinity, -kInfinity, 1e-6)); + EXPECT_TRUE(IsSmallerWithinTolerance(-kInfinity, kInfinity, 1e-6)); + EXPECT_FALSE(IsSmallerWithinTolerance(kInfinity, -kInfinity, 1e-6)); + + EXPECT_TRUE(IsSmallerWithinTolerance(123.456, kInfinity, 1e-6)); + EXPECT_FALSE(IsSmallerWithinTolerance(kInfinity, 4567.0, 1e-6)); + + const double kNaN = std::numeric_limits::quiet_NaN(); + EXPECT_FALSE(IsSmallerWithinTolerance(kNaN, kNaN, 1e-6)); + EXPECT_FALSE(IsSmallerWithinTolerance(kNaN, kInfinity, 1e-6)); + EXPECT_FALSE(IsSmallerWithinTolerance(kNaN, -kInfinity, 1e-6)); + EXPECT_FALSE(IsSmallerWithinTolerance(kNaN, 0.0, 1e-6)); + EXPECT_FALSE(IsSmallerWithinTolerance(kInfinity, kNaN, 1e-6)); + EXPECT_FALSE(IsSmallerWithinTolerance(-kInfinity, kNaN, 1e-6)); + EXPECT_FALSE(IsSmallerWithinTolerance(0.0, kNaN, 1e-6)); + + EXPECT_TRUE(IsSmallerWithinTolerance(-5.0 + 1e-6, -5.0, 1e-6)); + EXPECT_FALSE(IsSmallerWithinTolerance(-5.0 + 1e-6, -5.0, 1e-8)); + EXPECT_TRUE(IsSmallerWithinTolerance(1.0, 1.0, 0.0)); + EXPECT_FALSE(IsSmallerWithinTolerance( + 1.0 + std::numeric_limits::epsilon(), 1.0, 0.0)); +} + +TEST(IsIntegerWithinToleranceTest, BasicTest) { + EXPECT_TRUE(IsIntegerWithinTolerance(4.9, .2)); + EXPECT_FALSE(IsIntegerWithinTolerance(4.9, .05)); + + EXPECT_TRUE(IsIntegerWithinTolerance(5.1, .2)); + EXPECT_FALSE(IsIntegerWithinTolerance(5.1, .05)); +} + +TEST(IsIntegerWithinToleranceTest, InfTest) { + EXPECT_FALSE(IsIntegerWithinTolerance(kInfinity, 100.0)); + EXPECT_FALSE(IsIntegerWithinTolerance(-kInfinity, 100.0)); + EXPECT_FALSE(IsIntegerWithinTolerance(kInfinity, kInfinity)); + EXPECT_FALSE(IsIntegerWithinTolerance(-kInfinity, kInfinity)); +} + +TEST(GetBestScalingOfDoublesToInt64Test, ErrorCases) { + const double kInfinity = std::numeric_limits::infinity(); + std::vector input{0, 1, 2, 3}; + double scale; + double error; + + input[1] = kInfinity; + GetBestScalingOfDoublesToInt64(input, uint64_t{1} << 60, &scale, &error); + EXPECT_EQ(error, kInfinity); + + input[1] = -kInfinity; + GetBestScalingOfDoublesToInt64(input, uint64_t{1} << 60, &scale, &error); + EXPECT_EQ(error, kInfinity); + + input[1] = std::numeric_limits::quiet_NaN(); + GetBestScalingOfDoublesToInt64(input, uint64_t{1} << 60, &scale, &error); + EXPECT_EQ(error, kInfinity); + + input[1] = std::numeric_limits::signaling_NaN(); + GetBestScalingOfDoublesToInt64(input, uint64_t{1} << 60, &scale, &error); + EXPECT_EQ(error, kInfinity); + + // Just to show that it works with 0. + input[1] = 0; + GetBestScalingOfDoublesToInt64(input, uint64_t{1} << 60, &scale, &error); + EXPECT_EQ(error, 0); + + // And note that the max_absolute_sum negative also lead to an error. + GetBestScalingOfDoublesToInt64(input, -1, &scale, &error); + EXPECT_EQ(error, kInfinity); +} + +TEST(GetBestScalingOfDoublesToInt64Test, AllZeroVector) { + std::vector input(1000, 0.0); + double scale; + double error; + GetBestScalingOfDoublesToInt64(input, uint64_t{1} << 60, &scale, &error); + EXPECT_EQ(error, 0.0); + EXPECT_EQ(scale, 1.0); +} + +TEST(GetBestScalingOfDoublesToInt64Test, ZeroBounds) { + std::vector input(1000, 50.0); + std::vector lb(1000, 0.0); + std::vector ub(1000, 0.0); + const double scale = + GetBestScalingOfDoublesToInt64(input, lb, ub, uint64_t{1} << 60); + EXPECT_EQ(scale, 1.0); + + double error; + double abs_error; + ComputeScalingErrors(input, lb, ub, scale, &error, &abs_error); + EXPECT_EQ(error, 0.0); + EXPECT_EQ(abs_error, 0.0); +} + +TEST(GetBestScalingOfDoublesToInt64Test, PowerOfTwo) { + std::vector input{ldexp(1.0, 100), ldexp(-1.0, 10), ldexp(1.0, -10)}; + double scale; + double error; + GetBestScalingOfDoublesToInt64(input, int64_t{1} << 60, &scale, &error); + EXPECT_EQ(scale, ldexp(1.0, 60 - 100)); + EXPECT_EQ(error, 1.0); // The last value just disappeared... +} + +TEST(GetBestScalingOfDoublesToInt64Test, MaxSum) { + std::vector input{3, -3, 5, 0, 8}; + double scale; + double error; + + // The sum of the input absolute values is 19. + GetBestScalingOfDoublesToInt64(input, 19, &scale, &error); + EXPECT_EQ(scale, 1.0); + EXPECT_EQ(error, 0.0); + + // If we use a lower value, then the number needs to be scaled down. + GetBestScalingOfDoublesToInt64(input, 18, &scale, &error); + EXPECT_EQ(scale, 0.5); + + // We loose the bit at 1 from the 3s and 5. + EXPECT_COMPARABLE(error, 1.0 / 3.0, 1e-10); +} + +void CheckNoError(absl::Span input, double scale) { + for (double x : input) { + EXPECT_EQ(round(x * scale) / scale, x); + } +} + +TEST(GetBestScalingOfDoublesToInt64Test, NoErrorForIntegers) { + const int64_t max_mantissa = (1LL << std::numeric_limits::digits) - 1; + std::vector input{max_mantissa, 1, 123456789, 0, -max_mantissa, -10}; + double scale; + double error; + GetBestScalingOfDoublesToInt64(input, int64_t{1} << 60, &scale, &error); + EXPECT_EQ(scale, 32.0); // This depends on the limit 1LL << 60. + EXPECT_EQ(error, 0.0); + CheckNoError(input, scale); + + // Note that if the exponent of all number are shifted, then this still work. + for (int i = 0; i < input.size(); ++i) { + input[i] = ldexp(input[i], -123); + } + GetBestScalingOfDoublesToInt64(input, int64_t{1} << 60, &scale, &error); + EXPECT_EQ(scale, ldexp(32.0, 123)); + EXPECT_EQ(error, 0.0); + CheckNoError(input, scale); +} + +TEST(GetBestScalingOfDoublesToInt64Test, NoErrorForCloseDoubles) { + std::mt19937 random(12345); + std::vector input; + // Because std::numeric_limits::digits is 53, we can represent + // them exactly with a scale of 2^52, and their sum will still stay below + // the specified limit (because we just sum 2^10 of them). + for (int i = 0; i < 1024; ++i) { + input.push_back(absl::Uniform(random, 1.0, 2.0)); + } + double scale; + double error; + GetBestScalingOfDoublesToInt64(input, kint64max, &scale, &error); + EXPECT_EQ(error, 0.0); + EXPECT_EQ(scale, int64_t{1} << 52); + CheckNoError(input, scale); + + // Now, if we sum some more, then we will need a lower scale and we will loose + // one bit of precision. + for (int i = 0; i < 1024; ++i) { + input.push_back(-absl::Uniform(random, 1.0, 2.0)); + } + GetBestScalingOfDoublesToInt64(input, kint64max, &scale, &error); + EXPECT_EQ(error, std::numeric_limits::epsilon()); + EXPECT_EQ(scale, int64_t{1} << 51); +} + +TEST(GetBestScalingOfDoublesToInt64Test, BoundOnSumIsCorrect) { + std::mt19937 random(12345); + const double x = static_cast((1LL << 32) - 1); + std::vector input{1.0, x - 1, ldexp(x, 32)}; + double scale; + double error; + GetBestScalingOfDoublesToInt64(input, kint64max, &scale, &error); + + // Scaling by 0.5 is not enough, because the sum will be kint64max + 1 + // as shown by the following 3 lines: + EXPECT_EQ(1, static_cast(round(input[0] * 0.5))); + EXPECT_EQ((int64_t{1} << 31) - 1, + static_cast(round(input[1] * 0.5))); + EXPECT_EQ(((int64_t{1} << 32) - 1) << 31, + static_cast(round(input[2] * 0.5))); + + EXPECT_GT(error, 0.0); + EXPECT_EQ(scale, 1.0 / 4.0); + + // If x[0] is zero, then we can scale everyting by 0.5 without error. + input[0] = 0; + GetBestScalingOfDoublesToInt64(input, kint64max, &scale, &error); + EXPECT_EQ(error, 0.0); + CheckNoError(input, scale); + EXPECT_EQ(scale, 0.5); +} + +TEST(GetBestScalingOfDoublesToInt64Test, Infinity) { + std::vector input{1e-313}; + double scale; + double error; + GetBestScalingOfDoublesToInt64(input, kint64max, &scale, &error); + EXPECT_LT(scale, std::numeric_limits::infinity()); + EXPECT_EQ(scale, ldexp(1.0, std::numeric_limits::max_exponent - 1)); +} + +TEST(GetBestScalingOfDoublesToInt64Test, Robustness) { + std::mt19937 random(12345); + std::vector input; + double scale; + double error; + for (int i = 0; i < 1024; ++i) { + input.clear(); + + // Generate i "garbage" doubles. + for (int j = 0; j < i; ++j) { + input.push_back(absl::bit_cast(absl::Uniform(random))); + } + + // Verify that nothing crash and that the value seems reasonable. + GetBestScalingOfDoublesToInt64(input, kint64max, &scale, &error); + if (error != std::numeric_limits::infinity()) { + CHECK_GT(scale, 0); + CHECK_GE(error, 0); + CHECK_LE(error, 1); + } + } +} + +TEST(ComputeScalingErrorTest, BasicTest) { + std::vector coeffs = {1.1, 0.7, -1.3}; + std::vector lbs = {0, 0, 0}; + std::vector ubs = {10, 10, 10}; + double coeff_relative_error; + double sum_max_error; + ComputeScalingErrors(coeffs, lbs, ubs, /*scaling_factor=*/1, + &coeff_relative_error, &sum_max_error); + + // The biggest relative error is on the coeff 0.7 that is rounded to 1. + EXPECT_NEAR(coeff_relative_error, 0.3 / 0.7, 1e-10); + + // The maximum sum difference is obtained on {0, 10, 10} where the exact sum + // is (0.7 - 1.3) * 10 and the rounded one is (1 - 1) * 10. + EXPECT_NEAR(sum_max_error, 6.0, 1e-10); +} + +TEST(ComputeGcdOfRoundedDoublesTest, BasicTest) { + EXPECT_EQ(1, ComputeGcdOfRoundedDoubles({}, 1.0)); + EXPECT_EQ(1, ComputeGcdOfRoundedDoubles({0.1, -0.2, 0.3}, 1.0)); + EXPECT_EQ(10, ComputeGcdOfRoundedDoubles({50, 0, 40, 60}, 1.0)); + EXPECT_EQ(1, ComputeGcdOfRoundedDoubles({7, 5, 25, 14}, 1.0)); + EXPECT_EQ(1, ComputeGcdOfRoundedDoubles({-7, -5, 25, -14}, 1.0)); + EXPECT_EQ(7, ComputeGcdOfRoundedDoubles({7, 0}, 1.0)); + EXPECT_EQ(7, ComputeGcdOfRoundedDoubles({0, 7}, 1.0)); + EXPECT_EQ(7, ComputeGcdOfRoundedDoubles({-7, 0}, 1.0)); + EXPECT_EQ(7, ComputeGcdOfRoundedDoubles({0, -7}, 1.0)); + EXPECT_EQ(18, ComputeGcdOfRoundedDoubles({1, 0, 2, 3}, 18.0)); + EXPECT_EQ(18, ComputeGcdOfRoundedDoubles({1, 0, 2, 3}, -18.0)); +} + +TEST(InterpolateTest, BasicTest) { + EXPECT_DOUBLE_EQ(1.8, Interpolate(2, 1, .8)); +} + +constexpr int kDoubleMinExponent = std::numeric_limits::min_exponent; +constexpr int kDoubleMaxExponent = std::numeric_limits::max_exponent; +TEST(FastIlogbTest, Correctness) { + absl::BitGen bitgen; + for (int j = 0; j < 1024; ++j) { + for (int i = kDoubleMinExponent; i < kDoubleMaxExponent; ++i) { + for (int sign : {-1, 1}) { + const double input = + sign * absl::Uniform(absl::IntervalClosedOpen, bitgen, 1.0, 2.0); + ASSERT_EQ(fast_ilogb(scalbn(input, i)), i); + } + } + } +} + +TEST(FastScalbnTest, Correctness) { + std::mt19937 bitgen(19560618); + for (int j = 0; j < 1024; ++j) { + for (int i = kDoubleMinExponent; i < kDoubleMaxExponent; ++i) { + for (int sign : {-1, 1}) { + const double input = sign * absl::Uniform(bitgen, 1.0, 2.0); + ASSERT_EQ(fast_scalbn(input, i), scalbn(input, i)); + } + } + } +} + +template +T RandomElementOf(std::mt19937& random, const std::vector& array) { + return array[absl::Uniform(random, 0, array.size())]; +} + +// Test on random input, ensure that for finite numbers whose result is +// representable, we get the correct result. +TEST(FastScalbnTest, DontFailOnRubbish) { + constexpr int kNumTests = (1 << 25); + std::mt19937 bitgen(19540820); + int64_t count_ok = 0; + int64_t count_nan = 0; + int64_t count_subnormal = 0; + int64_t count_zero = 0; + int64_t count_infinity = 0; + // We inject some special values that are worth testing, but that are too + // unlikely to be produced by a purely bit-uniform random double. + const std::vector kSpecialValues = {kInfinity, -kInfinity, 0.0, -0.0}; + for (int i = 0; i < kNumTests; ++i) { + const double input = (i & 0xff) + ? absl::bit_cast(absl::Uniform( + bitgen, 0, kuint64max)) + : RandomElementOf(bitgen, kSpecialValues); + const int add_to_exponent = + (i % 97 != 0) ? absl::Uniform(bitgen, kDoubleMinExponent - 1, + kDoubleMaxExponent + 2) + : absl::Uniform(bitgen, kint32min, kint32max); + // Note that no matter the input, the function will not crash, although + // the result may be undefined behavior. + const double result = fast_scalbn(input, add_to_exponent); + if (std::isnan(input)) { + ++count_nan; + } else if (std::isinf(input)) { + ++count_infinity; + } else if (input == 0.0) { + ++count_zero; + ASSERT_EQ(result, input); + } else if (!std::isnormal(input)) { + ++count_subnormal; + } else { + const int input_exponent = fast_ilogb(input); + const int result_exponent = input_exponent + add_to_exponent; + if (result_exponent < kDoubleMaxExponent && + result_exponent >= kDoubleMinExponent) { + // If input and output is representable, the result must be equal to + // the value returned by the standard function. + const double posix_result = scalbn(input, add_to_exponent); + ASSERT_EQ(result, posix_result) << absl::StrFormat( + "\nBit representations:\n\t%10s %016llx %.16g\n\t%10s %016llx " + "%.16g\n\t%10s %016llx %.16g\nExponents:\n\t%10s %10d\n\t%10s " + "%10d\n\t%10s %10d\nIterations: %d", + "input", absl::bit_cast(input), input, "fast", + absl::bit_cast(result), result, "posix", + absl::bit_cast(posix_result), posix_result, "input", + input_exponent, "result", result_exponent, "add_to", + add_to_exponent, i); + ++count_ok; + } + } + } + LOG(INFO) << absl::StrFormat( + "Input tested:\n\t%20s %.4f%% (count %g)\n\t%20s %.4f%% (count " + "%g)\n\t%20s %.4f%% (count %g)\n\t%20s %.4f%% (count %g)\n\t%20s %.4f%% " + "(count %g)", + "Valid", count_ok * 100.0 / kNumTests, count_ok, "Subnormal", + count_subnormal * 100.0 / kNumTests, count_subnormal, "NaN", + count_nan * 100.0 / kNumTests, count_nan, "Infinity", + count_infinity * 100.0 / kNumTests, count_infinity, "Zero", + count_zero * 100.0 / kNumTests, count_zero); + EXPECT_LT(0.55 * kNumTests, count_ok); + EXPECT_LT(4e-4 * kNumTests, count_subnormal); + EXPECT_LT(4e-4 * kNumTests, count_nan); + EXPECT_LT(4e-4 * kNumTests, count_infinity); + EXPECT_LT(4e-4 * kNumTests, count_zero); +} + +} // namespace +} // namespace operations_research diff --git a/ortools/util/lazy_mutable_copy_test.cc b/ortools/util/lazy_mutable_copy_test.cc new file mode 100644 index 00000000000..c32d84fe1e2 --- /dev/null +++ b/ortools/util/lazy_mutable_copy_test.cc @@ -0,0 +1,245 @@ +// Copyright 2010-2025 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/util/lazy_mutable_copy.h" + +#include +#include +#include + +#include "absl/strings/string_view.h" +#include "gtest/gtest.h" + +namespace operations_research { +namespace { + +// An object that statically tracks the number of times its copy constructor +// was called. Not thread-safe at all, but for our purpose here it's fine. +class MyTestObject { + public: + explicit MyTestObject(absl::string_view data) : data_(data) {} + + MyTestObject(const MyTestObject& other) { + ++num_copies_; + data_ = other.data_; + } + + MyTestObject(MyTestObject&& other) { + ++num_moves_; + data_ = std::move(other.data_); + } + + ~MyTestObject() { ++num_deletes_; } + + const std::string& data() const { return data_; } + void set_data(absl::string_view data) { data_ = data; } + + static void ResetCounters() { + num_copies_ = 0; + num_moves_ = 0; + num_deletes_ = 0; + } + + static int num_copies_; + static int num_moves_; + static int num_deletes_; + + std::string data_; +}; + +// static +int MyTestObject::num_copies_ = 0; +int MyTestObject::num_moves_ = 0; +int MyTestObject::num_deletes_ = 0; + +TEST(LazyMutableCopyTest, SimpleOperations) { + // Create my baseline object. + MyTestObject::ResetCounters(); + MyTestObject obj("Hello"); + ASSERT_EQ(obj.data(), "Hello"); + + // Create a LazyMutableCopy. For now, don't mutate it: it doesn't copy. + LazyMutableCopy copy(obj); + ASSERT_FALSE(copy.has_ownership()); + EXPECT_EQ(MyTestObject::num_copies_, 0); + EXPECT_EQ(copy->data(), "Hello"); + obj.set_data("World"); + EXPECT_EQ(MyTestObject::num_copies_, 0); + EXPECT_EQ(copy->data(), "World"); + ASSERT_FALSE(copy.has_ownership()); + + // Mutate it a first time: it performs a copy. + copy.get_mutable()->set_data("Foo"); + ASSERT_TRUE(copy.has_ownership()); + EXPECT_EQ(MyTestObject::num_copies_, 1); + EXPECT_EQ(copy->data(), "Foo"); + EXPECT_EQ(obj.data(), "World"); // It did not affect the original object. + obj.set_data("Baz"); // Changing the original doesn't affect us. + EXPECT_EQ(copy->data(), "Foo"); + ASSERT_TRUE(copy.has_ownership()); + + // Mutate it a second time: no additional copy. + copy.get_mutable()->set_data("Bar"); + EXPECT_EQ(MyTestObject::num_copies_, 1); + EXPECT_EQ(copy->data(), "Bar"); + ASSERT_TRUE(copy.has_ownership()); +} + +TEST(LazyMutableCopyTest, MoveWorks) { + // We rely on segfaults and the heapchecker to detect duplicate deletes and + // memory leaks, respectively. + MyTestObject::ResetCounters(); + MyTestObject obj("Hello"); + + LazyMutableCopy initial_copy(obj); + ASSERT_FALSE(initial_copy.has_ownership()); + EXPECT_EQ(MyTestObject::num_copies_, 0); + EXPECT_EQ(initial_copy->data(), "Hello"); + LazyMutableCopy moved0 = std::move(initial_copy); + ASSERT_FALSE(moved0.has_ownership()); + EXPECT_EQ(MyTestObject::num_copies_, 0); + EXPECT_EQ(moved0->data(), "Hello"); + obj.set_data("World"); + ASSERT_FALSE(moved0.has_ownership()); + EXPECT_EQ(MyTestObject::num_copies_, 0); + EXPECT_EQ(moved0->data(), "World"); + + // Now, make it mutable, and move it again. + moved0.get_mutable()->set_data("Foo"); + ASSERT_TRUE(moved0.has_ownership()); + EXPECT_EQ(MyTestObject::num_copies_, 1); + EXPECT_EQ(moved0->data(), "Foo"); + LazyMutableCopy moved1 = std::move(moved0); + ASSERT_TRUE(moved1.has_ownership()); + EXPECT_EQ(MyTestObject::num_copies_, 1); + EXPECT_EQ(moved1->data(), "Foo"); + EXPECT_EQ(MyTestObject::num_copies_, 1); + EXPECT_EQ(moved1->data(), "Foo"); + + // At destruction, nothing bad should happen. +} + +TEST(LazyMutableCopyTest, ReferenceAndClear) { + MyTestObject::ResetCounters(); + MyTestObject a("1234"); + + { + LazyMutableCopy wrapper(a); + EXPECT_EQ(&*wrapper, &a); // Same object. + EXPECT_EQ(&*wrapper, &a); // Still same object. + + // Does nothing, but still nullptr after. + std::move(wrapper).dispose(); + + // NOLINTNEXTLINE(bugprone-use-after-move) + EXPECT_EQ(wrapper.get_mutable(), nullptr); + } + + EXPECT_EQ(MyTestObject::num_copies_, 0); + EXPECT_EQ(MyTestObject::num_moves_, 0); + EXPECT_EQ(MyTestObject::num_deletes_, 0); +} + +TEST(LazyMutableCopyTest, MoveAndClear) { + MyTestObject::ResetCounters(); + MyTestObject a("1234"); + + { + LazyMutableCopy wrapper(std::move(a)); + EXPECT_EQ(wrapper->data(), "1234"); + EXPECT_NE(&*wrapper, &a); // different object. + EXPECT_EQ(MyTestObject::num_moves_, 1); + + // This destroys the moved object. + std::move(wrapper).dispose(); + + // NOLINTNEXTLINE(bugprone-use-after-move) + EXPECT_EQ(wrapper.get_mutable(), nullptr); + } + + EXPECT_EQ(MyTestObject::num_copies_, 0); + EXPECT_EQ(MyTestObject::num_moves_, 1); + EXPECT_EQ(MyTestObject::num_deletes_, 1); +} + +TEST(LazyMutableCopyTest, ExtractAsUniquePtr) { + MyTestObject::ResetCounters(); + MyTestObject a("1234"); + + { + LazyMutableCopy wrapper(std::move(a)); + EXPECT_EQ(wrapper->data(), "1234"); + EXPECT_NE(&*wrapper, &a); // different object. + EXPECT_EQ(MyTestObject::num_moves_, 1); + + // This destroys the moved object at the end, still no copy. + std::unique_ptr up = + std::move(wrapper).copy_or_move_as_unique_ptr(); + + // NOLINTNEXTLINE(bugprone-use-after-move) + EXPECT_EQ(wrapper.get(), nullptr); + // NOLINTNEXTLINE(bugprone-use-after-move) + EXPECT_EQ(wrapper.get_mutable(), nullptr); + } + + EXPECT_EQ(MyTestObject::num_copies_, 0); + EXPECT_EQ(MyTestObject::num_moves_, 1); + EXPECT_EQ(MyTestObject::num_deletes_, 1); +} + +TEST(LazyMutableCopyTest, ReferenceAndCopy) { + MyTestObject::ResetCounters(); + MyTestObject a("1234"); + + { + LazyMutableCopy wrapper(a); + EXPECT_EQ(&*wrapper, &a); // Same object. + EXPECT_EQ(&*wrapper, &a); // Still same object. + + wrapper.get_mutable()->set_data("hello"); + EXPECT_NE(&*wrapper, &a); // different object. + EXPECT_EQ(wrapper->data(), "hello"); + } + + EXPECT_EQ(MyTestObject::num_copies_, 1); + EXPECT_EQ(MyTestObject::num_moves_, 0); + EXPECT_EQ(MyTestObject::num_deletes_, 1); +} + +TEST(LazyMutableCopyTest, MoveAndCopy) { + MyTestObject::ResetCounters(); + MyTestObject a("1234"); + + { + // Test that we can move the wrapper. + LazyMutableCopy w1(std::move(a)); + LazyMutableCopy wrapper(std::move(w1)); + + EXPECT_EQ(wrapper->data(), "1234"); + EXPECT_NE(&*wrapper, &a); // different object. + EXPECT_EQ(MyTestObject::num_moves_, 1); + + // We can mutate it without issue and not further work. + wrapper.get_mutable()->set_data("hello"); + EXPECT_EQ(wrapper->data(), "hello"); + wrapper.get_mutable()->set_data("other"); + EXPECT_EQ(wrapper->data(), "other"); + } + + EXPECT_EQ(MyTestObject::num_copies_, 0); + EXPECT_EQ(MyTestObject::num_moves_, 1); + EXPECT_EQ(MyTestObject::num_deletes_, 1); +} + +} // namespace +} // namespace operations_research diff --git a/ortools/util/logging_test.cc b/ortools/util/logging_test.cc new file mode 100644 index 00000000000..f4157d5cbaa --- /dev/null +++ b/ortools/util/logging_test.cc @@ -0,0 +1,130 @@ +// Copyright 2010-2025 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/util/logging.h" + +#include +#include + +#include "absl/log/check.h" +#include "absl/strings/numbers.h" +#include "absl/strings/string_view.h" +#include "gtest/gtest.h" +#include "ortools/port/scoped_std_stream_capture.h" + +namespace operations_research { +namespace { + +TEST(FormatCounterTest, BasicCases) { + EXPECT_EQ("12", FormatCounter(12)); + EXPECT_EQ("12'345", FormatCounter(12345)); + EXPECT_EQ("123'456'789", FormatCounter(123456789)); +} + +TEST(LoggingTest, StandardLogToStdout) { + SolverLogger logger; + logger.EnableLogging(true); + logger.SetLogToStdOut(true); + + ScopedStdStreamCapture stdout_capture(CapturedStream::kStdout); + SOLVER_LOG(&logger, "output to log"); + if (ScopedStdStreamCapture::kIsSupported) { + EXPECT_EQ(std::move(stdout_capture).StopCaptureAndReturnContents(), + "output to log\n"); + } +} + +TEST(LoggingTest, StandardLogDisabledStdout) { + SolverLogger logger; + logger.EnableLogging(true); + logger.SetLogToStdOut(false); + + ScopedStdStreamCapture stdout_capture(CapturedStream::kStdout); + SOLVER_LOG(&logger, "output to log"); + EXPECT_EQ(std::move(stdout_capture).StopCaptureAndReturnContents(), ""); +} + +TEST(LoggingTest, CaptureLogCallback) { + SolverLogger logger; + logger.EnableLogging(true); + logger.SetLogToStdOut(false); + + CHECK_EQ(logger.NumInfoLoggingCallbacks(), 0); + + // Custom callback method. + std::string output_string; + const auto write_to_string = [&output_string](absl::string_view message) { + output_string.append(message); + output_string.append("\n"); + }; + logger.AddInfoLoggingCallback(write_to_string); + + CHECK_EQ(logger.NumInfoLoggingCallbacks(), 1); + + SOLVER_LOG(&logger, "output to string"); + EXPECT_EQ(output_string, "output to string\n"); + SOLVER_LOG(&logger, "!!"); + EXPECT_EQ(output_string, "output to string\n!!\n"); + + logger.ClearInfoLoggingCallbacks(); + + CHECK_EQ(logger.NumInfoLoggingCallbacks(), 0); + SOLVER_LOG(&logger, "we should not see that"); + EXPECT_EQ(output_string, "output to string\n!!\n"); +} + +TEST(LoggingTest, Throttling) { + SolverLogger logger; + + int num_seen = 0; + int num_skipped = 0; + const auto callback = [&num_seen, &num_skipped](absl::string_view message) { + ++num_seen; + + const std::string marker = "[skipped_logs="; + const int start_pos = message.find(marker); + if (start_pos < 0) return; + int num = 0; + + std::string sub = std::string(message.substr(start_pos + marker.size())); + int end_pos = sub.find(']'); + if (end_pos < 0) return; + sub = sub.substr(0, end_pos); + + if (absl::SimpleAtoi(sub, &num)) num_skipped += num; + }; + + logger.EnableLogging(true); + logger.AddInfoLoggingCallback(callback); + + // Hopefully, this will be above the default limit in all settings. + const int id = logger.GetNewThrottledId(); + const int num_emitted = 1000; + for (int i = 0; i < num_emitted; ++i) { + logger.ThrottledLog(id, "test"); + if (i == 500) { + // Lets output some in the middle. + logger.FlushPendingThrottledLogs(/*ignore_rates=*/true); + } + } + + // Make sure we output last one. + logger.FlushPendingThrottledLogs(/*ignore_rates=*/true); + + EXPECT_GT(num_seen, 10); // We have a minimum limit. + EXPECT_LT(num_seen, 500); + EXPECT_EQ(num_seen + num_skipped, num_emitted); +} + +} // namespace +} // namespace operations_research diff --git a/ortools/util/permutation_test.cc b/ortools/util/permutation_test.cc new file mode 100644 index 00000000000..562065b24da --- /dev/null +++ b/ortools/util/permutation_test.cc @@ -0,0 +1,35 @@ +// Copyright 2010-2025 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/util/permutation.h" + +#include + +#include "gtest/gtest.h" + +namespace operations_research { + +TEST(PermutationTest, BasicOperation) { + auto data = std::to_array({1005, 1000, 1004, 1001, 1003, 1002}); + auto permutation = std::to_array({1, 3, 5, 4, 2, 0}); + + ArrayIndexCycleHandler handler(data.data()); + PermutationApplier permuter(&handler); + static_assert(data.size() == permutation.size()); + permuter.Apply(permutation.data(), 0, data.size()); + for (int i = 0; i < data.size(); ++i) { + EXPECT_EQ(data[i], 1000 + i); + } +} + +} // namespace operations_research diff --git a/ortools/util/random_engine_test.cc b/ortools/util/random_engine_test.cc new file mode 100644 index 00000000000..6ca875853a4 --- /dev/null +++ b/ortools/util/random_engine_test.cc @@ -0,0 +1,27 @@ +// Copyright 2010-2025 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/util/random_engine.h" + +#include + +#include "gtest/gtest.h" + +// This test is just here to make sure that a type has been selected that +// models UniformRandomBitGenerator. +TEST(RandomEngineCompileTest, TestIt) { + operations_research::random_engine_t engine; + engine.seed(10); + ASSERT_LT(engine.min(), engine.max()); + EXPECT_LT(-1, std::uniform_int_distribution(0, 1)(engine)); +} diff --git a/ortools/util/random_subset.h b/ortools/util/random_subset.h new file mode 100644 index 00000000000..4cabf7b5e9c --- /dev/null +++ b/ortools/util/random_subset.h @@ -0,0 +1,231 @@ +// Copyright 2010-2025 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. + +#ifndef ORTOOLS_UTIL_RANDOM_SUBSET_H_ +#define ORTOOLS_UTIL_RANDOM_SUBSET_H_ + +#include +#include +#include +#include +#include + +#include "absl/algorithm/container.h" +#include "absl/container/flat_hash_set.h" +#include "absl/log/check.h" +#include "absl/random/bit_gen_ref.h" +#include "absl/random/distributions.h" +#include "absl/types/span.h" +#include "ortools/base/types.h" + +namespace operations_research { + +// Returns a random subset of k integers in [0..n). Works in O(k). +// +// ORDER: Like RandomSubsetOf(): Deterministically dependent on `random`, but +// neither sorted nor a uniformly random order. +// +// NOTE: If you need to call this many times with the same n, you should use +// the faster RandomSubsetOf(). +template +std::vector RandomSubset(IntType n, IntType k, absl::BitGenRef random); + +// Returns a Span of size `k` of random elements from `shufflable_elements`. +// Mutates `shufflable_elements` by partially shuffling it, only if necessary: +// in the particular cases k=0 and k=shufflable_elements->size(), for example, +// no shuffling is done. +// The returned Span points to the first k elements of `shufflable_elements`. +// As such, it is invalidated by the next call to RandomSubsetOf(). +// +// ORDER: The order of elements in the returned Span is neither sorted nor +// a uniformly random shuffle, but it is deterministically tied to `random`. +// +// COMPLEXITY: O(min(k, N-k)) where N = shufflable_elements->size(). +template +absl::Span RandomSubsetOf(int64_t k, std::vector* shufflable_elements, + absl::BitGenRef random); + +// Returns k sorted numbers, distinct or not, from the [0..n) interval. The +// returned sequence will be picked uniformly at random. +// Base constraints: n ≥ 0, k ≥ 0. +// COMPLEXITY: O(k log k). Can be improved to O(min(n, k log k)) if necessary. +template +std::vector RandomSortedElements( + IntType n, IntType k, absl::BitGenRef random, + // If true, the same number may appear multiple times in the output. + bool allow_repetition = false); + +// Like RandomSortedElements(n+1, k), but with an arbitrary offset. The first +// element is always "mini" and the last element is always "maxi". +// Preconditions: k ≥ 2 and maxi ≥ mini. +template +std::vector RandomValuesSpanning( + IntType mini, IntType maxi, IntType num_values, absl::BitGenRef random, + // If true, the same number may appear multiple times in the output. + bool allow_repetition = false); + +// Returns `k` nonnegative numbers whose sum is equal to `sum`, +// picked uniformly at random. Same complexity as RandomSortedElements(). +template +std::vector RandomSplit(IntType sum, IntType k, + absl::BitGenRef random); + +// _____________________________________________________________________________ +// Implementation of the templates. + +template +std::vector RandomSubset(IntType n, IntType k, + absl::BitGenRef random) { + CHECK_GE(k, 0); + CHECK_LE(k, n); + // Performance optimizations for small k. + if (k <= 2) { + if (k == 0) return {}; + if (k == 1) return {absl::Uniform(random, 0, n)}; + // k = 2. We pick `a` uniformly, then pick `b-a` uniformly in [1, n). + // We obtain b from b-a by shifting it by `a`, circling back over n, and + // avoiding overflows. + const IntType a = absl::Uniform(random, 0, n); + IntType b = absl::Uniform(random, 1, n); // This is b-a so far. + if (b < n - a) { + b += a; + DCHECK_LT(b, n); + } else { + b -= n - a; // Like b += a [mod n], but without overflow. + DCHECK_GE(b, 0); + } + return {a, b}; + } + // This is a weird formula, based on data: go/random-subset-benchmark + const int kThresholdRatio = n < 30000 ? 100 + : n < 3000000 ? 50 + : n < 30000000 ? 16 + : 25; + if (k > n / kThresholdRatio) { + // It's faster to build the full vector and then use RandomSubsetOf(). + std::vector values(static_cast(n), 0); + absl::c_iota(values, 0); + // Instead of copying the returned Span, we directly resize the "values" + // vector. This spares a reallocation of memory. + CHECK_LE(k, kint64max); + RandomSubsetOf(static_cast(k), &values, random); + values.resize(static_cast(k)); + return values; + } + // Else, it's faster to build a sparse set of the drawn values. + std::vector values; + values.reserve(static_cast(k)); + absl::flat_hash_set values_set; + values_set.reserve(static_cast(k)); + while (values.size() < k) { + const IntType v = absl::Uniform(random, 0, n); + if (values_set.insert(v).second) values.push_back(v); + } + return values; +} + +template +absl::Span RandomSubsetOf(int64_t k, std::vector* shufflable_elements, + absl::BitGenRef random) { + DCHECK_GE(k, 0); + const int64_t n = shufflable_elements->size(); + if (k <= n / 2) { + for (int64_t i = 0; i < k; i++) { + using std::swap; // go/using-std-swap + swap((*shufflable_elements)[i], + (*shufflable_elements)[absl::Uniform(random, i, n)]); + } + } else { // k > n/2: we shuffle only n-k elements in the back. + for (int i = n - 1; i >= k; i--) { + using std::swap; + swap((*shufflable_elements)[i], + (*shufflable_elements)[absl::Uniform(random, 0, i + 1)]); + } + } + return {shufflable_elements->data(), static_cast(k)}; +} + +template +std::vector RandomSortedElements(IntType n, IntType k, + absl::BitGenRef random, + bool allow_repetition) { + if (k == 0) return {}; + if (allow_repetition) { + // Drawing a sorted list of k elements of [0..n) with repetitions is not + // trivial: the simple method of drawing k numbers independently and + // sorting them is biased, because (0, 0, 0) is 3 times less likely to be + // generated than (0, 0, 1) for example: the latter can be generated by 3 + // unsorted draws (0,0,1), (0,1,0) and (1,0,0). + // The trick is to fall back to the perfectly unbiased RandomSubset(), like + // the case with allow_repetition=false, by drawing in the [0, n+k-1) space + // and then applying out[i] -= i to each element, thus potentially getting + // duplicates. It's correct because it's bijective (the inverse is [i]+=i). + n += k - IntType{1}; + CHECK_GE(n, k - (IntType{1})); // Detect potential overflows. + } + std::vector out = RandomSubset(n, k, random); + std::sort(out.begin(), out.end()); + if (allow_repetition) { + for (size_t i = 1; i < k; ++i) out[i] -= static_cast(i); + } + return out; +} + +template +std::vector RandomValuesSpanning(IntType mini, IntType maxi, + IntType num_values, + absl::BitGenRef random, + bool allow_repetition) { + CHECK_GE(maxi, mini); + CHECK_GE(num_values, 2); + if (num_values == 2) return {mini, maxi}; + // We generate the inner portion, increment it by mini[+1], then manually + // prepend mini and append maxi at both ends. + const IntType n = + allow_repetition ? maxi - mini + IntType{1} : maxi - mini - IntType{1}; + // Detect overflows here. Note that we could completely avoid overflows by: + // - making the `n` argument of RandomSortedElements() inclusive; + // - and using Bits::UnsignedType::Type here, to make sure that the + // difference (maxi - mini) fits. + // But it would make the code more complex, so we didn't do it yet. + CHECK_GE(n, 1); + std::vector inner = RandomSortedElements( + n, num_values - IntType{2}, random, allow_repetition); + const IntType inner_offset = allow_repetition ? mini : mini + IntType{1}; + if (!allow_repetition) { + for (IntType& x : inner) x += inner_offset; + } + inner.insert(inner.begin(), mini); + inner.push_back(maxi); + return inner; +} + +template +std::vector RandomSplit(IntType sum, IntType k, + absl::BitGenRef random) { + if (k == 0) return {}; + // Catch overflows before they happen inside RandomSortedElements(). + CHECK_LE(sum, std::numeric_limits::max() - k - 2); + std::vector out = + RandomValuesSpanning(0, sum, k + IntType{1}, random, + /*allow_repetition=*/true); + for (size_t i = 0; i < k; ++i) { + out[i] = out[i + 1] - out[i]; + } + out.pop_back(); + return out; +} + +} // namespace operations_research + +#endif // ORTOOLS_UTIL_RANDOM_SUBSET_H_ diff --git a/ortools/util/random_subset_benchmark.cc b/ortools/util/random_subset_benchmark.cc new file mode 100644 index 00000000000..441de3c2764 --- /dev/null +++ b/ortools/util/random_subset_benchmark.cc @@ -0,0 +1,47 @@ +// Copyright 2010-2025 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 + +#include "absl/random/random.h" +#include "benchmark/benchmark.h" +#include "ortools/util/random_subset.h" + +namespace operations_research { +namespace { + +void BM_RandomSubset(benchmark::State& state) { + absl::BitGen random; + const int N = state.range(0); + const int K = state.range(1); + for (auto _ : state) { + std::vector subset = RandomSubset(N, K, random); + benchmark::DoNotOptimize(subset); + } +} + +void BenchmarkArgs(benchmark::Benchmark* bm) { + for (int n : {100, 1024, 10240, 102400, 1048576, 10485760, 104857600}) { + std::vector k_values = {0, 1, 2, 3, 4}; + while (k_values.back() < n / 10) { + k_values.push_back(k_values.back() * 2); + } + for (int k : k_values) { + bm->Args({n, k}); + } + } +} +BENCHMARK(BM_RandomSubset)->Apply(BenchmarkArgs); + +} // namespace +} // namespace operations_research diff --git a/ortools/util/random_subset_test.cc b/ortools/util/random_subset_test.cc new file mode 100644 index 00000000000..ec7ba25aa84 --- /dev/null +++ b/ortools/util/random_subset_test.cc @@ -0,0 +1,526 @@ +// Copyright 2010-2025 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/util/random_subset.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "absl/algorithm/container.h" +#include "absl/container/btree_map.h" +#include "absl/container/flat_hash_map.h" +#include "absl/container/flat_hash_set.h" +#include "absl/numeric/int128.h" +#include "absl/random/distributions.h" +#include "absl/random/random.h" +#include "absl/strings/str_format.h" +#include "absl/time/clock.h" +#include "absl/time/time.h" +#include "gtest/gtest.h" +#include "ortools/algorithms/n_choose_k.h" +#include "ortools/base/dump_vars.h" +#include "ortools/base/gmock.h" +#include "ortools/base/log_severity.h" +#include "ortools/base/mathutil.h" +#include "ortools/util/random_engine.h" +#include "ortools/util/testing_utils.h" + +namespace operations_research { +namespace { + +using ::testing::AnyOf; +using ::testing::ElementsAre; +using ::testing::IsEmpty; +using ::testing::UnorderedElementsAre; + +class RandomSubsetTest : public testing::Test { + protected: + absl::BitGen random_; +}; + +TEST_F(RandomSubsetTest, ZeroOfZero) { + EXPECT_THAT(RandomSubset(0, 0, random_), IsEmpty()); +} + +TEST_F(RandomSubsetTest, OneOfOne) { + EXPECT_THAT(RandomSubset(1, 1, random_), ElementsAre(0)); +} + +TEST_F(RandomSubsetTest, ZeroOfOne) { + EXPECT_THAT(RandomSubset(1, 0, random_), IsEmpty()); +} + +TEST_F(RandomSubsetTest, ThreeOfThree) { + EXPECT_THAT(RandomSubset(3, 3, random_), UnorderedElementsAre(0, 1, 2)); +} + +TEST_F(RandomSubsetTest, ZeroOfThree) { + EXPECT_THAT(RandomSubset(3, 0, random_), IsEmpty()); +} + +TEST_F(RandomSubsetTest, OneOfThree) { + EXPECT_THAT(RandomSubset(3, 1, random_), ElementsAre(AnyOf(0, 1, 2))); +} + +template +class TypedRandomSubsetTest : public testing::Test {}; + +TYPED_TEST_SUITE_P(TypedRandomSubsetTest); + +TYPED_TEST_P(TypedRandomSubsetTest, ResilienceToOverflowForSpecialCaseK) { + std::mt19937 random; // Deterministic, to avoid flakiness. + constexpr int kNumTests = 100'000; + constexpr TypeParam n = std::numeric_limits::max(); + for (int k = 0; k <= 3; ++k) { + TypeParam min_element = 0; + TypeParam max_element = 0; + for (int i = 0; i < kNumTests; ++i) { + const std::vector subset = + RandomSubset(n, k, random); + ASSERT_EQ(subset.size(), k); + for (int i = 1; i < k; ++i) { + for (int j = 0; j < i; ++j) { + ASSERT_NE(subset[i], subset[j]) << DUMP_VARS(subset); + } + } + for (int i = 0; i < k; ++i) { + ASSERT_GE(subset[i], 0); + ASSERT_LT(subset[i], n); + min_element = std::min(min_element, subset[i]); + max_element = std::max(max_element, subset[i]); + } + } + if (k > 0) { + // To verify that we didn't crop the high-order bit or did something else, + // funny, we check the approximate covered range of values. + ASSERT_LE(min_element, 5 * (1 + n / kNumTests)); + ASSERT_GE(max_element, n - 5 * (1 + n / kNumTests)); + } + } +} + +REGISTER_TYPED_TEST_SUITE_P(TypedRandomSubsetTest, + ResilienceToOverflowForSpecialCaseK); + +typedef testing::Types + SmallAndBigIntTypes; + +INSTANTIATE_TYPED_TEST_SUITE_P(Foo, TypedRandomSubsetTest, SmallAndBigIntTypes); + +TEST_F(RandomSubsetTest, PerformanceForHugeNSmallK) { + absl::Time start_time = absl::Now(); + constexpr int num_runs = DEBUG_MODE ? 10 : 100; + for (int iter = 0; iter < num_runs; ++iter) { + for (uint64_t n = 10000;; n *= 2) { + for (uint64_t k = 2; k < 1000; k *= 2) { + std::vector subset = RandomSubset(n, k, random_); + ASSERT_EQ(subset.size(), k); + absl::flat_hash_set elements(subset.begin(), subset.end()); + ASSERT_EQ(elements.size(), k) << "There are some duplicate elements"; + for (uint64_t x : subset) { + ASSERT_GE(x, 0); + ASSERT_LT(x, n); + } + } + // We test this limit here because the next multiplication by 2 would + // overflow. + if (n >= 10'000'000'000'000'000'000u) break; + } + } + const absl::Duration test_duration = absl::Now() - start_time; + if (!DEBUG_MODE && !kAnyXsanEnabled) { + // Performance in forge, as of 2021-04, opt: a little less than 0.2s. + EXPECT_LE(test_duration, absl::Seconds(2)); + } +} + +TEST(RandomSubsetOfTest, PerformanceForSmallNMinusK) { + absl::Time start_time = absl::Now(); + constexpr int N = 1000000; + constexpr int K = N - 10; + std::vector elements(N, 0); + absl::c_iota(elements, 0); + constexpr int num_runs = DEBUG_MODE ? 25000 : 250000; + constexpr int num_full_checks = 10; + absl::BitGen random; + for (int iter = 0; iter < num_runs; ++iter) { + absl::Span subset = RandomSubsetOf(K, &elements, random); + ASSERT_EQ(subset.size(), K); + if (iter < num_full_checks) { + std::vector hit(N, false); + for (int x : subset) { + ASSERT_GE(x, 0); + ASSERT_LT(x, N); + ASSERT_FALSE(hit[x]) << x; + hit[x] = true; + } + } + } + const absl::Duration test_duration = absl::Now() - start_time; + if (!DEBUG_MODE && !kAnyXsanEnabled) { + // Performance in forge, as of 2021-04, opt: a little more than 0.2s. + EXPECT_LE(test_duration, absl::Seconds(2)); + } +} + +TEST(RandomSubsetOfTest, MovesElementsDoesNotCopy) { + absl::Time start_time = absl::Now(); + constexpr int payload_size = 100000; + constexpr int N = 1000; + std::vector> elements(N); + for (int i = 0; i < N; ++i) elements[i].assign(payload_size, i); + constexpr int num_runs = DEBUG_MODE ? 50000 : 500000; + absl::BitGen random; + for (int iter = 0; iter < num_runs; ++iter) { + const int K = iter % 2 == 0 ? 10 : N - 10; + absl::Span> subset = RandomSubsetOf(K, &elements, random); + ASSERT_EQ(subset.size(), K); + } + const absl::Duration test_duration = absl::Now() - start_time; + if (!DEBUG_MODE && !kAnyXsanEnabled) { + // Performance in forge, as of 2021-04, opt: a little more than 0.2s. + EXPECT_LE(test_duration, absl::Seconds(2)); + } +} + +// The N and K of NChooseK(N, K): choose K elements among N. +// Or, in our use case: get a random subset of K elements of [0..N). +struct NK { + int n = 0; + int k = 0; +}; + +class RandomSubsetOfStressTest : public testing::TestWithParam { + protected: + absl::BitGen random_; +}; +INSTANTIATE_TEST_SUITE_P( + VariousNK, RandomSubsetOfStressTest, + // The idea is to run the stress test for values of (n,k) such that + // choose(n, k) isn't so big, so that we can do the brute-force statistical + // test in a reasonable time. + testing::Values(NK{1, 1}, NK{2, 1}, NK{3, 1}, NK{3, 2}, NK{10, 0}, NK{9, 9}, + NK{23, 1}, NK{7, 3}, NK{8, 6}, NK{37, 2}), + + // Pretty-print the NK parameter in the test name. + [](testing::TestParamInfo p) { + return absl::StrFormat("%d_%d", p.param.n, p.param.k); + }); + +template +testing::Matcher IsBetween(double lo, double hi) { + return testing::AllOf(testing::Ge(lo), testing::Le(hi)); +} + +TEST_P(RandomSubsetOfStressTest, StatisticalCorrectness) { + // Idea: we generate X * choose(N, K) random subsets of size K among N + // elements, and verify that the obtained distribution is similar to a + // uniform distribution over all possible choices. + constexpr int X = 100; + const int N = GetParam().n; + const int K = GetParam().k; + const int num_choices = NChooseK(N, K).value(); + const int num_trials = X * num_choices; + std::vector elements; + for (int i = 0; i < N; ++i) elements.push_back(static_cast('A' + i)); + absl::flat_hash_map subset_count; + for (int i = 0; i < num_trials; ++i) { + absl::Span subset_span = RandomSubsetOf(K, &elements, random_); + std::string subset(subset_span.begin(), subset_span.end()); + std::sort(subset.begin(), subset.end()); // To canonicalize it. + ++subset_count[subset]; + } + // Verify that all produced subsets were, in fact, valid subsets. + for (auto [subset, _] : subset_count) { + ASSERT_EQ(subset.size(), K); + std::vector hit(N, false); + for (char c : subset) { + ASSERT_TRUE(c >= 'A' && c < 'A' + N) << subset; + ASSERT_FALSE(hit[c - 'A']) << "Repeated '" << c << "' in " << subset; + hit[c - 'A'] = true; + } + } + // With the expected count at X, the probability for a given choice of not + // appearing at all is approximately exp(-X), so we do expect every + // choice to appear. + ASSERT_EQ(subset_count.size(), num_choices); + // When there is are few possible subsets, we make simple statistical tests. + if (num_choices < 100) { + for (auto [subset, count] : subset_count) { + ASSERT_THAT(count, IsBetween(0.5 * X, 2 * X)) << absl::StrFormat( + "Subset '%s' is anormally represented: expected %d, got %d", subset, + X, count); + } + return; + } + // Verify that we roughly have a Gaussian, with a few tests that should be + // robust enough to never fail (given enough "safety buffer"): + // - the stddev + // - the 68/95/99.7 rule, see: + // https://en.wikipedia.org/wiki/68%E2%80%9395%E2%80%9399.7_rule + // For a reference on the formulas, see for example: + // en.wikipedia.org/wiki/Binomial_distribution#Expected_value_and_variance, + // with p = 1/num_choices and n = num_trials. + const double p = 1.0 / num_choices; + const double expected_stddev = std::sqrt(num_trials * p * (1 - p)); + // We only test the stddev value if there are enough choices. For example, + // with only two choices, the expected stddev isn't zero, but it can happen + // that both choices get exactly X as count. + int64_t sum_squared_diff = 0; + for (auto [_, count] : subset_count) { + sum_squared_diff += (count - X) * (count - X); + } + const double observed_stddev = + std::sqrt(static_cast(sum_squared_diff) / num_choices); + // The observed stddev can be quite different from the expected one. + EXPECT_THAT(observed_stddev / expected_stddev, IsBetween(0.25, 4.0)) + << DUMP_VARS(observed_stddev, expected_stddev); + int num_within_stddev = 0; + int num_between_1_and_2_stddev = 0; + int num_between_2_and_3_stddev = 0; + int num_beyond_3_stddev = 0; + for (auto [_, count] : subset_count) { + const double dev = std::abs(count - X); + if (dev < expected_stddev) { + ++num_within_stddev; + } else if (dev < 2 * expected_stddev) { + ++num_between_1_and_2_stddev; + } else if (dev < 3 * expected_stddev) { + ++num_between_2_and_3_stddev; + } else { + ++num_beyond_3_stddev; + } + } + EXPECT_THAT(static_cast(num_within_stddev) / num_choices, + IsBetween(0.3, 0.8)); // Mean: 0.68 + EXPECT_THAT(static_cast(num_between_1_and_2_stddev) / num_choices, + // Mean: 0.27 + IsBetween(0.1, 0.6)); + EXPECT_THAT(static_cast(num_between_2_and_3_stddev) / num_choices, + // Mean: 0.05 + IsBetween(0.015, 0.2)); + EXPECT_THAT(static_cast(num_beyond_3_stddev) / num_choices, + IsBetween(num_choices < 30000 ? 0 : 0.001, + num_choices < 1000 ? 0.03 + : num_choices < 10000 ? 0.02 + : 0.01)); +} + +TEST(RandomSortedElementsTest, NoPoints) { + absl::BitGen random; + EXPECT_THAT(RandomSortedElements(/*n=*/0, /*k=*/0, random, + /*allow_repetition=*/false), + IsEmpty()); +} + +TEST(RandomSortedElementsTest, Singleton) { + absl::BitGen random; + EXPECT_THAT(RandomSortedElements(/*n=*/1, /*k=*/1, random, + /*allow_repetition=*/true), + ElementsAre(0)); + EXPECT_THAT(RandomSortedElements(/*n=*/1, /*k=*/1, random, + /*allow_repetition=*/false), + ElementsAre(0)); +} + +TEST(RandomValuesSpanningTest, NoRepetitions) { + absl::BitGen random; + EXPECT_THAT(RandomValuesSpanning(0, 3, /*num_values=*/3, random, + /*allow_repetition=*/false), + ElementsAre(0, testing::AnyOf(1, 2), 3)); +} + +TEST(RandomValuesSpanningTest, OnlyMiniAndMaxi) { + absl::BitGen random; + EXPECT_THAT(RandomValuesSpanning(-12, 15, /*num_values=*/2, random, + /*allow_repetition=*/false), + ElementsAre(-12, 15)); + EXPECT_THAT(RandomValuesSpanning(0, 1, /*num_values=*/2, random, + /*allow_repetition=*/false), + ElementsAre(0, 1)); +} + +TEST(RandomValuesSpanningTest, OnlyMiniAndMaxiAllowRepetition) { + absl::BitGen random; + EXPECT_THAT(RandomValuesSpanning(-12, 15, /*num_values=*/2, random, + /*allow_repetition=*/true), + ElementsAre(-12, 15)); + EXPECT_THAT(RandomValuesSpanning(-12, -12, /*num_values=*/2, random, + /*allow_repetition=*/true), + ElementsAre(-12, -12)); +} + +TEST(RandomSortedElementsAndRandomValuesSpanningTest, + VerifyInvariantsOnRandomizedInputs) { + absl::BitGen random; + for (bool allow_repetition : {false, true}) { + for (bool output_0_and_max : {false, true}) { + // Time per test: 7μs in fastbuild, 1.7μs in opt (forge, 2022-08-29). + constexpr int kNumTests = DEBUG_MODE ? 100'000 : 1'000'000; + for (int attempt = 0; attempt < kNumTests; ++attempt) { + const int k = absl::LogUniform(random, output_0_and_max ? 2 : 0, 15); + const int min_n = allow_repetition ? 1 : std::max(1, k); + const int n = absl::LogUniform(random, min_n, min_n + 1000); + std::vector sequence = + output_0_and_max + ? RandomValuesSpanning(0, n, k, random, allow_repetition) + : RandomSortedElements(n, k, random, allow_repetition); + EXPECT_EQ(sequence.size(), k); + EXPECT_TRUE(std::is_sorted(sequence.begin(), sequence.end())); + if (!allow_repetition) { + for (int i = 1; i < sequence.size(); ++i) { + EXPECT_GT(sequence[i], sequence[i - 1]) << i; + } + } + if (output_0_and_max && !sequence.empty()) { + EXPECT_EQ(sequence[0], 0); + EXPECT_EQ(sequence.back(), n); + } + ASSERT_FALSE(HasFailure()) << DUMP_VARS( + allow_repetition, output_0_and_max, attempt, k, n, sequence); + } + } + } +} + +TEST(RandomSortedElementsAndRandomValuesSpanningTest, Unbiased) { + random_engine_t random; // This is deterministic. + for (bool allow_repetition : {false, true}) { + for (bool output_0_and_max : {false, true}) { + absl::btree_map, int> sequence_count; + // Timing of this test, per partition: ~1.5μs in opt, ~5.4μs in fastbuild. + constexpr int kNumPartitions = DEBUG_MODE ? 1'000'000 : 2'000'000; + for (int i = 0; i < kNumPartitions; ++i) { + std::vector sequence = + output_0_and_max + ? RandomValuesSpanning(0, 6, 5, random, allow_repetition) + : RandomSortedElements( + /*n=*/7, /*k=*/5, random, allow_repetition); + ++sequence_count[std::move(sequence)]; + } + const int kNumDistinctPartitions = + output_0_and_max ? allow_repetition ? NChooseK(9, 3).value() + : NChooseK(5, 3).value() + : allow_repetition ? NChooseK(11, 5).value() + : NChooseK(7, 5).value(); + const int kAvgExpectedCount = kNumPartitions / kNumDistinctPartitions; + EXPECT_EQ(sequence_count.size(), kNumDistinctPartitions); + int num_counts_equal_to_avg = 0; + if (!HasFailure()) { + for (const auto& [partition, count] : sequence_count) { + EXPECT_GE(count, kAvgExpectedCount * 0.9); + EXPECT_LE(count, kAvgExpectedCount * 1.1); + // We also verify that our generation isn't artificially + // super-balanced: it shouldn't always be *exactly* 1/n. + if (count == kAvgExpectedCount) ++num_counts_equal_to_avg; + } + } + EXPECT_LE(num_counts_equal_to_avg, kNumDistinctPartitions / 10); + ASSERT_FALSE(HasFailure()) + << DUMP_VARS(allow_repetition, output_0_and_max); + } + } +} + +TEST(RandomSortedElementsTest, NegativeNOkWhenKIsZero) { + absl::BitGen random; + EXPECT_THAT(RandomSortedElements(/*n=*/-1, /*k=*/0, random), IsEmpty()); +} + +TEST(RandomSortedElementsDeathTest, NegativeN) { + absl::BitGen random; + EXPECT_DEATH(RandomSortedElements(/*n=*/-1, /*k=*/1, random), "-1"); +} + +TEST(RandomSortedElementsDeathTest, NTooSmall) { + absl::BitGen random; + EXPECT_DEATH(RandomSortedElements(/*n=*/0, /*k=*/1, random), "k <= n"); +} + +TEST(RandomSortedElementsDeathTest, NTooSmall2) { + absl::BitGen random; + EXPECT_DEATH(RandomSortedElements(/*n=*/3, /*k=*/4, random), "k <= n"); +} + +TEST(RandomSortedElementsDeathTest, NegativeK) { + absl::BitGen random; + EXPECT_DEATH(RandomSortedElements(/*n=*/10, /*k=*/-1, random), "-1"); +} + +TEST(RandomSortedElementsDeathTest, NegativeKAllowRepetition) { + absl::BitGen random; + EXPECT_DEATH(RandomSortedElements(/*n=*/10, /*k=*/-1, random, + /*allow_repetition=*/true), + "-1"); +} + +TEST(RandomValuesSpanningDeathTest, LessThanTwoValues) { + absl::BitGen random; + EXPECT_DEATH(RandomValuesSpanning(0, 10, /*num_values=*/1, random, + /*allow_repetition=*/false), + "1"); + EXPECT_DEATH(RandomValuesSpanning(0, 10, /*num_values=*/1, random, + /*allow_repetition=*/true), + "1"); +} + +TEST(RandomSplitTest, SimpleCase) { + absl::BitGen random; + const std::vector split = RandomSplit(100, 3, random); + ASSERT_EQ(split.size(), 3); + EXPECT_EQ(split[0] + split[1] + split[2], 100); +} + +TEST(RandomSplitTest, Singletons) { + absl::BitGen random; + EXPECT_THAT(RandomSplit(1, 1, random), ElementsAre(1)); + EXPECT_THAT(RandomSplit(0, 1, random), ElementsAre(0)); + EXPECT_THAT(RandomSplit(10'000'000'000'000'000'000U, 1, random), + ElementsAre(10'000'000'000'000'000'000U)); + const absl::uint128 kLarge = absl::uint128{1} << 127; + EXPECT_THAT(RandomSplit(kLarge, 1, random), + ElementsAre(kLarge)); +} + +TEST(RandomSplitTest, SplitZero) { + absl::BitGen random; + EXPECT_THAT(RandomSplit(0, 5, random), ElementsAre(0, 0, 0, 0, 0)); +} + +TEST(RandomSplitTest, SplitEmpty) { + absl::BitGen random; + EXPECT_THAT(RandomSplit(5, 0, random), IsEmpty()); +} + +TEST(RandomSplitTest, SplitEmptyAndZero) { + absl::BitGen random; + EXPECT_THAT(RandomSplit(0, 0, random), IsEmpty()); +} + +TEST(RandomSplitTest, RandomizedStressTest) { + absl::BitGen random; + const std::vector split = RandomSplit(100, 3, random); + ASSERT_EQ(split.size(), 3); + EXPECT_EQ(split[0] + split[1] + split[2], 100); +} + +} // namespace +} // namespace operations_research diff --git a/ortools/util/range_query_function_test.cc b/ortools/util/range_query_function_test.cc new file mode 100644 index 00000000000..dbaecb8967a --- /dev/null +++ b/ortools/util/range_query_function_test.cc @@ -0,0 +1,238 @@ +// Copyright 2010-2025 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/util/range_query_function.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "absl/log/check.h" +#include "gtest/gtest.h" +#include "ortools/base/types.h" + +namespace operations_research { +namespace { + +struct InsideIntervalTest { + int64_t range_begin; + int64_t range_end; + int64_t interval_begin; + int64_t interval_end; + int64_t first_inside_the_interval; + int64_t last_inside_the_interval; +}; + +struct TestCase { + std::function f; + int64_t domain_start; + int64_t domain_end; + std::vector inside_interval_tests; +}; + +class RangeQueryFunctionTest : public testing::Test { + protected: + void RunTests(const RangeIntToIntFunction* f, const TestCase& test); + static const struct TestCase kTests[]; +}; + +const TestCase RangeQueryFunctionTest::kTests[] = { + { + // TODO(user): add more inside_interval_tests + [](int64_t x) -> int64_t { return x; }, + /*domain_start=*/12, + /*domain_end=*/17, + /*inside_interval_tests=*/ + { + {14, 16, 15, 16, 15, 15}, + {12, 13, 12, 13, 12, 12}, + }, + }, + { + [](int64_t x) -> int64_t { return x % 4; }, + /*domain_start=*/11, + /*domain_end=*/16, + /*inside_interval_tests=*/ + { + {11, 16, 1, 3, 13, 14}, + }, + }, + { + [](int64_t x) -> int64_t { return 1000. * sin(x); }, + /*domain_start=*/-3, + /*domain_end=*/3, + /*inside_interval_tests=*/{}, + }, + { + [](int64_t x) -> int64_t { + const double value = sin(x) * cosh(x) * exp(x); + if (value > kint64max) return kint64max; + if (value < kint64min) return kint64min; + return value; + }, + /*domain_start=*/1153, + /*domain_end=*/1157, + /*inside_interval_tests=*/{}, + }, +}; + +void RangeQueryFunctionTest::RunTests(const RangeIntToIntFunction* f, + const TestCase& test) { + for (int64_t i = test.domain_start; i < test.domain_end; ++i) { + EXPECT_EQ(test.f(i), f->Query(i)); + } + + for (int64_t from = test.domain_start; from < test.domain_end; ++from) { + for (int64_t to = from + 1; to <= test.domain_end; ++to) { + int64_t brute_force_min = kint64max; + int64_t brute_force_max = kint64min; + for (int64_t i = from; i < to; ++i) { + brute_force_min = std::min(brute_force_min, test.f(i)); + brute_force_max = std::max(brute_force_max, test.f(i)); + } + EXPECT_EQ(brute_force_min, f->RangeMin(from, to)); + EXPECT_EQ(brute_force_max, f->RangeMax(from, to)); + } + } + + for (const InsideIntervalTest& inside_interval_test : + test.inside_interval_tests) { + EXPECT_EQ(inside_interval_test.first_inside_the_interval, + f->RangeFirstInsideInterval(inside_interval_test.range_begin, + inside_interval_test.range_end, + inside_interval_test.interval_begin, + inside_interval_test.interval_end)); + EXPECT_EQ(inside_interval_test.last_inside_the_interval, + f->RangeLastInsideInterval(inside_interval_test.range_begin, + inside_interval_test.range_end, + inside_interval_test.interval_begin, + inside_interval_test.interval_end)); + } +} + +TEST_F(RangeQueryFunctionTest, BareIntToIntFunction) { + for (const TestCase& test : kTests) { + std::unique_ptr f(MakeBareIntToIntFunction(test.f)); + RunTests(f.get(), test); + } +} + +TEST_F(RangeQueryFunctionTest, CachedIntToIntFunction) { + for (const TestCase& test : kTests) { + std::unique_ptr f( + MakeCachedIntToIntFunction(test.f, test.domain_start, test.domain_end)); + RunTests(f.get(), test); + } +} + +// The index rmq test is responsible for testing extreme sequences of values. +// This test should rather check if RangeMinMaxIndexFunction correctly calls the +// rmq, if functions with positive/negative domains work and the behaviour when +// the range is invalid. +class RangeMinMaxIndexFunctionTest : public testing::Test { + protected: + static int64_t CorrectResultsTestFunction(const int64_t arg) { + static const auto kValues = std::to_array({6, 8, -1, 3, 6}); + static const int64_t kOffset = 2; + CHECK(arg + kOffset < kValues.size()); + return kValues[arg + kOffset]; + } + static int64_t Identity(int64_t arg) { return arg; } +}; + +TEST_F(RangeMinMaxIndexFunctionTest, CorrectResults) { + std::unique_ptr f( + MakeCachedRangeMinMaxIndexFunction(CorrectResultsTestFunction, -2, 3)); + EXPECT_EQ(0, f->RangeMinArgument(-2, 3)); + EXPECT_EQ(-1, f->RangeMaxArgument(-2, 3)); + EXPECT_EQ(-2, f->RangeMinArgument(-2, 0)); + EXPECT_EQ(-1, f->RangeMaxArgument(-2, 0)); + EXPECT_EQ(1, f->RangeMinArgument(1, 3)); + EXPECT_EQ(2, f->RangeMaxArgument(1, 3)); +} + +TEST_F(RangeMinMaxIndexFunctionTest, DomainCheck) { + const std::function Identity = [](int64_t x) -> int64_t { + return x; + }; + { + std::unique_ptr negative_domain( + MakeCachedRangeMinMaxIndexFunction(Identity, -200, -100)); + for (int64_t range_start : {-160, -150, -180}) { + for (int64_t range_end : {-140, -149, -100}) { + EXPECT_EQ(range_start, + negative_domain->RangeMinArgument(range_start, range_end)); + EXPECT_EQ(range_end - 1, + negative_domain->RangeMaxArgument(range_start, range_end)); + } + } + } + { + std::unique_ptr positive_domain( + MakeCachedRangeMinMaxIndexFunction(Identity, 100, 200)); + for (int64_t range_start : {140, 150, 100}) { + for (int64_t range_end : {160, 151, 180}) { + EXPECT_EQ(range_start, + positive_domain->RangeMinArgument(range_start, range_end)); + EXPECT_EQ(range_end - 1, + positive_domain->RangeMaxArgument(range_start, range_end)); + } + } + } + { + std::unique_ptr kint64min_domain( + MakeCachedRangeMinMaxIndexFunction(Identity, kint64min, kint64min + 1)); + EXPECT_EQ(kint64min, + kint64min_domain->RangeMinArgument(kint64min, kint64min + 1)); + EXPECT_EQ(kint64min, + kint64min_domain->RangeMaxArgument(kint64min, kint64min + 1)); + + std::unique_ptr kint64max_domain( + MakeCachedRangeMinMaxIndexFunction(Identity, kint64max - 1, kint64max)); + EXPECT_EQ(kint64max - 1, + kint64max_domain->RangeMinArgument(kint64max - 1, kint64max)); + EXPECT_EQ(kint64max - 1, + kint64max_domain->RangeMaxArgument(kint64max - 1, kint64max)); + } + EXPECT_DEATH(MakeCachedRangeMinMaxIndexFunction(Identity, 0, 0), + "Check failed: [^ ]*"); +} + +#ifndef NDEBUG +// RangeMinArgument and RangeMaxArgument are performance citical functions, and +// they are guarded with DCHECKs. Their behavior is undefined in release mode, +// so we check debug mode only. +TEST_F(RangeMinMaxIndexFunctionTest, InvalidRangeQuery) { + std::unique_ptr f( + MakeCachedRangeMinMaxIndexFunction(Identity, 0, 10)); + EXPECT_DEATH(f->RangeMinArgument(-1, 10), "Check failed: [^ ]*"); + EXPECT_DEATH(f->RangeMinArgument(0, 11), "Check failed: [^ ]*"); + EXPECT_DEATH(f->RangeMinArgument(0, 0), "Check failed: [^ ]*"); + EXPECT_DEATH(f->RangeMinArgument(5, 5), "Check failed: [^ ]*"); + EXPECT_DEATH(f->RangeMinArgument(10, 10), "Check failed: [^ ]*"); + EXPECT_DEATH(f->RangeMinArgument(-1, -20), "Check failed: [^ ]*"); + + EXPECT_DEATH(f->RangeMaxArgument(-1, 10), "Check failed: [^ ]*"); + EXPECT_DEATH(f->RangeMaxArgument(0, 11), "Check failed: [^ ]*"); + EXPECT_DEATH(f->RangeMaxArgument(0, 0), "Check failed: [^ ]*"); + EXPECT_DEATH(f->RangeMaxArgument(5, 5), "Check failed: [^ ]*"); + EXPECT_DEATH(f->RangeMaxArgument(10, 10), "Check failed: [^ ]*"); + EXPECT_DEATH(f->RangeMaxArgument(-1, -20), "Check failed: [^ ]*"); +} +#endif // NDEBUG +} // namespace +} // namespace operations_research diff --git a/ortools/util/rational_approximation_test.cc b/ortools/util/rational_approximation_test.cc new file mode 100644 index 00000000000..8325011bb81 --- /dev/null +++ b/ortools/util/rational_approximation_test.cc @@ -0,0 +1,54 @@ +// Copyright 2010-2025 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/util/rational_approximation.h" + +#include + +#include "gtest/gtest.h" +#include "ortools/util/fp_utils.h" + +namespace operations_research { +namespace { +static const double kEpsilon = std::numeric_limits::epsilon(); + +TEST(RationalApproximation, ContinuedFraction) { + Fraction fraction = RationalApproximation(2.0 / 3, kEpsilon); + EXPECT_EQ(fraction.first, 2); + EXPECT_EQ(fraction.second, 3); + const double kTestedNumber = 17.373281721478865; + fraction = RationalApproximation(kTestedNumber, kEpsilon); + // The result for fraction.first and fraction.second varies depending on the + // type for Fractional (kTestedNumber cannot be represented accurately + // in a float), so we just test that the fraction is close enough. + EXPECT_COMPARABLE(kTestedNumber, + static_cast(fraction.first) / + static_cast(fraction.second), + kEpsilon); + fraction = RationalApproximation(0.4214, kEpsilon); + EXPECT_EQ(fraction.first, 2107); + EXPECT_EQ(fraction.second, 5000); + fraction = RationalApproximation(0.42139999999999994, kEpsilon); + EXPECT_EQ(fraction.first, 2107); + EXPECT_EQ(fraction.second, 5000); + // Expects rational approximations within a given precision. + fraction = RationalApproximation(0.66667, 1e-5); + EXPECT_EQ(fraction.first, 2); + EXPECT_EQ(fraction.second, 3); + fraction = RationalApproximation(0.1428572, 1e-6); + EXPECT_EQ(fraction.first, 1); + EXPECT_EQ(fraction.second, 7); +} + +} // anonymous namespace +} // namespace operations_research diff --git a/ortools/util/rev_test.cc b/ortools/util/rev_test.cc new file mode 100644 index 00000000000..4828e333f90 --- /dev/null +++ b/ortools/util/rev_test.cc @@ -0,0 +1,230 @@ +// Copyright 2010-2025 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/util/rev.h" + +#include +#include +#include + +#include "absl/container/btree_map.h" +#include "absl/container/flat_hash_map.h" +#include "absl/random/distributions.h" +#include "gtest/gtest.h" +#include "ortools/util/strong_integers.h" + +namespace operations_research { +namespace { + +TEST(RevRepositoryTest, BasicNonStampedBehavior) { + RevRepository repo; + int a = 1; + int b = 2; + int c = 3; + repo.SaveState(&a); // no-op. + a = 2; + repo.SetLevel(0); // no-op. + EXPECT_EQ(a, 2); + repo.SetLevel(1); + repo.SaveState(&a); + repo.SaveState(&b); + b = 13; + repo.SaveState(&b); + repo.SaveState(&c); + a = 12; + c = 14; + repo.SetLevel(10); + repo.SaveState(&a); + a = 20; + repo.SetLevel(11); + EXPECT_EQ(a, 20); + repo.SetLevel(10); + EXPECT_EQ(a, 20); + repo.SetLevel(9); + EXPECT_EQ(a, 12); + repo.SetLevel(1); + EXPECT_EQ(a, 12); + EXPECT_EQ(b, 13); + EXPECT_EQ(c, 14); + repo.SetLevel(0); + EXPECT_EQ(a, 2); + EXPECT_EQ(b, 2); + EXPECT_EQ(c, 3); +} + +TEST(RevRepositoryTest, NoDiffWithStampedBehavior) { + RevRepository repo1; + RevRepository repo2; + std::vector vars1(10); + std::vector vars2(10); + std::vector stamps2(10); + std::mt19937 random(12345); + for (int num_level_changes = 0; num_level_changes < 1000; + ++num_level_changes) { + const int level = absl::Uniform(random, 0, 100); + repo1.SetLevel(level); + repo2.SetLevel(level); + for (int i = 0; i < vars1.size(); ++i) { + ASSERT_EQ(vars1[i], vars2[i]); + repo1.SaveState(&vars1[i]); + } + for (int num_random_updates = 0; num_random_updates < 100; + ++num_random_updates) { + const int i = absl::Uniform(random, 0, vars1.size()); + const int value = absl::Uniform(random, 0, 1000); + vars1[i] = value; + repo2.SaveStateWithStamp(&vars2[i], &stamps2[i]); + vars2[i] = value; + } + } +} + +DEFINE_STRONG_INDEX_TYPE(TestType); + +TEST(RevVectorTest, BasicBehavior) { + RevVector v; + v.Grow(100); + + EXPECT_EQ(v[TestType(0)], 0); + EXPECT_EQ(v[TestType(10)], 0); + + v.SetLevel(3); + v.MutableRef(TestType(10)) = 4; + EXPECT_EQ(v[TestType(10)], 4); + + v.SetLevel(6); + v.MutableRef(TestType(10)) = 6; + EXPECT_EQ(v[TestType(10)], 6); + + v.SetLevel(7); + EXPECT_EQ(v[TestType(10)], 6); + + v.SetLevel(5); + EXPECT_EQ(v[TestType(10)], 4); + + v.SetLevel(0); + EXPECT_EQ(v[TestType(10)], 0); +} + +TEST(RevMapTest, LikeMapWhenNoBacktracking) { + RevMap> m; + EXPECT_FALSE(m.contains(4)); + m.Set(4, 5); + EXPECT_TRUE(m.contains(4)); + EXPECT_EQ(5, m.at(4)); + m.Set(4, 6); + EXPECT_TRUE(m.contains(4)); + EXPECT_EQ(6, m.at(4)); + m.EraseOrDie(4); + EXPECT_FALSE(m.contains(4)); +} + +TEST(RevMapDeathTest, EraseOrDie) { + RevMap> m; + EXPECT_FALSE(m.contains(4)); + m.Set(4, 5); + m.EraseOrDie(4); + EXPECT_DEATH(m.EraseOrDie(4), "key not present: '4'."); +} + +TEST(RevMapTest, InsertDeleteInsertAndBacktrack) { + RevMap> m; + EXPECT_FALSE(m.contains(4)); + + m.SetLevel(1); + EXPECT_FALSE(m.contains(4)); + m.Set(4, 5); + EXPECT_TRUE(m.contains(4)); + + m.SetLevel(4); + EXPECT_TRUE(m.contains(4)); + m.EraseOrDie(4); + EXPECT_FALSE(m.contains(4)); + + // The level do not need to be contiguous, but the implementation assumes + // they are mostly dense from an efficienty point of view. + m.SetLevel(10); + EXPECT_FALSE(m.contains(4)); + m.Set(4, 6); + EXPECT_TRUE(m.contains(4)); + EXPECT_EQ(6, m.at(4)); + m.Set(4, 7); + EXPECT_TRUE(m.contains(4)); + EXPECT_EQ(7, m.at(4)); + + // Backtrack + m.SetLevel(4); + EXPECT_FALSE(m.contains(4)); + + m.SetLevel(1); + EXPECT_TRUE(m.contains(4)); + EXPECT_EQ(5, m.at(4)); + + m.SetLevel(0); + EXPECT_FALSE(m.contains(4)); + + // Note that the higher level info is lost: + m.SetLevel(10); + EXPECT_FALSE(m.contains(4)); +} + +TEST(RevMapTest, ManySetsOperations) { + std::mt19937 random(12345); + const int kRange = 10000; + + RevMap> m; + absl::flat_hash_map reference; + + m.SetLevel(1); + for (int i = 0; i < 10000; ++i) { + const int key = absl::Uniform(random, 0, kRange); + const int value = absl::Uniform(random, 0, kRange); + m.Set(key, value); + reference[key] = value; + } + EXPECT_EQ(m.size(), reference.size()); + + // More insert at level 2. + m.SetLevel(2); + for (int i = 0; i < 10000; ++i) { + m.Set(absl::Uniform(random, 0, kRange), absl::Uniform(random, 0, kRange)); + } + EXPECT_GT(m.size(), reference.size()); + + // Backtrack to level 1, m and reference should be the same. + m.SetLevel(1); + EXPECT_EQ(m.size(), reference.size()); + for (const auto entry : reference) { + EXPECT_EQ(entry.second, m.at(entry.first)); + } + + m.SetLevel(0); + EXPECT_TRUE(m.empty()); +} + +TEST(RevGrowingMultiMapTest, BasicTest) { + RevGrowingMultiMap map; + map.Add(0, 1); + map.Add(0, 2); + map.SetLevel(1); + map.Add(0, 2); + map.Add(1, 3); + EXPECT_EQ(map.Values(0), std::vector({1, 2, 2})); + EXPECT_EQ(map.Values(1), std::vector({3})); + map.SetLevel(0); + EXPECT_EQ(map.Values(0), std::vector({1, 2})); + EXPECT_EQ(map.Values(1), std::vector({})); +} + +} // namespace +} // namespace operations_research diff --git a/ortools/util/running_stat_test.cc b/ortools/util/running_stat_test.cc new file mode 100644 index 00000000000..f00b60459cc --- /dev/null +++ b/ortools/util/running_stat_test.cc @@ -0,0 +1,121 @@ +// Copyright 2010-2025 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/util/running_stat.h" + +#include + +#include "gtest/gtest.h" + +namespace { + +TEST(MovingAverageTest, InitialValues) { + operations_research::RunningAverage running_average(10); + EXPECT_EQ(0.0, running_average.GlobalAverage()); + EXPECT_EQ(0.0, running_average.WindowAverage()); + EXPECT_FALSE(running_average.IsWindowFull()); +} + +TEST(MovingAverageTest, WindowOfSizeOne) { + operations_research::RunningAverage running_average(1); + for (int i = 0; i < 1000; ++i) { + running_average.Add(i); + EXPECT_EQ(i, running_average.WindowAverage()); + EXPECT_EQ(static_cast(i) / 2.0, running_average.GlobalAverage()); + EXPECT_TRUE(running_average.IsWindowFull()); + } +} + +TEST(MovingAverageTest, WindowOfSizeTwo) { + operations_research::RunningAverage running_average(2); + for (int i = 0; i < 1000; ++i) { + running_average.Add(i); + if (i == 0) { + EXPECT_FALSE(running_average.IsWindowFull()); + } else { + EXPECT_EQ(static_cast(2 * i - 1) / 2.0, + running_average.WindowAverage()); + EXPECT_TRUE(running_average.IsWindowFull()); + } + } +} + +TEST(MovingAverageTest, ClearWindow) { + operations_research::RunningAverage running_average(50); + for (int i = 0; i < 1000; ++i) { + running_average.Add(i); + } + EXPECT_TRUE(running_average.IsWindowFull()); + running_average.ClearWindow(); + EXPECT_FALSE(running_average.IsWindowFull()); + EXPECT_EQ(0.0, running_average.WindowAverage()); +} + +TEST(MovingAverageTest, Reset) { + operations_research::RunningAverage running_average(50); + for (int i = 0; i < 1000; ++i) { + running_average.Add(i); + } + EXPECT_TRUE(running_average.IsWindowFull()); + running_average.Reset(1); + EXPECT_EQ(0.0, running_average.GlobalAverage()); + EXPECT_EQ(0.0, running_average.WindowAverage()); + EXPECT_FALSE(running_average.IsWindowFull()); + for (int i = 0; i < 1000; ++i) { + running_average.Add(i); + EXPECT_EQ(i, running_average.WindowAverage()); + EXPECT_EQ(static_cast(i) / 2.0, running_average.GlobalAverage()); + EXPECT_TRUE(running_average.IsWindowFull()); + } +} + +TEST(RunningMaxTest, WindowOfSize1) { + operations_research::RunningMax<> running_max(1); + for (int i = 0; i < 1000; ++i) { + const double value = i % 15; + running_max.Add(value); + EXPECT_EQ(value, running_max.GetCurrentMax()); + } +} + +TEST(RunningMaxTest, SmallSequence) { + operations_research::RunningMax<> running_max(2); + running_max.Add(1); + EXPECT_EQ(1, running_max.GetCurrentMax()); + running_max.Add(10); + EXPECT_EQ(10, running_max.GetCurrentMax()); + running_max.Add(9); + EXPECT_EQ(10, running_max.GetCurrentMax()); + running_max.Add(8); + EXPECT_EQ(9, running_max.GetCurrentMax()); + running_max.Add(11); + EXPECT_EQ(11, running_max.GetCurrentMax()); +} + +TEST(RunningMaxTest, IncreasingSequence) { + operations_research::RunningMax<> running_max(10); + for (int i = 0; i < 1000; ++i) { + running_max.Add(i); + EXPECT_EQ(i, running_max.GetCurrentMax()); + } +} + +TEST(RunningMaxTest, DecreasingSequence) { + operations_research::RunningMax<> running_max(10); + for (int i = 1000; i >= 0; --i) { + running_max.Add(i); + EXPECT_EQ(std::min(1000, i + 9), running_max.GetCurrentMax()); + } +} + +} // namespace diff --git a/ortools/util/saturated_arithmetic_benchmark.cc b/ortools/util/saturated_arithmetic_benchmark.cc new file mode 100644 index 00000000000..527916daf86 --- /dev/null +++ b/ortools/util/saturated_arithmetic_benchmark.cc @@ -0,0 +1,127 @@ +// Copyright 2010-2025 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 +#include +#include +#include + +#include "absl/log/log.h" +#include "benchmark/benchmark.h" +#include "ortools/base/types.h" +#include "ortools/util/saturated_arithmetic.h" + +namespace operations_research { +namespace { + +std::vector GenerateInt64ValuesForTesting() { + std::vector v; + // Generate a bunch of special values... + for (int64_t base : + {int64_t{0}, kint64min, kint64max, static_cast(kint32max), + static_cast(kint32min)}) { + // ...scaled by various factor... + for (int64_t scaled : {base, base / 2, base / 3 * 2}) { + // ...with small offsets added. + for (int64_t offset : {-1000, -2, -1, 0, 1, 2, 1000}) { + // Avoid generating an overflow here. Do not want saturated as we + // want corner-case numbers, not saturated arithmetic. + v.push_back(TwosComplementAddition(scaled, offset)); + } + } + } + return v; +} + +std::vector NonOverflowCases() { + std::vector v; + for (int64_t i = -50; i < 50; ++i) { + v.push_back(i); + } + return v; +} + +// A renaming of GenerateInt64ValuesForTesting() to make the display of +// the micro-benchmarks look nicer. +inline std::vector CornerCases() { + return GenerateInt64ValuesForTesting(); +} + +// In the benchmark, we can use either the "special" testing values, or +// some "safe" values to measure peak performance. +#define BENCH_INT64_OPERATOR(Operator, GenerationFunction) \ + void BM_##Operator##_##GenerationFunction(benchmark::State& state) { \ + std::vector values1 = GenerationFunction(); \ + std::vector values2 = values1; \ + std::mt19937 random(12345); \ + std::shuffle(values2.begin(), values2.end(), random); \ + int64_t ret = 0; \ + for (auto _ : state) { \ + for (const int64_t a : values1) { \ + for (const int64_t b : values2) { \ + ret |= Operator(a, b); \ + } \ + } \ + } \ + state.SetItemsProcessed(state.max_iterations * values1.size() * \ + values2.size()); \ + VLOG(1) << "Result: " << ret; \ + } \ + BENCHMARK(BM_##Operator##_##GenerationFunction) + +// To be used only for speed benchmarking, on non-overflowing test cases. +inline int64_t SimpleMultiplication(int64_t x, int64_t y) { return x * y; } + +BENCH_INT64_OPERATOR(CapAdd, CornerCases); +BENCH_INT64_OPERATOR(CapSub, CornerCases); +BENCH_INT64_OPERATOR(CapProd, CornerCases); +#if defined(__GNUC__) && defined(__x86_64__) +BENCH_INT64_OPERATOR(CapAddAsm, CornerCases); +BENCH_INT64_OPERATOR(CapSubAsm, CornerCases); +BENCH_INT64_OPERATOR(CapProdAsm, CornerCases); +#endif +#if defined(__clang__) +BENCH_INT64_OPERATOR(CapAddBuiltIn, CornerCases); +BENCH_INT64_OPERATOR(CapSubBuiltIn, CornerCases); +BENCH_INT64_OPERATOR(CapProdBuiltIn, CornerCases); +#endif + +BENCH_INT64_OPERATOR(CapAddGeneric, CornerCases); +BENCH_INT64_OPERATOR(CapSubGeneric, CornerCases); +BENCH_INT64_OPERATOR(CapProdGeneric, CornerCases); + +BENCH_INT64_OPERATOR(CapAdd, NonOverflowCases); +BENCH_INT64_OPERATOR(CapSub, NonOverflowCases); +BENCH_INT64_OPERATOR(CapProd, NonOverflowCases); +#if defined(__GNUC__) && defined(__x86_64__) +BENCH_INT64_OPERATOR(CapAddAsm, NonOverflowCases); +BENCH_INT64_OPERATOR(CapSubAsm, NonOverflowCases); +BENCH_INT64_OPERATOR(CapProdAsm, NonOverflowCases); +#endif +#if defined(__clang__) +BENCH_INT64_OPERATOR(CapAddBuiltIn, NonOverflowCases); +BENCH_INT64_OPERATOR(CapSubBuiltIn, NonOverflowCases); +BENCH_INT64_OPERATOR(CapProdBuiltIn, NonOverflowCases); +#endif + +BENCH_INT64_OPERATOR(CapAddGeneric, NonOverflowCases); +BENCH_INT64_OPERATOR(CapSubGeneric, NonOverflowCases); +BENCH_INT64_OPERATOR(CapProdGeneric, NonOverflowCases); +BENCH_INT64_OPERATOR(TwosComplementAddition, NonOverflowCases); +BENCH_INT64_OPERATOR(TwosComplementSubtraction, NonOverflowCases); +BENCH_INT64_OPERATOR(SimpleMultiplication, NonOverflowCases); + +// TODO(user): do the same for CapOpp. + +} // namespace +} // namespace operations_research diff --git a/ortools/util/saturated_arithmetic_test.cc b/ortools/util/saturated_arithmetic_test.cc new file mode 100644 index 00000000000..e0b3db0acb8 --- /dev/null +++ b/ortools/util/saturated_arithmetic_test.cc @@ -0,0 +1,229 @@ +// Copyright 2010-2025 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/util/saturated_arithmetic.h" + +#include +#include +#include +#include + +#include "absl/numeric/int128.h" +#include "absl/random/random.h" +#include "gtest/gtest.h" +#include "ortools/base/types.h" +#include "ortools/util/strong_integers.h" + +namespace operations_research { +namespace { + +DEFINE_STRONG_INT64_TYPE(TestType); + +void TestSafeAdd64(int64_t a, int64_t b, bool error_expected) { + TestType result_b(b); + TestType result_a(a); + EXPECT_EQ(SafeAddInto(TestType(a), &result_b), !error_expected) + << a << " + " << b; + EXPECT_EQ(SafeAddInto(TestType(b), &result_a), !error_expected) + << a << " + " << b; + if (!error_expected) { + EXPECT_EQ(result_a.value(), a + b) << a << " + " << b; + EXPECT_EQ(result_b.value(), a + b) << a << " + " << b; + } +} + +TEST(SafeAddTest, BasicCases) { + TestSafeAdd64(5, 42, /*error_expected*/ false); + TestSafeAdd64(kint64min / 2, kint64min / 2, /*error_expected=*/false); + TestSafeAdd64(kint64max / 2, kint64max / 2, /*error_expected=*/false); + TestSafeAdd64(kint64min / 2, kint64min / 2 - 2, + /*error_expected=*/true); + TestSafeAdd64(kint64max / 2, kint64max / 2 + 2, + /*error_expected=*/true); + TestSafeAdd64(kint64min / 2, kint64min / 2 - 1, + /*error_expected=*/true); + TestSafeAdd64(kint64max / 2, kint64max / 2 + 1, + /*error_expected=*/false); + TestSafeAdd64(kint64min, -1, /*error_expected=*/true); + TestSafeAdd64(kint64max, +1, /*error_expected=*/true); + TestSafeAdd64(kint64min, +1, /*error_expected=*/false); + TestSafeAdd64(kint64max, -1, /*error_expected=*/false); +} + +std::vector GenerateInt64ValuesForTesting() { + std::vector v; + // Generate a bunch of special values... + for (int64_t base : + {int64_t{0}, kint64min, kint64max, static_cast(kint32max), + static_cast(kint32min)}) { + // ...scaled by various factor... + for (int64_t scaled : {base, base / 2, base / 3 * 2}) { + // ...with small offsets added. + for (int64_t offset : {-1000, -2, -1, 0, 1, 2, 1000}) { + // Avoid generating an overflow here. Do not want saturated as we + // want corner-case numbers, not saturated arithmetic. + v.push_back(TwosComplementAddition(scaled, offset)); + } + } + } + return v; +} + +class SaturatedArithmeticTest : public ::testing::Test { + protected: + // Reference (safe, but slow) versions of the functions whose behavior + // we are testing. We'd like to use int128, but base/int128.h only provides + // uint128, so we use bit tricks, and define our own conversions + // uint128 <-> int64_t (see the "private" section). + int64_t ReferenceCapAdd(int64_t a, int64_t b) { + return Int128ToCapped64(Int64To128(a) + Int64To128(b)); + } + int64_t ReferenceCapSub(int64_t a, int64_t b) { + return Int128ToCapped64(Int64To128(a) - Int64To128(b)); + } + int64_t ReferenceCapOpp(int64_t v) { + return Int128ToCapped64(-Int64To128(v)); + } + int64_t ReferenceCapProd(int64_t a, int64_t b) { + return Int128ToCapped64(Int64To128(a) * Int64To128(b)); + } + + void SetUp() override { + int64_values_ = GenerateInt64ValuesForTesting(); + // Now, add a few random values. + const int kNumRandomValues = 1000; + std::mt19937 random(12345); + for (int i = 0; i < kNumRandomValues; ++i) { + int64_values_.push_back( + static_cast(absl::Uniform(random))); + } + } + + std::vector int64_values_; + + private: + bool IsNegative(absl::uint128 x) { + return static_cast(absl::Uint128High64(x)) < 0; + } + + absl::uint128 Int64To128(int64_t x) { + return absl::MakeUint128(static_cast(x < 0 ? -1 : 0), + static_cast(x)); + } + + int64_t Int128ToCapped64(absl::uint128 x) { + const int64_t low = static_cast(absl::Uint128Low64(x)); + if (IsNegative(x)) { + if (absl::Uint128High64(x) != static_cast(-1)) return kint64min; + if (low >= 0) return kint64min; + return low; + } + if (absl::Uint128High64(x) != 0) return kint64max; + if (low < 0) return kint64max; + return low; + } + + uint64_t Uint128ToCappedU64(absl::uint128 x) { + return x > absl::uint128(kuint64max) ? kuint64max : absl::Uint128Low64(x); + } +}; + +// Don't use EXPECT to avoid gunit issues -- we test ~1M pairs of +// values, and generating that much output can freeze your test. +#define TEST_BINARY_OPERATOR(name, symbol, reference) \ + TEST_F(SaturatedArithmeticTest, name) { \ + for (int64_t a : int64_values_) { \ + for (int64_t b : int64_values_) { \ + ASSERT_EQ(reference(a, b), name(a, b)) \ + << a << " " << symbol << " " << b; \ + } \ + } \ + } + +TEST_BINARY_OPERATOR(CapAdd, "+", ReferenceCapAdd); +TEST_BINARY_OPERATOR(CapAddGeneric, "+", ReferenceCapAdd); +TEST_BINARY_OPERATOR(CapSub, "-", ReferenceCapSub); +TEST_BINARY_OPERATOR(CapSubGeneric, "-", ReferenceCapSub); +TEST_BINARY_OPERATOR(CapProd, "*", ReferenceCapProd); +TEST_BINARY_OPERATOR(CapProdGeneric, "*", ReferenceCapProd); + +#if defined(__GNUC__) && defined(__x86_64__) +TEST_BINARY_OPERATOR(CapAddAsm, "+", ReferenceCapAdd); +TEST_BINARY_OPERATOR(CapSubAsm, "-", ReferenceCapSub); +TEST_BINARY_OPERATOR(CapProdAsm, "*", ReferenceCapProd); +#endif + +TEST_F(SaturatedArithmeticTest, CapOpp) { + for (int64_t a : int64_values_) { + ASSERT_EQ(ReferenceCapOpp(a), CapOpp(a)) << a; + } +} + +TEST_F(SaturatedArithmeticTest, CapAbs) { + ASSERT_EQ(CapAbs(kint64min), kint64max); + ASSERT_EQ(CapAbs(kint64max), kint64max); + ASSERT_EQ(CapAbs(-100), 100); + ASSERT_EQ(CapAbs(0), 0); + ASSERT_EQ(CapAbs(200), 200); +} + +TEST_F(SaturatedArithmeticTest, CapAddTo) { + for (int64_t a : int64_values_) { + for (int64_t b : int64_values_) { + int64_t result = a; + CapAddTo(b, &result); + ASSERT_EQ(result, CapAdd(a, b)) << a << " + " << b; + } + } +} + +TEST_F(SaturatedArithmeticTest, AddIntoOverflow) { + for (int64_t a : int64_values_) { + for (int64_t b : int64_values_) { + int64_t result = b; + if (AddIntoOverflow(a, &result)) { + EXPECT_TRUE(AddOverflows(a, b)); + } else { + EXPECT_FALSE(AddOverflows(a, b)); + EXPECT_EQ(result, a + b); + } + } + } +} + +TEST_F(SaturatedArithmeticTest, CapSubFrom) { + for (int64_t a : int64_values_) { + for (int64_t b : int64_values_) { + int64_t result = a; + CapSubFrom(/*amount=*/b, /*target=*/&result); + ASSERT_EQ(result, CapSub(a, b)) << a << " + " << b; + } + } +} + +TEST(CapOrFloatAdd, VariousTypes) { + constexpr double kInf = std::numeric_limits::infinity(); + EXPECT_EQ(CapOrFloatAdd(0.5, 0.25), 0.75); + EXPECT_EQ(CapOrFloatAdd(kInf, 0.25), kInf); + EXPECT_EQ(CapOrFloatAdd(int64_t{1}, kint64max), kint64max); + EXPECT_EQ(CapOrFloatAdd(4, kint32max - 2), kint32max); + + // A corner case: float isn't accurate enough to represent 1+1e-10, which we + // use to verify that the return type is the same as the argument type. + EXPECT_EQ(static_cast(CapOrFloatAdd(float{1}, float{1e-10})), 1.0); +} + +// TODO(user): do the same for CapOpp. + +} // namespace +} // namespace operations_research diff --git a/ortools/util/solve_interrupter_test.cc b/ortools/util/solve_interrupter_test.cc new file mode 100644 index 00000000000..c1599291b9f --- /dev/null +++ b/ortools/util/solve_interrupter_test.cc @@ -0,0 +1,204 @@ +// Copyright 2010-2025 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/util/solve_interrupter.h" + +#include +#include +#include + +#include "absl/strings/str_cat.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" + +namespace operations_research { +namespace { + +using ::testing::ElementsAre; +using ::testing::IsEmpty; + +TEST(SolveInterrupterTest, NotCopyableOrAssignable) { + EXPECT_FALSE(std::is_copy_constructible::value); + EXPECT_FALSE(std::is_copy_assignable::value); +} + +TEST(SolveInterrupterTest, Untriggered) { + // Note that we use a `const SolveInterrupter` here to test that the + // "readonly" part of the interface is indeed `const`. + const SolveInterrupter interrupter; + EXPECT_FALSE(interrupter.IsInterrupted()); + + std::vector calls; + const SolveInterrupter::CallbackId id = + interrupter.AddInterruptionCallback([&]() { + // Test that we can call IsInterrupted() from the callback on top of + // registering all calls. + calls.push_back(interrupter.IsInterrupted()); + }); + + // Test that AddInterruptionCallback() has not called the callback. + EXPECT_THAT(calls, IsEmpty()); + + interrupter.RemoveInterruptionCallback(id); +} + +TEST(SolveInterrupterTest, Triggered) { + SolveInterrupter interrupter; + interrupter.Interrupt(); + + EXPECT_TRUE(interrupter.IsInterrupted()); + + std::vector calls; + interrupter.AddInterruptionCallback([&]() { + // Test that we can call IsInterrupted() from the callback on top of + // registering all calls. + calls.push_back(interrupter.IsInterrupted()); + }); + + // Test that AddInterruptionCallback() has called the callback once and that + // IsInterrupted() was true at that time. + EXPECT_THAT(calls, ElementsAre(true)); +} + +TEST(SolveInterrupterTest, Triggering) { + SolveInterrupter interrupter; + + std::vector calls; + interrupter.AddInterruptionCallback([&]() { + calls.push_back(absl::StrCat("callback1: ", interrupter.IsInterrupted())); + }); + interrupter.AddInterruptionCallback([&]() { + calls.push_back(absl::StrCat("callback2: ", interrupter.IsInterrupted())); + }); + + EXPECT_THAT(calls, IsEmpty()); + + interrupter.Interrupt(); + + EXPECT_TRUE(interrupter.IsInterrupted()); + EXPECT_THAT(calls, ElementsAre("callback1: 1", "callback2: 1")); + + // Testing that calling Interrupt() a second time does not call the callbacks + // again. + calls.clear(); + interrupter.Interrupt(); + + EXPECT_TRUE(interrupter.IsInterrupted()); + EXPECT_THAT(calls, IsEmpty()); +} + +TEST(SolveInterrupterTest, Unregistering) { + SolveInterrupter interrupter; + + std::vector callback1_calls; + const SolveInterrupter::CallbackId callback1 = + interrupter.AddInterruptionCallback([&]() { + // Test that we can call IsInterrupted() from the callback on top of + // registering all calls. + callback1_calls.push_back(interrupter.IsInterrupted()); + }); + std::vector callback2_calls; + interrupter.AddInterruptionCallback([&]() { + // Test that we can call IsInterrupted() from the callback on top of + // registering all calls. + callback2_calls.push_back(interrupter.IsInterrupted()); + }); + + EXPECT_THAT(callback1_calls, IsEmpty()); + EXPECT_THAT(callback2_calls, IsEmpty()); + + interrupter.RemoveInterruptionCallback(callback1); + interrupter.Interrupt(); + + EXPECT_TRUE(interrupter.IsInterrupted()); + EXPECT_THAT(callback1_calls, IsEmpty()); + EXPECT_THAT(callback2_calls, ElementsAre(true)); +} + +TEST(SolveInterrupterDeathTest, UnregisteringTwice) { + SolveInterrupter interrupter; + const SolveInterrupter::CallbackId callback = + interrupter.AddInterruptionCallback([]() {}); + + interrupter.RemoveInterruptionCallback(callback); + EXPECT_DEATH(interrupter.RemoveInterruptionCallback(callback), + "unregistered callback id"); +} + +TEST(ScopedSolveInterrupterCallbackTest, CallbackNotCalled) { + // Note that we use a `const` variable here to test that we can use + // ScopedSolveInterrupterCallbackTest with a pointer to a `const + // SolveInterrupter`. + const SolveInterrupter interrupter; + int calls = 0; + { + const ScopedSolveInterrupterCallback scoped_callback(&interrupter, + [&]() { ++calls; }); + } + EXPECT_EQ(calls, 0); +} + +TEST(ScopedSolveInterrupterCallbackTest, CallbackCalled) { + SolveInterrupter interrupter; + int calls = 0; + { + const ScopedSolveInterrupterCallback scoped_callback(&interrupter, + [&]() { ++calls; }); + interrupter.Interrupt(); + } + EXPECT_EQ(calls, 1); +} + +TEST(ScopedSolveInterrupterCallbackTest, CallbackRemoved) { + SolveInterrupter interrupter; + int calls = 0; + { + const ScopedSolveInterrupterCallback scoped_callback(&interrupter, + [&]() { ++calls; }); + } + interrupter.Interrupt(); + EXPECT_EQ(calls, 0); +} + +TEST(ScopedSolveInterrupterCallbackTest, CallbackRemovedBeforeDestruction) { + SolveInterrupter interrupter; + int calls = 0; + { + ScopedSolveInterrupterCallback scoped_callback(&interrupter, + [&]() { ++calls; }); + scoped_callback.RemoveCallbackIfNecessary(); + interrupter.Interrupt(); + } + EXPECT_EQ(calls, 0); +} + +TEST(ScopedSolveInterrupterCallbackTest, NullInterrupter) { + int calls = 0; + { + const ScopedSolveInterrupterCallback scoped_callback(nullptr, + [&]() { ++calls; }); + } + EXPECT_EQ(calls, 0); +} + +TEST(ScopedSolveInterrupterCallbackTest, RemoveCallbackNullInterrupter) { + int calls = 0; + { + ScopedSolveInterrupterCallback scoped_callback(nullptr, [&]() { ++calls; }); + scoped_callback.RemoveCallbackIfNecessary(); + } + EXPECT_EQ(calls, 0); +} + +} // namespace +} // namespace operations_research diff --git a/ortools/util/status_macros_test.cc b/ortools/util/status_macros_test.cc new file mode 100644 index 00000000000..9a2dd037bfd --- /dev/null +++ b/ortools/util/status_macros_test.cc @@ -0,0 +1,61 @@ +// Copyright 2010-2025 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/util/status_macros.h" + +#include + +#include "absl/status/status.h" +#include "absl/status/status_macros.h" +#include "absl/status/statusor.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" + +namespace operations_research { +namespace { + +using ::testing::HasSubstr; +using ::testing::Pair; +using ::testing::status::IsOkAndHolds; +using ::testing::status::StatusIs; + +TEST(OrAssignOrReturn3Test, CostlyExpression) { + // We use a function in the test to validate that OR_ASSIGN_OR_RETURN3 does + // not evaluate the third parameters unless the status is failing. + int costly_function_calls = 0; + const auto costly_function = [&]() { + ++costly_function_calls; + return "costly result"; + }; + + const auto macro_invoker = + [&](const absl::StatusOr> input) + -> absl::StatusOr> { + // Here we also test that we can use destructured assignment, even if we + // want a pair in the end. + OR_ASSIGN_OR_RETURN3((const auto [x, y]), input, _ << costly_function()); + return std::make_pair(x, y); + }; + + EXPECT_THAT(macro_invoker(std::make_pair(3, -1.5)), + IsOkAndHolds(Pair(3, -1.5))); + EXPECT_EQ(costly_function_calls, 0); + + EXPECT_THAT(macro_invoker(absl::InternalError("oops")), + StatusIs(absl::StatusCode::kInternal, + AllOf(HasSubstr("oops"), HasSubstr("costly result")))); + EXPECT_EQ(costly_function_calls, 1); +} + +} // namespace +} // namespace operations_research diff --git a/ortools/util/status_streaming_test.cc b/ortools/util/status_streaming_test.cc new file mode 100644 index 00000000000..16da423d9e7 --- /dev/null +++ b/ortools/util/status_streaming_test.cc @@ -0,0 +1,90 @@ +// Copyright 2010-2025 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/util/status_streaming.h" + +#include + +#include "absl/log/scoped_mock_log.h" +#include "absl/status/status.h" +#include "absl/types/source_location.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/base/log_severity.h" +#include "ortools/util/status.pb.h" + +namespace operations_research { +namespace { + +using ::testing::AnyNumber; +using ::testing::ElementsAre; +using ::testing::EqualsProto; +using ::testing::HasSubstr; +using ::testing::Property; +using ::testing::status::StatusIs; + +TEST(StreamStatusTest, Ok) { + EXPECT_THAT(StreamStatus(absl::OkStatus()), EqualsProto(StatusProto{})); +} + +TEST(StreamStatusTest, Failure) { + StatusProto expected; + expected.set_code(static_cast(absl::StatusCode::kUnimplemented)); + expected.set_message("some message"); + EXPECT_THAT(StreamStatus(absl::UnimplementedError("some message")), + EqualsProto(expected)); +} + +TEST(StreamStatusTest, FailureWithPayload) { + absl::Status status = absl::UnimplementedError("some message"); + status.SetPayload("some_payload_url", {}); + + StatusProto expected; + expected.set_code(static_cast(absl::StatusCode::kUnimplemented)); + expected.set_message("some message"); + + absl::ScopedMockLog log; + EXPECT_CALL(log, Log).Times(AnyNumber()); + EXPECT_CALL(log, Log(absl::LogSeverity::kWarning, ::testing::_, + HasSubstr("some_payload_url"))) + .Times(1); + + log.StartCapturingLogs(); + EXPECT_THAT(StreamStatus(status), EqualsProto(expected)); +} + +TEST(UnstreamStatusTest, Ok) { EXPECT_OK(UnstreamStatus(StatusProto{})); } + +TEST(UnstreamStatusTest, FailureWithDefaultLocation) { + StatusProto proto; + proto.set_code(static_cast(absl::StatusCode::kUnimplemented)); + proto.set_message("some message"); + + const absl::Status unstreamed_status = UnstreamStatus(proto); + EXPECT_THAT(unstreamed_status, + StatusIs(absl::StatusCode::kUnimplemented, "some message")); +} + +TEST(UnstreamStatusTest, FailureWithCustomLocation) { + StatusProto proto; + proto.set_code(static_cast(absl::StatusCode::kUnimplemented)); + proto.set_message("some message"); + + const absl::SourceLocation location = absl::SourceLocation::current(); + const absl::Status unstreamed_status = UnstreamStatus(proto, location); + EXPECT_THAT(unstreamed_status, + StatusIs(absl::StatusCode::kUnimplemented, "some message")); +} + +} // namespace +} // namespace operations_research diff --git a/ortools/util/time_limit_benchmark.cc b/ortools/util/time_limit_benchmark.cc new file mode 100644 index 00000000000..b75c50d56b0 --- /dev/null +++ b/ortools/util/time_limit_benchmark.cc @@ -0,0 +1,104 @@ +// Copyright 2010-2025 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 + +#include "absl/log/check.h" +#include "absl/strings/str_cat.h" +#include "benchmark/benchmark.h" +#include "ortools/util/time_limit.h" + +namespace operations_research { +namespace { + +const double kInfinity = std::numeric_limits::infinity(); + +template +static void BM_TimeLimitReached(benchmark::State& state) { + for (auto _ : state) { + TimeLimit time_limit(1.0); + double sum = 0.0; + for (int i = 0; i < 10000; ++i) { + if (with_time_limit) { + CHECK(!time_limit.LimitReached()); + } + // Something that takes some time. + for (int j = 0; j < 50; ++j) { + sum += i * j; + } + } + CHECK(!time_limit.LimitReached()); + CHECK_GE(sum, 0.0); + } + state.SetLabel(absl::StrCat(with_time_limit ? "With" : "Without", + " TimeLimitReached()")); +} + +// This just gives an idea of how slow it is to call LimitReached(). +// blaze run -c opt --linkopt=-static --dynamic_mode=off +// ortools/util:time_limit_test -- --benchmarks=all +// +// Here are perflab benchmark results: +// Run on lpl35 (24 X 2800 MHz CPUs); 2014/05/21-04:12:59 +// CPU: Intel Westmere with HyperThreading (12 cores) dL1:32KB dL2:256KB +// Benchmark Time(ns) CPU(ns) Iterations +// ----------------------------------------------------------- +// BM_TimeLimitReached 716538 713331 981 Without +// BM_TimeLimitReached 742439 739093 947 With TimeLimit +BENCHMARK_TEMPLATE(BM_TimeLimitReached, /*with_time_limit=*/false); +BENCHMARK_TEMPLATE(BM_TimeLimitReached, /*with_time_limit=*/true); + +// This is to verify that the messages do not have any effect on performance +// of the optimized code (that the compiler in opt mode removes the messages). +// blaze run -c opt --linkopt=static --dynamic_mode=off +// //ortools/util:time_limit_test -- --benchmarks=all +// +// Here are perflab benchmark results: +// Run on lpab10 (32 X 2600 MHz CPUs); 2015/07/27-02:59:39 +// CPU: Intel Sandybridge with HyperThreading (16 cores) dL1:32KB dL2:256KB +// Benchmark Time(ns) CPU(ns) Iterations +// ------------------------------------------------------------------- +// BM_AdvanceDeterministicTime 514067 513431 1000 Without +// BM_AdvanceDeterministicTime 440777 440644 1596 With +// +// Note that the difference between the two benchmarks is most likely because of +// memory cache behavior. The same difference (but with opposite values) is +// observed when the order of the two benchmarks is reversed. +template +static void BM_AdvanceDeterministicTime(benchmark::State& state) { + for (auto _ : state) { + TimeLimit time_limit(kInfinity, 1000.0); + double sum = 0.0; + while (!time_limit.LimitReached()) { + // Something that takes time. + for (int i = 0; i < 500; ++i) { + sum += i * i; + } + if (with_messages) { + time_limit.AdvanceDeterministicTime(1.0, "AdvanceTime"); + } else { + time_limit.AdvanceDeterministicTime(1.0); + } + } + CHECK_GE(sum, 0.0); + } + state.SetLabel(absl::StrCat("AdvanceDeterministicTime", + with_messages ? "With" : "Without", + "NamedCounters")); +} + +BENCHMARK_TEMPLATE(BM_AdvanceDeterministicTime, /*with_messages=*/false); +BENCHMARK_TEMPLATE(BM_AdvanceDeterministicTime, /*with_messages=*/true); + +} // namespace +} // namespace operations_research diff --git a/ortools/util/time_limit_test.cc b/ortools/util/time_limit_test.cc new file mode 100644 index 00000000000..db032f25f71 --- /dev/null +++ b/ortools/util/time_limit_test.cc @@ -0,0 +1,425 @@ +// Copyright 2010-2025 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/util/time_limit.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "absl/base/macros.h" +#include "absl/flags/flag.h" +#include "absl/log/log_streamer.h" +#include "absl/random/random.h" +#include "absl/strings/str_format.h" +#include "absl/strings/str_split.h" +#include "absl/time/clock.h" +#include "absl/time/time.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/base/log_severity.h" +#include "ortools/base/threadpool.h" +#include "ortools/util/testing_utils.h" + +namespace operations_research { +namespace { + +using ::testing::MatchesRegex; +using ::testing::UnorderedElementsAre; + +const double kInfinity = std::numeric_limits::infinity(); +constexpr int kTimeMultiplier = + (DEBUG_MODE ? 5 : 1) * (kAnyXsanEnabled ? 2 : 1); + +TEST(TimeLimitTest, NoLimit) { + TimeLimit time_limit(kInfinity); + for (int i = 0; i < 100; ++i) { + EXPECT_EQ(time_limit.CurrentState(), TimeLimit::State::kRunning); + } + EXPECT_EQ(kInfinity, time_limit.GetTimeLeft()); +} + +TEST(TimeLimitTest, NoLimitSecondaryBoolean) { + TimeLimit time_limit(kInfinity); + std::atomic stop = false; + time_limit.RegisterSecondaryExternalBooleanAsLimit(&stop); + EXPECT_EQ(time_limit.CurrentState(), TimeLimit::State::kRunning); + stop.store(true); + EXPECT_EQ(time_limit.CurrentState(), + TimeLimit::State::kSecondaryExternalBoolean); +} + +TEST(TimeLimitTest, ZeroLimit) { + TimeLimit time_limit(0.0, 0.0); + EXPECT_EQ(time_limit.CurrentState(), TimeLimit::State::kDeterministicTime); + EXPECT_EQ(time_limit.CurrentState(), TimeLimit::State::kDeterministicTime); + EXPECT_EQ(0.0, time_limit.GetTimeLeft()); + EXPECT_EQ(0.0, time_limit.GetDeterministicTimeLeft()); +} + +TEST(TimeLimitTest, NegativeLimit) { + TimeLimit time_limit(-100); + EXPECT_EQ(time_limit.CurrentState(), TimeLimit::State::kTime); + EXPECT_EQ(time_limit.CurrentState(), TimeLimit::State::kTime); + EXPECT_EQ(0.0, time_limit.GetTimeLeft()); +} + +TEST(TimeLimitTest, NegativeInfiniteLimit) { + TimeLimit time_limit(-kInfinity); + EXPECT_EQ(time_limit.CurrentState(), TimeLimit::State::kTime); + EXPECT_EQ(time_limit.CurrentState(), TimeLimit::State::kTime); + EXPECT_EQ(0.0, time_limit.GetTimeLeft()); +} + +TEST(TimeLimitTest, SmallestLimit) { + TimeLimit time_limit(1e-6); + // We are defensive and return that the limit is reached if less than 0.1ms is + // left. + EXPECT_EQ(time_limit.CurrentState(), TimeLimit::State::kTime); + EXPECT_EQ(time_limit.CurrentState(), TimeLimit::State::kTime); + // Even though the limit is reached, some time is left because we are + // defensive in the limit enforcement. + EXPECT_GT(1e-7, time_limit.GetTimeLeft()); +} + +TEST(TimeLimitTest, SmallLimit) { + // FLAKINESS: This test is hard to de-flake, so we simply try a few times. + constexpr int kLastAttempt = 5; + for (int attempt = 0;; ++attempt) { + TimeLimit time_limit(1e-3 * kTimeMultiplier); + if (time_limit.LimitReached()) { + ASSERT_LT(attempt, kLastAttempt); + continue; + } + double sum = 0.0; + for (int i = 0; i < 100; ++i) { + if (time_limit.LimitReached()) break; + sum += i * i; + } + if (time_limit.LimitReached() || time_limit.GetTimeLeft() < 0) { + ASSERT_LT(attempt, kLastAttempt); + continue; + } + // The rest of the test below isn't expected to be flaky. + while (true) { + if (time_limit.LimitReached()) break; + sum += 123456789.0; + } + EXPECT_EQ(time_limit.CurrentState(), TimeLimit::State::kTime); + EXPECT_EQ(time_limit.CurrentState(), TimeLimit::State::kTime); + EXPECT_GE(sum, 0.0); + break; + } +} + +TEST(TimeLimitTest, TimeSeemsRight) { + for (bool usertime_flag : {false, true}) { + SCOPED_TRACE(absl::StrFormat("usertime_flag: %v", usertime_flag)); + // UserTimer is not implemented yet + if (usertime_flag) continue; + + const absl::Duration kSleepInterval = + absl::Milliseconds(10) * kTimeMultiplier; + constexpr int kSafetyBufferIterations = 5; + constexpr int kNumIterations = 10; + const absl::Duration kTimeLimit = kSleepInterval * kNumIterations; + // FLAKINESS: This test is hard to de-flake, so we simply re-try many times. + constexpr int kLastAttempt = 40; + for (int attempt = 0;; ++attempt) { + TimeLimit time_limit(absl::ToDoubleSeconds(kTimeLimit)); + EXPECT_EQ(time_limit.CurrentState(), TimeLimit::State::kRunning); + int limit_reached_at = -1; + for (int i = 0; i < kNumIterations - kSafetyBufferIterations; ++i) { + absl::SleepFor(kSleepInterval); + if (time_limit.LimitReached()) { + limit_reached_at = i; + break; + } + } + if (limit_reached_at >= 0) { + ASSERT_LT(attempt, kLastAttempt) + << "Limit reached too early -- even after " << kLastAttempt + 1 + << " attempt. i=" << limit_reached_at; + continue; // Start a new attempt. + } + + for (int i = 0; i < kSafetyBufferIterations; ++i) { + absl::SleepFor(kSleepInterval); + } + if (absl::GetFlag(FLAGS_time_limit_use_usertime)) { + // Sleep doesn't count towards the usertime! + if (time_limit.LimitReached() || + absl::Seconds(time_limit.GetTimeLeft()) < + kTimeLimit - absl::Milliseconds(50) * kTimeMultiplier) { + ASSERT_LT(attempt, kLastAttempt); + continue; + } + } else { + // This part isn't flaky at all. + ASSERT_EQ(time_limit.CurrentState(), TimeLimit::State::kTime); + } + break; // We've succeeded! No need to re-attempt. + } + } +} + +TEST(TimeLimitTest, NegativeDeterministicLimit) { + TimeLimit time_limit(kInfinity, -100); + EXPECT_EQ(time_limit.CurrentState(), TimeLimit::State::kDeterministicTime); + EXPECT_EQ(time_limit.CurrentState(), TimeLimit::State::kDeterministicTime); + EXPECT_EQ(kInfinity, time_limit.GetTimeLeft()); + EXPECT_EQ(0.0, time_limit.GetDeterministicTimeLeft()); + + TimeLimit both_time_limit(-100, -100); + EXPECT_EQ(both_time_limit.CurrentState(), + TimeLimit::State::kDeterministicTime); + EXPECT_EQ(both_time_limit.CurrentState(), + TimeLimit::State::kDeterministicTime); + EXPECT_EQ(0.0, both_time_limit.GetTimeLeft()); + EXPECT_EQ(0.0, both_time_limit.GetDeterministicTimeLeft()); +} + +TEST(TimeLimitTest, DeterministicTimeOnly) { + const double kDeterministicLimit = 10.0; + TimeLimit time_limit(kInfinity, kDeterministicLimit); + // Consume all the deterministic time. + for (int i = 0; i < kDeterministicLimit; ++i) { + EXPECT_EQ(time_limit.CurrentState(), TimeLimit::State::kRunning); + EXPECT_EQ(kInfinity, time_limit.GetTimeLeft()); + EXPECT_EQ(kDeterministicLimit - i, time_limit.GetDeterministicTimeLeft()); + EXPECT_EQ(i, time_limit.GetElapsedDeterministicTime()); + time_limit.AdvanceDeterministicTime(1.0); + } + // The limit should be reached. + EXPECT_EQ(time_limit.CurrentState(), TimeLimit::State::kDeterministicTime); + EXPECT_EQ(kInfinity, time_limit.GetTimeLeft()); + EXPECT_EQ(0.0, time_limit.GetDeterministicTimeLeft()); + + // Consuming one more deterministic time. + time_limit.AdvanceDeterministicTime(1.0); + EXPECT_EQ(time_limit.CurrentState(), TimeLimit::State::kDeterministicTime); + EXPECT_EQ(kInfinity, time_limit.GetTimeLeft()); + EXPECT_EQ(0.0, time_limit.GetDeterministicTimeLeft()); +} + +TEST(TimeLimitTest, AdvanceDeterministicTimeWithCounterName) { + static const char kCounterFoo[] = "DoingFoo"; + static const char kCounterBar[] = "DoingBar"; + static const double kDeterministicStepFoo = 1.0; + static const double kDeterministicStepBar = 2.0; + static const int kNumSteps = 10; + static const double kExpectedElapseDeterministicTime = 30.0; + TimeLimit time_limit(kInfinity, kInfinity); + for (int i = 0; i < kNumSteps; ++i) { + time_limit.AdvanceDeterministicTime(kDeterministicStepFoo, kCounterFoo); + time_limit.AdvanceDeterministicTime(kDeterministicStepBar, kCounterBar); + } + EXPECT_NEAR(kExpectedElapseDeterministicTime, + time_limit.GetElapsedDeterministicTime(), 1e-10); + const std::vector debug_string_lines = + absl::StrSplit(time_limit.DebugString(), '\n'); +#ifdef NDEBUG + EXPECT_THAT( + debug_string_lines, + UnorderedElementsAre("Time left: inf", "Deterministic time left: inf", + MatchesRegex("Elapsed time: [-0-9.e]*"), + "Elapsed deterministic time: 30")); +#else + EXPECT_THAT( + debug_string_lines, + UnorderedElementsAre("Time left: inf", "Deterministic time left: inf", + MatchesRegex("Elapsed time: [-0-9.e]*"), + "Elapsed deterministic time: 30", "DoingFoo: 10", + "DoingBar: 20")); +#endif +} + +TEST(TimeLimitTest, ElapsedTimeReachedFirst) { + const double kLimit = 0.01; // 10ms + const double kUnitTime = 0.001; + TimeLimit time_limit(kLimit, kLimit); + EXPECT_EQ(time_limit.CurrentState(), TimeLimit::State::kRunning); + + // Sleeps 100ms. + absl::SleepFor(absl::Milliseconds(100)); + time_limit.AdvanceDeterministicTime(kUnitTime); + EXPECT_GE(0.0, time_limit.GetTimeLeft()); + EXPECT_EQ(kLimit - kUnitTime, time_limit.GetDeterministicTimeLeft()); + EXPECT_EQ(time_limit.CurrentState(), TimeLimit::State::kTime); + EXPECT_EQ(0.0, time_limit.GetTimeLeft()); + EXPECT_EQ(kLimit - kUnitTime, time_limit.GetDeterministicTimeLeft()); +} + +TEST(TimeLimitTest, DeterniministicTimeReachedFirst) { + const double kLimit = 0.2 * kTimeMultiplier; // 200ms in opt. + TimeLimit time_limit(kLimit, kLimit); + EXPECT_EQ(time_limit.CurrentState(), TimeLimit::State::kRunning); + + absl::SleepFor(absl::Milliseconds(1) * kTimeMultiplier); + time_limit.AdvanceDeterministicTime(1.0 * kTimeMultiplier); + EXPECT_LT(0.0, time_limit.GetTimeLeft()); + EXPECT_EQ(0.0, time_limit.GetDeterministicTimeLeft()); + EXPECT_EQ(time_limit.CurrentState(), TimeLimit::State::kDeterministicTime); + EXPECT_LT(0.0, time_limit.GetTimeLeft()); + EXPECT_EQ(0.0, time_limit.GetDeterministicTimeLeft()); +} + +TEST(TimeLimitTest, ExternalLimitReached) { + std::atomic external_boolean_as_limit(false); + TimeLimit time_limit(kInfinity, kInfinity); + time_limit.RegisterExternalBooleanAsLimit(&external_boolean_as_limit); + EXPECT_EQ(time_limit.CurrentState(), TimeLimit::State::kRunning); + + absl::SleepFor(absl::Milliseconds(10)); + time_limit.AdvanceDeterministicTime(1.0); + EXPECT_EQ(time_limit.CurrentState(), TimeLimit::State::kRunning); + + external_boolean_as_limit = true; + EXPECT_EQ(time_limit.CurrentState(), TimeLimit::State::kExternalBoolean); + + external_boolean_as_limit = false; + EXPECT_EQ(time_limit.CurrentState(), TimeLimit::State::kRunning); +} + +TEST(TimeLimitTest, Infinite) { + std::unique_ptr time_limit = TimeLimit::Infinite(); + EXPECT_EQ(kInfinity, time_limit->GetTimeLeft()); + EXPECT_EQ(kInfinity, time_limit->GetDeterministicTimeLeft()); +} + +TEST(TimeLimitTest, FromDeterministicTime) { + const double kDeterministicTimeLimit = 2.0; + std::unique_ptr time_limit = + TimeLimit::FromDeterministicTime(kDeterministicTimeLimit); + EXPECT_EQ(kInfinity, time_limit->GetTimeLeft()); + EXPECT_EQ(kDeterministicTimeLimit, time_limit->GetDeterministicTimeLeft()); +} + +class TestParameters { + public: + TestParameters(double max_time_in_seconds, double max_deterministic_time) + : max_time_in_seconds_(max_time_in_seconds), + max_deterministic_time_(max_deterministic_time) {} + + double max_time_in_seconds() const { return max_time_in_seconds_; } + double max_deterministic_time() const { return max_deterministic_time_; } + + private: + double max_time_in_seconds_; + double max_deterministic_time_; +}; + +TEST(TimeLimitTest, FromParameters) { + const double kTimeLimitSeconds = 1.0; + const double kDeterministicTimeLimit = 2.0; + TestParameters parameters(kTimeLimitSeconds, kDeterministicTimeLimit); + std::unique_ptr time_limit = TimeLimit::FromParameters(parameters); + EXPECT_NEAR(kTimeLimitSeconds, time_limit->GetTimeLeft(), + 1e-3 * kTimeMultiplier); + EXPECT_EQ(kDeterministicTimeLimit, time_limit->GetDeterministicTimeLeft()); +} + +TEST(TimeLimitTest, ResetLimitFromParameters) { + TestParameters parameters(kInfinity, 1.0); + std::unique_ptr time_limit = TimeLimit::FromParameters(parameters); + EXPECT_EQ(time_limit->CurrentState(), TimeLimit::State::kRunning); + time_limit->AdvanceDeterministicTime(2.0); + EXPECT_EQ(time_limit->CurrentState(), TimeLimit::State::kDeterministicTime); + EXPECT_EQ(time_limit->GetElapsedDeterministicTime(), 2.0); + EXPECT_EQ(time_limit->GetDeterministicTimeLeft(), 0.0); + + time_limit->ResetLimitFromParameters(parameters); + EXPECT_EQ(time_limit->CurrentState(), TimeLimit::State::kRunning); + EXPECT_EQ(time_limit->GetElapsedDeterministicTime(), 0.0); + EXPECT_EQ(time_limit->GetDeterministicTimeLeft(), 1.0); + + time_limit->AdvanceDeterministicTime(2.0); + EXPECT_EQ(time_limit->CurrentState(), TimeLimit::State::kDeterministicTime); +} + +TEST(TimeLimitTest, ResetDeterministicTimeLimit) { + TimeLimit limit(/*limit_in_seconds=*/1.0, /*deterministic_limit=*/1.0); + EXPECT_EQ(limit.CurrentState(), TimeLimit::State::kRunning); + limit.AdvanceDeterministicTime(2.0); + EXPECT_EQ(limit.CurrentState(), TimeLimit::State::kDeterministicTime); + limit.ChangeDeterministicLimit(4.0); + EXPECT_EQ(limit.CurrentState(), TimeLimit::State::kRunning); + EXPECT_EQ(limit.GetElapsedDeterministicTime(), 2.0); + EXPECT_EQ(limit.GetDeterministicTimeLeft(), 2.0); + EXPECT_EQ(limit.GetDeterministicLimit(), 4.0); +} + +TEST(SharedTimeLimitTest, IsThreadSafe) { + absl::BitGen gen; + + const int num_workers = 100; + const int num_repeats = 10; + + int total_time = 0; + TimeLimit time_limit; + SharedTimeLimit shared_time_limit(&time_limit); + { + ThreadPool pool("SharedTimeLimitTest", num_workers); + + for (int i = 0; i < num_workers; ++i) { + const int deterministic_ticks = + absl::Uniform(absl::IntervalClosedClosed, gen, 1, 100); + total_time += num_repeats * deterministic_ticks; + pool.Schedule([&shared_time_limit, deterministic_ticks]() { + for (int j = 0; j < num_repeats; ++j) { + shared_time_limit.AdvanceDeterministicTime(deterministic_ticks); + absl::SleepFor(absl::Milliseconds(deterministic_ticks)); + } + }); + } + } + EXPECT_EQ(total_time, shared_time_limit.GetElapsedDeterministicTime()); +} + +TEST(TimeLimitCheckEveryNCallsTest, WaitNBeforeFirstCheck) { + TimeLimit limit; + std::atomic stop = false; + limit.RegisterExternalBooleanAsLimit(&stop); + TimeLimitCheckEveryNCalls checker(10, &limit); + stop = true; + + for (int i = 0; i < 10; ++i) { + EXPECT_FALSE(checker.LimitReached()); + } + EXPECT_TRUE(checker.LimitReached()); +} + +TEST(TimeLimitCheckEveryNCallsTest, StickyTrue) { + TimeLimit limit; + std::atomic stop = false; + limit.RegisterExternalBooleanAsLimit(&stop); + TimeLimitCheckEveryNCalls checker(10, &limit); + EXPECT_FALSE(checker.LimitReached()); + stop = true; + + for (int i = 0; i < 9; ++i) { + EXPECT_FALSE(checker.LimitReached()); + } + + // Stays true forwever. + EXPECT_TRUE(checker.LimitReached()); + EXPECT_TRUE(checker.LimitReached()); + EXPECT_TRUE(checker.LimitReached()); +} + +} // namespace +} // namespace operations_research diff --git a/ortools/util/tuple_set_test.cc b/ortools/util/tuple_set_test.cc new file mode 100644 index 00000000000..c84beed5c36 --- /dev/null +++ b/ortools/util/tuple_set_test.cc @@ -0,0 +1,236 @@ +// Copyright 2010-2025 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/util/tuple_set.h" + +#include +#include +#include + +#include "gtest/gtest.h" + +namespace operations_research { +namespace { + +TEST(IntTupleSetTest, SimpleFlow) { + IntTupleSet set(3); + set.Insert3(1, 3, 5); + set.Insert3(1, 3, 5); + set.Insert3(1, 3, 6); + set.Insert3(2, 4, 5); + set.Insert3(2, 4, 7); + EXPECT_EQ(4, set.NumTuples()); + EXPECT_EQ(3, set.Arity()); + std::vector tuple1 = {1, 3, 6}; + std::vector tuple2 = {1, 3, 6}; + std::vector tuple3 = {1, 3, 16}; + std::vector tuple4 = {1, 3, 16}; + EXPECT_TRUE(set.Contains(tuple1)); + EXPECT_TRUE(set.Contains(tuple2)); + EXPECT_FALSE(set.Contains(tuple3)); + EXPECT_FALSE(set.Contains(tuple4)); +} + +TEST(IntTupleSetTest, OneCopy) { + IntTupleSet set(3); + set.Insert3(1, 3, 5); + set.Insert3(1, 3, 5); + set.Insert3(1, 3, 6); + // NOLINTNEXTLINE(performance-unnecessary-copy-initialization) + IntTupleSet set2(set); + EXPECT_EQ(2, set2.NumTuples()); + EXPECT_EQ(3, set2.Arity()); + std::vector tuple1 = {1, 3, 6}; + std::vector tuple2 = {1, 3, 16}; + EXPECT_TRUE(set2.Contains(tuple1)); + EXPECT_FALSE(set2.Contains(tuple2)); +} + +TEST(IntTupleSetTest, OneCopyWithEqual) { + IntTupleSet set(3); + set.Insert3(1, 3, 5); + set.Insert3(1, 3, 5); + set.Insert3(1, 3, 6); + // NOLINTNEXTLINE(performance-unnecessary-copy-initialization) + IntTupleSet set2 = set; + EXPECT_EQ(2, set2.NumTuples()); + EXPECT_EQ(3, set2.Arity()); + std::vector tuple1 = {1, 3, 6}; + std::vector tuple2 = {1, 3, 16}; + EXPECT_TRUE(set2.Contains(tuple1)); + EXPECT_FALSE(set2.Contains(tuple2)); +} + +TEST(IntTupleSetTest, CopyAndBranchOnOriginal) { + IntTupleSet set(3); + set.Insert3(1, 3, 5); + set.Insert3(1, 3, 5); + set.Insert3(1, 3, 6); + // NOLINTNEXTLINE(performance-unnecessary-copy-initialization) + IntTupleSet set2(set); + set.Insert3(3, 3, 3); + EXPECT_EQ(3, set.NumTuples()); + EXPECT_EQ(2, set2.NumTuples()); + std::vector tuple1 = {3, 3, 3}; + EXPECT_TRUE(set.Contains(tuple1)); + EXPECT_FALSE(set2.Contains(tuple1)); +} + +TEST(IntTupleSetTest, CopyAndBranchOnCopy) { + IntTupleSet set(3); + set.Insert3(1, 3, 5); + set.Insert3(1, 3, 5); + set.Insert3(1, 3, 6); + // NOLINTNEXTLINE(performance-unnecessary-copy-initialization) + IntTupleSet set2(set); + set2.Insert3(3, 3, 3); + EXPECT_EQ(2, set.NumTuples()); + EXPECT_EQ(3, set2.NumTuples()); + std::vector tuple1 = {3, 3, 3}; + EXPECT_FALSE(set.Contains(tuple1)); + EXPECT_TRUE(set2.Contains(tuple1)); +} + +TEST(IntTupleSetTest, MultipleCopiesAndDeletions) { + std::unique_ptr set(new IntTupleSet(3)); + set->Insert3(1, 3, 5); + set->Insert3(1, 3, 5); + set->Insert3(2, 4, 6); + std::unique_ptr set2(new IntTupleSet(*set)); + std::unique_ptr set3(new IntTupleSet(*set2)); + set2->Insert3(3, 3, 3); + + std::vector tuple1 = {3, 3, 3}; + EXPECT_EQ(2, set->NumTuples()); + EXPECT_EQ(3, set2->NumTuples()); + EXPECT_EQ(2, set3->NumTuples()); + EXPECT_FALSE(set->Contains(tuple1)); + EXPECT_TRUE(set2->Contains(tuple1)); + EXPECT_FALSE(set3->Contains(tuple1)); + + // Delete 'set' + set.reset(nullptr); + EXPECT_EQ(3, set2->NumTuples()); + EXPECT_EQ(2, set3->NumTuples()); + EXPECT_TRUE(set2->Contains(tuple1)); + EXPECT_FALSE(set3->Contains(tuple1)); + + // Delete 'set3' + set3.reset(nullptr); + EXPECT_EQ(3, set2->NumTuples()); + EXPECT_TRUE(set2->Contains(tuple1)); +} + +TEST(IntTupleSetTest, ZeroArity) { + IntTupleSet set(0); + EXPECT_EQ(0, set.Arity()); + EXPECT_EQ(0, set.NumTuples()); + std::vector empty_tuple; + EXPECT_FALSE(set.Contains(empty_tuple)); + EXPECT_EQ(0, set.Insert(empty_tuple)); + EXPECT_EQ(1, set.NumTuples()); + EXPECT_TRUE(set.Contains(empty_tuple)); + EXPECT_EQ(-1, set.Insert(empty_tuple)); +} + +TEST(IntTupleSetTest, NumDifferentValuesInColumn) { + IntTupleSet set(3); + set.Insert3(1, 3, 5); + set.Insert3(1, 3, 5); + set.Insert3(1, 3, 6); + set.Insert3(2, 4, 5); + set.Insert3(2, 4, 7); + EXPECT_EQ(2, set.NumDifferentValuesInColumn(0)); + EXPECT_EQ(2, set.NumDifferentValuesInColumn(1)); + EXPECT_EQ(3, set.NumDifferentValuesInColumn(2)); + EXPECT_EQ(0, set.NumDifferentValuesInColumn(-1)); + EXPECT_EQ(0, set.NumDifferentValuesInColumn(3)); +} + +TEST(IntTupleSetTest, SortedTupleSet) { + IntTupleSet set(3); + set.Insert3(1, 2, 5); + set.Insert3(1, 4, 5); + set.Insert3(1, 5, 6); + set.Insert3(2, 3, 5); + set.Insert3(2, 0, 7); + EXPECT_EQ(5, set.NumDifferentValuesInColumn(1)); + IntTupleSet sorted = set.SortedByColumn(1); + EXPECT_EQ(0, sorted.Value(0, 1)); + EXPECT_EQ(2, sorted.Value(1, 1)); + EXPECT_EQ(3, sorted.Value(2, 1)); + EXPECT_EQ(4, sorted.Value(3, 1)); + EXPECT_EQ(5, sorted.Value(4, 1)); + EXPECT_EQ(2, set.Value(0, 1)); + EXPECT_EQ(4, set.Value(1, 1)); + EXPECT_EQ(5, set.Value(2, 1)); + EXPECT_EQ(3, set.Value(3, 1)); + EXPECT_EQ(0, set.Value(4, 1)); +} + +TEST(IntTupleSetTest, SortedTupleSetStability) { + IntTupleSet set(3); + set.Insert3(1, 5, 6); + set.Insert3(2, 3, 5); + set.Insert3(2, 0, 7); + set.Insert3(1, 4, 5); + set.Insert3(1, 2, 5); + EXPECT_EQ(5, set.NumDifferentValuesInColumn(1)); + IntTupleSet sorted = set.SortedByColumn(0); + EXPECT_EQ(5, sorted.Value(0, 1)); + EXPECT_EQ(4, sorted.Value(1, 1)); + EXPECT_EQ(2, sorted.Value(2, 1)); + EXPECT_EQ(3, sorted.Value(3, 1)); + EXPECT_EQ(0, sorted.Value(4, 1)); +} + +void Expect3(const IntTupleSet& set, int index, int64_t v0, int64_t v1, + int64_t v2) { + EXPECT_EQ(v0, set.Value(index, 0)); + EXPECT_EQ(v1, set.Value(index, 1)); + EXPECT_EQ(v2, set.Value(index, 2)); +} + +TEST(IntTupleSetTest, LexicographicallySortedTupleSet) { + IntTupleSet set(3); + set.Insert3(1, 2, 5); + set.Insert3(1, 4, 6); + set.Insert3(1, 5, 6); + set.Insert3(1, 4, 5); + set.Insert3(2, 3, 5); + set.Insert3(2, 0, 7); + IntTupleSet sorted = set.SortedLexicographically(); + Expect3(sorted, 0, 1, 2, 5); + Expect3(sorted, 1, 1, 4, 5); + Expect3(sorted, 2, 1, 4, 6); + Expect3(sorted, 3, 1, 5, 6); + Expect3(sorted, 4, 2, 0, 7); + Expect3(sorted, 5, 2, 3, 5); +} + +TEST(IntTupleSetTest, InsertReturnValue) { + IntTupleSet set(2); + EXPECT_EQ(0, set.Insert2(0, 1)); + EXPECT_EQ(1, set.Insert2(1, 0)); + EXPECT_EQ(-1, set.Insert2(0, 1)); + EXPECT_EQ(2, set.Insert2(1, 1)); + EXPECT_EQ(-1, set.Insert2(1, 0)); +} + +TEST(IntTupleSetDeathTest, WrongArity) { + IntTupleSet set(3); + EXPECT_DEATH(set.Insert2(2, 4), ""); + EXPECT_DEATH(set.Insert4(2, 4, 4, 0), ""); +} +} // namespace +} // namespace operations_research diff --git a/ortools/util/zvector_test.cc b/ortools/util/zvector_test.cc new file mode 100644 index 00000000000..2ccd24921d0 --- /dev/null +++ b/ortools/util/zvector_test.cc @@ -0,0 +1,171 @@ +// Copyright 2010-2025 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/util/zvector.h" + +#include +#include +#include +#include + +#include "absl/random/random.h" +#include "gtest/gtest.h" + +namespace operations_research { + +template +void TestZVectorZeroIndex() { + constexpr int64_t kZeroIndex = 0; + constexpr int64_t kNonZeroIndex = 10; + constexpr std::array kArrayBounds = {std::array{kZeroIndex, kZeroIndex}, + std::array{kZeroIndex, kNonZeroIndex}, + std::array{-kNonZeroIndex, kZeroIndex}}; + const T kValue = 1; + for (int64_t i = 0; i < kArrayBounds.size(); ++i) { + ZVector array(kArrayBounds[i][0], kArrayBounds[i][1]); + array.Set(kZeroIndex, kValue); + EXPECT_EQ(kValue, array[kZeroIndex]); + } +} + +TEST(ZVectorTest, SingleZeroIndex) { + TestZVectorZeroIndex(); + TestZVectorZeroIndex(); + TestZVectorZeroIndex(); + TestZVectorZeroIndex(); + TestZVectorZeroIndex(); + TestZVectorZeroIndex(); + TestZVectorZeroIndex(); + TestZVectorZeroIndex(); +} + +template +void TestZeroLengthZVector() { + ZVector a; + a.SetAll(1); +} + +TEST(ZVectorTest, ZeroLength) { + TestZeroLengthZVector(); + TestZeroLengthZVector(); + TestZeroLengthZVector(); + TestZeroLengthZVector(); + TestZeroLengthZVector(); + TestZeroLengthZVector(); + TestZeroLengthZVector(); + TestZeroLengthZVector(); +} + +template +struct make_unsigned; +template <> +struct make_unsigned { + typedef uint8_t type; +}; +template <> +struct make_unsigned { + typedef uint16_t type; +}; +template <> +struct make_unsigned { + typedef uint32_t type; +}; +template <> +struct make_unsigned { + typedef uint64_t type; +}; +template <> +struct make_unsigned { + typedef uint8_t type; +}; +template <> +struct make_unsigned { + typedef uint16_t type; +}; +template <> +struct make_unsigned { + typedef uint32_t type; +}; +template <> +struct make_unsigned { + typedef uint64_t type; +}; + +template +T Random(std::mt19937* const randomizer) { + // Using the unsigned version of the type guarantees modular arithmetic, + // and avoids overflow on things such as INT_MAX - INT_MIN. + typedef typename make_unsigned::type U; + const U kMin = std::numeric_limits::min(); + const U kMax = std::numeric_limits::max(); + return static_cast(kMin + + absl::Uniform(*randomizer, 0, kMax - kMin)); +} + +template +void TestZVectorRangeValue() { + const int64_t kMinIndex = -100000; + const int64_t kMaxIndex = 100000; + + ZVector array(kMinIndex, kMaxIndex); + std::mt19937 randomizer(0); + for (int64_t i = kMinIndex; i <= kMaxIndex; ++i) { + T value = Random(&randomizer); + array.Set(i, value); + } + randomizer.seed(0); + for (int64_t i = kMinIndex; i <= kMaxIndex; ++i) { + T value = Random(&randomizer); + EXPECT_EQ(value, array[i]); + EXPECT_EQ(value, array.Value(i)); + } + array.SetAll(0); + for (int64_t i = kMinIndex; i <= kMaxIndex; ++i) { + EXPECT_EQ(0, array[i]); + EXPECT_EQ(0, array.Value(i)); + } +} + +TEST(ZVectorTest, Random) { + TestZVectorRangeValue(); + TestZVectorRangeValue(); + TestZVectorRangeValue(); + TestZVectorRangeValue(); + TestZVectorRangeValue(); + TestZVectorRangeValue(); + TestZVectorRangeValue(); + TestZVectorRangeValue(); +} + +#ifndef NDEBUG +// Death depends on DCHECKs, so we do not check for death in opt mode. +TEST(ZVectorDeathTest, ReverseBounds) { + const int64_t kMinIndex = 1000; + const int64_t kMaxIndex = -1000; + EXPECT_DEATH({ ZVector(kMinIndex, kMaxIndex); }, ""); +} + +TEST(ZVectorDeathTest, BoundOverflow) { + const int64_t kMinIndex = -1000; + const int64_t kMaxIndex = 1000; + ZVector array = ZVector(kMinIndex, kMaxIndex); + EXPECT_DEATH({ array.Set(kMaxIndex + 1, 12); }, ""); + EXPECT_DEATH({ array.Value(kMaxIndex + 1); }, ""); + EXPECT_DEATH({ array[kMaxIndex + 1]; }, ""); + EXPECT_DEATH({ array.Set(kMinIndex - 1, 12); }, ""); + EXPECT_DEATH({ array.Value(kMinIndex - 1); }, ""); + EXPECT_DEATH({ array[kMinIndex - 1]; }, ""); +} +#endif + +} // namespace operations_research From 70291ee2cde35ad15564906ab6501748d2bab269 Mon Sep 17 00:00:00 2001 From: Guillaume Chatelet Date: Wed, 22 Jul 2026 12:52:07 +0000 Subject: [PATCH 2/3] Do not test random_subset_test with CMake as well --- ortools/util/CMakeLists.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ortools/util/CMakeLists.txt b/ortools/util/CMakeLists.txt index e56bcbcf272..a288e2ba664 100644 --- a/ortools/util/CMakeLists.txt +++ b/ortools/util/CMakeLists.txt @@ -60,4 +60,8 @@ if(BUILD_CXX_TESTING) GTest::gmock ) endforeach() + # These tests are too long so we disable them. + set_tests_properties( + cxx_util_random_subset_test + PROPERTIES DISABLED TRUE) endif() From 30e09bd7545dfccaac2e66c0b28f030855c2b195 Mon Sep 17 00:00:00 2001 From: Guillaume Chatelet Date: Wed, 22 Jul 2026 13:31:56 +0000 Subject: [PATCH 3/3] Add missing library --- ortools/util/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/ortools/util/CMakeLists.txt b/ortools/util/CMakeLists.txt index a288e2ba664..fba7c06e809 100644 --- a/ortools/util/CMakeLists.txt +++ b/ortools/util/CMakeLists.txt @@ -55,6 +55,7 @@ if(BUILD_CXX_TESTING) SOURCES ${_FILE_NAME} LINK_LIBRARIES + absl::scoped_mock_log GTest::gtest GTest::gtest_main GTest::gmock