diff --git a/manifests/cpp.yml b/manifests/cpp.yml index f47379886b7..3dbff5bfdb1 100644 --- a/manifests/cpp.yml +++ b/manifests/cpp.yml @@ -124,6 +124,7 @@ manifest: tests/parametric/test_extract_behavior.py::Test_ExtractBehavior_Restart_With_Extract_First::test_baggage: missing_feature (Automatic baggage propagation is not implemented) tests/parametric/test_ffe/test_configuration_sources.py: irrelevant (cpp is not in scope for v1 agentless server SDK configuration-source coverage) tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Dynamic_Evaluation: missing_feature + tests/parametric/test_ffe/test_serial_id_metadata.py: missing_feature tests/parametric/test_ffe/test_span_enrichment.py: '>=2.1.1' # TODO: a lower version might be supported tests/parametric/test_ffe/test_span_enrichment.py::Test_Span_Enrichment_Child_Span_Propagation: missing_feature tests/parametric/test_ffe/test_span_enrichment.py::Test_Span_Enrichment_Default_Fallback: missing_feature diff --git a/manifests/dotnet.yml b/manifests/dotnet.yml index 1a06585da38..63d456a3e5e 100644 --- a/manifests/dotnet.yml +++ b/manifests/dotnet.yml @@ -965,6 +965,7 @@ manifest: tests/parametric/test_ffe/test_configuration_sources.py: missing_feature (FFL-2703 tracks .NET agentless configuration-source implementation; FFL-2731 tracks system-tests configuration-source contract) tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Dynamic_Evaluation: '>=3.36.0' # Modified by easy win activation script tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Dynamic_Evaluation::test_ffe_flag_evaluation: missing_feature # Created by easy win activation script + tests/parametric/test_ffe/test_serial_id_metadata.py: missing_feature tests/parametric/test_ffe/test_span_enrichment.py: missing_feature 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) diff --git a/manifests/golang.yml b/manifests/golang.yml index cdf890f8a9d..66f2acc104c 100644 --- a/manifests/golang.yml +++ b/manifests/golang.yml @@ -1631,6 +1631,7 @@ manifest: tests/parametric/test_extract_behavior.py::Test_ExtractBehavior_Restart_With_Extract_First: missing_feature (DD_TRACE_PROPAGATION_EXTRACT_FIRST=true is unsupported; dd-trace-go panics when extraction fails) tests/parametric/test_ffe/test_configuration_sources.py: v2.11.0-dev tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Dynamic_Evaluation: v2.5.0-dev + tests/parametric/test_ffe/test_serial_id_metadata.py: missing_feature tests/parametric/test_ffe/test_span_enrichment.py: '>=2.10.0' # TODO: a lower version might be supported tests/parametric/test_ffe/test_span_enrichment.py::Test_Span_Enrichment_Child_Span_Propagation: missing_feature tests/parametric/test_ffe/test_span_enrichment.py::Test_Span_Enrichment_Default_Fallback: missing_feature diff --git a/manifests/java.yml b/manifests/java.yml index 3dfa97d6905..fd289b19138 100644 --- a/manifests/java.yml +++ b/manifests/java.yml @@ -3963,6 +3963,7 @@ manifest: : v1.65.0 tests/parametric/test_ffe/test_configuration_sources.py::Test_Feature_Flag_Configuration_Source_Warm_State_Preservation: v1.65.0 tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Dynamic_Evaluation: v1.56.0 + tests/parametric/test_ffe/test_serial_id_metadata.py: missing_feature tests/parametric/test_ffe/test_span_enrichment.py: '>=1.64.2' # TODO: a lower version might be supported tests/parametric/test_ffe/test_span_enrichment.py::Test_Span_Enrichment_Child_Span_Propagation: missing_feature tests/parametric/test_ffe/test_span_enrichment.py::Test_Span_Enrichment_Default_Fallback: missing_feature diff --git a/manifests/nodejs.yml b/manifests/nodejs.yml index 9d1025d7d09..3476d912f50 100644 --- a/manifests/nodejs.yml +++ b/manifests/nodejs.yml @@ -2348,6 +2348,7 @@ manifest: tests/parametric/test_extract_behavior.py::Test_ExtractBehavior_Restart_With_Extract_First::test_baggage: incomplete_test_app (The parametric test app does not preserve baggage after extraction) tests/parametric/test_ffe/test_configuration_sources.py: *ref_6_8_0 tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Dynamic_Evaluation: *ref_5_75_0 + tests/parametric/test_ffe/test_serial_id_metadata.py: missing_feature tests/parametric/test_ffe/test_span_enrichment.py: "missing_feature (dd-trace-js#8343)" tests/parametric/test_headers_b3.py::Test_Headers_B3::test_headers_b3_migrated_extract_invalid: # TODO: a lower version might be supported - declaration: missing_feature (Need to remove b3=b3multi alias) diff --git a/manifests/php.yml b/manifests/php.yml index 266887892a9..72b52467c69 100644 --- a/manifests/php.yml +++ b/manifests/php.yml @@ -1476,6 +1476,7 @@ manifest: : missing_feature (FFL-2705 tracks PHP agentless configuration-source implementation; FFL-2731 tracks system-tests configuration-source contract) tests/parametric/test_ffe/test_configuration_sources.py::Test_Feature_Flag_Configuration_Source_Warm_State_Preservation: missing_feature (FFL-2705 tracks PHP agentless configuration-source implementation; FFL-2731 tracks system-tests configuration-source contract) tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Dynamic_Evaluation: v1.21.0-dev + tests/parametric/test_ffe/test_serial_id_metadata.py: missing_feature (Pending PHP SDK serial ID flag metadata support and release) tests/parametric/test_ffe/test_span_enrichment.py: '>=1.23.3' # TODO: a lower version might be supported tests/parametric/test_ffe/test_span_enrichment.py::Test_Span_Enrichment_Child_Span_Propagation: missing_feature tests/parametric/test_ffe/test_span_enrichment.py::Test_Span_Enrichment_Default_Fallback: missing_feature diff --git a/manifests/python.yml b/manifests/python.yml index cee16cef26a..405640a64fc 100644 --- a/manifests/python.yml +++ b/manifests/python.yml @@ -2005,6 +2005,7 @@ manifest: tests/parametric/test_extract_behavior.py::Test_ExtractBehavior_Restart_With_Extract_First: v2.20.0 tests/parametric/test_ffe/test_configuration_sources.py: v4.14.0-dev tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Dynamic_Evaluation: v4.0.0 + tests/parametric/test_ffe/test_serial_id_metadata.py: missing_feature tests/parametric/test_ffe/test_span_enrichment.py: '>=4.9.0' # TODO: a lower version might be supported tests/parametric/test_ffe/test_span_enrichment.py::Test_Span_Enrichment_Child_Span_Propagation: missing_feature tests/parametric/test_ffe/test_span_enrichment.py::Test_Span_Enrichment_Default_Fallback: missing_feature diff --git a/manifests/ruby.yml b/manifests/ruby.yml index e5c9c253985..0e8575362dd 100644 --- a/manifests/ruby.yml +++ b/manifests/ruby.yml @@ -2419,6 +2419,7 @@ manifest: tests/parametric/test_extract_behavior.py::Test_ExtractBehavior_Restart_With_Extract_First: incomplete_test_app (The parametric test app does not emit restart span links or preserve baggage) tests/parametric/test_ffe/test_configuration_sources.py: missing_feature (FFL-2701 tracks Ruby agentless configuration-source implementation; FFL-2731 tracks system-tests configuration-source contract) tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Dynamic_Evaluation: v2.23.0 + tests/parametric/test_ffe/test_serial_id_metadata.py: missing_feature tests/parametric/test_ffe/test_span_enrichment.py: missing_feature tests/parametric/test_headers_b3.py::Test_Headers_B3::test_headers_b3_extract_invalid: # Easy win for all weblogs and version 2.27.0 - declaration: missing_feature (Missing for 2.x) diff --git a/manifests/rust.yml b/manifests/rust.yml index 0a2d4fdca38..3cc8c2c7273 100644 --- a/manifests/rust.yml +++ b/manifests/rust.yml @@ -138,6 +138,7 @@ manifest: tests/parametric/test_extract_behavior.py: '>=0.4.1-dev' tests/parametric/test_ffe/test_configuration_sources.py: irrelevant (rust is not in scope for v1 agentless server SDK configuration-source coverage) tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Dynamic_Evaluation: missing_feature + tests/parametric/test_ffe/test_serial_id_metadata.py: missing_feature tests/parametric/test_ffe/test_span_enrichment.py: missing_feature tests/parametric/test_headers_b3.py: '>=0.2.1' # Modified by easy win activation script tests/parametric/test_headers_b3.py::Test_Headers_B3::test_headers_b3_extract_valid: missing_feature # Created by easy win activation script diff --git a/tests/parametric/test_ffe/serial-id-metadata-flags.json b/tests/parametric/test_ffe/serial-id-metadata-flags.json new file mode 100644 index 00000000000..35bf0ab4c74 --- /dev/null +++ b/tests/parametric/test_ffe/serial-id-metadata-flags.json @@ -0,0 +1,97 @@ +{ + "createdAt": "2024-01-01T00:00:00.000Z", + "format": "SERVER", + "environment": { "name": "Test" }, + "flags": { + "serial-id-basic-flag": { + "key": "serial-id-basic-flag", + "enabled": true, + "variationType": "BOOLEAN", + "variations": { + "on": { "key": "on", "value": true }, + "off": { "key": "off", "value": false } + }, + "allocations": [ + { + "key": "on-allocation", + "rules": [], + "splits": [ + { "variationKey": "on", "shards": [], "serialId": 42 } + ], + "doLog": true + } + ] + }, + "serial-id-zero-flag": { + "key": "serial-id-zero-flag", + "enabled": true, + "variationType": "BOOLEAN", + "variations": { + "on": { "key": "on", "value": true }, + "off": { "key": "off", "value": false } + }, + "allocations": [ + { + "key": "zero-allocation", + "rules": [], + "splits": [ + { "variationKey": "on", "shards": [], "serialId": 0 } + ], + "doLog": true + } + ] + }, + "serial-id-missing-flag": { + "key": "serial-id-missing-flag", + "enabled": true, + "variationType": "STRING", + "variations": { + "control": { "key": "control", "value": "control" }, + "treatment": { "key": "treatment", "value": "treatment" } + }, + "allocations": [ + { + "key": "no-serial-allocation", + "rules": [], + "splits": [ + { "variationKey": "control", "shards": [] } + ], + "doLog": true + } + ] + }, + "serial-id-targeting-flag": { + "key": "serial-id-targeting-flag", + "enabled": true, + "variationType": "STRING", + "variations": { + "vip": { "key": "vip", "value": "vip" }, + "standard": { "key": "standard", "value": "standard" } + }, + "allocations": [ + { + "key": "vip-allocation", + "rules": [ + { + "conditions": [ + { "attribute": "segment", "operator": "ONE_OF", "value": ["vip"] } + ] + } + ], + "splits": [ + { "variationKey": "vip", "shards": [], "serialId": 201 } + ], + "doLog": true + }, + { + "key": "default-allocation", + "rules": [], + "splits": [ + { "variationKey": "standard", "shards": [], "serialId": 200 } + ], + "doLog": true + } + ] + } + } +} diff --git a/tests/parametric/test_ffe/test_dynamic_evaluation.py b/tests/parametric/test_ffe/test_dynamic_evaluation.py index 7d48a1e0b7c..5ceea0560c9 100644 --- a/tests/parametric/test_ffe/test_dynamic_evaluation.py +++ b/tests/parametric/test_ffe/test_dynamic_evaluation.py @@ -41,7 +41,7 @@ def _get_test_case_files() -> list[str]: return [] # Exclude base fixtures that aren't test cases - excluded = {"flags-v1.json", "span-enrichment-flags.json"} + excluded = {"flags-v1.json", "serial-id-metadata-flags.json", "span-enrichment-flags.json"} return [f.name for f in test_data_dir.iterdir() if f.suffix == ".json" and f.name not in excluded] diff --git a/tests/parametric/test_ffe/test_serial_id_metadata.py b/tests/parametric/test_ffe/test_serial_id_metadata.py new file mode 100644 index 00000000000..8e8a9e14d45 --- /dev/null +++ b/tests/parametric/test_ffe/test_serial_id_metadata.py @@ -0,0 +1,271 @@ +"""Test that UFC split serial IDs are threaded through OpenFeature flagMetadata. + +Every evaluated (allocation, variant) pair has a stable per-org serial ID +(`Split.serialId` in the UFC wire format). Downstream consumers -- APM span +enrichment, exposure caching, evaluation-metrics correlation -- need that +serial ID without re-deriving it from (flag_key, allocation_key, variant_key). + +The contract under test: when span enrichment is enabled, the SDK's OpenFeature +EvaluationDetails-equivalent object must expose the selected split's serial ID +via `flagMetadata` under the internal key `__dd_split_serial_id`, matching the +value present in the UFC config pushed over Remote Config. This is the field +`openfeature-js-client#269` introduced for the Node server SDK; this test +generalizes the contract to every parametric-tested SDK. + +This module tests: +1. flagMetadata carries the exact serialId of the selected split. +2. Zero is a valid serial ID and must not be treated as "missing" (falsy-check trap). +3. Evaluation details always expose flagMetadata; for a split with no serialId, the map must not + contain the serial ID key. +4. flagMetadata reflects whichever split targeting actually selected, not always the first one. +5. Repeated evaluations of the same context return the same serial ID. +""" + +import json +import time +from pathlib import Path +from typing import Any + +from utils import pytest + +from utils import features, scenarios +from utils.dd_constants import RemoteConfigApplyState +from utils.docker_fixtures import TestAgentAPI +from tests.parametric.conftest import APMLibrary + +RC_PRODUCT = "FFE_FLAGS" +RC_PATH = f"datadog/2/{RC_PRODUCT}" +FFE_READY_RETRY_ATTEMPTS = 10 +FFE_READY_RETRY_INTERVAL_SECONDS = 0.2 + +SERIAL_ID_METADATA_KEY = "__dd_split_serial_id" + +parametrize = pytest.mark.parametrize + + +def _load_serial_id_metadata_fixture() -> dict[str, Any]: + """Load the UFC fixture file for serial ID metadata tests.""" + fixture_path = Path(__file__).parent / "serial-id-metadata-flags.json" + + if not fixture_path.exists(): + pytest.skip(f"Fixture file not found: {fixture_path}") + + with fixture_path.open() as f: + return json.load(f) + + +UFC_SERIAL_ID_METADATA_DATA = _load_serial_id_metadata_fixture() + +DEFAULT_ENVVARS = { + "DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED": "true", + "DD_EXPERIMENTAL_FLAGGING_PROVIDER_SPAN_ENRICHMENT_ENABLED": "true", + "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE": "remote_config", + "DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS": "0.2", +} + + +def _set_and_wait_ffe_rc( + test_agent: TestAgentAPI, ufc_data: dict[str, Any], config_id: str | None = None +) -> dict[str, Any]: + """Set FFE Remote Config and wait for it to be acknowledged.""" + if not config_id: + config_id = str(hash(json.dumps(ufc_data, sort_keys=True))) + + test_agent.set_remote_config(path=f"{RC_PATH}/{config_id}/config", payload=ufc_data) + return test_agent.wait_for_rc_apply_state(RC_PRODUCT, state=RemoteConfigApplyState.ACKNOWLEDGED, clear=True) + + +def _is_ffe_waiting_for_rc(result: dict[str, Any]) -> bool: + provider_state = result.get("providerState") + return result.get("errorCode") == "PROVIDER_NOT_READY" or ( + isinstance(provider_state, dict) and provider_state.get("hasConfig") is False + ) + + +def _require_flag_metadata(result: dict[str, Any]) -> dict[str, Any]: + """Require evaluation details to expose flagMetadata, allowing a null map.""" + assert "flagMetadata" in result, f"FFE evaluation details did not emit flagMetadata; result={result}" + + flag_metadata = result["flagMetadata"] + assert flag_metadata is None or isinstance(flag_metadata, dict), ( + f"Expected flagMetadata to be a map or null, got {flag_metadata!r}" + ) + return flag_metadata or {} + + +def _ffe_evaluate_with_rc_retry( + test_library: APMLibrary, + *, + flag: str, + variation_type: str, + default_value: bool | str | float | dict[str, Any], + targeting_key: str, + attributes: dict[str, Any] | None = None, +) -> dict[str, Any]: + result = test_library.ffe_evaluate( + flag=flag, + variation_type=variation_type, + default_value=default_value, + targeting_key=targeting_key, + attributes=attributes, + ) + for _ in range(FFE_READY_RETRY_ATTEMPTS - 1): + if not _is_ffe_waiting_for_rc(result): + return result + time.sleep(FFE_READY_RETRY_INTERVAL_SECONDS) + result = test_library.ffe_evaluate( + flag=flag, + variation_type=variation_type, + default_value=default_value, + targeting_key=targeting_key, + attributes=attributes, + ) + + return result + + +@scenarios.parametric +@features.feature_flags_serial_id_metadata +class Test_FFE_Serial_Id_Metadata: + """Test that the selected split's serial ID is threaded through flagMetadata.""" + + @parametrize("library_env", [{**DEFAULT_ENVVARS}]) + def test_serial_id_present_in_flag_metadata(self, test_agent: TestAgentAPI, test_library: APMLibrary) -> None: + """The serial ID of the selected split must appear in flagMetadata.""" + _set_and_wait_ffe_rc(test_agent, UFC_SERIAL_ID_METADATA_DATA) + assert test_library.ffe_start(), "Failed to start FFE provider" + + result = _ffe_evaluate_with_rc_retry( + test_library, + flag="serial-id-basic-flag", + variation_type="BOOLEAN", + default_value=False, + targeting_key="user-1", + ) + assert not _is_ffe_waiting_for_rc(result), f"FFE provider did not load RC data; result={result}" + + flag_metadata = _require_flag_metadata(result) + assert flag_metadata.get(SERIAL_ID_METADATA_KEY) == 42, ( + f"Expected flagMetadata['{SERIAL_ID_METADATA_KEY}'] == 42 for the selected split, " + f"got flagMetadata={flag_metadata}" + ) + + @parametrize("library_env", [{**DEFAULT_ENVVARS}]) + def test_serial_id_zero_is_not_treated_as_missing(self, test_agent: TestAgentAPI, test_library: APMLibrary) -> None: + """Serial ID 0 is a real, valid ID and must survive falsy-value checks.""" + _set_and_wait_ffe_rc(test_agent, UFC_SERIAL_ID_METADATA_DATA) + assert test_library.ffe_start(), "Failed to start FFE provider" + + result = _ffe_evaluate_with_rc_retry( + test_library, + flag="serial-id-zero-flag", + variation_type="BOOLEAN", + default_value=False, + targeting_key="user-1", + ) + assert not _is_ffe_waiting_for_rc(result), f"FFE provider did not load RC data; result={result}" + + flag_metadata = _require_flag_metadata(result) + assert SERIAL_ID_METADATA_KEY in flag_metadata, ( + f"flagMetadata is missing '{SERIAL_ID_METADATA_KEY}' entirely; a 0 serial ID must not be " + f"dropped by a truthiness check. flagMetadata={flag_metadata}" + ) + assert flag_metadata.get(SERIAL_ID_METADATA_KEY) == 0, ( + f"Expected flagMetadata['{SERIAL_ID_METADATA_KEY}'] == 0, got flagMetadata={flag_metadata}" + ) + + @parametrize("library_env", [{**DEFAULT_ENVVARS}]) + def test_serial_id_absent_when_split_has_no_serial_id( + self, test_agent: TestAgentAPI, test_library: APMLibrary + ) -> None: + """A split without a serialId must not have one fabricated for it.""" + _set_and_wait_ffe_rc(test_agent, UFC_SERIAL_ID_METADATA_DATA) + assert test_library.ffe_start(), "Failed to start FFE provider" + + result = _ffe_evaluate_with_rc_retry( + test_library, + flag="serial-id-missing-flag", + variation_type="STRING", + default_value="default", + targeting_key="user-1", + ) + assert not _is_ffe_waiting_for_rc(result), f"FFE provider did not load RC data; result={result}" + assert result.get("errorCode") in {None, ""}, f"FFE evaluation failed; result={result}" + assert result.get("reason") != "ERROR", f"FFE evaluation failed; result={result}" + assert result.get("value") == "control", f"Expected the configured control variation; result={result}" + + flag_metadata = _require_flag_metadata(result) + assert SERIAL_ID_METADATA_KEY not in flag_metadata, ( + f"Expected flagMetadata to omit '{SERIAL_ID_METADATA_KEY}' for a split without a serial ID, " + f"got flagMetadata={flag_metadata}" + ) + + @parametrize("library_env", [{**DEFAULT_ENVVARS}]) + def test_serial_id_matches_selected_split_under_targeting( + self, test_agent: TestAgentAPI, test_library: APMLibrary + ) -> None: + """FlagMetadata must reflect whichever split targeting actually selected.""" + _set_and_wait_ffe_rc(test_agent, UFC_SERIAL_ID_METADATA_DATA) + assert test_library.ffe_start(), "Failed to start FFE provider" + + vip_result = _ffe_evaluate_with_rc_retry( + test_library, + flag="serial-id-targeting-flag", + variation_type="STRING", + default_value="default", + targeting_key="user-vip", + attributes={"segment": "vip"}, + ) + standard_result = _ffe_evaluate_with_rc_retry( + test_library, + flag="serial-id-targeting-flag", + variation_type="STRING", + default_value="default", + targeting_key="user-standard", + attributes={"segment": "standard"}, + ) + + for label, result in (("vip", vip_result), ("standard", standard_result)): + assert not _is_ffe_waiting_for_rc(result), f"FFE provider did not load RC data ({label}); result={result}" + + vip_metadata = _require_flag_metadata(vip_result) + standard_metadata = _require_flag_metadata(standard_result) + + assert vip_result.get("value") == "vip" + assert vip_metadata.get(SERIAL_ID_METADATA_KEY) == 201, ( + f"VIP targeting should select the split with serialId 201, got flagMetadata={vip_metadata}" + ) + + assert standard_result.get("value") == "standard" + assert standard_metadata.get(SERIAL_ID_METADATA_KEY) == 200, ( + f"Non-VIP targeting should select the split with serialId 200, got flagMetadata={standard_metadata}" + ) + + @parametrize("library_env", [{**DEFAULT_ENVVARS}]) + def test_serial_id_stable_across_repeated_evaluations( + self, test_agent: TestAgentAPI, test_library: APMLibrary + ) -> None: + """The same context evaluated twice must yield the same serial ID.""" + _set_and_wait_ffe_rc(test_agent, UFC_SERIAL_ID_METADATA_DATA) + assert test_library.ffe_start(), "Failed to start FFE provider" + + first = _ffe_evaluate_with_rc_retry( + test_library, + flag="serial-id-basic-flag", + variation_type="BOOLEAN", + default_value=False, + targeting_key="user-repeat", + ) + second = _ffe_evaluate_with_rc_retry( + test_library, + flag="serial-id-basic-flag", + variation_type="BOOLEAN", + default_value=False, + targeting_key="user-repeat", + ) + + first_id = _require_flag_metadata(first).get(SERIAL_ID_METADATA_KEY) + second_id = _require_flag_metadata(second).get(SERIAL_ID_METADATA_KEY) + assert first_id == second_id == 42, ( + f"Expected stable serial ID 42 across repeated evaluations, got {first_id} then {second_id}" + ) diff --git a/utils/_features.py b/utils/_features.py index 0cd8b4acf13..38f5c3f8c6b 100644 --- a/utils/_features.py +++ b/utils/_features.py @@ -2729,6 +2729,14 @@ def feature_flags_event_enrichment(test_object): """ return _mark_test_object(test_object, feature_id=551, owner=_Owner.ffe) + @staticmethod + def feature_flags_serial_id_metadata(test_object): + """Feature Flags Serial ID Metadata (UFC split serialId threaded through flagMetadata) + + No feature-parity page yet; using NOT_REPORTED_ID until one is filed. + """ + return _mark_test_object(test_object, feature_id=NOT_REPORTED_ID, owner=_Owner.ffe) + @staticmethod def appsec_extended_data_collection(test_object): """AppSec supports extended data collection including headers and body diff --git a/utils/build/docker/php/parametric/server.php b/utils/build/docker/php/parametric/server.php index f8b29d40de0..2577e07f4f4 100644 --- a/utils/build/docker/php/parametric/server.php +++ b/utils/build/docker/php/parametric/server.php @@ -116,7 +116,7 @@ function ffeDetailsPayload($details) { 'variant' => $details->getVariant(), 'errorCode' => $details->getErrorCode(), 'errorMessage' => $details->getErrorMessage(), - 'flagMetadata' => $details->getFlagMetadata(), + 'flagMetadata' => (object) $details->getFlagMetadata(), 'exposureData' => $details->getExposureData(), 'providerState' => $details->getProviderState(), ]; diff --git a/utils/pytest.py b/utils/pytest.py new file mode 100644 index 00000000000..cb74ce04b15 --- /dev/null +++ b/utils/pytest.py @@ -0,0 +1,62 @@ +"""Approved pytest API for system tests. + +Test modules should import this facade with ``from utils import pytest``. +The facade intentionally exposes only the pytest APIs used by this repository. +In particular, force-skip markers are available only through the semantic +decorators exported by :mod:`utils`. +""" + +import pytest as _pytest # noqa: PT013 - keep the underlying pytest module private + + +Config = _pytest.Config +FixtureRequest = _pytest.FixtureRequest +Item = _pytest.Item +Mark = _pytest.Mark +MarkDecorator = _pytest.MarkDecorator +MonkeyPatch = _pytest.MonkeyPatch +CaptureFixture = _pytest.CaptureFixture +Session = _pytest.Session +CallInfo = _pytest.CallInfo +TempPathFactory = _pytest.TempPathFactory + +approx = _pytest.approx +exit = _pytest.exit # noqa: A001 - preserve the public pytest API name +fail = _pytest.fail +fixture = _pytest.fixture +param = _pytest.param +raises = _pytest.raises +skip = _pytest.skip +hookimpl = _pytest.hookimpl + + +class _AllowedMarks: + """Pytest markers approved for direct use by system tests.""" + + features = _pytest.mark.features + parametrize = _pytest.mark.parametrize + scenario = _pytest.mark.scenario + usefixtures = _pytest.mark.usefixtures + + +mark = _AllowedMarks() + +__all__ = [ + "CallInfo", + "Config", + "FixtureRequest", + "Item", + "Mark", + "MarkDecorator", + "MonkeyPatch", + "Session", + "approx", + "exit", + "fail", + "fixture", + "hookimpl", + "mark", + "param", + "raises", + "skip", +]