diff --git a/featureflags_client/http/conditions.py b/featureflags_client/http/conditions.py index 2adeca9..8926ba4 100644 --- a/featureflags_client/http/conditions.py +++ b/featureflags_client/http/conditions.py @@ -3,6 +3,7 @@ from typing import Any, Callable, Dict, List, Optional, Set from featureflags_client.http.types import Check, Flag, Operator +from featureflags_client.http.utils import hash_flag_value log = logging.getLogger(__name__) @@ -79,7 +80,11 @@ def percent(name: str, value: Any) -> Callable: @except_false def proc(ctx: Dict[str, Any]) -> bool: ctx_val = ctx.get(name, _UNDEFINED) - return ctx_val is not _UNDEFINED and hash(ctx_val) % 100 < int(value) + if ctx_val is _UNDEFINED: + return False + + hash_ctx_val = hash_flag_value(name, ctx_val) + return hash_ctx_val % 100 < int(value) return proc diff --git a/featureflags_client/http/utils.py b/featureflags_client/http/utils.py index 05f001a..059f4fa 100644 --- a/featureflags_client/http/utils.py +++ b/featureflags_client/http/utils.py @@ -1,4 +1,6 @@ +import hashlib import inspect +import struct from enum import Enum, EnumMeta from typing import Any, Dict, Generator, Mapping, Type, Union @@ -54,3 +56,9 @@ def intervals_gen( else: success = yield retry_interval retry_interval = min(retry_interval * 2, retry_interval_max) + + +def hash_flag_value(name: str, value: Any) -> int: + hash_digest = hashlib.md5(f"{name}{value}".encode()).digest() # noqa: S324 + (hash_int,) = struct.unpack(" bool: - return op("var", right)({"var": left} if left is not _UNDEFINED else {}) + context = {TEST_VARIABLE_NAME: left} if left is not _UNDEFINED else {} + return op(TEST_OPERATOR_NAME, right)(context) def test_false(): @@ -82,24 +87,30 @@ def test_contains(): def test_percent(): - assert check_op(0, percent, 1) is True - assert check_op(1, percent, 1) is False - assert check_op(1, percent, 2) is True - + # If percent <= 0 return False for i in range(-150, 150): assert check_op(i, percent, 0) is False + + # If percent >= 100 return True for i in range(-150, 150): assert check_op(i, percent, 100) is True - assert check_op("foo", percent, 100) is True - assert check_op("foo", percent, 0) is False - assert check_op("foo", percent, hash("foo") % 100 + 1) is True - assert check_op("foo", percent, hash("foo") % 100 - 1) is False - + # Check not integer values assert check_op(_UNDEFINED, percent, 100) is False + assert check_op(50, percent, _UNDEFINED) is False + assert check_op(_UNDEFINED, percent, _UNDEFINED) is False + assert check_op("foo", percent, "not_number") is False + # Check string values assert check_op("foo", percent, "100") is True - assert check_op("foo", percent, "not_number") is False + assert check_op("foo", percent, "0") is False + assert check_op("foo", percent, 100) is True + assert check_op("foo", percent, 0) is False + + # Check hash comparison + foo_hash = hash_flag_value(TEST_VARIABLE_NAME, "foo") + assert check_op("foo", percent, foo_hash % 100 + 1) is True + assert check_op("foo", percent, foo_hash % 100 - 1) is False def test_regexp():