|
1 | 1 | """Tests for isolated OpenFeature API instances (spec section 1.8).""" |
2 | 2 |
|
| 3 | +import gc |
3 | 4 | import inspect |
4 | 5 | import time |
| 6 | +import weakref |
5 | 7 | from unittest.mock import MagicMock |
6 | 8 |
|
7 | 9 | import pytest |
|
12 | 14 | from openfeature.event import ProviderEvent, ProviderEventDetails |
13 | 15 | from openfeature.hook import Hook |
14 | 16 | from openfeature.isolated import OpenFeatureAPI, create_api |
15 | | -from openfeature.provider import FeatureProvider, ProviderStatus |
| 17 | +from openfeature.provider import FeatureProvider, Metadata, ProviderStatus |
16 | 18 | from openfeature.provider.no_op_provider import NoOpProvider |
17 | 19 | from openfeature.transaction_context import ContextVarsTransactionContextPropagator |
18 | 20 |
|
@@ -46,8 +48,10 @@ def test_isolated_instance_is_openfeature_api(): |
46 | 48 | _ISOLATED_API_PUBLIC_METHODS = ( |
47 | 49 | "add_handler", |
48 | 50 | "add_hooks", |
| 51 | + "clear_evaluation_context", |
49 | 52 | "clear_hooks", |
50 | 53 | "clear_providers", |
| 54 | + "clear_transaction_context_propagator", |
51 | 55 | "get_client", |
52 | 56 | "get_evaluation_context", |
53 | 57 | "get_hooks", |
@@ -189,6 +193,37 @@ def test_provider_can_be_rebound_after_being_released(): |
189 | 193 | assert api2.get_provider() is provider |
190 | 194 |
|
191 | 195 |
|
| 196 | +def test_set_provider_rejects_non_weak_referenceable_provider(): |
| 197 | + """Providers must be weak-referenceable so the SDK can track bindings |
| 198 | + without leaking memory; surfacing this requirement up front (rather than |
| 199 | + silently skipping the spec 1.8.4 check) avoids hard-to-diagnose bugs.""" |
| 200 | + |
| 201 | + # A direct ``object`` subclass with ``__slots__`` and no ``__weakref__`` |
| 202 | + # entry; instances are not weak-referenceable. Implements the |
| 203 | + # ``FeatureProvider`` protocol structurally rather than via inheritance |
| 204 | + # (which would inherit ``__weakref__`` from the parent class). |
| 205 | + class NotWeakReferenceable: |
| 206 | + __slots__ = () |
| 207 | + |
| 208 | + def attach(self, on_emit): |
| 209 | + pass |
| 210 | + |
| 211 | + def detach(self): |
| 212 | + pass |
| 213 | + |
| 214 | + def get_metadata(self): |
| 215 | + return Metadata(name="not-weak-referenceable") |
| 216 | + |
| 217 | + def get_provider_hooks(self): |
| 218 | + return [] |
| 219 | + |
| 220 | + provider = NotWeakReferenceable() |
| 221 | + api_instance = create_api() |
| 222 | + |
| 223 | + with pytest.raises(TypeError, match="weak-referenceable"): |
| 224 | + api_instance.set_provider(provider) # type: ignore[arg-type] |
| 225 | + |
| 226 | + |
192 | 227 | # --- Isolated state: hooks --- |
193 | 228 |
|
194 | 229 |
|
@@ -320,6 +355,25 @@ def test_isolated_event_handlers_do_not_affect_global(): |
320 | 355 | assert handler.call_count == 0 |
321 | 356 |
|
322 | 357 |
|
| 358 | +def test_client_handlers_do_not_retain_clients(): |
| 359 | + """Registering an event handler on a client must not keep the client |
| 360 | + (or its owning API) alive once the user drops their references.""" |
| 361 | + api_instance = create_api() |
| 362 | + provider = NoOpProvider() |
| 363 | + api_instance.set_provider(provider) |
| 364 | + |
| 365 | + client = api_instance.get_client(domain="ephemeral") |
| 366 | + client.add_handler(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, MagicMock()) |
| 367 | + |
| 368 | + client_ref = weakref.ref(client) |
| 369 | + assert client_ref() is client |
| 370 | + |
| 371 | + del client |
| 372 | + gc.collect() |
| 373 | + |
| 374 | + assert client_ref() is None |
| 375 | + |
| 376 | + |
323 | 377 | # --- Provider lifecycle on isolated instances --- |
324 | 378 |
|
325 | 379 |
|
|
0 commit comments