Skip to content

Commit b506bdc

Browse files
committed
feat: Add Testcontainers and Gherkin execution for our test-harness
Signed-off-by: Simon Schrottner <[email protected]>
1 parent d936a47 commit b506bdc

File tree

4 files changed

+309
-0
lines changed

4 files changed

+309
-0
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import typing
2+
3+
from tests.e2eGherkin.steps import * # noqa: F403
4+
5+
JsonPrimitive = typing.Union[str, bool, float, int]
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
def to_bool(s: str) -> bool:
2+
return s.lower() == "true"
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
import time
2+
import typing
3+
4+
import pytest
5+
from pytest_bdd import given, parsers, then, when
6+
from tests.e2e.parsers import to_bool
7+
8+
from openfeature import api
9+
from openfeature.client import OpenFeatureClient
10+
from openfeature.evaluation_context import EvaluationContext
11+
from openfeature.event import EventDetails, ProviderEvent
12+
13+
JsonPrimitive = typing.Union[str, bool, float, int]
14+
15+
16+
@pytest.fixture
17+
def evaluation_context() -> EvaluationContext:
18+
return EvaluationContext()
19+
20+
21+
@given("a flagd provider is set", target_fixture="client")
22+
def setup_provider() -> OpenFeatureClient:
23+
return api.get_client()
24+
25+
26+
@when(
27+
parsers.cfparse(
28+
'a zero-value boolean flag with key "{key}" is evaluated with default value "{default:bool}"',
29+
extra_types={"bool": to_bool},
30+
),
31+
target_fixture="key_and_default",
32+
)
33+
@when(
34+
parsers.cfparse(
35+
'a zero-value string flag with key "{key}" is evaluated with default value "{default}"',
36+
),
37+
target_fixture="key_and_default",
38+
)
39+
@when(
40+
parsers.cfparse(
41+
'a string flag with key "{key}" is evaluated with default value "{default}"'
42+
),
43+
target_fixture="key_and_default",
44+
)
45+
@when(
46+
parsers.cfparse(
47+
'a zero-value integer flag with key "{key}" is evaluated with default value {default:d}',
48+
),
49+
target_fixture="key_and_default",
50+
)
51+
@when(
52+
parsers.cfparse(
53+
'an integer flag with key "{key}" is evaluated with default value {default:d}',
54+
),
55+
target_fixture="key_and_default",
56+
)
57+
@when(
58+
parsers.cfparse(
59+
'a zero-value float flag with key "{key}" is evaluated with default value {default:f}',
60+
),
61+
target_fixture="key_and_default",
62+
)
63+
def setup_key_and_default(
64+
key: str, default: JsonPrimitive
65+
) -> typing.Tuple[str, JsonPrimitive]:
66+
return (key, default)
67+
68+
69+
@when(
70+
parsers.cfparse(
71+
'a context containing a targeting key with value "{targeting_key}"'
72+
),
73+
)
74+
def assign_targeting_context(evaluation_context: EvaluationContext, targeting_key: str):
75+
"""a context containing a targeting key with value <targeting key>."""
76+
evaluation_context.targeting_key = targeting_key
77+
78+
79+
@when(
80+
parsers.cfparse('a context containing a key "{key}", with value "{value}"'),
81+
)
82+
@when(
83+
parsers.cfparse('a context containing a key "{key}", with value {value:d}'),
84+
)
85+
def update_context(
86+
evaluation_context: EvaluationContext, key: str, value: JsonPrimitive
87+
):
88+
"""a context containing a key and value."""
89+
evaluation_context.attributes[key] = value
90+
91+
92+
@when(
93+
parsers.cfparse(
94+
'a context containing a nested property with outer key "{outer}" and inner key "{inner}", with value "{value}"'
95+
),
96+
)
97+
@when(
98+
parsers.cfparse(
99+
'a context containing a nested property with outer key "{outer}" and inner key "{inner}", with value {value:d}'
100+
),
101+
)
102+
def update_context_nested(
103+
evaluation_context: EvaluationContext,
104+
outer: str,
105+
inner: str,
106+
value: typing.Union[str, int],
107+
):
108+
"""a context containing a nested property with outer key, and inner key, and value."""
109+
if outer not in evaluation_context.attributes:
110+
evaluation_context.attributes[outer] = {}
111+
evaluation_context.attributes[outer][inner] = value
112+
113+
114+
@then(
115+
parsers.cfparse(
116+
'the resolved boolean zero-value should be "{expected_value:bool}"',
117+
extra_types={"bool": to_bool},
118+
)
119+
)
120+
def assert_boolean_value(
121+
client: OpenFeatureClient,
122+
key_and_default: tuple,
123+
expected_value: bool,
124+
evaluation_context: EvaluationContext,
125+
):
126+
key, default = key_and_default
127+
evaluation_result = client.get_boolean_value(key, default, evaluation_context)
128+
assert evaluation_result == expected_value
129+
130+
131+
@then(
132+
parsers.cfparse(
133+
"the resolved integer zero-value should be {expected_value:d}",
134+
)
135+
)
136+
@then(parsers.cfparse("the returned value should be {expected_value:d}"))
137+
def assert_integer_value(
138+
client: OpenFeatureClient,
139+
key_and_default: tuple,
140+
expected_value: bool,
141+
evaluation_context: EvaluationContext,
142+
):
143+
key, default = key_and_default
144+
evaluation_result = client.get_integer_details(key, default, evaluation_context)
145+
assert evaluation_result == expected_value
146+
147+
148+
@then(
149+
parsers.cfparse(
150+
"the resolved float zero-value should be {expected_value:f}",
151+
)
152+
)
153+
def assert_float_value(
154+
client: OpenFeatureClient,
155+
key_and_default: tuple,
156+
expected_value: bool,
157+
evaluation_context: EvaluationContext,
158+
):
159+
key, default = key_and_default
160+
evaluation_result = client.get_float_value(key, default, evaluation_context)
161+
assert evaluation_result == expected_value
162+
163+
164+
@then(parsers.cfparse('the returned value should be "{expected_value}"'))
165+
def assert_string_value(
166+
client: OpenFeatureClient,
167+
key_and_default: tuple,
168+
expected_value: bool,
169+
evaluation_context: EvaluationContext,
170+
):
171+
key, default = key_and_default
172+
evaluation_result = client.get_string_value(key, default, evaluation_context)
173+
assert evaluation_result == expected_value
174+
175+
176+
@then(
177+
parsers.cfparse(
178+
'the resolved string zero-value should be ""',
179+
)
180+
)
181+
def assert_empty_string(
182+
client: OpenFeatureClient,
183+
key_and_default: tuple,
184+
evaluation_context: EvaluationContext,
185+
):
186+
key, default = key_and_default
187+
evaluation_result = client.get_string_value(key, default, evaluation_context)
188+
assert evaluation_result == ""
189+
190+
191+
@then(parsers.cfparse('the returned reason should be "{reason}"'))
192+
def assert_reason(
193+
client: OpenFeatureClient,
194+
key_and_default: tuple,
195+
evaluation_context: EvaluationContext,
196+
reason: str,
197+
):
198+
"""the returned reason should be <reason>."""
199+
key, default = key_and_default
200+
evaluation_result = client.get_string_details(key, default, evaluation_context)
201+
assert evaluation_result.reason.value == reason
202+
203+
204+
provider_ready_ran = False
205+
206+
207+
@when(parsers.cfparse("a PROVIDER_READY handler is added"))
208+
def provider_ready_add(client: OpenFeatureClient):
209+
client.add_handler(ProviderEvent.PROVIDER_READY, provider_ready_handler)
210+
211+
212+
def provider_ready_handler(event_details: EventDetails):
213+
global provider_ready_ran
214+
provider_ready_ran = True
215+
216+
217+
@then(parsers.cfparse("the PROVIDER_READY handler must run"))
218+
def provider_ready_was_executed(client: OpenFeatureClient):
219+
assert provider_ready_ran
220+
221+
222+
provider_changed_ran = False
223+
224+
225+
@when(parsers.cfparse("a PROVIDER_CONFIGURATION_CHANGED handler is added"))
226+
def provider_changed_add(client: OpenFeatureClient):
227+
client.add_handler(
228+
ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, provider_changed_handler
229+
)
230+
231+
232+
def provider_changed_handler(event_details: EventDetails):
233+
global provider_changed_ran
234+
provider_changed_ran = True
235+
236+
237+
@pytest.fixture(scope="function")
238+
def context():
239+
return {}
240+
241+
242+
@when(parsers.cfparse('a flag with key "{flag_key}" is modified'))
243+
def assert_reason2(
244+
client: OpenFeatureClient,
245+
context,
246+
flag_key: str,
247+
):
248+
context["flag_key"] = flag_key
249+
250+
251+
@then(parsers.cfparse("the PROVIDER_CONFIGURATION_CHANGED handler must run"))
252+
def provider_changed_was_executed(client: OpenFeatureClient):
253+
wait_for(lambda: provider_changed_ran)
254+
assert provider_changed_ran
255+
256+
257+
def wait_for(pred, poll_sec=2, timeout_sec=10):
258+
start = time.time()
259+
while not (ok := pred()) and (time.time() - start < timeout_sec):
260+
time.sleep(poll_sec)
261+
assert pred()
262+
return ok
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import pytest
2+
from pytest_bdd import scenarios
3+
from testcontainers.core.container import DockerContainer
4+
from tests.e2eGherkin.steps import wait_for
5+
6+
from openfeature import api
7+
from openfeature.client import ProviderStatus
8+
from openfeature.contrib.provider.flagd import FlagdProvider
9+
from openfeature.contrib.provider.flagd.config import ResolverType
10+
11+
12+
@pytest.fixture(autouse=True, scope="module")
13+
def setup(request):
14+
# Setup code
15+
with DockerContainer("ghcr.io/open-feature/flagd-testbed:v0.5.6").with_bind_ports(
16+
8013
17+
) as container:
18+
container.start()
19+
api.set_provider(
20+
FlagdProvider(
21+
resolver_type=ResolverType.GRPC,
22+
port=int(container.get_exposed_port(8013)),
23+
)
24+
)
25+
client = api.get_client()
26+
wait_for(lambda: client.get_provider_status() == ProviderStatus.READY)
27+
assert client.get_provider_status() == ProviderStatus.READY
28+
29+
def fin():
30+
container.stop()
31+
32+
# Teardown code
33+
34+
request.addfinalizer(fin)
35+
36+
37+
scenarios(
38+
"../../test-harness/gherkin/flagd.feature",
39+
"../../test-harness/gherkin/flagd-json-evaluator.feature",
40+
)

0 commit comments

Comments
 (0)