Moves the generic bug utility to QUICHE.
Protected by refactoring, no functional change; not protected.
PiperOrigin-RevId: 712628602
diff --git a/build/source_list.bzl b/build/source_list.bzl
index 32e4def..ac160d7 100644
--- a/build/source_list.bzl
+++ b/build/source_list.bzl
@@ -10,6 +10,8 @@
]
quiche_core_hdrs = [
"common/btree_scheduler.h",
+ "common/bug_utils.h",
+ "common/bug_utils_test_helper.h",
"common/capsule.h",
"common/http/http_header_block.h",
"common/http/http_header_storage.h",
@@ -409,6 +411,7 @@
"web_transport/web_transport_priority_scheduler.h",
]
quiche_core_srcs = [
+ "common/bug_utils.cc",
"common/capsule.cc",
"common/http/http_header_block.cc",
"common/http/http_header_storage.cc",
@@ -1077,6 +1080,7 @@
"balsa/simple_buffer_test.cc",
"binary_http/binary_http_message_test.cc",
"common/btree_scheduler_test.cc",
+ "common/bug_utils_test.cc",
"common/capsule_test.cc",
"common/http/http_header_block_test.cc",
"common/http/http_header_storage_test.cc",
diff --git a/build/source_list.gni b/build/source_list.gni
index 3b75a6b..a8bf15a 100644
--- a/build/source_list.gni
+++ b/build/source_list.gni
@@ -10,6 +10,8 @@
]
quiche_core_hdrs = [
"src/quiche/common/btree_scheduler.h",
+ "src/quiche/common/bug_utils.h",
+ "src/quiche/common/bug_utils_test_helper.h",
"src/quiche/common/capsule.h",
"src/quiche/common/http/http_header_block.h",
"src/quiche/common/http/http_header_storage.h",
@@ -409,6 +411,7 @@
"src/quiche/web_transport/web_transport_priority_scheduler.h",
]
quiche_core_srcs = [
+ "src/quiche/common/bug_utils.cc",
"src/quiche/common/capsule.cc",
"src/quiche/common/http/http_header_block.cc",
"src/quiche/common/http/http_header_storage.cc",
@@ -1078,6 +1081,7 @@
"src/quiche/balsa/simple_buffer_test.cc",
"src/quiche/binary_http/binary_http_message_test.cc",
"src/quiche/common/btree_scheduler_test.cc",
+ "src/quiche/common/bug_utils_test.cc",
"src/quiche/common/capsule_test.cc",
"src/quiche/common/http/http_header_block_test.cc",
"src/quiche/common/http/http_header_storage_test.cc",
diff --git a/build/source_list.json b/build/source_list.json
index 5eb9364..ab414ec 100644
--- a/build/source_list.json
+++ b/build/source_list.json
@@ -9,6 +9,8 @@
],
"quiche_core_hdrs": [
"quiche/common/btree_scheduler.h",
+ "quiche/common/bug_utils.h",
+ "quiche/common/bug_utils_test_helper.h",
"quiche/common/capsule.h",
"quiche/common/http/http_header_block.h",
"quiche/common/http/http_header_storage.h",
@@ -408,6 +410,7 @@
"quiche/web_transport/web_transport_priority_scheduler.h"
],
"quiche_core_srcs": [
+ "quiche/common/bug_utils.cc",
"quiche/common/capsule.cc",
"quiche/common/http/http_header_block.cc",
"quiche/common/http/http_header_storage.cc",
@@ -1077,6 +1080,7 @@
"quiche/balsa/simple_buffer_test.cc",
"quiche/binary_http/binary_http_message_test.cc",
"quiche/common/btree_scheduler_test.cc",
+ "quiche/common/bug_utils_test.cc",
"quiche/common/capsule_test.cc",
"quiche/common/http/http_header_block_test.cc",
"quiche/common/http/http_header_storage_test.cc",
diff --git a/quiche/common/bug_utils.cc b/quiche/common/bug_utils.cc
new file mode 100644
index 0000000..68bcb6b
--- /dev/null
+++ b/quiche/common/bug_utils.cc
@@ -0,0 +1,64 @@
+#include "quiche/common/bug_utils.h"
+
+#include <atomic>
+#include <cstdint>
+#include <limits>
+#include <string>
+
+#include "absl/base/log_severity.h"
+#include "absl/strings/str_cat.h"
+#include "absl/strings/string_view.h"
+
+namespace quiche {
+namespace internal {
+
+GenericBugStreamHandler::GenericBugStreamHandler(
+ const char* prefix, const char* bug_id, const GenericBugOptions& options)
+ : bug_id_(bug_id), options_(options) {
+ if (options_.condition_str.empty()) {
+ absl::StrAppend(&str_, prefix, "(", bug_id, "): ");
+ } else {
+ absl::StrAppend(&str_, prefix, "_IF(", bug_id, ", ", options_.condition_str,
+ "): ");
+ }
+}
+
+GenericBugStreamHandler::~GenericBugStreamHandler() {
+ GenericBugStreamHandler::OverrideFunction override_function =
+ GetOverrideFunction();
+ if (options_.bug_listener != nullptr) {
+ options_.bug_listener->OnBug(bug_id_, options_.file_name, options_.line,
+ str_);
+ }
+ if (override_function != nullptr) {
+ override_function(options_.severity, options_.file_name, options_.line,
+ str_);
+ }
+}
+
+// static
+void GenericBugStreamHandler::SetOverrideFunction(
+ GenericBugStreamHandler::OverrideFunction override_function) {
+ atomic_override_function_.store(override_function);
+}
+
+// static
+GenericBugStreamHandler::OverrideFunction
+GenericBugStreamHandler::GetOverrideFunction() {
+ return atomic_override_function_.load(std::memory_order_relaxed);
+}
+
+// static
+std::atomic<GenericBugStreamHandler::OverrideFunction>
+ GenericBugStreamHandler::atomic_override_function_ = nullptr;
+
+void GenericBugWithoutLog(const char* bug_id,
+ const GenericBugOptions& options) {
+ if (options.bug_listener != nullptr) {
+ options.bug_listener->OnBug(bug_id, options.file_name, options.line,
+ /*No bug message*/ "");
+ }
+}
+
+} // namespace internal
+} // namespace quiche
diff --git a/quiche/common/bug_utils.h b/quiche/common/bug_utils.h
new file mode 100644
index 0000000..ea6fbf8
--- /dev/null
+++ b/quiche/common/bug_utils.h
@@ -0,0 +1,151 @@
+#ifndef QUICHE_COMMON_BUG_UTILS_H_
+#define QUICHE_COMMON_BUG_UTILS_H_
+
+// This file contains macros for emitting bug log events when invariants are
+// violated.
+//
+// Each instance of a QUICHE_BUG and friends has an associated id, which can be
+// helpful for quickly finding the associated source code.
+//
+// The IDs are free form, but are expected to be unique. Best practice is to
+// provide a *short* description of the condition being detected, without
+// quotes, e.g.,
+//
+// QUICHE_BUG(http2_decoder_invalid_parse_state) << "...";
+//
+// QUICHE_BUG is defined in platform/api/quiche_bug_tracker.h.
+//
+
+#include <atomic>
+#include <cstdint>
+#include <sstream>
+#include <string>
+#include <tuple>
+#include <type_traits>
+
+#include "absl/base/log_severity.h"
+#include "absl/strings/str_cat.h"
+#include "absl/strings/string_view.h"
+#include "quiche/common/platform/api/quiche_export.h"
+
+namespace quiche {
+namespace internal {
+
+QUICHE_EXPORT class GenericBugListener {
+ public:
+ virtual ~GenericBugListener() = default;
+ virtual void OnBug(const char* bug_id, const char* file, int line,
+ absl::string_view bug_message) = 0;
+};
+
+QUICHE_NO_EXPORT struct GenericBugOptions {
+ explicit GenericBugOptions(absl::LogSeverity log_severity,
+ const char* file_name, int line)
+ : severity(log_severity), file_name(file_name), line(line) {}
+
+ GenericBugOptions& SetCondition(absl::string_view condition) {
+ this->condition_str = condition;
+ return *this;
+ }
+
+ GenericBugOptions& SetBugListener(GenericBugListener* listener) {
+ this->bug_listener = listener;
+ return *this;
+ }
+
+ absl::LogSeverity severity;
+ const char* const file_name;
+ const int line;
+ // !empty() for conditional GENERIC_BUGs.
+ absl::string_view condition_str;
+ // If not nullptr, |bug_listener| will be notified of this GENERIC_BUG hit.
+ // Since GenericBugListener may be a temporary object, this is only safe to
+ // access from GenericBugStreamHandler, whose scope is strictly narrower.
+ GenericBugListener* bug_listener = nullptr;
+};
+
+QUICHE_EXPORT inline GenericBugOptions DefaultBugOptions(const char* file_name,
+ int line) {
+ return GenericBugOptions(absl::kLogDebugFatal, file_name, line);
+}
+
+// Called if a GENERIC_BUG is hit, but logging is omitted.
+QUICHE_EXPORT void GenericBugWithoutLog(const char* bug_id,
+ const GenericBugOptions& options);
+
+// GenericBugStreamHandler exposes an interface similar to Abseil log objects,
+// and is used by GENERIC_BUG to trigger a function which can be overridden in
+// tests. By default, this class performs no action. SetOverrideFunction must be
+// called to accomplish anything interesting.
+class QUICHE_EXPORT GenericBugStreamHandler {
+ public:
+ // |prefix| and |bug_id| must be literal strings. They are used in the
+ // destructor.
+ explicit GenericBugStreamHandler(const char* prefix, const char* bug_id,
+ const GenericBugOptions& options);
+
+ ~GenericBugStreamHandler();
+
+ template <typename T,
+ std::enable_if_t<absl::HasAbslStringify<T>::value, bool> = true>
+ QUICHE_EXPORT GenericBugStreamHandler& operator<<(const T& v) {
+ absl::StrAppend(&str_, v);
+ return *this;
+ }
+
+ // For types that support only operator<<. There's a better solution in
+ // Abseil, but unfortunately OStringStream is in a namespace marked internal.
+ template <typename T,
+ std::enable_if_t<!absl::HasAbslStringify<T>::value, bool> = true>
+ QUICHE_EXPORT GenericBugStreamHandler& operator<<(const T& v) {
+ absl::StrAppend(&str_, (std::ostringstream() << v).view());
+ return *this;
+ }
+
+ // Interface similar to Abseil log objects.
+ GenericBugStreamHandler& stream() { return *this; }
+
+ using OverrideFunction = void (*)(absl::LogSeverity severity,
+ const char* file, int line,
+ absl::string_view log_message);
+
+ // Allows overriding the internal implementation. Call with nullptr to make
+ // this class a no-op. This getter and setter are thread-safe.
+ static void SetOverrideFunction(OverrideFunction override_function);
+ static OverrideFunction GetOverrideFunction();
+
+ private:
+ static std::atomic<OverrideFunction> atomic_override_function_;
+
+ const char* bug_id_;
+ std::string str_;
+ const GenericBugOptions& options_;
+};
+
+} // namespace internal
+} // namespace quiche
+
+// The GNU compiler emits a warning for code like:
+//
+// if (foo)
+// if (bar) { } else baz;
+//
+// because it thinks you might want the else to bind to the first if. This
+// leads to problems with code like:
+//
+// if (do_expr) GENERIC_BUG(bug_id) << "Some message";
+//
+// The "switch (0) case 0:" idiom is used to suppress this.
+#define GENERIC_BUG_UNBRACED_ELSE_BLOCKER \
+ switch (0) \
+ case 0: \
+ default:
+
+#define GENERIC_BUG_IMPL(prefix, bug_id, skip_log_condition, options) \
+ if (skip_log_condition) { \
+ ::quiche::internal::GenericBugWithoutLog(#bug_id, (options)); \
+ } else /* NOLINT */ \
+ ::quiche::internal::GenericBugStreamHandler(prefix, #bug_id, (options)) \
+ .stream()
+
+#endif // QUICHE_COMMON_BUG_UTILS_H_
diff --git a/quiche/common/bug_utils_test.cc b/quiche/common/bug_utils_test.cc
new file mode 100644
index 0000000..0bddb0d
--- /dev/null
+++ b/quiche/common/bug_utils_test.cc
@@ -0,0 +1,201 @@
+#include "quiche/common/bug_utils.h"
+
+#include <string.h>
+
+#include <algorithm>
+#include <memory>
+#include <string>
+
+#include "absl/base/log_severity.h"
+#include "absl/strings/string_view.h"
+#include "quiche/common/bug_utils_test_helper.h"
+#include "quiche/common/platform/api/quiche_test.h"
+
+namespace quiche {
+namespace internal {
+namespace {
+
+using ::testing::_;
+using ::testing::AnyNumber;
+using ::testing::EndsWith;
+using ::testing::InSequence;
+
+class BugHandler {
+ public:
+ virtual ~BugHandler() = default;
+ virtual void OnBug(absl::string_view file, int line,
+ absl::string_view message) = 0;
+};
+
+// This class provides a convenient way to write expectations for the bug
+// override function.
+class MockBugHandler : public BugHandler {
+ public:
+ MockBugHandler() = default;
+
+ MOCK_METHOD(void, OnBug,
+ (absl::string_view file, int line, absl::string_view message),
+ (override));
+};
+
+MockBugHandler* mock_bug_handler = nullptr;
+
+MockBugHandler* GetInstance() {
+ if (mock_bug_handler == nullptr) {
+ mock_bug_handler = new MockBugHandler;
+ }
+ return mock_bug_handler;
+}
+
+void ResetInstance() {
+ delete mock_bug_handler;
+ mock_bug_handler = nullptr;
+}
+
+class BugUtilsTest : public ::quiche::test::QuicheTest {
+ public:
+ void SetUp() override {
+ fn_ = GenericBugStreamHandler::GetOverrideFunction();
+ GenericBugStreamHandler::SetOverrideFunction(
+ [](absl::LogSeverity /*severity*/, const char* file, int line,
+ absl::string_view log_message) {
+ GetInstance()->OnBug(absl::string_view(file, strlen(file)), line,
+ log_message);
+ });
+ }
+
+ void TearDown() override {
+ GenericBugStreamHandler::SetOverrideFunction(fn_);
+ ResetInstance();
+ }
+
+ inline static GenericBugStreamHandler::OverrideFunction fn_ = nullptr;
+};
+
+// Tests several permutations.
+TEST_F(BugUtilsTest, TestsEverythingUsing23And26) {
+ InSequence seq;
+ EXPECT_CALL(*GetInstance(), OnBug(EndsWith("bug_utils_test_helper.h"), 23,
+ EndsWith("Here on line 23")));
+ LogBugLine23();
+
+ EXPECT_CALL(*GetInstance(), OnBug(EndsWith("bug_utils_test_helper.h"), 23,
+ EndsWith("Here on line 23")));
+ LogBugLine23();
+
+ EXPECT_CALL(*GetInstance(), OnBug(EndsWith("bug_utils_test_helper.h"), 26,
+ EndsWith("Here on line 26")));
+ EXPECT_CALL(*GetInstance(), OnBug(EndsWith("bug_utils_test_helper.h"), 27,
+ EndsWith("And 27!")));
+ LogBugLine26();
+}
+
+TEST_F(BugUtilsTest, TestBugIf) {
+ InSequence seq;
+
+ // Verify that we don't invoke the function for a false condition.
+ LogIfBugLine31(false);
+
+ // The first true should trigger an invocation.
+ EXPECT_CALL(*GetInstance(), OnBug(EndsWith("bug_utils_test_helper.h"), 31,
+ EndsWith("Here on line 31")));
+ LogIfBugLine31(true);
+
+ // It's always a no-op if the condition is false.
+ LogIfBugLine31(false); // no-op
+ LogIfBugLine31(false); // no-op
+}
+
+TEST_F(BugUtilsTest, TestBugIfMessage) {
+ int i;
+
+ // Check success
+ LogIfBugNullCheckLine35(&i);
+
+ // Check failure
+ EXPECT_CALL(
+ *GetInstance(),
+ OnBug(
+ EndsWith("bug_utils_test_helper.h"), 35,
+ EndsWith(
+ "QUICHE_TEST_BUG_IF(Bug 35, ptr == nullptr): Here on line 35")));
+ LogIfBugNullCheckLine35(nullptr);
+}
+
+// Don't actually need to crash, just cause a side effect the test can assert
+// on.
+int num_times_called = 0;
+bool BadCondition() {
+ ++num_times_called;
+ return true;
+}
+
+TEST_F(BugUtilsTest, BadCondition) {
+ InSequence seq;
+
+ EXPECT_EQ(num_times_called, 0);
+
+ EXPECT_CALL(*GetInstance(), OnBug(_, _, EndsWith("Called BadCondition")));
+ QUICHE_TEST_BUG_IF(id, BadCondition()) << "Called BadCondition";
+ EXPECT_EQ(num_times_called, 1);
+}
+
+TEST_F(BugUtilsTest, NoDanglingElse) {
+ auto unexpected_bug_message = [] {
+ ADD_FAILURE() << "This should not be called";
+ return "bad";
+ };
+
+ if (false) QUICHE_TEST_BUG(dangling_else) << unexpected_bug_message();
+
+ bool expected_else_reached = false;
+ if (false)
+ QUICHE_TEST_BUG(dangling_else_2) << unexpected_bug_message();
+ else
+ expected_else_reached = true;
+
+ EXPECT_TRUE(expected_else_reached);
+}
+
+TEST_F(BugUtilsTest, BugListener) {
+ class TestListener : public GenericBugListener {
+ public:
+ explicit TestListener(bool expect_log_message)
+ : expect_log_message_(expect_log_message) {}
+
+ ~TestListener() override { EXPECT_EQ(hit_count_, 1); }
+
+ void OnBug(const char* bug_id, const char* file, int line,
+ absl::string_view bug_message) override {
+ ++hit_count_;
+ EXPECT_EQ(bug_id, "bug_listener_test");
+ EXPECT_EQ(file, __FILE__);
+ EXPECT_GT(line, 0);
+ if (expect_log_message_) {
+ EXPECT_EQ(bug_message, "TEST_BUG(bug_listener_test): Bug listener msg");
+ } else {
+ EXPECT_EQ(bug_message, "");
+ }
+ }
+
+ TestListener* self() { return this; }
+
+ private:
+ int hit_count_ = 0;
+ const bool expect_log_message_;
+ };
+
+ GENERIC_BUG_IMPL("TEST_BUG", bug_listener_test, /*skip_log_condition=*/false,
+ QUICHE_TEST_BUG_OPTIONS().SetBugListener(
+ TestListener(/*expect_log_message=*/true).self()))
+ << "Bug listener msg";
+
+ GENERIC_BUG_IMPL("TEST_BUG", bug_listener_test, /*skip_log_condition=*/true,
+ QUICHE_TEST_BUG_OPTIONS().SetBugListener(
+ TestListener(/*expect_log_message=*/false).self()))
+ << "Bug listener msg";
+}
+
+} // namespace
+} // namespace internal
+} // namespace quiche
diff --git a/quiche/common/bug_utils_test_helper.h b/quiche/common/bug_utils_test_helper.h
new file mode 100644
index 0000000..6497bca
--- /dev/null
+++ b/quiche/common/bug_utils_test_helper.h
@@ -0,0 +1,41 @@
+#ifndef QUICHE_COMMON_BUG_UTILS_TEST_HELPER_H_
+#define QUICHE_COMMON_BUG_UTILS_TEST_HELPER_H_
+
+#include "quiche/common/bug_utils.h"
+
+// Sticking various logging functions used by the test in a separate file,
+// so their line numbers are unlikely to change as we modify the test file
+// itself, as the expectations we set take the file + line numbers into account.
+
+#define QUICHE_TEST_BUG(bug_id) \
+ GENERIC_BUG_UNBRACED_ELSE_BLOCKER \
+ GENERIC_BUG_IMPL("QUICHE_TEST_BUG", bug_id, false, \
+ ::quiche::internal::DefaultBugOptions(__FILE__, __LINE__))
+
+#define QUICHE_TEST_BUG_IF(bug_id, condition) \
+ GENERIC_BUG_UNBRACED_ELSE_BLOCKER \
+ if (!(condition)) { /* Do nothing */ \
+ } else /* NOLINT */ \
+ GENERIC_BUG_IMPL("QUICHE_TEST_BUG", bug_id, false, \
+ ::quiche::internal::DefaultBugOptions(__FILE__, __LINE__) \
+ .SetCondition(#condition))
+
+inline void LogBugLine23() { QUICHE_TEST_BUG(Bug 23) << "Here on line 23"; }
+
+inline void LogBugLine26() {
+ QUICHE_TEST_BUG(Bug 26) << "Here on line 26";
+ QUICHE_TEST_BUG(Bug 27) << "And 27!";
+}
+
+inline void LogIfBugLine31(bool condition) {
+ QUICHE_TEST_BUG_IF(Bug 31, condition) << "Here on line 31";
+}
+
+inline void LogIfBugNullCheckLine35(int *ptr) {
+ QUICHE_TEST_BUG_IF(Bug 35, ptr == nullptr) << "Here on line 35";
+}
+
+#define QUICHE_TEST_BUG_OPTIONS() \
+ ::quiche::internal::DefaultBugOptions(__FILE__, __LINE__)
+
+#endif // QUICHE_COMMON_BUG_UTILS_TEST_HELPER_H_