-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Change how notifications work for feature flags.
Due to an oversight in what Python allows, the notification methods for feature flag changes was made abstract, requiring subclasses to implement it. However, this was intended to be optional because the feature flag base class is meant to only require implementations for the get/set interface. To resolve this, the `abstractmethod` decoration has been removed. Another change here regards nonsensical notifications in `CachingFeatureFlagRouter`. The previous notification logged any time `set` was called, and always said that the flag changed from its current value to its current value (that is not a typo). In addition to fixing this, the notification now indicates: * When an attempt was made to set a flag to its current value * When a flag was set, and what its new value is * When a new flag is set and cached Related #98
- Loading branch information
Showing
4 changed files
with
229 additions
and
29 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
134 changes: 134 additions & 0 deletions
134
src/platform/test/unit/feature_flags/test_caching_feature_flag_router.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
import logging | ||
from typing import Any | ||
|
||
import pytest | ||
from BL_Python.platform.feature_flag.caching_feature_flag_router import ( | ||
CachingFeatureFlagRouter, | ||
) | ||
from typing_extensions import override | ||
|
||
_FEATURE_FLAG_TEST_NAME = "foo_feature" | ||
|
||
|
||
def test__feature_is_enabled__disallows_empty_name(): | ||
logger = logging.getLogger("FeatureFlagLogger") | ||
caching_feature_flag_router = CachingFeatureFlagRouter(logger) | ||
|
||
with pytest.raises(ValueError): | ||
_ = caching_feature_flag_router.feature_is_enabled("") | ||
|
||
|
||
@pytest.mark.parametrize("name", [0, False, True, {}, [], (0,)]) | ||
def test__feature_is_enabled__disallows_non_string_names(name: Any): | ||
logger = logging.getLogger("FeatureFlagLogger") | ||
caching_feature_flag_router = CachingFeatureFlagRouter(logger) | ||
|
||
with pytest.raises(TypeError): | ||
_ = caching_feature_flag_router.feature_is_enabled(name) | ||
|
||
|
||
def test__set_feature_is_enabled__disallows_empty_name(): | ||
logger = logging.getLogger("FeatureFlagLogger") | ||
caching_feature_flag_router = CachingFeatureFlagRouter(logger) | ||
|
||
with pytest.raises(ValueError): | ||
caching_feature_flag_router.set_feature_is_enabled("", False) | ||
|
||
|
||
@pytest.mark.parametrize("name", [0, False, True, {}, [], (0,)]) | ||
def test__set_feature_is_enabled__disallows_non_string_names(name: Any): | ||
logger = logging.getLogger("FeatureFlagLogger") | ||
caching_feature_flag_router = CachingFeatureFlagRouter(logger) | ||
|
||
with pytest.raises(TypeError): | ||
caching_feature_flag_router.set_feature_is_enabled(name, False) | ||
|
||
|
||
@pytest.mark.parametrize("value", [None, "", "False", "True", 0, 1, -1, {}, [], (0,)]) | ||
def test__set_feature_is_enabled__disallows_non_bool_values(value: Any): | ||
logger = logging.getLogger("FeatureFlagLogger") | ||
caching_feature_flag_router = CachingFeatureFlagRouter(logger) | ||
|
||
with pytest.raises(TypeError) as e: | ||
_ = caching_feature_flag_router.set_feature_is_enabled( | ||
_FEATURE_FLAG_TEST_NAME, value | ||
) | ||
|
||
assert e.match("`is_enabled` must be a boolean") | ||
|
||
|
||
@pytest.mark.parametrize("value", [True, False]) | ||
def test__set_feature_is_enabled__sets_correct_value(value: bool): | ||
logger = logging.getLogger("FeatureFlagLogger") | ||
caching_feature_flag_router = CachingFeatureFlagRouter(logger) | ||
|
||
caching_feature_flag_router.set_feature_is_enabled(_FEATURE_FLAG_TEST_NAME, value) | ||
is_enabled = caching_feature_flag_router.feature_is_enabled(_FEATURE_FLAG_TEST_NAME) | ||
|
||
assert is_enabled == value | ||
|
||
|
||
def test__feature_is_enabled__defaults_to_false_when_flag_does_not_exist(): | ||
logger = logging.getLogger("FeatureFlagLogger") | ||
caching_feature_flag_router = CachingFeatureFlagRouter(logger) | ||
|
||
is_enabled = caching_feature_flag_router.feature_is_enabled(_FEATURE_FLAG_TEST_NAME) | ||
|
||
assert is_enabled == False | ||
|
||
|
||
@pytest.mark.parametrize("value", [True, False]) | ||
def test__set_feature_is_enabled__caches_new_flag(value: bool): | ||
logger = logging.getLogger("FeatureFlagLogger") | ||
caching_feature_flag_router = CachingFeatureFlagRouter(logger) | ||
|
||
caching_feature_flag_router.set_feature_is_enabled(_FEATURE_FLAG_TEST_NAME, value) | ||
assert _FEATURE_FLAG_TEST_NAME in caching_feature_flag_router._feature_flags # pyright: ignore[reportPrivateUsage] | ||
is_enabled = caching_feature_flag_router._feature_flags.get(_FEATURE_FLAG_TEST_NAME) # pyright: ignore[reportPrivateUsage] | ||
assert is_enabled == value | ||
|
||
|
||
@pytest.mark.parametrize("value", [True, False]) | ||
def test__feature_is_enabled__uses_cache(value: bool): | ||
logger = logging.getLogger("FeatureFlagLogger") | ||
caching_feature_flag_router = CachingFeatureFlagRouter(logger) | ||
|
||
call_count = 0 | ||
|
||
class FakeDict(dict): # pyright: ignore[reportMissingTypeArgument] | ||
@override | ||
def __getitem__(self, key: Any) -> Any: | ||
nonlocal call_count | ||
call_count += 1 | ||
return super().__getitem__(key) # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType] | ||
|
||
caching_feature_flag_router._feature_flags = FakeDict() # pyright: ignore[reportPrivateUsage] | ||
|
||
caching_feature_flag_router.set_feature_is_enabled(_FEATURE_FLAG_TEST_NAME, value) | ||
_ = caching_feature_flag_router.feature_is_enabled(_FEATURE_FLAG_TEST_NAME) | ||
_ = caching_feature_flag_router.feature_is_enabled(_FEATURE_FLAG_TEST_NAME) | ||
|
||
assert call_count == 2 | ||
|
||
|
||
@pytest.mark.parametrize("enable", [True, False]) | ||
def test__set_feature_is_enabled__resets_cache_when_flag_enable_is_set(enable: bool): | ||
logger = logging.getLogger("FeatureFlagLogger") | ||
caching_feature_flag_router = CachingFeatureFlagRouter(logger) | ||
|
||
caching_feature_flag_router.set_feature_is_enabled(_FEATURE_FLAG_TEST_NAME, enable) | ||
_ = caching_feature_flag_router.feature_is_enabled(_FEATURE_FLAG_TEST_NAME) | ||
first_value = caching_feature_flag_router.feature_is_enabled( | ||
_FEATURE_FLAG_TEST_NAME | ||
) | ||
|
||
caching_feature_flag_router.set_feature_is_enabled( | ||
_FEATURE_FLAG_TEST_NAME, not enable | ||
) | ||
_ = caching_feature_flag_router.feature_is_enabled(_FEATURE_FLAG_TEST_NAME) | ||
second_value = caching_feature_flag_router.feature_is_enabled( | ||
_FEATURE_FLAG_TEST_NAME | ||
) | ||
|
||
assert first_value == enable | ||
assert second_value == (not enable) |
73 changes: 73 additions & 0 deletions
73
src/platform/test/unit/feature_flags/test_feature_flag_router.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import ast | ||
import inspect | ||
|
||
from BL_Python.platform.feature_flag.feature_flag_router import FeatureFlagRouter | ||
from typing_extensions import override | ||
|
||
_FEATURE_FLAG_TEST_NAME = "foo_feature" | ||
|
||
|
||
class TestFeatureFlagRouter(FeatureFlagRouter): | ||
@override | ||
def set_feature_is_enabled(self, name: str, is_enabled: bool) -> None: | ||
return super().set_feature_is_enabled(name, is_enabled) | ||
|
||
@override | ||
def feature_is_enabled( | ||
self, name: str, default: bool | None = False | ||
) -> bool | None: | ||
return super().feature_is_enabled(name, default) | ||
|
||
|
||
class NotifyingFeatureFlagRouter(TestFeatureFlagRouter): | ||
def __init__(self) -> None: | ||
self.notification_count = 0 | ||
super().__init__() | ||
|
||
@override | ||
def _notify_change( | ||
self, name: str, new_value: bool, old_value: bool | None | ||
) -> None: | ||
self.notification_count += 1 | ||
return super()._notify_change(name, new_value, old_value) | ||
|
||
|
||
def test___notify_change__is_a_noop(): | ||
test_feature_flag_router = TestFeatureFlagRouter() | ||
source = inspect.getsource(test_feature_flag_router._notify_change) # pyright: ignore[reportPrivateUsage] | ||
# strip the leading \t because the method is in a class | ||
# and the AST parse will fail because of unexpected indent | ||
parsed = ast.parse(source.strip()) | ||
function_node = parsed.body[0] | ||
# [0] is the docblock | ||
function_body_nodes = function_node.body[1:] # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType,reportAttributeAccessIssue] | ||
|
||
# empty means the function doesn't contain any code | ||
assert not function_body_nodes | ||
|
||
|
||
def test__set_feature_is_enabled__does_not_notify(): | ||
notifying_feature_flag_router = NotifyingFeatureFlagRouter() | ||
|
||
notifying_feature_flag_router.set_feature_is_enabled(_FEATURE_FLAG_TEST_NAME, True) | ||
|
||
assert notifying_feature_flag_router.notification_count == 0 | ||
|
||
|
||
def test__feature_is_enabled__does_not_notify(): | ||
notifying_feature_flag_router = NotifyingFeatureFlagRouter() | ||
|
||
_ = notifying_feature_flag_router.feature_is_enabled(_FEATURE_FLAG_TEST_NAME) | ||
|
||
assert notifying_feature_flag_router.notification_count == 0 | ||
|
||
|
||
def test__feature_is_enabled__does_not_notify_after_flag_is_set(): | ||
notifying_feature_flag_router = NotifyingFeatureFlagRouter() | ||
|
||
notifying_feature_flag_router.set_feature_is_enabled(_FEATURE_FLAG_TEST_NAME, True) | ||
notifying_feature_flag_router.notification_count = 0 | ||
|
||
_ = notifying_feature_flag_router.feature_is_enabled(_FEATURE_FLAG_TEST_NAME) | ||
|
||
assert notifying_feature_flag_router.notification_count == 0 |