Skip to content

Commit 73d84db

Browse files
committed
test(flagd): adopt the OpenFeature Provider TCK
Runs the conformance suite against the flagd provider in both resolver modes. The whole adoption is a shared abstract base and two subclasses that differ only in resolver and port: the TCK brings its own Gherkin, its own step definitions and its own Compose lifecycle, and works out which suite is running from the JUnit test plan, so a mode needs no registration and no build configuration. The Compose stack wraps the unmodified flagd-testbed image, which already serves both flagd and the launchpad control API that this TCK's control API contract was derived from. No host port bindings: the TCK discovers dynamically mapped ports after startup, so the suite runs in parallel and does not collide with a developer's local flagd. capabilities() is declarableExcept(NUMERIC_COERCION). Evaluating float-flag (0.5) through the integer API returns 0 with no error code rather than TYPE_MISMATCH with the code default -- the value is silently truncated. Coercion as such is permitted; it is the lossy case being accepted that is the defect, tracked as open-feature/flagd#1996. Both resolvers behave identically, which places it in the shared provider layer rather than in either transport, so it is declared once here. Delete the override when the defect is fixed. Split out of #1830 so that the suite and its first adopter are reviewed as separate questions: whether the TCK is the right contract, and whether flagd satisfies it. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
1 parent 3a8a4a1 commit 73d84db

5 files changed

Lines changed: 184 additions & 0 deletions

File tree

providers/flagd/pom.xml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
<com.vmlens.version>1.2.28</com.vmlens.version>
2323
<!-- Transitive flagd-core version -->
2424
<flagd-core.version>[2.0.0,3.0.0)</flagd-core.version>
25+
<!-- Match any provider-tck version locally; CI resolves it from the reactor -->
26+
<provider-tck.version>[0.0.1,)</provider-tck.version>
2527
</properties>
2628

