|
| 1 | +import json |
| 2 | + |
| 3 | +import pytest |
1 | 4 | from growthbook.common_types import FeatureResult |
2 | 5 | from sqlalchemy import select |
3 | 6 |
|
4 | 7 | from couchers import experimentation |
5 | 8 | from couchers.config import config |
6 | 9 | from couchers.context import make_background_user_context, make_logged_out_context |
7 | 10 | from couchers.db import session_scope |
8 | | -from couchers.experimentation import _record_feature_usage |
| 11 | +from couchers.experimentation import GrowthBookUnavailableError, _record_feature_usage, setup_experimentation |
9 | 12 | from couchers.i18n import LocalizationContext |
10 | 13 | from couchers.models.logging import ExperimentExposure, FeatureUsage |
11 | 14 | from couchers.proto import bugs_pb2 |
@@ -145,3 +148,68 @@ def test_global_evaluation_gets_global_force_on_flag(feature_flags): |
145 | 148 |
|
146 | 149 | def test_global_evaluation_unknown_feature_returns_in_code_default(feature_flags): |
147 | 150 | assert experimentation.get_global_string_value("does_not_exist", "my_default") == "my_default" |
| 151 | + |
| 152 | + |
| 153 | +@pytest.fixture |
| 154 | +def setup_isolation(monkeypatch, tmp_path): |
| 155 | + """Run setup_experimentation() against a clean module state and a tmp cache path, and make sure the |
| 156 | + background refresh thread it starts is stopped afterwards.""" |
| 157 | + monkeypatch.setattr(experimentation, "_initialized", False) |
| 158 | + monkeypatch.setattr(experimentation, "_last_fetch_time", None) |
| 159 | + monkeypatch.setattr(experimentation, "_state", {"features": {}, "savedGroups": {}}) |
| 160 | + monkeypatch.setitem(config, "EXPERIMENTATION_ENABLED", True) |
| 161 | + monkeypatch.setitem(config, "GROWTHBOOK_CACHE_PATH", str(tmp_path / "cache.json")) |
| 162 | + yield tmp_path / "cache.json" |
| 163 | + experimentation._refresh_stop.set() |
| 164 | + if experimentation._refresh_thread is not None: |
| 165 | + experimentation._refresh_thread.join(timeout=5) |
| 166 | + experimentation._refresh_stop.clear() |
| 167 | + experimentation._refresh_thread = None |
| 168 | + |
| 169 | + |
| 170 | +def test_setup_writes_cache_and_records_fetch_time(setup_isolation, monkeypatch): |
| 171 | + cache = setup_isolation |
| 172 | + payload = {"features": {"f": {"defaultValue": True}}, "savedGroups": {}} |
| 173 | + monkeypatch.setattr(experimentation, "_fetch_features", lambda: payload) |
| 174 | + |
| 175 | + setup_experimentation() |
| 176 | + |
| 177 | + assert experimentation._state["features"] == {"f": {"defaultValue": True}} |
| 178 | + assert experimentation.seconds_since_last_fetch() is not None |
| 179 | + written = json.loads(cache.read_text()) |
| 180 | + assert written["response"] == payload |
| 181 | + assert "fetched_at" in written |
| 182 | + |
| 183 | + |
| 184 | +def test_setup_falls_back_to_disk_cache_when_fetch_fails(setup_isolation, monkeypatch): |
| 185 | + cache = setup_isolation |
| 186 | + cached_payload = {"features": {"cached": {"defaultValue": "x"}}, "savedGroups": {}} |
| 187 | + cache.write_text(json.dumps({"fetched_at": 1000.0, "response": cached_payload})) |
| 188 | + monkeypatch.setattr(experimentation, "_fetch_features", lambda: None) |
| 189 | + |
| 190 | + setup_experimentation() |
| 191 | + |
| 192 | + assert experimentation._state["features"] == {"cached": {"defaultValue": "x"}} |
| 193 | + # fetch time reflects the cached pull time, so staleness is large immediately |
| 194 | + staleness = experimentation.seconds_since_last_fetch() |
| 195 | + assert staleness is not None and staleness > 0 |
| 196 | + |
| 197 | + |
| 198 | +def test_setup_raises_when_fetch_fails_and_no_cache(setup_isolation, monkeypatch): |
| 199 | + monkeypatch.setattr(experimentation, "_fetch_features", lambda: None) |
| 200 | + |
| 201 | + with pytest.raises(GrowthBookUnavailableError): |
| 202 | + setup_experimentation() |
| 203 | + |
| 204 | + |
| 205 | +def test_setup_raises_on_corrupt_cache(setup_isolation, monkeypatch): |
| 206 | + cache = setup_isolation |
| 207 | + cache.write_text("this is not json") |
| 208 | + monkeypatch.setattr(experimentation, "_fetch_features", lambda: None) |
| 209 | + |
| 210 | + with pytest.raises(json.JSONDecodeError): |
| 211 | + setup_experimentation() |
| 212 | + |
| 213 | + |
| 214 | +def test_seconds_since_last_fetch_none_when_never_fetched(setup_isolation): |
| 215 | + assert experimentation.seconds_since_last_fetch() is None |
0 commit comments