-
Notifications
You must be signed in to change notification settings - Fork 15
Enable PHP FFE evaluation system tests #7003
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
Changes from 7 commits
224c241
8fbb3a5
9bcf43d
e2d4370
19b9c41
83bc16b
afd0d5f
1d6ac67
552010f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3644,7 +3644,6 @@ manifest: | |
| tests/parametric/test_dynamic_configuration.py::TestDynamicConfigV1_ServiceTargets::test_not_match_service_target: irrelevant (APMAPI-1003) | ||
| tests/parametric/test_dynamic_configuration.py::TestDynamicConfigV2: v1.31.0 | ||
| tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Dynamic_Evaluation: v1.56.0 | ||
| tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Dynamic_Evaluation::test_ffe_of7_empty_targeting_key: bug (FFL-1729) | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was fixed; removing |
||
| tests/parametric/test_headers_b3.py::Test_Headers_B3::test_headers_b3_migrated_extract_invalid: # Modified by easy win activation script | ||
| - declaration: missing_feature (Need to remove b3=b3multi alias) | ||
| component_version: <1.58.2+06122213c8 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2013,7 +2013,6 @@ manifest: | |
| tests/parametric/test_dynamic_configuration.py::TestDynamicConfigV1_ServiceTargets::test_not_match_service_target: bug (APMAPI-865) | ||
| tests/parametric/test_dynamic_configuration.py::TestDynamicConfigV2: *ref_4_23_0 | ||
| tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Dynamic_Evaluation: *ref_5_75_0 | ||
| tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Dynamic_Evaluation::test_ffe_of7_empty_targeting_key: bug (FFL-1730) | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was fixed; removing |
||
| tests/parametric/test_headers_b3.py::Test_Headers_B3::test_headers_b3_migrated_extract_invalid: missing_feature (Need to remove b3=b3multi alias) | ||
| tests/parametric/test_headers_b3.py::Test_Headers_B3::test_headers_b3_migrated_extract_valid: missing_feature (Need to remove b3=b3multi alias) | ||
| tests/parametric/test_headers_b3.py::Test_Headers_B3::test_headers_b3_migrated_inject_valid: missing_feature (Need to remove b3=b3multi alias) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,7 +6,6 @@ | |
| from typing import Any | ||
|
|
||
| from utils import ( | ||
| context, | ||
| features, | ||
| scenarios, | ||
| ) | ||
|
|
@@ -109,13 +108,6 @@ def test_ffe_flag_evaluation(self, test_case_file: str, test_agent: TestAgentAPI | |
| 4. Handles user targeting, attribute matching, and rollout percentages | ||
|
|
||
| """ | ||
| # Skip OF.7 (empty targeting key) test for libraries with known bugs | ||
| # Java: FFL-1729 - OpenFeature Java SDK rejects empty targeting keys | ||
| # Node.js: FFL-1730 - OpenFeature JS SDK rejects empty targeting keys | ||
| if test_case_file == "test-case-of-7-empty-targeting-key.json": | ||
| if context.library.name in ("java", "nodejs"): | ||
| pytest.skip("OF.7 empty targeting key bug: FFL-1729 (java), FFL-1730 (nodejs)") | ||
|
Comment on lines
-114
to
-119
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was fixed; removing - works for php as well |
||
|
|
||
| # Load the test case file | ||
| test_case_path = Path(__file__).parent / test_case_file | ||
|
|
||
|
|
@@ -129,7 +121,7 @@ def test_ffe_flag_evaluation(self, test_case_file: str, test_agent: TestAgentAPI | |
| _set_and_wait_ffe_rc(test_agent, UFC_FIXTURE_DATA) | ||
|
|
||
| # Initialize FFE provider | ||
| success = test_library.ffe_start() | ||
| success = test_library.ffe_start(UFC_FIXTURE_DATA) | ||
| assert success, "Failed to start FFE provider" | ||
|
|
||
| # Run each test case | ||
|
|
@@ -156,33 +148,3 @@ def test_ffe_flag_evaluation(self, test_case_file: str, test_agent: TestAgentAPI | |
| f"flag='{flag}', targetingKey='{targeting_key}', " | ||
| f"expected={expected_result}, actual={actual_value}" | ||
| ) | ||
|
|
||
| @parametrize("library_env", [{**DEFAULT_ENVVARS}]) | ||
| def test_ffe_of7_empty_targeting_key(self, test_agent: TestAgentAPI, test_library: APMLibrary) -> None: | ||
| """OF.7: Empty string is a valid targeting key. | ||
|
Comment on lines
-162
to
-164
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. special test not needed; using the centralized fixtures again for all languages |
||
|
|
||
| This test validates that flag evaluation succeeds when the targeting key | ||
| is an empty string. The flag should still match allocations and return | ||
| the expected value, not fail with TARGETING_KEY_MISSING. | ||
|
|
||
| Temporary dedicated test until FFL-1729 (Java) and FFL-1730 (Node.js) are resolved. | ||
| """ | ||
| # Set up UFC Remote Config and wait for it to be applied | ||
| _set_and_wait_ffe_rc(test_agent, UFC_FIXTURE_DATA) | ||
|
|
||
| # Initialize FFE provider | ||
| success = test_library.ffe_start() | ||
| assert success, "Failed to start FFE provider" | ||
|
|
||
| # Evaluate flag with empty targeting key | ||
| result = test_library.ffe_evaluate( | ||
| flag="empty-targeting-key-flag", | ||
| variation_type="STRING", | ||
| default_value="default", | ||
| targeting_key="", | ||
| attributes={}, | ||
| ) | ||
|
|
||
| assert result.get("value") == "on-value", ( | ||
| f"OF.7 failed: empty targeting key should return 'on-value', got '{result.get('value')}'" | ||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,209 @@ | ||
| <?php | ||
|
|
||
| function dd_ffe_json_response($statusCode, array $payload) | ||
| { | ||
| header('Content-Type: application/json'); | ||
| http_response_code($statusCode); | ||
| echo json_encode($payload, JSON_UNESCAPED_SLASHES); | ||
| } | ||
|
|
||
| function dd_ffe_error_response($statusCode, $errorCode, $errorMessage) | ||
| { | ||
| dd_ffe_json_response($statusCode, array( | ||
| 'value' => null, | ||
| 'reason' => 'ERROR', | ||
| 'variant' => null, | ||
| 'errorCode' => $errorCode, | ||
| 'errorMessage' => $errorMessage, | ||
| 'providerState' => array('ready' => false), | ||
| )); | ||
| } | ||
|
|
||
| function dd_ffe_read_payload() | ||
| { | ||
| $rawBody = file_get_contents('php://input'); | ||
| if ($rawBody === false || $rawBody === '') { | ||
| return array(); | ||
| } | ||
|
|
||
| $payload = json_decode($rawBody, true); | ||
| if (json_last_error() !== JSON_ERROR_NONE || !is_array($payload)) { | ||
| dd_ffe_error_response(400, 'INVALID_REQUEST', 'Expected a JSON object request body.'); | ||
| exit; | ||
| } | ||
|
|
||
| return $payload; | ||
| } | ||
|
|
||
| function dd_ffe_normalized_variation_type($variationType) | ||
| { | ||
| return strtoupper(str_replace('-', '_', (string) $variationType)); | ||
| } | ||
|
|
||
| function dd_ffe_normalize_default_value($defaultValue, $variationType) | ||
| { | ||
| switch (dd_ffe_normalized_variation_type($variationType)) { | ||
| case 'BOOLEAN': | ||
| return is_bool($defaultValue) ? $defaultValue : (bool) $defaultValue; | ||
| case 'STRING': | ||
| return is_string($defaultValue) ? $defaultValue : (string) $defaultValue; | ||
| case 'INTEGER': | ||
| return is_int($defaultValue) ? $defaultValue : (int) $defaultValue; | ||
| case 'NUMERIC': | ||
| case 'FLOAT': | ||
| case 'DOUBLE': | ||
| return is_int($defaultValue) || is_float($defaultValue) ? $defaultValue : (float) $defaultValue; | ||
| case 'JSON': | ||
| case 'OBJECT': | ||
| return is_array($defaultValue) ? $defaultValue : array(); | ||
| default: | ||
| return $defaultValue; | ||
| } | ||
| } | ||
|
|
||
| function dd_ffe_scalar_attributes(array $attributes) | ||
| { | ||
| $normalized = array(); | ||
| foreach ($attributes as $key => $value) { | ||
| if (is_bool($value) || is_int($value) || is_float($value) || is_string($value)) { | ||
| $normalized[(string) $key] = $value; | ||
| } | ||
| } | ||
|
|
||
| return $normalized; | ||
| } | ||
|
|
||
| function dd_ffe_evaluate_with_client($flagKey, $variationType, $defaultValue, $targetingKey, array $attributes) | ||
| { | ||
| if (!class_exists('\\DDTrace\\FeatureFlags\\Client')) { | ||
| return null; | ||
| } | ||
|
|
||
| if (method_exists('\\DDTrace\\FeatureFlags\\Client', 'create')) { | ||
| $client = \DDTrace\FeatureFlags\Client::create(); | ||
| } else { | ||
| $client = new \DDTrace\FeatureFlags\Client(); | ||
| } | ||
|
|
||
| $context = array( | ||
| 'targetingKey' => $targetingKey, | ||
| 'attributes' => $attributes, | ||
| ); | ||
|
|
||
| switch (dd_ffe_normalized_variation_type($variationType)) { | ||
| case 'BOOLEAN': | ||
| return $client->getBooleanDetails($flagKey, $defaultValue, $context); | ||
| case 'STRING': | ||
| return $client->getStringDetails($flagKey, $defaultValue, $context); | ||
| case 'INTEGER': | ||
| return $client->getIntegerDetails($flagKey, $defaultValue, $context); | ||
| case 'NUMERIC': | ||
| case 'FLOAT': | ||
| case 'DOUBLE': | ||
| return $client->getFloatDetails($flagKey, $defaultValue, $context); | ||
| case 'JSON': | ||
| case 'OBJECT': | ||
| return $client->getObjectDetails($flagKey, is_array($defaultValue) ? $defaultValue : array(), $context); | ||
| default: | ||
| throw new InvalidArgumentException('Unsupported variationType: ' . (string) $variationType); | ||
| } | ||
| } | ||
|
|
||
| function dd_ffe_warning_handler($severity, $message) | ||
| { | ||
| if ($severity === E_USER_WARNING && strpos($message, 'Datadog-backed PHP feature flag evaluation') !== false) { | ||
| return true; | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| function dd_ffe_evaluate($flagKey, $variationType, $defaultValue, $targetingKey, array $attributes) | ||
| { | ||
| set_error_handler('dd_ffe_warning_handler'); | ||
| try { | ||
| return dd_ffe_evaluate_with_client($flagKey, $variationType, $defaultValue, $targetingKey, $attributes); | ||
| } finally { | ||
| restore_error_handler(); | ||
| } | ||
| } | ||
|
|
||
| function dd_ffe_details_payload($details) | ||
| { | ||
| $payload = array( | ||
| 'value' => $details->getValue(), | ||
| 'reason' => $details->getReason(), | ||
| 'variant' => $details->getVariant(), | ||
| 'errorCode' => $details->getErrorCode(), | ||
| 'errorMessage' => $details->getErrorMessage(), | ||
| 'flagMetadata' => $details->getFlagMetadata(), | ||
| 'exposureData' => $details->getExposureData(), | ||
| 'providerState' => $details->getProviderState(), | ||
| ); | ||
|
|
||
| if (method_exists($details, 'getValueType')) { | ||
| $payload['valueType'] = $details->getValueType(); | ||
| } | ||
|
|
||
| return $payload; | ||
| } | ||
|
|
||
| $payload = dd_ffe_read_payload(); | ||
|
|
||
| if (!isset($payload['flag']) || !is_string($payload['flag']) || $payload['flag'] === '') { | ||
| dd_ffe_error_response(400, 'INVALID_REQUEST', 'Expected non-empty string field: flag.'); | ||
| exit; | ||
| } | ||
|
|
||
| if (!array_key_exists('variationType', $payload) || !is_string($payload['variationType'])) { | ||
| dd_ffe_error_response(400, 'INVALID_REQUEST', 'Expected string field: variationType.'); | ||
| exit; | ||
| } | ||
|
|
||
| if (!array_key_exists('defaultValue', $payload)) { | ||
| dd_ffe_error_response(400, 'INVALID_REQUEST', 'Expected field: defaultValue.'); | ||
| exit; | ||
| } | ||
|
|
||
| $flagKey = $payload['flag']; | ||
| $variationType = $payload['variationType']; | ||
| $defaultValue = dd_ffe_normalize_default_value($payload['defaultValue'], $variationType); | ||
| $targetingKey = isset($payload['targetingKey']) && $payload['targetingKey'] !== null | ||
| ? (string) $payload['targetingKey'] | ||
| : null; | ||
| $attributes = isset($payload['attributes']) && is_array($payload['attributes']) | ||
| ? dd_ffe_scalar_attributes($payload['attributes']) | ||
| : array(); | ||
|
|
||
| try { | ||
| $details = dd_ffe_evaluate($flagKey, $variationType, $defaultValue, $targetingKey, $attributes); | ||
| if ($details !== null) { | ||
| dd_ffe_json_response(200, dd_ffe_details_payload($details)); | ||
| return; | ||
| } | ||
| } catch (Throwable $exception) { | ||
| dd_ffe_json_response(200, array( | ||
| 'value' => $defaultValue, | ||
| 'reason' => 'ERROR', | ||
| 'variant' => null, | ||
| 'errorCode' => 'PROVIDER_NOT_READY', | ||
| 'errorMessage' => $exception->getMessage(), | ||
| 'providerState' => array( | ||
| 'ready' => false, | ||
| 'productionRuntime' => false, | ||
| ), | ||
| )); | ||
| return; | ||
| } | ||
|
|
||
| dd_ffe_json_response(200, array( | ||
| 'value' => $defaultValue, | ||
| 'reason' => 'ERROR', | ||
| 'variant' => null, | ||
| 'errorCode' => 'PROVIDER_NOT_READY', | ||
| 'errorMessage' => 'Datadog-backed PHP feature flag evaluation is not wired in this weblog yet.', | ||
| 'providerState' => array( | ||
| 'ready' => false, | ||
| 'productionRuntime' => false, | ||
| ), | ||
| )); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was fixed; removing