Skip to content

Commit 2db5904

Browse files
committed
fixup: adding gherkin tests for evaluations, and fxing found issues
Signed-off-by: Simon Schrottner <[email protected]>
1 parent e9cd2ed commit 2db5904

25 files changed

+976
-559
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Subproject commit 3c737a6e86ae0aa9bd81fcbfe8b6ada9a33993a7

providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/in_process.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,6 @@ def resolve_float_details(
8484
evaluation_context: typing.Optional[EvaluationContext] = None,
8585
) -> FlagResolutionDetails[float]:
8686
result = self._resolve(key, default_value, evaluation_context)
87-
if not isinstance(result.value, float):
88-
result.value = float(result.value)
8987
return result
9088

9189
def resolve_integer_details(
@@ -95,8 +93,6 @@ def resolve_integer_details(
9593
evaluation_context: typing.Optional[EvaluationContext] = None,
9694
) -> FlagResolutionDetails[int]:
9795
result = self._resolve(key, default_value, evaluation_context)
98-
if not isinstance(result.value, int):
99-
result.value = int(result.value)
10096
return result
10197

10298
def resolve_object_details(

providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/connector/grpc_watcher.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,14 @@ def __init__(
3030
):
3131
self.flag_store = flag_store
3232
channel_factory = grpc.secure_channel if config.tls else grpc.insecure_channel
33-
self.channel = channel_factory(f"{config.host}:{config.port}")
33+
self.channel = channel_factory(
34+
f"{config.host}:{config.port}",
35+
options=(
36+
("grpc.max_reconnect_backoff_ms", 1000),
37+
("grpc.initial_reconnect_backoff_ms", 1000),
38+
("grpc.keepalive_time_ms", 1000),
39+
),
40+
)
3441
self.stub = sync_pb2_grpc.FlagSyncServiceStub(self.channel)
3542
self.timeout = config.timeout
3643
self.retry_backoff_seconds = config.retry_backoff_seconds
@@ -62,11 +69,10 @@ def shutdown(self) -> None:
6269
self.active = False
6370

6471
def sync_flags(self) -> None:
65-
request = sync_pb2.SyncFlagsRequest(selector=self.selector) # type:ignore[attr-defined]
66-
6772
retry_delay = self.retry_backoff_seconds
6873
while self.active:
6974
try:
75+
request = sync_pb2.SyncFlagsRequest(selector=self.selector) # type:ignore[attr-defined]
7076
logger.debug("Setting up gRPC sync flags connection")
7177
for flag_rsp in self.stub.SyncFlags(request):
7278
flag_str = flag_rsp.flag_configuration
@@ -107,4 +113,4 @@ def sync_flags(self) -> None:
107113
)
108114
logger.info(f"gRPC sync disconnected, reconnecting in {retry_delay}s")
109115
time.sleep(retry_delay)
110-
retry_delay = min(2 * retry_delay, self.MAX_BACK_OFF)
116+
retry_delay = min(2, self.MAX_BACK_OFF)

providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/flags.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ def from_dict(cls, key: str, data: dict) -> "Flag":
7272
data["default_variant"] = data["defaultVariant"]
7373
del data["defaultVariant"]
7474

75+
if "source" in data:
76+
del data["source"]
77+
if "selector" in data:
78+
del data["selector"]
7579
try:
7680
flag = cls(key=key, **data)
7781
return flag
Lines changed: 15 additions & 300 deletions
Original file line numberDiff line numberDiff line change
@@ -1,312 +1,27 @@
1-
import logging
2-
import time
31
import typing
42

53
import pytest
6-
from pytest_bdd import parsers, then, when
4+
from testcontainers.core.container import DockerContainer
75

8-
from openfeature.client import OpenFeatureClient, ProviderEvent
9-
from openfeature.evaluation_context import EvaluationContext
6+
from tests.e2e.flagd_container import FlagDContainer
7+
from tests.e2e.steps import * # noqa: F403
108

119
JsonPrimitive = typing.Union[str, bool, float, int]
1210

1311

14-
def to_bool(s: str) -> bool:
15-
return s.lower() == "true"
16-
17-
18-
@pytest.fixture
19-
def evaluation_context() -> EvaluationContext:
20-
return EvaluationContext()
21-
22-
23-
@when(
24-
parsers.cfparse(
25-
'a zero-value boolean flag with key "{key}" is evaluated with default value "{default:bool}"',
26-
extra_types={"bool": to_bool},
27-
),
28-
target_fixture="key_and_default",
29-
)
30-
@when(
31-
parsers.cfparse(
32-
'a zero-value string flag with key "{key}" is evaluated with default value "{default}"',
33-
),
34-
target_fixture="key_and_default",
35-
)
36-
@when(
37-
parsers.cfparse(
38-
'a string flag with key "{key}" is evaluated with default value "{default}"'
39-
),
40-
target_fixture="key_and_default",
41-
)
42-
@when(
43-
parsers.cfparse(
44-
'a zero-value integer flag with key "{key}" is evaluated with default value {default:d}',
45-
),
46-
target_fixture="key_and_default",
47-
)
48-
@when(
49-
parsers.cfparse(
50-
'an integer flag with key "{key}" is evaluated with default value {default:d}',
51-
),
52-
target_fixture="key_and_default",
53-
)
54-
@when(
55-
parsers.cfparse(
56-
'a zero-value float flag with key "{key}" is evaluated with default value {default:f}',
57-
),
58-
target_fixture="key_and_default",
59-
)
60-
def setup_key_and_default(
61-
key: str, default: JsonPrimitive
62-
) -> typing.Tuple[str, JsonPrimitive]:
63-
return (key, default)
64-
65-
66-
@when(
67-
parsers.cfparse(
68-
'a context containing a targeting key with value "{targeting_key}"'
69-
),
70-
)
71-
def assign_targeting_context(evaluation_context: EvaluationContext, targeting_key: str):
72-
"""a context containing a targeting key with value <targeting key>."""
73-
evaluation_context.targeting_key = targeting_key
74-
75-
76-
@when(
77-
parsers.cfparse('a context containing a key "{key}", with value "{value}"'),
78-
)
79-
@when(
80-
parsers.cfparse('a context containing a key "{key}", with value {value:d}'),
81-
)
82-
def update_context(
83-
evaluation_context: EvaluationContext, key: str, value: JsonPrimitive
84-
):
85-
"""a context containing a key and value."""
86-
evaluation_context.attributes[key] = value
87-
88-
89-
@when(
90-
parsers.cfparse(
91-
'a context containing a nested property with outer key "{outer}" and inner key "{inner}", with value "{value}"'
92-
),
93-
)
94-
@when(
95-
parsers.cfparse(
96-
'a context containing a nested property with outer key "{outer}" and inner key "{inner}", with value {value:d}'
97-
),
98-
)
99-
def update_context_nested(
100-
evaluation_context: EvaluationContext,
101-
outer: str,
102-
inner: str,
103-
value: typing.Union[str, int],
104-
):
105-
"""a context containing a nested property with outer key, and inner key, and value."""
106-
if outer not in evaluation_context.attributes:
107-
evaluation_context.attributes[outer] = {}
108-
evaluation_context.attributes[outer][inner] = value
109-
110-
111-
@then(
112-
parsers.cfparse(
113-
'the resolved boolean zero-value should be "{expected_value:bool}"',
114-
extra_types={"bool": to_bool},
115-
)
116-
)
117-
def assert_boolean_value(
118-
client: OpenFeatureClient,
119-
key_and_default: tuple,
120-
expected_value: bool,
121-
evaluation_context: EvaluationContext,
122-
):
123-
key, default = key_and_default
124-
evaluation_result = client.get_boolean_value(key, default, evaluation_context)
125-
assert evaluation_result == expected_value
126-
127-
128-
@then(
129-
parsers.cfparse(
130-
"the resolved integer zero-value should be {expected_value:d}",
131-
)
132-
)
133-
@then(parsers.cfparse("the returned value should be {expected_value:d}"))
134-
def assert_integer_value(
135-
client: OpenFeatureClient,
136-
key_and_default: tuple,
137-
expected_value: bool,
138-
evaluation_context: EvaluationContext,
139-
):
140-
key, default = key_and_default
141-
evaluation_result = client.get_integer_value(key, default, evaluation_context)
142-
assert evaluation_result == expected_value
143-
144-
145-
@then(
146-
parsers.cfparse(
147-
"the resolved float zero-value should be {expected_value:f}",
148-
)
149-
)
150-
def assert_float_value(
151-
client: OpenFeatureClient,
152-
key_and_default: tuple,
153-
expected_value: bool,
154-
evaluation_context: EvaluationContext,
155-
):
156-
key, default = key_and_default
157-
evaluation_result = client.get_float_value(key, default, evaluation_context)
158-
assert evaluation_result == expected_value
159-
160-
161-
@then(parsers.cfparse('the returned value should be "{expected_value}"'))
162-
def assert_string_value(
163-
client: OpenFeatureClient,
164-
key_and_default: tuple,
165-
expected_value: bool,
166-
evaluation_context: EvaluationContext,
167-
):
168-
key, default = key_and_default
169-
evaluation_result = client.get_string_value(key, default, evaluation_context)
170-
assert evaluation_result == expected_value
171-
172-
173-
@then(
174-
parsers.cfparse(
175-
'the resolved string zero-value should be ""',
176-
)
177-
)
178-
def assert_empty_string(
179-
client: OpenFeatureClient,
180-
key_and_default: tuple,
181-
evaluation_context: EvaluationContext,
182-
):
183-
key, default = key_and_default
184-
evaluation_result = client.get_string_value(key, default, evaluation_context)
185-
assert evaluation_result == ""
186-
187-
188-
@then(parsers.cfparse('the returned reason should be "{reason}"'))
189-
def assert_reason(
190-
client: OpenFeatureClient,
191-
key_and_default: tuple,
192-
evaluation_context: EvaluationContext,
193-
reason: str,
194-
):
195-
"""the returned reason should be <reason>."""
196-
key, default = key_and_default
197-
evaluation_result = client.get_string_details(key, default, evaluation_context)
198-
assert evaluation_result.reason.value == reason
199-
200-
201-
@pytest.fixture
202-
def handles() -> list:
203-
return []
204-
205-
206-
@when(
207-
parsers.cfparse(
208-
"a {event_type:ProviderEvent} handler is added",
209-
extra_types={"ProviderEvent": ProviderEvent},
210-
),
211-
target_fixture="handles",
212-
)
213-
def add_event_handler(
214-
client: OpenFeatureClient, event_type: ProviderEvent, handles: list
215-
):
216-
def handler(event):
217-
logging.info((event_type, event))
218-
handles.append(
219-
{
220-
"type": event_type,
221-
"event": event,
222-
}
223-
)
224-
225-
client.add_handler(event_type, handler)
226-
return handles
227-
228-
229-
@when(
230-
parsers.cfparse(
231-
"a {event_type:ProviderEvent} handler and a {event_type2:ProviderEvent} handler are added",
232-
extra_types={"ProviderEvent": ProviderEvent},
233-
),
234-
target_fixture="handles",
235-
)
236-
def add_event_handlers(
237-
client: OpenFeatureClient,
238-
event_type: ProviderEvent,
239-
event_type2: ProviderEvent,
240-
handles: list,
241-
):
242-
add_event_handler(client, event_type, handles)
243-
add_event_handler(client, event_type2, handles)
244-
245-
246-
def assert_handlers(
247-
handles, event_type: ProviderEvent, max_wait: int = 2, num_events: int = 1
248-
):
249-
poll_interval = 0.05
250-
while max_wait > 0:
251-
if sum([h["type"] == event_type for h in handles]) < num_events:
252-
max_wait -= poll_interval
253-
time.sleep(poll_interval)
254-
continue
255-
break
256-
257-
logging.info(f"asserting num({event_type}) >= {num_events}: {handles}")
258-
actual_num_events = sum([h["type"] == event_type for h in handles])
259-
assert (
260-
num_events <= actual_num_events
261-
), f"Expected {num_events} but got {actual_num_events}: {handles}"
262-
263-
264-
@then(
265-
parsers.cfparse(
266-
"the {event_type:ProviderEvent} handler must run",
267-
extra_types={"ProviderEvent": ProviderEvent},
268-
)
269-
)
270-
@then(
271-
parsers.cfparse(
272-
"the {event_type:ProviderEvent} handler must run when the provider connects",
273-
extra_types={"ProviderEvent": ProviderEvent},
274-
)
275-
)
276-
def assert_handler_run(handles, event_type: ProviderEvent):
277-
assert_handlers(handles, event_type, max_wait=3)
278-
279-
280-
@then(
281-
parsers.cfparse(
282-
"the {event_type:ProviderEvent} handler must run when the provider's connection is lost",
283-
extra_types={"ProviderEvent": ProviderEvent},
284-
)
285-
)
286-
def assert_disconnect_handler(handles, event_type: ProviderEvent):
287-
# docker sync upstream restarts every 5s, waiting 2 cycles reduces test noise
288-
assert_handlers(handles, event_type, max_wait=10)
289-
290-
291-
@then(
292-
parsers.cfparse(
293-
"when the connection is reestablished the {event_type:ProviderEvent} handler must run again",
294-
extra_types={"ProviderEvent": ProviderEvent},
12+
@pytest.fixture(autouse=True, scope="module")
13+
def setup(request, port, image):
14+
container: DockerContainer = FlagDContainer(
15+
image=image,
16+
port=port,
29517
)
296-
)
297-
def assert_disconnect_error(client: OpenFeatureClient, event_type: ProviderEvent):
298-
reconnect_handles = []
299-
add_event_handler(client, event_type, reconnect_handles)
300-
assert_handlers(reconnect_handles, event_type, max_wait=6)
18+
# Setup code
19+
c = container.start()
30120

21+
def fin():
22+
c.stop()
30223

303-
@then(parsers.cfparse('the event details must indicate "{key}" was altered'))
304-
def assert_flag_changed(handles, key):
305-
handle = None
306-
for h in handles:
307-
if h["type"] == ProviderEvent.PROVIDER_CONFIGURATION_CHANGED:
308-
handle = h
309-
break
24+
# Teardown code
25+
request.addfinalizer(fin)
31026

311-
assert handle is not None
312-
assert key in handle["event"].flags_changed
27+
return c.get_exposed_port(port)

0 commit comments

Comments
 (0)