11"""Shared wiring for the two flagd conformance suites.
22
33flagd resolves flags two quite different ways -- RPC evaluates remotely over
4- gRPC, in-process syncs the ruleset and evaluates locally -- and they are separate
5- suites because they are separately conformant. Any difference between the two
6- results is a difference an application would see when it switches resolver,
7- which is exactly the class of thing the conformance suite exists to surface.
8-
9- Everything they share lives here; everything that differs lives in ``test_rpc.py``
10- and ``test_in_process.py`` next to it, where a reader can see the whole of a
11- resolver's declaration in one place. The TCK owns the container stack (see
12- ``conftest.py``), so what is left here is the timings, which are flagd's own and
13- interact, and the one function that turns a resolver plus a running endpoint into
14- a ``TckConfig``.
4+ gRPC, in-process syncs the ruleset and evaluates locally -- and they are
5+ separate suites because they are separately conformant. What is shared is the
6+ timings, which are flagd's own and interact, and the one function that turns a
7+ resolver plus a running endpoint into a ``TckConfig``; what differs lives in
8+ ``test_rpc.py`` and ``test_in_process.py``.
159"""
1610
1711from __future__ import annotations
3428IN_PROCESS_PORT = 8015
3529"""Container-internal ports flagd serves remote evaluation and the sync stream on.
3630
37- Both are declared in one ``ComposeBackend`` because one flagd process serves both
38- and both suites share the stack. Named here rather than in ``conftest.py`` so
39- that the declaration and the resolver that uses it cannot drift apart.
31+ Named here rather than in ``conftest.py`` so that the declaration and the
32+ resolver that uses it cannot drift apart.
4033"""
4134
42- # Timings. flagd exposes several and they interact, so they are named here once
43- # rather than scattered through two suites.
44-
4535DEADLINE_MS = 5000
4636"""How long a provider blocks during initialisation before giving up.
4737
4838This, and not ``TckConfig.ready_timeout``, is what actually bounds flagd's
4939initialisation: the resolvers block inside ``initialize`` and raise
5040``ProviderNotReadyError`` when it expires (grpc.py:175, grpc_watcher.py:151).
51- Generous because every scenario is preceded by a control-API ``/start``, which
52- restarts the flagd process -- so a provider is routinely built against a backend
53- that came up milliseconds ago.
41+ Generous because every scenario is preceded by a control-API ``/start``, so a
42+ provider is routinely built against a backend that came up milliseconds ago.
5443
5544For the RPC resolver it also bounds each individual resolution call.
5645"""
6150RETRY_BACKOFF_MAX_MS = 5000
6251"""Longest delay between reconnect attempts, and the wait between stream retries.
6352
64- Kept at or above :data:`DEADLINE_MS`: the provider passes the deadline to gRPC as
65- ``grpc.min_reconnect_backoff_ms`` and this as ``grpc.max_reconnect_backoff_ms``
66- (grpc.py:90-92), so a smaller value here would be a channel configured with a
67- minimum backoff above its maximum.
53+ Kept at or above :data:`DEADLINE_MS`: the provider passes the deadline to gRPC
54+ as ``grpc.min_reconnect_backoff_ms`` and this as
55+ ``grpc.max_reconnect_backoff_ms`` (grpc.py:90-92), so a smaller value here would
56+ be a channel configured with a minimum backoff above its maximum.
6857"""
6958
7059RETRY_GRACE_PERIOD_SECONDS = 30
7160"""How long a disconnected provider stays STALE before escalating to ERROR.
7261
73- Load-bearing for the ``@stale`` scenario. Both resolvers go STALE the moment the
74- channel fails and start a timer that escalates to ERROR when this expires
75- (grpc.py:204-210, grpc_watcher.py:180-186). Too short a value turns a scenario
76- about staleness into one about failure, because the outage lasts as long as the
77- scenario needs to observe it.
62+ Load-bearing for the ``@stale`` scenario: both resolvers start the escalation
63+ timer the moment the channel fails (grpc.py:204-210, grpc_watcher.py:180-186),
64+ so too short a value turns a scenario about staleness into one about failure.
7865"""
7966
8067STREAM_DEADLINE_MS = 0
8168"""Disable periodic stream recycling, as the existing e2e suites do.
8269
83- A recycle is invisible to the provider contract, but disabling it removes a
84- source of background reconnects from a suite whose whole subject is what a
85- reconnect looks like.
70+ It removes a source of background reconnects from a suite whose whole subject
71+ is what a reconnect looks like.
8672"""
8773
8874UNAVAILABLE_DEADLINE_MS = 500
9278
9379The ``@unavailable`` scenarios assert that failure is reported *promptly*, so a
9480provider taking 30 seconds to give up would pass a test about eventual failure
95- while failing the one that matters. The backoff is long for the opposite reason:
96- after the error is reported, the next failed reconnect attempt would emit
97- another ``PROVIDER_STALE`` and move the provider out of the ERROR state the
81+ while failing the one that matters. The backoff is long for the opposite
82+ reason: after the error is reported, the next failed reconnect attempt would
83+ emit another ``PROVIDER_STALE`` and move the provider out of the ERROR state the
9884scenario is about to assert.
9985"""
10086
@@ -117,16 +103,12 @@ class ResolverSuite:
117103 resolver_type : ResolverType
118104
119105 backend_port : int
120- """The container-internal port this resolver connects to.
121-
122- Resolved to a host port through the endpoint, after the stack is up.
123- """
106+ """Container-internal port, resolved to a host port once the stack is up."""
124107
125108 capabilities : frozenset [Capability ]
126109 """What this resolver was run against the suite and seen to satisfy.
127110
128- See each suite module for the evidence behind every entry, and behind every
129- omission.
111+ See each suite module for the evidence behind every entry and omission.
130112 """
131113
132114 ready_timeout : float
@@ -135,11 +117,10 @@ class ResolverSuite:
135117 """Requirements this resolver is known to fail, acknowledged rather than hidden.
136118
137119 **Per resolver, and that is the whole reason this field is here rather than
138- shared.** The two resolvers do not deviate alike: RPC narrows ``float-flag``
139- to ``0`` where in-process refuses it, so an entry naming
140- :attr:`~Capability.NUMERIC_COERCION` is true of one and false of the other,
141- and attaching it to both would publish a defect against the resolver that
142- does not have it.
120+ shared.** RPC narrows ``float-flag`` to ``0`` where in-process refuses it,
121+ so an entry naming :attr:`~Capability.NUMERIC_COERCION` is true of one and
122+ false of the other, and attaching it to both would publish a defect against
123+ the resolver that does not have it.
143124 """
144125
145126
@@ -150,9 +131,8 @@ def build_config(
150131) -> TckConfig :
151132 """Wire one resolver up to the running testbed.
152133
153- The host and port are read off ``backend.endpoint`` inside the factory, which
154- is why the factory exists: the mapped host port does not exist until the
155- stack is up. The host comes from the endpoint rather than being written as
134+ A factory because the mapped host port does not exist until the stack is up.
135+ The host comes from the endpoint rather than being written as
156136 ``"localhost"``, because with a remote Docker daemon, Docker Desktop on some
157137 platforms or a rootless setup it is neither localhost nor predictable.
158138 """
@@ -171,9 +151,8 @@ def new_provider() -> FeatureProvider:
171151 )
172152
173153 def new_unavailable_provider () -> FeatureProvider :
174- # Pointed at a closed port on localhost, never at the backend under
175- # test: that has to stay up, and simulated outages belong to the control
176- # API.
154+ # A closed port on localhost, never the backend under test: that has to
155+ # stay up for every other scenario.
177156 return FlagdProvider (
178157 resolver_type = suite .resolver_type ,
179158 host = "localhost" ,
0 commit comments