From 990fa80aad75e6b26828d9db6b860738a7f2bb1b Mon Sep 17 00:00:00 2001 From: lea konvalinka Date: Wed, 30 Jul 2025 10:03:56 +0200 Subject: [PATCH 01/10] feat: adjust to flagd metadata toggle Signed-off-by: Konvalinka --- providers/openfeature-provider-flagd/openfeature/test-harness | 2 +- .../tests/e2e/step/provider_steps.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/providers/openfeature-provider-flagd/openfeature/test-harness b/providers/openfeature-provider-flagd/openfeature/test-harness index 59c3c3cc..fe68e031 160000 --- a/providers/openfeature-provider-flagd/openfeature/test-harness +++ b/providers/openfeature-provider-flagd/openfeature/test-harness @@ -1 +1 @@ -Subproject commit 59c3c3ccfb018db82281684d231067e332c8103d +Subproject commit fe68e0310fd817a8f9bc1e2559f2277fed3aed34 diff --git a/providers/openfeature-provider-flagd/tests/e2e/step/provider_steps.py b/providers/openfeature-provider-flagd/tests/e2e/step/provider_steps.py index 3d8d5195..a2589d12 100644 --- a/providers/openfeature-provider-flagd/tests/e2e/step/provider_steps.py +++ b/providers/openfeature-provider-flagd/tests/e2e/step/provider_steps.py @@ -32,6 +32,7 @@ class TestProviderType(Enum): SSL = "ssl" SOCKET = "socket" METADATA = "metadata" + SYNCPAYLOAD = "syncpayload" @given("a provider is registered", target_fixture="client") @@ -71,6 +72,8 @@ def get_default_options_for_provider( return options, True elif t == TestProviderType.METADATA: launchpad = "metadata" + elif t == TestProviderType.SYNCPAYLOAD: + launchpad = "sync-payload" if resolver_type == ResolverType.FILE: if "selector" in option_values: From da62cd248132de5b00f4718b9652897d5e350dc0 Mon Sep 17 00:00:00 2001 From: lea konvalinka Date: Wed, 30 Jul 2025 11:42:05 +0200 Subject: [PATCH 02/10] fix: implementation, update schema Signed-off-by: Konvalinka --- .../openfeature/schemas | 2 +- .../process/connector/grpc_watcher.py | 37 ++++++++++++------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/providers/openfeature-provider-flagd/openfeature/schemas b/providers/openfeature-provider-flagd/openfeature/schemas index 76d611fd..2852d777 160000 --- a/providers/openfeature-provider-flagd/openfeature/schemas +++ b/providers/openfeature-provider-flagd/openfeature/schemas @@ -1 +1 @@ -Subproject commit 76d611fd94689d906af316105ac12670d40f7648 +Subproject commit 2852d7772e6b8674681a6ee6b88db10dbe3f6899 diff --git a/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/connector/grpc_watcher.py b/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/connector/grpc_watcher.py index 34eb1c1c..af20f4c4 100644 --- a/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/connector/grpc_watcher.py +++ b/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/connector/grpc_watcher.py @@ -6,7 +6,6 @@ import grpc from google.protobuf.json_format import MessageToDict -from google.protobuf.struct_pb2 import Struct from openfeature.evaluation_context import EvaluationContext from openfeature.event import ProviderEventDetails @@ -210,6 +209,17 @@ def _create_request_args(self) -> dict: return request_args + def _fetch_metadata(self) -> typing.Optional[sync_pb2.GetMetadataResponse]: + if self.config.sync_metadata_disabled: + return None + + context_values_request = sync_pb2.GetMetadataRequest() + try: + return self.stub.GetMetadata(context_values_request, wait_for_ready=True) + except grpc.RpcError as e: + logger.debug(f"Error getting sync metadata: {e}") + return None + def listen(self) -> None: call_args = ( {"timeout": self.streamline_deadline_seconds} @@ -220,18 +230,9 @@ def listen(self) -> None: while self.active: try: - context_values_response: sync_pb2.GetMetadataResponse - if self.config.sync_metadata_disabled: - context_values_response = sync_pb2.GetMetadataResponse( - metadata=Struct() - ) - else: - context_values_request = sync_pb2.GetMetadataRequest() - context_values_response = self.stub.GetMetadata( - context_values_request, wait_for_ready=True - ) - - context_values = MessageToDict(context_values_response) + context_values_response: typing.Optional[ + sync_pb2.GetMetadataResponse + ] = self._fetch_metadata() request = sync_pb2.SyncFlagsRequest(**request_args) @@ -245,12 +246,20 @@ def listen(self) -> None: ) self.flag_store.update(json.loads(flag_str)) + context_values = None + if flag_rsp.sync_context: + context_values = MessageToDict(flag_rsp.sync_context) + elif context_values_response: + context_values = MessageToDict(context_values_response)[ + "metadata" + ] + if not self.connected: self.emit_provider_ready( ProviderEventDetails( message="gRPC sync connection established" ), - context_values["metadata"], + context_values, ) self.connected = True From 7f692a61d688811801e8da6ecc90f8aca77be0c9 Mon Sep 17 00:00:00 2001 From: lea konvalinka Date: Wed, 30 Jul 2025 13:02:46 +0200 Subject: [PATCH 03/10] fix: type issues Signed-off-by: Konvalinka --- .../resolvers/process/connector/grpc_watcher.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/connector/grpc_watcher.py b/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/connector/grpc_watcher.py index af20f4c4..34659a53 100644 --- a/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/connector/grpc_watcher.py +++ b/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/connector/grpc_watcher.py @@ -214,8 +214,12 @@ def _fetch_metadata(self) -> typing.Optional[sync_pb2.GetMetadataResponse]: return None context_values_request = sync_pb2.GetMetadataRequest() + context_values_response: sync_pb2.GetMetadataResponse try: - return self.stub.GetMetadata(context_values_request, wait_for_ready=True) + context_values_response = self.stub.GetMetadata( + context_values_request, wait_for_ready=True + ) + return context_values_response except grpc.RpcError as e: logger.debug(f"Error getting sync metadata: {e}") return None @@ -230,9 +234,7 @@ def listen(self) -> None: while self.active: try: - context_values_response: typing.Optional[ - sync_pb2.GetMetadataResponse - ] = self._fetch_metadata() + context_values_response = self._fetch_metadata() request = sync_pb2.SyncFlagsRequest(**request_args) @@ -246,7 +248,7 @@ def listen(self) -> None: ) self.flag_store.update(json.loads(flag_str)) - context_values = None + context_values = {} if flag_rsp.sync_context: context_values = MessageToDict(flag_rsp.sync_context) elif context_values_response: From 6d6ebb8d94d3703a81535f96a13c34775b747375 Mon Sep 17 00:00:00 2001 From: lea konvalinka Date: Wed, 30 Jul 2025 14:58:15 +0200 Subject: [PATCH 04/10] fix: pr feedback Signed-off-by: Konvalinka --- .../process/connector/grpc_watcher.py | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/connector/grpc_watcher.py b/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/connector/grpc_watcher.py index 34659a53..e8839199 100644 --- a/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/connector/grpc_watcher.py +++ b/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/connector/grpc_watcher.py @@ -6,6 +6,7 @@ import grpc from google.protobuf.json_format import MessageToDict +from grpc import StatusCode from openfeature.evaluation_context import EvaluationContext from openfeature.event import ProviderEventDetails @@ -209,20 +210,22 @@ def _create_request_args(self) -> dict: return request_args - def _fetch_metadata(self) -> typing.Optional[sync_pb2.GetMetadataResponse]: + def _fetch_metadata(self) -> dict: if self.config.sync_metadata_disabled: - return None + return {} context_values_request = sync_pb2.GetMetadataRequest() - context_values_response: sync_pb2.GetMetadataResponse try: context_values_response = self.stub.GetMetadata( context_values_request, wait_for_ready=True ) - return context_values_response + return MessageToDict(context_values_response) except grpc.RpcError as e: - logger.debug(f"Error getting sync metadata: {e}") - return None + if e.code() == StatusCode.UNIMPLEMENTED: + logger.debug("Metadata endpoint disabled") + return {} + else: + raise e def listen(self) -> None: call_args = ( @@ -234,7 +237,7 @@ def listen(self) -> None: while self.active: try: - context_values_response = self._fetch_metadata() + context_values = self._fetch_metadata()["metadata"] request = sync_pb2.SyncFlagsRequest(**request_args) @@ -248,13 +251,8 @@ def listen(self) -> None: ) self.flag_store.update(json.loads(flag_str)) - context_values = {} if flag_rsp.sync_context: context_values = MessageToDict(flag_rsp.sync_context) - elif context_values_response: - context_values = MessageToDict(context_values_response)[ - "metadata" - ] if not self.connected: self.emit_provider_ready( From c75821f43eb7b8ac00b7c0256ae6b1328c7491f8 Mon Sep 17 00:00:00 2001 From: lea konvalinka Date: Thu, 31 Jul 2025 08:45:16 +0200 Subject: [PATCH 05/10] fix: tests Signed-off-by: Konvalinka --- .../flagd/resolvers/process/connector/grpc_watcher.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/connector/grpc_watcher.py b/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/connector/grpc_watcher.py index e8839199..a735b9be 100644 --- a/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/connector/grpc_watcher.py +++ b/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/connector/grpc_watcher.py @@ -215,11 +215,12 @@ def _fetch_metadata(self) -> dict: return {} context_values_request = sync_pb2.GetMetadataRequest() + context_values_response: sync_pb2.GetMetadataResponse try: context_values_response = self.stub.GetMetadata( context_values_request, wait_for_ready=True ) - return MessageToDict(context_values_response) + return MessageToDict(context_values_response)["metadata"] except grpc.RpcError as e: if e.code() == StatusCode.UNIMPLEMENTED: logger.debug("Metadata endpoint disabled") @@ -237,7 +238,7 @@ def listen(self) -> None: while self.active: try: - context_values = self._fetch_metadata()["metadata"] + context_values = self._fetch_metadata() request = sync_pb2.SyncFlagsRequest(**request_args) From adee6440e282e6ae75bb9e06119199bec4279cea Mon Sep 17 00:00:00 2001 From: lea konvalinka Date: Thu, 31 Jul 2025 08:59:18 +0200 Subject: [PATCH 06/10] fix: type issues Signed-off-by: Konvalinka --- .../resolvers/process/connector/grpc_watcher.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/connector/grpc_watcher.py b/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/connector/grpc_watcher.py index a735b9be..65d44cc7 100644 --- a/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/connector/grpc_watcher.py +++ b/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/connector/grpc_watcher.py @@ -210,9 +210,9 @@ def _create_request_args(self) -> dict: return request_args - def _fetch_metadata(self) -> dict: + def _fetch_metadata(self) -> typing.Optional[sync_pb2.GetMetadataResponse]: if self.config.sync_metadata_disabled: - return {} + return None context_values_request = sync_pb2.GetMetadataRequest() context_values_response: sync_pb2.GetMetadataResponse @@ -220,11 +220,11 @@ def _fetch_metadata(self) -> dict: context_values_response = self.stub.GetMetadata( context_values_request, wait_for_ready=True ) - return MessageToDict(context_values_response)["metadata"] + return context_values_response except grpc.RpcError as e: if e.code() == StatusCode.UNIMPLEMENTED: - logger.debug("Metadata endpoint disabled") - return {} + logger.debug(f"Error getting sync metadata: {e}") + return None else: raise e @@ -238,7 +238,7 @@ def listen(self) -> None: while self.active: try: - context_values = self._fetch_metadata() + context_values_response = self._fetch_metadata() request = sync_pb2.SyncFlagsRequest(**request_args) @@ -252,8 +252,13 @@ def listen(self) -> None: ) self.flag_store.update(json.loads(flag_str)) + context_values = {} if flag_rsp.sync_context: context_values = MessageToDict(flag_rsp.sync_context) + elif context_values_response: + context_values = MessageToDict(context_values_response)[ + "metadata" + ] if not self.connected: self.emit_provider_ready( From 4ebebf8742fc46547204b241f5d0b89d711c1da0 Mon Sep 17 00:00:00 2001 From: Lea Konvalinka Date: Mon, 4 Aug 2025 14:23:05 +0200 Subject: [PATCH 07/10] Add grpc_watcher unit tests Signed-off-by: Konvalinka --- .../tests/test_grpc_watcher.py | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 providers/openfeature-provider-flagd/tests/test_grpc_watcher.py diff --git a/providers/openfeature-provider-flagd/tests/test_grpc_watcher.py b/providers/openfeature-provider-flagd/tests/test_grpc_watcher.py new file mode 100644 index 00000000..1db6779b --- /dev/null +++ b/providers/openfeature-provider-flagd/tests/test_grpc_watcher.py @@ -0,0 +1,126 @@ +import threading +import time +import unittest +from unittest.mock import MagicMock, Mock, patch + +from google.protobuf.json_format import MessageToDict +from google.protobuf.struct_pb2 import Struct +from grpc import Channel + +from openfeature.contrib.provider.flagd.config import Config +from openfeature.contrib.provider.flagd.resolvers.process.connector.grpc_watcher import ( + GrpcWatcher, +) +from openfeature.contrib.provider.flagd.resolvers.process.flags import FlagStore +from openfeature.event import ProviderEventDetails +from openfeature.schemas.protobuf.flagd.sync.v1.sync_pb2 import ( + GetMetadataResponse, + SyncFlagsResponse, +) +from openfeature.schemas.protobuf.flagd.sync.v1.sync_pb2_grpc import FlagSyncServiceStub + + +class TestGrpcWatcher(unittest.TestCase): + def setUp(self): + config = Mock(spec=Config) + config.retry_backoff_ms = 1000 + config.retry_backoff_max_ms = 5000 + config.retry_grace_period = 5 + config.stream_deadline_ms = 1000 + config.deadline_ms = 5000 + config.selector = None + config.provider_id = None + config.tls = False + config.cert_path = None + config.channel_credentials = None + config.host = "localhost" + config.port = 5000 + config.sync_metadata_disabled = False + + flag_store = Mock(spec=FlagStore) + flag_store.update.return_value = None + self.emit_provider_ready = Mock() + emit_provider_error = Mock() + emit_provider_stale = Mock() + channel = Mock(spec=Channel) + + with patch( + "openfeature.contrib.provider.flagd.resolvers.process.connector.grpc_watcher.GrpcWatcher._generate_channel", + return_value=channel, + ): + self.grpc_watcher = GrpcWatcher( + config=config, + flag_store=flag_store, + emit_provider_ready=self.emit_provider_ready, + emit_provider_error=emit_provider_error, + emit_provider_stale=emit_provider_stale, + ) + self.mock_stub = MagicMock(spec=FlagSyncServiceStub) + self.mock_metadata = GetMetadataResponse(metadata={"attribute": "value1"}) + self.mock_stub.GetMetadata = Mock(return_value=self.mock_metadata) + self.grpc_watcher.stub = self.mock_stub + self.grpc_watcher.active = True + self.shutdown_thread = lambda: threading.Thread( + target=self.shutdown_after_x_seconds + ) + + def shutdown_after_x_seconds(self, seconds=1): + time.sleep(seconds) + self.grpc_watcher.shutdown() + + def test_listen_with_sync_metadata_and_sync_context(self): + sync_context = Struct() + sync_context.update({"attribute": "value"}) + mock_stream_with_sync_context = iter( + [ + SyncFlagsResponse( + flag_configuration='{"flag_key": "flag_value"}', + sync_context=sync_context, + ), + ] + ) + self.mock_stub.SyncFlags = Mock(return_value=mock_stream_with_sync_context) + + self.shutdown_thread().start() + + self.grpc_watcher.listen() + + self.emit_provider_ready.assert_called_once_with( + ProviderEventDetails(message="gRPC sync connection established"), + MessageToDict(sync_context), + ) + + def test_listen_with_sync_metadata_only(self): + mock_stream_no_sync_context = iter( + [ + SyncFlagsResponse(flag_configuration='{"flag_key": "flag_value"}'), + ] + ) + self.mock_stub.SyncFlags = Mock(return_value=mock_stream_no_sync_context) + + self.shutdown_thread().start() + + self.grpc_watcher.listen() + + self.emit_provider_ready.assert_called_once_with( + ProviderEventDetails(message="gRPC sync connection established"), + MessageToDict(self.mock_metadata.metadata), + ) + + def test_listen_with_sync_metadata_disabled_in_config(self): + self.grpc_watcher.config.sync_metadata_disabled = True + mock_stream_no_sync_context = iter( + [ + SyncFlagsResponse(flag_configuration='{"flag_key": "flag_value"}'), + ] + ) + self.mock_stub.SyncFlags = Mock(return_value=mock_stream_no_sync_context) + self.shutdown_thread().start() + + self.grpc_watcher.listen() + + self.mock_stub.GetMetadata.assert_not_called() + + self.emit_provider_ready.assert_called_once_with( + ProviderEventDetails(message="gRPC sync connection established"), {} + ) From 3047216e26114b349c4e24d85dbf5b028c615a9e Mon Sep 17 00:00:00 2001 From: Lea Konvalinka Date: Tue, 5 Aug 2025 10:05:30 +0200 Subject: [PATCH 08/10] pr feedback Signed-off-by: Konvalinka --- .github/workflows/build.yml | 4 +-- .../process/connector/grpc_watcher.py | 5 ++-- .../tests/test_grpc_watcher.py | 29 ++++++++++--------- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7871c723..5ab26ec8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -37,8 +37,8 @@ jobs: - 'hooks/openfeature-hooks-opentelemetry/**' providers/openfeature-provider-env-var: - 'providers/openfeature-provider-env-var/**' - providers/providers/openfeature-provider-flagd: - - 'providers/providers/openfeature-provider-flagd/**' + providers/openfeature-provider-flagd: + - 'providers/openfeature-provider-flagd/**' providers/openfeature-provider-flipt: - 'providers/openfeature-provider-flipt/**' providers/openfeature-provider-ofrep: diff --git a/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/connector/grpc_watcher.py b/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/connector/grpc_watcher.py index 65d44cc7..8a0184e3 100644 --- a/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/connector/grpc_watcher.py +++ b/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/connector/grpc_watcher.py @@ -215,10 +215,9 @@ def _fetch_metadata(self) -> typing.Optional[sync_pb2.GetMetadataResponse]: return None context_values_request = sync_pb2.GetMetadataRequest() - context_values_response: sync_pb2.GetMetadataResponse try: - context_values_response = self.stub.GetMetadata( - context_values_request, wait_for_ready=True + context_values_response: sync_pb2.GetMetadataResponse = ( + self.stub.GetMetadata(context_values_request, wait_for_ready=True) ) return context_values_response except grpc.RpcError as e: diff --git a/providers/openfeature-provider-flagd/tests/test_grpc_watcher.py b/providers/openfeature-provider-flagd/tests/test_grpc_watcher.py index 1db6779b..9ee78e07 100644 --- a/providers/openfeature-provider-flagd/tests/test_grpc_watcher.py +++ b/providers/openfeature-provider-flagd/tests/test_grpc_watcher.py @@ -60,13 +60,6 @@ def setUp(self): self.mock_stub.GetMetadata = Mock(return_value=self.mock_metadata) self.grpc_watcher.stub = self.mock_stub self.grpc_watcher.active = True - self.shutdown_thread = lambda: threading.Thread( - target=self.shutdown_after_x_seconds - ) - - def shutdown_after_x_seconds(self, seconds=1): - time.sleep(seconds) - self.grpc_watcher.shutdown() def test_listen_with_sync_metadata_and_sync_context(self): sync_context = Struct() @@ -81,9 +74,12 @@ def test_listen_with_sync_metadata_and_sync_context(self): ) self.mock_stub.SyncFlags = Mock(return_value=mock_stream_with_sync_context) - self.shutdown_thread().start() + listener = threading.Thread(target=self.grpc_watcher.listen) + listener.start() - self.grpc_watcher.listen() + time.sleep(0.5) + self.grpc_watcher.shutdown() + listener.join(timeout=1) self.emit_provider_ready.assert_called_once_with( ProviderEventDetails(message="gRPC sync connection established"), @@ -98,9 +94,12 @@ def test_listen_with_sync_metadata_only(self): ) self.mock_stub.SyncFlags = Mock(return_value=mock_stream_no_sync_context) - self.shutdown_thread().start() + listener = threading.Thread(target=self.grpc_watcher.listen) + listener.start() - self.grpc_watcher.listen() + time.sleep(0.5) + self.grpc_watcher.shutdown() + listener.join(timeout=1) self.emit_provider_ready.assert_called_once_with( ProviderEventDetails(message="gRPC sync connection established"), @@ -115,9 +114,13 @@ def test_listen_with_sync_metadata_disabled_in_config(self): ] ) self.mock_stub.SyncFlags = Mock(return_value=mock_stream_no_sync_context) - self.shutdown_thread().start() - self.grpc_watcher.listen() + listener = threading.Thread(target=self.grpc_watcher.listen) + listener.start() + + time.sleep(0.5) + self.grpc_watcher.shutdown() + listener.join(timeout=1) self.mock_stub.GetMetadata.assert_not_called() From aaf635e84cce2fbc4a11753469c6dfa49f7b395b Mon Sep 17 00:00:00 2001 From: lea konvalinka Date: Tue, 5 Aug 2025 11:39:05 +0200 Subject: [PATCH 09/10] pr feedback Signed-off-by: Konvalinka --- .../tests/test_grpc_watcher.py | 62 ++++++++++--------- 1 file changed, 34 insertions(+), 28 deletions(-) diff --git a/providers/openfeature-provider-flagd/tests/test_grpc_watcher.py b/providers/openfeature-provider-flagd/tests/test_grpc_watcher.py index 9ee78e07..ad7a8015 100644 --- a/providers/openfeature-provider-flagd/tests/test_grpc_watcher.py +++ b/providers/openfeature-provider-flagd/tests/test_grpc_watcher.py @@ -1,5 +1,6 @@ import threading import time +import typing import unittest from unittest.mock import MagicMock, Mock, patch @@ -39,10 +40,12 @@ def setUp(self): flag_store = Mock(spec=FlagStore) flag_store.update.return_value = None - self.emit_provider_ready = Mock() emit_provider_error = Mock() emit_provider_stale = Mock() channel = Mock(spec=Channel) + self.provider_done = False + self.provider_details: typing.Optional[ProviderEventDetails] = None + self.context: typing.Optional[dict] = None with patch( "openfeature.contrib.provider.flagd.resolvers.process.connector.grpc_watcher.GrpcWatcher._generate_channel", @@ -51,7 +54,7 @@ def setUp(self): self.grpc_watcher = GrpcWatcher( config=config, flag_store=flag_store, - emit_provider_ready=self.emit_provider_ready, + emit_provider_ready=self.provider_ready, emit_provider_error=emit_provider_error, emit_provider_stale=emit_provider_stale, ) @@ -61,6 +64,23 @@ def setUp(self): self.grpc_watcher.stub = self.mock_stub self.grpc_watcher.active = True + def provider_ready(self, details: ProviderEventDetails, context: dict): + self.provider_done = True + self.provider_details = details + self.context = context + + def run_listen_and_shutdown_after(self): + listener = threading.Thread(target=self.grpc_watcher.listen) + listener.start() + for _i in range(0, 100): + if self.provider_done: + break + time.sleep(0.001) + + self.assertTrue(self.provider_done) + self.grpc_watcher.shutdown() + listener.join(timeout=0.5) + def test_listen_with_sync_metadata_and_sync_context(self): sync_context = Struct() sync_context.update({"attribute": "value"}) @@ -74,17 +94,12 @@ def test_listen_with_sync_metadata_and_sync_context(self): ) self.mock_stub.SyncFlags = Mock(return_value=mock_stream_with_sync_context) - listener = threading.Thread(target=self.grpc_watcher.listen) - listener.start() - - time.sleep(0.5) - self.grpc_watcher.shutdown() - listener.join(timeout=1) + self.run_listen_and_shutdown_after() - self.emit_provider_ready.assert_called_once_with( - ProviderEventDetails(message="gRPC sync connection established"), - MessageToDict(sync_context), + self.assertEqual( + self.provider_details.message, "gRPC sync connection established" ) + self.assertEqual(self.context, MessageToDict(sync_context)) def test_listen_with_sync_metadata_only(self): mock_stream_no_sync_context = iter( @@ -94,17 +109,12 @@ def test_listen_with_sync_metadata_only(self): ) self.mock_stub.SyncFlags = Mock(return_value=mock_stream_no_sync_context) - listener = threading.Thread(target=self.grpc_watcher.listen) - listener.start() - - time.sleep(0.5) - self.grpc_watcher.shutdown() - listener.join(timeout=1) + self.run_listen_and_shutdown_after() - self.emit_provider_ready.assert_called_once_with( - ProviderEventDetails(message="gRPC sync connection established"), - MessageToDict(self.mock_metadata.metadata), + self.assertEqual( + self.provider_details.message, "gRPC sync connection established" ) + self.assertEqual(self.context, MessageToDict(self.mock_metadata.metadata)) def test_listen_with_sync_metadata_disabled_in_config(self): self.grpc_watcher.config.sync_metadata_disabled = True @@ -115,15 +125,11 @@ def test_listen_with_sync_metadata_disabled_in_config(self): ) self.mock_stub.SyncFlags = Mock(return_value=mock_stream_no_sync_context) - listener = threading.Thread(target=self.grpc_watcher.listen) - listener.start() - - time.sleep(0.5) - self.grpc_watcher.shutdown() - listener.join(timeout=1) + self.run_listen_and_shutdown_after() self.mock_stub.GetMetadata.assert_not_called() - self.emit_provider_ready.assert_called_once_with( - ProviderEventDetails(message="gRPC sync connection established"), {} + self.assertEqual( + self.provider_details.message, "gRPC sync connection established" ) + self.assertEqual(self.context, {}) From edac1d652f9bcb27e2d92113533e769065d42052 Mon Sep 17 00:00:00 2001 From: lea konvalinka Date: Tue, 5 Aug 2025 12:25:30 +0200 Subject: [PATCH 10/10] up test time limit Signed-off-by: Konvalinka --- providers/openfeature-provider-flagd/tests/test_errors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/openfeature-provider-flagd/tests/test_errors.py b/providers/openfeature-provider-flagd/tests/test_errors.py index dc5fded2..e64ca376 100644 --- a/providers/openfeature-provider-flagd/tests/test_errors.py +++ b/providers/openfeature-provider-flagd/tests/test_errors.py @@ -138,5 +138,5 @@ def fail(*args, **kwargs): ) elapsed = time.time() - t - assert abs(elapsed - wait * 0.001) < 0.15 + assert abs(elapsed - wait * 0.001) < 0.17 assert init_failed