summaryrefslogtreecommitdiffstats
path: root/toolkit/components/featuregates/test/python/test_gen_feature_definitions.py
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/featuregates/test/python/test_gen_feature_definitions.py')
-rw-r--r--toolkit/components/featuregates/test/python/test_gen_feature_definitions.py306
1 files changed, 306 insertions, 0 deletions
diff --git a/toolkit/components/featuregates/test/python/test_gen_feature_definitions.py b/toolkit/components/featuregates/test/python/test_gen_feature_definitions.py
new file mode 100644
index 0000000000..b6a3e40789
--- /dev/null
+++ b/toolkit/components/featuregates/test/python/test_gen_feature_definitions.py
@@ -0,0 +1,306 @@
+import json
+import sys
+import unittest
+from os import path
+from textwrap import dedent
+
+import mozunit
+import toml
+import voluptuous
+from io import StringIO
+
+
+FEATURE_GATES_ROOT_PATH = path.abspath(
+ path.join(path.dirname(__file__), path.pardir, path.pardir)
+)
+sys.path.append(FEATURE_GATES_ROOT_PATH)
+from gen_feature_definitions import (
+ ExceptionGroup,
+ expand_feature,
+ feature_schema,
+ FeatureGateException,
+ hyphens_to_camel_case,
+ main,
+ process_configured_value,
+ process_files,
+)
+
+
+def make_test_file_path(name):
+ return path.join(FEATURE_GATES_ROOT_PATH, "test", "python", "data", name + ".toml")
+
+
+def minimal_definition(**kwargs):
+ defaults = {
+ "id": "test-feature",
+ "title": "Test Feature",
+ "description": "A feature for testing things",
+ "bug-numbers": [1479127],
+ "restart-required": False,
+ "type": "boolean",
+ }
+ defaults.update(dict([(k.replace("_", "-"), v) for k, v in kwargs.items()]))
+ return defaults
+
+
+class TestHyphensToCamelCase(unittest.TestCase):
+ simple_cases = [
+ ("", ""),
+ ("singleword", "singleword"),
+ ("more-than-one-word", "moreThanOneWord"),
+ ]
+
+ def test_simple_cases(self):
+ for in_string, out_string in self.simple_cases:
+ assert hyphens_to_camel_case(in_string) == out_string
+
+
+class TestExceptionGroup(unittest.TestCase):
+ def test_str_indentation_of_grouped_lines(self):
+ errors = [
+ Exception("single line error 1"),
+ Exception("single line error 2"),
+ Exception("multiline\nerror 1"),
+ Exception("multiline\nerror 2"),
+ ]
+
+ assert str(ExceptionGroup(errors)) == dedent(
+ """\
+ There were errors while processing feature definitions:
+ * single line error 1
+ * single line error 2
+ * multiline
+ error 1
+ * multiline
+ error 2"""
+ )
+
+
+class TestFeatureGateException(unittest.TestCase):
+ def test_str_no_file(self):
+ error = FeatureGateException("oops")
+ assert str(error) == "In unknown file: oops"
+
+ def test_str_with_file(self):
+ error = FeatureGateException("oops", filename="some/bad/file.txt")
+ assert str(error) == 'In file "some/bad/file.txt":\n oops'
+
+ def test_repr_no_file(self):
+ error = FeatureGateException("oops")
+ assert repr(error) == "FeatureGateException('oops', filename=None)"
+
+ def test_repr_with_file(self):
+ error = FeatureGateException("oops", filename="some/bad/file.txt")
+ assert (
+ repr(error) == "FeatureGateException('oops', filename='some/bad/file.txt')"
+ )
+
+
+class TestProcessFiles(unittest.TestCase):
+ def test_valid_file(self):
+ filename = make_test_file_path("good")
+ result = process_files([filename])
+ assert result == {
+ "demo-feature": {
+ "id": "demo-feature",
+ "title": "Demo Feature",
+ "description": "A no-op feature to demo the feature gate system.",
+ "restartRequired": False,
+ "preference": "foo.bar.baz",
+ "type": "boolean",
+ "bugNumbers": [1479127],
+ "isPublic": {"default": True},
+ "defaultValue": {"default": False},
+ },
+ "minimal-feature": {
+ "id": "minimal-feature",
+ "title": "Minimal Feature",
+ "description": "The smallest feature that is valid",
+ "restartRequired": True,
+ "preference": "features.minimal-feature.enabled",
+ "type": "boolean",
+ "bugNumbers": [1479127],
+ "isPublic": {"default": False},
+ "defaultValue": {"default": None},
+ },
+ }
+
+ def test_invalid_toml(self):
+ filename = make_test_file_path("invalid_toml")
+ with self.assertRaises(ExceptionGroup) as context:
+ process_files([filename])
+ error_group = context.exception
+ assert len(error_group.errors) == 1
+ assert type(error_group.errors[0]) == FeatureGateException
+
+ def test_empty_feature(self):
+ filename = make_test_file_path("empty_feature")
+ with self.assertRaises(ExceptionGroup) as context:
+ process_files([filename])
+ error_group = context.exception
+ assert len(error_group.errors) == 1
+ assert type(error_group.errors[0]) == FeatureGateException
+ assert "required key not provided" in str(error_group.errors[0])
+
+ def test_missing_file(self):
+ filename = make_test_file_path("file_does_not_exist")
+ with self.assertRaises(ExceptionGroup) as context:
+ process_files([filename])
+ error_group = context.exception
+ assert len(error_group.errors) == 1
+ assert type(error_group.errors[0]) == FeatureGateException
+ assert "No such file or directory" in str(error_group.errors[0])
+
+
+class TestFeatureSchema(unittest.TestCase):
+ def make_test_features(self, *overrides):
+ if len(overrides) == 0:
+ overrides = [{}]
+ features = {}
+ for override in overrides:
+ feature = minimal_definition(**override)
+ feature_id = feature.pop("id")
+ features[feature_id] = feature
+ return features
+
+ def test_minimal_valid(self):
+ definition = self.make_test_features()
+ # should not raise an exception
+ feature_schema(definition)
+
+ def test_extra_keys_not_allowed(self):
+ definition = self.make_test_features({"unexpected_key": "oh no!"})
+ with self.assertRaises(voluptuous.Error) as context:
+ feature_schema(definition)
+ assert "extra keys not allowed" in str(context.exception)
+
+ def test_required_fields(self):
+ required_keys = [
+ "title",
+ "description",
+ "bug-numbers",
+ "restart-required",
+ "type",
+ ]
+ for key in required_keys:
+ definition = self.make_test_features({"id": "test-feature"})
+ del definition["test-feature"][key]
+ with self.assertRaises(voluptuous.Error) as context:
+ feature_schema(definition)
+ assert "required key not provided" in str(context.exception)
+ assert key in str(context.exception)
+
+ def test_nonempty_keys(self):
+ test_parameters = [("title", ""), ("description", ""), ("bug-numbers", [])]
+ for key, empty in test_parameters:
+ definition = self.make_test_features({key: empty})
+ with self.assertRaises(voluptuous.Error) as context:
+ feature_schema(definition)
+ assert "length of value must be at least" in str(context.exception)
+ assert "['{}']".format(key) in str(context.exception)
+
+
+class ExpandFeatureTests(unittest.TestCase):
+ def test_hyphenation_to_snake_case(self):
+ feature = minimal_definition()
+ assert "bug-numbers" in feature
+ assert "bugNumbers" in expand_feature(feature)
+
+ def test_default_value_default(self):
+ feature = minimal_definition(type="boolean")
+ assert "default-value" not in feature
+ assert "defaultValue" not in feature
+ assert expand_feature(feature)["defaultValue"] == {"default": None}
+
+ def test_default_value_override_constant(self):
+ feature = minimal_definition(type="boolean", default_value=True)
+ assert expand_feature(feature)["defaultValue"] == {"default": True}
+
+ def test_default_value_override_configured_value(self):
+ feature = minimal_definition(
+ type="boolean", default_value={"default": False, "nightly": True}
+ )
+ assert expand_feature(feature)["defaultValue"] == {
+ "default": False,
+ "nightly": True,
+ }
+
+ def test_preference_default(self):
+ feature = minimal_definition(type="boolean")
+ assert "preference" not in feature
+ assert expand_feature(feature)["preference"] == "features.test-feature.enabled"
+
+ def test_preference_override(self):
+ feature = minimal_definition(preference="test.feature.a")
+ assert expand_feature(feature)["preference"] == "test.feature.a"
+
+
+class ProcessConfiguredValueTests(unittest.TestCase):
+ def test_expands_single_values(self):
+ for value in [True, False, 2, "features"]:
+ assert process_configured_value("test", value) == {"default": value}
+
+ def test_default_key_is_required(self):
+ with self.assertRaises(FeatureGateException) as context:
+ assert process_configured_value("test", {"nightly": True})
+ assert "has no default" in str(context.exception)
+
+ def test_invalid_keys_rejected(self):
+ with self.assertRaises(FeatureGateException) as context:
+ assert process_configured_value("test", {"default": True, "bogus": True})
+ assert "Unexpected target bogus" in str(context.exception)
+
+ def test_simple_key(self):
+ value = {"nightly": True, "default": False}
+ assert process_configured_value("test", value) == value
+
+ def test_compound_keys(self):
+ value = {"win,nightly": True, "default": False}
+ assert process_configured_value("test", value) == value
+
+ def test_multiple_keys(self):
+ value = {"win": True, "mac": True, "default": False}
+ assert process_configured_value("test", value) == value
+
+
+class MainTests(unittest.TestCase):
+ def test_it_outputs_json(self):
+ output = StringIO()
+ filename = make_test_file_path("good")
+ main(output, filename)
+ output.seek(0)
+ results = json.load(output)
+ assert results == {
+ "demo-feature": {
+ "id": "demo-feature",
+ "title": "Demo Feature",
+ "description": "A no-op feature to demo the feature gate system.",
+ "restartRequired": False,
+ "preference": "foo.bar.baz",
+ "type": "boolean",
+ "bugNumbers": [1479127],
+ "isPublic": {"default": True},
+ "defaultValue": {"default": False},
+ },
+ "minimal-feature": {
+ "id": "minimal-feature",
+ "title": "Minimal Feature",
+ "description": "The smallest feature that is valid",
+ "restartRequired": True,
+ "preference": "features.minimal-feature.enabled",
+ "type": "boolean",
+ "bugNumbers": [1479127],
+ "isPublic": {"default": False},
+ "defaultValue": {"default": None},
+ },
+ }
+
+ def test_it_returns_1_for_errors(self):
+ output = StringIO()
+ filename = make_test_file_path("invalid_toml")
+ assert main(output, filename) == 1
+ assert output.getvalue() == ""
+
+
+if __name__ == "__main__":
+ mozunit.main(*sys.argv[1:])