Skip to content

Commit 936282c

Browse files
leoromanovskydevflow.devflow-routing-intake
andauthored
Use canonical FFE fixtures (#11355)
Use canonical FFE fixtures Catch up FFE missing flag behavior Align Java smoke fixtures with canonical schema Use canonical fixtures in Java evaluator tests Update FFE fixture submodule Fix OpenFeature smoke CodeNarc Merge branch 'master' into leo.romanovsky/ffe-canonical-fixtures-20260512 Merge branch 'master' into leo.romanovsky/ffe-canonical-fixtures-20260512 Expand OpenFeature smoke exposure coverage Restore missing flag error reporting Apply OpenFeature smoke test formatting Document OpenFeature fixture submodule Merge branch 'master' into leo.romanovsky/ffe-canonical-fixtures-20260512 Merge remote-tracking branch 'origin/master' into leo.romanovsky/ffe-canonical-fixtures-20260512 # Conflicts: # products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/DDEvaluatorTest.java Co-authored-by: devflow.devflow-routing-intake <devflow.devflow-routing-intake@kubernetes.us1.ddbuild.io>
1 parent 2068d0d commit 936282c

29 files changed

Lines changed: 266 additions & 7874 deletions

.github/dependabot.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,8 @@ updates:
3636
prefix: 'chore(build): '
3737
cooldown:
3838
default-days: 2
39+
40+
- package-ecosystem: gitsubmodule
41+
directory: /
42+
schedule:
43+
interval: weekly

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
[submodule "dd-java-agent/agent-jmxfetch/integrations-core"]
22
path = dd-java-agent/agent-jmxfetch/integrations-core
33
url = https://github.com/DataDog/integrations-core.git
4+
[submodule "dd-smoke-tests/openfeature/src/test/resources/ffe-system-test-data"]
5+
path = dd-smoke-tests/openfeature/src/test/resources/ffe-system-test-data
6+
url = https://github.com/DataDog/ffe-system-test-data.git

BUILDING.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,8 @@ winget install --id Docker.DockerDesktop
179179
> Without this configuration, you will need to remember to add `--recurse-submodules` to `git checkout` when switching to old branches.
180180
181181
> [!TIP]
182-
> This will keep the submodule in `dd-java-agent/agent-jmxfetch/integrations-core` up-to-date.
182+
> This will keep the submodules in `dd-java-agent/agent-jmxfetch/integrations-core` and
183+
> `dd-smoke-tests/openfeature/src/test/resources/ffe-system-test-data` up-to-date.
183184
> There is also an automated check when opening a pull request if you are trying to submit a module version change (usually an outdated version).
184185
185186
> [!NOTE]

dd-smoke-tests/openfeature/src/test/groovy/datadog/smoketest/springboot/OpenFeatureProviderSmokeTest.groovy

Lines changed: 79 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,13 @@ import spock.util.concurrent.PollingConditions
2121
class OpenFeatureProviderSmokeTest extends AbstractServerSmokeTest {
2222

2323
@Shared
24-
private final rcPayload = new JsonSlurper().parse(fetchResource("config/flags-v1.json")).with { json ->
25-
return JsonOutput.toJson(json.data.attributes)
26-
}
24+
private final rcConfig = new JsonSlurper().parse(fetchResource("ffe-system-test-data/ufc-config.json")) as Map<String, Object>
25+
26+
@Shared
27+
private final rcPayload = JsonOutput.toJson(rcConfig)
28+
29+
@Shared
30+
private final loggedAllocations = buildLoggedAllocations(rcConfig)
2731

2832
@Override
2933
ProcessBuilder createProcessBuilder() {
@@ -70,34 +74,40 @@ class OpenFeatureProviderSmokeTest extends AbstractServerSmokeTest {
7074
setup:
7175
setRemoteConfig("datadog/2/FFE_FLAGS/1/config", rcPayload)
7276
final url = "http://localhost:${httpPort}/openfeature/evaluate"
73-
final testCases = parseTestCases().findAll {
74-
it.result.flagMetadata?.doLog
75-
}
77+
final testCases = parseTestCases()
78+
assert !testCases.isEmpty()
7679

7780
when:
78-
final responses = testCases.collect {
81+
final results = testCases.collect {
7982
testCase ->
8083
final request = new Request.Builder()
8184
.url(url)
8285
.post(RequestBody.create(MediaType.parse('application/json'), JsonOutput.toJson(testCase)))
8386
.build()
84-
client.newCall(request).execute()
87+
final response = client.newCall(request).execute()
88+
final responseBody = new JsonSlurper().parse(response.body().byteStream())
89+
return [testCase: testCase, response: response, body: responseBody]
8590
}
91+
final expectedExposures = uniqueExpectedExposures(results)
8692

8793
then:
88-
responses.every {
89-
it.code() == 200
94+
results.every {
95+
it.response.code() == 200
9096
}
97+
!expectedExposures.isEmpty()
9198
new PollingConditions(timeout: 10).eventually {
9299
final requests = evpProxyMessages*.getV2() as List<Map<String, Object>>
93100
final events = requests*.exposures.flatten()
94-
assert events.size() == testCases.size()
95-
testCases.each {
96-
testCase ->
101+
assert events.size() == expectedExposures.size()
102+
expectedExposures.each {
103+
expected ->
97104
assert events.find {
98105
event ->
99-
event.flag.key == testCase.flag && event.subject.id == testCase.targetingKey
100-
} != null : "Unable to find exposure with flag=${testCase.flag} and targetingKey=${testCase.targetingKey}"
106+
event.flag.key == expected.flag &&
107+
event.allocation.key == expected.allocation &&
108+
event.variant.key == expected.variant &&
109+
event.subject.id == expected.targetingKey
110+
} != null : "Unable to find exposure ${expected}"
101111
}
102112
}
103113
}
@@ -119,8 +129,16 @@ class OpenFeatureProviderSmokeTest extends AbstractServerSmokeTest {
119129
response.code() == 200
120130
final responseBody = new JsonSlurper().parse(response.body().byteStream())
121131
responseBody.value == testCase.result.value
122-
responseBody.variant == testCase.result.variant
123-
responseBody.flagMetadata?.allocationKey == testCase.result.flagMetadata?.allocationKey
132+
responseBody.reason == testCase.result.reason
133+
if (testCase.result.containsKey('errorCode')) {
134+
assert responseBody.errorCode == testCase.result.errorCode
135+
}
136+
if (testCase.result.containsKey('variant')) {
137+
assert responseBody.variant == testCase.result.variant
138+
}
139+
if (testCase.result.flagMetadata?.allocationKey) {
140+
assert responseBody.flagMetadata?.allocationKey == testCase.result.flagMetadata?.allocationKey
141+
}
124142

125143
where:
126144
testCase << parseTestCases()
@@ -131,11 +149,12 @@ class OpenFeatureProviderSmokeTest extends AbstractServerSmokeTest {
131149
}
132150

133151
private static List<Map<String, Object>> parseTestCases() {
134-
final folder = fetchResource('data')
152+
final folder = fetchResource('ffe-system-test-data/evaluation-cases')
135153
final uri = folder.toURI()
136154
final testsPath = Paths.get(uri)
137155
final files = Files.list(testsPath)
138156
.filter(path -> path.toString().endsWith('.json'))
157+
.sorted(Comparator.comparing(path -> path.fileName.toString()))
139158
final result = []
140159
final slurper = new JsonSlurper()
141160
files.each {
@@ -148,9 +167,51 @@ class OpenFeatureProviderSmokeTest extends AbstractServerSmokeTest {
148167
}
149168
result.addAll(testCases)
150169
}
170+
assert !result.isEmpty()
151171
return result
152172
}
153173

174+
private List<Map<String, String>> uniqueExpectedExposures(final List<Map<String, Object>> results) {
175+
final expected = []
176+
final seen = [] as Set<String>
177+
results.each { result ->
178+
final testCase = result.testCase as Map<String, Object>
179+
final body = result.body as Map<String, Object>
180+
final flag = testCase.flag as String
181+
final allocation = body.flagMetadata?.allocationKey as String
182+
final variant = body.variant as String
183+
if (!variant || !allocation || !allocationLogs(flag, allocation)) {
184+
return
185+
}
186+
187+
final exposure = [
188+
flag: flag,
189+
allocation: allocation,
190+
variant: variant,
191+
targetingKey: testCase.targetingKey
192+
]
193+
final key = "${exposure.flag}\u0000${exposure.targetingKey}\u0000${exposure.allocation}\u0000${exposure.variant}"
194+
if (seen.add(key)) {
195+
expected.add(exposure)
196+
}
197+
}
198+
return expected
199+
}
200+
201+
private boolean allocationLogs(final String flag, final String allocation) {
202+
return loggedAllocations["${flag}\u0000${allocation}"] == true
203+
}
204+
205+
private static Map<String, Boolean> buildLoggedAllocations(final Map<String, Object> config) {
206+
final logged = [:]
207+
(config.flags as Map<String, Object>).each { flag, definition ->
208+
(definition.allocations ?: []).each { allocation ->
209+
logged["${flag}\u0000${allocation.key}"] = allocation.doLog == true
210+
}
211+
}
212+
return logged
213+
}
214+
154215
private static Set<Product> decodeProducts(final Map<String, Object> request) {
155216
return request.client.products.collect { Product.valueOf(it) }
156217
}

0 commit comments

Comments
 (0)