diff --git a/openfeature/BUILD b/openfeature/BUILD index 62e2868..fcd4d58 100644 --- a/openfeature/BUILD +++ b/openfeature/BUILD @@ -88,6 +88,16 @@ cc_library( include_prefix = "openfeature", ) +cc_library( + name = "evaluation_options", + hdrs = ["evaluation_options.h"], + include_prefix = "openfeature", + deps = [ + ":general_hook", + ":hook_hints", + ], +) + cc_library( name = "feature_provider_status_manager", srcs = ["feature_provider_status_manager.cpp"], diff --git a/openfeature/evaluation_options.h b/openfeature/evaluation_options.h new file mode 100644 index 0000000..8066bc2 --- /dev/null +++ b/openfeature/evaluation_options.h @@ -0,0 +1,14 @@ +#ifndef CPP_SDK_INCLUDE_OPENFEATURE_EVALUATION_OPTIONS_H_ +#define CPP_SDK_INCLUDE_OPENFEATURE_EVALUATION_OPTIONS_H_ +#include +#include + +#include "openfeature/general_hook.h" +#include "openfeature/hook_hints.h" +namespace openfeature { +struct EvaluationOptions { + std::vector> hooks; + HookHints hook_hints; +}; +} // namespace openfeature +#endif // CPP_SDK_INCLUDE_OPENFEATURE_EVALUATION_OPTIONS_H_ \ No newline at end of file diff --git a/test/BUILD b/test/BUILD index a8942c1..1fbfae3 100644 --- a/test/BUILD +++ b/test/BUILD @@ -142,4 +142,16 @@ cc_test( "//openfeature:value", "@googletest//:gtest_main", ], +) + +cc_test( + name = "evaluation_options_test", + srcs = ["evaluation_options_test.cpp"], + deps = [ + "//openfeature:general_hook", + "//openfeature:evaluation_options", + "//openfeature:hook", + "//openfeature:hook_hints", + "@googletest//:gtest_main", + ], ) \ No newline at end of file diff --git a/test/evaluation_options_test.cpp b/test/evaluation_options_test.cpp new file mode 100644 index 0000000..12fd687 --- /dev/null +++ b/test/evaluation_options_test.cpp @@ -0,0 +1,121 @@ +#include "openfeature/evaluation_options.h" + +#include + +#include +#include +#include +#include + +#include "openfeature/general_hook.h" +#include "openfeature/hook.h" +#include "openfeature/hook_hints.h" + +namespace openfeature { + +class CustomTestHook : public BoolHook {}; +class AnotherTestHook : public StringHook {}; + +TEST(EvaluationOptionsTest, DefaultConstructorInitializesEmptyContainers) { + EvaluationOptions options; + EXPECT_TRUE(options.hooks.empty()); + EXPECT_TRUE(options.hook_hints.empty()); +} + +TEST(EvaluationOptionsTest, StoresAndPreservesMultipleHooksInOrder) { + EvaluationOptions options; + + auto hook1 = std::make_shared(); + auto hook2 = std::make_shared(); + auto hook3 = std::make_shared(); + + options.hooks.push_back(hook1); + options.hooks.push_back(hook2); + options.hooks.push_back(hook3); + + ASSERT_EQ(options.hooks.size(), 3); + EXPECT_EQ(options.hooks[0], hook1); + EXPECT_EQ(options.hooks[1], hook2); + EXPECT_EQ(options.hooks[2], hook3); +} + +TEST(EvaluationOptionsTest, CanDowncastBaseHookPointersToConcreteTypes) { + EvaluationOptions options; + + auto bool_hook = std::make_shared(); + auto string_hook = std::make_shared(); + + options.hooks.push_back(bool_hook); + options.hooks.push_back(string_hook); + + ASSERT_EQ(options.hooks.size(), 2); + + auto retrieved_bool_hook = + std::dynamic_pointer_cast(options.hooks[0]); + ASSERT_NE(retrieved_bool_hook, nullptr); + EXPECT_EQ(retrieved_bool_hook, bool_hook); + + auto retrieved_string_hook = + std::dynamic_pointer_cast(options.hooks[1]); + ASSERT_NE(retrieved_string_hook, nullptr); + EXPECT_EQ(retrieved_string_hook, string_hook); + + // Cross-type downcast should return nullptr safely + auto invalid_cast = std::dynamic_pointer_cast(options.hooks[0]); + EXPECT_EQ(invalid_cast, nullptr); +} + +TEST(EvaluationOptionsTest, StoresAndRetrievesHookHints) { + const std::string trace_id_key = "trace_id"; + const std::string timeout_key = "timeout_ms"; + const std::string is_debug_key = "is_debug"; + const std::string trace_id_value = "trace-12345"; + const int timeout_value = 500; + const bool is_debug_value = true; + EvaluationOptions options; + + options.hook_hints[trace_id_key] = std::string(trace_id_value); + options.hook_hints[timeout_key] = timeout_value; + options.hook_hints[is_debug_key] = is_debug_value; + + EXPECT_EQ(options.hook_hints.size(), 3); + + auto it_trace = options.hook_hints.find(trace_id_key); + ASSERT_NE(it_trace, options.hook_hints.end()); + EXPECT_EQ(std::any_cast(it_trace->second), trace_id_value); + + auto it_timeout = options.hook_hints.find(timeout_key); + ASSERT_NE(it_timeout, options.hook_hints.end()); + EXPECT_EQ(std::any_cast(it_timeout->second), timeout_value); + + auto it_debug = options.hook_hints.find(is_debug_key); + ASSERT_NE(it_debug, options.hook_hints.end()); + EXPECT_TRUE(std::any_cast(it_debug->second)); +} + +TEST(EvaluationOptionsTest, SupportsCopyAndMoveSemantics) { + const std::string trace_id_key = "trace_id"; + const std::string trace_id_value = "value"; + EvaluationOptions original; + auto hook = std::make_shared(); + original.hooks.push_back(hook); + original.hook_hints[trace_id_key] = trace_id_value; + + // Copy construction + EvaluationOptions copy_constructed(original); + EXPECT_EQ(copy_constructed.hooks.size(), 1); + EXPECT_EQ(copy_constructed.hooks[0], hook); + EXPECT_EQ( + std::any_cast(copy_constructed.hook_hints[trace_id_key]), + trace_id_value); + + // Move construction + EvaluationOptions moved_constructed(std::move(original)); + EXPECT_EQ(moved_constructed.hooks.size(), 1); + EXPECT_EQ(moved_constructed.hooks[0], hook); + EXPECT_EQ( + std::any_cast(moved_constructed.hook_hints[trace_id_key]), + trace_id_value); +} + +} // namespace openfeature diff --git a/test/hook_test.cpp b/test/hook_test.cpp index 418b7a5..ed9796d 100644 --- a/test/hook_test.cpp +++ b/test/hook_test.cpp @@ -327,4 +327,4 @@ TEST_F(HookTest, TypedHookIgnoresMismatchedFlagTypes) { EXPECT_FALSE(bool_hook.after_called); } -} // namespace openfeature +} // namespace openfeature \ No newline at end of file diff --git a/test/openfeature_api_test.cpp b/test/openfeature_api_test.cpp index cc42717..ec6ea6c 100644 --- a/test/openfeature_api_test.cpp +++ b/test/openfeature_api_test.cpp @@ -9,21 +9,22 @@ #include "mocks/mock_feature_provider.h" #include "openfeature/noop_provider.h" -using namespace openfeature; using ::testing::_; using ::testing::Return; +namespace openfeature { + class OpenFeatureAPITest : public ::testing::Test { protected: // To ensure test isolation for the singleton, we shut it down before and // after each test, to reset it to its default state. void SetUp() override {} void TearDown() override { - api.Shutdown(); - api.SetEvaluationContext(EvaluationContext ::Builder().build()); + api_.Shutdown(); + api_.SetEvaluationContext(EvaluationContext::Builder().build()); } - OpenFeatureAPI& api = OpenFeatureAPI::GetInstance(); + OpenFeatureAPI& api_ = OpenFeatureAPI::GetInstance(); }; // Test that GetInstance always returns the same singleton instance. @@ -35,7 +36,7 @@ TEST_F(OpenFeatureAPITest, GetInstanceReturnsSameInstance) { // Test that the API is initialized with a NoopProvider by default. TEST_F(OpenFeatureAPITest, InitialStateHasNoopProvider) { - std::shared_ptr provider = api.GetProvider(); + std::shared_ptr provider = api_.GetProvider(); ASSERT_NE(provider, nullptr); EXPECT_NE(dynamic_cast(provider.get()), nullptr); } @@ -46,9 +47,9 @@ TEST_F(OpenFeatureAPITest, SetAndGetDefaultProviderAndWait) { std::make_shared(); EXPECT_CALL(*mock_provider, Init(_)).WillOnce(Return(absl::OkStatus())); - api.SetProviderAndWait(mock_provider); + api_.SetProviderAndWait(mock_provider); - EXPECT_EQ(api.GetProvider(), mock_provider); + EXPECT_EQ(api_.GetProvider(), mock_provider); } // Test setting a named provider and waiting for its initialization. @@ -58,24 +59,24 @@ TEST_F(OpenFeatureAPITest, SetAndGetNamedProviderAndWait) { std::string domain = "test-domain"; EXPECT_CALL(*mock_provider, Init(_)).WillOnce(Return(absl::OkStatus())); - api.SetProviderAndWait(domain, mock_provider); + api_.SetProviderAndWait(domain, mock_provider); - EXPECT_EQ(api.GetProvider(domain), mock_provider); - EXPECT_NE(dynamic_cast(api.GetProvider().get()), nullptr); + EXPECT_EQ(api_.GetProvider(domain), mock_provider); + EXPECT_NE(dynamic_cast(api_.GetProvider().get()), nullptr); } // Test that getting a provider for a non-existent domain falls back to the // default. TEST_F(OpenFeatureAPITest, GetProviderFallsBackToDefault) { - std::shared_ptr default_provider = api.GetProvider(); + std::shared_ptr default_provider = api_.GetProvider(); std::shared_ptr unknown_domain_provider = - api.GetProvider("unknown-domain"); + api_.GetProvider("unknown-domain"); EXPECT_EQ(default_provider, unknown_domain_provider); } // Test getting metadata from the default provider. TEST_F(OpenFeatureAPITest, GetProviderMetadataForDefault) { - Metadata metadata = api.GetProviderMetadata(); + Metadata metadata = api_.GetProviderMetadata(); EXPECT_EQ(metadata.name, "Noop Provider"); } @@ -91,8 +92,8 @@ TEST_F(OpenFeatureAPITest, GetProviderMetadataForNamed) { .WillOnce(Return(expected_metadata)); EXPECT_CALL(*mock_provider, Init(_)).WillOnce(Return(absl::OkStatus())); - api.SetProviderAndWait(domain, mock_provider); - Metadata actual_metadata = api.GetProviderMetadata(domain); + api_.SetProviderAndWait(domain, mock_provider); + Metadata actual_metadata = api_.GetProviderMetadata(domain); EXPECT_EQ(actual_metadata.name, expected_metadata.name); } @@ -109,15 +110,15 @@ TEST_F(OpenFeatureAPITest, ShutdownCallsProviderShutdown) { .WillOnce(Return(absl::OkStatus())); EXPECT_CALL(*mock_named_provider, Init(_)).WillOnce(Return(absl::OkStatus())); - api.SetProviderAndWait(mock_default_provider); - api.SetProviderAndWait(domain, mock_named_provider); + api_.SetProviderAndWait(mock_default_provider); + api_.SetProviderAndWait(domain, mock_named_provider); EXPECT_CALL(*mock_default_provider, Shutdown()) .WillOnce(Return(absl::OkStatus())); EXPECT_CALL(*mock_named_provider, Shutdown()) .WillOnce(Return(absl::OkStatus())); - api.Shutdown(); + api_.Shutdown(); testing::Mock::VerifyAndClearExpectations(mock_default_provider.get()); testing::Mock::VerifyAndClearExpectations(mock_named_provider.get()); @@ -141,7 +142,7 @@ TEST_F(OpenFeatureAPITest, SetProviderAsyncDoesNotBlock) { }); EXPECT_CALL(*mock_provider, Shutdown()).WillOnce(Return(absl::OkStatus())); - api.SetProvider(mock_provider); + api_.SetProvider(mock_provider); // Confirm the background task has started. auto status = init_started_future.wait_for(std::chrono::seconds(1)); @@ -173,7 +174,7 @@ TEST_F(OpenFeatureAPITest, SetNamedProviderAsyncDoesNotBlock) { ; }); EXPECT_CALL(*mock_provider, Shutdown()).WillOnce(Return(absl::OkStatus())); - api.SetProvider(domain, mock_provider); + api_.SetProvider(domain, mock_provider); // Confirm the background task has started. auto status = init_started_future.wait_for(std::chrono::seconds(1)); @@ -187,14 +188,14 @@ TEST_F(OpenFeatureAPITest, SetNamedProviderAsyncDoesNotBlock) { // Test that GetClient returns a valid default ClientAPI instance. TEST_F(OpenFeatureAPITest, GetDefaultClient) { - std::shared_ptr client = api.GetClient(); + std::shared_ptr client = api_.GetClient(); EXPECT_NE(client, nullptr) << "GetClient() should return a valid ptr"; EXPECT_EQ(client->GetMetadata().name, ""); } // Test that GetClient returns a valid named ClientAPI instance. TEST_F(OpenFeatureAPITest, GetNamedClient) { - std::shared_ptr named_client = api.GetClient("some-domain"); + std::shared_ptr named_client = api_.GetClient("some-domain"); EXPECT_NE(named_client, nullptr) << "GetClient(domain) should return a valid ptr"; EXPECT_EQ(named_client->GetMetadata().name, "some-domain"); @@ -202,3 +203,5 @@ TEST_F(OpenFeatureAPITest, GetNamedClient) { // TODO: Add tests for "GetEvaluationContext" and "SetEvaluationContext" once. // EvaluationContext logic is implemented. + +} // namespace openfeature