This is an automated email from the ASF dual-hosted git repository.

ethanfeng pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/celeborn.git


The following commit(s) were added to refs/heads/main by this push:
     new b2b9a0ab4 [CELEBORN-1754][CIP-14] Add exceptions and checking utils to 
cppClient
b2b9a0ab4 is described below

commit b2b9a0ab4b8a8313b0d189bc5e03c22c10c4d93f
Author: HolyLow <[email protected]>
AuthorDate: Wed Dec 4 14:05:38 2024 +0800

    [CELEBORN-1754][CIP-14] Add exceptions and checking utils to cppClient
    
    ### What changes were proposed in this pull request?
    This PR adds exceptions and checking utils code to CppClient.
    Besides, the ctest framework is added to CppClient for UTs.
    
    ### Why are the changes needed?
    To provide exception utils and UT frmework to CppClient.
    
    ### Does this PR introduce _any_ user-facing change?
    No.
    
    ### How was this patch tested?
    Compilation and UTs.
    
    Closes #2966 from 
HolyLow/issue/celeborn-1754-add-exceptions-utils-to-cppClient.
    
    Authored-by: HolyLow <[email protected]>
    Signed-off-by: mingji <[email protected]>
---
 LICENSE                                       |   4 +-
 cpp/CMakeLists.txt                            |  11 +
 cpp/README.md                                 |   7 +-
 cpp/celeborn/utils/CMakeLists.txt             |   8 +-
 cpp/celeborn/utils/Exceptions.cpp             |  26 +
 cpp/celeborn/utils/Exceptions.h               | 421 ++++++++++++
 cpp/celeborn/utils/{ => tests}/CMakeLists.txt |  11 +-
 cpp/celeborn/utils/tests/ExceptionTest.cpp    | 932 ++++++++++++++++++++++++++
 cpp/scripts/setup-ubuntu.sh                   |  21 +-
 9 files changed, 1432 insertions(+), 9 deletions(-)

diff --git a/LICENSE b/LICENSE
index 0147fe37f..798e05593 100644
--- a/LICENSE
+++ b/LICENSE
@@ -266,8 +266,10 @@ Meta Velox
 ./cpp/celeborn/utils/StackTrace.cpp
 ./cpp/celeborn/utils/CelebornException.h
 ./cpp/celeborn/utils/CelebornException.cpp
+./cpp/celeborn/utils/Exceptions.h
+./cpp/celeborn/utils/Exceptions.cpp
 ./cpp/celeborn/utils/flags.cpp
-
+./cpp/celeborn/utils/tests/ExceptionTest.cpp
 
 
------------------------------------------------------------------------------------
 This product bundles various third-party components under the CC0 license.
diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt
index 1ad45f867..e3d869fbb 100644
--- a/cpp/CMakeLists.txt
+++ b/cpp/CMakeLists.txt
@@ -19,6 +19,7 @@ if (NOT DEFINED PACKAGE_VERSION)
 endif ()
 
 project("celeborn" VERSION ${PACKAGE_VERSION} LANGUAGES CXX C)
+enable_testing()
 
 set(CMAKE_CXX_STANDARD 17)
 set(CMAKE_CXX_STANDARD_REQUIRED True)
@@ -140,4 +141,14 @@ include_directories(SYSTEM ${OPENSSL_INCLUDE_DIR})
 include_directories(SYSTEM celeborn)
 include_directories(.)
 
+option(CELEBORN_BUILD_TESTS "CELEBORN_BUILD_TESTS" ON)
+if(CELEBORN_BUILD_TESTS)
+    ### TODO: we use prebuilt gtest prebuilt package here. A better way is
+    ###  to use source package, but the setting would be more complicated.
+    ###  Maybe we could change the method later.
+    find_package(GTest CONFIG REQUIRED)
+    # Include after project() but before add_subdirectory().
+    include(CTest)
+endif()
+
 add_subdirectory(celeborn)
diff --git a/cpp/README.md b/cpp/README.md
index aefb67c25..159ce5ab9 100644
--- a/cpp/README.md
+++ b/cpp/README.md
@@ -14,7 +14,7 @@ docker run \
     -w /celeborn \
     -it --rm \
     --name celeborn-cpp-dev-container \
-    holylow/celeborn-cpp-dev:0.1 \
+    holylow/celeborn-cpp-dev:0.2 \
     /bin/bash
 ```
 
@@ -38,13 +38,14 @@ bash setup-ubuntu.sh
 ```
 Other platforms are not supported yet, and you could use the container above 
as your dev environment.
 
-## Compile
+## Compile and test
 Currently, the modules are under development. 
-You could compile the code within the dev container by
+You could compile the code and run the tests within the dev container by
 ```
 cd celeborn/cpp
 mkdir -p build && cd build
 cmake ..
 make
+ctest
 ```
 
diff --git a/cpp/celeborn/utils/CMakeLists.txt 
b/cpp/celeborn/utils/CMakeLists.txt
index ed5340180..f040d73b7 100644
--- a/cpp/celeborn/utils/CMakeLists.txt
+++ b/cpp/celeborn/utils/CMakeLists.txt
@@ -12,7 +12,9 @@
 # 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.