2729
<name>flagd</name>
@@ -98,6 +100,17 @@
98100
<version>5.14.3</version>
99101
<scope>test</scope>
100102
</dependency>
103+
<!--
104+
OpenFeature Provider TCK. Brings its own Gherkin, step definitions and Compose
105+
lifecycle; FlagdTckTest is the whole adoption. Version range so a local reactor
106+
build matches whatever is checked out.
107+
-->
108+
<dependency>
109+
<groupId>dev.openfeature.contrib.tools</groupId>
110+
<artifactId>provider-tck</artifactId>
111+
<version>${provider-tck.version}</version>
112+
<scope>test</scope>
113+
</dependency>
101114
<dependency>
102115
<groupId>org.testcontainers</groupId>
103116
<artifactId>testcontainers</artifactId>
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package dev.openfeature.contrib.providers.flagd.e2e;
2+
3+
import dev.openfeature.contrib.providers.flagd.Config;
4+
import dev.openfeature.contrib.providers.flagd.FlagdOptions;
5+
import dev.openfeature.contrib.providers.flagd.FlagdProvider;
6+
import dev.openfeature.contrib.tools.providertck.BackendEndpoint;
7+
import dev.openfeature.contrib.tools.providertck.Capability;
8+
import dev.openfeature.contrib.tools.providertck.ContainerizedProviderTckTest;
9+
import dev.openfeature.sdk.FeatureProvider;
10+
import java.io.File;
11+
import java.util.Collections;
12+
import java.util.List;
13+
import java.util.Set;
14+
15+
/**
16+
* Shared configuration for running the OpenFeature Provider TCK against the flagd provider.
17+
*
18+
* <p>flagd resolves flags in two quite different ways, and both are worth conforming: RPC evaluates
19+
* remotely over gRPC, while in-process syncs the ruleset and evaluates locally. They share a backend
20+
* stack and differ only in resolver and port, so the modes are two small subclasses.
21+
*
22+
* <p>Each concrete subclass is its own JUnit suite and its own TCK harness; the TCK works out which
23+
* one is running from the JUnit test plan, so adding a mode needs no registration or build
24+
* configuration.
25+
*/
26+
abstract class AbstractFlagdTckTest extends ContainerizedProviderTckTest {
27+
28+
/**
29+
* A port nothing listens on, for the initialisation-failure scenarios.
30+
*
31+
* <p>Deliberately not a port on the Compose stack: the stack must stay up for the whole suite,
32+
* and simulated outages belong to the control API.
33+
*/
34+
private static final int UNAVAILABLE_PORT = 9999;
35+
36+
/**
37+
* gRPC deadline for a provider that is expected to connect.
38+
*
39+
* <p>Generous on purpose. flagd derives its initialisation deadline from this value, and the
40+
* in-process resolver must sync the entire ruleset before it reports ready — which intermittently
41+
* takes longer than a deadline tuned for a single RPC round trip.
42+
*/
43+
private static final int CONNECTED_DEADLINE_MS = 5000;
44+
45+
/**
46+
* gRPC deadline for a provider pointed at a dead port.
47+
*
48+
* <p>Short on purpose, and deliberately not the same as {@link #CONNECTED_DEADLINE_MS}: the
49+
* initialisation-failure scenarios assert that the failure is reported <em>promptly</em>, so a
50+
* provider that takes as long to give up as it does to connect would defeat the point.
51+
*/
52+
private static final int UNAVAILABLE_DEADLINE_MS = 1000;
53+
54+
/** The resolver under test. */
55+
protected abstract Config.Resolver resolver();
56+
57+
/** The container-internal port that resolver connects to. */
58+
protected abstract int backendPort();
59+
60+
@Override
61+
public File composeFile() {
62+
return new File("src/test/resources/tck/docker-compose.yaml");
63+
}
64+
65+
@Override
66+
public List<Integer> backendPorts() {
67+
return Collections.singletonList(backendPort());
68+
}
69+
70+
@Override
71+
public FeatureProvider createProvider(BackendEndpoint endpoint) {
72+
return new FlagdProvider(baseOptions()
73+
.deadline(CONNECTED_DEADLINE_MS)
74+
.host(endpoint.host())
75+
.port(endpoint.port(backendPort()))
76+
.build());
77+
}
78+
79+
@Override
80+
public FeatureProvider createUnavailableProvider() {
81+
return new FlagdProvider(baseOptions()
82+
.deadline(UNAVAILABLE_DEADLINE_MS)
83+
.host("localhost")
84+
.port(UNAVAILABLE_PORT)
85+
.build());
86+
}
87+
88+
/**
89+
* {@inheritDoc}
90+
*
91+
* <p>Everything declarable except {@link Capability#NUMERIC_COERCION}. Evaluating
92+
* {@code float-flag} (0.5) through the integer API returns {@code 0} with <em>no</em> error code
93+
* rather than {@code TYPE_MISMATCH} with the code default — the value is silently truncated.
94+
* Coercion as such is permitted, and the capability says so: the rule is that a lossless
95+
* coercion must succeed and a lossy one must fail. It is the lossy case being accepted that is a
96+
* defect to fix, not a design choice; this override should be deleted once it is.
97+
*
98+
* <p>Declared here rather than per mode because both resolvers behave identically, which places
99+
* the defect in the shared provider layer rather than in either transport. Every other
100+
* capability, including the full non-numeric type-mismatch matrix, holds in both modes.
101+
*
102+
* <p>That includes {@link Capability#LIFECYCLE}, and legitimately so: flagd reaches its backend
103+
* during initialisation in both modes — an RPC round trip, or a full ruleset sync — so the
104+
* lifecycle scenarios assert something real here rather than passing vacuously.
105+
*
106+
* <p>{@link Capability#declarableExcept} rather than {@code EnumSet.complementOf}, which is what
107+
* this used to be. The complement of one capability is every other <em>enum constant</em>,
108+
* including {@code @targeting} and {@code @caching} — reserved tags no scenario carries — so a
109+
* report emitted from here claimed two capabilities nothing had examined.
110+
*/
111+
@Override
112+
public Set<Capability> capabilities() {
113+
return Capability.declarableExcept(Capability.NUMERIC_COERCION);
114+
}
115+
116+
private FlagdOptions.FlagdOptionsBuilder baseOptions() {
117+
return FlagdOptions.builder()
118+
.resolverType(resolver())
119+
.retryGracePeriod(2)
120+
.retryBackoffMs(500);
121+
}
122+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package dev.openfeature.contrib.providers.flagd.e2e;
2+
3+
import dev.openfeature.contrib.providers.flagd.Config;
4+
5+
/** Runs the OpenFeature Provider TCK against the flagd provider in in-process mode. */
6+
public class FlagdInProcessTckTest extends AbstractFlagdTckTest {
7+
8+
@Override
9+
protected Config.Resolver resolver() {
10+
return Config.Resolver.IN_PROCESS;
11+
}
12+
13+
@Override
14+
protected int backendPort() {
15+
return 8015;
16+
}
17+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package dev.openfeature.contrib.providers.flagd.e2e;
2+
3+
import dev.openfeature.contrib.providers.flagd.Config;
4+
5+
/** Runs the OpenFeature Provider TCK against the flagd provider in RPC mode. */
6+
public class FlagdRpcTckTest extends AbstractFlagdTckTest {
7+
8+
@Override
9+
protected Config.Resolver resolver() {
10+
return Config.Resolver.RPC;
11+
}
12+
13+
@Override
14+
protected int backendPort() {
15+
return 8013;
16+
}
17+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Backend stack for the OpenFeature Provider TCK, wrapping the unmodified flagd testbed image.
2+
#
3+
# The image already serves everything the TCK needs: flagd itself, and the "launchpad" control
4+
# API on 8080 whose endpoints this TCK's control API contract was derived from.
5+
#
6+
# Note there are no host port bindings. The TCK requires dynamically mapped ports and discovers
7+
# them after startup — a pinned host port would make the suite unrunnable in parallel and would
8+
# collide with a developer's local flagd.
9+
services:
10+
backend:
11+
image: ghcr.io/open-feature/flagd-testbed:v3.8.0
12+
ports:
13+
- 8013 # flagd RPC evaluation (gRPC)
14+
- 8015 # flagd in-process sync (gRPC)
15+
- 8080 # launchpad control API

0 commit comments

Comments
 (0)