Skip to content

Commit 783c683

Browse files
committed
test(lazy): Lift _make_lazy_context into pytest fixtures
Replaces the module-level helper with three fixtures: * ``lazy_context_factory`` — keyword-only callable producing an evaluation context, for tests that need a non-default shape. * ``lazy_context`` — a default context (identity matches the segment override). * ``lazy_flags`` — a ``Flags`` built from the default context. Tests now request whichever fixture suits their case instead of calling the helper directly. While here, mark each test body with Given / When / Then comments to match the rest of the file. beep boop
1 parent 3a9e0c4 commit 783c683

1 file changed

Lines changed: 141 additions & 93 deletions

File tree

tests/test_models.py

Lines changed: 141 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -172,162 +172,207 @@ def test_get_flag_without_pipeline_processor() -> None:
172172
assert flag.enabled is True
173173

174174

175-
def _make_lazy_context(
176-
*,
177-
extra_features: int = 2,
178-
identity_trait_value: str = "premium",
179-
segment_match_value: str = "premium",
180-
) -> SDKEvaluationContext:
181-
"""Build a minimal evaluation context for lazy-Flags tests.
175+
LazyContextFactory = typing.Callable[..., SDKEvaluationContext]
176+
177+
178+
@pytest.fixture
179+
def lazy_context_factory() -> LazyContextFactory:
180+
"""Factory for minimal evaluation contexts used by the lazy-Flags tests.
182181
183-
Structure: a "target" feature with a single segment override that
184-
matches when ``tier == segment_match_value`` (priority 0), plus a
185-
handful of no-override "noise" features whose values should come
186-
straight off the base feature context. ``identity_trait_value`` sets
187-
the identity's ``tier`` trait so tests can exercise match / no-match.
182+
The returned context has a ``target`` feature with a single segment
183+
override (matches when ``tier == segment_match_value``, priority 0)
184+
plus ``extra_features`` no-override "noise" features whose values
185+
should come straight off the base feature context.
188186
"""
189-
features: typing.Dict[str, typing.Any] = {
190-
"target": {
191-
"key": "target",
192-
"name": "target",
193-
"enabled": False,
194-
"value": "base-value",
195-
"metadata": {"id": 1},
196-
},
197-
}
198-
for i in range(extra_features):
199-
features[f"noise_{i}"] = {
200-
"key": f"noise_{i}",
201-
"name": f"noise_{i}",
202-
"enabled": True,
203-
"value": f"noise-value-{i}",
204-
"metadata": {"id": 100 + i},
187+
188+
def make(
189+
*,
190+
extra_features: int = 2,
191+
identity_trait_value: str = "premium",
192+
segment_match_value: str = "premium",
193+
) -> SDKEvaluationContext:
194+
features: typing.Dict[str, typing.Any] = {
195+
"target": {
196+
"key": "target",
197+
"name": "target",
198+
"enabled": False,
199+
"value": "base-value",
200+
"metadata": {"id": 1},
201+
},
205202
}
206-
return {
207-
"environment": {"key": "env-key", "name": "env"},
208-
"features": features,
209-
"segments": {
210-
"premium_segment": {
211-
"key": "premium_segment",
212-
"name": "premium_segment",
213-
"rules": [
214-
{
215-
"type": "ALL",
216-
"conditions": [
217-
{
218-
"property": "tier",
219-
"operator": "EQUAL",
220-
"value": segment_match_value,
221-
},
222-
],
223-
}
224-
],
225-
"overrides": [
226-
{
227-
"key": "target",
228-
"name": "target",
229-
"enabled": True,
230-
"value": "premium-value",
231-
"priority": 0.0,
232-
"metadata": {"id": 1},
233-
},
234-
],
203+
for i in range(extra_features):
204+
features[f"noise_{i}"] = {
205+
"key": f"noise_{i}",
206+
"name": f"noise_{i}",
207+
"enabled": True,
208+
"value": f"noise-value-{i}",
209+
"metadata": {"id": 100 + i},
210+
}
211+
return {
212+
"environment": {"key": "env-key", "name": "env"},
213+
"features": features,
214+
"segments": {
215+
"premium_segment": {
216+
"key": "premium_segment",
217+
"name": "premium_segment",
218+
"rules": [
219+
{
220+
"type": "ALL",
221+
"conditions": [
222+
{
223+
"property": "tier",
224+
"operator": "EQUAL",
225+
"value": segment_match_value,
226+
},
227+
],
228+
}
229+
],
230+
"overrides": [
231+
{
232+
"key": "target",
233+
"name": "target",
234+
"enabled": True,
235+
"value": "premium-value",
236+
"priority": 0.0,
237+
"metadata": {"id": 1},
238+
},
239+
],
240+
},
235241
},
236-
},
237-
"identity": {
238-
"identifier": "user-1",
239-
"key": "env-key_user-1",
240-
"traits": {"tier": identity_trait_value},
241-
},
242-
}
242+
"identity": {
243+
"identifier": "user-1",
244+
"key": "env-key_user-1",
245+
"traits": {"tier": identity_trait_value},
246+
},
247+
}
243248

249+
return make
244250

245-
def test_lazy_flags__get_flag__applies_matching_segment_override() -> None:
246-
ctx = _make_lazy_context()
247-
flags = Flags.from_evaluation_context(
248-
context=ctx,
249-
overrides_index=build_segment_overrides_index(ctx),
251+
252+
@pytest.fixture
253+
def lazy_context(
254+
lazy_context_factory: LazyContextFactory,
255+
) -> SDKEvaluationContext:
256+
"""Default evaluation context: identity matches the segment override."""
257+
return lazy_context_factory()
258+
259+
260+
@pytest.fixture
261+
def lazy_flags(lazy_context: SDKEvaluationContext) -> Flags:
262+
"""Lazy ``Flags`` built from the default context, no analytics, no handler."""
263+
return Flags.from_evaluation_context(
264+
context=lazy_context,
265+
overrides_index=build_segment_overrides_index(lazy_context),
250266
analytics_processor=None,
251267
default_flag_handler=None,
252268
)
253269

254-
target = flags.get_flag("target")
270+
271+
def test_lazy_flags__get_flag__applies_matching_segment_override(
272+
lazy_flags: Flags,
273+
) -> None:
274+
# Given: identity matches the segment rule (default context).
275+
# When: we read the targeted feature.
276+
target = lazy_flags.get_flag("target")
277+
# Then: the override wins over the base feature value.
255278
assert target.enabled is True
256279
assert target.value == "premium-value"
257280

258281

259-
def test_lazy_flags__get_flag__skips_non_matching_segment_override() -> None:
260-
# Segment rule requires tier == "premium"; identity has tier "free",
261-
# so the override must not win and base-value should come through.
262-
ctx = _make_lazy_context(identity_trait_value="free")
263-
282+
def test_lazy_flags__get_flag__skips_non_matching_segment_override(
283+
lazy_context_factory: LazyContextFactory,
284+
) -> None:
285+
# Given: segment rule requires tier == "premium" but identity has tier "free".
286+
ctx = lazy_context_factory(identity_trait_value="free")
264287
flags = Flags.from_evaluation_context(
265288
context=ctx,
266289
overrides_index=build_segment_overrides_index(ctx),
267290
analytics_processor=None,
268291
default_flag_handler=None,
269292
)
293+
294+
# When: we read the targeted feature.
270295
target = flags.get_flag("target")
296+
297+
# Then: the override doesn't win and base-value comes through.
271298
assert target.enabled is False
272299
assert target.value == "base-value"
273300

274301

275-
def test_lazy_flags__get_flag__caches_per_feature() -> None:
276-
ctx = _make_lazy_context(extra_features=5)
302+
def test_lazy_flags__get_flag__caches_per_feature(
303+
lazy_context_factory: LazyContextFactory,
304+
) -> None:
305+
# Given: a context with several no-override features.
306+
ctx = lazy_context_factory(extra_features=5)
277307
flags = Flags.from_evaluation_context(
278308
context=ctx,
279309
overrides_index=build_segment_overrides_index(ctx),
280310
analytics_processor=None,
281311
default_flag_handler=None,
282312
)
283313

314+
# When: we read a single feature once.
284315
flags.get_flag("noise_0")
285-
# Only the accessed feature is populated.
316+
317+
# Then: only that feature is populated in the cache.
286318
assert set(flags.flags.keys()) == {"noise_0"}
287319

288-
# A repeated read hits the cache rather than rebuilding the Flag.
320+
# And: repeated reads return the same Flag instance, not a rebuild.
289321
first = flags.get_flag("noise_0")
290322
second = flags.get_flag("noise_0")
291323
assert first is second
292324

293325

294-
def test_lazy_flags__all_flags__materialises_every_feature() -> None:
295-
ctx = _make_lazy_context(extra_features=3)
326+
def test_lazy_flags__all_flags__materialises_every_feature(
327+
lazy_context_factory: LazyContextFactory,
328+
) -> None:
329+
# Given: a context with three no-override features plus the target.
330+
ctx = lazy_context_factory(extra_features=3)
296331
flags = Flags.from_evaluation_context(
297332
context=ctx,
298333
overrides_index=build_segment_overrides_index(ctx),
299334
analytics_processor=None,
300335
default_flag_handler=None,
301336
)
302337

338+
# When: the caller asks for the full set.
303339
materialised = flags.all_flags()
340+
341+
# Then: every feature in the context is present.
304342
names = {flag.feature_name for flag in materialised}
305343
assert names == {"target", "noise_0", "noise_1", "noise_2"}
306-
# Second call is a no-op: everything is already resolved.
307-
assert flags.all_flags() == materialised
308344

345+
# And: a second call is a no-op — everything is already resolved.
346+
assert flags.all_flags() == materialised
309347

310-
def test_lazy_flags__missing_feature__falls_through_to_default_handler() -> None:
311-
ctx = _make_lazy_context()
312348

349+
def test_lazy_flags__missing_feature__falls_through_to_default_handler(
350+
lazy_context: SDKEvaluationContext,
351+
) -> None:
352+
# Given: a Flags wired to a default-flag handler.
313353
def default(name: str) -> DefaultFlag:
314354
return DefaultFlag(enabled=False, value=f"default-for-{name}")
315355

316356
flags = Flags.from_evaluation_context(
317-
context=ctx,
318-
overrides_index=build_segment_overrides_index(ctx),
357+
context=lazy_context,
358+
overrides_index=build_segment_overrides_index(lazy_context),
319359
analytics_processor=None,
320360
default_flag_handler=default,
321361
)
362+
363+
# When: we ask for a feature that isn't in the context.
322364
result = flags.get_flag("does_not_exist")
365+
366+
# Then: the handler produces the default flag for that name.
323367
assert result.value == "default-for-does_not_exist"
324368

325369

326-
def test_build_segment_overrides_index__indexes_only_overriding_segments() -> None:
327-
ctx = _make_lazy_context()
328-
# Add a second segment without overrides — must not appear in the index.
329-
assert ctx["segments"] is not None
330-
ctx["segments"]["no_override_segment"] = {
370+
def test_build_segment_overrides_index__indexes_only_overriding_segments(
371+
lazy_context: SDKEvaluationContext,
372+
) -> None:
373+
# Given: a second segment with no overrides on top of the default context.
374+
assert lazy_context["segments"] is not None
375+
lazy_context["segments"]["no_override_segment"] = {
331376
"key": "no_override_segment",
332377
"name": "no_override_segment",
333378
"rules": [
@@ -340,6 +385,9 @@ def test_build_segment_overrides_index__indexes_only_overriding_segments() -> No
340385
],
341386
}
342387

343-
index = build_segment_overrides_index(ctx)
388+
# When: we build the reverse index.
389+
index = build_segment_overrides_index(lazy_context)
390+
391+
# Then: only segments that actually carry an override appear.
344392
assert set(index) == {"target"}
345393
assert index["target"][0]["name"] == "premium_segment"

0 commit comments

Comments
 (0)