Skip to content

Commit

Permalink
Add central interface for defining feature flags
Browse files Browse the repository at this point in the history
The intended use for colcon feature flags is to ship pre-production and
prototype features in a disabled state, which can be enabled by
specifying a particular environment variable value. By using an
environment variable, these possibly dangerous or unstable features are
hidden from common users but are enabled in a way which can be audited.
  • Loading branch information
cottsay committed Apr 17, 2024
1 parent 15ed7d6 commit aaacfc0
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 0 deletions.
45 changes: 45 additions & 0 deletions colcon_core/feature_flags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Copyright 2024 Open Source Robotics Foundation, Inc.
# Licensed under the Apache License, Version 2.0

import os
import re

from colcon_core.environment_variable import EnvironmentVariable


"""Environment variable to enable feature flags"""
FEATURE_FLAGS_ENVIRONMENT_VARIABLE = EnvironmentVariable(
'COLCON_FEATURE_FLAGS',
'Enable pre-production features and behaviors')


def get_feature_flags():
"""
Retrieve all enabled feature flags.
:returns: List of enabled flags
:rtype: list
"""
return [
flag for flag in (
os.environ.get(FEATURE_FLAGS_ENVIRONMENT_VARIABLE.name) or ''
).split(os.pathsep) if flag
]


def is_feature_flag_set(flag):
"""
Determine if a specific feature flag is enabled.
Feature flags are case-sensitive and separated by the os-specific path
separator character.
:param str flag: Name of the flag to search for
:returns: True if the flag is set
:rtype: bool
"""
return bool(flag and re.search(
fr'(?:^|{os.pathsep}){flag}(?:{os.pathsep}|$)',
os.environ.get(FEATURE_FLAGS_ENVIRONMENT_VARIABLE.name) or '',
))
4 changes: 4 additions & 0 deletions test/spell_check.words
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
addfinalizer
addopts
apache
argparse
Expand Down Expand Up @@ -35,8 +36,10 @@ docstring
executables
exitstatus
fdopen
ffoo
filterwarnings
foobar
fooo
fromhex
functools
getcategory
Expand Down Expand Up @@ -139,5 +142,6 @@ unittest
unittests
unlinking
unrenamed
usefixtures
wildcards
workaround
71 changes: 71 additions & 0 deletions test/test_feature_flags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Copyright 2024 Open Source Robotics Foundation, Inc.
# Licensed under the Apache License, Version 2.0

import os
from unittest.mock import patch

from colcon_core.feature_flags import FEATURE_FLAGS_ENVIRONMENT_VARIABLE
from colcon_core.feature_flags import get_feature_flags
from colcon_core.feature_flags import is_feature_flag_set
import pytest


_FLAGS_TO_TEST = (
('foo',),
('foo', 'foo'),
('foo', ''),
('', 'foo'),
('', 'foo', ''),
('foo', 'bar'),
('bar', 'foo'),
('bar', 'foo', 'baz'),
)


@pytest.fixture
def feature_flags_value(request):
env = dict(os.environ)
if request.param is not None:
env[FEATURE_FLAGS_ENVIRONMENT_VARIABLE.name] = os.pathsep.join(
request.param)
else:
env.pop(FEATURE_FLAGS_ENVIRONMENT_VARIABLE.name, None)

mock_env = patch('colcon_core.feature_flags.os.environ', env)
request.addfinalizer(mock_env.stop)
mock_env.start()
return request.param


@pytest.mark.parametrize(
'feature_flags_value',
_FLAGS_TO_TEST,
indirect=('feature_flags_value',))
@pytest.mark.usefixtures('feature_flags_value')
def test_flag_is_set():
assert is_feature_flag_set('foo')


@pytest.mark.parametrize(
'feature_flags_value',
(None, *_FLAGS_TO_TEST),
indirect=('feature_flags_value',))
@pytest.mark.usefixtures('feature_flags_value')
def test_flag_not_set():
assert not is_feature_flag_set('')
assert not is_feature_flag_set('fo')
assert not is_feature_flag_set('oo')
assert not is_feature_flag_set('fooo')
assert not is_feature_flag_set('ffoo')
assert not is_feature_flag_set('qux')


@pytest.mark.parametrize(
'feature_flags_value',
(None, *_FLAGS_TO_TEST),
indirect=('feature_flags_value',))
@pytest.mark.usefixtures('feature_flags_value')
def test_get_flags(feature_flags_value):
assert [
flag for flag in (feature_flags_value or ()) if flag
] == get_feature_flags()

0 comments on commit aaacfc0

Please sign in to comment.