Skip to content

Commit

Permalink
Merge pull request quipucords#2132 from nicolearagao/task-feature-flag
Browse files Browse the repository at this point in the history
Creates class FeatureFlag to handle when new features will be added
  • Loading branch information
nicolearagao committed Feb 23, 2022
2 parents 9fde45f + 57463c6 commit e901edf
Show file tree
Hide file tree
Showing 5 changed files with 217 additions and 1 deletion.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,6 @@ yarn.lock

# symbolic link to roles
quipucords/roles/

# asdf
.tool-versions
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ swagger-valid:
node_modules/swagger-cli/swagger-cli.js validate docs/swagger.yml

lint-flake8:
flake8 . --ignore D203,W504,W605 --exclude quipucords/api/migrations,docs,build,.vscode,client,venv,deploy,quipucords/local_gunicorn.conf.py
flake8 . --ignore D203,W504,W605,Q000 --exclude quipucords/api/migrations,docs,build,.vscode,client,venv,deploy,quipucords/local_gunicorn.conf.py

lint-pylint:
find . -name "*.py" -not -name "*0*.py" -not -path "./build/*" -not -path "./docs/*" -not -path "./.vscode/*" -not -path "./client/*" -not -path "./venv/*" -not -path "./deploy/*" -not -path "./quipucords/local_gunicorn.conf.py" | DJANGO_SETTINGS_MODULE=quipucords.settings xargs $(PYTHON) -m pylint --load-plugins=pylint_django --disable=duplicate-code,wrong-import-order,useless-import-alias,unnecessary-pass,too-many-lines,raise-missing-from
Expand Down
46 changes: 46 additions & 0 deletions quipucords/quipucords/featureflag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""Filters feature flags from system env variables."""

import os

DEFAULT_FEATURE_FLAGS_VALUES = {
"OVERALL_STATUS": False,
}

VALID_VALUES_FOR_ENV_VARIABLES = ["0", "1"]


class FeatureFlag:
"""Handle environment variables and its status."""

def __init__(self):
"""Attributes and values are generated dynamically.
Attributes keys-values are received in initial_data.
"""
initial_data = self.get_feature_flags_from_env()
for key, value in initial_data.items():
setattr(self, key, value)

def is_feature_active(self, feature_name):
"""Return attribute value."""
try:
return getattr(self, feature_name)
except AttributeError:
raise ValueError(f"{feature_name=} is not a valid input.")

@classmethod
def get_feature_flags_from_env(cls):
"""Filter feature flags from environment variables."""
feature_flags = DEFAULT_FEATURE_FLAGS_VALUES.copy()
for key, value in os.environ.items():
if key.upper().startswith("QPC_FEATURE_"):
feature_name = key.upper().replace("QPC_FEATURE_", "")
feature_value = value.strip()
if feature_value in VALID_VALUES_FOR_ENV_VARIABLES:
feature_flags[feature_name] = bool(int(feature_value))
else:
raise ValueError(
f"'{feature_value}' from '{key}' can't be converted "
"to int, verify your environment variables."
)
return feature_flags
5 changes: 5 additions & 0 deletions quipucords/quipucords/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
import logging
import os

from .featureflag import FeatureFlag

# Get an instance of a logger
logger = logging.getLogger(__name__) # pylint: disable=invalid-name

Expand Down Expand Up @@ -405,3 +407,6 @@ def is_int(value):
'QPC_INSIGHTS_REPORT_SLICE_SIZE', '10000')
if is_int(QPC_INSIGHTS_REPORT_SLICE_SIZE):
QPC_INSIGHTS_REPORT_SLICE_SIZE = int(QPC_INSIGHTS_REPORT_SLICE_SIZE)

# Load Feature Flags
QPC_FEATURE_FLAGS = FeatureFlag()
162 changes: 162 additions & 0 deletions quipucords/quipucords/tests_feature_flags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
"""Test the FeatureFlag class and helper function."""

import os
from unittest import mock

import pytest

from .featureflag import FeatureFlag


@pytest.mark.parametrize(
"env_name,env_value,feature_name,feature_value",
(
("QPC_FEATURE_TEST", "0", "TEST", False),
("QPC_FEATURE_TEST", "1", "TEST", True),
),
)
def test_get_feature_flags_from_env(
env_name, env_value, feature_name, feature_value
):
"""Tests if function retrieves new variables set in .env."""
with mock.patch.dict(os.environ, {env_name: env_value}):
dict_with_test_flags = FeatureFlag.get_feature_flags_from_env()
assert feature_name in dict_with_test_flags
assert dict_with_test_flags[feature_name] is feature_value


def test_if_returns_default_value_if_another_env_set():
"""Tests if function also returns variables in default dict."""
with mock.patch.dict(os.environ, ({"QPC_FEATURE_TEST": "0"})):
dict_with_test_flags = FeatureFlag.get_feature_flags_from_env()
assert "OVERALL_STATUS" in dict_with_test_flags
assert "TEST" in dict_with_test_flags
assert dict_with_test_flags["OVERALL_STATUS"] is False
assert dict_with_test_flags["TEST"] is False


@pytest.mark.parametrize(
"env_name,env_value,feature_name,feature_value",
(
("QPC_FEATURE_OVERALL_STATUS", "1", "OVERALL_STATUS", True),
("QPC_FEATURE_TEST", "0", "TEST", False),
),
)
def test_if_value_for_env_default_list_gets_updated(
env_name, env_value, feature_name, feature_value
):
"""Tests if function updates env variable in default."""
with mock.patch.dict(os.environ, ({env_name: env_value})):
dict_with_test_flags = FeatureFlag.get_feature_flags_from_env()
assert feature_name in dict_with_test_flags
assert dict_with_test_flags[feature_name] is feature_value


def test_when_value_cant_be_cast_to_int():
"""Tests if function only updates values if it can be cast to int."""
with mock.patch.dict(
os.environ, ({"QPC_FEATURE_OVERALL_STATUS": "wrong"})
), pytest.raises(ValueError) as exc:
FeatureFlag.get_feature_flags_from_env()
assert (
str(exc.value) == "'wrong' from 'QPC_FEATURE_OVERALL_STATUS'"
" can't be converted to int, verify your"
" environment variables."
)


@pytest.mark.parametrize(
"env_name,env_value",
(
("QPC_FEATURE_TEST", "10"),
("QPC_FEATURE_TEST1", "3"),
("QPC_FEATURE_TEST2", "2000"),
),
)
def test_when_int_is_not_valid_value_for_env(
env_name, env_value,
):
"""Test when int is not a valid value for env."""
with mock.patch.dict(os.environ, ({env_name: env_value})), pytest.raises(
ValueError, match="can't be converted to int"
):
FeatureFlag.get_feature_flags_from_env()


@pytest.mark.parametrize(
"env_name,env_value,feature_name",
(
("TEST_QPC_FEATURE_", "1", "TEST"),
("TEST_QPC_FEATURE_", "1", "TEST_"),
("QPC_TEST1_FEATURE_", "0", "TEST1"),
("QPC_TEST1_FEATURE_", "0", "_TEST1"),
("QPC_TEST1_FEATURE_", "0", "TEST1_"),
),
)
def test_function_only_adds_names_follow_standard(
env_name, env_value, feature_name
):
"""Tests if function only adds variables that start with QPC_FEATURE_."""
with mock.patch.dict(os.environ, ({env_name: env_value})):
dict_with_test_flags = FeatureFlag.get_feature_flags_from_env()
assert feature_name not in dict_with_test_flags


@pytest.mark.parametrize(
"env_name,env_value,feature_name,feature_value",
(
("qpc_feature_test", "1", "TEST", True),
("qpc_feature_TEST1", "0", "TEST1", False),
("QPC_feature_TEST2", "0", "TEST2", False),
("qpc_FEATURE_test3", "1", "TEST3", True),
),
)
def test_if_function_is_not_case_sensitive(
env_name, env_value, feature_name, feature_value
):
"""Tests if function is not case-sensitive."""
with mock.patch.dict(os.environ, ({env_name: env_value})):
dict_with_test_flags = FeatureFlag.get_feature_flags_from_env()
assert feature_name in dict_with_test_flags
assert dict_with_test_flags[feature_name] is feature_value


@pytest.fixture
def setup_feature_flag_instance_for_tests():
"""Set up instance of FeatureFlag class for tests."""
with mock.patch.dict(os.environ, {"QPC_FEATURE_TEST": "1"}):
feature_flag_instance = FeatureFlag()
return feature_flag_instance


def test_if_instance_contains_all_attributes(
setup_feature_flag_instance_for_tests, # pylint: disable=redefined-outer-name # noqa: E501
):
"""Tests if the constructor loads all attributes correctly."""
assert hasattr(setup_feature_flag_instance_for_tests, "TEST")
assert hasattr(setup_feature_flag_instance_for_tests, "OVERALL_STATUS")


def test_if_instance_attributes_values_are_correct(
setup_feature_flag_instance_for_tests, # pylint: disable=redefined-outer-name # noqa: E501
):
"""Tests if the right values are attributed to attribute."""
assert setup_feature_flag_instance_for_tests.TEST is True
assert setup_feature_flag_instance_for_tests.OVERALL_STATUS is False


def test_is_feature_active(setup_feature_flag_instance_for_tests): # pylint: disable=redefined-outer-name # noqa: E501
"""Tests method is_feature_active."""
assert (
setup_feature_flag_instance_for_tests.is_feature_active("TEST") is True
)
assert (
setup_feature_flag_instance_for_tests.is_feature_active(
"OVERALL_STATUS"
)
is False
)
with pytest.raises(ValueError):
setup_feature_flag_instance_for_tests.is_feature_active(
"FALSE_ATTRIBUTE"
)

0 comments on commit e901edf

Please sign in to comment.