-add_library(utils ProcessBase.cpp StackTrace.cpp CelebornException.cpp 
flags.cpp)
+add_library(
+        utils
+        ProcessBase.cpp StackTrace.cpp CelebornException.cpp Exceptions.cpp 
flags.cpp)
 
 target_link_libraries(
         utils
@@ -23,3 +25,7 @@ target_link_libraries(
         ${GLOG}
         ${GFLAGS_LIBRARIES}
 )
+
+if(CELEBORN_BUILD_TESTS)
+    add_subdirectory(tests)
+endif()
diff --git a/cpp/celeborn/utils/Exceptions.cpp 
b/cpp/celeborn/utils/Exceptions.cpp
new file mode 100644
index 000000000..202d50d14
--- /dev/null
+++ b/cpp/celeborn/utils/Exceptions.cpp
@@ -0,0 +1,26 @@
+/*
+ * Based on Exceptions.cpp from Facebook Velox
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * 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 "celeborn/utils/Exceptions.h"
+
+namespace celeborn::detail {
+
+CELEBORN_DEFINE_CHECK_FAIL_TEMPLATES(::celeborn::CelebornRuntimeError);
+CELEBORN_DEFINE_CHECK_FAIL_TEMPLATES(::celeborn::CelebornUserError);
+
+} // namespace celeborn::detail
diff --git a/cpp/celeborn/utils/Exceptions.h b/cpp/celeborn/utils/Exceptions.h
new file mode 100644
index 000000000..2a1e2fc98
--- /dev/null
+++ b/cpp/celeborn/utils/Exceptions.h
@@ -0,0 +1,421 @@
+/*
+ * Based on Exceptions.h from Facebook Velox
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * 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.
+ */
+
+#pragma once
+
+#include <memory>
+#include <sstream>
+
+#include <fmt/format.h>
+#include <fmt/ostream.h>
+#include <glog/logging.h>
+
+#include <folly/Conv.h>
+#include <folly/Exception.h>
+#include <folly/Preprocessor.h>
+
+#include "celeborn/utils/CelebornException.h"
+// TODO: maybe remove the file permanently...
+// #include "celeborn/utils/FmtStdFormatters.h"
+
+namespace celeborn {
+namespace detail {
+
+struct CelebornCheckFailArgs {
+  const char* file;
+  size_t line;
+  const char* function;
+  const char* expression;
+  const char* errorSource;
+  const char* errorCode;
+  bool isRetriable;
+};
+
+struct CompileTimeEmptyString {
+  CompileTimeEmptyString() = default;
+  constexpr operator const char*() const {
+    return "";
+  }
+  constexpr operator std::string_view() const {
+    return {};
+  }
+  operator std::string() const {
+    return {};
+  }
+};
+
+// celebornCheckFail is defined as a separate helper function rather than
+// a macro or inline `throw` expression to allow the compiler *not* to
+// inline it when it is large. Having an out-of-line error path helps
+// otherwise-small functions that call error-checking macros stay
+// small and thus stay eligible for inlining.
+template <typename Exception, typename StringType>
+[[noreturn]] void celebornCheckFail(
+    const CelebornCheckFailArgs& args,
+    StringType s) {
+  static_assert(
+      !std::is_same_v<StringType, std::string>,
+      "BUG: we should not pass std::string by value to celebornCheckFail");
+  if constexpr (!std::is_same_v<Exception, CelebornUserError>) {
+    LOG(ERROR) << "Line: " << args.file << ":" << args.line
+               << ", Function:" << args.function
+               << ", Expression: " << args.expression << " " << s
+               << ", Source: " << args.errorSource
+               << ", ErrorCode: " << args.errorCode;
+  }
+
+  ++threadNumCelebornThrow();
+  throw Exception(
+      args.file,
+      args.line,
+      args.function,
+      args.expression,
+      s,
+      args.errorSource,
+      args.errorCode,
+      args.isRetriable);
+}
+
+// CelebornCheckFailStringType helps us pass by reference to
+// celebornCheckFail exactly when the string type is std::string.
+template <typename T>
+struct CelebornCheckFailStringType;
+
+template <>
+struct CelebornCheckFailStringType<CompileTimeEmptyString> {
+  using type = CompileTimeEmptyString;
+};
+
+template <>
+struct CelebornCheckFailStringType<const char*> {
+  using type = const char*;
+};
+
+template <>
+struct CelebornCheckFailStringType<std::string> {
+  using type = const std::string&;
+};
+
+// Declare explicit instantiations of celebornCheckFail for the given
+// exceptionType. Just like normal function declarations (prototypes),
+// this allows the compiler to assume that they are defined elsewhere
+// and simply insert a function call for the linker to fix up, rather
+// than emitting a definition of these templates into every
+// translation unit they are used in.
+#define CELEBORN_DECLARE_CHECK_FAIL_TEMPLATES(exception_type)                  
        \
+  namespace detail {                                                          \
+  extern template void                                                        \
+  celebornCheckFail<exception_type, CompileTimeEmptyString>(                  \
+      const CelebornCheckFailArgs& args,                                      \
+      CompileTimeEmptyString);                                                \
+  extern template void celebornCheckFail<exception_type, const char*>(        \
+      const CelebornCheckFailArgs& args,                                      \
+      const char*);                                                           \
+  extern template void celebornCheckFail<exception_type, const std::string&>( \
+      const CelebornCheckFailArgs& args,                                      \
+      const std::string&);                                                    \
+  } // namespace detail
+
+// Definitions corresponding to CELEBORN_DECLARE_CHECK_FAIL_TEMPLATES. Should
+// only be used in Exceptions.cpp.
+#define CELEBORN_DEFINE_CHECK_FAIL_TEMPLATES(exception_type)                   
     \
+  template void celebornCheckFail<exception_type, CompileTimeEmptyString>( \
+      const CelebornCheckFailArgs& args, CompileTimeEmptyString);          \
+  template void celebornCheckFail<exception_type, const char*>(            \
+      const CelebornCheckFailArgs& args, const char*);                     \
+  template void celebornCheckFail<exception_type, const std::string&>(     \
+      const CelebornCheckFailArgs& args, const std::string&);
+
+// When there is no message passed, we can statically detect this case
+// and avoid passing even a single unnecessary argument pointer,
+// minimizing size and thus maximizing eligibility for inlining.
+inline CompileTimeEmptyString errorMessage() {
+  return {};
+}
+
+inline const char* errorMessage(const char* s) {
+  return s;
+}
+
+inline std::string errorMessage(const std::string& str) {
+  return str;
+}
+
+template <typename... Args>
+std::string errorMessage(fmt::string_view fmt, const Args&... args) {
+  return fmt::vformat(fmt, fmt::make_format_args(args...));
+}
+
+} // namespace detail
+
+#define _CELEBORN_THROW_IMPL(                                          \
+    exception, exprStr, errorSource, errorCode, isRetriable, ...)      \
+  {                                                                    \
+    /* GCC 9.2.1 doesn't accept this code with constexpr. */           \
+    static const ::celeborn::detail::CelebornCheckFailArgs             \
+        celebornCheckFailArgs = {                                      \
+            __FILE__,                                                  \
+            __LINE__,                                                  \
+            __FUNCTION__,                                              \
+            exprStr,                                                   \
+            errorSource,                                               \
+            errorCode,                                                 \
+            isRetriable};                                              \
+    auto message = ::celeborn::detail::errorMessage(__VA_ARGS__);      \
+    ::celeborn::detail::celebornCheckFail<                             \
+        exception,                                                     \
+        typename ::celeborn::detail::CelebornCheckFailStringType<      \
+            decltype(message)>::type>(celebornCheckFailArgs, message); \
+  }
+
+#define _CELEBORN_CHECK_AND_THROW_IMPL(                                        
\
+    expr, exprStr, exception, errorSource, errorCode, isRetriable, ...)        
\
+  if (UNLIKELY(!(expr))) {                                                     
\
+    _CELEBORN_THROW_IMPL(                                                      
\
+        exception, exprStr, errorSource, errorCode, isRetriable, __VA_ARGS__); 
\
+  }
+
+#define _CELEBORN_THROW(exception, ...) \
+  _CELEBORN_THROW_IMPL(exception, "", ##__VA_ARGS__)
+
+CELEBORN_DECLARE_CHECK_FAIL_TEMPLATES(::celeborn::CelebornRuntimeError);
+
+#define _CELEBORN_CHECK_IMPL(expr, exprStr, ...)             \
+  _CELEBORN_CHECK_AND_THROW_IMPL(                            \
+      expr,                                                  \
+      exprStr,                                               \
+      ::celeborn::CelebornRuntimeError,                      \
+      ::celeborn::error_source::kErrorSourceRuntime.c_str(), \
+      ::celeborn::error_code::kInvalidState.c_str(),         \
+      /* isRetriable */ false,                               \
+      ##__VA_ARGS__)
+
+// If the caller passes a custom message (4 *or more* arguments), we
+// have to construct a format string from ours ("({} vs. {})") plus
+// theirs by adding a space and shuffling arguments. If they don't (exactly 3
+// arguments), we can just pass our own format string and arguments straight
+// through.
+
+#define _CELEBORN_CHECK_OP_WITH_USER_FMT_HELPER( \
+    implmacro, expr1, expr2, op, user_fmt, ...)  \
+  implmacro(                                     \
+      (expr1)op(expr2),                          \
+      #expr1 " " #op " " #expr2,                 \
+      "({} vs. {}) " user_fmt,                   \
+      expr1,                                     \
+      expr2,                                     \
+      ##__VA_ARGS__)
+
+#define _CELEBORN_CHECK_OP_HELPER(implmacro, expr1, expr2, op, ...) \
+  if constexpr (FOLLY_PP_DETAIL_NARGS(__VA_ARGS__) > 0) {           \
+    _CELEBORN_CHECK_OP_WITH_USER_FMT_HELPER(                        \
+        implmacro, expr1, expr2, op, __VA_ARGS__);                  \
+  } else {                                                          \
+    implmacro(                                                      \
+        (expr1)op(expr2),                                           \
+        #expr1 " " #op " " #expr2,                                  \
+        "({} vs. {})",                                              \
+        expr1,                                                      \
+        expr2);                                                     \
+  }
+
+#define _CELEBORN_CHECK_OP(expr1, expr2, op, ...) \
+  _CELEBORN_CHECK_OP_HELPER(                      \
+      _CELEBORN_CHECK_IMPL, expr1, expr2, op, ##__VA_ARGS__)
+
+#define _CELEBORN_USER_CHECK_IMPL(expr, exprStr, ...)     \
+  _CELEBORN_CHECK_AND_THROW_IMPL(                         \
+      expr,                                               \
+      exprStr,                                            \
+      ::celeborn::CelebornUserError,                      \
+      ::celeborn::error_source::kErrorSourceUser.c_str(), \
+      ::celeborn::error_code::kInvalidArgument.c_str(),   \
+      /* isRetriable */ false,                            \
+      ##__VA_ARGS__)
+
+#define _CELEBORN_USER_CHECK_OP(expr1, expr2, op, ...) \
+  _CELEBORN_CHECK_OP_HELPER(                           \
+      _CELEBORN_USER_CHECK_IMPL, expr1, expr2, op, ##__VA_ARGS__)
+
+// For all below macros, an additional message can be passed using a
+// format string and arguments, as with `fmt::format`.
+#define CELEBORN_CHECK(expr, ...) \
+  _CELEBORN_CHECK_IMPL(expr, #expr, ##__VA_ARGS__)
+#define CELEBORN_CHECK_GT(e1, e2, ...) \
+  _CELEBORN_CHECK_OP(e1, e2, >, ##__VA_ARGS__)
+#define CELEBORN_CHECK_GE(e1, e2, ...) \
+  _CELEBORN_CHECK_OP(e1, e2, >=, ##__VA_ARGS__)
+#define CELEBORN_CHECK_LT(e1, e2, ...) \
+  _CELEBORN_CHECK_OP(e1, e2, <, ##__VA_ARGS__)
+#define CELEBORN_CHECK_LE(e1, e2, ...) \
+  _CELEBORN_CHECK_OP(e1, e2, <=, ##__VA_ARGS__)
+#define CELEBORN_CHECK_EQ(e1, e2, ...) \
+  _CELEBORN_CHECK_OP(e1, e2, ==, ##__VA_ARGS__)
+#define CELEBORN_CHECK_NE(e1, e2, ...) \
+  _CELEBORN_CHECK_OP(e1, e2, !=, ##__VA_ARGS__)
+#define CELEBORN_CHECK_NULL(e, ...) CELEBORN_CHECK(e == nullptr, ##__VA_ARGS__)
+#define CELEBORN_CHECK_NOT_NULL(e, ...) \
+  CELEBORN_CHECK(e != nullptr, ##__VA_ARGS__)
+
+#define CELEBORN_CHECK_OK(expr)                          \
+  do {                                                   \
+    ::celeborn::Status _s = (expr);                      \
+    _CELEBORN_CHECK_IMPL(_s.ok(), #expr, _s.toString()); \
+  } while (false)
+
+#define CELEBORN_UNSUPPORTED(...)                         \
+  _CELEBORN_THROW(                                        \
+      ::celeborn::CelebornUserError,                      \
+      ::celeborn::error_source::kErrorSourceUser.c_str(), \
+      ::celeborn::error_code::kUnsupported.c_str(),       \
+      /* isRetriable */ false,                            \
+      ##__VA_ARGS__)
+
+#define CELEBORN_ARITHMETIC_ERROR(...)                    \
+  _CELEBORN_THROW(                                        \
+      ::celeborn::CelebornUserError,                      \
+      ::celeborn::error_source::kErrorSourceUser.c_str(), \
+      ::celeborn::error_code::kArithmeticError.c_str(),   \
+      /* isRetriable */ false,                            \
+      ##__VA_ARGS__)
+
+#define CELEBORN_SCHEMA_MISMATCH_ERROR(...)               \
+  _CELEBORN_THROW(                                        \
+      ::celeborn::CelebornUserError,                      \
+      ::celeborn::error_source::kErrorSourceUser.c_str(), \
+      ::celeborn::error_code::kSchemaMismatch.c_str(),    \
+      /* isRetriable */ false,                            \
+      ##__VA_ARGS__)
+
+#define CELEBORN_FILE_NOT_FOUND_ERROR(...)                   \
+  _CELEBORN_THROW(                                           \
+      ::celeborn::CelebornRuntimeError,                      \
+      ::celeborn::error_source::kErrorSourceRuntime.c_str(), \
+      ::celeborn::error_code::kFileNotFound.c_str(),         \
+      /* isRetriable */ false,                               \
+      ##__VA_ARGS__)
+
+#define CELEBORN_UNREACHABLE(...)                            \
+  _CELEBORN_THROW(                                           \
+      ::celeborn::CelebornRuntimeError,                      \
+      ::celeborn::error_source::kErrorSourceRuntime.c_str(), \
+      ::celeborn::error_code::kUnreachableCode.c_str(),      \
+      /* isRetriable */ false,                               \
+      ##__VA_ARGS__)
+
+#ifndef NDEBUG
+#define CELEBORN_DCHECK(expr, ...) CELEBORN_CHECK(expr, ##__VA_ARGS__)
+#define CELEBORN_DCHECK_GT(e1, e2, ...) CELEBORN_CHECK_GT(e1, e2, 
##__VA_ARGS__)
+#define CELEBORN_DCHECK_GE(e1, e2, ...) CELEBORN_CHECK_GE(e1, e2, 
##__VA_ARGS__)
+#define CELEBORN_DCHECK_LT(e1, e2, ...) CELEBORN_CHECK_LT(e1, e2, 
##__VA_ARGS__)
+#define CELEBORN_DCHECK_LE(e1, e2, ...) CELEBORN_CHECK_LE(e1, e2, 
##__VA_ARGS__)
+#define CELEBORN_DCHECK_EQ(e1, e2, ...) CELEBORN_CHECK_EQ(e1, e2, 
##__VA_ARGS__)
+#define CELEBORN_DCHECK_NE(e1, e2, ...) CELEBORN_CHECK_NE(e1, e2, 
##__VA_ARGS__)
+#define CELEBORN_DCHECK_NULL(e, ...) CELEBORN_CHECK_NULL(e, ##__VA_ARGS__)
+#define CELEBORN_DCHECK_NOT_NULL(e, ...) \
+  CELEBORN_CHECK_NOT_NULL(e, ##__VA_ARGS__)
+#else
+#define CELEBORN_DCHECK(expr, ...) CELEBORN_CHECK(true)
+#define CELEBORN_DCHECK_GT(e1, e2, ...) CELEBORN_CHECK(true)
+#define CELEBORN_DCHECK_GE(e1, e2, ...) CELEBORN_CHECK(true)
+#define CELEBORN_DCHECK_LT(e1, e2, ...) CELEBORN_CHECK(true)
+#define CELEBORN_DCHECK_LE(e1, e2, ...) CELEBORN_CHECK(true)
+#define CELEBORN_DCHECK_EQ(e1, e2, ...) CELEBORN_CHECK(true)
+#define CELEBORN_DCHECK_NE(e1, e2, ...) CELEBORN_CHECK(true)
+#define CELEBORN_DCHECK_NULL(e, ...) CELEBORN_CHECK(true)
+#define CELEBORN_DCHECK_NOT_NULL(e, ...) CELEBORN_CHECK(true)
+#endif
+
+#define CELEBORN_FAIL(...)                                   \
+  _CELEBORN_THROW(                                           \
+      ::celeborn::CelebornRuntimeError,                      \
+      ::celeborn::error_source::kErrorSourceRuntime.c_str(), \
+      ::celeborn::error_code::kInvalidState.c_str(),         \
+      /* isRetriable */ false,                               \
+      ##__VA_ARGS__)
+
+CELEBORN_DECLARE_CHECK_FAIL_TEMPLATES(::celeborn::CelebornUserError);
+
+// For all below macros, an additional message can be passed using a
+// format string and arguments, as with `fmt::format`.
+#define CELEBORN_USER_CHECK(expr, ...) \
+  _CELEBORN_USER_CHECK_IMPL(expr, #expr, ##__VA_ARGS__)
+#define CELEBORN_USER_CHECK_GT(e1, e2, ...) \
+  _CELEBORN_USER_CHECK_OP(e1, e2, >, ##__VA_ARGS__)
+#define CELEBORN_USER_CHECK_GE(e1, e2, ...) \
+  _CELEBORN_USER_CHECK_OP(e1, e2, >=, ##__VA_ARGS__)
+#define CELEBORN_USER_CHECK_LT(e1, e2, ...) \
+  _CELEBORN_USER_CHECK_OP(e1, e2, <, ##__VA_ARGS__)
+#define CELEBORN_USER_CHECK_LE(e1, e2, ...) \
+  _CELEBORN_USER_CHECK_OP(e1, e2, <=, ##__VA_ARGS__)
+#define CELEBORN_USER_CHECK_EQ(e1, e2, ...) \
+  _CELEBORN_USER_CHECK_OP(e1, e2, ==, ##__VA_ARGS__)
+#define CELEBORN_USER_CHECK_NE(e1, e2, ...) \
+  _CELEBORN_USER_CHECK_OP(e1, e2, !=, ##__VA_ARGS__)
+#define CELEBORN_USER_CHECK_NULL(e, ...) \
+  CELEBORN_USER_CHECK(e == nullptr, ##__VA_ARGS__)
+#define CELEBORN_USER_CHECK_NOT_NULL(e, ...) \
+  CELEBORN_USER_CHECK(e != nullptr, ##__VA_ARGS__)
+
+#ifndef NDEBUG
+#define CELEBORN_USER_DCHECK(expr, ...) CELEBORN_USER_CHECK(expr, 
##__VA_ARGS__)
+#define CELEBORN_USER_DCHECK_GT(e1, e2, ...) \
+  CELEBORN_USER_CHECK_GT(e1, e2, ##__VA_ARGS__)
+#define CELEBORN_USER_DCHECK_GE(e1, e2, ...) \
+  CELEBORN_USER_CHECK_GE(e1, e2, ##__VA_ARGS__)
+#define CELEBORN_USER_DCHECK_LT(e1, e2, ...) \
+  CELEBORN_USER_CHECK_LT(e1, e2, ##__VA_ARGS__)
+#define CELEBORN_USER_DCHECK_LE(e1, e2, ...) \
+  CELEBORN_USER_CHECK_LE(e1, e2, ##__VA_ARGS__)
+#define CELEBORN_USER_DCHECK_EQ(e1, e2, ...) \
+  CELEBORN_USER_CHECK_EQ(e1, e2, ##__VA_ARGS__)
+#define CELEBORN_USER_DCHECK_NE(e1, e2, ...) \
+  CELEBORN_USER_CHECK_NE(e1, e2, ##__VA_ARGS__)
+#define CELEBORN_USER_DCHECK_NOT_NULL(e, ...) \
+  CELEBORN_USER_CHECK_NOT_NULL(e, ##__VA_ARGS__)
+#define CELEBORN_USER_DCHECK_NULL(e, ...) \
+  CELEBORN_USER_CHECK_NULL(e, ##__VA_ARGS__)
+#else
+#define CELEBORN_USER_DCHECK(expr, ...) CELEBORN_USER_CHECK(true)
+#define CELEBORN_USER_DCHECK_GT(e1, e2, ...) CELEBORN_USER_CHECK(true)
+#define CELEBORN_USER_DCHECK_GE(e1, e2, ...) CELEBORN_USER_CHECK(true)
+#define CELEBORN_USER_DCHECK_LT(e1, e2, ...) CELEBORN_USER_CHECK(true)
+#define CELEBORN_USER_DCHECK_LE(e1, e2, ...) CELEBORN_USER_CHECK(true)
+#define CELEBORN_USER_DCHECK_EQ(e1, e2, ...) CELEBORN_USER_CHECK(true)
+#define CELEBORN_USER_DCHECK_NE(e1, e2, ...) CELEBORN_USER_CHECK(true)
+#define CELEBORN_USER_DCHECK_NULL(e, ...) CELEBORN_USER_CHECK(true)
+#define CELEBORN_USER_DCHECK_NOT_NULL(e, ...) CELEBORN_USER_CHECK(true)
+#endif
+
+#define CELEBORN_USER_FAIL(...)                           \
+  _CELEBORN_THROW(                                        \
+      ::celeborn::CelebornUserError,                      \
+      ::celeborn::error_source::kErrorSourceUser.c_str(), \
+      ::celeborn::error_code::kInvalidArgument.c_str(),   \
+      /* isRetriable */ false,                            \
+      ##__VA_ARGS__)
+
+#define CELEBORN_NYI(...)                                    \
+  _CELEBORN_THROW(                                           \
+      ::celeborn::CelebornRuntimeError,                      \
+      ::celeborn::error_source::kErrorSourceRuntime.c_str(), \
+      ::celeborn::error_code::kNotImplemented.c_str(),       \
+      /* isRetriable */ false,                               \
+      ##__VA_ARGS__)
+
+} // namespace celeborn
diff --git a/cpp/celeborn/utils/CMakeLists.txt 
b/cpp/celeborn/utils/tests/CMakeLists.txt
similarity index 80%
copy from cpp/celeborn/utils/CMakeLists.txt
copy to cpp/celeborn/utils/tests/CMakeLists.txt
index ed5340180..a820b4ac1 100644
--- a/cpp/celeborn/utils/CMakeLists.txt
+++ b/cpp/celeborn/utils/tests/CMakeLists.txt
@@ -12,9 +12,14 @@
 # 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.
-add_library(utils ProcessBase.cpp StackTrace.cpp CelebornException.cpp 
flags.cpp)
+
+add_executable(celeborn_utils_test ExceptionTest.cpp)
+
+add_test(NAME celeborn_utils_test COMMAND celeborn_utils_test)
 
 target_link_libraries(
+        celeborn_utils_test
+        PRIVATE
         utils
         ${WANGLE}
         ${FIZZ}
@@ -22,4 +27,6 @@ target_link_libraries(
         ${FOLLY_WITH_DEPENDENCIES}
         ${GLOG}
         ${GFLAGS_LIBRARIES}
-)
+        GTest::gtest
+        GTest::gmock
+        GTest::gtest_main)
diff --git a/cpp/celeborn/utils/tests/ExceptionTest.cpp 
b/cpp/celeborn/utils/tests/ExceptionTest.cpp
new file mode 100644
index 000000000..b1dc302ef
--- /dev/null
+++ b/cpp/celeborn/utils/tests/ExceptionTest.cpp
@@ -0,0 +1,932 @@
+/*
+ * Based on ExceptionTest.cpp from Facebook Velox
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * 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 <fmt/format.h>
+#include <folly/Random.h>
+#include <gtest/gtest.h>
+
+#include "celeborn/utils/Exceptions.h"
+
+using namespace celeborn;
+
+struct Counter {
+  mutable int counter = 0;
+};
+
+std::ostream& operator<<(std::ostream& os, const Counter& c) {
+  os << c.counter;
+  ++c.counter;
+  return os;
+}
+
+template <>
+struct fmt::formatter<Counter> {
+  constexpr auto parse(format_parse_context& ctx) {
+    return ctx.begin();
+  }
+
+  template <typename FormatContext>
+  auto format(const Counter& c, FormatContext& ctx) const {
+    auto x = c.counter++;
+    return format_to(ctx.out(), "{}", x);
+  }
+};
+
+template <typename T>
+void verifyException(
+    std::function<void()> f,
+    std::function<void(const T&)> exceptionVerifier) {
+  try {
+    f();
+    FAIL() << "Expected exception of type " << typeid(T).name()
+           << ", but no exception was thrown.";
+  } catch (const T& e) {
+    exceptionVerifier(e);
+  } catch (...) {
+    FAIL() << "Expected exception of type " << typeid(T).name()
+           << ", but instead got an exception of a different type.";
+  }
+}
+
+void verifyCelebornException(
+    std::function<void()> f,
+    const std::string& messagePrefix) {
+  verifyException<CelebornException>(f, [&messagePrefix](const auto& e) {
+    EXPECT_TRUE(folly::StringPiece{e.what()}.startsWith(messagePrefix))
+        << "\nException message prefix mismatch.\n\nExpected prefix: "
+        << messagePrefix << "\n\nActual message: " << e.what();
+  });
+}
+
+void testExceptionTraceCollectionControl(bool userException, bool enabled) {
+  // Disable rate control in the test.
+  FLAGS_celeborn_exception_user_stacktrace_rate_limit_ms = 0;
+  FLAGS_celeborn_exception_system_stacktrace_rate_limit_ms = 0;
+
+  if (userException) {
+    FLAGS_celeborn_exception_user_stacktrace_enabled = enabled ? true : false;
+    FLAGS_celeborn_exception_system_stacktrace_enabled =
+        folly::Random::oneIn(2);
+  } else {
+    FLAGS_celeborn_exception_system_stacktrace_enabled = enabled ? true : 
false;
+    FLAGS_celeborn_exception_user_stacktrace_enabled = folly::Random::oneIn(2);
+  }
+  try {
+    if (userException) {
+      throw CelebornUserError(
+          "file_name",
+          1,
+          "function_name()",
+          "operator()",
+          "test message",
+          "",
+          error_code::kArithmeticError,
+          false);
+    } else {
+      throw CelebornRuntimeError(
+          "file_name",
+          1,
+          "function_name()",
+          "operator()",
+          "test message",
+          "",
+          error_code::kArithmeticError,
+          false);
+    }
+  } catch (CelebornException& e) {
+    SCOPED_TRACE(fmt::format(
+        "enabled: {}, user flag: {}, sys flag: {}",
+        enabled,
+        FLAGS_celeborn_exception_user_stacktrace_enabled,
+        FLAGS_celeborn_exception_system_stacktrace_enabled));
+    ASSERT_EQ(
+        userException, e.exceptionType() == CelebornException::Type::kUser);
+    ASSERT_EQ(enabled, e.stackTrace() != nullptr);
+  }
+}
+
+void testExceptionTraceCollectionRateControl(
+    bool userException,
+    bool hasRateLimit) {
+  // Disable rate control in the test.
+  // Enable trace rate control in the test.
+  FLAGS_celeborn_exception_user_stacktrace_enabled = true;
+  FLAGS_celeborn_exception_system_stacktrace_enabled = true;
+  // Set rate control interval to a large value to avoid time related test
+  // flakiness.
+  const int kRateLimitIntervalMs = 4000;
+  if (hasRateLimit) {
+    // Wait a bit to ensure that the last stack trace collection time has
+    // passed sufficient long.
+    /* sleep override */
+    std::this_thread::sleep_for(
+        std::chrono::milliseconds(kRateLimitIntervalMs)); // NOLINT
+  }
+  if (userException) {
+    FLAGS_celeborn_exception_user_stacktrace_rate_limit_ms =
+        hasRateLimit ? kRateLimitIntervalMs : 0;
+    FLAGS_celeborn_exception_system_stacktrace_rate_limit_ms =
+        folly::Random::rand32();
+  } else {
+    // Set rate control to a large interval to avoid time related test
+    // flakiness.
+    FLAGS_celeborn_exception_system_stacktrace_rate_limit_ms =
+        hasRateLimit ? kRateLimitIntervalMs : 0;
+    FLAGS_celeborn_exception_user_stacktrace_rate_limit_ms =
+        folly::Random::rand32();
+  }
+  for (int iter = 0; iter < 3; ++iter) {
+    try {
+      if (userException) {
+        throw CelebornUserError(
+            "file_name",
+            1,
+            "function_name()",
+            "operator()",
+            "test message",
+            "",
+            error_code::kArithmeticError,
+            false);
+      } else {
+        throw CelebornRuntimeError(
+            "file_name",
+            1,
+            "function_name()",
+            "operator()",
+            "test message",
+            "",
+            error_code::kArithmeticError,
+            false);
+      }
+    } catch (CelebornException& e) {
+      SCOPED_TRACE(fmt::format(
+          "userException: {}, hasRateLimit: {}, user limit: {}ms, sys limit: 
{}ms",
+          userException,
+          hasRateLimit,
+          FLAGS_celeborn_exception_user_stacktrace_rate_limit_ms,
+          FLAGS_celeborn_exception_system_stacktrace_rate_limit_ms));
+      ASSERT_EQ(
+          userException, e.exceptionType() == CelebornException::Type::kUser);
+      ASSERT_EQ(!hasRateLimit || ((iter % 2) == 0), e.stackTrace() != nullptr);
+      // NOTE: with rate limit control, we want to verify if we can collect
+      // stack trace after waiting for a while.
+      if (hasRateLimit && (iter % 2 != 0)) {
+        /* sleep override */
+        std::this_thread::sleep_for(
+            std::chrono::milliseconds(kRateLimitIntervalMs)); // NOLINT
+      }
+    }
+  }
+}
+
+// Ensures that expressions on the stream are not evaluated unless the 
condition
+// is met.
+TEST(ExceptionTest, lazyStreamEvaluation) {
+  Counter c;
+
+  EXPECT_EQ(0, c.counter);
+  CELEBORN_CHECK(true, "{}", c);
+  EXPECT_EQ(0, c.counter);
+
+  EXPECT_THROW(
+      ([&]() { CELEBORN_CHECK(false, "{}", c); })(), CelebornRuntimeError);
+  EXPECT_EQ(1, c.counter);
+
+  CELEBORN_CHECK(true, "{}", c);
+  EXPECT_EQ(1, c.counter);
+
+  EXPECT_THROW(
+      ([&]() { CELEBORN_USER_CHECK(false, "{}", c); })(), CelebornUserError);
+  EXPECT_EQ(2, c.counter);
+
+  EXPECT_THROW(
+      ([&]() { CELEBORN_CHECK(false, "{}", c); })(), CelebornRuntimeError);
+  EXPECT_EQ(3, c.counter);
+
+  // Simple types.
+  size_t i = 0;
+  CELEBORN_CHECK(true, "{}", i++);
+  EXPECT_EQ(0, i);
+  CELEBORN_CHECK(true, "{}", ++i);
+  EXPECT_EQ(0, i);
+
+  EXPECT_THROW(
+      ([&]() { CELEBORN_CHECK(false, "{}", i++); })(), CelebornRuntimeError);
+  EXPECT_EQ(1, i);
+  EXPECT_THROW(
+      ([&]() { CELEBORN_CHECK(false, "{}", ++i); })(), CelebornRuntimeError);
+  EXPECT_EQ(2, i);
+}
+
+TEST(ExceptionTest, messageCheck) {
+  verifyCelebornException(
+      []() { CELEBORN_CHECK(4 > 5, "Test message 1"); },
+      "Exception: CelebornRuntimeError\nError Source: RUNTIME\n"
+      "Error Code: INVALID_STATE\nReason: Test message 1\n"
+      "Retriable: False\nExpression: 4 > 5\nFunction: operator()\nFile: ");
+}
+
+TEST(ExceptionTest, messageUnreachable) {
+  verifyCelebornException(
+      []() { CELEBORN_UNREACHABLE("Test message 3"); },
+      "Exception: CelebornRuntimeError\nError Source: RUNTIME\n"
+      "Error Code: UNREACHABLE_CODE\nReason: Test message 3\n"
+      "Retriable: False\nFunction: operator()\nFile: ");
+}
+
+#define RUN_TEST(test)            \
+  TEST_##test(                    \
+      CELEBORN_CHECK_##test,      \
+      "RUNTIME",                  \
+      "INVALID_STATE",            \
+      "CelebornRuntimeError");    \
+  TEST_##test(                    \
+      CELEBORN_USER_CHECK_##test, \
+      "USER",                     \
+      "INVALID_ARGUMENT",         \
+      "CelebornUserError");
+
+#define TEST_GT(macro, system, code, prefix)                               \
+  verifyCelebornException(                                                 \
+      []() { macro(4, 5); },                                               \
+      "Exception: " prefix "\nError Source: " system "\nError Code: " code \
+      "\nReason: (4 vs. 5)"                                                \
+      "\nRetriable: False"                                                 \
+      "\nExpression: 4 > 5"                                                \
+      "\nFunction: operator()"                                             \
+      "\nFile: ");                                                         \
+                                                                           \
+  verifyCelebornException(                                                 \
+      []() { macro(3, 3); },                                               \
+      "Exception: " prefix "\nError Source: " system "\nError Code: " code \
+      "\nReason: (3 vs. 3)"                                                \
+      "\nRetriable: False"                                                 \
+      "\nExpression: 3 > 3"                                                \
+      "\nFunction: operator()"                                             \
+      "\nFile: ");                                                         \
+                                                                           \
+  verifyCelebornException(                                                 \
+      []() { macro(-1, 1, "Message 1"); },                                 \
+      "Exception: " prefix "\nError Source: " system "\nError Code: " code \
+      "\nReason: (-1 vs. 1) Message 1"                                     \
+      "\nRetriable: False"                                                 \
+      "\nExpression: -1 > 1"                                               \
+      "\nFunction: operator()"                                             \
+      "\nFile: ");                                                         \
+                                                                           \
+  macro(3, 2);                                                             \
+  macro(1, -1, "Message 2");
+
+TEST(ExceptionTest, greaterThan) {
+  RUN_TEST(GT);
+}
+
+#define TEST_GE(macro, system, code, prefix)                               \
+  verifyCelebornException(                                                 \
+      []() { macro(4, 5); },                                               \
+      "Exception: " prefix "\nError Source: " system "\nError Code: " code \
+      "\nReason: (4 vs. 5)"                                                \
+      "\nRetriable: False"                                                 \
+      "\nExpression: 4 >= 5"                                               \
+      "\nFunction: operator()"                                             \
+      "\nFile: ");                                                         \
+                                                                           \
+  verifyCelebornException(                                                 \
+      []() { macro(-1, 1, "Message 1"); },                                 \
+      "Exception: " prefix "\nError Source: " system "\nError Code: " code \
+      "\nReason: (-1 vs. 1) Message 1"                                     \
+      "\nRetriable: False"                                                 \
+      "\nExpression: -1 >= 1"                                              \
+      "\nFunction: operator()"                                             \
+      "\nFile: ");                                                         \
+                                                                           \
+  macro(3, 2);                                                             \
+  macro(3, 3);                                                             \
+  macro(1, -1, "Message 2");
+
+TEST(ExceptionTest, greaterEqual) {
+  RUN_TEST(GE);
+}
+
+#define TEST_LT(macro, system, code, prefix)                               \
+  verifyCelebornException(                                                 \
+      []() { macro(5, 4); },                                               \
+      "Exception: " prefix "\nError Source: " system "\nError Code: " code \
+      "\nReason: (5 vs. 4)"                                                \
+      "\nRetriable: False"                                                 \
+      "\nExpression: 5 < 4"                                                \
+      "\nFunction: operator()"                                             \
+      "\nFile: ");                                                         \
+                                                                           \
+  verifyCelebornException(                                                 \
+      []() { macro(2, 2); },                                               \
+      "Exception: " prefix "\nError Source: " system "\nError Code: " code \
+      "\nReason: (2 vs. 2)"                                                \
+      "\nRetriable: False"                                                 \
+      "\nExpression: 2 < 2"                                                \
+      "\nFunction: operator()"                                             \
+      "\nFile: ");                                                         \
+                                                                           \
+  verifyCelebornException(                                                 \
+      []() { macro(1, -1, "Message 1"); },                                 \
+      "Exception: " prefix "\nError Source: " system "\nError Code: " code \
+      "\nReason: (1 vs. -1) Message 1"                                     \
+      "\nRetriable: False"                                                 \
+      "\nExpression: 1 < -1"                                               \
+      "\nFunction: operator()"                                             \
+      "\nFile: ");                                                         \
+                                                                           \
+  macro(2, 3);                                                             \
+  macro(-1, 1, "Message 2");
+
+TEST(ExceptionTest, lessThan) {
+  RUN_TEST(LT);
+}
+
+#define TEST_LE(macro, system, code, prefix)                               \
+  verifyCelebornException(                                                 \
+      []() { macro(6, 2); },                                               \
+      "Exception: " prefix "\nError Source: " system "\nError Code: " code \
+      "\nReason: (6 vs. 2)"                                                \
+      "\nRetriable: False"                                                 \
+      "\nExpression: 6 <= 2"                                               \
+      "\nFunction: operator()"                                             \
+      "\nFile: ");                                                         \
+                                                                           \
+  verifyCelebornException(                                                 \
+      []() { macro(3, -3, "Message 1"); },                                 \
+      "Exception: " prefix "\nError Source: " system "\nError Code: " code \
+      "\nReason: (3 vs. -3) Message 1"                                     \
+      "\nRetriable: False"                                                 \
+      "\nExpression: 3 <= -3"                                              \
+      "\nFunction: operator()"                                             \
+      "\nFile: ");                                                         \
+                                                                           \
+  macro(5, 54);                                                            \
+  macro(1, 1);                                                             \
+  macro(-3, 3, "Message 2");
+
+TEST(ExceptionTest, lessEqual) {
+  RUN_TEST(LE);
+}
+
+#define TEST_EQ(macro, system, code, prefix)                                 \
+  {                                                                          \
+    verifyCelebornException(                                                 \
+        []() { macro(1, 2); },                                               \
+        "Exception: " prefix "\nError Source: " system "\nError Code: " code \
+        "\nReason: (1 vs. 2)"                                                \
+        "\nRetriable: False"                                                 \
+        "\nExpression: 1 == 2"                                               \
+        "\nFunction: operator()"                                             \
+        "\nFile: ");                                                         \
+                                                                             \
+    verifyCelebornException(                                                 \
+        []() { macro(2, 1, "Message 1"); },                                  \
+        "Exception: " prefix "\nError Source: " system "\nError Code: " code \
+        "\nReason: (2 vs. 1) Message 1"                                      \
+        "\nRetriable: False"                                                 \
+        "\nExpression: 2 == 1"                                               \
+        "\nFunction: operator()"                                             \
+        "\nFile: ");                                                         \
+                                                                             \
+    auto t = true;                                                           \
+    auto f = false;                                                          \
+    macro(521, 521);                                                         \
+    macro(1.1, 1.1);                                                         \
+    macro(true, t, "Message 2");                                             \
+    macro(f, false, "Message 3");                                            \
+  }
+
+TEST(ExceptionTest, equal) {
+  RUN_TEST(EQ);
+}
+
+#define TEST_NE(macro, system, code, prefix)                                 \
+  {                                                                          \
+    verifyCelebornException(                                                 \
+        []() { macro(1, 1); },                                               \
+        "Exception: " prefix "\nError Source: " system "\nError Code: " code \
+        "\nReason: (1 vs. 1)"                                                \
+        "\nRetriable: False"                                                 \
+        "\nExpression: 1 != 1"                                               \
+        "\nFunction: operator()"                                             \
+        "\nFile: ");                                                         \
+                                                                             \
+    verifyCelebornException(                                                 \
+        []() { macro(2.2, 2.2, "Message 1"); },                              \
+        "Exception: " prefix "\nError Source: " system "\nError Code: " code \
+        "\nReason: (2.2 vs. 2.2) Message 1"                                  \
+        "\nRetriable: False"                                                 \
+        "\nExpression: 2.2 != 2.2"                                           \
+        "\nFunction: operator()"                                             \
+        "\nFile: ");                                                         \
+                                                                             \
+    auto t = true;                                                           \
+    auto f = false;                                                          \
+    macro(521, 522);                                                         \
+    macro(1.2, 1.1);                                                         \
+    macro(true, f, "Message 2");                                             \
+    macro(t, false, "Message 3");                                            \
+  }
+
+TEST(ExceptionTest, notEqual) {
+  RUN_TEST(NE);
+}
+
+#define TEST_NOT_NULL(macro, system, code, prefix)                           \
+  {                                                                          \
+    verifyCelebornException(                                                 \
+        []() { macro(nullptr); },                                            \
+        "Exception: " prefix "\nError Source: " system "\nError Code: " code \
+        "\nRetriable: False"                                                 \
+        "\nExpression: nullptr != nullptr"                                   \
+        "\nFunction: operator()"                                             \
+        "\nFile: ");                                                         \
+    verifyCelebornException(                                                 \
+        []() {                                                               \
+          std::shared_ptr<int> a;                                            \
+          macro(a, "Message 1");                                             \
+        },                                                                   \
+        "Exception: " prefix "\nError Source: " system "\nError Code: " code \
+        "\nReason: Message 1"                                                \
+        "\nRetriable: False"                                                 \
+        "\nExpression: a != nullptr"                                         \
+        "\nFunction: operator()"                                             \
+        "\nFile: ");                                                         \
+    auto b = std::make_shared<int>(5);                                       \
+    macro(b);                                                                \
+  }
+
+TEST(ExceptionTest, notNull) {
+  RUN_TEST(NOT_NULL);
+}
+
+TEST(ExceptionTest, expressionString) {
+  size_t i = 1;
+  size_t j = 100;
+  constexpr auto msgTemplate =
+      "Exception: CelebornRuntimeError"
+      "\nError Source: RUNTIME"
+      "\nError Code: INVALID_STATE"
+      "\nReason: ({1})"
+      "\nRetriable: False"
+      "\nExpression: {0}"
+      "\nFunction: operator()"
+      "\nFile: ";
+
+  verifyCelebornException(
+      [&]() { CELEBORN_CHECK_EQ(i, j); },
+      fmt::format(msgTemplate, "i == j", "1 vs. 100"));
+
+  verifyCelebornException(
+      [&]() { CELEBORN_CHECK_NE(i, 1); },
+      fmt::format(msgTemplate, "i != 1", "1 vs. 1"));
+
+  verifyCelebornException(
+      [&]() { CELEBORN_CHECK_LT(i + j, j); },
+      fmt::format(msgTemplate, "i + j < j", "101 vs. 100"));
+
+  verifyCelebornException(
+      [&]() { CELEBORN_CHECK_GE(i + j * 2, 1000); },
+      fmt::format(msgTemplate, "i + j * 2 >= 1000", "201 vs. 1000"));
+}
+
+TEST(ExceptionTest, notImplemented) {
+  verifyCelebornException(
+      []() { CELEBORN_NYI(); },
+      "Exception: CelebornRuntimeError\nError Source: RUNTIME\n"
+      "Error Code: NOT_IMPLEMENTED\n"
+      "Retriable: False\nFunction: operator()\nFile: ");
+
+  verifyCelebornException(
+      []() { CELEBORN_NYI("Message 1"); },
+      "Exception: CelebornRuntimeError\nError Source: RUNTIME\n"
+      "Error Code: NOT_IMPLEMENTED\nReason: Message 1\nRetriable: False\n"
+      "Function: operator()\nFile: ");
+}
+
+TEST(ExceptionTest, errorCode) {
+  std::string msgTemplate =
+      "Exception: {}"
+      "\nError Source: {}"
+      "\nError Code: {}"
+      "\nRetriable: {}"
+      "\nExpression: {}"
+      "\nFunction: {}"
+      "\nFile: ";
+
+  verifyCelebornException(
+      [&]() { CELEBORN_FAIL(); },
+      fmt::format(
+          "Exception: {}"
+          "\nError Source: {}"
+          "\nError Code: {}"
+          "\nRetriable: {}"
+          "\nFunction: {}"
+          "\nFile: ",
+          "CelebornRuntimeError",
+          "RUNTIME",
+          "INVALID_STATE",
+          "False",
+          "operator()"));
+
+  verifyCelebornException(
+      [&]() { CELEBORN_USER_FAIL(); },
+      fmt::format(
+          "Exception: {}"
+          "\nError Source: {}"
+          "\nError Code: {}"
+          "\nRetriable: {}"
+          "\nFunction: {}"
+          "\nFile: ",
+          "CelebornUserError",
+          "USER",
+          "INVALID_ARGUMENT",
+          "False",
+          "operator()"));
+}
+
+TEST(ExceptionTest, context) {
+  // No context.
+  verifyCelebornException(
+      [&]() { CELEBORN_CHECK_EQ(1, 3); },
+      "Exception: CelebornRuntimeError"
+      "\nError Source: RUNTIME"
+      "\nError Code: INVALID_STATE"
+      "\nReason: (1 vs. 3)"
+      "\nRetriable: False"
+      "\nExpression: 1 == 3"
+      "\nFunction: operator()"
+      "\nFile: ");
+
+  // With context.
+  int callCount = 0;
+
+  struct MessageFunctionArg {
+    std::string message;
+    int* callCount;
+  };
+
+  auto messageFunction = [](celeborn::CelebornException::Type exceptionType,
+                            void* untypedArg) {
+    auto arg = static_cast<MessageFunctionArg*>(untypedArg);
+    ++(*arg->callCount);
+    switch (exceptionType) {
+      case celeborn::CelebornException::Type::kUser:
+        return fmt::format("User error: {}", arg->message);
+      case celeborn::CelebornException::Type::kSystem:
+        return fmt::format("System error: {}", arg->message);
+      default:
+        return fmt::format("Unexpected error type: {}", arg->message);
+    }
+  };
+
+  {
+    // Create multi-layer contexts with top level marked as essential.
+    MessageFunctionArg topLevelTroubleshootingAid{
+        "Top-level troubleshooting aid.", &callCount};
+    celeborn::ExceptionContextSetter additionalContext(
+        {.messageFunc = messageFunction, .arg = &topLevelTroubleshootingAid});
+
+    MessageFunctionArg midLevelTroubleshootingAid{
+        "Mid-level troubleshooting aid.", &callCount};
+    celeborn::ExceptionContextSetter midLevelContext(
+        {messageFunction, &midLevelTroubleshootingAid});
+
+    MessageFunctionArg innerLevelTroubleshootingAid{
+        "Inner-level troubleshooting aid.", &callCount};
+    celeborn::ExceptionContextSetter innerLevelContext(
+        {messageFunction, &innerLevelTroubleshootingAid});
+
+    verifyCelebornException(
+        [&]() { CELEBORN_CHECK_EQ(1, 3); },
+        "Exception: CelebornRuntimeError"
+        "\nError Source: RUNTIME"
+        "\nError Code: INVALID_STATE"
+        "\nReason: (1 vs. 3)"
+        "\nRetriable: False"
+        "\nExpression: 1 == 3"
+        "\nContext: System error: Inner-level troubleshooting aid."
+        "\nTop-Level Context: System error: Top-level troubleshooting aid."
+        "\nFunction: operator()"
+        "\nFile: ");
+
+    EXPECT_EQ(2, callCount);
+
+    verifyCelebornException(
+        [&]() { CELEBORN_USER_CHECK_EQ(1, 3); },
+        "Exception: CelebornUserError"
+        "\nError Source: USER"
+        "\nError Code: INVALID_ARGUMENT"
+        "\nReason: (1 vs. 3)"
+        "\nRetriable: False"
+        "\nExpression: 1 == 3"
+        "\nContext: User error: Inner-level troubleshooting aid."
+        "\nTop-Level Context: User error: Top-level troubleshooting aid."
+        "\nFunction: operator()"
+        "\nFile: ");
+
+    EXPECT_EQ(4, callCount);
+  }
+
+  {
+    callCount = 0;
+    // Create multi-layer contexts with none marked as essential.
+    MessageFunctionArg topLevelTroubleshootingAid{
+        "Top-level troubleshooting aid.", &callCount};
+    celeborn::ExceptionContextSetter additionalContext(
+        {.messageFunc = messageFunction, .arg = &topLevelTroubleshootingAid});
+
+    MessageFunctionArg midLevelTroubleshootingAid{
+        "Mid-level troubleshooting aid.", &callCount};
+    celeborn::ExceptionContextSetter midLevelContext(
+        {.messageFunc = messageFunction, .arg = &midLevelTroubleshootingAid});
+
+    MessageFunctionArg innerLevelTroubleshootingAid{
+        "Inner-level troubleshooting aid.", &callCount};
+    celeborn::ExceptionContextSetter innerLevelContext(
+        {messageFunction, &innerLevelTroubleshootingAid});
+
+    verifyCelebornException(
+        [&]() { CELEBORN_CHECK_EQ(1, 3); },
+        "Exception: CelebornRuntimeError"
+        "\nError Source: RUNTIME"
+        "\nError Code: INVALID_STATE"
+        "\nReason: (1 vs. 3)"
+        "\nRetriable: False"
+        "\nExpression: 1 == 3"
+        "\nContext: System error: Inner-level troubleshooting aid."
+        "\nTop-Level Context: System error: Top-level troubleshooting aid."
+        "\nFunction: operator()"
+        "\nFile: ");
+
+    EXPECT_EQ(2, callCount);
+
+    verifyCelebornException(
+        [&]() { CELEBORN_USER_CHECK_EQ(1, 3); },
+        "Exception: CelebornUserError"
+        "\nError Source: USER"
+        "\nError Code: INVALID_ARGUMENT"
+        "\nReason: (1 vs. 3)"
+        "\nRetriable: False"
+        "\nExpression: 1 == 3"
+        "\nContext: User error: Inner-level troubleshooting aid."
+        "\nTop-Level Context: User error: Top-level troubleshooting aid."
+        "\nFunction: operator()"
+        "\nFile: ");
+
+    EXPECT_EQ(4, callCount);
+  }
+
+  // Different context.
+  {
+    callCount = 0;
+
+    // Create a single layer of context. Context and top-level context are
+    // expected to be the same.
+    MessageFunctionArg debuggingInfo{"Debugging info.", &callCount};
+    celeborn::ExceptionContextSetter context({messageFunction, 
&debuggingInfo});
+
+    verifyCelebornException(
+        [&]() { CELEBORN_CHECK_EQ(1, 3); },
+        "Exception: CelebornRuntimeError"
+        "\nError Source: RUNTIME"
+        "\nError Code: INVALID_STATE"
+        "\nReason: (1 vs. 3)"
+        "\nRetriable: False"
+        "\nExpression: 1 == 3"
+        "\nContext: System error: Debugging info."
+        "\nTop-Level Context: Same as context."
+        "\nFunction: operator()"
+        "\nFile: ");
+
+    EXPECT_EQ(1, callCount);
+
+    verifyCelebornException(
+        [&]() { CELEBORN_USER_CHECK_EQ(1, 3); },
+        "Exception: CelebornUserError"
+        "\nError Source: USER"
+        "\nError Code: INVALID_ARGUMENT"
+        "\nReason: (1 vs. 3)"
+        "\nRetriable: False"
+        "\nExpression: 1 == 3"
+        "\nContext: User error: Debugging info."
+        "\nTop-Level Context: Same as context."
+        "\nFunction: operator()"
+        "\nFile: ");
+
+    EXPECT_EQ(2, callCount);
+  }
+
+  callCount = 0;
+
+  // No context.
+  verifyCelebornException(
+      [&]() { CELEBORN_CHECK_EQ(1, 3); },
+      "Exception: CelebornRuntimeError"
+      "\nError Source: RUNTIME"
+      "\nError Code: INVALID_STATE"
+      "\nReason: (1 vs. 3)"
+      "\nRetriable: False"
+      "\nExpression: 1 == 3"
+      "\nFunction: operator()"
+      "\nFile: ");
+
+  EXPECT_EQ(0, callCount);
+
+  // With message function throwing an exception.
+  auto throwingMessageFunction =
+      [](celeborn::CelebornException::Type /*exceptionType*/,
+         void* untypedArg) -> std::string {
+    auto arg = static_cast<MessageFunctionArg*>(untypedArg);
+    ++(*arg->callCount);
+    CELEBORN_FAIL("Test failure.");
+  };
+  {
+    MessageFunctionArg debuggingInfo{"Debugging info.", &callCount};
+    celeborn::ExceptionContextSetter context(
+        {throwingMessageFunction, &debuggingInfo});
+
+    verifyCelebornException(
+        [&]() { CELEBORN_CHECK_EQ(1, 3); },
+        "Exception: CelebornRuntimeError"
+        "\nError Source: RUNTIME"
+        "\nError Code: INVALID_STATE"
+        "\nReason: (1 vs. 3)"
+        "\nRetriable: False"
+        "\nExpression: 1 == 3"
+        "\nContext: Failed to produce additional context."
+        "\nTop-Level Context: Same as context."
+        "\nFunction: operator()"
+        "\nFile: ");
+
+    EXPECT_EQ(1, callCount);
+  }
+}
+
+TEST(ExceptionTest, traceCollectionEnabling) {
+  // Switch on/off tests.
+  for (const bool enabled : {false, true}) {
+    for (const bool userException : {false, true}) {
+      testExceptionTraceCollectionControl(userException, enabled);
+    }
+  }
+}
+
+TEST(ExceptionTest, traceCollectionRateControl) {
+  // Rate limit tests.
+  for (const bool withLimit : {false, true}) {
+    for (const bool userException : {false, true}) {
+      testExceptionTraceCollectionRateControl(userException, withLimit);
+    }
+  }
+}
+
+TEST(ExceptionTest, wrappedException) {
+  try {
+    throw std::invalid_argument("This is a test.");
+  } catch (const std::exception& e) {
+    CelebornUserError ve(std::current_exception(), e.what(), false);
+    ASSERT_EQ(ve.message(), "This is a test.");
+    ASSERT_TRUE(ve.isUserError());
+    ASSERT_EQ(ve.context(), "");
+    ASSERT_EQ(ve.topLevelContext(), "");
+    ASSERT_THROW(
+        std::rethrow_exception(ve.wrappedException()), std::invalid_argument);
+  }
+
+  try {
+    throw std::invalid_argument("This is a test.");
+  } catch (const std::exception& e) {
+    CelebornRuntimeError ve(std::current_exception(), e.what(), false);
+    ASSERT_EQ(ve.message(), "This is a test.");
+    ASSERT_FALSE(ve.isUserError());
+    ASSERT_EQ(ve.context(), "");
+    ASSERT_EQ(ve.topLevelContext(), "");
+    ASSERT_THROW(
+        std::rethrow_exception(ve.wrappedException()), std::invalid_argument);
+  }
+
+  try {
+    CELEBORN_FAIL("This is a test.");
+  } catch (const CelebornException& e) {
+    ASSERT_EQ(e.message(), "This is a test.");
+    ASSERT_TRUE(e.wrappedException() == nullptr);
+  }
+}
+
+TEST(ExceptionTest, wrappedExceptionWithContext) {
+  auto messageFunction = [](celeborn::CelebornException::Type exceptionType,
+                            void* untypedArg) {
+    auto data = static_cast<char*>(untypedArg);
+    switch (exceptionType) {
+      case celeborn::CelebornException::Type::kUser:
+        return fmt::format("User error: {}", data);
+      case celeborn::CelebornException::Type::kSystem:
+        return fmt::format("System error: {}", data);
+      default:
+        return fmt::format("Unexpected error type: {}", data);
+    }
+  };
+
+  std::string data = "lakes";
+  celeborn::ExceptionContextSetter context({messageFunction, data.data()});
+
+  try {
+    throw std::invalid_argument("This is a test.");
+  } catch (const std::exception& e) {
+    CelebornUserError ve(std::current_exception(), e.what(), false);
+    ASSERT_EQ(ve.message(), "This is a test.");
+    ASSERT_TRUE(ve.isUserError());
+    ASSERT_EQ(ve.context(), "User error: lakes");
+    ASSERT_EQ(ve.topLevelContext(), "Same as context.");
+    ASSERT_THROW(
+        std::rethrow_exception(ve.wrappedException()), std::invalid_argument);
+  }
+
+  try {
+    throw std::invalid_argument("This is a test.");
+  } catch (const std::exception& e) {
+    CelebornRuntimeError ve(std::current_exception(), e.what(), false);
+    ASSERT_EQ(ve.message(), "This is a test.");
+    ASSERT_FALSE(ve.isUserError());
+    ASSERT_EQ(ve.context(), "System error: lakes");
+    ASSERT_EQ(ve.topLevelContext(), "Same as context.");
+    ASSERT_THROW(
+        std::rethrow_exception(ve.wrappedException()), std::invalid_argument);
+  }
+
+  std::string innerData = "mountains";
+  celeborn::ExceptionContextSetter innerContext(
+      {messageFunction, innerData.data()});
+
+  try {
+    throw std::invalid_argument("This is a test.");
+  } catch (const std::exception& e) {
+    CelebornUserError ve(std::current_exception(), e.what(), false);
+    ASSERT_EQ(ve.message(), "This is a test.");
+    ASSERT_TRUE(ve.isUserError());
+    ASSERT_EQ(ve.context(), "User error: mountains");
+    ASSERT_EQ(ve.topLevelContext(), "User error: lakes");
+    ASSERT_THROW(
+        std::rethrow_exception(ve.wrappedException()), std::invalid_argument);
+  }
+
+  try {
+    throw std::invalid_argument("This is a test.");
+  } catch (const std::exception& e) {
+    CelebornRuntimeError ve(std::current_exception(), e.what(), false);
+    ASSERT_EQ(ve.message(), "This is a test.");
+    ASSERT_FALSE(ve.isUserError());
+    ASSERT_EQ(ve.context(), "System error: mountains");
+    ASSERT_EQ(ve.topLevelContext(), "System error: lakes");
+    ASSERT_THROW(
+        std::rethrow_exception(ve.wrappedException()), std::invalid_argument);
+  }
+}
+
+TEST(ExceptionTest, exceptionMacroInlining) {
+  // Verify that the right formatting method is inlined when using
+  // _CELEBORN_THROW macro. This test can be removed if fmt::vformat changes
+  // behavior and starts ignoring extra brackets.
+
+  // The following string should throw an error when passed to fmt::vformat.
+  std::string errorStr = "This {} {is a test.";
+  // Inlined with the method that directly returns the std::string input.
+  try {
+    CELEBORN_USER_FAIL(errorStr);
+  } catch (const CelebornUserError& ve) {
+    ASSERT_EQ(ve.message(), errorStr);
+  }
+
+  // Inlined with the method that directly returns the char* input.
+  try {
+    CELEBORN_USER_FAIL(errorStr.c_str());
+  } catch (const CelebornUserError& ve) {
+    ASSERT_EQ(ve.message(), errorStr);
+  }
+
+  // Inlined with the method that passes the errorStr and the next argument via
+  // fmt::vformat. Should throw format_error.
+  try {
+    CELEBORN_USER_FAIL(errorStr, "definitely");
+  } catch (const std::exception& e) {
+    ASSERT_TRUE(folly::StringPiece{e.what()}.startsWith("argument not found"));
+  }
+}
diff --git a/cpp/scripts/setup-ubuntu.sh b/cpp/scripts/setup-ubuntu.sh
index 1ebd4cb81..a0b6d88ca 100755
--- a/cpp/scripts/setup-ubuntu.sh
+++ b/cpp/scripts/setup-ubuntu.sh
@@ -65,6 +65,21 @@ BOOST_VERSION="boost-1.84.0"
 ARROW_VERSION="15.0.0"
 STEMMER_VERSION="2.2.0"
 
+# Install gtest library package for tests.
+function install_gtest {
+  ${SUDO} apt-get install -y libgtest-dev cmake
+  mkdir -p $HOME/build
+  pushd $HOME/build
+  ${SUDO} cmake /usr/src/googletest/googletest
+  ${SUDO} make
+  ${SUDO} cp lib/libgtest* /usr/lib/
+  popd
+  ${SUDO} rm -rf $HOME/build
+  ${SUDO} mkdir /usr/local/lib/googletest
+  ${SUDO} ln -s /usr/lib/libgtest.a /usr/local/lib/googletest/libgtest.a
+  ${SUDO} ln -s /usr/lib/libgtest_main.a 
/usr/local/lib/googletest/libgtest_main.a
+}
+
 # Install packages required for build.
 function install_build_prerequisites {
   ${SUDO} apt update
@@ -82,6 +97,8 @@ function install_build_prerequisites {
     pkg-config \
     gdb \
     wget
+    
+  install_gtest
 
   # Install to /usr/local to make it available to all users.
   ${SUDO} pip3 install cmake==3.28.3
@@ -140,8 +157,8 @@ function install_protobuf {
     cd ${DEPENDENCY_DIR}/protobuf
     ./configure CXXFLAGS="-fPIC" --prefix=${INSTALL_PREFIX}
     make "-j${NPROC}"
-    sudo make install
-    sudo ldconfig
+    ${SUDO} make install
+    ${SUDO} ldconfig
   )
 }
 

Reply via email to