diff --git a/src/iceberg/exception.h b/src/iceberg/exception.h index 0333eb12a..a182cb0d8 100644 --- a/src/iceberg/exception.h +++ b/src/iceberg/exception.h @@ -30,6 +30,13 @@ #include "iceberg/iceberg_export.h" +#define ICEBERG_CHECK_OR_DIE(condition, ...) \ + do { \ + if (!(condition)) [[unlikely]] { \ + throw ::iceberg::IcebergError(std::format(__VA_ARGS__)); \ + } \ + } while (0) + namespace iceberg { /// \brief Base exception class for exceptions thrown by the Iceberg library. @@ -44,11 +51,4 @@ class ICEBERG_EXPORT ExpressionError : public IcebergError { explicit ExpressionError(const std::string& what) : IcebergError(what) {} }; -#define ICEBERG_CHECK_OR_DIE(condition, ...) \ - do { \ - if (!(condition)) [[unlikely]] { \ - throw iceberg::IcebergError(std::format(__VA_ARGS__)); \ - } \ - } while (0) - } // namespace iceberg diff --git a/src/iceberg/test/CMakeLists.txt b/src/iceberg/test/CMakeLists.txt index a86bc9087..60f16b52e 100644 --- a/src/iceberg/test/CMakeLists.txt +++ b/src/iceberg/test/CMakeLists.txt @@ -135,6 +135,7 @@ add_iceberg_test(util_test data_file_set_test.cc decimal_test.cc endian_test.cc + error_macros_test.cc expected_test.cc file_io_test.cc formatter_test.cc diff --git a/src/iceberg/test/error_macros_test.cc b/src/iceberg/test/error_macros_test.cc new file mode 100644 index 000000000..f0b1f53eb --- /dev/null +++ b/src/iceberg/test/error_macros_test.cc @@ -0,0 +1,133 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#include +#include + +#include "iceberg/exception.h" +#include "iceberg/result.h" +#include "iceberg/test/matchers.h" +#include "iceberg/util/error_collector.h" +#include "iceberg/util/macros.h" + +namespace { + +using ::iceberg::HasErrorMessage; +using ::iceberg::HasValue; +using ::iceberg::IsError; +using ::iceberg::IsOk; + +// The macros expand here, in a namespace with its own nested iceberg namespace, so a +// macro that names iceberg::X instead of ::iceberg::X fails to compile. +namespace caller { + +namespace iceberg {} + +::iceberg::Result Parse(bool valid) { + if (!valid) { + return ::iceberg::InvalidArgument("Invalid input"); + } + return 42; +} + +::iceberg::Result AddOne(bool valid) { + ICEBERG_ASSIGN_OR_RAISE(auto value, Parse(valid)); + return value + 1; +} + +::iceberg::Status Validate(bool valid) { + ICEBERG_RETURN_UNEXPECTED(Parse(valid)); + return {}; +} + +::iceberg::Status CheckRange(int value) { + ICEBERG_PRECHECK(value > 0, "Value must be positive, got {}", value); + ICEBERG_CHECK(value < 100, "Value must be below 100, got {}", value); + return {}; +} + +int AddOneOrThrow(bool valid) { + ICEBERG_ASSIGN_OR_THROW(auto value, Parse(valid)); + return value + 1; +} + +int CheckPositiveOrDie(int value) { + ICEBERG_CHECK_OR_DIE(value > 0, "Value must be positive, got {}", value); + return value; +} + +class TestBuilder : public ::iceberg::ErrorCollector { + public: + TestBuilder& Set(int value) { + ICEBERG_BUILDER_CHECK(value > 0, "Value must be positive, got {}", value); + ICEBERG_BUILDER_ASSIGN_OR_RETURN(value_, Parse(value < 100)); + ICEBERG_BUILDER_ASSIGN_OR_RETURN_WITH_ERROR(value_, Parse(value < 50), + "Value {} is too large", value); + return *this; + } + + private: + int value_ = 0; +}; + +} // namespace caller + +using caller::AddOne; +using caller::AddOneOrThrow; +using caller::CheckPositiveOrDie; +using caller::CheckRange; +using caller::TestBuilder; +using caller::Validate; + +} // namespace + +TEST(ErrorMacrosTest, AssignOrRaise) { + EXPECT_THAT(AddOne(true), HasValue(::testing::Eq(43))); + EXPECT_THAT(AddOne(false), IsError(iceberg::ErrorKind::kInvalidArgument)); +} + +TEST(ErrorMacrosTest, ReturnUnexpected) { + EXPECT_THAT(Validate(true), IsOk()); + EXPECT_THAT(Validate(false), HasErrorMessage("Invalid input")); +} + +TEST(ErrorMacrosTest, PrecheckAndCheck) { + EXPECT_THAT(CheckRange(1), IsOk()); + EXPECT_THAT(CheckRange(0), IsError(iceberg::ErrorKind::kInvalidArgument)); + EXPECT_THAT(CheckRange(100), IsError(iceberg::ErrorKind::kValidationFailed)); +} + +TEST(ErrorMacrosTest, AssignOrThrow) { + EXPECT_EQ(AddOneOrThrow(true), 43); + EXPECT_THROW(AddOneOrThrow(false), iceberg::IcebergError); +} + +TEST(ErrorMacrosTest, CheckOrDie) { + EXPECT_EQ(CheckPositiveOrDie(1), 1); + EXPECT_THROW(CheckPositiveOrDie(0), iceberg::IcebergError); +} + +TEST(ErrorMacrosTest, BuilderMacros) { + EXPECT_THAT(TestBuilder().Set(1).CheckErrors(), IsOk()); + EXPECT_THAT(TestBuilder().Set(0).CheckErrors(), + HasErrorMessage("Value must be positive, got 0")); + EXPECT_THAT(TestBuilder().Set(150).CheckErrors(), HasErrorMessage("Invalid input")); + EXPECT_THAT(TestBuilder().Set(70).CheckErrors(), + HasErrorMessage("Value 70 is too large")); +} diff --git a/src/iceberg/util/error_collector.h b/src/iceberg/util/error_collector.h index 9eb8de15a..d8834f284 100644 --- a/src/iceberg/util/error_collector.h +++ b/src/iceberg/util/error_collector.h @@ -29,8 +29,8 @@ #include "iceberg/iceberg_export.h" #include "iceberg/result.h" -namespace iceberg { - +// The builder macros name Iceberg symbols with their full namespace, so builders outside +// namespace iceberg can use them. #define ICEBERG_BUILDER_RETURN_IF_ERROR(result) \ if (auto&& result_name = result; !result_name) [[unlikely]] { \ errors_.emplace_back(std::move(result_name.error())); \ @@ -49,7 +49,7 @@ namespace iceberg { #define ICEBERG_BUILDER_ASSIGN_OR_RETURN_WITH_ERROR_IMPL(result_name, lhs, rexpr, ...) \ auto&& result_name = (rexpr); \ if (!result_name) [[unlikely]] { \ - AddError(ErrorKind::kInvalidArgument, __VA_ARGS__); \ + AddError(::iceberg::ErrorKind::kInvalidArgument, __VA_ARGS__); \ return *this; \ } \ lhs = std::move(result_name.value()); @@ -58,14 +58,16 @@ namespace iceberg { ICEBERG_BUILDER_ASSIGN_OR_RETURN_WITH_ERROR_IMPL( \ ICEBERG_ASSIGN_OR_RAISE_NAME(result_, __COUNTER__), lhs, rexpr, __VA_ARGS__) -#define ICEBERG_BUILDER_CHECK(expr, ...) \ - do { \ - if (!(expr)) [[unlikely]] { \ - AddError(ErrorKind::kInvalidArgument, __VA_ARGS__); \ - return *this; \ - } \ +#define ICEBERG_BUILDER_CHECK(expr, ...) \ + do { \ + if (!(expr)) [[unlikely]] { \ + AddError(::iceberg::ErrorKind::kInvalidArgument, __VA_ARGS__); \ + return *this; \ + } \ } while (false) +namespace iceberg { + /// \brief Base class for collecting errors in the builder pattern. /// /// This class equips builders with error accumulation capabilities to make it easy diff --git a/src/iceberg/util/macros.h b/src/iceberg/util/macros.h index ca481a939..cd046ba6c 100644 --- a/src/iceberg/util/macros.h +++ b/src/iceberg/util/macros.h @@ -21,15 +21,18 @@ /// \file iceberg/util/macros.h /// \brief Define common Iceberg utility macros. +/// +/// The macros name Iceberg symbols with their full namespace, so code outside namespace +/// iceberg can use them. #include #include "iceberg/exception.h" #include "iceberg/result.h" -#define ICEBERG_RETURN_UNEXPECTED(expr) \ - if (auto&& result_name = expr; !result_name) [[unlikely]] { \ - return iceberg::unexpected(result_name.error()); \ +#define ICEBERG_RETURN_UNEXPECTED(expr) \ + if (auto&& result_name = expr; !result_name) [[unlikely]] { \ + return ::iceberg::unexpected<::iceberg::Error>(result_name.error()); \ } #define ICEBERG_ASSIGN_OR_RAISE_IMPL(result_name, lhs, rexpr) \ @@ -49,26 +52,26 @@ #define ICEBERG_DCHECK(expr, message) assert((expr) && (message)) // Macro for precondition checks, usually used for function arguments -#define ICEBERG_PRECHECK(expr, ...) \ - do { \ - if (!(expr)) [[unlikely]] { \ - return InvalidArgument(__VA_ARGS__); \ - } \ +#define ICEBERG_PRECHECK(expr, ...) \ + do { \ + if (!(expr)) [[unlikely]] { \ + return ::iceberg::InvalidArgument(__VA_ARGS__); \ + } \ } while (0) // Macro for state checks, usually used for unexpected states -#define ICEBERG_CHECK(expr, ...) \ - do { \ - if (!(expr)) [[unlikely]] { \ - return ValidationFailed(__VA_ARGS__); \ - } \ +#define ICEBERG_CHECK(expr, ...) \ + do { \ + if (!(expr)) [[unlikely]] { \ + return ::iceberg::ValidationFailed(__VA_ARGS__); \ + } \ } while (0) -#define ERROR_TO_EXCEPTION(error) \ - if (error.kind == iceberg::ErrorKind::kInvalidExpression) { \ - throw iceberg::ExpressionError(error.message); \ - } else { \ - throw iceberg::IcebergError(error.message); \ +#define ERROR_TO_EXCEPTION(error) \ + if (error.kind == ::iceberg::ErrorKind::kInvalidExpression) { \ + throw ::iceberg::ExpressionError(error.message); \ + } else { \ + throw ::iceberg::IcebergError(error.message); \ } #define ICEBERG_THROW_NOT_OK(expr) \