Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add initial attempt at env-var based flag specification (and tests). #187

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# These are required to run the tests.
Django
django_jinja
flake8

# Everything else is in a Django-version-free version
Expand Down
82 changes: 82 additions & 0 deletions waffle/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from decimal import Decimal
import random
import os

from waffle.utils import get_setting, keyfmt

Expand All @@ -10,6 +11,37 @@
__version__ = '.'.join(map(str, VERSION))


def parse_env_vars(var_name):
"""
Takes the name of an environment variable that should be expressed as a
comma separated list of items. Returns the equivalent Python data
structure.
"""
value = os.getenv(var_name, '')
if value:
return [x for x in value.split(',') if x]
return []

"""
We default the settings derived from environment vars so the app can't break
when it's started because we've forgotton to configure something. Rather, it'll
just assume an 'all flags off' policy.

There are three arbitrary buckets: ALPHA, BETA and ALL. ALPHA and BETA have
named users associated with them and flags set for each bucket will only be
active for those users. ALL related flags apply to all users.
"""
USE_ENV_VARS = os.getenv('WAFFLE_USE_ENV_VARS', False)
if USE_ENV_VARS:
ALPHA_USERS = parse_env_vars('WAFFLE_ALPHA_USERS')
BETA_USERS = parse_env_vars('WAFFLE_BETA_USERS')
ALPHA_FLAGS = parse_env_vars('WAFFLE_ALPHA_FLAGS')
BETA_FLAGS = parse_env_vars('WAFFLE_BETA_FLAGS')
ALL_FLAGS = parse_env_vars('WAFFLE_ALL_FLAGS')
SWITCHES = parse_env_vars('WAFFLE_SWITCHES')
SAMPLES = parse_env_vars('WAFFLE_SAMPLES')


class DoesNotExist(object):
"""The record does not exist."""
@property
Expand All @@ -25,6 +57,56 @@ def set_flag(request, flag_name, active=True, session_only=False):


def flag_is_active(request, flag_name):
"""
Given the context of the request, indicate if the flag identified by the
flag_name is active.

This wraps two functions that use either the "normal" database method or
one based upon environment variables.
"""
if USE_ENV_VARS:
return flag_is_active_from_env(request, flag_name)
return flag_is_active_from_database(request, flag_name)


def flag_is_active_from_env(request, flag_name):
"""
A flag_name indicates that a referenced feature is active. The boolean
response depends on several things:

* The flag group the flag_name is found in.
* The membership of associated user buckets for the requesting user.

There are three flag groups: ALPHA, BETA and ALL. The ALPHA and BETA flag
groups refer to associated user buckets. ALL flags are applied to all
users.

Ergo, flags are explicitly set for ALPHA and BETA but flags set in ALL are
universal. For example, a flag called 'foo' set in the ALPHA_FLAGS group
will only be seen by members of the ALPHA_USERS bucket. However, if a flag
'bar' is set in the ALL_FLAGS group ALL users will see it.
"""
# Check there are flags set via the environment variables.
if not (ALPHA_FLAGS or BETA_FLAGS or ALL_FLAGS):
return False
# If the flag is an ALL flag, it's on for everyone!
if ALL_FLAGS and flag_name in ALL_FLAGS:
return True
# User may not be logged in.
if hasattr(request, 'user'):
username = request.user.username
# Arbitrary decision that ALPHA takes precedence.
if ALPHA_FLAGS and flag_name in ALPHA_FLAGS:
return username in ALPHA_USERS
elif BETA_FLAGS and flag_name in BETA_FLAGS:
return username in BETA_USERS
return False


def flag_is_active_from_database(request, flag_name):
"""
A regular waffle.
"""
from .models import cache_flag, Flag
from .compat import cache

Expand Down
135 changes: 135 additions & 0 deletions waffle/tests/test_waffle.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,141 @@ def test_set_then_unset_testing_flag(self):
self.assertEqual(b'on', response.content)


class EnvVarTests(TestCase):
"""
Tests related to configuration derived from environment variables.
"""

def setUp(cls):
setattr(waffle, 'USE_ENV_VARS', True)
setattr(waffle, 'ALPHA_USERS', [])
setattr(waffle, 'BETA_USERS', [])
setattr(waffle, 'ALPHA_FLAGS', [])
setattr(waffle, 'BETA_FLAGS', [])
setattr(waffle, 'ALL_FLAGS', [])

