Add canonical FFE fixture tests - #5742
Conversation
|
👋 Hey @DataDog/ruby-guild, please fill "Change log entry" section in the pull request description. If changes need to be present in CHANGELOG.md you can state it this way **Change log entry**
Yes. A brief summary to be placed into the CHANGELOG.md(possible answers Yes/Yep/Yeah) Or you can opt out like that **Change log entry**
None.(possible answers No/Nope/None) Visited at: 2026-08-10 17:53:55 UTC |
|
5d09d29 to
c33e981
Compare
## Motivation Use the shared FFE fixture corpus. This prevents copied Go fixtures from drifting from other SDKs. The same migration is merged in [Java](DataDog/dd-trace-java#11355) and [libdatadog](DataDog/libdatadog#1979). Related migrations are open for [Python](DataDog/dd-trace-py#19390), [JavaScript](DataDog/dd-trace-js#8441), [Ruby](DataDog/dd-trace-rb#5742), and [.NET](DataDog/dd-trace-dotnet#8616). ## Changes and Decisions - Add `DataDog/ffe-system-test-data` as an OpenFeature test submodule. - Read all canonical configuration and evaluation cases from the submodule. - Parse and validate flags independently so one invalid flag does not reject valid neighbors. - Assert canonical values and reasons, including temporal defaults and missing-flag errors. - Initialize submodules in Go test workflows and configure weekly Dependabot updates. Co-authored-by: leo.romanovsky <leo.romanovsky@datadoghq.com>
Typing analysisNote: Ignored files are excluded from the next sections. Untyped methodsThis PR clears 2 partially typed methods. It increases the percentage of typed methods from 69.52% to 69.64% (+0.12%). Partially typed methods (+0-2)✅ Cleared: |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f8eabf79ee
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| return false unless ["MATCHES", "NOT_MATCHES"].include?(condition["operator"]) | ||
| return false unless condition["value"].is_a?(String) | ||
|
|
||
| Regexp.new(condition["value"]) |
There was a problem hiding this comment.
Validate regexes with the evaluator's regex dialect
When a condition uses syntax on which Ruby's Regexp and libdatadog's regex engine disagree, this check produces the wrong resolution: for example, Rust-regex flag syntax such as scoped Unicode-mode options can be valid to libdatadog but rejected here, causing a valid flag to return PARSE_ERROR without evaluation; conversely, lookaround such as (?=foo) is accepted here but unsupported by the evaluator, so the invalid configuration falls through as a non-error DEFAULT now that the previous invalid-configuration translation was removed. Validate with the same regex parser as libdatadog, or preserve translation of its invalid-configuration result.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
The general gist of this AI review comment is: Ruby's regular expressions might not be 100% compatible with regular expressions created for other environments.
Ruby uses a slightly modified version of the Onigmo's regex library: https://github.com/ruby/ruby/blob/master/regsyntax.c
It's a superset of https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/V1_chap09.html (POSIX Extended Regular Expressions (ERE)).
This is likely outside the scope of this PR, but relevant to feature flags as a whole.
There was a problem hiding this comment.
The general gist of this AI review comment is: Ruby's regular expressions might not be 100% compatible with regular expressions created for other environments.
Totally understand - all languages have different capabilities they support. We are working on a set of conformance tests that give us a universal amount of support (DataDog/ffe-system-test-data#21) so we'll be editing this in a follow up PR as the specification lands to constraint them.
| error_message: "Variant value does not match the declared variation type" | ||
| ) | ||
| end | ||
| if @invalid_regex_flags.include?(flag_key) |
There was a problem hiding this comment.
Preserve type mismatch precedence for invalid-regex flags
When an invalid-regex flag is requested through an API for the wrong type—for example, a declared STRING flag fetched with fetch_boolean_value—this unconditional lookup returns PARSE_ERROR before the native evaluator can return TYPE_MISMATCH. The variant-value validation immediately above avoids this by associating each invalid flag with its declared expected types; the regex validation needs the same type guard so malformed regexes do not change wrong-type resolution semantics.
Useful? React with 👍 / 👎.
| fixture_root = File.expand_path("../open_feature/ffe-system-test-data", __dir__) | ||
| fixture_files = Dir[File.join(fixture_root, "evaluation-cases", "*.json")].sort | ||
|
|
||
| raise "FFE fixture submodule is missing or empty" if fixture_files.empty? |
There was a problem hiding this comment.
Document initialization of the required fixture submodule
In a normal fresh clone without --recurse-submodules, this directory is empty, so loading spec/datadog/core/feature_flags_spec.rb raises before any examples run and the documented local test:core_with_libdatadog_api workflow cannot be used after the repository's normal setup steps. CI checkouts were updated to fetch the submodule, but local/container setup was not; add git submodule update --init to the documented setup path or avoid making the suite fail solely because the fixture checkout is absent.
AGENTS.md reference: AGENTS.md:L7-L15
Useful? React with 👍 / 👎.
marcotc
left a comment
There was a problem hiding this comment.
I wasn't able to finish my review today, I'll continue tomorrow.
| result.error_code.nil? && | ||
| result.error_message == INVALID_FLAG_CONFIGURATION_ERROR_MESSAGE | ||
| def find_flag_validation_errors(configuration) | ||
| flags = JSON.parse(configuration)["flags"] |
There was a problem hiding this comment.
Is the JSON guaranteed to:
- Be valid?
- Contain a JSON object as the root?
We have a rescue for invalid JSON parsing, 1, which tells me we are confident receiving invalid JSONs is likely, aka we don't trust the JSON received here.
There's no error handling in case the scenario 2 above is not true.
We should not add error handling unnecessarily, since it adds overhead and code maintenance; but we should add necessary error handling if needed.
So my question here is: is it needed?
| # @return [Core::FeatureFlags::ResolutionDetails] The assignment for the flag | ||
| def get_assignment(flag_key, default_value:, expected_type:, context:) | ||
| result = @configuration.get_assignment(flag_key, expected_type, context) | ||
| if @variant_type_mismatch_flags.fetch(flag_key, []).include?(expected_type) |
There was a problem hiding this comment.
We are creating a new object ([]) on every call, to make include? work nicely, but we don't need that extra short-lived, brand new object here.
There was a problem hiding this comment.
Yea this looks like an undesirable design, thanks for mentioning. I will shift this implementation down to libdatadog; please hold off on further review I'll re-ping when a new libdatadog version is ready.
| end | ||
|
|
||
| def expected_types_for(variation_type) | ||
| { |
There was a problem hiding this comment.
This method is creating this lookup hash for every method invocation, when it can be a static constant instead.
| end | ||
| end | ||
|
|
||
| invalid_regexes[flag_key] = true if flag_has_invalid_regex?(flag) |
There was a problem hiding this comment.
invalid_regexes looks like a set instead of a hash, since the lookups are all for the simple presence of elements.
| return false unless ["MATCHES", "NOT_MATCHES"].include?(condition["operator"]) | ||
| return false unless condition["value"].is_a?(String) | ||
|
|
||
| Regexp.new(condition["value"]) |
There was a problem hiding this comment.
The general gist of this AI review comment is: Ruby's regular expressions might not be 100% compatible with regular expressions created for other environments.
Ruby uses a slightly modified version of the Onigmo's regex library: https://github.com/ruby/ruby/blob/master/regsyntax.c
It's a superset of https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/V1_chap09.html (POSIX Extended Regular Expressions (ERE)).
This is likely outside the scope of this PR, but relevant to feature flags as a whole.
## Motivation Use the shared FFE fixture corpus so the .NET evaluator is checked against the same behavior as the other tracer implementations. This reduces fixture drift and gives us a repeatable way to expose and correct evaluator bugs when new canonical cases are added. The same fixture corpus is used by [Java](DataDog/dd-trace-java#11355), [libdatadog](DataDog/libdatadog#1979), [Go](DataDog/dd-trace-go#4753), [Python](DataDog/dd-trace-py#19390), [JavaScript](DataDog/dd-trace-js#8441), and [Ruby](DataDog/dd-trace-rb#5742). ## Changes - Replace the legacy copied fixtures with a generated, checked-in snapshot from `DataDog/ffe-system-test-data`. - Record the exact upstream commit in `SOURCE.md`. - Add a script that fetches, validates, and copies the canonical configuration and evaluation cases. - Add a weekly and manually dispatchable workflow that opens a signed draft dependency PR only when fixture contents have changed. - Parse flags independently so malformed flags do not reject valid neighbors. - Return `FLAG_NOT_FOUND` for missing flags and classify temporal, static, and split allocations. - Assert canonical values and reasons through the existing .NET unit-test suite. ## Fixture update flow When we add or change shared evaluator behavior, I imagine the flow working like this: 1. Add the new configuration and evaluation cases to [`DataDog/ffe-system-test-data`](https://github.com/DataDog/ffe-system-test-data) and review the expected behavior there. 2. The weekly updater, or a manually dispatched run for a specific ref, fetches the canonical repository and compares its fixture contents with the checked-in .NET snapshot. 3. If nothing changed, the workflow exits without opening or updating a PR. 4. If fixtures changed, the workflow copies them into this repository, records the source commit, and opens a signed draft PR with the normal dependency labels. 5. The .NET unit tests run against the updated cases. New tests may intentionally fail when they catch an evaluator bug or unsupported behavior. 6. Address those evaluator failures in the same dependency PR, keeping the fixture expectations unchanged unless the shared expectation itself is incorrect. 7. Merge the update once the .NET evaluator satisfies the new canonical cases. This keeps new behavior explicit and reviewable: fixture changes land in the canonical repository first, and each tracer then gets a visible compatibility PR rather than silently changing at build time. ## Decisions - `DataDog/ffe-system-test-data` remains the canonical source of shared evaluator behavior. - Keep the generated snapshot checked in so local and CI unit tests do not require network access or submodule initialization. - Use a scheduled dependency-update workflow instead of a git submodule. - Treat failures introduced by new canonical fixtures as useful regression signals and fix the evaluator as part of accepting the update. - Do not create a PR when the canonical fixture contents are unchanged, even if the upstream repository has unrelated commits.
Motivation
Use the shared FFE fixture corpus. This prevents copied Ruby fixtures from drifting from other SDKs.
Identical submodule-backed canonical-fixture implementations are already merged in
dd-trace-javaanddd-trace-go. The shared evaluator implementation is also merged inlibdatadog.Related migrations are open for Python, JavaScript, and .NET.
Changes
DataDog/ffe-system-test-dataas a submodule under the OpenFeature specs..gitmodulesfrom the packaged gem file set and configure weekly Dependabot updates.Decisions