From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- .../featuregates/gen_feature_definitions.py | 216 +++++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100755 toolkit/components/featuregates/gen_feature_definitions.py (limited to 'toolkit/components/featuregates/gen_feature_definitions.py') diff --git a/toolkit/components/featuregates/gen_feature_definitions.py b/toolkit/components/featuregates/gen_feature_definitions.py new file mode 100755 index 0000000000..da4a9fe177 --- /dev/null +++ b/toolkit/components/featuregates/gen_feature_definitions.py @@ -0,0 +1,216 @@ +#!/usr/bin/env python +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +import json +import re +import sys +import toml +import voluptuous +import voluptuous.humanize +from voluptuous import Schema, Optional, Any, All, Required, Length, Range, Msg, Match + + +Text = Any(str, bytes) + + +id_regex = re.compile(r"^[a-z0-9-]+$") +feature_schema = Schema( + { + Match(id_regex): { + Required("title"): All(Text, Length(min=1)), + Required("description"): All(Text, Length(min=1)), + Required("bug-numbers"): All(Length(min=1), [All(int, Range(min=1))]), + Required("restart-required"): bool, + Required("type"): "boolean", # In the future this may include other types + Optional("preference"): Text, + Optional("default-value"): Any( + bool, dict + ), # the types of the keys here should match the value of `type` + Optional("is-public"): Any(bool, dict), + Optional("description-links"): dict, + }, + } +) + + +EXIT_OK = 0 +EXIT_ERROR = 1 + + +def main(output, *filenames): + features = {} + errors = False + try: + features = process_files(filenames) + json.dump(features, output, sort_keys=True) + except ExceptionGroup as error_group: + print(str(error_group)) + return EXIT_ERROR + return EXIT_OK + + +class ExceptionGroup(Exception): + def __init__(self, errors): + self.errors = errors + + def __str__(self): + rv = ["There were errors while processing feature definitions:"] + for error in self.errors: + # indent the message + s = "\n".join(" " + line for line in str(error).split("\n")) + # add a * at the beginning of the first line + s = " * " + s[4:] + rv.append(s) + return "\n".join(rv) + + +class FeatureGateException(Exception): + def __init__(self, message, filename=None): + super(FeatureGateException, self).__init__(message) + self.filename = filename + + def __str__(self): + message = super(FeatureGateException, self).__str__() + rv = ["In"] + if self.filename is None: + rv.append("unknown file:") + else: + rv.append('file "{}":\n'.format(self.filename)) + rv.append(message) + return " ".join(rv) + + def __repr__(self): + # Turn "FeatureGateExcept(,)" into "FeatureGateException(, filename=)" + original = super(FeatureGateException, self).__repr__() + with_comma = original[:-1] + # python 2 adds a trailing comma and python 3 does not, so we need to conditionally reinclude it + if len(with_comma) > 0 and with_comma[-1] != ",": + with_comma = with_comma + "," + return with_comma + " filename={!r})".format(self.filename) + + +def process_files(filenames): + features = {} + errors = [] + + for filename in filenames: + try: + with open(filename, "r") as f: + feature_data = toml.load(f) + + voluptuous.humanize.validate_with_humanized_errors( + feature_data, feature_schema + ) + + for feature_id, feature in feature_data.items(): + feature["id"] = feature_id + features[feature_id] = expand_feature(feature) + except ( + voluptuous.error.Error, + IOError, + FeatureGateException, + toml.TomlDecodeError, + ) as e: + # Wrap errors in enough information to know which file they came from + errors.append(FeatureGateException(e, filename)) + + if errors: + raise ExceptionGroup(errors) + + return features + + +def hyphens_to_camel_case(s): + """Convert names-with-hyphens to namesInCamelCase""" + rv = "" + for part in s.split("-"): + if rv == "": + rv = part.lower() + else: + rv += part[0].upper() + part[1:].lower() + return rv + + +def expand_feature(feature): + """Fill in default values for optional fields""" + + # convert all names-with-hyphens to namesInCamelCase + key_changes = [] + for key in feature.keys(): + if "-" in key: + new_key = hyphens_to_camel_case(key) + key_changes.append((key, new_key)) + + for old_key, new_key in key_changes: + feature[new_key] = feature[old_key] + del feature[old_key] + + if feature["type"] == "boolean": + feature.setdefault("preference", "features.{}.enabled".format(feature["id"])) + # set default value to None so that we can test for perferences where we forgot to set the default value + feature.setdefault("defaultValue", None) + elif "preference" not in feature: + raise FeatureGateException( + "Features of type {} must specify an explicit preference name".format( + feature["type"] + ) + ) + + feature.setdefault("isPublic", False) + + try: + for key in ["defaultValue", "isPublic"]: + feature[key] = process_configured_value(key, feature[key]) + except FeatureGateException as e: + raise FeatureGateException( + "Error when processing feature {}: {}".format(feature["id"], e) + ) + + return feature + + +def process_configured_value(name, value): + if not isinstance(value, dict): + return {"default": value} + + if "default" not in value: + raise FeatureGateException( + "Config for {} has no default: {}".format(name, value) + ) + + expected_keys = set( + { + "default", + "win", + "mac", + "linux", + "android", + "nightly", + "early_beta_or_earlier", + "beta", + "release", + "dev-edition", + "esr", + "thunderbird", + } + ) + + for key in value.keys(): + parts = [p.strip() for p in key.split(",")] + for part in parts: + if part not in expected_keys: + raise FeatureGateException( + "Unexpected target {}, expected any of {}".format( + part, expected_keys + ) + ) + + # TODO Compute values at build time, so that it always returns only a single value. + + return value + + +if __name__ == "__main__": + sys.exit(main(sys.stdout, *sys.argv[1:])) -- cgit v1.2.3