[log] Prepare helper for streaming container contents to strings. PiperOrigin-RevId: 846769302 Change-Id: Ice80fd61edaf7fa4b97286444251abccb204679f
diff --git a/CMake/AbseilDll.cmake b/CMake/AbseilDll.cmake index 91e220a..b8b137c 100644 --- a/CMake/AbseilDll.cmake +++ b/CMake/AbseilDll.cmake
@@ -179,6 +179,7 @@ "log/internal/conditions.cc" "log/internal/conditions.h" "log/internal/config.h" + "log/internal/container.h" "log/internal/fnmatch.h" "log/internal/fnmatch.cc" "log/internal/globals.cc"
diff --git a/absl/log/CMakeLists.txt b/absl/log/CMakeLists.txt index 51f399e..9d097ab 100644 --- a/absl/log/CMakeLists.txt +++ b/absl/log/CMakeLists.txt
@@ -1204,3 +1204,36 @@ absl::log_internal_fnmatch GTest::gmock_main ) + +absl_cc_library( + NAME + log_internal_container + HDRS + "internal/container.h" + COPTS + ${ABSL_DEFAULT_COPTS} + LINKOPTS + ${ABSL_DEFAULT_LINKOPTS} + DEPS + absl::config + absl::requires_internal + absl::strings +) + +absl_cc_test( + NAME + internal_container_test + SRCS + "internal/container_test.cc" + COPTS + ${ABSL_TEST_COPTS} + LINKOPTS + ${ABSL_DEFAULT_LINKOPTS} + DEPS + absl::config + absl::log_internal_container + absl::span + absl::strings + absl::str_format + GTest::gmock_main +)
diff --git a/absl/log/internal/BUILD.bazel b/absl/log/internal/BUILD.bazel index bb20a95..c5bdd67 100644 --- a/absl/log/internal/BUILD.bazel +++ b/absl/log/internal/BUILD.bazel
@@ -548,3 +548,31 @@ "@google_benchmark//:benchmark_main", ], ) + +cc_library( + name = "container", + hdrs = ["container.h"], + copts = ABSL_DEFAULT_COPTS, + linkopts = ABSL_DEFAULT_LINKOPTS, + deps = [ + "//absl/base:config", + "//absl/meta:requires", + "//absl/strings", + ], +) + +cc_test( + name = "container_test", + srcs = ["container_test.cc"], + copts = ABSL_TEST_COPTS, + linkopts = ABSL_DEFAULT_LINKOPTS, + deps = [ + ":container", + "//absl/base:config", + "//absl/strings", + "//absl/strings:str_format", + "//absl/types:span", + "@googletest//:gtest", + "@googletest//:gtest_main", + ], +)
diff --git a/absl/log/internal/container.h b/absl/log/internal/container.h new file mode 100644 index 0000000..1144652 --- /dev/null +++ b/absl/log/internal/container.h
@@ -0,0 +1,312 @@ +// Copyright 2025 The Abseil Authors. +// +// 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 +// +// https://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. +// +// The typical use looks like this: +// +// LOG(INFO) << LogContainer(container); +// +// By default, LogContainer() uses the LogShortUpTo100 policy: comma-space +// separation, no newlines, and with limit of 100 items. +// +// Policies can be specified: +// +// LOG(INFO) << LogContainer(container, LogMultiline()); +// +// The above example will print the container using newlines between elements, +// enclosed in [] braces. +// +// See below for further details on policies. + +#ifndef ABSL_LOG_INTERNAL_CONTAINER_H_ +#define ABSL_LOG_INTERNAL_CONTAINER_H_ + +#include <cstdint> +#include <limits> +#include <ostream> +#include <sstream> +#include <string> +#include <type_traits> + +#include "absl/base/config.h" +#include "absl/meta/internal/requires.h" +#include "absl/strings/str_cat.h" + +namespace absl { +ABSL_NAMESPACE_BEGIN +namespace log_internal { + +// Several policy classes below determine how LogRangeToStream will +// format a range of items. A Policy class should have these methods: +// +// Called to print an individual container element. +// void Log(ostream &out, const ElementT &element) const; +// +// Called before printing the set of elements: +// void LogOpening(ostream &out) const; +// +// Called after printing the set of elements: +// void LogClosing(ostream &out) const; +// +// Called before printing the first element: +// void LogFirstSeparator(ostream &out) const; +// +// Called before printing the remaining elements: +// void LogSeparator(ostream &out) const; +// +// Returns the maximum number of elements to print: +// int64 MaxElements() const; +// +// Called to print an indication that MaximumElements() was reached: +// void LogEllipsis(ostream &out) const; + +namespace internal { + +struct LogBase { + template <typename ElementT> + void Log(std::ostream &out, const ElementT &element) const { // NOLINT + // Fallback to `AbslStringify` if the type does not have `operator<<`. + if constexpr (meta_internal::Requires<std::ostream, ElementT>( + [](auto&& x, auto&& y) -> decltype(x << y) {})) { + out << element; + } else { + out << absl::StrCat(element); + } + } + void LogEllipsis(std::ostream &out) const { // NOLINT + out << "..."; + } +}; + +struct LogShortBase : public LogBase { + void LogOpening(std::ostream &out) const { out << "["; } // NOLINT + void LogClosing(std::ostream &out) const { out << "]"; } // NOLINT + void LogFirstSeparator(std::ostream &out) const { out << ""; } // NOLINT + void LogSeparator(std::ostream &out) const { out << ", "; } // NOLINT +}; + +struct LogMultilineBase : public LogBase { + void LogOpening(std::ostream &out) const { out << "["; } // NOLINT + void LogClosing(std::ostream &out) const { out << "\n]"; } // NOLINT + void LogFirstSeparator(std::ostream &out) const { out << "\n"; } // NOLINT + void LogSeparator(std::ostream &out) const { out << "\n"; } // NOLINT +}; + +struct LogLegacyBase : public LogBase { + void LogOpening(std::ostream &out) const { out << ""; } // NOLINT + void LogClosing(std::ostream &out) const { out << ""; } // NOLINT + void LogFirstSeparator(std::ostream &out) const { out << ""; } // NOLINT + void LogSeparator(std::ostream &out) const { out << " "; } // NOLINT +}; + +} // namespace internal + +// LogShort uses [] braces and separates items with comma-spaces. For +// example "[1, 2, 3]". +struct LogShort : public internal::LogShortBase { + int64_t MaxElements() const { return (std::numeric_limits<int64_t>::max)(); } +}; + +// LogShortUpToN(max_elements) formats the same as LogShort but prints no more +// than the max_elements elements. +class LogShortUpToN : public internal::LogShortBase { + public: + explicit LogShortUpToN(int64_t max_elements) : max_elements_(max_elements) {} + int64_t MaxElements() const { return max_elements_; } + + private: + int64_t max_elements_; +}; + +// LogShortUpTo100 formats the same as LogShort but prints no more +// than 100 elements. +struct LogShortUpTo100 : public LogShortUpToN { + LogShortUpTo100() : LogShortUpToN(100) {} +}; + +// LogMultiline uses [] braces and separates items with +// newlines. For example "[ +// 1 +// 2 +// 3 +// ]". +struct LogMultiline : public internal::LogMultilineBase { + int64_t MaxElements() const { return (std::numeric_limits<int64_t>::max)(); } +}; + +// LogMultilineUpToN(max_elements) formats the same as LogMultiline but +// prints no more than max_elements elements. +class LogMultilineUpToN : public internal::LogMultilineBase { + public: + explicit LogMultilineUpToN(int64_t max_elements) + : max_elements_(max_elements) {} + int64_t MaxElements() const { return max_elements_; } + + private: + int64_t max_elements_; +}; + +// LogMultilineUpTo100 formats the same as LogMultiline but +// prints no more than 100 elements. +struct LogMultilineUpTo100 : public LogMultilineUpToN { + LogMultilineUpTo100() : LogMultilineUpToN(100) {} +}; + +// The legacy behavior of LogSequence() does not use braces and +// separates items with spaces. For example "1 2 3". +struct LogLegacyUpTo100 : public internal::LogLegacyBase { + int64_t MaxElements() const { return 100; } +}; +struct LogLegacy : public internal::LogLegacyBase { + int64_t MaxElements() const { return (std::numeric_limits<int64_t>::max)(); } +}; + +// The default policy for new code. +typedef LogShortUpTo100 LogDefault; + +// LogRangeToStream should be used to define operator<< for +// STL and STL-like containers. For example, see stl_logging.h. +template <typename IteratorT, typename PolicyT> +inline void LogRangeToStream(std::ostream &out, // NOLINT + IteratorT begin, IteratorT end, + const PolicyT &policy) { + policy.LogOpening(out); + for (int64_t i = 0; begin != end && i < policy.MaxElements(); ++i, ++begin) { + if (i == 0) { + policy.LogFirstSeparator(out); + } else { + policy.LogSeparator(out); + } + policy.Log(out, *begin); + } + if (begin != end) { + policy.LogSeparator(out); + policy.LogEllipsis(out); + } + policy.LogClosing(out); +} + +namespace detail { + +// RangeLogger is a helper class for LogRange and LogContainer; do not use it +// directly. This object captures iterators into the argument of the LogRange +// and LogContainer functions, so its lifetime should be confined to a single +// logging statement. Objects of this type should not be assigned to local +// variables. +template <typename IteratorT, typename PolicyT> +class RangeLogger { + public: + RangeLogger(const IteratorT &begin, const IteratorT &end, + const PolicyT &policy) + : begin_(begin), end_(end), policy_(policy) {} + + friend std::ostream &operator<<(std::ostream &out, const RangeLogger &range) { + LogRangeToStream<IteratorT, PolicyT>(out, range.begin_, range.end_, + range.policy_); + return out; + } + + // operator<< above is generally recommended. However, some situations may + // require a string, so a convenience str() method is provided as well. + std::string str() const { + std::stringstream ss; + ss << *this; + return ss.str(); + } + + private: + IteratorT begin_; + IteratorT end_; + PolicyT policy_; +}; + +template <typename E> +class EnumLogger { + public: + explicit EnumLogger(E e) : e_(e) {} + + friend std::ostream &operator<<(std::ostream &out, const EnumLogger &v) { + using I = typename std::underlying_type<E>::type; + return out << static_cast<I>(v.e_); + } + + private: + E e_; +}; + +} // namespace detail + +// Log a range using "policy". For example: +// +// LOG(INFO) << LogRange(start_pos, end_pos, LogMultiline()); +// +// The above example will print the range using newlines between +// elements, enclosed in [] braces. +template <typename IteratorT, typename PolicyT> +detail::RangeLogger<IteratorT, PolicyT> LogRange(const IteratorT &begin, + const IteratorT &end, + const PolicyT &policy) { + return detail::RangeLogger<IteratorT, PolicyT>(begin, end, policy); +} + +// Log a range. For example: +// +// LOG(INFO) << LogRange(start_pos, end_pos); +// +// By default, Range() uses the LogShortUpTo100 policy: comma-space +// separation, no newlines, and with limit of 100 items. +template <typename IteratorT> +detail::RangeLogger<IteratorT, LogDefault> LogRange(const IteratorT &begin, + const IteratorT &end) { + return LogRange(begin, end, LogDefault()); +} + +// Log a container using "policy". For example: +// +// LOG(INFO) << LogContainer(container, LogMultiline()); +// +// The above example will print the container using newlines between +// elements, enclosed in [] braces. +template <typename ContainerT, typename PolicyT> +auto LogContainer(const ContainerT& container, const PolicyT& policy) + -> decltype(LogRange(container.begin(), container.end(), policy)) { + return LogRange(container.begin(), container.end(), policy); +} + +// Log a container. For example: +// +// LOG(INFO) << LogContainer(container); +// +// By default, Container() uses the LogShortUpTo100 policy: comma-space +// separation, no newlines, and with limit of 100 items. +template <typename ContainerT> +auto LogContainer(const ContainerT& container) + -> decltype(LogContainer(container, LogDefault())) { + return LogContainer(container, LogDefault()); +} + +// Log a (possibly scoped) enum. For example: +// +// enum class Color { kRed, kGreen, kBlue }; +// LOG(INFO) << LogEnum(kRed); +template <typename E> +detail::EnumLogger<E> LogEnum(E e) { + static_assert(std::is_enum<E>::value, "must be an enum"); + return detail::EnumLogger<E>(e); +} + +} // namespace log_internal +ABSL_NAMESPACE_END +} // namespace absl + +#endif // ABSL_LOG_INTERNAL_CONTAINER_H_
diff --git a/absl/log/internal/container_test.cc b/absl/log/internal/container_test.cc new file mode 100644 index 0000000..0a5a058 --- /dev/null +++ b/absl/log/internal/container_test.cc
@@ -0,0 +1,254 @@ +// Copyright 2025 The Abseil Authors. +// +// 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 +// +// https://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 "absl/log/internal/container.h" + +#include <cstdint> +#include <map> +#include <memory> +#include <ostream> +#include <set> +#include <sstream> +#include <string> +#include <utility> +#include <vector> + +#include "gtest/gtest.h" +#include "absl/base/config.h" +#include "absl/strings/str_format.h" +#include "absl/strings/str_join.h" +#include "absl/types/span.h" + +namespace absl { +ABSL_NAMESPACE_BEGIN +namespace log_internal { +namespace { + +class ContainerLoggingTest : public ::testing::Test { + protected: + ContainerLoggingTest() : stream_(new std::stringstream) {} + std::ostream& stream() { return *stream_; } + std::string logged() { + std::string r = stream_->str(); + stream_ = std::make_unique<std::stringstream>(); + return r; + } + + private: + std::unique_ptr<std::stringstream> stream_; +}; + +TEST_F(ContainerLoggingTest, ShortRange) { + std::vector<std::string> words = {"hi", "hello"}; + LogRangeToStream(stream(), words.begin(), words.end(), LogMultiline()); + EXPECT_EQ("[\nhi\nhello\n]", logged()); +} + +TEST_F(ContainerLoggingTest, LegacyRange) { + std::vector<int> lengths = {1, 2}; + LogRangeToStream(stream(), lengths.begin(), lengths.end(), + LogLegacyUpTo100()); + EXPECT_EQ("1 2", logged()); +} + +TEST_F(ContainerLoggingTest, ToString) { + std::vector<int> lengths = {1, 2, 3, 4, 5}; + EXPECT_EQ(LogContainer(lengths).str(), "[1, 2, 3, 4, 5]"); +} + +class UserDefFriend { + public: + explicit UserDefFriend(int i) : i_(i) {} + + private: + friend std::ostream& operator<<(std::ostream& str, const UserDefFriend& i) { + return str << i.i_; + } + int i_; +}; + +TEST_F(ContainerLoggingTest, RangeOfUserDefined) { + std::vector<UserDefFriend> ints = {UserDefFriend(1), UserDefFriend(2), + UserDefFriend(3)}; + LogRangeToStream(stream(), ints.begin(), ints.begin() + 1, LogDefault()); + LogRangeToStream(stream(), ints.begin() + 1, ints.begin() + 2, + LogMultiline()); + LogRangeToStream(stream(), ints.begin() + 2, ints.begin() + 3, LogDefault()); + LogRangeToStream(stream(), ints.begin(), ints.begin(), LogMultiline()); + + EXPECT_EQ("[1][\n2\n][3][\n]", logged()); +} + +TEST_F(ContainerLoggingTest, FullContainer) { + std::vector<int> ints; + std::vector<int> ints100; + std::vector<int> ints123; + int64_t max_elements = 123; + std::string expected1; + std::string expected2; + std::string expected3; + std::string expected4; + std::string expected5; + std::string expected6; + std::string expected7; + std::string expected8; + std::string expected9; + for (int i = 0; i < 1000; ++i) { + ints.push_back(i); + if (i < 100) { + ints100.push_back(i); + } + if (i < max_elements) { + ints123.push_back(i); + } + } + expected1 = "[\n" + absl::StrJoin(ints, "\n") + "\n]"; + expected2 = "[" + absl::StrJoin(ints, ", ") + "]"; + expected3 = "[\n" + absl::StrJoin(ints100, "\n") + "\n...\n]"; + expected4 = "[" + absl::StrJoin(ints100, ", ") + ", ...]"; + expected5 = absl::StrJoin(ints100, " ") + " ..."; + expected6 = "[\n" + absl::StrJoin(ints, "\n") + "\n]"; + expected7 = "[\n" + absl::StrJoin(ints123, "\n") + "\n...\n]"; + expected8 = "[" + absl::StrJoin(ints, ", ") + "]"; + expected9 = "[" + absl::StrJoin(ints123, ", ") + ", ...]"; + + LogRangeToStream(stream(), ints.begin(), ints.end(), LogMultiline()); + EXPECT_EQ(expected1, logged()); + LogRangeToStream(stream(), ints.begin(), ints.end(), LogShort()); + EXPECT_EQ(expected2, logged()); + LogRangeToStream(stream(), ints.begin(), ints.end(), LogMultilineUpTo100()); + EXPECT_EQ(expected3, logged()); + LogRangeToStream(stream(), ints.begin(), ints.end(), LogShortUpTo100()); + EXPECT_EQ(expected4, logged()); + LogRangeToStream(stream(), ints.begin(), ints.end(), LogLegacyUpTo100()); + EXPECT_EQ(expected5, logged()); + + LogRangeToStream(stream(), ints.begin(), ints.end(), + LogMultilineUpToN(ints.size())); + EXPECT_EQ(expected6, logged()); + LogRangeToStream(stream(), ints.begin(), ints.end(), + LogMultilineUpToN(max_elements)); + EXPECT_EQ(expected7, logged()); + LogRangeToStream(stream(), ints.begin(), ints.end(), + LogShortUpToN(ints.size())); + EXPECT_EQ(expected8, logged()); + LogRangeToStream(stream(), ints.begin(), ints.end(), + LogShortUpToN(max_elements)); + EXPECT_EQ(expected9, logged()); +} + +TEST_F(ContainerLoggingTest, LogContainer) { + std::set<int> ints = {1, 2, 3}; + stream() << LogContainer(ints, LogMultiline()); + EXPECT_EQ("[\n1\n2\n3\n]", logged()); + + stream() << LogContainer(ints); + EXPECT_EQ("[1, 2, 3]", logged()); + + stream() << LogContainer(std::vector<int>(ints.begin(), ints.end()), + LogLegacyUpTo100()); + EXPECT_EQ("1 2 3", logged()); +} + +TEST_F(ContainerLoggingTest, LogMutableSpan) { + std::vector<int> ints = {1, 2, 3}; + absl::Span<int> int_span(ints); + stream() << LogContainer(int_span); + EXPECT_EQ("[1, 2, 3]", logged()); +} + +TEST_F(ContainerLoggingTest, LogRange) { + std::set<int> ints = {1, 2, 3}; + stream() << LogRange(ints.begin(), ints.end(), LogMultiline()); + EXPECT_EQ("[\n1\n2\n3\n]", logged()); + + stream() << LogRange(ints.begin(), ints.end()); + EXPECT_EQ("[1, 2, 3]", logged()); +} + +// Some class with a custom Stringify +class C { + public: + explicit C(int x) : x_(x) {} + + private: + // This is intentionally made private for the purposes of the test; + //` AbslStringify` isn't meant to be called directly, and instead invoked + // via `StrCat` and friends. + template <typename Sink> + friend void AbslStringify(Sink& sink, const C& p) { + absl::Format(&sink, "C(%d)", p.x_); + } + + int x_; +}; + +TEST_F(ContainerLoggingTest, LogContainerWithCustomStringify) { + std::vector<C> c = {C(1), C(2), C(3)}; + stream() << LogContainer(c); + EXPECT_EQ("[C(1), C(2), C(3)]", logged()); +} + +class LogEnumTest : public ContainerLoggingTest { + protected: + enum Unscoped { kUnscoped0, kUnscoped1, kUnscoped2 }; + + enum StreamableUnscoped { + kStreamableUnscoped0, + kStreamableUnscoped1, + kStreamableUnscoped2 + }; + + enum class Scoped { k0, k1, k2 }; + + enum class StreamableScoped { k0, k1, k2 }; + + friend std::ostream& operator<<(std::ostream& os, StreamableUnscoped v) { + return os << LogEnum(v); + } + + friend std::ostream& operator<<(std::ostream& os, StreamableScoped v) { + return os << LogEnum(v); + } +}; + +TEST_F(LogEnumTest, Unscoped) { + stream() << LogEnum(kUnscoped0) << "," << LogEnum(kUnscoped1) << "," + << LogEnum(kUnscoped2); + EXPECT_EQ("0,1,2", logged()); +} + +TEST_F(LogEnumTest, StreamableUnscoped) { + stream() << kStreamableUnscoped0 << "," << kStreamableUnscoped1 << "," + << kStreamableUnscoped2; + EXPECT_EQ("0,1,2", logged()); +} + +TEST_F(LogEnumTest, Scoped) { + stream() << LogEnum(Scoped::k0) << "," << LogEnum(Scoped::k1) << "," + << LogEnum(Scoped::k2); + EXPECT_EQ("0,1,2", logged()); +} + +TEST_F(LogEnumTest, StreamableScoped) { + // Test using LogEnum to implement an operator<<. + stream() << StreamableScoped::k0 << "," << StreamableScoped::k1 << "," + << StreamableScoped::k2; + EXPECT_EQ("0,1,2", logged()); +} + +} // namespace +} // namespace log_internal +ABSL_NAMESPACE_END +} // namespace absl