@classmethod
def tearDownClass(cls):
setattr(waffle, 'USE_ENV_VARS', False)

def test_parse_env_vars(self):
"""
A comma-separated list of items is turned into a ordered list.
"""
with mock.patch('os.getenv', return_value='foo,bar,baz') as patch:
actual = waffle.parse_env_vars('foo')
expected = ['foo', 'bar', 'baz']
assert expected == actual

def test_parse_env_vars_malformed(self):
"""
A malformed comma-separated list is "mended" appropriately (empty
entries are ignored).
"""
with mock.patch('os.getenv', return_value='foo,,baz') as patch:
actual = waffle.parse_env_vars('foo')
expected = ['foo', 'baz']
assert expected == actual

def test_parse_env_vars_nonexistent(self):
"""
A none-existent environment variable will return an empty list.
"""
with mock.patch('os.getenv', return_value=None) as patch:
actual = waffle.parse_env_vars('foo')
expected = []
assert expected == actual

def test_use_env_vars_code_branch(self):
"""
Ensure the expected code branch is used when USE_ENV_VARS is truthy.
"""
with mock.patch('waffle.flag_is_active_from_env',
return_value=False) as patch:
request = get()
assert not waffle.flag_is_active(request, 'foo')
patch.assert_called_once_with(request, 'foo')

def test_no_flags_set(self):
"""
ALPHA, BETA and ALL are empty. Must return False
"""
setattr(waffle, 'ALPHA_FLAGS', [])
setattr(waffle, 'BETA_FLAGS', [])
setattr(waffle, 'ALL_FLAGS', [])
request = get()
assert not waffle.flag_is_active(request, 'foo')

def test_all_flags_matched(self):
"""
The flag has been found in ALL_FLAGS. Must return True.
"""
setattr(waffle, 'ALL_FLAGS', ['foo', ])
request = get()
assert waffle.flag_is_active(request, 'foo')

def test_flag_in_alpha_not_alpha_user(self):
"""
The referenced flag is in the ALPHA bucket but the requesting user
isn't an ALPHA_USER. Must return False.
"""
setattr(waffle, 'ALPHA_FLAGS', ['foo', ])
request = get()
assert not waffle.flag_is_active(request, 'foo')

def test_flag_in_alpha_with_alpha_user(self):
"""
The referenced flag is in the ALPHA bucket AND the requesting user is
an ALPHA_USER. Must return True.
"""
setattr(waffle, 'ALPHA_FLAGS', ['foo', ])
setattr(waffle, 'ALPHA_USERS', ['bar', ])
request = get()
request.user.username = 'bar'
assert waffle.flag_is_active(request, 'foo')

def test_flag_in_beta_not_beta_user(self):
"""
The referenced flag is in the BETA bucket but the requesting user
isn't a BETA_USER. Must return False.
"""
setattr(waffle, 'BETA_FLAGS', ['foo', ])
request = get()
assert not waffle.flag_is_active(request, 'foo')

def test_flag_in_beta_with_beta_user(self):
"""
The referenced flag is in the BETA bucket AND the requesting user is
a BETA_USER. Must return True.
"""
setattr(waffle, 'BETA_FLAGS', ['foo', ])
setattr(waffle, 'BETA_USERS', ['bar', ])
request = get()
request.user.username = 'bar'
assert waffle.flag_is_active(request, 'foo')

def test_flag_does_not_match_existing_flags(self):
"""
There are flags in all the *_FLAGS buckets but the referenced flag does
not match. Must return False.
"""
setattr(waffle, 'ALL_FLAGS', ['foo', ])
setattr(waffle, 'ALPHA_FLAGS', ['bar', ])
setattr(waffle, 'BETA_FLAGS', ['baz', ])
request = get()
assert not waffle.flag_is_active(request, 'qux')

def test_flag_no_user(self):
"""
If there is no user to match into the ALPHA and BETA buckets then
return False.
"""
setattr(waffle, 'ALPHA_FLAGS', ['foo', ])
request = get()
delattr(request, 'user')
assert not waffle.flag_is_active(request, 'foo')


class SwitchTests(TestCase):
def test_switch_active(self):
switch = Switch.objects.create(name='myswitch', active=True)
Expand Down