-
Notifications
You must be signed in to change notification settings - Fork 23
test: Add Testcontainers and Gherkin execution for our test-harness #101
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
toddbaert
merged 6 commits into
open-feature:main
from
open-feature-forking:feat/gherkin_and_testcontainers
Nov 18, 2024
Merged
Changes from 5 commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
7fbc770
feat: Add Testcontainers and Gherkin execution for our test-harness
aepfli 7c809e8
fixup: make tests run, only 4 tests are missing now
aepfli 1e78ebe
Merge branch 'main' into feat/gherkin_and_testcontainers
aepfli 90526fe
fixup: adding gherkin tests for in-process via file
aepfli 6b012ba
fixup: deactivating the tests
aepfli dcbef10
fixup: renaming FlagD to Flagd
aepfli File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,3 +4,6 @@ | |
| [submodule "providers/openfeature-provider-flagd/test-harness"] | ||
| path = providers/openfeature-provider-flagd/test-harness | ||
| url = [email protected]:open-feature/flagd-testbed.git | ||
| [submodule "providers/openfeature-provider-flagd/spec"] | ||
| path = providers/openfeature-provider-flagd/spec | ||
| url = https://github.com/open-feature/spec | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
209 changes: 17 additions & 192 deletions
209
providers/openfeature-provider-flagd/tests/e2e/conftest.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,208 +1,33 @@ | ||
| import typing | ||
|
|
||
| import pytest | ||
| from pytest_bdd import given, parsers, then, when | ||
| from tests.e2e.parsers import to_bool | ||
| from testcontainers.core.container import DockerContainer | ||
| from tests.e2e.flagd_container import FlagDContainer | ||
| from tests.e2e.steps import * # noqa: F403 | ||
|
|
||
| from openfeature import api | ||
| from openfeature.client import OpenFeatureClient | ||
| from openfeature.contrib.provider.flagd import FlagdProvider | ||
| from openfeature.contrib.provider.flagd.config import ResolverType | ||
| from openfeature.evaluation_context import EvaluationContext | ||
|
|
||
| JsonPrimitive = typing.Union[str, bool, float, int] | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def evaluation_context() -> EvaluationContext: | ||
| return EvaluationContext() | ||
|
|
||
|
|
||
| @given("a flagd provider is set", target_fixture="client") | ||
| def setup_provider(flag_file) -> OpenFeatureClient: | ||
| @pytest.fixture(autouse=True, scope="package") | ||
| def setup(request, port, image, resolver_type): | ||
| container: DockerContainer = FlagDContainer( | ||
| image=image, | ||
| port=port, | ||
| ) | ||
| # Setup code | ||
| c = container.start() | ||
| api.set_provider( | ||
| FlagdProvider( | ||
| resolver_type=ResolverType.IN_PROCESS, | ||
| offline_flag_source_path=flag_file, | ||
| offline_poll_interval_seconds=0.1, | ||
| resolver_type=resolver_type, | ||
| port=int(container.get_exposed_port(port)), | ||
| ) | ||
| ) | ||
| return api.get_client() | ||
|
|
||
|
|
||
| @when( | ||
| parsers.cfparse( | ||
| 'a zero-value boolean flag with key "{key}" is evaluated with default value "{default:bool}"', | ||
| extra_types={"bool": to_bool}, | ||
| ), | ||
| target_fixture="key_and_default", | ||
| ) | ||
| @when( | ||
| parsers.cfparse( | ||
| 'a zero-value string flag with key "{key}" is evaluated with default value "{default}"', | ||
| ), | ||
| target_fixture="key_and_default", | ||
| ) | ||
| @when( | ||
| parsers.cfparse( | ||
| 'a string flag with key "{key}" is evaluated with default value "{default}"' | ||
| ), | ||
| target_fixture="key_and_default", | ||
| ) | ||
| @when( | ||
| parsers.cfparse( | ||
| 'a zero-value integer flag with key "{key}" is evaluated with default value {default:d}', | ||
| ), | ||
| target_fixture="key_and_default", | ||
| ) | ||
| @when( | ||
| parsers.cfparse( | ||
| 'an integer flag with key "{key}" is evaluated with default value {default:d}', | ||
| ), | ||
| target_fixture="key_and_default", | ||
| ) | ||
| @when( | ||
| parsers.cfparse( | ||
| 'a zero-value float flag with key "{key}" is evaluated with default value {default:f}', | ||
| ), | ||
| target_fixture="key_and_default", | ||
| ) | ||
| def setup_key_and_default( | ||
| key: str, default: JsonPrimitive | ||
| ) -> typing.Tuple[str, JsonPrimitive]: | ||
| return (key, default) | ||
|
|
||
|
|
||
| @when( | ||
| parsers.cfparse( | ||
| 'a context containing a targeting key with value "{targeting_key}"' | ||
| ), | ||
| ) | ||
| def assign_targeting_context(evaluation_context: EvaluationContext, targeting_key: str): | ||
| """a context containing a targeting key with value <targeting key>.""" | ||
| evaluation_context.targeting_key = targeting_key | ||
|
|
||
|
|
||
| @when( | ||
| parsers.cfparse('a context containing a key "{key}", with value "{value}"'), | ||
| ) | ||
| @when( | ||
| parsers.cfparse('a context containing a key "{key}", with value {value:d}'), | ||
| ) | ||
| def update_context( | ||
| evaluation_context: EvaluationContext, key: str, value: JsonPrimitive | ||
| ): | ||
| """a context containing a key and value.""" | ||
| evaluation_context.attributes[key] = value | ||
|
|
||
|
|
||
| @when( | ||
| parsers.cfparse( | ||
| 'a context containing a nested property with outer key "{outer}" and inner key "{inner}", with value "{value}"' | ||
| ), | ||
| ) | ||
| @when( | ||
| parsers.cfparse( | ||
| 'a context containing a nested property with outer key "{outer}" and inner key "{inner}", with value {value:d}' | ||
| ), | ||
| ) | ||
| def update_context_nested( | ||
| evaluation_context: EvaluationContext, | ||
| outer: str, | ||
| inner: str, | ||
| value: typing.Union[str, int], | ||
| ): | ||
| """a context containing a nested property with outer key, and inner key, and value.""" | ||
| if outer not in evaluation_context.attributes: | ||
| evaluation_context.attributes[outer] = {} | ||
| evaluation_context.attributes[outer][inner] = value | ||
|
|
||
|
|
||
| @then( | ||
| parsers.cfparse( | ||
| 'the resolved boolean zero-value should be "{expected_value:bool}"', | ||
| extra_types={"bool": to_bool}, | ||
| ) | ||
| ) | ||
| def assert_boolean_value( | ||
| client: OpenFeatureClient, | ||
| key_and_default: tuple, | ||
| expected_value: bool, | ||
| evaluation_context: EvaluationContext, | ||
| ): | ||
| key, default = key_and_default | ||
| evaluation_result = client.get_boolean_value(key, default, evaluation_context) | ||
| assert evaluation_result == expected_value | ||
|
|
||
|
|
||
| @then( | ||
| parsers.cfparse( | ||
| "the resolved integer zero-value should be {expected_value:d}", | ||
| ) | ||
| ) | ||
| @then(parsers.cfparse("the returned value should be {expected_value:d}")) | ||
| def assert_integer_value( | ||
| client: OpenFeatureClient, | ||
| key_and_default: tuple, | ||
| expected_value: bool, | ||
| evaluation_context: EvaluationContext, | ||
| ): | ||
| key, default = key_and_default | ||
| evaluation_result = client.get_integer_value(key, default, evaluation_context) | ||
| assert evaluation_result == expected_value | ||
|
|
||
|
|
||
| @then( | ||
| parsers.cfparse( | ||
| "the resolved float zero-value should be {expected_value:f}", | ||
| ) | ||
| ) | ||
| def assert_float_value( | ||
| client: OpenFeatureClient, | ||
| key_and_default: tuple, | ||
| expected_value: bool, | ||
| evaluation_context: EvaluationContext, | ||
| ): | ||
| key, default = key_and_default | ||
| evaluation_result = client.get_float_value(key, default, evaluation_context) | ||
| assert evaluation_result == expected_value | ||
|
|
||
|
|
||
| @then(parsers.cfparse('the returned value should be "{expected_value}"')) | ||
| def assert_string_value( | ||
| client: OpenFeatureClient, | ||
| key_and_default: tuple, | ||
| expected_value: bool, | ||
| evaluation_context: EvaluationContext, | ||
| ): | ||
| key, default = key_and_default | ||
| evaluation_result = client.get_string_value(key, default, evaluation_context) | ||
| assert evaluation_result == expected_value | ||
|
|
||
|
|
||
| @then( | ||
| parsers.cfparse( | ||
| 'the resolved string zero-value should be ""', | ||
| ) | ||
| ) | ||
| def assert_empty_string( | ||
| client: OpenFeatureClient, | ||
| key_and_default: tuple, | ||
| evaluation_context: EvaluationContext, | ||
| ): | ||
| key, default = key_and_default | ||
| evaluation_result = client.get_string_value(key, default, evaluation_context) | ||
| assert evaluation_result == "" | ||
|
|
||
| def fin(): | ||
| c.stop() | ||
|
|
||
| @then(parsers.cfparse('the returned reason should be "{reason}"')) | ||
| def assert_reason( | ||
| client: OpenFeatureClient, | ||
| key_and_default: tuple, | ||
| evaluation_context: EvaluationContext, | ||
| reason: str, | ||
| ): | ||
| """the returned reason should be <reason>.""" | ||
| key, default = key_and_default | ||
| evaluation_result = client.get_string_details(key, default, evaluation_context) | ||
| assert evaluation_result.reason.value == reason | ||
| # Teardown code | ||
| request.addfinalizer(fin) |
59 changes: 59 additions & 0 deletions
59
providers/openfeature-provider-flagd/tests/e2e/flagd_container.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| import time | ||
|
|
||
| import grpc | ||
| from grpc_health.v1 import health_pb2, health_pb2_grpc | ||
| from testcontainers.core.container import DockerContainer | ||
| from testcontainers.core.waiting_utils import wait_container_is_ready, wait_for_logs | ||
|
|
||
| HEALTH_CHECK = 8014 | ||
|
|
||
|
|
||
| class FlagDContainer(DockerContainer): | ||
aepfli marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| def __init__( | ||
| self, | ||
| image: str = "ghcr.io/open-feature/flagd-testbed:v0.5.13", | ||
| port: int = 8013, | ||
| **kwargs, | ||
| ) -> None: | ||
| super().__init__(image, **kwargs) | ||
| self.port = port | ||
| self.with_exposed_ports(self.port, HEALTH_CHECK) | ||
|
|
||
| def start(self) -> "FlagDContainer": | ||
aepfli marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| super().start() | ||
| self._checker(self.get_container_host_ip(), self.get_exposed_port(HEALTH_CHECK)) | ||
| return self | ||
|
|
||
| @wait_container_is_ready(ConnectionError) | ||
| def _checker(self, host: str, port: int) -> None: | ||
| # First we wait for Flagd to say it's listening | ||
| wait_for_logs( | ||
| self, | ||
| "listening", | ||
| 5, | ||
| ) | ||
|
|
||
| time.sleep(1) | ||
| # Second we use the GRPC health check endpoint | ||
| with grpc.insecure_channel(host + ":" + port) as channel: | ||
| health_stub = health_pb2_grpc.HealthStub(channel) | ||
|
|
||
| def health_check_call(stub: health_pb2_grpc.HealthStub): | ||
| request = health_pb2.HealthCheckRequest() | ||
| resp = stub.Check(request) | ||
| if resp.status == health_pb2.HealthCheckResponse.SERVING: | ||
| return True | ||
| elif resp.status == health_pb2.HealthCheckResponse.NOT_SERVING: | ||
| return False | ||
|
|
||
| # Should succeed | ||
| # Check health status every 1 second for 30 seconds | ||
| ok = False | ||
| for _ in range(30): | ||
| ok = health_check_call(health_stub) | ||
| if ok: | ||
| break | ||
| time.sleep(1) | ||
|
|
||
| if not ok: | ||
| raise ConnectionError("flagD not ready in time") | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,7 @@ | ||
| def to_bool(s: str) -> bool: | ||
| return s.lower() == "true" | ||
|
|
||
|
|
||
| def to_list(s: str) -> list: | ||
| values = s.replace('"', "").split(",") | ||
| return [s.strip() for s in values] |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.