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:])