Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions manifests/cpp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,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_experimentation_opt_out.py: 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
Expand Down
1 change: 1 addition & 0 deletions manifests/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1183,6 +1183,7 @@ manifest:
component_version: <3.55.0
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_experimentation_opt_out.py: missing_feature
tests/parametric/test_ffe/test_serial_id_metadata.py: missing_feature
tests/parametric/test_ffe/test_span_enrichment.py: '>=3.54.0' # TODO: a lower version might be supported
tests/parametric/test_ffe/test_span_enrichment.py::Test_Span_Enrichment_Child_Span_Propagation: missing_feature
Expand Down
1 change: 1 addition & 0 deletions manifests/golang.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2170,6 +2170,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 # Easy win for all weblogs and version 2.10.1
tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Dynamic_Evaluation: v2.5.0-dev
tests/parametric/test_ffe/test_experimentation_opt_out.py: missing_feature
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
Expand Down
1 change: 1 addition & 0 deletions manifests/java.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4242,6 +4242,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_experimentation_opt_out.py: missing_feature
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
Expand Down
1 change: 1 addition & 0 deletions manifests/nodejs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2658,6 +2658,7 @@ manifest:
tests/parametric/test_extract_behavior.py::Test_ExtractBehavior_Restart_With_Extract_First::test_baggage: *ref_5_56_0
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_experimentation_opt_out.py: missing_feature
tests/parametric/test_ffe/test_serial_id_metadata.py: missing_feature
tests/parametric/test_ffe/test_span_enrichment.py: # TODO: a lower version might be supported
- declaration: missing_feature (dd-trace-js#8343)
Expand Down
1 change: 1 addition & 0 deletions manifests/php.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1598,6 +1598,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_experimentation_opt_out.py: missing_feature
tests/parametric/test_ffe/test_serial_id_metadata.py: missing_feature
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
Expand Down
1 change: 1 addition & 0 deletions manifests/python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2257,6 +2257,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_experimentation_opt_out.py: missing_feature
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
Expand Down
1 change: 1 addition & 0 deletions manifests/ruby.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2679,6 +2679,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: v2.46.0-dev
tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Dynamic_Evaluation: v2.23.0
tests/parametric/test_ffe/test_experimentation_opt_out.py: 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::Test_Headers_B3::test_headers_b3_extract_invalid: # Easy win for all weblogs and version 2.27.0
Expand Down
1 change: 1 addition & 0 deletions manifests/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ manifest:
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: '>=0.5.3-dev+fd1307f' # TODO: a lower version might be supported
tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Dynamic_Evaluation::test_ffe_flag_evaluation: missing_feature
tests/parametric/test_ffe/test_experimentation_opt_out.py: missing_feature
tests/parametric/test_ffe/test_serial_id_metadata.py: missing_feature
tests/parametric/test_ffe/test_span_enrichment.py: '>=0.5.3-dev+fd1307f' # TODO: a lower version might be supported
tests/parametric/test_ffe/test_span_enrichment.py::Test_Span_Enrichment_Child_Span_Propagation: missing_feature
Expand Down
57 changes: 57 additions & 0 deletions tests/parametric/test_ffe/EXPERIMENTATION_OPT_OUT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Experimentation opt-out runtime contract

This companion suite tests the real Java provider through its public evaluation API.
It checks values, allocation keys, variant keys, serial IDs, exposure classification, and hook metadata.
It does not infer success from an application default with an equal value.

## Shared inputs

The nested fixture snapshot comes from [shared fixture PR #48](https://github.com/DataDog/ffe-system-test-data/pull/48).
The exact commit is `5e2430d9ad36644e2cecb6a32ddb8d05d7c98ac6`.
`fixtures/experimentation-opt-out/SOURCE.json` records every original Git blob ID.
The loader verifies every blob and all 136 case IDs before using the snapshot.
Do not edit copied configurations or expectations. Update the shared repository and refresh the snapshot together.

The runtime suite executes 116 cases in 11 configuration/provider-option groups.
It excludes 20 cases with fixed allocation time boundaries.
The Java SDK fixture adapter covers those cases with its fixed-clock test seam.
The dogfooding companion also runs them while their original time window is valid.
Changing generated dates would invalidate that proof.

## Local candidate run

Copy the agent, provider, and API JARs from the same candidate build into `binaries/`.
Do not combine a released agent with a candidate provider.
The Java image installer already accepts all three local JARs.

```sh
PYTEST_XDIST_AUTO_NUM_WORKERS=1 TEST_LIBRARY=java ./run.sh PARAMETRIC \
tests/parametric/test_ffe/test_experimentation_opt_out.py --runxfail
```

`run.sh` appends `-n auto`. The environment variable bounds that setting to one worker.
Use `--skip-parametric-build` only when the image and candidate artifacts did not change.
The manifest marks this unreleased behavior as `missing_feature` without inventing a release version.
The local command uses `--runxfail` so a candidate failure fails the run.
The suite enables the existing span-enrichment setting so ordinary allocation serial metadata is observable.
This does not enable experimentation opt-out. The public provider option controls that behavior separately.

`ffe_start(provider_options=...)` activates the public provider options before evaluation.
A missing option method is a setup failure, not a behavioral negative control.
The baseline negative control uses the existing evaluation path without the new method.
Contexts remain per evaluation. The test does not replace server-global subject state.

The Java controllers retain JSON integers that Jackson parses as `Long`.
They convert these values through OpenFeature's supported `Double` context type.
The bounded regression in `test_dynamic_evaluation.py` checks an exactly representable integer and both adjacent values.
This does not add exact 64-bit integer support to the SDK.

## Evidence boundary

These groups check assignment behavior only. The SDK shares exposure deduplication across providers.
Repeated fixture subjects can therefore suppress duplicate exposures correctly.
The dogfooding companion restarts its own app between the four independent telemetry cases.
It retains exact per-subject exposure and evaluation receipts, metrics, and correlated assignment traces.
It also checks overlapping evaluations and confirms that a skipped assignment does not reserve a deduplication key.

The 20 Precompute protocol cases and nine client state sequences do not apply to this server runtime suite.
63 changes: 63 additions & 0 deletions tests/parametric/test_ffe/experimentation_contract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Read an immutable shared fixture snapshot and compare actual provider details."""

import hashlib
import json
from pathlib import Path
from typing import Any

FIXTURES = Path(__file__).parent / "fixtures" / "experimentation-opt-out"


def load_contract() -> tuple[dict[str, Any], dict[str, Any], list[dict[str, Any]]]:
"""Verify every source blob before loading the shared contract."""
source = json.loads((FIXTURES / "SOURCE.json").read_text())
assert source["commit"] == "5e2430d9ad36644e2cecb6a32ddb8d05d7c98ac6"
for name, expected in source["blobs"].items():
contents = (FIXTURES / name).read_bytes()
actual = hashlib.sha1(b"blob " + str(len(contents)).encode() + b"\0" + contents).hexdigest()
assert actual == expected, f"Modified shared fixture: {name}"
manifest = json.loads((FIXTURES / "manifest.json").read_text())
configurations = {
item["id"]: json.loads((FIXTURES / item["path"]).read_text()) for item in manifest["configurations"]
}
cases: list[dict[str, Any]] = []
for group in manifest["evaluationCases"]:
group_cases = json.loads((FIXTURES / group["path"]).read_text())
assert [case["id"] for case in group_cases] == group["ids"]
cases.extend(group_cases)
assert len(cases) == manifest["expectedCounts"]["evaluations"] == 136
return manifest, configurations, cases


def has_time_boundary(configuration: dict[str, Any], case: dict[str, Any]) -> bool:
"""Time-gated cases require fixed-clock SDK tests or a guarded live runtime."""
flag = configuration["flags"][case["evaluation"]["flag"]]
return any(allocation.get("startAt") or allocation.get("endAt") for allocation in flag["allocations"])


def assert_details(result: dict[str, Any], case: dict[str, Any]) -> None:
"""Require assignment identity and metadata, not only an equal value."""
expected = case["expected"]
assert result["value"] == expected["value"], (case["id"], result)
assignment = expected["assignment"]
if assignment is None:
assert not result.get("allocationKey"), (case["id"], result)
assert not result.get("variant"), (case["id"], result)
assert result.get("serialId") is None, (case["id"], result)
if expected["outcome"] != "no-assignment":
assert result.get("errorCode"), (case["id"], result)
return
assert not result.get("errorCode"), (case["id"], result)
assert result.get("reason"), (case["id"], result)
assert result.get("allocationKey") == assignment["allocationKey"], (case["id"], result)
assert result.get("variant") == assignment["variationKey"], (case["id"], result)
assert result.get("serialId") == assignment["serialId"], (case["id"], result)
metadata = result.get("flagMetadata", {})
assert metadata.get("__dd_do_log") is assignment["doLog"], (case["id"], result)
expected_features = {
"__dd_feature." + feature["key"]: feature["value"]
for feature in assignment.get("features", [])
if "HOOK" in feature["destinations"]
}
actual_features = {key: value for key, value in metadata.items() if key.startswith("__dd_feature.")}
assert actual_features == expected_features, (case["id"], actual_features, expected_features)
Loading
Loading