diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /toolkit/components/featuregates/test/python | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/featuregates/test/python')
5 files changed, 328 insertions, 0 deletions
diff --git a/toolkit/components/featuregates/test/python/data/empty_feature.toml b/toolkit/components/featuregates/test/python/data/empty_feature.toml new file mode 100644 index 0000000000..cd89859d1f --- /dev/null +++ b/toolkit/components/featuregates/test/python/data/empty_feature.toml @@ -0,0 +1 @@ +[empty-feature] diff --git a/toolkit/components/featuregates/test/python/data/good.toml b/toolkit/components/featuregates/test/python/data/good.toml new file mode 100644 index 0000000000..1fbdc3298d --- /dev/null +++ b/toolkit/components/featuregates/test/python/data/good.toml @@ -0,0 +1,16 @@ +[demo-feature] +title = "Demo Feature" +description = "A no-op feature to demo the feature gate system." +restart-required = false +preference = "foo.bar.baz" +type = "boolean" +bug-numbers = [1479127] +is-public = true +default-value = false + +[minimal-feature] +title = "Minimal Feature" +description = "The smallest feature that is valid" +restart-required = true +type = "boolean" +bug-numbers = [1479127] diff --git a/toolkit/components/featuregates/test/python/data/invalid_toml.toml b/toolkit/components/featuregates/test/python/data/invalid_toml.toml new file mode 100644 index 0000000000..d4a8001e58 --- /dev/null +++ b/toolkit/components/featuregates/test/python/data/invalid_toml.toml @@ -0,0 +1 @@ +this: is: not: valid: toml diff --git a/toolkit/components/featuregates/test/python/python.toml b/toolkit/components/featuregates/test/python/python.toml new file mode 100644 index 0000000000..34a3aff0e9 --- /dev/null +++ b/toolkit/components/featuregates/test/python/python.toml @@ -0,0 +1,4 @@ +[DEFAULT] +subsuite = "featuregates" + +["test_gen_feature_definitions.py"] 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:]) |